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 = {}; 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);