Add middleware and logic for bootstrapping the session auth
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
91abcdf8ef
commit
f00233d49a
@ -1,6 +1,12 @@
|
|||||||
import {Inject, Injectable} from '../di'
|
import {Inject, Injectable, Instantiable, StaticClass} from '../di'
|
||||||
import {Unit} from '../lifecycle/Unit'
|
import {Unit} from '../lifecycle/Unit'
|
||||||
import {Logging} from '../service/Logging'
|
import {Logging} from '../service/Logging'
|
||||||
|
import {CanonicalResolver} from '../service/Canonical'
|
||||||
|
import {Middleware} from '../http/routing/Middleware'
|
||||||
|
import {SessionAuthMiddleware} from './middleware/SessionAuthMiddleware'
|
||||||
|
import {AuthRequiredMiddleware} from './middleware/AuthRequiredMiddleware'
|
||||||
|
import {GuestRequiredMiddleware} from './middleware/GuestRequiredMiddleware'
|
||||||
|
import {Middlewares} from '../service/Middlewares'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit class that bootstraps the authentication framework.
|
* Unit class that bootstraps the authentication framework.
|
||||||
@ -10,7 +16,21 @@ export class Authentication extends Unit {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly logging!: Logging
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly middleware!: Middlewares
|
||||||
|
|
||||||
async up(): Promise<void> {
|
async up(): Promise<void> {
|
||||||
this.container()
|
this.container()
|
||||||
|
this.middleware.registerNamespace('@auth', this.getMiddlewareResolver())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getMiddlewareResolver(): CanonicalResolver<StaticClass<Middleware, Instantiable<Middleware>>> {
|
||||||
|
return (key: string) => {
|
||||||
|
return ({
|
||||||
|
web: SessionAuthMiddleware,
|
||||||
|
required: AuthRequiredMiddleware,
|
||||||
|
guest: GuestRequiredMiddleware,
|
||||||
|
})[key]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
src/auth/NotAuthorizedError.ts
Normal file
11
src/auth/NotAuthorizedError.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import {HTTPError} from '../http/HTTPError'
|
||||||
|
import {HTTPStatus} from '../util'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when a user attempts an action that they are not authorized to perform.
|
||||||
|
*/
|
||||||
|
export class NotAuthorizedError extends HTTPError {
|
||||||
|
constructor(message = 'Not Authorized') {
|
||||||
|
super(HTTPStatus.FORBIDDEN, message)
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import {Awaitable, Maybe} from '../util'
|
|||||||
import {Authenticatable, AuthenticatableRepository} from './types'
|
import {Authenticatable, AuthenticatableRepository} from './types'
|
||||||
import {UserAuthenticatedEvent} from './event/UserAuthenticatedEvent'
|
import {UserAuthenticatedEvent} from './event/UserAuthenticatedEvent'
|
||||||
import {UserFlushedEvent} from './event/UserFlushedEvent'
|
import {UserFlushedEvent} from './event/UserFlushedEvent'
|
||||||
|
import {UserAuthenticationResumedEvent} from './event/UserAuthenticationResumedEvent'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base-class for a context that authenticates users and manages security.
|
* Base-class for a context that authenticates users and manages security.
|
||||||
@ -101,6 +102,19 @@ export abstract class SecurityContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assuming a user is still authenticated in the context,
|
||||||
|
* try to look up and fill in the user.
|
||||||
|
*/
|
||||||
|
async resume(): Promise<void> {
|
||||||
|
const credentials = await this.getCredentials()
|
||||||
|
const user = await this.repository.getByCredentials(credentials)
|
||||||
|
if ( user ) {
|
||||||
|
this.authenticatedUser = user
|
||||||
|
await this.bus.dispatch(new UserAuthenticationResumedEvent(user, this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write the current state of the security context to whatever storage
|
* Write the current state of the security context to whatever storage
|
||||||
* medium the context's host provides.
|
* medium the context's host provides.
|
||||||
@ -119,4 +133,11 @@ export abstract class SecurityContext {
|
|||||||
getUser(): Maybe<Authenticatable> {
|
getUser(): Maybe<Authenticatable> {
|
||||||
return this.authenticatedUser
|
return this.authenticatedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there is a currently authenticated user.
|
||||||
|
*/
|
||||||
|
hasUser(): boolean {
|
||||||
|
return Boolean(this.authenticatedUser)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
25
src/auth/config.ts
Normal file
25
src/auth/config.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {Instantiable} from '../di'
|
||||||
|
import {ORMUserRepository} from './orm/ORMUserRepository'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inferface for type-checking the AuthenticatableRepositories values.
|
||||||
|
*/
|
||||||
|
export interface AuthenticatableRepositoryMapping {
|
||||||
|
orm: Instantiable<ORMUserRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String mapping of AuthenticatableRepository implementations.
|
||||||
|
*/
|
||||||
|
export const AuthenticatableRepositories: AuthenticatableRepositoryMapping = {
|
||||||
|
orm: ORMUserRepository,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for making the auth config type-safe.
|
||||||
|
*/
|
||||||
|
export interface AuthConfig {
|
||||||
|
repositories: {
|
||||||
|
session: keyof AuthenticatableRepositoryMapping,
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import {SecurityContext} from '../SecurityContext'
|
|||||||
import {Inject, Injectable} from '../../di'
|
import {Inject, Injectable} from '../../di'
|
||||||
import {Session} from '../../http/session/Session'
|
import {Session} from '../../http/session/Session'
|
||||||
import {Awaitable} from '../../util'
|
import {Awaitable} from '../../util'
|
||||||
|
import {AuthenticatableRepository} from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Security context implementation that uses the session as storage.
|
* Security context implementation that uses the session as storage.
|
||||||
@ -11,6 +12,13 @@ export class SessionSecurityContext extends SecurityContext {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly session!: Session
|
protected readonly session!: Session
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
/** The repository from which to draw users. */
|
||||||
|
protected readonly repository: AuthenticatableRepository,
|
||||||
|
) {
|
||||||
|
super(repository, 'session')
|
||||||
|
}
|
||||||
|
|
||||||
getCredentials(): Awaitable<Record<string, string>> {
|
getCredentials(): Awaitable<Record<string, string>> {
|
||||||
return {
|
return {
|
||||||
securityIdentifier: this.session.get('extollo.auth.securityIdentifier'),
|
securityIdentifier: this.session.get('extollo.auth.securityIdentifier'),
|
||||||
|
27
src/auth/event/UserAuthenticationResumedEvent.ts
Normal file
27
src/auth/event/UserAuthenticationResumedEvent.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import {Event} from '../../event/Event'
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
export class UserAuthenticationResumedEvent 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,13 +1,21 @@
|
|||||||
export * from './types'
|
export * from './types'
|
||||||
|
export * from './NotAuthorizedError'
|
||||||
|
|
||||||
export * from './SecurityContext'
|
export * from './SecurityContext'
|
||||||
|
|
||||||
export * from './event/UserAuthenticatedEvent'
|
export * from './event/UserAuthenticatedEvent'
|
||||||
export * from './event/UserFlushedEvent'
|
export * from './event/UserFlushedEvent'
|
||||||
|
export * from './event/UserAuthenticationResumedEvent'
|
||||||
|
|
||||||
export * from './contexts/SessionSecurityContext'
|
export * from './contexts/SessionSecurityContext'
|
||||||
|
|
||||||
export * from './orm/ORMUser'
|
export * from './orm/ORMUser'
|
||||||
export * from './orm/ORMUserRepository'
|
export * from './orm/ORMUserRepository'
|
||||||
|
|
||||||
|
export * from './middleware/AuthRequiredMiddleware'
|
||||||
|
export * from './middleware/GuestRequiredMiddleware'
|
||||||
|
export * from './middleware/SessionAuthMiddleware'
|
||||||
|
|
||||||
export * from './Authentication'
|
export * from './Authentication'
|
||||||
|
|
||||||
|
export * from './config'
|
||||||
|
19
src/auth/middleware/AuthRequiredMiddleware.ts
Normal file
19
src/auth/middleware/AuthRequiredMiddleware.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {Middleware} from '../../http/routing/Middleware'
|
||||||
|
import {Inject, Injectable} from '../../di'
|
||||||
|
import {SecurityContext} from '../SecurityContext'
|
||||||
|
import {ResponseObject} from '../../http/routing/Route'
|
||||||
|
import {error} from '../../http/response/ErrorResponseFactory'
|
||||||
|
import {NotAuthorizedError} from '../NotAuthorizedError'
|
||||||
|
import {HTTPStatus} from '../../util'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthRequiredMiddleware extends Middleware {
|
||||||
|
@Inject()
|
||||||
|
protected readonly security!: SecurityContext
|
||||||
|
|
||||||
|
async apply(): Promise<ResponseObject> {
|
||||||
|
if ( !this.security.hasUser() ) {
|
||||||
|
return error(new NotAuthorizedError(), HTTPStatus.FORBIDDEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
src/auth/middleware/GuestRequiredMiddleware.ts
Normal file
19
src/auth/middleware/GuestRequiredMiddleware.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {Middleware} from '../../http/routing/Middleware'
|
||||||
|
import {Inject, Injectable} from '../../di'
|
||||||
|
import {SecurityContext} from '../SecurityContext'
|
||||||
|
import {ResponseObject} from '../../http/routing/Route'
|
||||||
|
import {error} from '../../http/response/ErrorResponseFactory'
|
||||||
|
import {NotAuthorizedError} from '../NotAuthorizedError'
|
||||||
|
import {HTTPStatus} from '../../util'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GuestRequiredMiddleware extends Middleware {
|
||||||
|
@Inject()
|
||||||
|
protected readonly security!: SecurityContext
|
||||||
|
|
||||||
|
async apply(): Promise<ResponseObject> {
|
||||||
|
if ( this.security.hasUser() ) {
|
||||||
|
return error(new NotAuthorizedError(), HTTPStatus.FORBIDDEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
src/auth/middleware/SessionAuthMiddleware.ts
Normal file
34
src/auth/middleware/SessionAuthMiddleware.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import {Middleware} from '../../http/routing/Middleware'
|
||||||
|
import {Inject, Injectable} from '../../di'
|
||||||
|
import {ResponseObject} from '../../http/routing/Route'
|
||||||
|
import {Config} from '../../service/Config'
|
||||||
|
import {AuthenticatableRepository} from '../types'
|
||||||
|
import {SessionSecurityContext} from '../contexts/SessionSecurityContext'
|
||||||
|
import {SecurityContext} from '../SecurityContext'
|
||||||
|
import {ORMUserRepository} from '../orm/ORMUserRepository'
|
||||||
|
import {AuthConfig} from '../config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects a SessionSecurityContext into the request and attempts to
|
||||||
|
* resume the user's authentication.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class SessionAuthMiddleware extends Middleware {
|
||||||
|
@Inject()
|
||||||
|
protected readonly config!: Config
|
||||||
|
|
||||||
|
async apply(): Promise<ResponseObject> {
|
||||||
|
const context = <SessionSecurityContext> this.make(SessionSecurityContext, this.getRepository())
|
||||||
|
this.request.registerSingletonInstance(SecurityContext, context)
|
||||||
|
await context.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the correct AuthenticatableRepository based on the auth config.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected getRepository(): AuthenticatableRepository {
|
||||||
|
const config: AuthConfig | undefined = this.config.get('auth')
|
||||||
|
return this.make<AuthenticatableRepository>(config?.repositories?.session ?? ORMUserRepository)
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
import {Authenticatable, AuthenticatableIdentifier, AuthenticatableRepository} from '../types'
|
import {Authenticatable, AuthenticatableIdentifier, AuthenticatableRepository} from '../types'
|
||||||
import {Awaitable, Maybe} from '../../util'
|
import {Awaitable, Maybe} from '../../util'
|
||||||
import {ORMUser} from './ORMUser'
|
import {ORMUser} from './ORMUser'
|
||||||
import {Singleton} from '../../di'
|
import {Injectable} from '../../di'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A user repository implementation that looks up users stored in the database.
|
* A user repository implementation that looks up users stored in the database.
|
||||||
*/
|
*/
|
||||||
@Singleton()
|
@Injectable()
|
||||||
export class ORMUserRepository extends AuthenticatableRepository {
|
export class ORMUserRepository extends AuthenticatableRepository {
|
||||||
/** Look up the user by their username. */
|
/** Look up the user by their username. */
|
||||||
getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> {
|
getByIdentifier(id: AuthenticatableIdentifier): Awaitable<Maybe<Authenticatable>> {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {DependencyKey, InstanceRef, Instantiable, isInstantiable} from './types'
|
import {DependencyKey, InstanceRef, Instantiable, isInstantiable, StaticClass} from './types'
|
||||||
import {AbstractFactory} from './factory/AbstractFactory'
|
import {AbstractFactory} from './factory/AbstractFactory'
|
||||||
import {collect, Collection, globalRegistry, logIfDebugging} from '../util'
|
import {collect, Collection, globalRegistry, logIfDebugging} from '../util'
|
||||||
import {Factory} from './factory/Factory'
|
import {Factory} from './factory/Factory'
|
||||||
@ -132,7 +132,7 @@ export class Container {
|
|||||||
* @param staticClass
|
* @param staticClass
|
||||||
* @param instance
|
* @param instance
|
||||||
*/
|
*/
|
||||||
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T): this {
|
registerSingletonInstance<T>(staticClass: StaticClass<T, any> | Instantiable<T>, instance: T): this {
|
||||||
if ( this.resolve(staticClass) ) {
|
if ( this.resolve(staticClass) ) {
|
||||||
throw new DuplicateFactoryKeyError(staticClass)
|
throw new DuplicateFactoryKeyError(staticClass)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {Container, MaybeDependency, MaybeFactory} from './Container'
|
import {Container, MaybeDependency, MaybeFactory} from './Container'
|
||||||
import {DependencyKey, Instantiable} from './types'
|
import {DependencyKey, Instantiable, StaticClass} from './types'
|
||||||
import {AbstractFactory} from './factory/AbstractFactory'
|
import {AbstractFactory} from './factory/AbstractFactory'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,7 +113,7 @@ export class ScopedContainer extends Container {
|
|||||||
* @param staticClass
|
* @param staticClass
|
||||||
* @param instance
|
* @param instance
|
||||||
*/
|
*/
|
||||||
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T): this {
|
registerSingletonInstance<T>(staticClass: StaticClass<T, any> | Instantiable<T>, instance: T): this {
|
||||||
return this.withoutParentScopes(() => super.registerSingletonInstance(staticClass, instance))
|
return this.withoutParentScopes(() => super.registerSingletonInstance(staticClass, instance))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,14 +7,14 @@ import {Middleware} from '../http/routing/Middleware'
|
|||||||
* A canonical unit that loads the middleware classes from `app/http/middlewares`.
|
* A canonical unit that loads the middleware classes from `app/http/middlewares`.
|
||||||
*/
|
*/
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class Middlewares extends CanonicalStatic<Instantiable<Middleware>, Middleware> {
|
export class Middlewares extends CanonicalStatic<Middleware, Instantiable<Middleware>> {
|
||||||
protected appPath = ['http', 'middlewares']
|
protected appPath = ['http', 'middlewares']
|
||||||
|
|
||||||
protected canonicalItem = 'middleware'
|
protected canonicalItem = 'middleware'
|
||||||
|
|
||||||
protected suffix = '.middleware.js'
|
protected suffix = '.middleware.js'
|
||||||
|
|
||||||
public async initCanonicalItem(definition: CanonicalDefinition): Promise<StaticClass<Instantiable<Middleware>, Middleware>> {
|
public async initCanonicalItem(definition: CanonicalDefinition): Promise<StaticClass<Middleware, Instantiable<Middleware>>> {
|
||||||
const item = await super.initCanonicalItem(definition)
|
const item = await super.initCanonicalItem(definition)
|
||||||
if ( !(item.prototype instanceof Middleware) ) {
|
if ( !(item.prototype instanceof Middleware) ) {
|
||||||
throw new TypeError(`Invalid middleware definition: ${definition.originalName}. Controllers must extend from @extollo/lib.http.routing.Middleware.`)
|
throw new TypeError(`Invalid middleware definition: ${definition.originalName}. Controllers must extend from @extollo/lib.http.routing.Middleware.`)
|
||||||
|
Loading…
Reference in New Issue
Block a user