• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / blurts-server / #13253

pending completion
#13253

push

circleci

mansaj
name changes, fetch pubkey func

282 of 1683 branches covered (16.76%)

Branch coverage included in aggregate %.

11 of 11 new or added lines in 1 file covered. (100.0%)

959 of 4573 relevant lines covered (20.97%)

1.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/controllers/fxaRpEvents.js
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
 * License, v. 2.0. If a copy of the MPL was not distributed with this
3
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4

5
import * as jwt from 'jsonwebtoken'
6
import jwkToPem from 'jwk-to-pem'
7
import { captureException } from '@sentry/node'
8
import { UnauthorizedError } from '../utils/error.js'
9
import { deleteSubscriberByFxAUID, getSubscriberByFxaUid, updateFxAProfileData } from '../db/tables/subscribers.js'
10
import mozlog from '../utils/log.js'
11
import appConstants from '../appConstants.js'
12
const log = mozlog('controllers.fxa-rp-events')
×
13

14
const FXA_PROFILE_CHANGE_EVENT = 'https://schemas.accounts.firefox.com/event/profile-change'
×
15
const FXA_SUBSCRIPTION_CHANGE_EVENT = 'https://schemas.accounts.firefox.com/event/subscription-state-change'
×
16
const FXA_DELETE_USER_EVENT = 'https://schemas.accounts.firefox.com/event/delete-user'
×
17

18
const getJwtPubKey = async () => {
×
19
  jwtKeyUri = `${appConstants.OAUTH_ACCOUNT_URI}/jwt`
×
20
  try {
×
21
    const res = await fetch(jwtKeyUri, {
×
22
      headers: {
23
        'Content-Type': 'application/json'
24
      }
25
    })
26
    const { keys } = res
×
27
    log.info('getJwtPubKey', `fetched jwt public keys from: ${jwtKeyUri} - ${keys}`)
×
28
    return keys
×
29
  } catch (e) {
30
    captureException('Could not get JWT public key', jwtKeyUri)
×
31
  }
32
}
33

34
const authenticateFxaJWT = async (req) => {
×
35
  // Assuming this is how you retrieve your auth header.
36
  const authHeader = req?.headers?.authorization
×
37

38
  // Require an auth header
39
  if (!authHeader) {
×
40
    captureException('No auth header found', req?.headers)
×
41
    throw UnauthorizedError('No auth header found')
×
42
  }
43

44
  // Extract the first portion which should be 'Bearer'
45
  const headerType = authHeader.substr(0, authHeader.indexOf(' '))
×
46
  if (headerType !== 'Bearer') {
×
47
    throw UnauthorizedError('Invalid auth type')
×
48
  }
49

50
  // The remaining portion, which should be the token
51
  const headerToken = authHeader.substr(authHeader.indexOf(' ') + 1)
×
52

53
  // Decode the token, require it to come out ok as an object
54
  const token = jwt.decode(headerToken, { complete: true })
×
55
  if (!token || typeof token === 'string') {
×
56
    throw UnauthorizedError('Invalid token type')
×
57
  }
58

59
  // Verify we have a key for this kid, this assumes that you have fetched
60
  // the publicJwks from FxA and put both them in an Array.
61
  const publicJwks = await getJwtPubKey()
×
62
  const jwk = publicJwks.find(j => j.kid === token.header.kid)
×
63
  if (!jwk) {
×
64
    throw UnauthorizedError('No jwk found for this kid: ' + token.header.kid)
×
65
  }
66
  const jwkPem = jwkToPem(jwk)
×
67

68
  // Verify the token is valid
69
  const decoded = jwt.verify(headerToken, jwkPem, {
×
70
    algorithms: ['RS256']
71
  })
72
  // if (!isIdToken(decoded)) {
73
  //   throw UnauthorizedError('Invalid token format: ' + decoded);
74
  // }
75
  // This is the JWT data itself.
76
  return decoded
×
77
}
78

79
/**
80
 * Handler for FxA events, used by FxA as a callback URI endpoint
81
 * Example events include FxA user deletion, profile changes, and subscription changes
82
 *
83
 * @param {*} req
84
 * @param {*} res
85
 * @returns
86
 */
87
const fxaRpEvents = async (req, res) => {
×
88
  const decodedJWT = authenticateFxaJWT(req)
×
89
  if (!decodedJWT?.events) {
×
90
    // capture an exception in Sentry only. Throwing error will trigger FXA retry
91
    log.error('fxaRpEvents', decodedJWT)
×
92
    captureException(new Error('fxaRpEvents: decodedJWT is missing attribute "events"', decodedJWT))
×
93
    res.status(202)
×
94
  }
95

96
  const fxaUserId = decodedJWT?.sub
×
97
  if (!fxaUserId) {
×
98
    // capture an exception in Sentry only. Throwing error will trigger FXA retry
99
    captureException(new Error('fxaRpEvents: decodedJWT is missing attribute "sub"', decodedJWT))
×
100
    res.status(202)
×
101
  }
102

103
  const subscriber = getSubscriberByFxaUid(fxaUserId)
×
104

105
  // reference example events: https://github.com/mozilla/fxa/blob/main/packages/fxa-event-broker/README.md
106
  for (const event in decodedJWT?.events) {
×
107
    switch (event) {
×
108
      case FXA_DELETE_USER_EVENT:
109
        log.debug('fxa_delete_user', {
×
110
          event
111
        })
112

113
        // delete user events only have keys. Keys point to empty objects
114
        await deleteSubscriberByFxAUID(fxaUserId)
×
115
        break
×
116
      case FXA_PROFILE_CHANGE_EVENT: {
117
        const updatedProfileFromEvent = decodedJWT.events[event]
×
118
        log.debug('fxa_profile_update', {
×
119
          event,
120
          updatedProfileFromEvent
121
        })
122

123
        // get current profiledata
124
        const { currentFxAProfile: fxa_profile_json } = subscriber
×
125

126
        // merge new event into existing profile data
127
        for (const key in updatedProfileFromEvent) {
×
128
          if (currentFxAProfile[key]) currentFxAProfile[key] = updatedProfileFromEvent[key]
×
129
        }
130

131
        // update fxa profile data
132
        await updateFxAProfileData(subscriber, currentFxAProfile)
×
133
        break
×
134
      }
135
      case FXA_SUBSCRIPTION_CHANGE_EVENT:
136
        // TODO: to be implemented after subplat
137
        break
×
138
      default:
139
        log.warn('unhandled_event', {
×
140
          event
141
        })
142
        break
×
143
    }
144
  }
145

146
  res.status(200)
×
147
}
148

149
export {
150
  fxaRpEvents
151
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc