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

mozilla / blurts-server / #13221

pending completion
#13221

push

circleci

mansaj
fxa-rp-events handler

282 of 1673 branches covered (16.86%)

Branch coverage included in aggregate %.

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

959 of 4533 relevant lines covered (21.16%)

1.76 hits per line

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

0.0
/src/controllers/fxa-rp-events.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
const log = mozlog('controllers.fxa-rp-events')
×
12

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

17
function authenticateFxaJWT (req) {
18
  // Assuming this is how you retrieve your auth header.
19
  const authHeader = req?.headers?.authorization
×
20

21
  // Require an auth header
22
  if (!authHeader) {
×
23
    throw UnauthorizedError('No auth header found')
×
24
  }
25

26
  // Extract the first portion which should be 'Bearer'
27
  const headerType = authHeader.substr(0, authHeader.indexOf(' '))
×
28
  if (headerType !== 'Bearer') {
×
29
    throw UnauthorizedError('Invalid auth type')
×
30
  }
31

32
  // The remaining portion, which should be the token
33
  const headerToken = authHeader.substr(authHeader.indexOf(' ') + 1)
×
34

35
  // Decode the token, require it to come out ok as an object
36
  const token = jwt.decode(headerToken, { complete: true })
×
37
  if (!token || typeof token === 'string') {
×
38
    throw UnauthorizedError('Invalid token type')
×
39
  }
40

41
  // Verify we have a key for this kid, this assumes that you have fetched
42
  // the publicJwks from FxA and put both them in an Array.
43
  const jwk = publicJwks.find(j => j.kid === token.header.kid)
×
44
  if (!jwk) {
×
45
    throw UnauthorizedError('No jwk found for this kid: ' + token.header.kid)
×
46
  }
47
  const jwkPem = jwkToPem(jwk)
×
48

49
  // Verify the token is valid
50
  const decoded = jwt.verify(headerToken, jwkPem, {
×
51
    algorithms: ['RS256']
52
  })
53
  // if (!isIdToken(decoded)) {
54
  //   throw UnauthorizedError('Invalid token format: ' + decoded);
55
  // }
56
  // This is the JWT data itself.
57
  return decoded
×
58
}
59

60
/**
61
 * Handler for FxA events, used by FxA as a callback URI endpoint
62
 * Example events include FxA user deletion, profile changes, and subscription changes
63
 *
64
 * @param {*} req
65
 * @param {*} res
66
 * @returns
67
 */
68
const fxaRpEvents = async (req, res) => {
×
69
  const decodedJWT = authenticateFxaJWT(req)
×
70
  if (!decodedJWT?.events) {
×
71
    // capture an exception in Sentry only. Throwing error will trigger FXA retry
72
    log.error('fxaRpEvents', decodedJWT)
×
73
    captureException(new Error('fxaRpEvents: decodedJWT is missing attribute "events"', decodedJWT))
×
74
    res.status(202)
×
75
  }
76

77
  const fxaUserId = decodedJWT?.sub
×
78
  if (!fxaUserId) {
×
79
    // capture an exception in Sentry only. Throwing error will trigger FXA retry
80
    captureException(new Error('fxaRpEvents: decodedJWT is missing attribute "sub"', decodedJWT))
×
81
    res.status(202)
×
82
  }
83

84
  const subscriber = getSubscriberByFxaUid(fxaUserId)
×
85

86
  // reference example events: https://github.com/mozilla/fxa/blob/main/packages/fxa-event-broker/README.md
87
  for (const event in decodedJWT?.events) {
×
88
    switch (event) {
×
89
      case FXA_DELETE_USER_EVENT:
90
        log.debug('fxa_delete_user', {
×
91
          event
92
        })
93

94
        // delete user events only have keys. Keys point to empty objects
95
        await deleteSubscriberByFxAUID(fxaUserId)
×
96
        break
×
97
      case FXA_PROFILE_CHANGE_EVENT: {
98
        const updatedProfileFromEvent = decodedJWT.events[event]
×
99
        log.debug('fxa_profile_update', {
×
100
          event,
101
          updatedProfileFromEvent
102
        })
103

104
        // get current profiledata
105
        const { currentFxAProfile: fxa_profile_json } = subscriber
×
106

107
        // merge new event into existing profile data
108
        for (const key in updatedProfileFromEvent) {
×
109
          if (currentFxAProfile[key]) currentFxAProfile[key] = updatedProfileFromEvent[key]
×
110
        }
111

112
        // update fxa profile data
113
        await updateFxAProfileData(subscriber, currentFxAProfile)
×
114
        break
×
115
      }
116
      case FXA_SUBSCRIPTION_CHANGE_EVENT:
117
        // TODO: to be implemented after subplat
118
        break
×
119
      default:
120
        log.warn('unhandled_event', {
×
121
          event
122
        })
123
        break
×
124
    }
125
  }
126

127
  res.status(200)
×
128
}
129

130
export {
131
  fxaRpEvents
132
}
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