Support periodic auth checks in SecurityContext on web socket connections
This commit is contained in:
parent
efb9726470
commit
e339ec718d
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@extollo/lib",
|
||||
"version": "0.13.6",
|
||||
"version": "0.13.7",
|
||||
"description": "The framework library that lifts up your code.",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
|
@ -80,6 +80,9 @@ export abstract class SecurityContext {
|
||||
/**
|
||||
* Assuming a user is still authenticated in the context,
|
||||
* try to look up and fill in the user.
|
||||
*
|
||||
* If there is NO USER to be resumed, then the method should flush
|
||||
* the user from this context.
|
||||
*/
|
||||
abstract resume(): Awaitable<void>
|
||||
|
||||
|
@ -33,7 +33,10 @@ export class SessionSecurityContext extends SecurityContext {
|
||||
if ( user ) {
|
||||
this.authenticatedUser = user
|
||||
await this.bus.push(new UserAuthenticationResumedEvent(user, this))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.authenticatedUser = undefined
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,9 @@ export class TokenSecurityContext extends SecurityContext {
|
||||
if ( user ) {
|
||||
this.authenticatedUser = user
|
||||
await this.bus.push(new UserAuthenticationResumedEvent(user, this))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.authenticatedUser = undefined
|
||||
}
|
||||
}
|
||||
|
27
src/auth/event/AuthCheckFailed.ts
Normal file
27
src/auth/event/AuthCheckFailed.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {BaseEvent, BaseSerializer, ObjectSerializer} from '../../support/bus'
|
||||
import {Awaitable} from '../../util'
|
||||
|
||||
/** An event raised when a required auth check has failed. */
|
||||
export class AuthCheckFailed extends BaseEvent {
|
||||
eventName = '@extollo/lib:AuthCheckFailed'
|
||||
}
|
||||
|
||||
/** Serializes AuthCheckFailed events. */
|
||||
@ObjectSerializer()
|
||||
export class AuthCheckFailedSerializer extends BaseSerializer<AuthCheckFailed, { authCheckFailed: true }> {
|
||||
protected decodeSerial(): Awaitable<AuthCheckFailed> {
|
||||
return new AuthCheckFailed()
|
||||
}
|
||||
|
||||
protected encodeActual(): Awaitable<{ authCheckFailed: true }> {
|
||||
return { authCheckFailed: true }
|
||||
}
|
||||
|
||||
protected getName(): string {
|
||||
return '@extollo/lib:AuthCheckFailedSerializer'
|
||||
}
|
||||
|
||||
matchActual(some: AuthCheckFailed): boolean {
|
||||
return some instanceof AuthCheckFailed
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ export * from './event/AuthenticationEvent'
|
||||
export * from './event/UserAuthenticatedEvent'
|
||||
export * from './event/UserAuthenticationResumedEvent'
|
||||
export * from './event/UserFlushedEvent'
|
||||
export * from './event/AuthCheckFailed'
|
||||
|
||||
export * from './middleware/AuthRequiredMiddleware'
|
||||
export * from './middleware/GuestRequiredMiddleware'
|
||||
@ -33,6 +34,8 @@ export * from './repository/orm/ORMUserRepository'
|
||||
|
||||
export * from './config'
|
||||
|
||||
export * from './webSocketAuthCheck'
|
||||
|
||||
export * from './server/types'
|
||||
export * from './server/models/OAuth2TokenModel'
|
||||
export * from './server/repositories/ConfigClientRepository'
|
||||
|
36
src/auth/webSocketAuthCheck.ts
Normal file
36
src/auth/webSocketAuthCheck.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import {Container} from '../di'
|
||||
import {RequestLocalStorage} from '../http/RequestLocalStorage'
|
||||
import {Session} from '../http/session/Session'
|
||||
import {Logging} from '../service/Logging'
|
||||
import {SecurityContext} from './context/SecurityContext'
|
||||
import {Bus} from '../support/bus'
|
||||
import {AuthCheckFailed} from './event/AuthCheckFailed'
|
||||
|
||||
/**
|
||||
* Check if the security context for the current request's web socket is still valid.
|
||||
* If not, raise an `AuthCheckFailed` event. This is meant to be used as a subscriber
|
||||
* to `WebSocketHealthCheckEvent` on the request.
|
||||
*
|
||||
* @see AuthCheckFailed
|
||||
*/
|
||||
export async function webSocketAuthCheck(): Promise<void> {
|
||||
const request = Container.getContainer()
|
||||
.make<RequestLocalStorage>(RequestLocalStorage)
|
||||
.get()
|
||||
|
||||
const logging = request.make<Logging>(Logging)
|
||||
|
||||
try {
|
||||
// Try to re-load the session in case we're using the SessionSecurityContext
|
||||
await request.make<Session>(Session).load()
|
||||
} catch (e: unknown) {
|
||||
logging.error(e)
|
||||
}
|
||||
|
||||
const security = request.make<SecurityContext>(SecurityContext)
|
||||
await security.resume()
|
||||
|
||||
if ( !security.hasUser() ) {
|
||||
await request.make<Bus>(Bus).push(new AuthCheckFailed())
|
||||
}
|
||||
}
|
11
src/http/lifecycle/WebSocketHealthCheckEvent.ts
Normal file
11
src/http/lifecycle/WebSocketHealthCheckEvent.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import {Event} from '../../support/bus'
|
||||
import {uuid4} from '../../util'
|
||||
|
||||
/** Event used to tell the server to close the websocket connection. */
|
||||
export class WebSocketHealthCheckEvent implements Event {
|
||||
eventName = '@extollo/lib:WebSocketHealthCheckEvent'
|
||||
|
||||
eventUuid = uuid4()
|
||||
|
||||
shouldBroadcast = false
|
||||
}
|
@ -36,6 +36,7 @@ export * from './http/kernel/HTTPCookieJar'
|
||||
export * from './http/lifecycle/Request'
|
||||
export * from './http/lifecycle/Response'
|
||||
export * from './http/lifecycle/WebSocketCloseEvent'
|
||||
export * from './http/lifecycle/WebSocketHealthCheckEvent'
|
||||
export * from './http/RequestLocalStorage'
|
||||
|
||||
export * from './make'
|
||||
|
@ -17,6 +17,8 @@ import {WebSocketCloseEvent} from '../../http/lifecycle/WebSocketCloseEvent'
|
||||
import {apiEvent, error} from '../../http/response/api'
|
||||
import {AsyncResource, executionAsyncId} from 'async_hooks'
|
||||
import {Session} from '../../http/session/Session'
|
||||
import {Config} from '../../service/Config'
|
||||
import {WebSocketHealthCheckEvent} from '../../http/lifecycle/WebSocketHealthCheckEvent'
|
||||
|
||||
@Injectable()
|
||||
export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
||||
@ -50,6 +52,9 @@ export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
||||
@Inject()
|
||||
protected readonly session!: Session
|
||||
|
||||
@Inject()
|
||||
protected readonly config!: Config
|
||||
|
||||
public readonly uuid = uuid4()
|
||||
|
||||
private connected = false
|
||||
@ -125,7 +130,7 @@ export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
up(): void {
|
||||
async up(): Promise<void> {
|
||||
const resource = new AsyncResource('WebSocketBus', {
|
||||
triggerAsyncId: executionAsyncId(),
|
||||
requireManualDestroy: false,
|
||||
@ -147,10 +152,53 @@ export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
||||
})
|
||||
})
|
||||
|
||||
this.ws.on('close', () => this.bus.push(new WebSocketCloseEvent()))
|
||||
this.ws.on('error', () => this.bus.push(new WebSocketCloseEvent()))
|
||||
|
||||
await this.registerHealthCheck()
|
||||
|
||||
this.connected = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up an interval that fires a `WebSocketHealthCheckEvent` on the request bus.
|
||||
* This can be used, e.g., to check if the user is still logged in.
|
||||
* You can control the interval by setting the `server.socket.healthCheckIntervalSeconds`
|
||||
* config option. Set the option to 0 to disable the interval.
|
||||
* @protected
|
||||
*/
|
||||
protected async registerHealthCheck(): Promise<void> {
|
||||
const interval = this.config.safe('server.socket.healthCheckIntervalSeconds')
|
||||
.or(60)
|
||||
.integer()
|
||||
|
||||
if ( interval === 0 ) {
|
||||
return
|
||||
}
|
||||
|
||||
const handle = setInterval(() => {
|
||||
if ( !this.isConnected() ) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.bus.push(new WebSocketHealthCheckEvent())
|
||||
}, interval * 1000)
|
||||
|
||||
await this.bus.subscribe(WebSocketCloseEvent, () => {
|
||||
clearInterval(handle)
|
||||
})
|
||||
}
|
||||
|
||||
down(): void {
|
||||
if ( this.connected ) {
|
||||
try {
|
||||
this.ws.close()
|
||||
} catch (e) {
|
||||
this.logging.error('Error closing socket:')
|
||||
this.logging.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
this.connected = false
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user