import { Authenticatable, AuthenticatableCredentials, AuthenticatableIdentifier, AuthenticatableRepository, } from '../../types' import {Inject, Injectable} from '../../../di' import { Awaitable, dataGetUnsafe, fetch, Maybe, MethodNotSupportedError, UniversalPath, universalPath, uuid4, } from '../../../util' import {OAuth2LoginConfig} from './OAuth2LoginController' import {Session} from '../../../http/session/Session' import {ResponseObject} from '../../../http/routing/Route' import {temporary} from '../../../http/response/TemporaryRedirectResponseFactory' import {Request} from '../../../http/lifecycle/Request' import {Logging} from '../../../service/Logging' import {OAuth2User} from './OAuth2User' @Injectable() export class OAuth2Repository implements AuthenticatableRepository { @Inject() protected readonly session!: Session @Inject() protected readonly request!: Request @Inject() protected readonly logging!: Logging constructor( protected readonly config: OAuth2LoginConfig, ) { } public createByCredentials(): Awaitable { throw new MethodNotSupportedError() } getByCredentials(credentials: AuthenticatableCredentials): Awaitable> { return this.getAuthenticatableFromBearer(credentials.credential) } getByIdentifier(id: AuthenticatableIdentifier): Awaitable> { return undefined } public getRedirectUrl(state?: string): UniversalPath { const url = universalPath(this.config.redirectUrl) if ( state ) { url.query.append('state', state) } return url } public getTokenEndpoint(): UniversalPath { return universalPath(this.config.tokenEndpoint) } public getUserEndpoint(): UniversalPath { return universalPath(this.config.userEndpoint) } public async redeem(): Promise> { if ( !this.stateIsValid() ) { return // FIXME throw } const body = new URLSearchParams() if ( this.config.tokenEndpointMapping ) { if ( this.config.tokenEndpointMapping.clientId ) { body.append(this.config.tokenEndpointMapping.clientId, this.config.clientId) } if ( this.config.tokenEndpointMapping.clientSecret ) { body.append(this.config.tokenEndpointMapping.clientSecret, this.config.clientSecret) } if ( this.config.tokenEndpointMapping.codeKey ) { body.append(this.config.tokenEndpointMapping.codeKey, String(this.request.input(this.config.authorizationCodeField))) } if ( this.config.tokenEndpointMapping.grantType ) { body.append(this.config.tokenEndpointMapping.grantType, 'authorization_code') } } this.logging.debug(`Redeeming auth code: ${body.toString()}`) const response = await fetch(this.getTokenEndpoint().toRemote, { method: 'post', body: body, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', }, }) const data = await response.json() if ( typeof data !== 'object' || data === null ) { throw new Error() } this.logging.debug(data) const bearer = String(dataGetUnsafe(data, this.config.tokenEndpointResponseMapping?.token ?? 'bearer')) this.logging.debug(bearer) if ( !bearer || typeof bearer !== 'string' ) { throw new Error() } return this.getAuthenticatableFromBearer(bearer) } public async getAuthenticatableFromBearer(bearer: string): Promise> { const response = await fetch(this.getUserEndpoint().toRemote, { method: 'get', headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${bearer}`, }, }) const data = await response.json() if ( typeof data !== 'object' || data === null ) { throw new Error() } return new OAuth2User(data, this.config) } public stateIsValid(): boolean { const correctState = this.session.get('extollo.auth.oauth2.state', '') const inputState = this.request.input('state') || '' return correctState === inputState } public shouldRedirect(): boolean { const codeField = this.config.authorizationCodeField const code = this.request.input(codeField) return !code } public async redirect(): Promise { const state = uuid4() await this.session.set('extollo.auth.oauth2.state', state) return temporary(this.getRedirectUrl(state).toRemote) } }