add jwt model, logger wrapper, prettier formatting
This commit is contained in:
parent
0f726741f9
commit
f8640e40d1
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80
|
||||||
|
}
|
23
package-lock.json
generated
23
package-lock.json
generated
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@icynet/oauth2",
|
"name": "@icynet/oauth2-provider",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@icynet/oauth2",
|
"name": "@icynet/oauth2-provider",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -16,6 +16,7 @@
|
|||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/express-session": "^1.17.4",
|
"@types/express-session": "^1.17.4",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
"typescript": "^4.5.5"
|
"typescript": "^4.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -470,6 +471,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin-prettier.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@ -1038,6 +1051,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||||
},
|
},
|
||||||
|
"prettier": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"proxy-addr": {
|
"proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/express-session": "^1.17.4",
|
"@types/express-session": "^1.17.4",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
"typescript": "^4.5.5"
|
"typescript": "^4.5.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -24,30 +24,38 @@ export const authorization = wrap(async (req, res) => {
|
|||||||
const { oauth2 } = req;
|
const { oauth2 } = req;
|
||||||
|
|
||||||
if (!req.query.redirect_uri) {
|
if (!req.query.redirect_uri) {
|
||||||
throw new InvalidRequest('redirect_uri field is mandatory for authorization endpoint');
|
throw new InvalidRequest(
|
||||||
|
'redirect_uri field is mandatory for authorization endpoint'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectUri = req.query.redirect_uri as string;
|
redirectUri = req.query.redirect_uri as string;
|
||||||
console.debug('Parameter redirect uri is', redirectUri);
|
req.oauth2.logger.debug('Parameter redirect uri is', redirectUri);
|
||||||
|
|
||||||
if (!req.query.client_id) {
|
if (!req.query.client_id) {
|
||||||
throw new InvalidRequest('client_id field is mandatory for authorization endpoint');
|
throw new InvalidRequest(
|
||||||
|
'client_id field is mandatory for authorization endpoint'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for client_secret (prevent passing it)
|
// Check for client_secret (prevent passing it)
|
||||||
if (req.query.client_secret) {
|
if (req.query.client_secret) {
|
||||||
throw new InvalidRequest('client_secret field should not be passed to the authorization endpoint');
|
throw new InvalidRequest(
|
||||||
|
'client_secret field should not be passed to the authorization endpoint'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
clientId = req.query.client_id as string;
|
clientId = req.query.client_id as string;
|
||||||
console.debug('Parameter client_id is', clientId);
|
req.oauth2.logger.debug('Parameter client_id is', clientId);
|
||||||
|
|
||||||
if (!req.query.response_type) {
|
if (!req.query.response_type) {
|
||||||
throw new InvalidRequest('response_type field is mandatory for authorization endpoint');
|
throw new InvalidRequest(
|
||||||
|
'response_type field is mandatory for authorization endpoint'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
responseType = req.query.response_type as string;
|
responseType = req.query.response_type as string;
|
||||||
console.debug('Parameter response_type is', responseType);
|
req.oauth2.logger.debug('Parameter response_type is', responseType);
|
||||||
|
|
||||||
// Support multiple types
|
// Support multiple types
|
||||||
const responseTypes = responseType.split(' ');
|
const responseTypes = responseType.split(' ');
|
||||||
@ -59,24 +67,32 @@ export const authorization = wrap(async (req, res) => {
|
|||||||
case 'token':
|
case 'token':
|
||||||
grantTypes.push('implicit');
|
grantTypes.push('implicit');
|
||||||
break;
|
break;
|
||||||
// case 'id_token':
|
case 'id_token':
|
||||||
|
grantTypes.push('id_token');
|
||||||
|
break;
|
||||||
case 'none':
|
case 'none':
|
||||||
grantTypes.push(responseTypes[i]);
|
grantTypes.push(responseTypes[i]);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedResponseType('Unknown response_type parameter passed');
|
throw new UnsupportedResponseType(
|
||||||
|
'Unknown response_type parameter passed'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out duplicates
|
// Filter out duplicates
|
||||||
grantTypes = grantTypes.filter((value, index, self) => self.indexOf(value) === index);
|
grantTypes = grantTypes.filter(
|
||||||
|
(value, index, self) => self.indexOf(value) === index
|
||||||
|
);
|
||||||
|
|
||||||
// "None" type cannot be combined with others
|
// "None" type cannot be combined with others
|
||||||
if (grantTypes.length > 1 && grantTypes.indexOf('none') !== -1) {
|
if (grantTypes.length > 1 && grantTypes.indexOf('none') !== -1) {
|
||||||
throw new InvalidRequest('Grant type "none" cannot be combined with other grant types');
|
throw new InvalidRequest(
|
||||||
|
'Grant type "none" cannot be combined with other grant types'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('Parameter grant_type is', grantTypes.join(' '));
|
req.oauth2.logger.debug('Parameter grant_type is', grantTypes.join(' '));
|
||||||
|
|
||||||
const client = await oauth2.model.client.fetchById(clientId);
|
const client = await oauth2.model.client.fetchById(clientId);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
@ -89,21 +105,26 @@ export const authorization = wrap(async (req, res) => {
|
|||||||
} else if (!oauth2.model.client.checkRedirectUri(client, redirectUri)) {
|
} else if (!oauth2.model.client.checkRedirectUri(client, redirectUri)) {
|
||||||
throw new InvalidRequest('Wrong RedirectUri provided');
|
throw new InvalidRequest('Wrong RedirectUri provided');
|
||||||
}
|
}
|
||||||
console.debug('redirect_uri check passed');
|
req.oauth2.logger.debug('redirect_uri check passed');
|
||||||
|
|
||||||
// The client needs to support all grant types
|
// The client needs to support all grant types
|
||||||
for (const grantType of grantTypes) {
|
for (const grantType of grantTypes) {
|
||||||
if (!oauth2.model.client.checkGrantType(client, grantType) && grantType !== 'none') {
|
if (
|
||||||
throw new UnauthorizedClient('This client does not support grant type ' + grantType);
|
!oauth2.model.client.checkGrantType(client, grantType) &&
|
||||||
|
grantType !== 'none'
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedClient(
|
||||||
|
'This client does not support grant type ' + grantType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.debug('Grant type check passed');
|
req.oauth2.logger.debug('Grant type check passed');
|
||||||
|
|
||||||
scope = oauth2.model.client.transformScope(req.query.scope as string);
|
scope = oauth2.model.client.transformScope(req.query.scope as string);
|
||||||
if (!oauth2.model.client.checkScope(client, scope)) {
|
if (!oauth2.model.client.checkScope(client, scope)) {
|
||||||
throw new InvalidScope('Client does not allow access to this scope');
|
throw new InvalidScope('Client does not allow access to this scope');
|
||||||
}
|
}
|
||||||
console.debug('Scope check passed');
|
req.oauth2.logger.debug('Scope check passed');
|
||||||
|
|
||||||
user = await oauth2.model.user.fetchFromRequest(req);
|
user = await oauth2.model.user.fetchFromRequest(req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -112,10 +133,10 @@ export const authorization = wrap(async (req, res) => {
|
|||||||
if (!user.username) {
|
if (!user.username) {
|
||||||
throw new AccessDenied(user.username);
|
throw new AccessDenied(user.username);
|
||||||
}
|
}
|
||||||
console.debug('User fetched from request')
|
req.oauth2.logger.debug('User fetched from request');
|
||||||
}
|
}
|
||||||
|
|
||||||
let resObj = {};
|
let resObj: Record<string, string | number> = {};
|
||||||
let consented = false;
|
let consented = false;
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
@ -128,24 +149,31 @@ export const authorization = wrap(async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Ask for consent
|
// Ask for consent
|
||||||
if (!consented) return oauth2.decision(req, res, client, scope, user, redirectUri);
|
if (!consented)
|
||||||
|
return oauth2.decision(req, res, client, scope, user, redirectUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consent pushed, ensure valid session
|
// Consent pushed, ensure valid session
|
||||||
const { session: { csrf } } = req;
|
const {
|
||||||
if (req.method === 'POST' && csrf && !(req.body.csrf && req.body.csrf === csrf)) {
|
session: { csrf },
|
||||||
|
} = req;
|
||||||
|
if (
|
||||||
|
req.method === 'POST' &&
|
||||||
|
csrf &&
|
||||||
|
!(req.body.csrf && req.body.csrf === csrf)
|
||||||
|
) {
|
||||||
throw new InvalidRequest('Invalid session');
|
throw new InvalidRequest('Invalid session');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save consent
|
// Save consent
|
||||||
if (!consented) {
|
if (!consented) {
|
||||||
if (!req.body || (typeof req.body.decision) === 'undefined') {
|
if (!req.body || typeof req.body.decision === 'undefined') {
|
||||||
throw new InvalidRequest('No decision parameter passed');
|
throw new InvalidRequest('No decision parameter passed');
|
||||||
} else if (req.body.decision === '0') {
|
} else if (req.body.decision === '0') {
|
||||||
throw new AccessDenied('User denied access to the resource');
|
throw new AccessDenied('User denied access to the resource');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('Decision check passed');
|
req.oauth2.logger.debug('Decision check passed');
|
||||||
|
|
||||||
await oauth2.model.user.consent(
|
await oauth2.model.user.consent(
|
||||||
oauth2.model.user.getId(user),
|
oauth2.model.user.getId(user),
|
||||||
@ -155,17 +183,17 @@ export const authorization = wrap(async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const i in grantTypes) {
|
for (const i in grantTypes) {
|
||||||
let data = null
|
let data = null;
|
||||||
switch (grantTypes[i]) {
|
switch (grantTypes[i]) {
|
||||||
case 'authorization_code':
|
case 'authorization_code':
|
||||||
data = await oauth2.model.code.create(
|
data = await oauth2.model.code.create(
|
||||||
oauth2.model.user.getId(user),
|
oauth2.model.user.getId(user),
|
||||||
oauth2.model.client.getId(client),
|
oauth2.model.client.getId(client),
|
||||||
scope,
|
scope,
|
||||||
oauth2.model.code.ttl,
|
oauth2.model.code.ttl
|
||||||
);
|
);
|
||||||
|
|
||||||
resObj = Object.assign({ code: data }, resObj);
|
resObj = { code: data, ...resObj };
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'implicit':
|
case 'implicit':
|
||||||
@ -176,18 +204,36 @@ export const authorization = wrap(async (req, res) => {
|
|||||||
oauth2.model.accessToken.ttl
|
oauth2.model.accessToken.ttl
|
||||||
);
|
);
|
||||||
|
|
||||||
resObj = Object.assign({
|
resObj = {
|
||||||
token_type: 'bearer',
|
token_type: 'bearer',
|
||||||
access_token: data,
|
access_token: data,
|
||||||
expires_in: oauth2.model.accessToken.ttl
|
expires_in: oauth2.model.accessToken.ttl,
|
||||||
}, resObj);
|
...resObj,
|
||||||
|
};
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case 'id_token':
|
||||||
|
if (!oauth2.model.jwt || !scope.includes('openid')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = await oauth2.model.jwt.issueIdToken(
|
||||||
|
user,
|
||||||
|
scope,
|
||||||
|
resObj.access_token as string | undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
resObj = {
|
||||||
|
id_token: data,
|
||||||
|
...resObj,
|
||||||
|
};
|
||||||
case 'none':
|
case 'none':
|
||||||
resObj = {};
|
resObj = {};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedResponseType('Unknown response_type parameter passed');
|
throw new UnsupportedResponseType(
|
||||||
|
'Unknown response_type parameter passed'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,11 @@ export const introspection = wrap(async function (req, res) {
|
|||||||
if (req.body.client_id && req.body.client_secret) {
|
if (req.body.client_id && req.body.client_secret) {
|
||||||
clientId = req.body.client_id as string;
|
clientId = req.body.client_id as string;
|
||||||
clientSecret = req.body.client_secret as string;
|
clientSecret = req.body.client_secret as string;
|
||||||
console.debug('Client credentials parsed from body parameters ', clientId, clientSecret);
|
req.oauth2.logger.debug(
|
||||||
|
'Client credentials parsed from body parameters ',
|
||||||
|
clientId,
|
||||||
|
clientSecret
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (!req.headers || !req.headers.authorization) {
|
if (!req.headers || !req.headers.authorization) {
|
||||||
throw new InvalidRequest('No authorization header passed');
|
throw new InvalidRequest('No authorization header passed');
|
||||||
@ -23,7 +27,9 @@ export const introspection = wrap(async function (req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pieces[0] !== 'Basic') {
|
if (pieces[0] !== 'Basic') {
|
||||||
throw new InvalidRequest(`Unsupported authorization method: ${pieces[0]}`);
|
throw new InvalidRequest(
|
||||||
|
`Unsupported authorization method: ${pieces[0]}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2);
|
pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2);
|
||||||
@ -33,7 +39,11 @@ export const introspection = wrap(async function (req, res) {
|
|||||||
|
|
||||||
clientId = pieces[0];
|
clientId = pieces[0];
|
||||||
clientSecret = pieces[1];
|
clientSecret = pieces[1];
|
||||||
console.debug('Client credentials parsed from basic auth header: ', clientId, clientSecret);
|
req.oauth2.logger.debug(
|
||||||
|
'Client credentials parsed from basic auth header: ',
|
||||||
|
clientId,
|
||||||
|
clientSecret
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.body.token) {
|
if (!req.body.token) {
|
||||||
@ -49,7 +59,7 @@ export const introspection = wrap(async function (req, res) {
|
|||||||
const resObj = {
|
const resObj = {
|
||||||
token_type: 'bearer',
|
token_type: 'bearer',
|
||||||
token: token.token,
|
token: token.token,
|
||||||
expires_in: Math.floor(ttl / 1000)
|
expires_in: Math.floor(ttl / 1000),
|
||||||
};
|
};
|
||||||
|
|
||||||
dataResponse(req, res, resObj);
|
dataResponse(req, res, resObj);
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import * as tokens from './tokens'
|
import * as tokens from './tokens';
|
||||||
import {
|
import {
|
||||||
InvalidRequest,
|
InvalidRequest,
|
||||||
InvalidClient,
|
InvalidClient,
|
||||||
UnauthorizedClient,
|
UnauthorizedClient,
|
||||||
UnsupportedGrantType,
|
UnsupportedGrantType,
|
||||||
OAuth2Error
|
OAuth2Error,
|
||||||
} from '../model/error'
|
} from '../model/error';
|
||||||
import { data as dataResponse, error as errorResponse } from '../utils/response'
|
import {
|
||||||
import wrap from '../utils/wrap'
|
data as dataResponse,
|
||||||
|
error as errorResponse,
|
||||||
|
} from '../utils/response';
|
||||||
|
import wrap from '../utils/wrap';
|
||||||
import { OAuth2TokenResponse } from '../model/model';
|
import { OAuth2TokenResponse } from '../model/model';
|
||||||
|
|
||||||
export const token = wrap(async (req, res) => {
|
export const token = wrap(async (req, res) => {
|
||||||
@ -20,7 +23,11 @@ export const token = wrap(async (req, res) => {
|
|||||||
if (req.body.client_id && req.body.client_secret) {
|
if (req.body.client_id && req.body.client_secret) {
|
||||||
clientId = req.body.client_id as string;
|
clientId = req.body.client_id as string;
|
||||||
clientSecret = req.body.client_secret as string;
|
clientSecret = req.body.client_secret as string;
|
||||||
console.debug('Client credentials parsed from body parameters', clientId, clientSecret);
|
req.oauth2.logger.debug(
|
||||||
|
'Client credentials parsed from body parameters',
|
||||||
|
clientId,
|
||||||
|
clientSecret
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (!req.headers || !req.headers.authorization) {
|
if (!req.headers || !req.headers.authorization) {
|
||||||
throw new InvalidRequest('No authorization header passed');
|
throw new InvalidRequest('No authorization header passed');
|
||||||
@ -32,7 +39,9 @@ export const token = wrap(async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pieces[0] !== 'Basic') {
|
if (pieces[0] !== 'Basic') {
|
||||||
throw new InvalidRequest(`Unsupported authorization method: ${pieces[0]}`);
|
throw new InvalidRequest(
|
||||||
|
`Unsupported authorization method: ${pieces[0]}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2);
|
pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2);
|
||||||
@ -42,15 +51,21 @@ export const token = wrap(async (req, res) => {
|
|||||||
|
|
||||||
clientId = pieces[0];
|
clientId = pieces[0];
|
||||||
clientSecret = pieces[1];
|
clientSecret = pieces[1];
|
||||||
console.debug('Client credentials parsed from basic auth header:', clientId, clientSecret);
|
req.oauth2.logger.debug(
|
||||||
|
'Client credentials parsed from basic auth header:',
|
||||||
|
clientId,
|
||||||
|
clientSecret
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.body.grant_type) {
|
if (!req.body.grant_type) {
|
||||||
throw new InvalidRequest('Request body does not contain grant_type parameter');
|
throw new InvalidRequest(
|
||||||
|
'Request body does not contain grant_type parameter'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
grantType = req.body.grant_type as string;
|
grantType = req.body.grant_type as string;
|
||||||
console.debug('Parameter grant_type is', grantType);
|
req.oauth2.logger.debug('Parameter grant_type is', grantType);
|
||||||
|
|
||||||
const client = await oauth2.model.client.fetchById(clientId);
|
const client = await oauth2.model.client.fetchById(clientId);
|
||||||
|
|
||||||
@ -63,36 +78,58 @@ export const token = wrap(async (req, res) => {
|
|||||||
throw new UnauthorizedClient('Invalid client secret');
|
throw new UnauthorizedClient('Invalid client secret');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!oauth2.model.client.checkGrantType(client, grantType) && grantType !== 'refresh_token') {
|
if (
|
||||||
|
!oauth2.model.client.checkGrantType(client, grantType) &&
|
||||||
|
grantType !== 'refresh_token'
|
||||||
|
) {
|
||||||
throw new UnauthorizedClient('Invalid grant type for the client');
|
throw new UnauthorizedClient('Invalid grant type for the client');
|
||||||
} else {
|
} else {
|
||||||
console.debug('Grant type check passed');
|
req.oauth2.logger.debug('Grant type check passed');
|
||||||
}
|
}
|
||||||
|
|
||||||
let tokenResponse: OAuth2TokenResponse;
|
let tokenResponse: OAuth2TokenResponse;
|
||||||
try {
|
try {
|
||||||
switch (grantType) {
|
switch (grantType) {
|
||||||
case 'authorization_code':
|
case 'authorization_code':
|
||||||
tokenResponse = await tokens.authorizationCode(oauth2, client, req.body.code);
|
tokenResponse = await tokens.authorizationCode(
|
||||||
|
oauth2,
|
||||||
|
client,
|
||||||
|
req.body.code
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'password':
|
case 'password':
|
||||||
tokenResponse = await tokens.password(oauth2, client, req.body.username, req.body.password, req.body.scope);
|
tokenResponse = await tokens.password(
|
||||||
|
oauth2,
|
||||||
|
client,
|
||||||
|
req.body.username,
|
||||||
|
req.body.password,
|
||||||
|
req.body.scope
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'client_credentials':
|
case 'client_credentials':
|
||||||
tokenResponse = await tokens.clientCredentials(oauth2, client, req.body.scope);
|
tokenResponse = await tokens.clientCredentials(
|
||||||
|
oauth2,
|
||||||
|
client,
|
||||||
|
req.body.scope
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'refresh_token':
|
case 'refresh_token':
|
||||||
tokenResponse = await tokens.refreshToken(oauth2, client, req.body.refresh_token);
|
tokenResponse = await tokens.refreshToken(
|
||||||
|
oauth2,
|
||||||
|
client,
|
||||||
|
req.body.refresh_token
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedGrantType('Grant type does not match any supported type');
|
throw new UnsupportedGrantType(
|
||||||
|
'Grant type does not match any supported type'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tokenResponse) {
|
if (tokenResponse) {
|
||||||
dataResponse(req, res, tokenResponse);
|
dataResponse(req, res, tokenResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorResponse(req, res, e as OAuth2Error);
|
errorResponse(req, res, e as OAuth2Error);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { InvalidRequest, ServerError, InvalidGrant } from '../../model/error';
|
import { InvalidRequest, ServerError, InvalidGrant } from '../../model/error';
|
||||||
import { OAuth2, OAuth2Client, OAuth2Code, OAuth2TokenResponse } from '../../model/model';
|
import {
|
||||||
|
OAuth2,
|
||||||
|
OAuth2Client,
|
||||||
|
OAuth2Code,
|
||||||
|
OAuth2TokenResponse,
|
||||||
|
} from '../../model/model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Issue an access token by authorization code
|
* Issue an access token by authorization code
|
||||||
@ -14,24 +19,28 @@ export async function authorizationCode(
|
|||||||
providedCode: string
|
providedCode: string
|
||||||
): Promise<OAuth2TokenResponse> {
|
): Promise<OAuth2TokenResponse> {
|
||||||
const respObj: OAuth2TokenResponse = {
|
const respObj: OAuth2TokenResponse = {
|
||||||
token_type: 'bearer'
|
token_type: 'bearer',
|
||||||
};
|
};
|
||||||
|
|
||||||
let code: OAuth2Code | null = null;
|
let code: OAuth2Code | null = null;
|
||||||
|
|
||||||
if (!providedCode) {
|
if (!providedCode) {
|
||||||
throw new InvalidRequest('code is mandatory for authorization_code grant type');
|
throw new InvalidRequest(
|
||||||
|
'code is mandatory for authorization_code grant type'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
code = await oauth2.model.code.fetchByCode(providedCode);
|
code = await oauth2.model.code.fetchByCode(providedCode);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
oauth2.logger.error(err);
|
||||||
throw new ServerError('Failed to call code.fetchByCode function');
|
throw new ServerError('Failed to call code.fetchByCode function');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
if (oauth2.model.code.getClientId(code) !== oauth2.model.client.getId(client)) {
|
if (
|
||||||
|
oauth2.model.code.getClientId(code) !== oauth2.model.client.getId(client)
|
||||||
|
) {
|
||||||
throw new InvalidGrant('Code was issued by another client');
|
throw new InvalidGrant('Code was issued by another client');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,56 +51,81 @@ export async function authorizationCode(
|
|||||||
throw new InvalidGrant('Code not found');
|
throw new InvalidGrant('Code not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('Code fetched', code);
|
oauth2.logger.debug('Code fetched', code);
|
||||||
|
|
||||||
|
const scope = oauth2.model.code.getScope(code);
|
||||||
|
const cleanScope = oauth2.model.client.transformScope(scope);
|
||||||
|
const userId = oauth2.model.code.getUserId(code);
|
||||||
|
const clientId = oauth2.model.code.getClientId(code);
|
||||||
|
|
||||||
|
if (oauth2.model.refreshToken.invalidateOld) {
|
||||||
try {
|
try {
|
||||||
await oauth2.model.refreshToken.removeByUserIdClientId(
|
await oauth2.model.refreshToken.removeByUserIdClientId(userId, clientId);
|
||||||
oauth2.model.code.getUserId(code),
|
|
||||||
oauth2.model.code.getClientId(code)
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
oauth2.logger.error(err);
|
||||||
throw new ServerError('Failed to call refreshToken.removeByUserIdClientId function');
|
throw new ServerError(
|
||||||
|
'Failed to call refreshToken.removeByUserIdClientId function'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('Refresh token removed');
|
oauth2.logger.debug('Refresh token removed');
|
||||||
|
}
|
||||||
|
|
||||||
if (!oauth2.model.client.checkGrantType(client, 'refresh_token')) {
|
if (!oauth2.model.client.checkGrantType(client, 'refresh_token')) {
|
||||||
console.debug('Client does not allow grant type refresh_token, skip creation');
|
oauth2.logger.debug(
|
||||||
|
'Client does not allow grant type refresh_token, skip creation'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
respObj.refresh_token = await oauth2.model.refreshToken.create(
|
respObj.refresh_token = await oauth2.model.refreshToken.create(
|
||||||
oauth2.model.code.getUserId(code),
|
userId,
|
||||||
oauth2.model.code.getClientId(code),
|
clientId,
|
||||||
oauth2.model.code.getScope(code)
|
scope
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
oauth2.logger.error(err);
|
||||||
throw new ServerError('Failed to call refreshToken.create function');
|
throw new ServerError('Failed to call refreshToken.create function');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
respObj.access_token = await oauth2.model.accessToken.create(
|
respObj.access_token = await oauth2.model.accessToken.create(
|
||||||
oauth2.model.code.getUserId(code),
|
userId,
|
||||||
oauth2.model.code.getClientId(code),
|
clientId,
|
||||||
oauth2.model.code.getScope(code),
|
scope,
|
||||||
oauth2.model.accessToken.ttl
|
oauth2.model.accessToken.ttl
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
oauth2.logger.error(err);
|
||||||
throw new ServerError('Failed to call accessToken.create function');
|
throw new ServerError('Failed to call accessToken.create function');
|
||||||
}
|
}
|
||||||
|
|
||||||
respObj.expires_in = oauth2.model.accessToken.ttl;
|
respObj.expires_in = oauth2.model.accessToken.ttl;
|
||||||
console.debug('Access token saved:', respObj.access_token);
|
oauth2.logger.debug('Access token saved:', respObj.access_token);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await oauth2.model.code.removeByCode(providedCode);
|
await oauth2.model.code.removeByCode(providedCode);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
oauth2.logger.error(err);
|
||||||
throw new ServerError('Failed to call code.removeByCode function');
|
throw new ServerError('Failed to call code.removeByCode function');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cleanScope.includes('openid') && oauth2.model.jwt) {
|
||||||
|
const user = await oauth2.model.user.fetchById(
|
||||||
|
oauth2.model.code.getUserId(code)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
respObj.id_token = await oauth2.model.jwt.issueIdToken(
|
||||||
|
user,
|
||||||
|
cleanScope,
|
||||||
|
respObj.access_token
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
oauth2.logger.error(err);
|
||||||
|
throw new ServerError('Failed to issue an ID token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return respObj;
|
return respObj;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ServerError, InvalidScope } from '../../model/error'
|
import { ServerError, InvalidScope } from '../../model/error';
|
||||||
import { OAuth2, OAuth2Client, OAuth2TokenResponse } from '../../model/model';
|
import { OAuth2, OAuth2Client, OAuth2TokenResponse } from '../../model/model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,7 +16,7 @@ export async function clientCredentials(
|
|||||||
let scope: string[] = [];
|
let scope: string[] = [];
|
||||||
|
|
||||||
const resObj: OAuth2TokenResponse = {
|
const resObj: OAuth2TokenResponse = {
|
||||||
token_type: 'bearer'
|
token_type: 'bearer',
|
||||||
};
|
};
|
||||||
|
|
||||||
scope = oauth2.model.client.transformScope(wantScope);
|
scope = oauth2.model.client.transformScope(wantScope);
|
||||||
@ -24,7 +24,7 @@ export async function clientCredentials(
|
|||||||
throw new InvalidScope('Client does not allow access to this scope');
|
throw new InvalidScope('Client does not allow access to this scope');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('Scope check passed ', scope);
|
oauth2.logger.debug('Scope check passed', scope);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resObj.access_token = await oauth2.model.accessToken.create(
|
resObj.access_token = await oauth2.model.accessToken.create(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export * from './authorizationCode'
|
export * from './authorizationCode';
|
||||||
export * from './clientCredentials'
|
export * from './clientCredentials';
|
||||||
export * from './password'
|
export * from './password';
|
||||||
export * from './refreshToken'
|
export * from './refreshToken';
|
||||||
|
@ -1,5 +1,15 @@
|
|||||||
import { ServerError, InvalidRequest, InvalidScope, InvalidClient } from '../../model/error'
|
import {
|
||||||
import { OAuth2, OAuth2Client, OAuth2User, OAuth2TokenResponse } from '../../model/model';
|
ServerError,
|
||||||
|
InvalidRequest,
|
||||||
|
InvalidScope,
|
||||||
|
InvalidClient,
|
||||||
|
} from '../../model/error';
|
||||||
|
import {
|
||||||
|
OAuth2,
|
||||||
|
OAuth2Client,
|
||||||
|
OAuth2User,
|
||||||
|
OAuth2TokenResponse,
|
||||||
|
} from '../../model/model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implicit access token response
|
* Implicit access token response
|
||||||
@ -20,8 +30,8 @@ export async function password(
|
|||||||
let user: OAuth2User | null = null;
|
let user: OAuth2User | null = null;
|
||||||
|
|
||||||
const resObj: OAuth2TokenResponse = {
|
const resObj: OAuth2TokenResponse = {
|
||||||
token_type: 'bearer'
|
token_type: 'bearer',
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
throw new InvalidRequest('Username is mandatory for password grant type');
|
throw new InvalidRequest('Username is mandatory for password grant type');
|
||||||
@ -35,7 +45,7 @@ export async function password(
|
|||||||
if (!oauth2.model.client.checkScope(client, scope)) {
|
if (!oauth2.model.client.checkScope(client, scope)) {
|
||||||
throw new InvalidScope('Client does not allow access to this scope');
|
throw new InvalidScope('Client does not allow access to this scope');
|
||||||
} else {
|
} else {
|
||||||
console.debug('Scope check passed: ', scope);
|
oauth2.logger.debug('Scope check passed: ', scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -59,13 +69,17 @@ export async function password(
|
|||||||
oauth2.model.client.getId(client)
|
oauth2.model.client.getId(client)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new ServerError('Failed to call refreshToken.removeByUserIdClientId function');
|
throw new ServerError(
|
||||||
|
'Failed to call refreshToken.removeByUserIdClientId function'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('Refresh token removed');
|
oauth2.logger.debug('Refresh token removed');
|
||||||
|
|
||||||
if (!oauth2.model.client.checkGrantType(client, 'refresh_token')) {
|
if (!oauth2.model.client.checkGrantType(client, 'refresh_token')) {
|
||||||
console.debug('Client does not allow grant type refresh_token, skip creation');
|
oauth2.logger.debug(
|
||||||
|
'Client does not allow grant type refresh_token, skip creation'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
resObj.refresh_token = await oauth2.model.refreshToken.create(
|
resObj.refresh_token = await oauth2.model.refreshToken.create(
|
||||||
@ -90,7 +104,7 @@ export async function password(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resObj.expires_in = oauth2.model.accessToken.ttl;
|
resObj.expires_in = oauth2.model.accessToken.ttl;
|
||||||
console.debug('Access token saved ', resObj.access_token);
|
oauth2.logger.debug('Access token saved ', resObj.access_token);
|
||||||
|
|
||||||
return resObj;
|
return resObj;
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,29 @@
|
|||||||
import { InvalidRequest, ServerError, InvalidGrant, InvalidClient } from '../../model/error';
|
import {
|
||||||
|
InvalidRequest,
|
||||||
|
ServerError,
|
||||||
|
InvalidGrant,
|
||||||
|
InvalidClient,
|
||||||
|
} from '../../model/error';
|
||||||
import {
|
import {
|
||||||
OAuth2,
|
OAuth2,
|
||||||
OAuth2AccessToken,
|
OAuth2AccessToken,
|
||||||
OAuth2Client,
|
OAuth2Client,
|
||||||
OAuth2RefreshToken,
|
OAuth2RefreshToken,
|
||||||
OAuth2User,
|
OAuth2User,
|
||||||
OAuth2TokenResponse
|
OAuth2TokenResponse,
|
||||||
} from '../../model/model';
|
} from '../../model/model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a new access token from a refresh token. Scope change may not be requested.
|
* Get a new access token from a refresh token. Scope change may not be requested.
|
||||||
* @param oauth2 - OAuth2 instance
|
* @param oauth2 - OAuth2 instance
|
||||||
* @param client - OAuth2 client
|
* @param client - OAuth2 client
|
||||||
* @param pRefreshToken - Refresh token
|
* @param providedToken - Refresh token
|
||||||
* @returns Access token
|
* @returns Access token
|
||||||
*/
|
*/
|
||||||
export async function refreshToken(
|
export async function refreshToken(
|
||||||
oauth2: OAuth2,
|
oauth2: OAuth2,
|
||||||
client: OAuth2Client,
|
client: OAuth2Client,
|
||||||
pRefreshToken: string,
|
providedToken: string
|
||||||
): Promise<OAuth2TokenResponse> {
|
): Promise<OAuth2TokenResponse> {
|
||||||
let user: OAuth2User | null = null;
|
let user: OAuth2User | null = null;
|
||||||
let ttl: number | null = null;
|
let ttl: number | null = null;
|
||||||
@ -26,15 +31,17 @@ export async function refreshToken(
|
|||||||
let accessToken: OAuth2AccessToken | null = null;
|
let accessToken: OAuth2AccessToken | null = null;
|
||||||
|
|
||||||
const resObj: OAuth2TokenResponse = {
|
const resObj: OAuth2TokenResponse = {
|
||||||
token_type: 'bearer'
|
token_type: 'bearer',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!pRefreshToken) {
|
if (!providedToken) {
|
||||||
throw new InvalidRequest('refresh_token is mandatory for refresh_token grant type');
|
throw new InvalidRequest(
|
||||||
|
'refresh_token is mandatory for refresh_token grant type'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
refreshToken = await oauth2.model.refreshToken.fetchByToken(pRefreshToken);
|
refreshToken = await oauth2.model.refreshToken.fetchByToken(providedToken);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new ServerError('Failed to call refreshToken.fetchByToken function');
|
throw new ServerError('Failed to call refreshToken.fetchByToken function');
|
||||||
}
|
}
|
||||||
@ -43,8 +50,12 @@ export async function refreshToken(
|
|||||||
throw new InvalidGrant('Refresh token not found');
|
throw new InvalidGrant('Refresh token not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oauth2.model.refreshToken.getClientId(refreshToken) !== oauth2.model.client.getId(client)) {
|
if (
|
||||||
console.warn('Client %s tried to fetch a refresh token which belongs to client %s!',
|
oauth2.model.refreshToken.getClientId(refreshToken) !==
|
||||||
|
oauth2.model.client.getId(client)
|
||||||
|
) {
|
||||||
|
oauth2.logger.warn(
|
||||||
|
'Client %s tried to fetch a refresh token which belongs to client %s!',
|
||||||
oauth2.model.client.getId(client),
|
oauth2.model.client.getId(client),
|
||||||
oauth2.model.refreshToken.getClientId(refreshToken)
|
oauth2.model.refreshToken.getClientId(refreshToken)
|
||||||
);
|
);
|
||||||
@ -52,7 +63,9 @@ export async function refreshToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
user = await oauth2.model.user.fetchById(oauth2.model.refreshToken.getUserId(refreshToken));
|
user = await oauth2.model.user.fetchById(
|
||||||
|
oauth2.model.refreshToken.getUserId(refreshToken)
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new InvalidClient('User not found');
|
throw new InvalidClient('User not found');
|
||||||
}
|
}
|
||||||
@ -62,10 +75,14 @@ export async function refreshToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
accessToken = await oauth2.model.accessToken.fetchByUserIdClientId(oauth2.model.user.getId(user),
|
accessToken = await oauth2.model.accessToken.fetchByUserIdClientId(
|
||||||
oauth2.model.client.getId(client));
|
oauth2.model.user.getId(user),
|
||||||
|
oauth2.model.client.getId(client)
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new ServerError('Failed to call accessToken.fetchByUserIdClientId function');
|
throw new ServerError(
|
||||||
|
'Failed to call accessToken.fetchByUserIdClientId function'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
|
@ -3,7 +3,7 @@ import { AccessDenied } from './model/error';
|
|||||||
import wrap from './utils/wrap';
|
import wrap from './utils/wrap';
|
||||||
|
|
||||||
export const middleware = wrap(async function (req: Request, res, next) {
|
export const middleware = wrap(async function (req: Request, res, next) {
|
||||||
console.debug('Parsing bearer token');
|
req.oauth2.logger.debug('Parsing bearer token');
|
||||||
let token = null;
|
let token = null;
|
||||||
|
|
||||||
// Look for token in header
|
// Look for token in header
|
||||||
@ -21,13 +21,16 @@ export const middleware = wrap(async function (req: Request, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
token = pieces[1];
|
token = pieces[1];
|
||||||
console.debug('Bearer token parsed from authorization header:', token);
|
req.oauth2.logger.debug(
|
||||||
|
'Bearer token parsed from authorization header:',
|
||||||
|
token
|
||||||
|
);
|
||||||
} else if (req.query?.access_token) {
|
} else if (req.query?.access_token) {
|
||||||
token = req.query.access_token;
|
token = req.query.access_token;
|
||||||
console.debug('Bearer token parsed from query params:', token);
|
req.oauth2.logger.debug('Bearer token parsed from query params:', token);
|
||||||
} else if (req.body?.access_token) {
|
} else if (req.body?.access_token) {
|
||||||
token = req.body.access_token;
|
token = req.body.access_token;
|
||||||
console.debug('Bearer token parsed from body params:', token);
|
req.oauth2.logger.debug('Bearer token parsed from body params:', token);
|
||||||
} else {
|
} else {
|
||||||
throw new AccessDenied('Bearer token not found');
|
throw new AccessDenied('Bearer token not found');
|
||||||
}
|
}
|
||||||
@ -40,7 +43,7 @@ export const middleware = wrap(async function (req: Request, res, next) {
|
|||||||
throw new AccessDenied('Token is expired');
|
throw new AccessDenied('Token is expired');
|
||||||
} else {
|
} else {
|
||||||
res.locals.accessToken = object;
|
res.locals.accessToken = object;
|
||||||
console.debug('AccessToken fetched', object);
|
req.oauth2.logger.debug('AccessToken fetched', object);
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2,8 +2,12 @@ export class OAuth2Error extends Error {
|
|||||||
public name = 'OAuth2AbstractError';
|
public name = 'OAuth2AbstractError';
|
||||||
public logLevel = 'error';
|
public logLevel = 'error';
|
||||||
|
|
||||||
constructor (public code: string, public message: string, public status: number) {
|
constructor(
|
||||||
super()
|
public code: string,
|
||||||
|
public message: string,
|
||||||
|
public status: number
|
||||||
|
) {
|
||||||
|
super();
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { OAuth2Logger } from '../utils/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth2 client object
|
* OAuth2 client object
|
||||||
@ -46,7 +47,6 @@ export interface OAuth2RefreshToken {
|
|||||||
* OAuth2 implicit user model
|
* OAuth2 implicit user model
|
||||||
*/
|
*/
|
||||||
export interface OAuth2User {
|
export interface OAuth2User {
|
||||||
id: string | number;
|
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
@ -55,6 +55,7 @@ export interface OAuth2User {
|
|||||||
* OAuth2 token response
|
* OAuth2 token response
|
||||||
*/
|
*/
|
||||||
export interface OAuth2TokenResponse {
|
export interface OAuth2TokenResponse {
|
||||||
|
id_token?: string;
|
||||||
access_token?: string;
|
access_token?: string;
|
||||||
refresh_token?: string;
|
refresh_token?: string;
|
||||||
expires_in?: number;
|
expires_in?: number;
|
||||||
@ -89,7 +90,9 @@ export interface OAuth2AccessTokenAdapter {
|
|||||||
/**
|
/**
|
||||||
* Fetch an access token by the token string from the database
|
* Fetch an access token by the token string from the database
|
||||||
*/
|
*/
|
||||||
fetchByToken: (token: OAuth2AccessToken | string) => Promise<OAuth2AccessToken>;
|
fetchByToken: (
|
||||||
|
token: OAuth2AccessToken | string
|
||||||
|
) => Promise<OAuth2AccessToken>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the time-to-live value
|
* Check the time-to-live value
|
||||||
@ -209,6 +212,11 @@ export interface OAuth2CodeAdapter {
|
|||||||
* OAuth2 refresh token adapter model
|
* OAuth2 refresh token adapter model
|
||||||
*/
|
*/
|
||||||
export interface OAuth2RefreshTokenAdapter {
|
export interface OAuth2RefreshTokenAdapter {
|
||||||
|
/**
|
||||||
|
* Invalidate all previous refresh tokens for user/client when new one is issued.
|
||||||
|
*/
|
||||||
|
invalidateOld: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new refresh token
|
* Create a new refresh token
|
||||||
*/
|
*/
|
||||||
@ -221,7 +229,9 @@ export interface OAuth2RefreshTokenAdapter {
|
|||||||
/**
|
/**
|
||||||
* Fetch a token from the database
|
* Fetch a token from the database
|
||||||
*/
|
*/
|
||||||
fetchByToken: (token: OAuth2RefreshToken | string) => Promise<OAuth2RefreshToken>;
|
fetchByToken: (
|
||||||
|
token: OAuth2RefreshToken | string
|
||||||
|
) => Promise<OAuth2RefreshToken>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove refresh token by user ID and client ID
|
* Remove refresh token by user ID and client ID
|
||||||
@ -300,6 +310,25 @@ export interface OAuth2UserAdapter {
|
|||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter for managing the `openid` scope.
|
||||||
|
*/
|
||||||
|
export interface JWTAdapter {
|
||||||
|
/**
|
||||||
|
* Issue a new ID token for user.
|
||||||
|
*/
|
||||||
|
issueIdToken: (
|
||||||
|
user: OAuth2User,
|
||||||
|
scope: string[],
|
||||||
|
accessToken?: string
|
||||||
|
) => Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate an ID token
|
||||||
|
*/
|
||||||
|
validateIdToken: (idToken: string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the OAuth2 decision page
|
* Render the OAuth2 decision page
|
||||||
*/
|
*/
|
||||||
@ -321,6 +350,7 @@ export interface OAuth2AdapterModel {
|
|||||||
user: OAuth2UserAdapter;
|
user: OAuth2UserAdapter;
|
||||||
client: OAuth2ClientAdapter;
|
client: OAuth2ClientAdapter;
|
||||||
code: OAuth2CodeAdapter;
|
code: OAuth2CodeAdapter;
|
||||||
|
jwt?: JWTAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -328,5 +358,6 @@ export interface OAuth2AdapterModel {
|
|||||||
*/
|
*/
|
||||||
export interface OAuth2 {
|
export interface OAuth2 {
|
||||||
model: OAuth2AdapterModel;
|
model: OAuth2AdapterModel;
|
||||||
|
logger: OAuth2Logger;
|
||||||
decision: RenderOAuth2Decision;
|
decision: RenderOAuth2Decision;
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,28 @@
|
|||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
import * as controller from './controller';
|
import * as controller from './controller';
|
||||||
import { middleware } from './middleware';
|
import { middleware } from './middleware';
|
||||||
import { RenderOAuth2Decision, OAuth2, OAuth2AdapterModel } from './model/model';
|
import {
|
||||||
|
RenderOAuth2Decision,
|
||||||
|
OAuth2,
|
||||||
|
OAuth2AdapterModel,
|
||||||
|
} from './model/model';
|
||||||
|
import { OAuth2Logger } from './utils/logger';
|
||||||
|
|
||||||
export class OAuth2Provider implements OAuth2 {
|
export class OAuth2Provider implements OAuth2 {
|
||||||
public bearer = middleware;
|
public bearer = middleware;
|
||||||
public controller = controller;
|
public controller = controller;
|
||||||
|
public logger = new OAuth2Logger('info');
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public model: OAuth2AdapterModel,
|
public model: OAuth2AdapterModel,
|
||||||
public decision: RenderOAuth2Decision,
|
public decision: RenderOAuth2Decision
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
express(): RequestHandler {
|
express(): RequestHandler {
|
||||||
return (req, _res, next) => {
|
return (req, _res, next) => {
|
||||||
console.debug('OAuth2 Injected into request');
|
req.oauth2.logger.debug('OAuth2 Injected into request');
|
||||||
req.oauth2 = this;
|
req.oauth2 = this;
|
||||||
next();
|
next();
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
43
src/utils/logger.ts
Normal file
43
src/utils/logger.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
export type LoggerLevel = 'none' | 'info' | 'warn' | 'error' | 'debug';
|
||||||
|
type LoggerFunction = (...args: any[]) => void;
|
||||||
|
|
||||||
|
const LOG_LEVELS = ['none', 'info', 'warn', 'error', 'debug'];
|
||||||
|
|
||||||
|
interface LoggerType {
|
||||||
|
info: LoggerFunction;
|
||||||
|
warn: LoggerFunction;
|
||||||
|
error: LoggerFunction;
|
||||||
|
debug: LoggerFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OAuth2Logger {
|
||||||
|
constructor(
|
||||||
|
public logLevel: LoggerLevel,
|
||||||
|
public logger: LoggerType | undefined = console
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public setLogLevel(logLevel: LoggerLevel): void {
|
||||||
|
this.logLevel = logLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLogger(logger?: LoggerType): void {
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public log(level: LoggerLevel, ...args: any[]): void {
|
||||||
|
if (!this.logger || this.logLevel === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LOG_LEVELS.indexOf(this.logLevel) < LOG_LEVELS.indexOf(level)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger[level as 'info' | 'warn' | 'error' | 'debug'](...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public info = this.log.bind(this, 'info');
|
||||||
|
public warn = this.log.bind(this, 'warn');
|
||||||
|
public error = this.log.bind(this, 'error');
|
||||||
|
public debug = this.log.bind(this, 'debug');
|
||||||
|
}
|
@ -13,40 +13,49 @@ function dataRes(req: Request, res: Response, code: number, data: any): void {
|
|||||||
res.header('Cache-Control', 'no-store');
|
res.header('Cache-Control', 'no-store');
|
||||||
res.header('Pragma', 'no-cache');
|
res.header('Pragma', 'no-cache');
|
||||||
res.status(code).send(data);
|
res.status(code).send(data);
|
||||||
console.debug('Response:', data);
|
req.oauth2.logger.debug('Response:', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirect(req: Request, res: Response, redirectUri: string): void {
|
function redirect(req: Request, res: Response, redirectUri: string): void {
|
||||||
res.header('Location', redirectUri);
|
res.header('Location', redirectUri);
|
||||||
res.status(302).end();
|
res.status(302).end();
|
||||||
console.debug('Redirecting to', redirectUri);
|
req.oauth2.logger.debug('Redirecting to', redirectUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function error(req: Request, res: Response, err: OAuth2Error, redirectUri?: string): void {
|
export function error(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
err: OAuth2Error,
|
||||||
|
redirectUri?: string
|
||||||
|
): void {
|
||||||
// Transform unknown error
|
// Transform unknown error
|
||||||
if (!(err instanceof OAuth2Error)) {
|
if (!(err instanceof OAuth2Error)) {
|
||||||
console.error((err as Error).stack);
|
req.oauth2.logger.error((err as Error).stack);
|
||||||
err = new ServerError('Uncaught exception');
|
err = new ServerError('Uncaught exception');
|
||||||
} else {
|
} else {
|
||||||
console.error('Exception caught', err.stack);
|
req.oauth2.logger.error('Exception caught', err.stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirectUri) {
|
if (redirectUri) {
|
||||||
const obj: ErrorResponseData = {
|
const obj: ErrorResponseData = {
|
||||||
error: err.code,
|
error: err.code,
|
||||||
error_description: err.message
|
error_description: err.message,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (req.query.state) {
|
if (req.query.state) {
|
||||||
obj.state = req.query.state as string;
|
obj.state = req.query.state as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectUri += '?' + (new URLSearchParams(obj as Record<string, string>).toString());
|
redirectUri +=
|
||||||
|
'?' + new URLSearchParams(obj as Record<string, string>).toString();
|
||||||
redirect(req, res, redirectUri);
|
redirect(req, res, redirectUri);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dataRes(req, res, err.status, { error: err.code, error_description: err.message });
|
dataRes(req, res, err.status, {
|
||||||
|
error: err.code,
|
||||||
|
error_description: err.message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function data(
|
export function data(
|
||||||
@ -57,15 +66,15 @@ export function data(
|
|||||||
fragment: boolean = false
|
fragment: boolean = false
|
||||||
): void {
|
): void {
|
||||||
if (redirectUri) {
|
if (redirectUri) {
|
||||||
redirectUri += fragment
|
redirectUri += fragment ? '#' : redirectUri.indexOf('?') === -1 ? '?' : '&';
|
||||||
? '#'
|
|
||||||
: (redirectUri.indexOf('?') === -1 ? '?' : '&');
|
|
||||||
|
|
||||||
if (req.query.state) {
|
if (req.query.state) {
|
||||||
obj.state = req.query.state as string;
|
obj.state = req.query.state as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectUri += new URLSearchParams(obj as Record<string, string>).toString();
|
redirectUri += new URLSearchParams(
|
||||||
|
obj as Record<string, string>
|
||||||
|
).toString();
|
||||||
redirect(req, res, redirectUri);
|
redirect(req, res, redirectUri);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
import { error } from './response';
|
import { error } from './response';
|
||||||
|
|
||||||
export default (fn: RequestHandler, redir?: boolean): RequestHandler => (req, res, next) =>
|
export default (fn: RequestHandler, redir?: boolean): RequestHandler =>
|
||||||
(fn(req, res, next) as unknown as Promise<void>).catch(e =>
|
(req, res, next) =>
|
||||||
error(req, res, e, redir ? (req.query.redirect_uri as string) : undefined));
|
(fn(req, res, next) as unknown as Promise<void>).catch((e) =>
|
||||||
|
error(req, res, e, redir ? (req.query.redirect_uri as string) : undefined)
|
||||||
|
);
|
||||||
|
Reference in New Issue
Block a user