Add model serializer and coreid login provider
This commit is contained in:
100
src/auth/provider/oauth/CoreIDLoginProvider.ts
Normal file
100
src/auth/provider/oauth/CoreIDLoginProvider.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/* eslint camelcase: 0 */
|
||||
import {OAuth2LoginProvider, OAuth2LoginProviderConfig} from './OAuth2LoginProvider'
|
||||
import {Authenticatable} from '../../types'
|
||||
import {Request} from '../../../http/lifecycle/Request'
|
||||
import {ErrorWithContext, uuid4, fetch} from '../../../util'
|
||||
|
||||
/**
|
||||
* OAuth2LoginProvider implementation that authenticates users against a
|
||||
* Starship CoreID server.
|
||||
*/
|
||||
export class CoreIDLoginProvider extends OAuth2LoginProvider<OAuth2LoginProviderConfig> {
|
||||
protected async callback(request: Request): Promise<Authenticatable> {
|
||||
// Get authentication_code from the request
|
||||
const code = String(request.input('code') || '')
|
||||
if ( !code ) {
|
||||
throw new ErrorWithContext('Unable to authenticate user: missing login code', {
|
||||
input: request.input(),
|
||||
})
|
||||
}
|
||||
|
||||
// Get OAuth2 token from CoreID
|
||||
const token = await this.getToken(code)
|
||||
|
||||
// Get user from endpoint
|
||||
const userData = await this.getUserData(token)
|
||||
|
||||
// Return authenticatable instance
|
||||
const existing = await this.security.repository.getByIdentifier(userData.uid)
|
||||
if ( existing ) {
|
||||
this.updateUser(existing, userData)
|
||||
return existing
|
||||
}
|
||||
|
||||
const user = await this.security.repository.createFromCredentials(userData.uid, uuid4())
|
||||
this.updateUser(user, userData)
|
||||
return user
|
||||
}
|
||||
|
||||
/** Given an access token, look up the associated user's information. */
|
||||
protected async getUserData(token: string): Promise<any> {
|
||||
const userResponse = await fetch(
|
||||
this.config.userUrl,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const userData: any = await userResponse.json()
|
||||
if ( !userData?.data?.uid ) {
|
||||
throw new ErrorWithContext('Unable to extract user from response', {
|
||||
userData,
|
||||
})
|
||||
}
|
||||
|
||||
return userData.data
|
||||
}
|
||||
|
||||
/** Given a login code, redeem it for an access token. */
|
||||
protected async getToken(code: string): Promise<string> {
|
||||
const body: string[] = [
|
||||
'code=' + encodeURIComponent(code),
|
||||
'client_id=' + encodeURIComponent(this.config.clientId),
|
||||
'client_secret=' + encodeURIComponent(this.config.clientSecret),
|
||||
'grant_type=authorization_code',
|
||||
]
|
||||
|
||||
const response = await fetch(
|
||||
this.config.tokenUrl,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: body.join('&'),
|
||||
},
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
const token = (data as any).access_token
|
||||
if ( !token ) {
|
||||
throw new ErrorWithContext('Unable to obtain access token from response', {
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
return String(token)
|
||||
}
|
||||
|
||||
/** Update values on the Authenticatable from user data. */
|
||||
protected updateUser(user: any, data: any): void {
|
||||
user.firstName = data.first_name
|
||||
user.lastName = data.last_name
|
||||
user.email = data.email
|
||||
user.tagline = data.tagline
|
||||
user.photoUrl = data.profile_photo
|
||||
}
|
||||
}
|
||||
98
src/auth/provider/oauth/OAuth2LoginProvider.ts
Normal file
98
src/auth/provider/oauth/OAuth2LoginProvider.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import {LoginProvider, LoginProviderConfig} from '../LoginProvider'
|
||||
import {ResponseObject, Route} from '../../../http/routing/Route'
|
||||
import {Inject, Injectable} from '../../../di'
|
||||
import {Routing} from '../../../service/Routing'
|
||||
import {GuestRequiredMiddleware} from '../../middleware/GuestRequiredMiddleware'
|
||||
import {redirect} from '../../../http/response/RedirectResponseFactory'
|
||||
import {view} from '../../../http/response/ViewResponseFactory'
|
||||
import {Request} from '../../../http/lifecycle/Request'
|
||||
import {Awaitable} from '../../../util'
|
||||
import {Authenticatable} from '../../types'
|
||||
|
||||
export interface OAuth2LoginProviderConfig extends LoginProviderConfig {
|
||||
displayName: string,
|
||||
clientId: string|number
|
||||
clientSecret: string
|
||||
loginUrl: string
|
||||
loginMessage?: string
|
||||
logoutUrl?: string
|
||||
tokenUrl: string,
|
||||
userUrl: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* LoginProvider implementation for OAuth2-based logins.
|
||||
*/
|
||||
@Injectable()
|
||||
export abstract class OAuth2LoginProvider<TConfig extends OAuth2LoginProviderConfig> extends LoginProvider<TConfig> {
|
||||
@Inject()
|
||||
protected readonly routing!: Routing
|
||||
|
||||
public routes(): void {
|
||||
super.routes()
|
||||
|
||||
Route.any('redirect')
|
||||
.alias(`@auth:${this.name}:redirect`)
|
||||
.pre(GuestRequiredMiddleware)
|
||||
.handledBy(() => redirect(this.getLoginUrl()))
|
||||
|
||||
Route.any('callback')
|
||||
.alias(`@auth:${this.name}:callback`)
|
||||
.pre(GuestRequiredMiddleware)
|
||||
.passingRequest()
|
||||
.handledBy(this.handleCallback.bind(this))
|
||||
}
|
||||
|
||||
protected async handleCallback(request: Request): Promise<ResponseObject> {
|
||||
const user = await this.callback(request)
|
||||
if ( user ) {
|
||||
await this.security.authenticate(user)
|
||||
return this.redirectToIntendedRoute()
|
||||
}
|
||||
|
||||
return redirect(this.routing.getNamedPath(`@auth:${this.name}:login`).toRemote)
|
||||
}
|
||||
|
||||
/**
|
||||
* After redirecting back from the OAuth2 server, look up the user information.
|
||||
* @param request
|
||||
* @protected
|
||||
*/
|
||||
protected abstract callback(request: Request): Awaitable<Authenticatable>
|
||||
|
||||
public login(): ResponseObject {
|
||||
const buttonUrl = this.routing
|
||||
.getNamedPath(`@auth:${this.name}:redirect`)
|
||||
.toRemote
|
||||
|
||||
return view('@extollo:auth:message', {
|
||||
message: this.config.loginMessage ?? `Sign-in with ${this.config.displayName} to continue`,
|
||||
buttonText: 'Sign-in',
|
||||
buttonUrl,
|
||||
})
|
||||
}
|
||||
|
||||
public async logout(): Promise<ResponseObject> {
|
||||
await this.security.flush()
|
||||
|
||||
if ( this.config.logoutUrl ) {
|
||||
return redirect(this.config.logoutUrl)
|
||||
}
|
||||
|
||||
return view('@extollo:auth:message', {
|
||||
message: 'You have been signed-out',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL where the user should be redirected to sign-in.
|
||||
* @protected
|
||||
*/
|
||||
protected getLoginUrl(): string {
|
||||
const callbackRoute = this.routing.getNamedPath(`@auth:${this.name}:callback`)
|
||||
|
||||
return this.config.loginUrl
|
||||
.replace(/%c/g, String(this.config.clientId))
|
||||
.replace(/%r/g, callbackRoute.toRemote)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user