-
Notifications
You must be signed in to change notification settings - Fork 107
Notify push and email on code exchanges #2985
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
'use strict'; | ||
|
||
const Joi = require('joi'); | ||
const validators = require('../routes/validators'); | ||
|
||
module.exports = (config) => { | ||
return { | ||
path: '/v1/verify', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. get rid of this, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in #3000 |
||
method: 'POST', | ||
validate: { | ||
payload: { | ||
token: validators.accessToken.required(), | ||
}, | ||
response: { | ||
user: Joi.string().required(), | ||
client_id: Joi.string().required(), | ||
scope: Joi.array(), | ||
profile_changed_at: Joi.number().min(0) | ||
} | ||
} | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
'use strict'; | ||
|
||
const encrypt = require('../../../fxa-oauth-server/lib/encrypt'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this going to be OK at runtime in the docker images we build for production? I wonder if it would be less risky to just copy the code over here, it's only 3 lines worth. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah! we have the technology! |
||
const ScopeSet = require('fxa-shared').oauth.scopes; | ||
|
||
// right now we only care about notifications for the following scopes | ||
// if not a match, then we don't notify | ||
const NOTIFICATION_SCOPES = ScopeSet.fromArray(['https://identity.mozilla.com/apps/oldsync']); | ||
|
||
module.exports = { | ||
newTokenNotification: async function newTokenNotification (db, oauthdb, mailer, devices, request, grant) { | ||
const clientId = request.payload.client_id; | ||
const scopeSet = ScopeSet.fromString(grant.scope); | ||
const credentials = request.auth && request.auth.credentials || {}; | ||
|
||
if (! scopeSet.intersects(NOTIFICATION_SCOPES)) { | ||
// right now we only care about notifications for the `oldsync` scope | ||
// if not a match, then we don't do any notifications | ||
return; | ||
} | ||
|
||
if (! credentials.uid) { | ||
// this can be removed once issue #3000 has been resolved | ||
const tokenVerify = await oauthdb.checkAccessToken({ | ||
token: grant.access_token | ||
}); | ||
// some grant flows won't have the uid in `credentials` | ||
credentials.uid = tokenVerify.user; | ||
} | ||
|
||
if (! credentials.refreshTokenId) { | ||
// provide a refreshToken for the device creation below | ||
credentials.refreshTokenId = encrypt.hash(grant.refresh_token).toString('hex'); | ||
} | ||
|
||
// we set tokenVerified because the granted scope is part of NOTIFICATION_SCOPES | ||
credentials.tokenVerified = true; | ||
credentials.client = await oauthdb.getClientInfo(clientId); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We ought to be able to do a cached lookup here, but not for this PR; I filed https://github.com/mozilla/fxa-auth-server/issues/3006 as a followup. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
// The following upsert gets no `deviceInfo`. | ||
// However, `credentials.client` lets it generate a default name for the device. | ||
await devices.upsert(request, credentials, {}); | ||
|
||
const geoData = request.app.geo; | ||
const ip = request.app.clientAddress; | ||
const emailOptions = { | ||
acceptLanguage: request.app.acceptLanguage, | ||
ip, | ||
location: geoData.location, | ||
service: clientId, | ||
timeZone: geoData.timeZone, | ||
uid: credentials.uid | ||
}; | ||
|
||
const account = await db.account(credentials.uid); | ||
await mailer.sendNewDeviceLoginNotification(account.emails, account, emailOptions); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
'use strict'; | ||
|
||
const sinon = require('sinon'); | ||
const assert = { ...sinon.assert, ...require('chai').assert }; | ||
const mocks = require('../../../mocks'); | ||
|
||
const TEST_EMAIL = '[email protected]'; | ||
const MOCK_UID = '23d4847823f24b0f95e1524987cb0391'; | ||
const MOCK_REFRESH_TOKEN = '40f61392cf69b0be709fbd3122d0726bb32247b476b2a28451345e7a5555cec7'; | ||
const MOCK_REFRESH_TOKEN_2 = '00661392cf69b0be709fbd3122d0726bb32247b476b2a28451345e7a5555cec7'; | ||
const MOCK_REFRESH_TOKEN_ID_2 = '0e4f2255bed0ae53af401150488e69f22beae103b7d6857a5194df00c9827d19'; | ||
const OAUTH_CLIENT_ID = '3c49430b43dfba77'; | ||
const MOCK_CHECK_RESPONSE = { | ||
user: MOCK_UID, | ||
client_id: OAUTH_CLIENT_ID, | ||
scope: ['https://identity.mozilla.com/apps/oldsync', 'openid'] | ||
}; | ||
|
||
describe('newTokenNotification', () => { | ||
let db; | ||
let oauthdb; | ||
let mailer; | ||
let devices; | ||
let request; | ||
let credentials; | ||
let grant; | ||
const oauthUtils = require('../../../../lib/routes/utils/oauth'); | ||
|
||
beforeEach(() => { | ||
db = mocks.mockDB({ | ||
email: TEST_EMAIL, | ||
emailVerified: true, | ||
uid: MOCK_UID | ||
}); | ||
oauthdb = mocks.mockOAuthDB({ | ||
checkAccessToken: sinon.spy(async () => { | ||
return MOCK_CHECK_RESPONSE; | ||
}) | ||
}); | ||
mailer = mocks.mockMailer(); | ||
devices = mocks.mockDevices(); | ||
credentials = { | ||
uid: MOCK_UID, | ||
refreshTokenId: MOCK_REFRESH_TOKEN | ||
}; | ||
request = mocks.mockRequest({credentials}); | ||
grant = { | ||
scope: 'profile https://identity.mozilla.com/apps/oldsync', | ||
refresh_token: MOCK_REFRESH_TOKEN_2 | ||
}; | ||
}); | ||
|
||
it('creates a device and sends an email with credentials uid', async () => { | ||
await oauthUtils.newTokenNotification(db, oauthdb, mailer, devices, request, grant); | ||
|
||
assert.equal(oauthdb.checkAccessToken.callCount, 0); | ||
assert.equal(mailer.sendNewDeviceLoginNotification.callCount, 1, 'sent email notification'); | ||
assert.equal(devices.upsert.callCount, 1, 'created a device'); | ||
const args = devices.upsert.args[0]; | ||
assert.equal(args[1].refreshTokenId, request.auth.credentials.refreshTokenId); | ||
}); | ||
|
||
it('creates a device and sends an email with checkAccessToken uid', async () => { | ||
credentials = {}; | ||
request = mocks.mockRequest({credentials}); | ||
await oauthUtils.newTokenNotification(db, oauthdb, mailer, devices, request, grant); | ||
|
||
assert.equal(oauthdb.checkAccessToken.callCount, 1); | ||
assert.equal(mailer.sendNewDeviceLoginNotification.callCount, 1, 'sent email notification'); | ||
assert.equal(devices.upsert.callCount, 1, 'created a device'); | ||
}); | ||
|
||
it('does nothing for non-NOTIFICATION_SCOPES', async () => { | ||
grant.scope = 'profile'; | ||
await oauthUtils.newTokenNotification(db, oauthdb, mailer, devices, request, grant); | ||
|
||
assert.equal(oauthdb.checkAccessToken.callCount, 0); | ||
assert.equal(mailer.sendNewDeviceLoginNotification.callCount, 0); | ||
assert.equal(devices.upsert.callCount, 0); | ||
}); | ||
|
||
it('uses refreshTokenId from grant if not provided', async () => { | ||
credentials = { | ||
uid: MOCK_UID, | ||
}; | ||
request = mocks.mockRequest({credentials}); | ||
await oauthUtils.newTokenNotification(db, oauthdb, mailer, devices, request, grant); | ||
|
||
assert.equal(oauthdb.checkAccessToken.callCount, 0); | ||
assert.equal(mailer.sendNewDeviceLoginNotification.callCount, 1); | ||
assert.equal(devices.upsert.callCount, 1); | ||
const args = devices.upsert.args[0]; | ||
assert.equal(args[1].refreshTokenId, MOCK_REFRESH_TOKEN_ID_2); | ||
}); | ||
|
||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this needed? I wonder if we can find a way to fix
request.app.devices
so that we maintain the nice caching behaviour.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's problematic, https://github.com/mozilla/fxa-auth-server/issues/2992 was filed.