Skip to content
This repository was archived by the owner on Apr 3, 2019. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bin/key_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ function main() {
})
}
})
.catch(process.exit.bind(null, 8))
.catch((err) => {
process.exit(8)
})
}

if (require.main === module) {
Expand Down
12 changes: 12 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ see [`mozilla/fxa-js-client`](https://github.com/mozilla/fxa-js-client).
* [Oauth](#oauth)
* [GET /oauth/client/{client_id}](#get-oauthclientclient_id)
* [POST /account/scoped-key-data (:lock: sessionToken)](#post-accountscoped-key-data)
* [POST /oauth/token](#post-oauthtoken)
* [Password](#password)
* [POST /password/change/start](#post-passwordchangestart)
* [POST /password/change/finish (:lock: passwordChangeToken)](#post-passwordchangefinish)
Expand Down Expand Up @@ -371,13 +372,15 @@ those common validations are defined here.
#### lib/routes/validators

* `HEX_STRING`: `/^(?:[a-fA-F0-9]{2})+$/`
* `B64URL_STRING`: `/^[A-Za-z0-9-_]+$/`
* `BASE_36`: `/^[a-zA-Z0-9]*$/`
* `URL_SAFE_BASE_64`: `/^[A-Za-z0-9_-]+$/`
* `DISPLAY_SAFE_UNICODE`: `/^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uD800-\uDFFF\uE000-\uF8FF\uFFF9-\uFFFF])*$/`
* `DISPLAY_SAFE_UNICODE_WITH_NON_BMP`: `/^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uE000-\uF8FF\uFFF9-\uFFFF])*$/`
* `service`: `string, max(16), regex(/^[a-zA-Z0-9\-]*$/)`
* `hexString`: `string, regex(/^(?:[a-fA-F0-9]{2})+$/)`
* `clientId`: `module.exports.hexString.length(16)`
* `clientSecret`: `string, length(), regex(/^(?:[a-fA-F0-9]{2})+$/)`
* `accessToken`: `module.exports.hexString.length(32)`
* `refreshToken`: `module.exports.hexString.length(32)`
* `scope`: `string, max(256), regex(/^[a-zA-Z0-9 _\/.:-]+$/)`
Expand All @@ -392,6 +395,9 @@ those common validations are defined here.
* `DIGITS`: `/^[0-9]+$/`
* `DEVICE_COMMAND_NAME`: `/^[a-zA-Z0-9._\/\-:]{1,100}$/`
* `IP_ADDRESS`: `string, ip`
* `codeVerifier`: `string, min(43), max(128), regex(/^[A-Za-z0-9-_]+$/)`
* `token`: `string, length(), regex(/^(?:[a-fA-F0-9]{2})+$/)`
* `redirectUri`: `string, max(256), regex(/^[a-zA-Z0-9\-_\/.:?=&]+$/)`

#### lib/metrics/context

Expand Down Expand Up @@ -2020,6 +2026,12 @@ requested by the specified OAuth client.
<!--end-request-body-post-accountscoped-key-data-scope-->


#### POST /oauth/token
<!--begin-route-post-oauthtoken-->

<!--end-route-post-oauthtoken-->


### Password

#### POST /password/change/start
Expand Down
117 changes: 117 additions & 0 deletions lib/oauthdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,73 @@ const createBackendServiceAPI = require('./backendService')
const error = require('./error')
const validators = require('./routes/validators')

// TODO: hook it up
const MAX_TTL_S = 604800000 / 1000; // 2 weeks ms? / 1000
const GRANT_AUTHORIZATION_CODE = 'authorization_code';
const GRANT_REFRESH_TOKEN = 'refresh_token';

const TOKEN_PAYLOAD_SCHEMA = Joi.object({

client_id: validators.clientId
.when('$headers.authorization', {
is: Joi.string().required(),
then: Joi.forbidden()
}),

client_secret: validators.clientSecret
.when('code_verifier', {
is: Joi.string().required(), // if (typeof code_verifier === 'string') {
then: Joi.forbidden()
})
.when('grant_type', {
is: GRANT_REFRESH_TOKEN,
then: Joi.optional()
})
.when('$headers.authorization', {
is: Joi.string().required(),
then: Joi.forbidden()
}),

code_verifier: validators.codeVerifier,

redirect_uri: validators.redirectUri.optional(),

grant_type: Joi.string()
.valid(GRANT_AUTHORIZATION_CODE, GRANT_REFRESH_TOKEN)
.default(GRANT_AUTHORIZATION_CODE)
.optional(),

ttl: Joi.number()
.positive()
.max(MAX_TTL_S)
.default(MAX_TTL_S)
.optional(),

scope: validators.scope
.when('grant_type', {
is: GRANT_REFRESH_TOKEN,
otherwise: Joi.forbidden()
}),

code: Joi.string()
//.length(config.get('unique.code') * 2)
.length(32 * 2)
.regex(validators.HEX_STRING)
.required()
.when('grant_type', {
is: GRANT_AUTHORIZATION_CODE,
otherwise: Joi.forbidden()
}),

refresh_token: validators.token
.required()
.when('grant_type', {
is: GRANT_REFRESH_TOKEN,
otherwise: Joi.forbidden()
})

});

// The oauth-server's error numbers overlap and conflict
// with the auth-server's, so we have to map them to new ones.
function mapOAuthError(log, err) {
Expand Down Expand Up @@ -86,6 +153,40 @@ module.exports = (log, config) => {
keyRotationTimestamp: Joi.number().required(),
}))
}
},

postVerifyToken: {
path: '/v1/verify',
method: 'POST',
validate: {
payload: {
token: validators.token.required(),
},
response: {
user: Joi.string().required(),
client_id: Joi.string().required(),
scope: Joi.array(),
profile_changed_at: Joi.number().min(0)
}
}
},

postToken: {
path: '/v1/token',
method: 'POST',
validate: {
payload: TOKEN_PAYLOAD_SCHEMA,
response: Joi.object().keys({
access_token: validators.token.required(),
refresh_token: validators.token,
id_token: validators.assertion,
scope: validators.scope.required(),
token_type: Joi.string().valid('bearer').required(),
expires_in: Joi.number().max(MAX_TTL_S).required(),
auth_at: Joi.number(),
keys_jwe: validators.jwe.optional()
})
}
}
})

Expand Down Expand Up @@ -142,6 +243,22 @@ module.exports = (log, config) => {
}
},

async postToken(payload) {
try {
return await api.postToken(payload)
} catch (err) {
throw mapOAuthError(log, err)
}
},

async postVerifyToken(token) {
try {
return await api.postVerifyToken(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:
Expand Down
2 changes: 1 addition & 1 deletion lib/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, push)
const devicesSessions = require('./devices-and-sessions')(log, db, config, customs, push, pushbox, devicesImpl)
const emails = require('./emails')(log, db, mailer, config, customs, push)
const password = require('./password')(
Expand Down
143 changes: 141 additions & 2 deletions lib/routes/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,79 @@ const Joi = require('joi')

const validators = require('./validators')

module.exports = (log, config, oauthdb) => {


// TODO: hook it up
const MAX_TTL_S = 604800000 / 1000; // 2 weeks ms? / 1000
const GRANT_AUTHORIZATION_CODE = 'authorization_code';
const GRANT_REFRESH_TOKEN = 'refresh_token';

const TOKEN_PAYLOAD_SCHEMA = Joi.object({

client_id: validators.clientId
.when('$headers.authorization', {
is: Joi.string().required(),
then: Joi.forbidden()
}),

client_secret: validators.clientSecret
.when('code_verifier', {
is: Joi.string().required(), // if (typeof code_verifier === 'string') {
then: Joi.forbidden()
})
.when('grant_type', {
is: GRANT_REFRESH_TOKEN,
then: Joi.optional()
})
.when('$headers.authorization', {
is: Joi.string().required(),
then: Joi.forbidden()
}),

code_verifier: validators.codeVerifier,

redirect_uri: validators.redirectUri.optional(),

grant_type: Joi.string()
.valid(GRANT_AUTHORIZATION_CODE, GRANT_REFRESH_TOKEN)
.default(GRANT_AUTHORIZATION_CODE)
.optional(),

ttl: Joi.number()
.positive()
.max(MAX_TTL_S)
.default(MAX_TTL_S)
.optional(),

scope: validators.scope
.when('grant_type', {
is: GRANT_REFRESH_TOKEN,
otherwise: Joi.forbidden()
}),

code: Joi.string()
//.length(config.get('unique.code') * 2)
.length(32 * 2)
.regex(validators.HEX_STRING)
.required()
.when('grant_type', {
is: GRANT_AUTHORIZATION_CODE,
otherwise: Joi.forbidden()
}),

refresh_token: validators.token
.required()
.when('grant_type', {
is: GRANT_REFRESH_TOKEN,
otherwise: Joi.forbidden()
})

});




module.exports = (log, config, oauthdb, db, mailer, push) => {
const routes = [
{
method: 'GET',
Expand Down Expand Up @@ -68,8 +140,75 @@ module.exports = (log, config, oauthdb) => {
handler: async function (request) {
const sessionToken = request.auth.credentials
return oauthdb.getScopedKeyData(sessionToken, request.payload)
}
},
},
{
method: 'POST',
path: '/oauth/token',
options: {
validate: {
payload: TOKEN_PAYLOAD_SCHEMA
},
response: {
schema: Joi.object().keys({
access_token: validators.token.required(),
refresh_token: validators.token,
id_token: validators.assertion,
scope: validators.scope.required(),
token_type: Joi.string().valid('bearer').required(),
expires_in: Joi.number().max(MAX_TTL_S).required(),
auth_at: Joi.number(),
keys_jwe: validators.jwe.optional()
})
}
},
handler: async function (request) {
return oauthdb.postToken(request.payload)
.then(async (resp) => {

// TODO: is this how we are gonna get the UIDs now?
const tokenVerify = await oauthdb.postVerifyToken({
token: resp.access_token
});

const uid = tokenVerify.user;
const accountRecord = await db.account(uid);

Copy link
Contributor

@rfk rfk Feb 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware this is all WIP, just noting that we only want to do the steps below if the token has "oldsync" scope. (Also I think you'll get the notifyDeviceConnected part for free if you actually create a placeholder device record at this point.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeap, thanks!

const devices = db.devices(uid);
push.notifyDeviceConnected(uid, devices, 'Android Reference Browser');

try {
await mailer.sendNewDeviceLoginNotification(
accountRecord.emails,
accountRecord,
{
acceptLanguage: request.app.acceptLanguage,
// deviceId,
// flowId,
// flowBeginTime,
//ip,
//location: geoData.location,
//service,
//timeZone: geoData.timeZone,
uaBrowser: request.app.ua.browser,
uaBrowserVersion: request.app.ua.browserVersion,
uaOS: request.app.ua.os,
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uid: uid
}
)
} catch (err) {
log.error({
op: 'Account.login.sendNewDeviceLoginNotification.error',
error: err
})
}

return resp;
})
}
}
]
return routes
}
18 changes: 18 additions & 0 deletions lib/routes/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const isA = require('joi')
const HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/
module.exports.HEX_STRING = HEX_STRING

const B64URL_STRING = /^[A-Za-z0-9-_]+$/;
module.exports.B64URL_STRING = B64URL_STRING;
module.exports.BASE_36 = /^[a-zA-Z0-9]*$/

// RFC 4648, section 5
Expand Down Expand Up @@ -77,6 +79,22 @@ module.exports.email = function() {
module.exports.service = isA.string().max(16).regex(/^[a-zA-Z0-9\-]*$/)
module.exports.hexString = isA.string().regex(HEX_STRING)
module.exports.clientId = module.exports.hexString.length(16)
module.exports.clientSecret = isA.string()
//.length(config.get('unique.clientSecret') * 2) // hex = bytes*2
.length(32 * 2) // hex = bytes*2
.regex(exports.HEX_STRING)
//.required();
exports.codeVerifier = isA.string()
.min(43)
.max(128)
.regex(B64URL_STRING); // https://tools.ietf.org/html/rfc7636#section-4.1
exports.token = isA.string()
//.length(config.get('unique.token') * 2)
.length(32 * 2)
.regex(exports.HEX_STRING);
exports.redirectUri = isA.string()
.max(256)
.regex(/^[a-zA-Z0-9\-_\/.:?=&]+$/);
module.exports.accessToken = module.exports.hexString.length(32)
module.exports.refreshToken = module.exports.hexString.length(32)
module.exports.scope = isA.string().max(256).regex(/^[a-zA-Z0-9 _\/.:-]+$/)
Expand Down