fix(scopes): Document scope-handling rules, use shared code to enforce them.
This commit is contained in:
Родитель
adfff658c4
Коммит
d7ea1fc239
|
@ -0,0 +1,199 @@
|
|||
# OAuth Scopes
|
||||
|
||||
Each authorization grant in OAuth has an associated "scope",
|
||||
a list containing one or more "scope values"
|
||||
that indicate what capabilities the granted token will have.
|
||||
Each individual scope value indicates a particular capability,
|
||||
such as the ability to read or write profile data,
|
||||
or to access the user's data in a particular service.
|
||||
|
||||
As defined in [RFC6749 Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3),
|
||||
the scope of a token is expressed as
|
||||
a list of space-delimited, case-sensitive strings,
|
||||
and it is left up to the service
|
||||
to define the format and semantics
|
||||
of the individual scope values
|
||||
that make up this string.
|
||||
|
||||
This document defines the scope values
|
||||
accepted in the Firefox Accounts ecosystem,
|
||||
and the rules for parsing and validating them.
|
||||
|
||||
## Short-name scope values
|
||||
|
||||
FxA supports a small set of "short-name" scope values
|
||||
that are identified by a short English word.
|
||||
These correspond either to
|
||||
scope values defined by external specifications
|
||||
(e.g. OpenID Connect),
|
||||
or to legacy scope values
|
||||
introduced during early development.
|
||||
|
||||
**No new short-name scope values should be added.**
|
||||
Instead we prefer to use URLs for new scope values,
|
||||
both to ensure uniqueness
|
||||
and to simplify parsing rules.
|
||||
|
||||
Short-name scope values imply read-only access by default,
|
||||
with write access indicated by the suffix ":write".
|
||||
The may also have "sub-scopes"
|
||||
to indicate finer-grained access control.
|
||||
Each name component may contain only ascii alphanumeric characters
|
||||
and the underscore.
|
||||
|
||||
For example:
|
||||
|
||||
* `profile` indicates read-only access
|
||||
to the user's profile data.
|
||||
* `profile:write` indicates read/write access
|
||||
to the user's profile data.
|
||||
* `profile:display_name` indicates read-only access
|
||||
to the user's display name, but not any other
|
||||
profile data.
|
||||
* `profile:email:write` indicates read/write access
|
||||
to the user's email address.
|
||||
|
||||
The following short-name scope values are recognized
|
||||
in the FxA ecosystem.
|
||||
|
||||
### Profile data
|
||||
|
||||
* `profile`: access the user's profile data.
|
||||
* `profile:uid`: access the user's opaque user id.
|
||||
* `profile:email`: access the user's email address.
|
||||
* `profile:locale`: access the user's locale.
|
||||
* `profile:avatar`: access the user's avatar picture.
|
||||
* `profile:display_name`: access the user's human-readable display name.
|
||||
* `profile:amr`: access information about the user's authentication methods and 2FA status.
|
||||
|
||||
### OpenID Connect
|
||||
|
||||
* `openid`: used to request an OpenID Connect `id_token`.
|
||||
* `email`: a synonym for `profile:email`, defined by the OIDC spec.
|
||||
|
||||
### OAuth Client Management
|
||||
|
||||
* `clients`: access the list of OAuth clients connected to a user's account.
|
||||
* `oauth`: register a new OAuth client record.
|
||||
|
||||
### Basket
|
||||
|
||||
* `basket`: access the user's subscription data in
|
||||
[basket](http://basket.readthedocs.io/)
|
||||
|
||||
|
||||
## URL Scopes
|
||||
|
||||
For new capabilities, scope values are represented as URLs.
|
||||
This helps to ensure uniqueness
|
||||
and reduces ambiguity in parsing.
|
||||
URL-format scope value imply read/write access by default,
|
||||
are compared as heirarchical resource references,
|
||||
and use the hash fragment for permission qualifiers.
|
||||
For example:
|
||||
|
||||
* `https://identity.mozilla.com/apps/oldsync` indicates full
|
||||
access to the user's data in Firefox Sync.
|
||||
* `https://identity.mozilla.com/apps/oldsync/bookmarks` indicates
|
||||
full access to the user's bookmark data in Firefox Sync,
|
||||
but not to other data types.
|
||||
* `https://identity.mozilla.com/apps/oldsync#read` indicates
|
||||
read-only access to the user's data in Firefox Sync.
|
||||
* `https://identity.mozilla.com/apps/oldsync/history#write` indicates
|
||||
write-only access to the user's history data in Firefox Sync.
|
||||
|
||||
To be a valid scope value, the URL must:
|
||||
|
||||
* Be an absolute `https://` URL.
|
||||
* Have no username, password, or query component.
|
||||
* If present, have a fragment component consisting only of alphanumeric ascii characters and underscore.
|
||||
* Remain unchanged when parsed and serialized following the rules in the
|
||||
[WhatWG URL Spec](https://url.spec.whatwg.org).
|
||||
|
||||
The following URL scope values are currently recognized by FxA:
|
||||
|
||||
* `https://identity.mozilla.com/apps/oldsync`: access to data in Firefox Sync.
|
||||
* `https://identity.mozilla.com/apps/notes`: access to data in Firefox Notes.
|
||||
|
||||
|
||||
## Scope Matching and Implication
|
||||
|
||||
We say that a scope value A *implies* another scope value B
|
||||
if they are exactly equal,
|
||||
or if A represents a more general capability than B.
|
||||
Similarly, a scope A implies scope value B
|
||||
if there is some scope value in A that implies B.
|
||||
This is the basic operation used to check
|
||||
permissions when processing an OAuth token.
|
||||
|
||||
Consumers of OAuth tokens should avoid
|
||||
directly parsing and comparing scopes where possible,
|
||||
and instead use the existing implementation
|
||||
in the `fxa-shared` node module.
|
||||
|
||||
For consumers that must implement their own scope checking,
|
||||
the rules for implication can be summarized as:
|
||||
|
||||
* For URL scope values, A implies B if A is a parent resource of B.
|
||||
* For short-name scope values, split on the ":" character,
|
||||
and A implies B if either:
|
||||
* B[-1] is not "write" and A is a prefix of B, or.
|
||||
* A[-1] is "write", and:
|
||||
* A[:-1] is a prefix of B, or
|
||||
* B[-1] is "write" and A[:-1] is a prefix of B[:-1]
|
||||
|
||||
More precisely, the algoritm for checking implication is:
|
||||
|
||||
* If A is a `https://` URL, then:
|
||||
* If B is not a `https://` URL, then fail.
|
||||
* If the origin of B is different than that of A, then fail.
|
||||
* If the path component list of A is not a prefix of the path
|
||||
component list of B, then fail.
|
||||
* If A has a fragment, then:
|
||||
* If B does not have a fragment, then fail.
|
||||
* If B has a fragment that differs from A, then fail.
|
||||
* Otherwise, succeed.
|
||||
* Otherwise:
|
||||
* If B is a `https://` URL, then fail.
|
||||
* Split A and B into components based on `:` delimiter.
|
||||
* If the last component of B is `write`, then:
|
||||
* If the last component of A is not `write`, then fail.
|
||||
* If the last component of A is `write`, remove it.
|
||||
* If A is not a prefix of B, then fail.
|
||||
* Otherwise, succeed.
|
||||
|
||||
Below are some testcases against which
|
||||
scope-checking code can be validated.
|
||||
|
||||
Valid implications:
|
||||
* `profile:write` implies `profile`.
|
||||
* `profile` implies `profile:email`.
|
||||
* `profile:write` implies `profile:email`.
|
||||
* `profile:write` implies `profile:email:write`.
|
||||
* `profile:email:write` implies `profile:email`.
|
||||
* `profile profile:email:write` implies `profile:email`.
|
||||
* `profile profile:email:write` implies `profile:display_name`.
|
||||
* `profile https://identity.mozilla.com/apps/oldsync` implies `profile`.
|
||||
* `profile https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync`.
|
||||
* `https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync#read`.
|
||||
* `https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync/bookmarks`.
|
||||
* `https://identity.mozilla.com/apps/oldsync` implies `https://identity.mozilla.com/apps/oldsync/bookmarks#read`.
|
||||
* `https://identity.mozilla.com/apps/oldsync#read` implies `https://identity.mozilla.com/apps/oldsync/bookmarks#read`.
|
||||
* `https://identity.mozilla.com/apps/oldsync#read profile` implies `https://identity.mozilla.com/apps/oldsync/bookmarks#read`.
|
||||
|
||||
Invalid implications:
|
||||
* `profile:email:write` does *not* imply `profile`.
|
||||
* `profile:email:write` does *not* imply `profile:write`.
|
||||
* `profile:email` does *not* imply `profile:display_name`.
|
||||
* `profilebogey` does *not* imply `profile`.
|
||||
* `profile:write` does *not* imply `https://identity.mozilla.com/apps/oldsync`.
|
||||
* `profile profile:email:write` does *not* imply `profile:write`.
|
||||
* `https` does *not* imply `https://identity.mozilla.com/apps/oldsync`.
|
||||
* `https://identity.mozilla.com/apps/oldsync` does *not* imply `profile`.
|
||||
* `https://identity.mozilla.com/apps/oldsync#read` does *not* imply `https://identity.mozilla.com/apps/oldsync/bookmarks`.
|
||||
* `https://identity.mozilla.com/apps/oldsync#write` does *not* imply `https://identity.mozilla.com/apps/oldsync/bookmarks#read`.
|
||||
* `https://identity.mozilla.com/apps/oldsync/bookmarks` does *not* imply `https://identity.mozilla.com/apps/oldsync`.
|
||||
* `https://identity.mozilla.com/apps/oldsync/bookmarks` does *not* imply `https://identity.mozilla.com/apps/oldsync/passwords`.
|
||||
* `https://identity.mozilla.com/apps/oldsyncer` does *not* imply `https://identity.mozilla.com/apps/oldsync`.
|
||||
* `https://identity.mozilla.com/apps/oldsync` does *not* imply `https://identity.mozilla.com/apps/oldsyncer`.
|
||||
* `https://identity.mozilla.org/apps/oldsync` does *not* imply `https://identity.mozilla.com/apps/oldsync`.
|
|
@ -2,6 +2,8 @@
|
|||
* 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/. */
|
||||
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
|
||||
const AppError = require('./error');
|
||||
const logger = require('./logging')('server.auth');
|
||||
const token = require('./token');
|
||||
|
@ -15,7 +17,7 @@ const WHITELIST = require('./config').get('admin.whitelist').map(function(re) {
|
|||
exports.AUTH_STRATEGY = 'dogfood';
|
||||
exports.AUTH_SCHEME = 'bearer';
|
||||
|
||||
exports.SCOPE_CLIENT_MANAGEMENT = 'oauth';
|
||||
exports.SCOPE_CLIENT_MANAGEMENT = ScopeSet.fromArray(['oauth']);
|
||||
|
||||
exports.strategy = function() {
|
||||
return {
|
||||
|
@ -32,7 +34,7 @@ exports.strategy = function() {
|
|||
}
|
||||
|
||||
token.verify(tok).done(function tokenFound(details) {
|
||||
if (details.scope.indexOf(exports.SCOPE_CLIENT_MANAGEMENT) !== -1) {
|
||||
if (details.scope.contains(exports.SCOPE_CLIENT_MANAGEMENT)) {
|
||||
logger.debug('check.whitelist');
|
||||
var blocked = ! WHITELIST.some(function(re) {
|
||||
return re.test(details.email);
|
||||
|
@ -47,6 +49,7 @@ exports.strategy = function() {
|
|||
}
|
||||
|
||||
logger.info('success', details);
|
||||
details.scope = details.scope.getScopeValues();
|
||||
reply.continue({
|
||||
credentials: details
|
||||
});
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
* 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/. */
|
||||
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
|
||||
const AppError = require('./error');
|
||||
const logger = require('./logging')('server.auth_bearer');
|
||||
const token = require('./token');
|
||||
const validators = require('./validators');
|
||||
|
||||
const authName = 'authBearer';
|
||||
const authOAuthScope = 'clients:write';
|
||||
const authOAuthScope = ScopeSet.fromArray(['clients:write']);
|
||||
|
||||
exports.AUTH_STRATEGY = authName;
|
||||
exports.AUTH_SCHEME = authName;
|
||||
|
@ -32,6 +34,7 @@ exports.strategy = function() {
|
|||
|
||||
token.verify(tok).done(function tokenFound(details) {
|
||||
logger.info(authName + '.success', details);
|
||||
details.scope = details.scope.getScopeValues();
|
||||
reply.continue({
|
||||
credentials: details
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
**/
|
||||
|
||||
const unbuf = require('buf').unbuf.hex;
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
|
@ -32,7 +33,6 @@ module.exports = {
|
|||
var activeClients = {};
|
||||
activeClientTokens.forEach(function (clientTokenObj) {
|
||||
var clientIdHex = unbuf(clientTokenObj.id);
|
||||
var scope = String(clientTokenObj.scope).split(/[\s,]+/);
|
||||
|
||||
if (! activeClients[clientIdHex]) {
|
||||
// add the OAuth client if not already in the Object
|
||||
|
@ -40,14 +40,11 @@ module.exports = {
|
|||
id: clientTokenObj.id,
|
||||
name: clientTokenObj.name,
|
||||
lastAccessTime: clientTokenObj.createdAt,
|
||||
scope: new Set()
|
||||
scope: ScopeSet.fromArray([])
|
||||
};
|
||||
}
|
||||
|
||||
scope.forEach(function (clientScope) {
|
||||
// aggregate the scopes from all available tokens
|
||||
activeClients[clientIdHex].scope.add(clientScope);
|
||||
});
|
||||
activeClients[clientIdHex].scope.add(clientTokenObj.scope);
|
||||
|
||||
var clientTokenTime = clientTokenObj.createdAt;
|
||||
if (clientTokenTime > activeClients[clientIdHex].lastAccessTime) {
|
||||
|
@ -58,8 +55,7 @@ module.exports = {
|
|||
|
||||
// Sort the scopes alphabetically, convert the Object structure to an array
|
||||
var activeClientsArray = Object.keys(activeClients).map(function (key) {
|
||||
var scopes = activeClients[key].scope;
|
||||
activeClients[key].scope = Array.from(scopes.values()).sort();
|
||||
activeClients[key].scope = activeClients[key].scope.getScopeValues().sort();
|
||||
return activeClients[key];
|
||||
});
|
||||
|
||||
|
|
|
@ -282,7 +282,7 @@ MemoryStore.prototype = {
|
|||
id: this.tokens[id].clientId,
|
||||
createdAt: this.tokens[id].createdAt,
|
||||
name: client.name,
|
||||
scope: String(this.tokens[id].scope)
|
||||
scope: this.tokens[id].scope
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ const config = require('../../config');
|
|||
const encrypt = require('../../encrypt');
|
||||
const helpers = require('../helpers');
|
||||
const P = require('../../promise');
|
||||
const Scope = require('../../scope');
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
const unique = require('../../unique');
|
||||
const patch = require('./patch');
|
||||
|
||||
|
@ -386,7 +386,7 @@ MysqlStore.prototype = {
|
|||
codeObj.clientId,
|
||||
codeObj.userId,
|
||||
codeObj.email,
|
||||
codeObj.scope.join(' '),
|
||||
codeObj.scope.toString(),
|
||||
codeObj.authAt,
|
||||
codeObj.amr ? codeObj.amr.join(',') : null,
|
||||
codeObj.aal || null,
|
||||
|
@ -403,7 +403,7 @@ MysqlStore.prototype = {
|
|||
var hash = encrypt.hash(code);
|
||||
return this._readOne(QUERY_CODE_FIND, [hash]).then(function(code) {
|
||||
if (code) {
|
||||
code.scope = code.scope.split(' ');
|
||||
code.scope = ScopeSet.fromString(code.scope);
|
||||
if (code.amr !== null) {
|
||||
code.amr = code.amr.split(',');
|
||||
}
|
||||
|
@ -420,7 +420,7 @@ MysqlStore.prototype = {
|
|||
clientId: buf(vals.clientId),
|
||||
userId: buf(vals.userId),
|
||||
email: vals.email,
|
||||
scope: Scope(vals.scope),
|
||||
scope: vals.scope,
|
||||
token: unique.token(),
|
||||
type: 'bearer',
|
||||
expiresAt: vals.expiresAt || new Date(Date.now() + (vals.ttl * 1000 || MAX_TTL))
|
||||
|
@ -446,7 +446,7 @@ MysqlStore.prototype = {
|
|||
getAccessToken: function getAccessToken(id) {
|
||||
return this._readOne(QUERY_ACCESS_TOKEN_FIND, [buf(id)]).then(function(t) {
|
||||
if (t) {
|
||||
t.scope = t.scope.split(' ');
|
||||
t.scope = ScopeSet.fromString(t.scope);
|
||||
}
|
||||
return t;
|
||||
});
|
||||
|
@ -470,6 +470,9 @@ MysqlStore.prototype = {
|
|||
return this._read(QUERY_ACTIVE_CLIENT_TOKENS_BY_UID, [
|
||||
buf(uid)
|
||||
]).then(function(activeClientTokens) {
|
||||
activeClientTokens.forEach(t => {
|
||||
t.scope = ScopeSet.fromString(t.scope);
|
||||
});
|
||||
return helpers.aggregateActiveClients(activeClientTokens);
|
||||
});
|
||||
},
|
||||
|
@ -509,7 +512,7 @@ MysqlStore.prototype = {
|
|||
clientId: vals.clientId,
|
||||
userId: vals.userId,
|
||||
email: vals.email,
|
||||
scope: Scope(vals.scope)
|
||||
scope: vals.scope
|
||||
};
|
||||
var token = unique.token();
|
||||
var hash = encrypt.hash(token);
|
||||
|
@ -529,7 +532,7 @@ MysqlStore.prototype = {
|
|||
return this._readOne(QUERY_REFRESH_TOKEN_FIND, [buf(token)])
|
||||
.then(function(t) {
|
||||
if (t) {
|
||||
t.scope = t.scope.split(' ');
|
||||
t.scope = ScopeSet.fromString(t.scope);
|
||||
}
|
||||
return t;
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ const config = require('../config');
|
|||
const db = require('../db');
|
||||
const logger = require('../logging')('routes.authorization');
|
||||
const P = require('../promise');
|
||||
const Scope = require('../scope');
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
const validators = require('../validators');
|
||||
const verify = require('../browserid');
|
||||
|
||||
|
@ -27,12 +27,12 @@ const PKCE_CODE_CHALLENGE_LENGTH = 43;
|
|||
|
||||
const MAX_TTL_S = config.get('expiration.accessToken') / 1000;
|
||||
|
||||
const UNTRUSTED_CLIENT_ALLOWED_SCOPES = [
|
||||
const UNTRUSTED_CLIENT_ALLOWED_SCOPES = ScopeSet.fromArray([
|
||||
'openid',
|
||||
'profile:uid',
|
||||
'profile:email',
|
||||
'profile:display_name'
|
||||
];
|
||||
]);
|
||||
|
||||
const allowHttpRedirects = config.get('allowHttpRedirects');
|
||||
|
||||
|
@ -50,18 +50,6 @@ function isLocalHost(url) {
|
|||
return host === 'localhost' || host === '127.0.0.1';
|
||||
}
|
||||
|
||||
function detectInvalidScopes(requestedScopes, validScopes) {
|
||||
var invalidScopes = [];
|
||||
|
||||
requestedScopes.forEach(function(scope) {
|
||||
if (validScopes.indexOf(scope) === -1) {
|
||||
invalidScopes.push(scope);
|
||||
}
|
||||
});
|
||||
|
||||
return invalidScopes;
|
||||
}
|
||||
|
||||
function generateCode(claims, client, scope, req) {
|
||||
return db.generateCode({
|
||||
clientId: client.id,
|
||||
|
@ -113,7 +101,7 @@ function generateGrant(claims, client, scope, req) {
|
|||
access_token: hex(token.token),
|
||||
token_type: 'bearer',
|
||||
expires_in: Math.floor((token.expiresAt - Date.now()) / 1000),
|
||||
scope: scope.join(' '),
|
||||
scope: scope.toString(),
|
||||
auth_at: claims['fxa-lastAuthAt']
|
||||
};
|
||||
});
|
||||
|
@ -230,7 +218,7 @@ module.exports = {
|
|||
var start = Date.now();
|
||||
var wantsGrant = req.payload.response_type === TOKEN;
|
||||
var exitEarly = false;
|
||||
var scope = Scope(req.payload.scope || []);
|
||||
var scope = ScopeSet.fromString(req.payload.scope || '');
|
||||
P.all([
|
||||
verify(req.payload.assertion).then(function(claims) {
|
||||
logger.info('time.browserid_verify', { ms: Date.now() - start });
|
||||
|
@ -241,7 +229,7 @@ module.exports = {
|
|||
// Any request for a key-bearing scope should be using a verified token.
|
||||
// Double-check that here as a defense-in-depth measure.
|
||||
if (! claims['fxa-tokenVerified']) {
|
||||
return P.each(scope.values(), scope => {
|
||||
return P.each(scope.getScopeValues(), scope => {
|
||||
// Don't bother hitting the DB if other checks have failed.
|
||||
if (exitEarly) {
|
||||
return;
|
||||
|
@ -272,11 +260,9 @@ module.exports = {
|
|||
logger.debug('notFound', { id: req.payload.client_id });
|
||||
throw AppError.unknownClient(req.payload.client_id);
|
||||
} else if (! client.trusted) {
|
||||
var invalidScopes = detectInvalidScopes(scope.values(),
|
||||
UNTRUSTED_CLIENT_ALLOWED_SCOPES);
|
||||
|
||||
if (invalidScopes.length) {
|
||||
throw AppError.invalidScopes(invalidScopes);
|
||||
var invalidScopes = scope.difference(UNTRUSTED_CLIENT_ALLOWED_SCOPES);
|
||||
if (! invalidScopes.isEmpty()) {
|
||||
throw AppError.invalidScopes(invalidScopes.getScopeValues());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -312,7 +298,7 @@ module.exports = {
|
|||
exitEarly = true;
|
||||
throw err;
|
||||
}),
|
||||
scope.values(),
|
||||
scope,
|
||||
req
|
||||
])
|
||||
.spread(wantsGrant ? generateGrant : generateCode)
|
||||
|
|
|
@ -8,7 +8,7 @@ const SCOPE_CLIENT_WRITE = require('../../auth_bearer').SCOPE_CLIENT_WRITE;
|
|||
module.exports = {
|
||||
auth: {
|
||||
strategy: 'authBearer',
|
||||
scope: [SCOPE_CLIENT_WRITE]
|
||||
scope: SCOPE_CLIENT_WRITE.getImplicantValues()
|
||||
},
|
||||
handler: function activeServices(req, reply) {
|
||||
var clientId = req.params.client_id;
|
||||
|
|
|
@ -27,7 +27,7 @@ function serialize(client, acceptLanguage) {
|
|||
module.exports = {
|
||||
auth: {
|
||||
strategy: 'authBearer',
|
||||
scope: [SCOPE_CLIENT_WRITE]
|
||||
scope: SCOPE_CLIENT_WRITE.getImplicantValues()
|
||||
},
|
||||
handler: function activeServices(req, reply) {
|
||||
return db.getActiveClientsByUid(req.auth.credentials.user)
|
||||
|
|
|
@ -10,7 +10,7 @@ const AppError = require('../../error');
|
|||
module.exports = {
|
||||
auth: {
|
||||
strategy: auth.AUTH_STRATEGY,
|
||||
scope: [auth.SCOPE_CLIENT_MANAGEMENT]
|
||||
scope: auth.SCOPE_CLIENT_MANAGEMENT.getImplicantValues()
|
||||
},
|
||||
validate: {
|
||||
params: {
|
||||
|
|
|
@ -23,7 +23,7 @@ function serialize(client) {
|
|||
module.exports = {
|
||||
auth: {
|
||||
strategy: auth.AUTH_STRATEGY,
|
||||
scope: [auth.SCOPE_CLIENT_MANAGEMENT]
|
||||
scope: auth.SCOPE_CLIENT_MANAGEMENT.getImplicantValues()
|
||||
},
|
||||
response: {
|
||||
schema: {
|
||||
|
|
|
@ -15,7 +15,7 @@ const AppError = require('../../error');
|
|||
module.exports = {
|
||||
auth: {
|
||||
strategy: auth.AUTH_STRATEGY,
|
||||
scope: [auth.SCOPE_CLIENT_MANAGEMENT]
|
||||
scope: auth.SCOPE_CLIENT_MANAGEMENT.getImplicantValues()
|
||||
},
|
||||
validate: {
|
||||
payload: {
|
||||
|
|
|
@ -13,7 +13,7 @@ const AppError = require('../../error');
|
|||
module.exports = {
|
||||
auth: {
|
||||
strategy: auth.AUTH_STRATEGY,
|
||||
scope: [auth.SCOPE_CLIENT_MANAGEMENT]
|
||||
scope: auth.SCOPE_CLIENT_MANAGEMENT.getImplicantValues()
|
||||
},
|
||||
validate: {
|
||||
params: {
|
||||
|
|
|
@ -17,7 +17,7 @@ function developerResponse(developer) {
|
|||
module.exports = {
|
||||
auth: {
|
||||
strategy: auth.AUTH_STRATEGY,
|
||||
scope: [auth.SCOPE_CLIENT_MANAGEMENT]
|
||||
scope: auth.SCOPE_CLIENT_MANAGEMENT.getImplicantValues()
|
||||
},
|
||||
handler: function activateRegistration(req, reply) {
|
||||
var email = req.auth.credentials.email;
|
||||
|
|
|
@ -10,7 +10,7 @@ const logger = require('../logging')('routes.key_data');
|
|||
const P = require('../promise');
|
||||
const validators = require('../validators');
|
||||
const verify = require('../browserid');
|
||||
const Scope = require('../scope');
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
const config = require('../config');
|
||||
|
||||
const AUTH_EXPIRES_AFTER_MS = config.get('expiration.keyDataAuth');
|
||||
|
@ -44,23 +44,17 @@ module.exports = {
|
|||
payload: req.payload
|
||||
});
|
||||
|
||||
const requestedScopes = Scope(req.payload.scope);
|
||||
const requestedScopes = ScopeSet.fromString(req.payload.scope);
|
||||
const requestedClientId = req.payload.client_id;
|
||||
|
||||
P.all([
|
||||
verify(req.payload.assertion),
|
||||
db.getClient(Buffer.from(requestedClientId, 'hex')).then((client) => {
|
||||
if (client) {
|
||||
// find all requested scopes in allowed scopes
|
||||
const scopeRequests = [];
|
||||
const allowedScopes = Scope(client.allowedScopes);
|
||||
requestedScopes.values().forEach((s) => {
|
||||
if (allowedScopes.has(s)) {
|
||||
scopeRequests.push(db.getScope(s));
|
||||
}
|
||||
});
|
||||
|
||||
return P.all(scopeRequests).then((result) => {
|
||||
// find all requested scopes that are allowed for this client.
|
||||
const allowedScopes = ScopeSet.fromString(client.allowedScopes);
|
||||
const scopeLookups = requestedScopes.filtered(allowedScopes).getScopeValues().map(s => db.getScope(s));
|
||||
return P.all(scopeLookups).then((result) => {
|
||||
return result.filter((s) => !! s.hasScopedKeys);
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -11,13 +11,13 @@ const buf = require('buf').hex;
|
|||
const hex = require('buf').to.hex;
|
||||
const Joi = require('joi');
|
||||
const JwTool = require('fxa-jwtool');
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
|
||||
const config = require('../config');
|
||||
const db = require('../db');
|
||||
const encrypt = require('../encrypt');
|
||||
const logger = require('../logging')('routes.token');
|
||||
const P = require('../promise');
|
||||
const Scope = require('../scope');
|
||||
const util = require('../util');
|
||||
const validators = require('../validators');
|
||||
|
||||
|
@ -36,7 +36,7 @@ const SERVICE_JWTOOL = new JwTool(config.get('serviceClients').map(function(clie
|
|||
return client.jku;
|
||||
}));
|
||||
|
||||
const SCOPE_OPENID = 'openid';
|
||||
const SCOPE_OPENID = ScopeSet.fromArray(['openid']);
|
||||
|
||||
const ID_TOKEN_EXPIRATION = Math.floor(config.get('openid.ttl') / 1000);
|
||||
const ID_TOKEN_ISSUER = config.get('openid.issuer');
|
||||
|
@ -173,6 +173,7 @@ module.exports = {
|
|||
},
|
||||
handler: function tokenEndpoint(req, reply) {
|
||||
var params = req.payload;
|
||||
params.scope = ScopeSet.fromString(params.scope || '');
|
||||
P.try(function() {
|
||||
|
||||
// Clients are allowed to provide credentials in either
|
||||
|
@ -201,8 +202,8 @@ module.exports = {
|
|||
}
|
||||
});
|
||||
} else if (params.grant_type === GRANT_REFRESH_TOKEN) {
|
||||
// If the client has a client_secret, check that it's provided and valid in the refresh request
|
||||
// If the client does not have client_secret, check that one was not provided in the refresh request
|
||||
// If the client has a client_secret, check that it's provided and valid in the refresh request.
|
||||
// If the client does not have client_secret, check that one was not provided in the refresh request.
|
||||
return getClientById(clientId).then(function(client) {
|
||||
var confirmClientPromise;
|
||||
|
||||
|
@ -231,7 +232,7 @@ module.exports = {
|
|||
})
|
||||
.then(function(vals) {
|
||||
vals.ttl = params.ttl;
|
||||
if (vals.scope && Scope(vals.scope).has(SCOPE_OPENID)) {
|
||||
if (vals.scope && vals.scope.contains(SCOPE_OPENID)) {
|
||||
vals.idToken = true;
|
||||
}
|
||||
return vals;
|
||||
|
@ -356,7 +357,7 @@ function confirmRefreshToken(params) {
|
|||
code: tokObj.clientId
|
||||
});
|
||||
throw AppError.invalidToken();
|
||||
} else if (! Scope(tokObj.scope).has(params.scope)) {
|
||||
} else if (! tokObj.scope.contains(params.scope)) {
|
||||
logger.debug('refresh_token.invalidScopes', {
|
||||
allowed: tokObj.scope,
|
||||
requested: params.scope
|
||||
|
@ -402,7 +403,8 @@ function confirmJwt(params) {
|
|||
throw AppError.invalidAssertion();
|
||||
}
|
||||
|
||||
if (! Scope(client.scope).has(payload.scope)) {
|
||||
const requestedScope = ScopeSet.fromString(payload.scope);
|
||||
if (! ScopeSet.fromString(client.scope).contains(requestedScope)) {
|
||||
logger.debug('jwt.invalid.scopes', {
|
||||
allowed: client.scope,
|
||||
requested: payload.scope
|
||||
|
@ -427,7 +429,7 @@ function confirmJwt(params) {
|
|||
return {
|
||||
clientId: client.id,
|
||||
userId: uid,
|
||||
scope: payload.scope,
|
||||
scope: requestedScope,
|
||||
email: ''
|
||||
};
|
||||
});
|
||||
|
@ -485,7 +487,7 @@ function generateTokens(options) {
|
|||
var json = {
|
||||
access_token: access.token.toString('hex'),
|
||||
token_type: access.type,
|
||||
scope: Scope(access.scope).toString()
|
||||
scope: access.scope.toString()
|
||||
};
|
||||
if (options.authAt) {
|
||||
json.auth_at = options.authAt;
|
||||
|
|
|
@ -25,6 +25,7 @@ module.exports = {
|
|||
},
|
||||
handler: function verify(req, reply) {
|
||||
token.verify(req.payload.token).then(function(info) {
|
||||
info.scope = info.scope.getScopeValues();
|
||||
if (req.payload.email !== undefined) {
|
||||
logger.warn('email.requested', {
|
||||
user: info.user,
|
||||
|
|
68
lib/scope.js
68
lib/scope.js
|
@ -1,68 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
|
||||
function Scope(arr) {
|
||||
if (arr instanceof Scope) {
|
||||
return arr;
|
||||
} else if (! (this instanceof Scope)) {
|
||||
return new Scope(arr);
|
||||
}
|
||||
if (! arr) {
|
||||
arr = [];
|
||||
} else if (typeof arr === 'string') {
|
||||
arr = arr.split(/\s+/);
|
||||
}
|
||||
var obj = {};
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
obj[arr[i]] = true;
|
||||
}
|
||||
this._values = obj;
|
||||
}
|
||||
|
||||
Scope.prototype = {
|
||||
|
||||
_values: undefined,
|
||||
|
||||
has: function has(scope) {
|
||||
return Scope(scope).values().every(function(word) {
|
||||
if (! word || word.lastIndexOf(':') === word.length - 1) {
|
||||
return false;
|
||||
} else if (word in this._values || word + ':write' in this._values) {
|
||||
return true;
|
||||
} else {
|
||||
var parts = word.split(':');
|
||||
var suffix = parts.pop();
|
||||
if (suffix === 'write') {
|
||||
// pop the next one off
|
||||
// but still require this to be a 'write' scope
|
||||
if (parts.pop()) {
|
||||
parts.push('write');
|
||||
} else {
|
||||
// this was a weird scope. don't try to fix it, just say NO!
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var prefix = parts.join(':');
|
||||
return prefix && this.has(prefix);
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
values: function values() {
|
||||
return Object.keys(this._values);
|
||||
},
|
||||
|
||||
toString: function toString() {
|
||||
return this.values().join(' ');
|
||||
},
|
||||
|
||||
toJSON: function toJSON() {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
module.exports = Scope;
|
11
lib/token.js
11
lib/token.js
|
@ -2,16 +2,18 @@
|
|||
* 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/. */
|
||||
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
|
||||
const AppError = require('./error');
|
||||
const auth = require('./auth');
|
||||
const config = require('./config');
|
||||
const db = require('./db');
|
||||
const encrypt = require('./encrypt');
|
||||
const Scope = require('./scope');
|
||||
const logger = require('./logging')('token');
|
||||
|
||||
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const SCOPES_REQUIRING_EMAIL = ScopeSet.fromArray(['profile:email', 'oauth']);
|
||||
|
||||
exports.verify = function verify(token) {
|
||||
return db.getAccessToken(encrypt.hash(token))
|
||||
.then(function(token) {
|
||||
|
@ -33,7 +35,7 @@ exports.verify = function verify(token) {
|
|||
logger.warn('token.verify.expired', {
|
||||
user: token.userId.toString('hex'),
|
||||
client_id: token.clientId.toString('hex'),
|
||||
scope: token.scope,
|
||||
scope: token.scope.toString(),
|
||||
created_at: token.createdAt,
|
||||
expires_at: token.expiresAt
|
||||
});
|
||||
|
@ -55,8 +57,7 @@ exports.verify = function verify(token) {
|
|||
scope: token.scope
|
||||
};
|
||||
|
||||
var scope = Scope(token.scope);
|
||||
if (scope.has('profile:email') || scope.has(auth.SCOPE_CLIENT_MANAGEMENT)) {
|
||||
if (token.scope.intersects(SCOPES_REQUIRING_EMAIL)) {
|
||||
tokenInfo.email = token.email;
|
||||
}
|
||||
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
44
package.json
44
package.json
|
@ -24,44 +24,44 @@
|
|||
"node": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": "^2.9.14",
|
||||
"bluebird": "2.11.0",
|
||||
"buf": "0.1.0",
|
||||
"commander": "^2.9.0",
|
||||
"commander": "2.15.1",
|
||||
"convict": "4.0.2",
|
||||
"fxa-jwtool": "^0.7.1",
|
||||
"fxa-jwtool": "0.7.2",
|
||||
"fxa-notifier-aws": "1.0.0",
|
||||
"fxa-shared": "1.0.5",
|
||||
"fxa-shared": "1.0.13",
|
||||
"hapi": "16.6.3",
|
||||
"hapi-hpkp": "1.0.0",
|
||||
"joi": "^10.0.1",
|
||||
"keypair": "^1.0.1",
|
||||
"mozlog": "^2.0.3",
|
||||
"mysql": "^2.5.5",
|
||||
"mysql-patcher": "^0.7.0",
|
||||
"joi": "10.6.0",
|
||||
"keypair": "1.0.1",
|
||||
"mozlog": "2.2.0",
|
||||
"mysql": "2.15.0",
|
||||
"mysql-patcher": "0.7.0",
|
||||
"newrelic": "2.2.2",
|
||||
"raven": "2.2.1",
|
||||
"request": "^2.83.0",
|
||||
"urijs": "^1.16.1"
|
||||
"request": "2.85.0",
|
||||
"urijs": "1.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-fxa": "git://github.com/mozilla/eslint-plugin-fxa.git#41504c9dd30e8b52900c15b524946aa0428aef95",
|
||||
"fxa-conventional-changelog": "1.1.0",
|
||||
"grunt": "^1.0.1",
|
||||
"grunt": "1.0.2",
|
||||
"grunt-bump": "0.8.0",
|
||||
"grunt-conventional-changelog": "6.1.0",
|
||||
"grunt-copyright": "^0.3.0",
|
||||
"grunt-copyright": "0.3.0",
|
||||
"grunt-eslint": "18.0.0",
|
||||
"grunt-nodemon": "^0.4.0",
|
||||
"grunt-nodemon": "0.4.2",
|
||||
"grunt-nsp": "2.3.1",
|
||||
"insist": "1.x",
|
||||
"load-grunt-tasks": "^3.1.0",
|
||||
"mocha": "^3.4.2",
|
||||
"nock": "^8.0.0",
|
||||
"npmshrink": "^1.0.1",
|
||||
"load-grunt-tasks": "3.5.2",
|
||||
"mocha": "3.5.3",
|
||||
"nock": "8.2.2",
|
||||
"npmshrink": "1.0.1",
|
||||
"nyc": "11.0.2",
|
||||
"proxyquire": "^1.6.0",
|
||||
"read": "^1.0.5",
|
||||
"sinon": "^1.15.4",
|
||||
"time-grunt": "^1.1.0"
|
||||
"proxyquire": "1.8.0",
|
||||
"read": "1.0.7",
|
||||
"sinon": "1.17.7",
|
||||
"time-grunt": "1.4.0"
|
||||
}
|
||||
}
|
||||
|
|
37
test/api.js
37
test/api.js
|
@ -8,6 +8,7 @@ const nock = require('nock');
|
|||
const buf = require('buf').hex;
|
||||
const generateRSAKeypair = require('keypair');
|
||||
const JWTool = require('fxa-jwtool');
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
|
||||
const auth = require('../lib/auth');
|
||||
const config = require('../lib/config');
|
||||
|
@ -178,7 +179,7 @@ function getUniqueUserAndToken(cId, options) {
|
|||
clientId: buf(cId),
|
||||
userId: buf(uid),
|
||||
email: email,
|
||||
scope: options.scopes || [auth.SCOPE_CLIENT_MANAGEMENT]
|
||||
scope: options.scopes ? ScopeSet.fromArray(options.scopes) : auth.SCOPE_CLIENT_MANAGEMENT
|
||||
}).then(function (token) {
|
||||
return {
|
||||
uid: uid,
|
||||
|
@ -358,15 +359,32 @@ describe('/v1', function() {
|
|||
url: '/authorization',
|
||||
payload: authParams({
|
||||
client_id: client.id,
|
||||
scope: 'profile profile:write profile:uid'
|
||||
scope: 'profile:write'
|
||||
})
|
||||
}).then(function(res) {
|
||||
assert.equal(res.statusCode, 400);
|
||||
assertSecurityHeaders(res);
|
||||
assert.equal(res.result.errno, 114);
|
||||
assert.ok(res.result.invalidScopes.indexOf('profile') !== -1);
|
||||
assert.ok(res.result.invalidScopes.indexOf('profile:write') !== -1);
|
||||
assert.ok(res.result.invalidScopes.indexOf('profile:uid') === -1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report all invalid scopes', function() {
|
||||
var client = clientByName('Untrusted');
|
||||
mockAssertion().reply(200, VERIFY_GOOD);
|
||||
return Server.api.post({
|
||||
url: '/authorization',
|
||||
payload: authParams({
|
||||
client_id: client.id,
|
||||
scope: 'profile:email profile:locale profile:amr'
|
||||
})
|
||||
}).then(function(res) {
|
||||
assert.equal(res.statusCode, 400);
|
||||
assertSecurityHeaders(res);
|
||||
assert.equal(res.result.errno, 114);
|
||||
assert.ok(res.result.invalidScopes.indexOf('profile:email') === -1);
|
||||
assert.ok(res.result.invalidScopes.indexOf('profile:locale') !== -1);
|
||||
assert.ok(res.result.invalidScopes.indexOf('profile:amr') !== -1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2069,14 +2087,14 @@ describe('/v1', function() {
|
|||
clientId: buf(clientId),
|
||||
userId: buf(USERID),
|
||||
email: VEMAIL,
|
||||
scope: [auth.SCOPE_CLIENT_MANAGEMENT]
|
||||
scope: auth.SCOPE_CLIENT_MANAGEMENT
|
||||
}).then(function(token) {
|
||||
tok = token.token.toString('hex');
|
||||
return db.generateAccessToken({
|
||||
clientId: buf(clientId),
|
||||
userId: unique(16),
|
||||
email: 'user@not.allow.ed',
|
||||
scope: [auth.SCOPE_CLIENT_MANAGEMENT]
|
||||
scope: auth.SCOPE_CLIENT_MANAGEMENT
|
||||
});
|
||||
}).then(function(token) {
|
||||
badTok = token.token.toString('hex');
|
||||
|
@ -3068,6 +3086,7 @@ describe('/v1', function() {
|
|||
});
|
||||
})
|
||||
.then(function (res) {
|
||||
assert.equal(res.statusCode, 200);
|
||||
// The API sorts the results by createdAt and then by name
|
||||
// The precision is one second, this test guarantees that if
|
||||
// the tokens were created in the same second, they will still be sorted by name.
|
||||
|
@ -3185,7 +3204,7 @@ describe('/v1', function() {
|
|||
assert.equal(result[0].id, client1Id.toString('hex'));
|
||||
assert.equal(result[0].lastAccessTime, tok.createdAt.getTime(), 'lastAccessTime should be equal to the latest Token createdAt time');
|
||||
assertSecurityHeaders(res);
|
||||
assert.deepEqual(result[0].scope, ['clients:write', 'profile', 'profile:write']);
|
||||
assert.deepEqual(result[0].scope, ['clients:write', 'profile:write']);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -3206,7 +3225,7 @@ describe('/v1', function() {
|
|||
return getUniqueUserAndToken(client1Id.toString('hex'), {
|
||||
uid: user1.uid,
|
||||
email: user1.email,
|
||||
scopes: ['basket:write', 'profile:email']
|
||||
scopes: ['basket', 'profile:email']
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
|
@ -3226,7 +3245,7 @@ describe('/v1', function() {
|
|||
})
|
||||
.then(function (res) {
|
||||
var result = res.result;
|
||||
assert.deepEqual(result[0].scope, ['basket:write', 'clients:write', 'profile', 'profile:email', 'profile:uid', 'profile:write']);
|
||||
assert.deepEqual(result[0].scope, ['basket', 'clients:write', 'profile:write']);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ const proxyquire = require('proxyquire');
|
|||
const AppError = require('../lib/error');
|
||||
const P = require('../lib/promise');
|
||||
const sinon = require('sinon');
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
|
||||
const modulePath = '../lib/auth_bearer';
|
||||
const mockRequest = {
|
||||
|
@ -30,7 +31,7 @@ describe('authBearer', function() {
|
|||
|
||||
sandbox.stub(dependencies['../../../lib/token'], 'verify', function() {
|
||||
return P.resolve({
|
||||
scope: ['bar:foo', 'clients:write'],
|
||||
scope: ScopeSet.fromArray(['bar:foo', 'clients:write']),
|
||||
user: 'bar'
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const assert = require('insist');
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
const helpers = require('../../lib/db/helpers');
|
||||
const unique = require('../../lib/unique');
|
||||
|
||||
|
@ -17,19 +18,19 @@ describe('aggregateActiveClients', function() {
|
|||
id: uid,
|
||||
createdAt: '2017-01-26T14:28:16.219Z',
|
||||
name: '123Done',
|
||||
scope: ['profile', 'profile:write']
|
||||
scope: ScopeSet.fromArray(['basket', 'profile:write'])
|
||||
},
|
||||
{
|
||||
id: uid,
|
||||
createdAt: '2017-01-27T14:28:16.219Z',
|
||||
name: '123Done',
|
||||
scope: ['clients:write']
|
||||
scope: ScopeSet.fromArray(['clients:write'])
|
||||
},
|
||||
{
|
||||
id: uid,
|
||||
createdAt: '2017-01-28T14:28:16.219Z',
|
||||
name: '123Done',
|
||||
scope: ['profile']
|
||||
scope: ScopeSet.fromArray(['profile'])
|
||||
}
|
||||
];
|
||||
});
|
||||
|
@ -38,7 +39,7 @@ describe('aggregateActiveClients', function() {
|
|||
var res = helpers.aggregateActiveClients(activeClientTokens);
|
||||
assert.equal(res[0].id, uid);
|
||||
assert.equal(res[0].name, '123Done');
|
||||
assert.deepEqual(res[0].scope, ['clients:write', 'profile', 'profile:write']);
|
||||
assert.deepEqual(res[0].scope, ['basket', 'clients:write', 'profile:write']);
|
||||
assert.equal(res[0].lastAccessTime, '2017-01-28T14:28:16.219Z');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ const crypto = require('crypto');
|
|||
const assert = require('insist');
|
||||
const buf = require('buf').hex;
|
||||
const hex = require('buf').to.hex;
|
||||
const ScopeSet = require('fxa-shared').oauth.scopes;
|
||||
|
||||
const encrypt = require('../../lib/encrypt');
|
||||
const db = require('../../lib/db');
|
||||
|
@ -130,7 +131,7 @@ describe('db', function() {
|
|||
var clientId = buf(randomString(8));
|
||||
var userId = buf(randomString(16));
|
||||
var email = 'a@b.c';
|
||||
var scope = ['no-scope'];
|
||||
var scope = ScopeSet.fromArray(['no_scope']);
|
||||
var code = null;
|
||||
var token = null;
|
||||
var refreshToken = null;
|
||||
|
@ -205,7 +206,7 @@ describe('db', function() {
|
|||
const clientId = buf(randomString(8));
|
||||
const userId = buf(randomString(16));
|
||||
const email = 'a@b' + randomString(16) + ' + .c';
|
||||
const scope = ['no-scope'];
|
||||
const scope = ['no_scope'];
|
||||
let tokenIdHash;
|
||||
let refreshTokenIdHash;
|
||||
|
||||
|
@ -297,7 +298,7 @@ describe('db', function() {
|
|||
var clientId = buf(randomString(8));
|
||||
var userId = buf(randomString(16));
|
||||
var email = 'a@b.c';
|
||||
var scope = ['no-scope'];
|
||||
var scope = ['no_scope'];
|
||||
var code = null;
|
||||
var refreshToken = null;
|
||||
|
||||
|
@ -524,7 +525,7 @@ describe('db', function() {
|
|||
var clientId = buf(randomString(8));
|
||||
var userId = buf(randomString(16));
|
||||
var email = 'a@b.c';
|
||||
var scope = ['no-scope'];
|
||||
var scope = ['no_scope'];
|
||||
var code = null;
|
||||
|
||||
before(function() {
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
/*global describe,it*/
|
||||
|
||||
const assert = require('insist');
|
||||
|
||||
const Scope = require('../lib/scope');
|
||||
|
||||
describe('Scope', function() {
|
||||
|
||||
describe('constructor', function() {
|
||||
|
||||
it('should accept a space-separated string', function() {
|
||||
var s1 = Scope('a b c');
|
||||
assert.deepEqual(s1.values(), ['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should accept an array', function() {
|
||||
var s1 = Scope(['a', 'b', 'c']);
|
||||
assert.deepEqual(s1.values(), ['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should accept a Scope instance', function() {
|
||||
var s1 = Scope(['a', 'b', 'c']);
|
||||
var s2 = Scope(s1);
|
||||
assert.equal(s1, s2);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('has', function() {
|
||||
|
||||
it('should work with a single value', function() {
|
||||
var s1 = Scope('foo bar');
|
||||
assert(s1.has('foo'));
|
||||
assert(s1.has('bar'));
|
||||
assert(! s1.has('baz'));
|
||||
});
|
||||
|
||||
it('should work with another Scope object', function() {
|
||||
var s1 = Scope('foo bar');
|
||||
var s2 = Scope('bar');
|
||||
assert(s1.has(s2));
|
||||
assert(! s2.has(s1));
|
||||
});
|
||||
|
||||
it('should allow sub-scopes', function() {
|
||||
var s1 = Scope('foo bar:baz');
|
||||
assert(s1.has('foo:dee'));
|
||||
assert(s1.has('bar:baz'));
|
||||
assert(s1.has('foo:mah:pa bar:baz:quux'));
|
||||
assert(! s1.has('bar'));
|
||||
|
||||
assert(! s1.has('foo:write'));
|
||||
assert(! s1.has('foo:dee:write'));
|
||||
|
||||
var s2 = Scope('foo bar baz:quux:write');
|
||||
assert(s2.has('foo bar baz:quux'));
|
||||
|
||||
assert(! s2.has('baz:write'));
|
||||
assert(! s2.has('foo bar baz'));
|
||||
|
||||
var s3 = Scope('foo:write');
|
||||
assert(s3.has('foo:bar'));
|
||||
assert(s3.has('foo:bar:write'));
|
||||
|
||||
assert(! s3.has('foo::write'));
|
||||
assert(! s3.has('foo:write:::'));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
Загрузка…
Ссылка в новой задаче