AuthMiddleware.ts 4.5 KB

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