import {Controller} from '../../http/Controller' import {Injectable} from '../../di' import {ResponseObject, Route} from '../../http/routing/Route' import {Request} from '../../http/lifecycle/Request' import {Session} from '../../http/session/Session' import { ClientRepository, OAuth2Client, OAuth2FlowType, OAuth2Scope, RedemptionCodeRepository, ScopeRepository, } from './types' import {HTTPError} from '../../http/HTTPError' import {HTTPStatus, Maybe} from '../../util' import {view} from '../../http/response/ViewResponseFactory' import {SecurityContext} from '../context/SecurityContext' import {redirect} from '../../http/response/RedirectResponseFactory' import {AuthRequiredMiddleware} from '../middleware/AuthRequiredMiddleware' @Injectable() export class OAuth2Server extends Controller { public static routes(): void { Route.get('/oauth2/authorize') .alias('@oauth2:authorize') .pre(AuthRequiredMiddleware) .passingRequest() .calls(OAuth2Server, x => x.promptForAuthorization) Route.post('/oauth2/authorize') .alias('@oauth2:authorize:submit') .pre(AuthRequiredMiddleware) .passingRequest() .calls(OAuth2Server, x => x.authorizeAndRedirect) Route.post('/oauth2/redeem') .alias('@oauth2:authorize:redeem') .passingRequest() .calls(OAuth2Server, x => x.redeemToken) } async redeemToken(request: Request): Promise { const authParts = String(request.getHeader('Authorization')).split(':') if ( authParts.length !== 2 ) { throw new HTTPError(HTTPStatus.BAD_REQUEST) } const clientRepo = request.make(ClientRepository) const [clientId, clientSecret] = authParts const client = await clientRepo.find(clientId) if ( !client || client.secret !== clientSecret ) { throw new HTTPError(HTTPStatus.UNAUTHORIZED) } const codeRepo = request.make(RedemptionCodeRepository) const codeString = request.safe('code').string() const code = await codeRepo.find(codeString) if ( !code ) { throw new HTTPError(HTTPStatus.BAD_REQUEST) } } async authorizeAndRedirect(request: Request): Promise { // Look up the client in the client repo const session = request.make(Session) const clientId = session.safe('oauth2.authorize.clientId').string() const client = await this.getClient(request, clientId) const flowType = session.safe('oauth2.authorize.flow').in(client.allowedFlows) if ( flowType === OAuth2FlowType.code ) { return this.authorizeCodeFlow(request, client) } } protected async authorizeCodeFlow(request: Request, client: OAuth2Client): Promise { const session = request.make(Session) const security = request.make(SecurityContext) const codeRepository = request.make(RedemptionCodeRepository) const user = security.user() const scope = session.get('oauth2.authorize.scope') const redirectUri = session.safe('oauth2.authorize.redirectUri').in(client.allowedRedirectUris) // FIXME store authorization const code = await codeRepository.issue(user, client, scope) const uri = new URL(redirectUri) uri.searchParams.set('code', code.code) return redirect(uri) } async promptForAuthorization(request: Request): Promise { // Look up the client in the client repo const clientId = request.safe('client_id').string() const client = await this.getClient(request, clientId) // Make sure the requested flow type is valid for this client const session = request.make(Session) const flowType = request.safe('response_type').in(client.allowedFlows) const redirectUri = request.safe('redirect_uri').in(client.allowedRedirectUris) session.set('oauth2.authorize.clientId', client.id) session.set('oauth2.authorize.flow', flowType) session.set('oauth2.authorize.redirectUri', redirectUri) // Set the state if necessary const state = request.input('state') || '' if ( state ) { session.set('oauth2.authorize.state', String(state)) } else { session.forget('oauth2.authorize.state') } // If the request specified a scope, validate it and set it in the session const scope = await this.getScope(request, client) // Show a view prompting the user to approve the access return view('@extollo:oauth2:authorize', { clientName: client.display, scopeDescription: scope?.description, redirectDomain: (new URL(redirectUri)).host, }) } protected async getClient(request: Request, clientId: string): Promise { const clientRepo = request.make(ClientRepository) const client = await clientRepo.find(clientId) if ( !client ) { throw new HTTPError(HTTPStatus.BAD_REQUEST, 'Invalid client configuration', { clientId, }) } return client } protected async getScope(request: Request, client: OAuth2Client): Promise> { const session = request.make(Session) const scopeName = String(request.input('scope') || '') let scope: Maybe = undefined if ( scopeName ) { const scopeRepo = request.make(ScopeRepository) scope = await scopeRepo.findByName(scopeName) if ( !scope || !client.allowedScopeIds.includes(scope.id) ) { throw new HTTPError(HTTPStatus.BAD_REQUEST, 'Invalid scope', { scopeName, }) } session.set('oauth2.authorize.scope', scope.id) } else { session.forget('oauth2.authorize.state') } return scope } }