oauth2-provider/src/controller/authorization.ts

254 lines
7.0 KiB
TypeScript

import {
InvalidRequest,
UnsupportedResponseType,
InvalidClient,
UnauthorizedClient,
InvalidScope,
AccessDenied,
InteractionRequired,
InvalidGrant,
} 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) {
throw new InvalidRequest(
'redirect_uri field is mandatory for authorization endpoint'
);
}
redirectUri = req.query.redirect_uri as string;
req.oauth2.logger.debug('Parameter redirect uri is', redirectUri);
if (!req.query.client_id) {
throw new InvalidRequest(
'client_id field is mandatory for authorization endpoint'
);
}
// Check for client_secret (prevent passing it)
if (req.query.client_secret) {
throw new InvalidRequest(
'client_secret field should not be passed to the authorization endpoint'
);
}
clientId = req.query.client_id as string;
req.oauth2.logger.debug('Parameter client_id is', clientId);
if (!req.query.response_type) {
throw new InvalidRequest(
'response_type field is mandatory for authorization endpoint'
);
}
responseType = req.query.response_type as string;
req.oauth2.logger.debug('Parameter response_type is', responseType);
// Support multiple types
const responseTypes = responseType.split(' ');
for (const i in responseTypes) {
switch (responseTypes[i]) {
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'
);
}
}
// Filter out duplicates
grantTypes = grantTypes.filter(
(value, index, self) => self.indexOf(value) === index
);
// "None" type cannot be combined with others
if (grantTypes.length > 1 && grantTypes.indexOf('none') !== -1) {
throw new InvalidRequest(
'Grant type "none" cannot be combined with other grant types'
);
}
req.oauth2.logger.debug('Parameter grant_type is', grantTypes.join(' '));
const client = await oauth2.model.client.fetchById(clientId);
if (!client) {
throw new InvalidClient('Client not found');
}
if (!(await oauth2.model.client.hasRedirectUri(client))) {
throw new UnsupportedResponseType('The client has not set a redirect uri');
} else if (
!(await oauth2.model.client.checkRedirectUri(client, redirectUri))
) {
throw new InvalidRequest('Wrong RedirectUri provided');
}
req.oauth2.logger.debug('redirect_uri check passed');
// The client needs to support all grant types
for (const grantType of grantTypes) {
if (
!oauth2.model.client.checkGrantType(client, grantType) &&
grantType !== 'none'
) {
throw new UnauthorizedClient(
'This client does not support grant type ' + grantType
);
}
}
req.oauth2.logger.debug('Grant type check passed');
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');
}
req.oauth2.logger.debug('Scope check passed');
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);
}
req.oauth2.logger.debug('User fetched from request');
}
const codeChallenge = req.query.code_challenge as string;
const codeChallengeMethod = (req.query.code_challenge_method as 'plain' | 'S256') || 'plain';
if (codeChallengeMethod && !['plain', 'S256'].includes(codeChallengeMethod)) {
throw new InvalidGrant('Invalid code challenge method');
}
const prompt = ((req.query.prompt || '') as string).split(' ');
let resObj: Record<string, string | number> = {};
let consented = false;
if (req.method === 'GET') {
// Check if the user has already consented to this client with this scope
consented = await oauth2.model.user.consented(
oauth2.model.user.getId(user),
oauth2.model.client.getId(client),
scope
);
if (!consented && prompt.includes('none')) {
throw new InteractionRequired('Interaction required!');
}
// Ask for consent
if (!consented || (
prompt.includes('login') ||
prompt.includes('consent') ||
prompt.includes('select_account')
)) {
return oauth2.decision(req, res, client, scope, user, redirectUri);
}
}
// Save consent
if (!consented) {
if (!req.body || typeof req.body.decision === 'undefined') {
throw new InvalidRequest('No decision parameter passed');
} else if (req.body.decision === '0') {
throw new AccessDenied('User denied access to the resource');
}
req.oauth2.logger.debug('Decision check passed');
await oauth2.model.user.consent(
oauth2.model.user.getId(user),
oauth2.model.client.getId(client),
scope
);
}
for (const i in grantTypes) {
let data = null;
switch (grantTypes[i]) {
case 'authorization_code':
data = await oauth2.model.code.create(
oauth2.model.user.getId(user),
oauth2.model.client.getId(client),
scope,
oauth2.model.code.ttl,
req.query.nonce as string,
codeChallenge,
codeChallengeMethod
);
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,
client,
scope,
req.query.nonce as string | undefined
);
resObj = {
id_token: data,
...resObj,
};
case 'none':
resObj = {};
break;
default:
throw new UnsupportedResponseType(
'Unknown response_type parameter passed'
);
}
}
// Return non-code response types as fragment instead of query
return dataResponse(req, res, resObj, redirectUri, responseType !== 'code');
}, true);