2022-02-25 16:31:59 +00:00
|
|
|
import {
|
|
|
|
InvalidRequest,
|
|
|
|
UnsupportedResponseType,
|
|
|
|
InvalidClient,
|
|
|
|
UnauthorizedClient,
|
|
|
|
InvalidScope,
|
|
|
|
AccessDenied,
|
|
|
|
} from '../model/error';
|
|
|
|
import { OAuth2User } from '../model/model';
|
|
|
|
import { data as dataResponse } from '../utils/response';
|
|
|
|
import wrap from '../utils/wrap';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Authorization and decision endpoint.
|
|
|
|
*/
|
|
|
|
export const authorization = wrap(async (req, res) => {
|
|
|
|
let clientId: string | null = null;
|
|
|
|
let redirectUri: string | null = null;
|
|
|
|
let responseType: string | null = null;
|
|
|
|
let grantTypes: string[] = [];
|
|
|
|
let scope: string[] | null = null;
|
|
|
|
let user: OAuth2User | null = null;
|
|
|
|
|
|
|
|
const { oauth2 } = req;
|
|
|
|
|
|
|
|
if (!req.query.redirect_uri) {
|
2022-03-07 19:32:20 +00:00
|
|
|
throw new InvalidRequest(
|
|
|
|
'redirect_uri field is mandatory for authorization endpoint'
|
|
|
|
);
|
2022-02-25 16:31:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
redirectUri = req.query.redirect_uri as string;
|
2022-03-07 19:32:20 +00:00
|
|
|
req.oauth2.logger.debug('Parameter redirect uri is', redirectUri);
|
2022-02-25 16:31:59 +00:00
|
|
|
|
|
|
|
if (!req.query.client_id) {
|
2022-03-07 19:32:20 +00:00
|
|
|
throw new InvalidRequest(
|
|
|
|
'client_id field is mandatory for authorization endpoint'
|
|
|
|
);
|
2022-02-25 16:31:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check for client_secret (prevent passing it)
|
|
|
|
if (req.query.client_secret) {
|
2022-03-07 19:32:20 +00:00
|
|
|
throw new InvalidRequest(
|
|
|
|
'client_secret field should not be passed to the authorization endpoint'
|
|
|
|
);
|
2022-02-25 16:31:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
clientId = req.query.client_id as string;
|
2022-03-07 19:32:20 +00:00
|
|
|
req.oauth2.logger.debug('Parameter client_id is', clientId);
|
2022-02-25 16:31:59 +00:00
|
|
|
|
|
|
|
if (!req.query.response_type) {
|
2022-03-07 19:32:20 +00:00
|
|
|
throw new InvalidRequest(
|
|
|
|
'response_type field is mandatory for authorization endpoint'
|
|
|
|
);
|
2022-02-25 16:31:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
responseType = req.query.response_type as string;
|
2022-03-07 19:32:20 +00:00
|
|
|
req.oauth2.logger.debug('Parameter response_type is', responseType);
|
2022-02-25 16:31:59 +00:00
|
|
|
|
|
|
|
// Support multiple types
|
|
|
|
const responseTypes = responseType.split(' ');
|
|
|
|
for (const i in responseTypes) {
|
|
|
|
switch (responseTypes[i]) {
|
2022-03-07 19:32:20 +00:00
|
|
|
case 'code':
|
|
|
|
grantTypes.push('authorization_code');
|
|
|
|
break;
|
|
|
|
case 'token':
|
|
|
|
grantTypes.push('implicit');
|
|
|
|
break;
|
|
|
|
case 'id_token':
|
|
|
|
grantTypes.push('id_token');
|
|
|
|
break;
|
|
|
|
case 'none':
|
|
|
|
grantTypes.push(responseTypes[i]);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new UnsupportedResponseType(
|
|
|
|
'Unknown response_type parameter passed'
|
|
|
|
);
|
2022-02-25 16:31:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Filter out duplicates
|
2022-03-07 19:32:20 +00:00
|
|
|
grantTypes = grantTypes.filter(
|
|
|
|
(value, index, self) => self.indexOf(value) === index
|
|
|
|
);
|
2022-02-25 16:31:59 +00:00
|
|
|
|
|
|
|
// "None" type cannot be combined with others
|
|
|
|
if (grantTypes.length > 1 && grantTypes.indexOf('none') !== -1) {
|
2022-03-07 19:32:20 +00:00
|
|
|
throw new InvalidRequest(
|
|
|
|
'Grant type "none" cannot be combined with other grant types'
|
|
|
|
);
|
2022-02-25 16:31:59 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 19:32:20 +00:00
|
|
|
req.oauth2.logger.debug('Parameter grant_type is', grantTypes.join(' '));
|
2022-02-25 16:31:59 +00:00
|
|
|
|
|
|
|
const client = await oauth2.model.client.fetchById(clientId);
|
|
|
|
if (!client) {
|
|
|
|
throw new InvalidClient('Client not found');
|
|
|
|
}
|
|
|
|
|
2022-03-08 16:44:50 +00:00
|
|
|
if (!(await oauth2.model.client.hasRedirectUri(client))) {
|
2022-02-25 16:31:59 +00:00
|
|
|
throw new UnsupportedResponseType('The client has not set a redirect uri');
|
2022-03-08 16:44:50 +00:00
|
|
|
} else if (
|
|
|
|
!(await oauth2.model.client.checkRedirectUri(client, redirectUri))
|
|
|
|
) {
|
2022-02-25 16:31:59 +00:00
|
|
|
throw new InvalidRequest('Wrong RedirectUri provided');
|
|
|
|
}
|
2022-03-07 19:32:20 +00:00
|
|
|
req.oauth2.logger.debug('redirect_uri check passed');
|
2022-02-25 16:31:59 +00:00
|
|
|
|
|
|
|
// The client needs to support all grant types
|
|
|
|
for (const grantType of grantTypes) {
|
2022-03-07 19:32:20 +00:00
|
|
|
if (
|
|
|
|
!oauth2.model.client.checkGrantType(client, grantType) &&
|
|
|
|
grantType !== 'none'
|
|
|
|
) {
|
|
|
|
throw new UnauthorizedClient(
|
|
|
|
'This client does not support grant type ' + grantType
|
|
|
|
);
|
2022-02-25 16:31:59 +00:00
|
|
|
}
|
|
|
|
}
|
2022-03-07 19:32:20 +00:00
|
|
|
req.oauth2.logger.debug('Grant type check passed');
|
2022-02-25 16:31:59 +00:00
|
|
|
|
|
|
|
scope = oauth2.model.client.transformScope(req.query.scope as string);
|
|
|
|
if (!oauth2.model.client.checkScope(client, scope)) {
|
|
|
|
throw new InvalidScope('Client does not allow access to this scope');
|
|
|
|
}
|
2022-03-07 19:32:20 +00:00
|
|
|
req.oauth2.logger.debug('Scope check passed');
|
2022-02-25 16:31:59 +00:00
|
|
|
|
|
|
|
user = await oauth2.model.user.fetchFromRequest(req);
|
|
|
|
if (!user) {
|
|
|
|
throw new InvalidRequest('There is no currently logged in user');
|
|
|
|
} else {
|
|
|
|
if (!user.username) {
|
|
|
|
throw new AccessDenied(user.username);
|
|
|
|
}
|
2022-03-07 19:32:20 +00:00
|
|
|
req.oauth2.logger.debug('User fetched from request');
|
2022-02-25 16:31:59 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 19:32:20 +00:00
|
|
|
let resObj: Record<string, string | number> = {};
|
2022-02-25 16:31:59 +00:00
|
|
|
let consented = false;
|
|
|
|
|
|
|
|
if (req.method === 'GET') {
|
|
|
|
// Check if the user has already consented to this client with this scope
|
|
|
|
// TODO: reevaluate security implications
|
|
|
|
consented = await oauth2.model.user.consented(
|
|
|
|
oauth2.model.user.getId(user),
|
|
|
|
oauth2.model.client.getId(client),
|
|
|
|
scope
|
|
|
|
);
|
|
|
|
|
|
|
|
// Ask for consent
|
2022-03-07 19:32:20 +00:00
|
|
|
if (!consented)
|
|
|
|
return oauth2.decision(req, res, client, scope, user, redirectUri);
|
2022-02-25 16:31:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Save consent
|
|
|
|
if (!consented) {
|
2022-03-07 19:32:20 +00:00
|
|
|
if (!req.body || typeof req.body.decision === 'undefined') {
|
2022-02-25 16:31:59 +00:00
|
|
|
throw new InvalidRequest('No decision parameter passed');
|
|
|
|
} else if (req.body.decision === '0') {
|
|
|
|
throw new AccessDenied('User denied access to the resource');
|
|
|
|
}
|
|
|
|
|
2022-03-07 19:32:20 +00:00
|
|
|
req.oauth2.logger.debug('Decision check passed');
|
2022-02-25 16:31:59 +00:00
|
|
|
|
|
|
|
await oauth2.model.user.consent(
|
|
|
|
oauth2.model.user.getId(user),
|
|
|
|
oauth2.model.client.getId(client),
|
|
|
|
scope
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const i in grantTypes) {
|
2022-03-07 19:32:20 +00:00
|
|
|
let data = null;
|
2022-02-25 16:31:59 +00:00
|
|
|
switch (grantTypes[i]) {
|
2022-03-07 19:32:20 +00:00
|
|
|
case 'authorization_code':
|
|
|
|
data = await oauth2.model.code.create(
|
|
|
|
oauth2.model.user.getId(user),
|
|
|
|
oauth2.model.client.getId(client),
|
|
|
|
scope,
|
|
|
|
oauth2.model.code.ttl
|
|
|
|
);
|
|
|
|
|
|
|
|
resObj = { code: data, ...resObj };
|
|
|
|
|
|
|
|
break;
|
|
|
|
case 'implicit':
|
|
|
|
data = await oauth2.model.accessToken.create(
|
|
|
|
oauth2.model.user.getId(user),
|
|
|
|
oauth2.model.client.getId(client),
|
|
|
|
scope,
|
|
|
|
oauth2.model.accessToken.ttl
|
|
|
|
);
|
|
|
|
|
|
|
|
resObj = {
|
|
|
|
token_type: 'bearer',
|
|
|
|
access_token: data,
|
|
|
|
expires_in: oauth2.model.accessToken.ttl,
|
|
|
|
...resObj,
|
|
|
|
};
|
|
|
|
|
|
|
|
break;
|
|
|
|
case 'id_token':
|
|
|
|
if (!oauth2.model.jwt || !scope.includes('openid')) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
data = await oauth2.model.jwt.issueIdToken(
|
|
|
|
user,
|
|
|
|
scope,
|
2022-03-07 20:05:21 +00:00
|
|
|
req.query.nonce as string | undefined
|
2022-03-07 19:32:20 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
resObj = {
|
|
|
|
id_token: data,
|
|
|
|
...resObj,
|
|
|
|
};
|
|
|
|
case 'none':
|
|
|
|
resObj = {};
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new UnsupportedResponseType(
|
|
|
|
'Unknown response_type parameter passed'
|
|
|
|
);
|
2022-02-25 16:31:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return non-code response types as fragment instead of query
|
|
|
|
return dataResponse(req, res, resObj, redirectUri, responseType !== 'code');
|
|
|
|
}, true);
|