123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142 |
- import { CrossServiceTokenData } from '@standardnotes/security'
- import { TimerInterface } from '@standardnotes/time'
- import { NextFunction, Request, Response } from 'express'
- import { BaseMiddleware } from 'inversify-express-utils'
- import { verify } from 'jsonwebtoken'
- import { AxiosError } from 'axios'
- import { Logger } from 'winston'
- import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
- import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
- export abstract class AuthMiddleware extends BaseMiddleware {
- constructor(
- private serviceProxy: ServiceProxyInterface,
- private jwtSecret: string,
- private crossServiceTokenCacheTTL: number,
- private crossServiceTokenCache: CrossServiceTokenCacheInterface,
- private timer: TimerInterface,
- protected logger: Logger,
- ) {
- super()
- }
- async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
- if (!this.handleMissingAuthHeader(request.headers.authorization, response, next)) {
- return
- }
- const authHeaderValue = request.headers.authorization as string
- const sharedVaultOwnerContextHeaderValue = request.headers['x-shared-vault-owner-context'] as string | undefined
- const cacheKey = `${authHeaderValue}${
- sharedVaultOwnerContextHeaderValue ? `:${sharedVaultOwnerContextHeaderValue}` : ''
- }`
- try {
- let crossServiceTokenFetchedFromCache = true
- let crossServiceToken = null
- if (this.crossServiceTokenCacheTTL) {
- crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
- }
- if (this.crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken)) {
- const authResponse = await this.serviceProxy.validateSession({
- authorization: authHeaderValue,
- sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
- })
- if (!this.handleSessionValidationResponse(authResponse, response, next)) {
- return
- }
- crossServiceToken = (authResponse.data as { authToken: string }).authToken
- crossServiceTokenFetchedFromCache = false
- }
- response.locals.authToken = crossServiceToken
- const decodedToken = <CrossServiceTokenData>(
- verify(response.locals.authToken, this.jwtSecret, { algorithms: ['HS256'] })
- )
- if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
- await this.crossServiceTokenCache.set({
- key: cacheKey,
- encodedCrossServiceToken: response.locals.authToken,
- expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
- userUuid: decodedToken.user.uuid,
- })
- }
- response.locals.user = decodedToken.user
- response.locals.session = decodedToken.session
- response.locals.roles = decodedToken.roles
- response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
- response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? []
- } catch (error) {
- const errorMessage = (error as AxiosError).isAxiosError
- ? JSON.stringify((error as AxiosError).response?.data)
- : (error as Error).message
- this.logger.error(`Could not pass the request to sessions/validate on underlying service: ${errorMessage}`)
- this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
- if ((error as AxiosError).response?.headers['content-type']) {
- response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
- }
- const errorCode =
- (error as AxiosError).isAxiosError && !isNaN(+((error as AxiosError).code as string))
- ? +((error as AxiosError).code as string)
- : 500
- response.status(errorCode).send(errorMessage)
- return
- }
- return next()
- }
- protected abstract handleSessionValidationResponse(
- authResponse: {
- status: number
- data: unknown
- headers: {
- contentType: string
- }
- },
- response: Response,
- next: NextFunction,
- ): boolean
- protected abstract handleMissingAuthHeader(
- authHeaderValue: string | undefined,
- response: Response,
- next: NextFunction,
- ): boolean
- private getCrossServiceTokenCacheExpireTimestamp(token: CrossServiceTokenData): number {
- const crossServiceTokenDefaultCacheExpiration = this.timer.getTimestampInSeconds() + this.crossServiceTokenCacheTTL
- if (token.session === undefined) {
- return crossServiceTokenDefaultCacheExpiration
- }
- const sessionAccessExpiration = this.timer.convertStringDateToSeconds(token.session.access_expiration)
- const sessionRefreshExpiration = this.timer.convertStringDateToSeconds(token.session.refresh_expiration)
- return Math.min(crossServiceTokenDefaultCacheExpiration, sessionAccessExpiration, sessionRefreshExpiration)
- }
- private crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken: string | null) {
- if (crossServiceToken === null) {
- return true
- }
- const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
- return decodedToken.ongoing_transition === true
- }
- }
|