Rework authentication system
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
bd7d6a2dbd
commit
5175d64e36
@ -1,20 +0,0 @@
|
|||||||
import {FormRequest, ValidationRules} from '../../forms'
|
|
||||||
import {Is, Str} from '../../forms/rules/rules'
|
|
||||||
import {Singleton} from '../../di'
|
|
||||||
import {AuthenticatableCredentials} from '../types'
|
|
||||||
|
|
||||||
@Singleton()
|
|
||||||
export class BasicLoginFormRequest extends FormRequest<AuthenticatableCredentials> {
|
|
||||||
protected getRules(): ValidationRules {
|
|
||||||
return {
|
|
||||||
identifier: [
|
|
||||||
Is.required,
|
|
||||||
Str.lengthMin(1),
|
|
||||||
],
|
|
||||||
credential: [
|
|
||||||
Is.required,
|
|
||||||
Str.lengthMin(1),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
import {FormRequest, ValidationRules} from '../../forms'
|
|
||||||
import {Is, Str} from '../../forms/rules/rules'
|
|
||||||
import {Singleton} from '../../di'
|
|
||||||
import {AuthenticatableCredentials} from '../types'
|
|
||||||
|
|
||||||
@Singleton()
|
|
||||||
export class BasicRegisterFormRequest extends FormRequest<AuthenticatableCredentials> {
|
|
||||||
protected getRules(): ValidationRules {
|
|
||||||
return {
|
|
||||||
identifier: [
|
|
||||||
Is.required,
|
|
||||||
Str.lengthMin(1),
|
|
||||||
Str.alphaNum,
|
|
||||||
],
|
|
||||||
credential: [
|
|
||||||
Is.required,
|
|
||||||
Str.lengthMin(8),
|
|
||||||
Str.confirmed,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +1,41 @@
|
|||||||
import {Instantiable} from '../di'
|
import {Instantiable, isInstantiable} from '../di'
|
||||||
import {ORMUserRepository} from './orm/ORMUserRepository'
|
import {AuthenticatableRepository} from './types'
|
||||||
import {OAuth2LoginConfig} from './external/oauth2/OAuth2LoginController'
|
import {hasOwnProperty} from '../util'
|
||||||
|
|
||||||
/**
|
|
||||||
* Inferface for type-checking the AuthenticatableRepositories values.
|
|
||||||
*/
|
|
||||||
export interface AuthenticatableRepositoryMapping {
|
|
||||||
orm: Instantiable<ORMUserRepository>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* String mapping of AuthenticatableRepository implementations.
|
|
||||||
*/
|
|
||||||
export const AuthenticatableRepositories: AuthenticatableRepositoryMapping = {
|
|
||||||
orm: ORMUserRepository,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
export interface AuthenticationConfig {
|
||||||
* Interface for making the auth config type-safe.
|
storage: Instantiable<AuthenticatableRepository>,
|
||||||
*/
|
|
||||||
export interface AuthConfig {
|
|
||||||
repositories: {
|
|
||||||
session: keyof AuthenticatableRepositoryMapping,
|
|
||||||
},
|
|
||||||
sources?: {
|
sources?: {
|
||||||
[key: string]: OAuth2LoginConfig,
|
[key: string]: Instantiable<AuthenticatableRepository>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAuthenticationConfig(what: unknown): what is AuthenticationConfig {
|
||||||
|
if ( typeof what !== 'object' || !what ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !hasOwnProperty(what, 'storage') || !hasOwnProperty(what, 'sources') ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !isInstantiable(what.storage) || !(what.storage.prototype instanceof AuthenticatableRepository) ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( typeof what.sources !== 'object' ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( const key in what.sources ) {
|
||||||
|
if ( !hasOwnProperty(what.sources, key) ) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = what.sources[key]
|
||||||
|
if ( !isInstantiable(source) || !(source.prototype instanceof AuthenticatableRepository) ) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
import {SecurityContext} from './SecurityContext'
|
||||||
|
import {Inject, Injectable} from '../../di'
|
||||||
|
import {Session} from '../../http/session/Session'
|
||||||
|
import {Awaitable} from '../../util'
|
||||||
|
import {AuthenticatableRepository} from '../types'
|
||||||
|
import {UserAuthenticationResumedEvent} from '../event/UserAuthenticationResumedEvent'
|
||||||
|
|
||||||
|
export const EXTOLLO_AUTH_SESSION_KEY = '@extollo:auth.securityIdentifier'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security context implementation that uses the session as storage.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class SessionSecurityContext extends SecurityContext {
|
||||||
|
@Inject()
|
||||||
|
protected readonly session!: Session
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
/** The repository from which to draw users. */
|
||||||
|
public readonly repository: AuthenticatableRepository,
|
||||||
|
) {
|
||||||
|
super(repository, 'session')
|
||||||
|
}
|
||||||
|
|
||||||
|
persist(): Awaitable<void> {
|
||||||
|
this.session.set(EXTOLLO_AUTH_SESSION_KEY, this.getUser()?.getIdentifier())
|
||||||
|
}
|
||||||
|
|
||||||
|
async resume(): Promise<void> {
|
||||||
|
const identifier = this.session.get(EXTOLLO_AUTH_SESSION_KEY)
|
||||||
|
if ( identifier ) {
|
||||||
|
const user = await this.repository.getByIdentifier(identifier)
|
||||||
|
if ( user ) {
|
||||||
|
this.authenticatedUser = user
|
||||||
|
await this.bus.dispatch(new UserAuthenticationResumedEvent(user, this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
import {SecurityContext} from '../SecurityContext'
|
|
||||||
import {Inject, Injectable} from '../../di'
|
|
||||||
import {Session} from '../../http/session/Session'
|
|
||||||
import {Awaitable} from '../../util'
|
|
||||||
import {AuthenticatableCredentials, AuthenticatableRepository} from '../types'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Security context implementation that uses the session as storage.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class SessionSecurityContext extends SecurityContext {
|
|
||||||
@Inject()
|
|
||||||
protected readonly session!: Session
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
/** The repository from which to draw users. */
|
|
||||||
public readonly repository: AuthenticatableRepository,
|
|
||||||
) {
|
|
||||||
super(repository, 'session')
|
|
||||||
}
|
|
||||||
|
|
||||||
getCredentials(): Awaitable<AuthenticatableCredentials> {
|
|
||||||
return {
|
|
||||||
identifier: '',
|
|
||||||
credential: this.session.get('extollo.auth.securityIdentifier'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
persist(): Awaitable<void> {
|
|
||||||
this.session.set('extollo.auth.securityIdentifier', this.getUser()?.getIdentifier())
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,27 @@
|
|||||||
|
import {Event} from '../../event/Event'
|
||||||
|
import {SecurityContext} from '../context/SecurityContext'
|
||||||
|
import {Awaitable, JSONState} from '../../util'
|
||||||
|
import {Authenticatable} from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event fired when a user is authenticated.
|
||||||
|
*/
|
||||||
|
export class AuthenticationEvent extends Event {
|
||||||
|
constructor(
|
||||||
|
public readonly user: Authenticatable,
|
||||||
|
public readonly context: SecurityContext,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
async dehydrate(): Promise<JSONState> {
|
||||||
|
return {
|
||||||
|
user: await this.user.dehydrate(),
|
||||||
|
contextName: this.context.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rehydrate(state: JSONState): Awaitable<void> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
// TODO fill this in
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,6 @@
|
|||||||
import {Event} from '../../event/Event'
|
import {AuthenticationEvent} from './AuthenticationEvent'
|
||||||
import {SecurityContext} from '../SecurityContext'
|
|
||||||
import {Awaitable, JSONState} from '../../util'
|
|
||||||
import {Authenticatable} from '../types'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event fired when a user is authenticated.
|
* Event fired when a user is authenticated.
|
||||||
*/
|
*/
|
||||||
export class UserAuthenticatedEvent extends Event {
|
export class UserAuthenticatedEvent extends AuthenticationEvent {}
|
||||||
constructor(
|
|
||||||
public readonly user: Authenticatable,
|
|
||||||
public readonly context: SecurityContext,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
async dehydrate(): Promise<JSONState> {
|
|
||||||
return {
|
|
||||||
user: await this.user.dehydrate(),
|
|
||||||
contextName: this.context.name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rehydrate(state: JSONState): Awaitable<void> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
// TODO fill this in
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,27 +1,6 @@
|
|||||||
import {Event} from '../../event/Event'
|
import {AuthenticationEvent} from './AuthenticationEvent'
|
||||||
import {SecurityContext} from '../SecurityContext'
|
|
||||||
import {Awaitable, JSONState} from '../../util'
|
|
||||||
import {Authenticatable} from '../types'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event fired when a security context for a given user is resumed.
|
* Event raised when a user is re-authenticated to a security context
|
||||||
*/
|
*/
|
||||||
export class UserAuthenticationResumedEvent extends Event {
|
export class UserAuthenticationResumedEvent extends AuthenticationEvent {}
|
||||||
constructor(
|
|
||||||
public readonly user: Authenticatable,
|
|
||||||
public readonly context: SecurityContext,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
async dehydrate(): Promise<JSONState> {
|
|
||||||
return {
|
|
||||||
user: await this.user.dehydrate(),
|
|
||||||
contextName: this.context.name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rehydrate(state: JSONState): Awaitable<void> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
// TODO fill this in
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,27 +1,6 @@
|
|||||||
import {Event} from '../../event/Event'
|
import {AuthenticationEvent} from './AuthenticationEvent'
|
||||||
import {SecurityContext} from '../SecurityContext'
|
|
||||||
import {Awaitable, JSONState} from '../../util'
|
|
||||||
import {Authenticatable} from '../types'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event fired when a user is unauthenticated.
|
* Event fired when a user is unauthenticated.
|
||||||
*/
|
*/
|
||||||
export class UserFlushedEvent extends Event {
|
export class UserFlushedEvent extends AuthenticationEvent {}
|
||||||
constructor(
|
|
||||||
public readonly user: Authenticatable,
|
|
||||||
public readonly context: SecurityContext,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
async dehydrate(): Promise<JSONState> {
|
|
||||||
return {
|
|
||||||
user: await this.user.dehydrate(),
|
|
||||||
contextName: this.context.name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rehydrate(state: JSONState): Awaitable<void> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
// TODO fill this in
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
import {Controller} from '../../../http/Controller'
|
|
||||||
import {Inject, Injectable} from '../../../di'
|
|
||||||
import {Config} from '../../../service/Config'
|
|
||||||
import {Request} from '../../../http/lifecycle/Request'
|
|
||||||
import {ResponseObject, Route} from '../../../http/routing/Route'
|
|
||||||
import {ErrorWithContext} from '../../../util'
|
|
||||||
import {OAuth2Repository} from './OAuth2Repository'
|
|
||||||
import {json} from '../../../http/response/JSONResponseFactory'
|
|
||||||
|
|
||||||
export interface OAuth2LoginConfig {
|
|
||||||
name: string,
|
|
||||||
clientId: string,
|
|
||||||
clientSecret: string,
|
|
||||||
redirectUrl: string,
|
|
||||||
authorizationCodeField: string,
|
|
||||||
tokenEndpoint: string,
|
|
||||||
tokenEndpointMapping?: {
|
|
||||||
clientId?: string,
|
|
||||||
clientSecret?: string,
|
|
||||||
grantType?: string,
|
|
||||||
codeKey?: string,
|
|
||||||
},
|
|
||||||
tokenEndpointResponseMapping?: {
|
|
||||||
token?: string,
|
|
||||||
expiresIn?: string,
|
|
||||||
expiresAt?: string,
|
|
||||||
},
|
|
||||||
userEndpoint: string,
|
|
||||||
userEndpointResponseMapping?: {
|
|
||||||
identifier?: string,
|
|
||||||
display?: string,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isOAuth2LoginConfig(what: unknown): what is OAuth2LoginConfig {
|
|
||||||
return (
|
|
||||||
Boolean(what)
|
|
||||||
&& typeof (what as any).name === 'string'
|
|
||||||
&& typeof (what as any).clientId === 'string'
|
|
||||||
&& typeof (what as any).clientSecret === 'string'
|
|
||||||
&& typeof (what as any).redirectUrl === 'string'
|
|
||||||
&& typeof (what as any).authorizationCodeField === 'string'
|
|
||||||
&& typeof (what as any).tokenEndpoint === 'string'
|
|
||||||
&& typeof (what as any).userEndpoint === 'string'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class OAuth2LoginController extends Controller {
|
|
||||||
public static routes(configName: string): void {
|
|
||||||
Route.group(`/auth/${configName}`, () => {
|
|
||||||
Route.get('login', (request: Request) => {
|
|
||||||
const controller = <OAuth2LoginController> request.make(OAuth2LoginController, configName)
|
|
||||||
return controller.getLogin()
|
|
||||||
}).pre('@auth:guest')
|
|
||||||
}).pre('@auth:web')
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
protected readonly config!: Config
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected readonly request: Request,
|
|
||||||
protected readonly configName: string,
|
|
||||||
) {
|
|
||||||
super(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getLogin(): Promise<ResponseObject> {
|
|
||||||
const repo = this.getRepository()
|
|
||||||
if ( repo.shouldRedirect() ) {
|
|
||||||
return repo.redirect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// We were redirected from the auth source
|
|
||||||
const user = await repo.redeem()
|
|
||||||
return json(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getRepository(): OAuth2Repository {
|
|
||||||
return this.request.make(OAuth2Repository, this.getConfig())
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getConfig(): OAuth2LoginConfig {
|
|
||||||
const config = this.config.get(`auth.sources.${this.configName}`)
|
|
||||||
if ( !isOAuth2LoginConfig(config) ) {
|
|
||||||
throw new ErrorWithContext('Invalid OAuth2 source config.', {
|
|
||||||
configName: this.configName,
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,155 +0,0 @@
|
|||||||
import {
|
|
||||||
Authenticatable,
|
|
||||||
AuthenticatableCredentials,
|
|
||||||
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(): 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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
import {Authenticatable, AuthenticatableIdentifier} from '../../types'
|
|
||||||
import {OAuth2LoginConfig} from './OAuth2LoginController'
|
|
||||||
import {Awaitable, dataGetUnsafe, InvalidJSONStateError, JSONState} from '../../../util'
|
|
||||||
|
|
||||||
export class OAuth2User implements Authenticatable {
|
|
||||||
protected displayField: string
|
|
||||||
|
|
||||||
protected identifierField: string
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected data: {[key: string]: any},
|
|
||||||
config: OAuth2LoginConfig,
|
|
||||||
) {
|
|
||||||
this.displayField = config.userEndpointResponseMapping?.display || 'name'
|
|
||||||
this.identifierField = config.userEndpointResponseMapping?.identifier || 'id'
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisplayIdentifier(): string {
|
|
||||||
return String(dataGetUnsafe(this.data, this.displayField || 'name', ''))
|
|
||||||
}
|
|
||||||
|
|
||||||
getIdentifier(): AuthenticatableIdentifier {
|
|
||||||
return String(dataGetUnsafe(this.data, this.identifierField || 'id', ''))
|
|
||||||
}
|
|
||||||
|
|
||||||
async dehydrate(): Promise<JSONState> {
|
|
||||||
return {
|
|
||||||
isOAuth2User: true,
|
|
||||||
data: this.data,
|
|
||||||
displayField: this.displayField,
|
|
||||||
identifierField: this.identifierField,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rehydrate(state: JSONState): Awaitable<void> {
|
|
||||||
if (
|
|
||||||
!state.isOAuth2User
|
|
||||||
|| typeof state.data !== 'object'
|
|
||||||
|| state.data === null
|
|
||||||
|| typeof state.displayField !== 'string'
|
|
||||||
|| typeof state.identifierField !== 'string'
|
|
||||||
) {
|
|
||||||
throw new InvalidJSONStateError('OAuth2User state is invalid', { state })
|
|
||||||
}
|
|
||||||
|
|
||||||
this.data = state.data
|
|
||||||
this.identifierField = state.identifierField
|
|
||||||
this.displayField = state.identifierField
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
import {
|
|
||||||
Authenticatable,
|
|
||||||
AuthenticatableCredentials,
|
|
||||||
AuthenticatableIdentifier,
|
|
||||||
AuthenticatableRepository,
|
|
||||||
} from '../types'
|
|
||||||
import {Awaitable, Maybe} from '../../util'
|
|
||||||
import {ORMUser} from './ORMUser'
|
|
||||||
import {Container, Inject, Injectable} from '../../di'
|
|
||||||
import {AuthenticatableAlreadyExistsError} from '../AuthenticatableAlreadyExistsError'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A user repository implementation that looks up users stored in the database.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class ORMUserRepository extends AuthenticatableRepository {
|
|
||||||
@Inject('injector')
|
|
||||||
protected readonly injector!: Container
|
|
||||||
|
|
||||||
/** Look up the user by their username. */
|
|
||||||
getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> {
|
|
||||||
return ORMUser.query<ORMUser>()
|
|
||||||
.where('username', '=', id)
|
|
||||||
.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to look up a user by the credentials provided.
|
|
||||||
* If a securityIdentifier is specified, look up the user by username.
|
|
||||||
* If username/password are specified, look up the user and verify the password.
|
|
||||||
* @param credentials
|
|
||||||
*/
|
|
||||||
async getByCredentials(credentials: AuthenticatableCredentials): Promise<Maybe<Authenticatable>> {
|
|
||||||
if ( !credentials.identifier && credentials.credential ) {
|
|
||||||
return ORMUser.query<ORMUser>()
|
|
||||||
.where('username', '=', credentials.credential)
|
|
||||||
.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( credentials.identifier && credentials.credential ) {
|
|
||||||
const user = await ORMUser.query<ORMUser>()
|
|
||||||
.where('username', '=', credentials.identifier)
|
|
||||||
.first()
|
|
||||||
|
|
||||||
if ( user && await user.verifyPassword(credentials.credential) ) {
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createByCredentials(credentials: AuthenticatableCredentials): Promise<Authenticatable> {
|
|
||||||
if ( await this.getByCredentials(credentials) ) {
|
|
||||||
throw new AuthenticatableAlreadyExistsError(`Authenticatable already exists with credentials.`, {
|
|
||||||
identifier: credentials.identifier,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = <ORMUser> this.injector.make(ORMUser)
|
|
||||||
user.username = credentials.identifier
|
|
||||||
await user.setPassword(credentials.credential)
|
|
||||||
await user.save()
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
Authenticatable,
|
||||||
|
AuthenticatableIdentifier,
|
||||||
|
AuthenticatableRepository,
|
||||||
|
} from '../../types'
|
||||||
|
import {Awaitable, Maybe, uuid4} from '../../../util'
|
||||||
|
import {ORMUser} from './ORMUser'
|
||||||
|
import {Container, Inject, Injectable} from '../../../di'
|
||||||
|
import {AuthenticatableAlreadyExistsError} from '../../AuthenticatableAlreadyExistsError'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A user repository implementation that looks up users stored in the database.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ORMUserRepository extends AuthenticatableRepository {
|
||||||
|
@Inject('injector')
|
||||||
|
protected readonly injector!: Container
|
||||||
|
|
||||||
|
/** Look up the user by their username. */
|
||||||
|
getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> {
|
||||||
|
return (this.injector.getStaticOverride(ORMUser) as typeof ORMUser).query<ORMUser>()
|
||||||
|
.where('username', '=', id)
|
||||||
|
.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if this repository supports registering users. */
|
||||||
|
supportsRegistration(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a user in this repository from basic credentials. */
|
||||||
|
async createFromCredentials(username: string, password: string): Promise<Authenticatable> {
|
||||||
|
if ( await this.getByIdentifier(username) ) {
|
||||||
|
throw new AuthenticatableAlreadyExistsError(`Authenticatable already exists with credentials.`, {
|
||||||
|
username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = <ORMUser> this.injector.makeByStaticOverride(ORMUser)
|
||||||
|
user.username = username
|
||||||
|
await user.setPassword(password)
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a user in this repository from an external Authenticatable instance. */
|
||||||
|
async createFromExternal(user: Authenticatable): Promise<Authenticatable> {
|
||||||
|
return this.createFromCredentials(String(user.getUniqueIdentifier()), uuid4())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import {FormRequest, ValidationRules} from '../../../forms'
|
||||||
|
import {Is, Str} from '../../../forms/rules/rules'
|
||||||
|
import {Singleton} from '../../../di'
|
||||||
|
|
||||||
|
export interface BasicLoginFormData {
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton()
|
||||||
|
export class BasicLoginFormRequest extends FormRequest<BasicLoginFormData> {
|
||||||
|
protected getRules(): ValidationRules {
|
||||||
|
return {
|
||||||
|
username: [
|
||||||
|
Is.required,
|
||||||
|
Str.lengthMin(1),
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
Is.required,
|
||||||
|
Str.lengthMin(1),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import {FormRequest, ValidationRules} from '../../../forms'
|
||||||
|
import {Is, Str} from '../../../forms/rules/rules'
|
||||||
|
import {Singleton} from '../../../di'
|
||||||
|
|
||||||
|
export interface BasicRegisterFormData {
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton()
|
||||||
|
export class BasicRegisterFormRequest extends FormRequest<BasicRegisterFormData> {
|
||||||
|
protected getRules(): ValidationRules {
|
||||||
|
return {
|
||||||
|
username: [
|
||||||
|
Is.required,
|
||||||
|
Str.lengthMin(1),
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
Is.required,
|
||||||
|
Str.lengthMin(1),
|
||||||
|
Str.confirmed,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
html
|
||||||
|
head
|
||||||
|
meta(http-equiv='Refresh' content='0; url="' + redirectUrl)
|
||||||
|
title Redirecting...
|
||||||
|
body
|
||||||
|
script.
|
||||||
|
var url = `${redirectUrl}`
|
||||||
|
window.location.assign(url)
|
Loading…
Reference in new issue