From 17dbac74cd1bea7e981efc690eff961c8bb0e96a Mon Sep 17 00:00:00 2001 From: Edouard Oger Date: Mon, 17 Sep 2018 11:37:50 -0400 Subject: [PATCH] feat(oauth): Add an OAuth-sessionToken exchange endpoint --- docs/api.md | 21 +++++++++++++++++ lib/error.js | 13 ++++++++++ lib/routes/account.js | 55 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/docs/api.md b/docs/api.md index 251c4bc6..93d6defc 100644 --- a/docs/api.md +++ b/docs/api.md @@ -24,6 +24,7 @@ see [`mozilla/fxa-js-client`](https://github.com/mozilla/fxa-js-client). * [Account](#account) * [POST /account/create](#post-accountcreate) * [POST /account/login](#post-accountlogin) + * [GET /account/oauth_exchange (:lock: oauthToken)](#get-accountoauth_exchange) * [GET /account/status (:lock::unlock: sessionToken)](#get-accountstatus) * [POST /account/status](#post-accountstatus) * [GET /account/profile (:lock: sessionToken, oauthToken)](#get-accountprofile) @@ -295,6 +296,8 @@ for `code` and `errno` are: This request requires two step authentication enabled on your account. * `code: 400, errno: 161`: Recovery key already exists. +* `code: 401, errno: 162`: + The required scope(s) for this request was not found. * `code: 503, errno: 201`: Service unavailable * `code: 503, errno: 202`: @@ -327,6 +330,7 @@ include additional response properties: * `errno: 135`: bouncedAt * `errno: 152` * `errno: 153` +* `errno: 162`: requiredScopes * `errno: 201`: retryAfter * `errno: 202`: retryAfter * `errno: 203`: service, operation @@ -716,6 +720,23 @@ by the following errors This request requires two step authentication enabled on your account. +#### GET /account/oauth_exchange + +:lock: authenticated with OAuth bearer token + + + + +##### Error responses + +Failing requests may be caused +by the following errors +(this is not an exhaustive list): + +* `code: 401, errno: 162`: + The required scope(s) for this request was not found. + + #### GET /account/status :lock::unlock: Optionally HAWK-authenticated with session token diff --git a/lib/error.js b/lib/error.js index 3a93a83d..3a792e4e 100644 --- a/lib/error.js +++ b/lib/error.js @@ -75,6 +75,8 @@ var ERRNO = { TOTP_REQUIRED: 160, RECOVERY_KEY_EXISTS: 161, + INVALID_SCOPE: 162, + SERVER_BUSY: 201, FEATURE_NOT_ENABLED: 202, BACKEND_SERVICE_FAILURE: 203, @@ -851,6 +853,17 @@ AppError.backendServiceFailure = (service, operation) => { }) } +AppError.invalidScope = (requiredScopes) => { + return new AppError({ + code: 401, + error: 'Unauthorized', + errno: ERRNO.INVALID_SCOPE, + message: 'The required scope(s) for this request was not found.' + }, { + requiredScopes + }) +} + AppError.unexpectedError = () => { return new AppError({}) } diff --git a/lib/routes/account.js b/lib/routes/account.js index d9e37f6f..c5b7984d 100644 --- a/lib/routes/account.js +++ b/lib/routes/account.js @@ -22,6 +22,8 @@ const MS_ONE_DAY = MS_ONE_HOUR * 24 const MS_ONE_WEEK = MS_ONE_DAY * 7 const MS_ONE_MONTH = MS_ONE_DAY * 30 +const SCOPE_ADMIN = 'https://identity.mozilla.com/admin' + module.exports = (log, db, mailer, Password, config, customs, signinUtils, push) => { const tokenCodeConfig = config.signinConfirmation.tokenVerificationCode const tokenCodeLifetime = tokenCodeConfig && tokenCodeConfig.codeLifetime || MS_ONE_HOUR @@ -726,6 +728,59 @@ module.exports = (log, db, mailer, Password, config, customs, signinUtils, push) } } }, + { + method: 'GET', + path: '/account/oauth_exchange', + options: { + auth: { + strategy: 'oauthToken', + payload: false, // no payload authentication in OAuth. + }, + response: { + schema: { + sessionToken: isA.string().regex(HEX_STRING).required(), + } + } + }, + handler: async function (request) { + const auth = request.auth + const uid = auth.credentials.user; + const scope = ScopeSet.fromArray(auth.credentials.scope); + if (! scope.contains(SCOPE_ADMIN)) { + throw error.invalidScope(SCOPE_ADMIN) + } + + const account = await db.account(uid); + const { + browser: uaBrowser, + browserVersion: uaBrowserVersion, + os: uaOS, + osVersion: uaOSVersion, + deviceType: uaDeviceType, + formFactor: uaFormFactor + } = request.app.ua + + const sessionToken = await db.createSessionToken({ + uid: account.uid, + email: account.email, + emailCode: account.emailCode, + emailVerified: account.emailVerified, + verifierSetAt: account.verifierSetAt, + mustVerify: false, + tokenVerificationCode: undefined, + tokenVerificationCodeExpiresAt: undefined, + tokenVerificationId: undefined, + uaBrowser, + uaBrowserVersion, + uaOS, + uaOSVersion, + uaDeviceType, + uaFormFactor + }) + + return {sessionToken: sessionToken.data} + } + }, { method: 'GET', path: '/account/status',