|
|
|
import {Controller} from '../../http/Controller'
|
|
|
|
import {Inject, 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,
|
|
|
|
TokenRepository,
|
|
|
|
} 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'
|
|
|
|
import {one} from '../../http/response/api'
|
|
|
|
import {AuthenticatableRepository} from '../types'
|
|
|
|
import {Logging} from '../../service/Logging'
|
|
|
|
|
|
|
|
export enum GrantType {
|
|
|
|
Client = 'client_credentials',
|
|
|
|
Password = 'password',
|
|
|
|
Code = 'authorization_code',
|
|
|
|
}
|
|
|
|
|
|
|
|
export const grantTypes: GrantType[] = [GrantType.Client, GrantType.Code, GrantType.Password]
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class OAuth2Server extends Controller {
|
|
|
|
@Inject()
|
|
|
|
protected readonly logging!: Logging
|
|
|
|
|
|
|
|
public static routes(): void {
|
|
|
|
Route.get('/oauth2/authorize')
|
|
|
|
.alias('@oauth2:authorize')
|
|
|
|
.pre(AuthRequiredMiddleware)
|
|
|
|
.passingRequest()
|
|
|
|
.calls<OAuth2Server>(OAuth2Server, x => x.promptForAuthorization)
|
|
|
|
|
|
|
|
Route.post('/oauth2/authorize')
|
|
|
|
.alias('@oauth2:authorize:submit')
|
|
|
|
.pre(AuthRequiredMiddleware)
|
|
|
|
.passingRequest()
|
|
|
|
.calls<OAuth2Server>(OAuth2Server, x => x.authorizeAndRedirect)
|
|
|
|
|
|
|
|
Route.post('/oauth2/token')
|
|
|
|
.alias('@oauth2:token')
|
|
|
|
.passingRequest()
|
|
|
|
.calls<OAuth2Server>(OAuth2Server, x => x.issue)
|
|
|
|
}
|
|
|
|
|
|
|
|
async issue(request: Request): Promise<ResponseObject> {
|
|
|
|
const grant = request.safe('grant_type').in(grantTypes)
|
|
|
|
const client = await this.getClientFromRequest(request)
|
|
|
|
|
|
|
|
if ( grant === GrantType.Client ) {
|
|
|
|
return this.issueFromClient(request, client)
|
|
|
|
} else if ( grant === GrantType.Code ) {
|
|
|
|
return this.issueFromCode(request, client)
|
|
|
|
} else if ( grant === GrantType.Password ) {
|
|
|
|
return this.issueFromCredential(request, client)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async issueFromCredential(request: Request, client: OAuth2Client): Promise<ResponseObject> {
|
|
|
|
const scope = String(this.request.input('scope') ?? '') || undefined
|
|
|
|
const username = this.request.safe('username').string()
|
|
|
|
const password = this.request.safe('password').string()
|
|
|
|
|
|
|
|
this.logging.verbose('Attempting password grant token issue...')
|
|
|
|
this.logging.verbose({
|
|
|
|
scope,
|
|
|
|
username,
|
|
|
|
client,
|
|
|
|
})
|
|
|
|
|
|
|
|
const userRepo = <AuthenticatableRepository> request.make(AuthenticatableRepository)
|
|
|
|
const user = await userRepo.getByIdentifier(username)
|
|
|
|
if ( !user || !(await user.validateCredential(password)) ) {
|
|
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST)
|
|
|
|
}
|
|
|
|
|
|
|
|
const tokenRepo = <TokenRepository> request.make(TokenRepository)
|
|
|
|
const token = await tokenRepo.issue(user, client, scope)
|
|
|
|
return one({
|
|
|
|
token: await tokenRepo.encode(token),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async issueFromCode(request: Request, client: OAuth2Client): Promise<ResponseObject> {
|
|
|
|
const scope = String(this.request.input('scope') ?? '') || undefined
|
|
|
|
const codeRepo = <RedemptionCodeRepository> request.make(RedemptionCodeRepository)
|
|
|
|
const codeString = request.safe('code').string()
|
|
|
|
const code = await codeRepo.find(codeString)
|
|
|
|
if ( !code ) {
|
|
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST)
|
|
|
|
}
|
|
|
|
|
|
|
|
const userRepo = <AuthenticatableRepository> request.make(AuthenticatableRepository)
|
|
|
|
const user = await userRepo.getByIdentifier(code.userId)
|
|
|
|
if ( !user ) {
|
|
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST)
|
|
|
|
}
|
|
|
|
|
|
|
|
const tokenRepo = <TokenRepository> request.make(TokenRepository)
|
|
|
|
const token = await tokenRepo.issue(user, client, scope)
|
|
|
|
return one({
|
|
|
|
token: await tokenRepo.encode(token),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async issueFromClient(request: Request, client: OAuth2Client): Promise<ResponseObject> {
|
|
|
|
const scope = String(this.request.input('scope') ?? '') || undefined
|
|
|
|
|
|
|
|
const tokenRepo = <TokenRepository> request.make(TokenRepository)
|
|
|
|
const token = await tokenRepo.issue(undefined, client, scope)
|
|
|
|
return one({
|
|
|
|
token: await tokenRepo.encode(token),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async getClientFromRequest(request: Request): Promise<OAuth2Client> {
|
|
|
|
const authParts = String(request.getHeader('Authorization')).split(':')
|
|
|
|
if ( authParts.length !== 2 ) {
|
|
|
|
throw new HTTPError(HTTPStatus.BAD_REQUEST)
|
|
|
|
}
|
|
|
|
|
|
|
|
this.logging.debug('Client auth parts:')
|
|
|
|
this.logging.debug(authParts)
|
|
|
|
|
|
|
|
const clientRepo = <ClientRepository> request.make(ClientRepository)
|
|
|
|
const [clientId, clientSecret] = authParts
|
|
|
|
const client = await clientRepo.find(clientId)
|
|
|
|
|
|
|
|
this.logging.verbose('Client:')
|
|
|
|
this.logging.verbose(client)
|
|
|
|
|
|
|
|
if ( !client || client.secret !== clientSecret ) {
|
|
|
|
throw new HTTPError(HTTPStatus.UNAUTHORIZED)
|
|
|
|
}
|
|
|
|
|
|
|
|
return client
|
|
|
|
}
|
|
|
|
|
|
|
|
async authorizeAndRedirect(request: Request): Promise<ResponseObject> {
|
|
|
|
// Look up the client in the client repo
|
|
|
|
const session = <Session> request.make(Session)
|
|
|
|
const client = await this.getClientFromRequest(request)
|
|
|
|
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<ResponseObject> {
|
|
|
|
const session = <Session> request.make(Session)
|
|
|
|
const security = <SecurityContext> request.make(SecurityContext)
|
|
|
|
const codeRepository = <RedemptionCodeRepository> 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<ResponseObject> {
|
|
|
|
// 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 = <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<OAuth2Client> {
|
|
|
|
const clientRepo = <ClientRepository> 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<Maybe<OAuth2Scope>> {
|
|
|
|
const session = <Session> request.make(Session)
|
|
|
|
const scopeName = String(request.input('scope') || '')
|
|
|
|
let scope: Maybe<OAuth2Scope> = undefined
|
|
|
|
if ( scopeName ) {
|
|
|
|
const scopeRepo = <ScopeRepository> 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
|
|
|
|
}
|
|
|
|
}
|