AuthMiddleware.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import { CrossServiceTokenData } from '@standardnotes/security'
  2. import { TimerInterface } from '@standardnotes/time'
  3. import { NextFunction, Request, Response } from 'express'
  4. import { BaseMiddleware } from 'inversify-express-utils'
  5. import { verify } from 'jsonwebtoken'
  6. import { AxiosError } from 'axios'
  7. import { Logger } from 'winston'
  8. import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
  9. import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
  10. export abstract class AuthMiddleware extends BaseMiddleware {
  11. constructor(
  12. private serviceProxy: ServiceProxyInterface,
  13. private jwtSecret: string,
  14. private crossServiceTokenCacheTTL: number,
  15. private crossServiceTokenCache: CrossServiceTokenCacheInterface,
  16. private timer: TimerInterface,
  17. protected logger: Logger,
  18. ) {
  19. super()
  20. }
  21. async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
  22. if (!this.handleMissingAuthHeader(request.headers.authorization, response, next)) {
  23. return
  24. }
  25. const authHeaderValue = request.headers.authorization as string
  26. const sharedVaultOwnerContextHeaderValue = request.headers['x-shared-vault-owner-context'] as string | undefined
  27. const cacheKey = `${authHeaderValue}${
  28. sharedVaultOwnerContextHeaderValue ? `:${sharedVaultOwnerContextHeaderValue}` : ''
  29. }`
  30. try {
  31. let crossServiceTokenFetchedFromCache = true
  32. let crossServiceToken = null
  33. if (this.crossServiceTokenCacheTTL) {
  34. crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
  35. }
  36. if (this.crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken)) {
  37. const authResponse = await this.serviceProxy.validateSession({
  38. authorization: authHeaderValue,
  39. sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
  40. })
  41. if (!this.handleSessionValidationResponse(authResponse, response, next)) {
  42. return
  43. }
  44. crossServiceToken = (authResponse.data as { authToken: string }).authToken
  45. crossServiceTokenFetchedFromCache = false
  46. }
  47. response.locals.authToken = crossServiceToken
  48. const decodedToken = <CrossServiceTokenData>(
  49. verify(response.locals.authToken, this.jwtSecret, { algorithms: ['HS256'] })
  50. )
  51. if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
  52. await this.crossServiceTokenCache.set({
  53. key: cacheKey,
  54. encodedCrossServiceToken: response.locals.authToken,
  55. expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
  56. userUuid: decodedToken.user.uuid,
  57. })
  58. }
  59. response.locals.user = decodedToken.user
  60. response.locals.session = decodedToken.session
  61. response.locals.roles = decodedToken.roles
  62. response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
  63. response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? []
  64. } catch (error) {
  65. const errorMessage = (error as AxiosError).isAxiosError
  66. ? JSON.stringify((error as AxiosError).response?.data)
  67. : (error as Error).message
  68. this.logger.error(`Could not pass the request to sessions/validate on underlying service: ${errorMessage}`)
  69. this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
  70. if ((error as AxiosError).response?.headers['content-type']) {
  71. response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
  72. }
  73. const errorCode =
  74. (error as AxiosError).isAxiosError && !isNaN(+((error as AxiosError).code as string))
  75. ? +((error as AxiosError).code as string)
  76. : 500
  77. response.status(errorCode).send(errorMessage)
  78. return
  79. }
  80. return next()
  81. }
  82. protected abstract handleSessionValidationResponse(
  83. authResponse: {
  84. status: number
  85. data: unknown
  86. headers: {
  87. contentType: string
  88. }
  89. },
  90. response: Response,
  91. next: NextFunction,
  92. ): boolean
  93. protected abstract handleMissingAuthHeader(
  94. authHeaderValue: string | undefined,
  95. response: Response,
  96. next: NextFunction,
  97. ): boolean
  98. private getCrossServiceTokenCacheExpireTimestamp(token: CrossServiceTokenData): number {
  99. const crossServiceTokenDefaultCacheExpiration = this.timer.getTimestampInSeconds() + this.crossServiceTokenCacheTTL
  100. if (token.session === undefined) {
  101. return crossServiceTokenDefaultCacheExpiration
  102. }
  103. const sessionAccessExpiration = this.timer.convertStringDateToSeconds(token.session.access_expiration)
  104. const sessionRefreshExpiration = this.timer.convertStringDateToSeconds(token.session.refresh_expiration)
  105. return Math.min(crossServiceTokenDefaultCacheExpiration, sessionAccessExpiration, sessionRefreshExpiration)
  106. }
  107. private crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken: string | null) {
  108. if (crossServiceToken === null) {
  109. return true
  110. }
  111. const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
  112. return decodedToken.ongoing_transition === true
  113. }
  114. }