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

mozilla / blurts-server / #13256

pending completion
#13256

push

circleci

mansaj
assign but

282 of 1683 branches covered (16.76%)

Branch coverage included in aggregate %.

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

959 of 4577 relevant lines covered (20.95%)

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
/**
19
 * Fetch FxA JWT Public for verification
20
 *
21
 * @returns {Array} keys an array of FxA JWT keys
22
 */
23
const getJwtPubKey = async () => {
×
24
  jwtKeyUri = `${appConstants.OAUTH_ACCOUNT_URI}/jwt`
×
25
  try {
×
26
    const res = await fetch(jwtKeyUri, {
×
27
      headers: {
28
        'Content-Type': 'application/json'
29
      }
30
    })
31
    const { keys } = res
×
32
    log.info('getJwtPubKey', `fetched jwt public keys from: ${jwtKeyUri} - ${keys}`)
×
33
    return keys
×
34
  } catch (e) {
35
    captureException('Could not get JWT public key', jwtKeyUri)
×
36
  }
37
}
38

39
/**
40
 * Authenticate FxA JWT for FxA relay event requests
41
 *
42
 * @param {*} req
43
 * @returns {object} decoded JWT data, which should contain FxA events
44
 */
45
const authenticateFxaJWT = async (req) => {
×
46
  // Assuming this is how you retrieve your auth header.
47
  const authHeader = req?.headers?.authorization
×
48

49
  // Require an auth header
50
  if (!authHeader) {
×
51
    captureException('No auth header found', req?.headers)
×
52
    throw UnauthorizedError('No auth header found')
×
53
  }
54

55
  // Extract the first portion which should be 'Bearer'
56
  const headerType = authHeader.substr(0, authHeader.indexOf(' '))
×
57
  if (headerType !== 'Bearer') {
×
58
    throw UnauthorizedError('Invalid auth type')
×
59
  }
60

61
  // The remaining portion, which should be the token
62
  const headerToken = authHeader.substr(authHeader.indexOf(' ') + 1)
×
63

64
  // Decode the token, require it to come out ok as an object
65
  const token = jwt.decode(headerToken, { complete: true })
×
66
  if (!token || typeof token === 'string') {
×
67
    throw UnauthorizedError('Invalid token type')
×
68
  }
69

70
  // Verify we have a key for this kid, this assumes that you have fetched
71
  // the publicJwks from FxA and put both them in an Array.
72
  const publicJwks = await getJwtPubKey()
×
73
  const jwk = publicJwks.find(j => j.kid === token.header.kid)
×
74
  if (!jwk) {
×
75
    throw UnauthorizedError('No jwk found for this kid: ' + token.header.kid)
×
76
  }
77
  const jwkPem = jwkToPem(jwk)
×
78

79
  // Verify the token is valid
80
  const decoded = jwt.verify(headerToken, jwkPem, {
×
81
    algorithms: ['RS256']
82
  })
83
  // This is the JWT data itself.
84
  return decoded
×
85
}
86

87
/**
88
 * Handler for FxA events, used by FxA as a callback URI endpoint
89
 * Example events include FxA user deletion, profile changes, and subscription changes
90
 *
91
 * @param {*} req
92
 * @param {*} res
93
 * @returns
94
 */
95
const fxaRpEvents = async (req, res) => {
×
96
  let decodedJWT
97
  try {
×
98
    decodedJWT = await authenticateFxaJWT(req)
×
99
  } catch (e) {
100
    log.error('fxaRpEvents', e)
×
101
    captureException(e)
×
102
    res.status(202)
×
103
  }
104

105
  if (!decodedJWT?.events) {
×
106
    // capture an exception in Sentry only. Throwing error will trigger FXA retry
107
    log.error('fxaRpEvents', decodedJWT)
×
108
    captureException(new Error('fxaRpEvents: decodedJWT is missing attribute "events"', decodedJWT))
×
109
    res.status(202)
×
110
  }
111

112
  const fxaUserId = decodedJWT?.sub
×
113
  if (!fxaUserId) {
×
114
    // capture an exception in Sentry only. Throwing error will trigger FXA retry
115
    captureException(new Error('fxaRpEvents: decodedJWT is missing attribute "sub"', decodedJWT))
×
116
    res.status(202)
×
117
  }
118

119
  const subscriber = getSubscriberByFxaUid(fxaUserId)
×
120

121
  // reference example events: https://github.com/mozilla/fxa/blob/main/packages/fxa-event-broker/README.md
122
  for (const event in decodedJWT?.events) {
×
123
    switch (event) {
×
124
      case FXA_DELETE_USER_EVENT:
125
        log.debug('fxa_delete_user', {
×
126
          event
127
        })
128

129
        // delete user events only have keys. Keys point to empty objects
130
        await deleteSubscriberByFxAUID(fxaUserId)
×
131
        break
×
132
      case FXA_PROFILE_CHANGE_EVENT: {
133
        const updatedProfileFromEvent = decodedJWT.events[event]
×
134
        log.debug('fxa_profile_update', {
×
135
          event,
136
          updatedProfileFromEvent
137
        })
138

139
        // get current profiledata
140
        const { fxa_profile_json: currentFxAProfile} = subscriber
×
141

142
        // merge new event into existing profile data
143
        for (const key in updatedProfileFromEvent) {
×
144
          if (currentFxAProfile[key]) currentFxAProfile[key] = updatedProfileFromEvent[key]
×
145
        }
146

147
        // update fxa profile data
148
        await updateFxAProfileData(subscriber, currentFxAProfile)
×
149
        break
×
150
      }
151
      case FXA_SUBSCRIPTION_CHANGE_EVENT:
152
        // TODO: to be implemented after subplat
153
        break
×
154
      default:
155
        log.warn('unhandled_event', {
×
156
          event
157
        })
158
        break
×
159
    }
160
  }
161

162
  res.status(200)
×
163
}
164

165
export {
166
  fxaRpEvents
167
}
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