SubscriptionTokenAuthMiddleware.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import { OfflineUserTokenData, CrossServiceTokenData } from '@standardnotes/security'
  2. import { NextFunction, Request, Response } from 'express'
  3. import { inject, injectable } from 'inversify'
  4. import { BaseMiddleware } from 'inversify-express-utils'
  5. import { verify } from 'jsonwebtoken'
  6. import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
  7. import { Logger } from 'winston'
  8. import { TYPES } from '../Bootstrap/Types'
  9. import { TokenAuthenticationMethod } from './TokenAuthenticationMethod'
  10. @injectable()
  11. export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
  12. constructor(
  13. @inject(TYPES.ApiGateway_HTTPClient) private httpClient: AxiosInstance,
  14. @inject(TYPES.ApiGateway_AUTH_SERVER_URL) private authServerUrl: string,
  15. @inject(TYPES.ApiGateway_AUTH_JWT_SECRET) private jwtSecret: string,
  16. @inject(TYPES.ApiGateway_Logger) private logger: Logger,
  17. ) {
  18. super()
  19. }
  20. async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
  21. const subscriptionToken = request.query.subscription_token || request.body.subscription_token
  22. const email = request.headers['x-offline-email']
  23. if (!subscriptionToken) {
  24. response.status(401).send({
  25. error: {
  26. tag: 'invalid-auth',
  27. message: 'Invalid login credentials.',
  28. },
  29. })
  30. return
  31. }
  32. response.locals.tokenAuthenticationMethod = email
  33. ? TokenAuthenticationMethod.OfflineSubscriptionToken
  34. : TokenAuthenticationMethod.SubscriptionToken
  35. try {
  36. const url =
  37. response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken
  38. ? `${this.authServerUrl}/offline/subscription-tokens/${subscriptionToken}/validate`
  39. : `${this.authServerUrl}/subscription-tokens/${subscriptionToken}/validate`
  40. const authResponse = await this.httpClient.request({
  41. method: 'POST',
  42. headers: {
  43. Accept: 'application/json',
  44. },
  45. data: {
  46. email,
  47. },
  48. validateStatus: (status: number) => {
  49. return status >= 200 && status < 500
  50. },
  51. url,
  52. })
  53. if (authResponse.status > 200) {
  54. response.setHeader('content-type', authResponse.headers['content-type'] as string)
  55. response.status(authResponse.status).send(authResponse.data)
  56. return
  57. }
  58. if (response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken) {
  59. this.handleOfflineAuthTokenValidationResponse(response, authResponse)
  60. return next()
  61. }
  62. this.handleAuthTokenValidationResponse(response, authResponse)
  63. return next()
  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(
  69. `Could not pass the request to ${this.authServerUrl}/subscription-tokens/${subscriptionToken}/validate on underlying service: ${errorMessage}`,
  70. )
  71. this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
  72. if ((error as AxiosError).response?.headers['content-type']) {
  73. response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
  74. }
  75. const errorCode =
  76. (error as AxiosError).isAxiosError && !isNaN(+((error as AxiosError).code as string))
  77. ? +((error as AxiosError).code as string)
  78. : 500
  79. response.status(errorCode).send(errorMessage)
  80. return
  81. }
  82. }
  83. private handleOfflineAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
  84. response.locals.offlineAuthToken = authResponse.data.authToken
  85. const decodedToken = <OfflineUserTokenData>(
  86. verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
  87. )
  88. response.locals.offlineUserEmail = decodedToken.userEmail
  89. response.locals.offlineFeaturesToken = decodedToken.featuresToken
  90. }
  91. private handleAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
  92. response.locals.authToken = authResponse.data.authToken
  93. const decodedToken = <CrossServiceTokenData>(
  94. verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
  95. )
  96. response.locals.user = decodedToken.user
  97. response.locals.roles = decodedToken.roles
  98. }
  99. }