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",
|
"name": "@extollo/lib",
|
||||||
"version": "0.13.6",
|
"version": "0.13.7",
|
||||||
"description": "The framework library that lifts up your code.",
|
"description": "The framework library that lifts up your code.",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"types": "lib/index.d.ts",
|
"types": "lib/index.d.ts",
|
||||||
|
@ -80,6 +80,9 @@ export abstract class SecurityContext {
|
|||||||
/**
|
/**
|
||||||
* Assuming a user is still authenticated in the context,
|
* Assuming a user is still authenticated in the context,
|
||||||
* try to look up and fill in the user.
|
* 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>
|
abstract resume(): Awaitable<void>
|
||||||
|
|
||||||
|
@ -33,7 +33,10 @@ export class SessionSecurityContext extends SecurityContext {
|
|||||||
if ( user ) {
|
if ( user ) {
|
||||||
this.authenticatedUser = user
|
this.authenticatedUser = user
|
||||||
await this.bus.push(new UserAuthenticationResumedEvent(user, this))
|
await this.bus.push(new UserAuthenticationResumedEvent(user, this))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.authenticatedUser = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,9 @@ export class TokenSecurityContext extends SecurityContext {
|
|||||||
if ( user ) {
|
if ( user ) {
|
||||||
this.authenticatedUser = user
|
this.authenticatedUser = user
|
||||||
await this.bus.push(new UserAuthenticationResumedEvent(user, this))
|
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/UserAuthenticatedEvent'
|
||||||
export * from './event/UserAuthenticationResumedEvent'
|
export * from './event/UserAuthenticationResumedEvent'
|
||||||
export * from './event/UserFlushedEvent'
|
export * from './event/UserFlushedEvent'
|
||||||
|
export * from './event/AuthCheckFailed'
|
||||||
|
|
||||||
export * from './middleware/AuthRequiredMiddleware'
|
export * from './middleware/AuthRequiredMiddleware'
|
||||||
export * from './middleware/GuestRequiredMiddleware'
|
export * from './middleware/GuestRequiredMiddleware'
|
||||||
@ -33,6 +34,8 @@ export * from './repository/orm/ORMUserRepository'
|
|||||||
|
|
||||||
export * from './config'
|
export * from './config'
|
||||||
|
|
||||||
|
export * from './webSocketAuthCheck'
|
||||||
|
|
||||||
export * from './server/types'
|
export * from './server/types'
|
||||||
export * from './server/models/OAuth2TokenModel'
|
export * from './server/models/OAuth2TokenModel'
|
||||||
export * from './server/repositories/ConfigClientRepository'
|
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/Request'
|
||||||
export * from './http/lifecycle/Response'
|
export * from './http/lifecycle/Response'
|
||||||
export * from './http/lifecycle/WebSocketCloseEvent'
|
export * from './http/lifecycle/WebSocketCloseEvent'
|
||||||
|
export * from './http/lifecycle/WebSocketHealthCheckEvent'
|
||||||
export * from './http/RequestLocalStorage'
|
export * from './http/RequestLocalStorage'
|
||||||
|
|
||||||
export * from './make'
|
export * from './make'
|
||||||
|
@ -17,6 +17,8 @@ import {WebSocketCloseEvent} from '../../http/lifecycle/WebSocketCloseEvent'
|
|||||||
import {apiEvent, error} from '../../http/response/api'
|
import {apiEvent, error} from '../../http/response/api'
|
||||||
import {AsyncResource, executionAsyncId} from 'async_hooks'
|
import {AsyncResource, executionAsyncId} from 'async_hooks'
|
||||||
import {Session} from '../../http/session/Session'
|
import {Session} from '../../http/session/Session'
|
||||||
|
import {Config} from '../../service/Config'
|
||||||
|
import {WebSocketHealthCheckEvent} from '../../http/lifecycle/WebSocketHealthCheckEvent'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
||||||
@ -50,6 +52,9 @@ export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly session!: Session
|
protected readonly session!: Session
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly config!: Config
|
||||||
|
|
||||||
public readonly uuid = uuid4()
|
public readonly uuid = uuid4()
|
||||||
|
|
||||||
private connected = false
|
private connected = false
|
||||||
@ -125,7 +130,7 @@ export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
up(): void {
|
async up(): Promise<void> {
|
||||||
const resource = new AsyncResource('WebSocketBus', {
|
const resource = new AsyncResource('WebSocketBus', {
|
||||||
triggerAsyncId: executionAsyncId(),
|
triggerAsyncId: executionAsyncId(),
|
||||||
requireManualDestroy: false,
|
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
|
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 {
|
down(): void {
|
||||||
|
if ( this.connected ) {
|
||||||
|
try {
|
||||||
|
this.ws.close()
|
||||||
|
} catch (e) {
|
||||||
|
this.logging.error('Error closing socket:')
|
||||||
|
this.logging.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.connected = false
|
this.connected = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user