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