UNPKG

@quarks/quarks-iam

Version:

A modern authorization server built to authenticate your users and protect your APIs

375 lines (314 loc) 7.96 kB
/** * Module dependencies */ var async = require('async') var client = require('../boot/redis').getClient() var settings = require('../boot/settings') var Modinha = require('modinha') var Document = require('modinha-redis') var random = Modinha.defaults.random var AccessTokenJWT = require('../models/AccessTokenJWT') var InvalidTokenError = require('../errors/InvalidTokenError') var UnauthorizedError = require('../errors/UnauthorizedError') var InsufficientScopeError = require('../errors/InsufficientScopeError') var nowSeconds = require('../lib/time-utils').nowSeconds /** * Model definition */ var AccessToken = Modinha.define('accesstokens', { // access token at: { type: 'string', required: true, default: random(10), uniqueId: true }, // token type tt: { type: 'string', enum: ['Bearer', 'mac'], default: 'Bearer' }, // expires in ei: { type: 'number', default: 3600 }, // refresh token rt: { type: 'string', unique: true }, // issuer iss: { type: 'string' }, // client id cid: { type: 'string', required: true }, // user id uid: { type: 'string', required: true }, // scope scope: { type: 'string', required: true }, // jwt????? jwt: { type: 'string' } }) /** * Document persistence */ AccessToken.extend(Document) AccessToken.__client = client /** * Index the token to manage a list * of authorized clients for a user. */ AccessToken.defineIndex({ type: 'sorted', key: ['users:$:clients', 'uid'], score: 'created', member: 'cid' }) /** * Index a token by it's user/client pair. * This makes it possible to resuse an access token * instead of issuing a new one. */ AccessToken.defineIndex({ type: 'hash', key: 'user:client:token', field: ['$:$', 'uid', 'cid'], value: 'at' }) /** * */ AccessToken.exists = function (userId, clientId, callback) { var key = 'user:client:token' var field = userId + ':' + clientId this.__client.hget(key, field, function (err, id) { if (err) return callback(err) return callback(null, Boolean(id)) }) } /** * Mappings */ AccessToken.mappings.issue = { 'at': 'access_token', 'tt': 'token_type', 'ei': 'expires_in', 'rt': 'refresh_token' } AccessToken.mappings.exchange = { 'cid': 'client_id', 'uid': 'user_id', 'ei': 'max_age', 'scope': 'scope' } /** * Exchange authorization code for access token */ // TODO: Need to modify modinha-redis // insert method to merge options for // initialization so we can use the // mapping directly with insert. AccessToken.exchange = function (request, callback) { var token = AccessToken.initialize(request.code, { mapping: 'exchange' }) token.iss = settings.issuer token.rt = random(settings.refresh_token_bytes_range)() this.insert(token, function (err, token) { if (err) { return callback(err) } callback(null, token) }) } /** * Issue access token */ AccessToken.issue = function (request, callback) { if (!request.user || !request.client) { return callback(new Error('invalid_request')) } this.insert({ iss: settings.issuer, uid: request.user._id, cid: request.client._id, ei: ( request.connectParams && parseInt(request.connectParams.max_age, 10) ) || request.client.default_max_age, scope: request.scope }, function (err, token) { if (err) { return callback(err) } var response = token.project('issue') // Unless the client is set to issue a random token, // transform it to a signed JWT. if (request.client.access_token_type !== 'random') { response.access_token = token.toJWT(settings.keys.sig.prv) } callback(null, response) }) } /** * Refresh access token */ AccessToken.refresh = function (refreshToken, clientId, callback) { AccessToken.getByRt(refreshToken, function (err, at) { if (err) { return callback(err) } if (!at) { return callback(new InvalidTokenError('Unknown refresh token')) } if (at.cid !== clientId) { return callback(new InvalidTokenError('Mismatching client id')) } AccessToken.insert({ iss: settings.issuer, uid: at.uid, cid: at.cid, ei: at.ei, rt: random(settings.refresh_token_bytes_range)(), scope: at.scope }, function (err, token) { if (err) { return callback(err) } // we should destroy the current token AccessToken.delete(at.at, function (err) { if (err) { return callback(err) } callback(null, token) }) }) }) } /** * Revoke access token */ AccessToken.revoke = function (userId, clientId, callback) { var key = 'user:client:token' var field = userId + ':' + clientId this.__client.hget(key, field, function (err, id) { if (err) { return callback(err) } if (!id) { return callback(null, null) } AccessToken.delete(id, function (err, result) { if (err) { return callback(err) } callback(null, result) }) }) } /** * Verify access token */ AccessToken.verify = function (token, options, callback) { async.parallel({ // Handle JWT jwt: function (done) { // the token is a JWT if (token.indexOf('.') !== -1) { var decoded = AccessTokenJWT.decode(token, options.key) if (!decoded || decoded instanceof Error) { done(new UnauthorizedError({ realm: 'user', error: 'invalid_token', error_description: 'Invalid access token', statusCode: 401 })) } else { done(null, decoded) } // the token is not a JWT } else { done() } }, // Fetch from database random: function (done) { // the token is random if (token.indexOf('.') === -1) { AccessToken.get(token, function (err, instance) { if (err) { return done(err) } if (!instance) { return done(new UnauthorizedError({ realm: 'user', error: 'invalid_request', error_description: 'Unknown access token', statusCode: 401 })) } done(null, { jti: instance.at, iss: instance.iss, sub: instance.uid, aud: instance.cid, iat: instance.created, exp: instance.created + instance.ei, scope: instance.scope }) }) // the token is not random } else { done() } } }, function (err, result) { if (err) { return callback(err) } var claims = result.random || result.jwt.payload var issuer = options.iss var scope = options.scope // mismatching issuer if (claims.iss !== issuer) { return callback(new UnauthorizedError({ error: 'invalid_token', error_description: 'Mismatching issuer', statusCode: 403 })) } // expired token if (nowSeconds() > claims.exp) { return callback(new UnauthorizedError({ realm: 'user', error: 'invalid_token', error_description: 'Expired access token', statusCode: 403 })) } // insufficient scope if (scope && claims.scope.indexOf(scope) === -1) { return callback(new UnauthorizedError({ realm: 'user', error: 'insufficient_scope', error_description: 'Insufficient scope', statusCode: 403 })) } callback(null, claims) }) } /** * Convert an AccessToken instance into a JWT */ AccessToken.prototype.toJWT = function (secret) { var jwt = new AccessTokenJWT(this) jwt.payload.exp = nowSeconds(this.ei) return jwt.encode(secret) } /** * Errors */ AccessToken.InvalidTokenError = InvalidTokenError AccessToken.InsufficientScopeError = InsufficientScopeError /** * Exports */ module.exports = AccessToken