diff --git a/lib/devices.js b/lib/devices.js index cb40323d6..2c927c0a0 100644 --- a/lib/devices.js +++ b/lib/devices.js @@ -114,7 +114,7 @@ module.exports = (log, db, push) => { deviceName = synthesizeName(deviceInfo); } if (credentials.tokenVerified) { - request.app.devices.then(devices => { + db.devices(credentials.uid).then(devices => { const otherDevices = devices.filter(device => device.id !== result.id); return push.notifyDeviceConnected(credentials.uid, otherDevices, deviceName); }); diff --git a/lib/oauthdb/check-access-token.js b/lib/oauthdb/check-access-token.js new file mode 100644 index 000000000..6bcad752a --- /dev/null +++ b/lib/oauthdb/check-access-token.js @@ -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', + 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) + } + } + } +} diff --git a/lib/oauthdb/index.js b/lib/oauthdb/index.js index f29feeed2..b9cf79a88 100644 --- a/lib/oauthdb/index.js +++ b/lib/oauthdb/index.js @@ -32,6 +32,7 @@ module.exports = (log, config) => { grantTokensFromAuthorizationCode: require('./grant-tokens-from-authorization-code')(config), grantTokensFromRefreshToken: require('./grant-tokens-from-refresh-token')(config), grantTokensFromCredentials: require('./grant-tokens-from-credentials')(config), + checkAccessToken: require('./check-access-token')(config), }); const api = new OAuthAPI(config.oauth.url, config.oauth.poolee); @@ -115,6 +116,14 @@ module.exports = (log, config) => { } }, + async checkAccessToken(token) { + try { + return await api.checkAccessToken(token) + } catch (err) { + throw mapOAuthError(log, err) + } + } + /* As we work through the process of merging oauth-server * into auth-server, future methods we might want to include * here will be things like the following: @@ -122,9 +131,6 @@ module.exports = (log, config) => { async getClientInstances(account) { }, - async checkAccessToken(token) { - } - async revokeAccessToken(token) { } diff --git a/lib/routes/index.js b/lib/routes/index.js index 5a72bc0a1..df2245973 100644 --- a/lib/routes/index.js +++ b/lib/routes/index.js @@ -36,7 +36,7 @@ module.exports = function ( signinUtils, push ); - const oauth = require('./oauth')(log, config, oauthdb); + const oauth = require('./oauth')(log, config, oauthdb, db, mailer, devicesImpl); const devicesSessions = require('./devices-and-sessions')(log, db, config, customs, push, pushbox, devicesImpl, oauthdb); const emails = require('./emails')(log, db, mailer, config, customs, push); const password = require('./password')( diff --git a/lib/routes/oauth.js b/lib/routes/oauth.js index 95871f85a..af4efcb0d 100644 --- a/lib/routes/oauth.js +++ b/lib/routes/oauth.js @@ -18,8 +18,9 @@ const Joi = require('joi'); const error = require('../error'); +const oauthRouteUtils = require('./utils/oauth'); -module.exports = (log, config, oauthdb) => { +module.exports = (log, config, oauthdb, db, mailer, devices) => { const routes = [ { method: 'GET', @@ -110,19 +111,31 @@ module.exports = (log, config, oauthdb) => { }, handler: async function (request) { const sessionToken = request.auth.credentials; + let grant; switch (request.payload.grant_type) { case 'authorization_code': - return await oauthdb.grantTokensFromAuthorizationCode(request.payload); + grant = await oauthdb.grantTokensFromAuthorizationCode(request.payload); + break; case 'refresh_token': - return await oauthdb.grantTokensFromRefreshToken(request.payload); + grant = await oauthdb.grantTokensFromRefreshToken(request.payload); + break; case 'fxa-credentials': if (! sessionToken) { throw error.invalidToken(); } - return await oauthdb.grantTokensFromSessionToken(sessionToken, request.payload); + grant = await oauthdb.grantTokensFromSessionToken(sessionToken, request.payload); + break; default: throw error.internalValidationError(); } + + switch (request.payload.grant_type) { + case 'authorization_code': + case 'fxa-assertion': + await oauthRouteUtils.newTokenNotification(db, oauthdb, mailer, devices, request, sessionToken, grant) + } + + return grant; } }, ]; diff --git a/lib/routes/utils/oauth.js b/lib/routes/utils/oauth.js new file mode 100644 index 000000000..93996483c --- /dev/null +++ b/lib/routes/utils/oauth.js @@ -0,0 +1,55 @@ +/* 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 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, credentials, grant) { + const scopeSet = ScopeSet.fromString(grant.scope) + + 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 + } + + const tokenVerify = await oauthdb.checkAccessToken({ + token: grant.access_token + }) + + if (! credentials) { + credentials = {} + } + + const uid = tokenVerify.user + credentials.uid = uid + credentials.tokenVerified = true // XXX TODO: check this? + credentials.refreshTokenId = grant.refresh_token + credentials.client = await oauthdb.getClientInfo(tokenVerify.client_id) + + await devices.upsert(request, credentials, {}) + + const geoData = request.app.geo + const ip = request.app.clientAddress + const service = request.payload.service || request.query.service || tokenVerify.client_id + + const emailOptions = { + acceptLanguage: request.app.acceptLanguage, + ip: ip, + location: geoData.location, + service: service, + timeZone: geoData.timeZone, + uid: credentials.uid + } + + const account = await db.account(uid) + await mailer.sendNewDeviceLoginNotification(account.emails, account, emailOptions) + } +} diff --git a/test/client/api.js b/test/client/api.js index 6a6d56b94..8cc4da6df 100644 --- a/test/client/api.js +++ b/test/client/api.js @@ -1021,6 +1021,18 @@ module.exports = config => { ); }; + ClientApi.prototype.grantTokensFromSessionToken = function (sessionTokenHex, oauthParams) { + return tokens.SessionToken.fromHex(sessionTokenHex) + .then((token) => { + return this.doRequest( + 'POST', + `${this.baseURL}/oauth/token`, + token, + oauthParams + ) + }) + } + ClientApi.heartbeat = function (origin) { return (new ClientApi(origin)).doRequest('GET', `${origin }/__heartbeat__`); }; diff --git a/test/client/index.js b/test/client/index.js index 8cf10a480..f960bf18a 100644 --- a/test/client/index.js +++ b/test/client/index.js @@ -692,5 +692,9 @@ module.exports = config => { return this.api.grantOAuthTokens(oauthParams); }; + Client.prototype.grantTokensFromSessionToken = function (oauthParams) { + return this.api.grantTokensFromSessionToken(this.sessionToken, oauthParams); + } + return Client; -}; +}