You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
157 lines
4.8 KiB
157 lines
4.8 KiB
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<Authenticatable> {
|
|
throw new MethodNotSupportedError()
|
|
}
|
|
|
|
getByCredentials(credentials: AuthenticatableCredentials): Awaitable<Maybe<Authenticatable>> {
|
|
return this.getAuthenticatableFromBearer(credentials.credential)
|
|
}
|
|
|
|
getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> {
|
|
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<Maybe<OAuth2User>> {
|
|
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<Maybe<OAuth2User>> {
|
|
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<ResponseObject> {
|
|
const state = uuid4()
|
|
await this.session.set('extollo.auth.oauth2.state', state)
|
|
return temporary(this.getRedirectUrl(state).toRemote)
|
|
}
|
|
}
|