فهرست منبع

fix(auth): update user agent upon refreshing session token (#685)

Karol Sójko 1 سال پیش
والد
کامیت
bd5f492a73

+ 5 - 3
packages/auth/src/Domain/Auth/AuthenticationMethodResolver.spec.ts

@@ -40,7 +40,7 @@ describe('AuthenticationMethodResolver', () => {
     userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
 
     sessionService = {} as jest.Mocked<SessionServiceInterface>
-    sessionService.getSessionFromToken = jest.fn()
+    sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session: undefined, isEphemeral: false })
     sessionService.getRevokedSessionFromToken = jest.fn()
     sessionService.markRevokedSessionAsReceived = jest.fn().mockReturnValue(revokedSession)
 
@@ -70,7 +70,7 @@ describe('AuthenticationMethodResolver', () => {
   })
 
   it('should resolve session authentication method', async () => {
-    sessionService.getSessionFromToken = jest.fn().mockReturnValue(session)
+    sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
 
     expect(await createResolver().resolve('test')).toEqual({
       session,
@@ -80,7 +80,9 @@ describe('AuthenticationMethodResolver', () => {
   })
 
   it('should not resolve session authentication method with invalid user uuid on session', async () => {
-    sessionService.getSessionFromToken = jest.fn().mockReturnValue({ userUuid: 'invalid' })
+    sessionService.getSessionFromToken = jest
+      .fn()
+      .mockReturnValue({ session: { userUuid: 'invalid' }, isEphemeral: false })
 
     expect(await createResolver().resolve('test')).toBeUndefined
   })

+ 1 - 1
packages/auth/src/Domain/Auth/AuthenticationMethodResolver.ts

@@ -43,7 +43,7 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
       }
     }
 
-    const session = await this.sessionService.getSessionFromToken(token)
+    const { session } = await this.sessionService.getSessionFromToken(token)
     if (session) {
       this.logger.debug('Token decoded successfully. Session found.')
 

+ 0 - 7
packages/auth/src/Domain/Session/EphemeralSessionRepositoryInterface.ts

@@ -4,13 +4,6 @@ export interface EphemeralSessionRepositoryInterface {
   findOneByUuid(uuid: string): Promise<EphemeralSession | null>
   findOneByUuidAndUserUuid(uuid: string, userUuid: string): Promise<EphemeralSession | null>
   findAllByUserUuid(userUuid: string): Promise<Array<EphemeralSession>>
-  updateTokensAndExpirationDates(
-    uuid: string,
-    hashedAccessToken: string,
-    hashedRefreshToken: string,
-    accessExpiration: Date,
-    refreshExpiration: Date,
-  ): Promise<void>
   deleteOne(uuid: string, userUuid: string): Promise<void>
   save(ephemeralSession: EphemeralSession): Promise<void>
 }

+ 0 - 2
packages/auth/src/Domain/Session/SessionRepositoryInterface.ts

@@ -9,8 +9,6 @@ export interface SessionRepositoryInterface {
   findAllByUserUuid(userUuid: string): Promise<Array<Session>>
   deleteAllByUserUuidExceptOne(dto: { userUuid: Uuid; currentSessionUuid: Uuid }): Promise<void>
   deleteOneByUuid(uuid: string): Promise<void>
-  updateHashedTokens(uuid: string, hashedAccessToken: string, hashedRefreshToken: string): Promise<void>
-  updatedTokenExpirationDates(uuid: string, accessExpiration: Date, refreshExpiration: Date): Promise<void>
   save(session: Session): Promise<Session>
   remove(session: Session): Promise<Session>
   clearUserAgentByUserUuid(userUuid: string): Promise<void>

+ 88 - 59
packages/auth/src/Domain/Session/SessionService.spec.ts

@@ -24,8 +24,8 @@ describe('SessionService', () => {
   let sessionRepository: SessionRepositoryInterface
   let ephemeralSessionRepository: EphemeralSessionRepositoryInterface
   let revokedSessionRepository: RevokedSessionRepositoryInterface
-  let session: Session
-  let ephemeralSession: EphemeralSession
+  let existingSession: Session
+  let existingEphemeralSession: EphemeralSession
   let revokedSession: RevokedSession
   let settingService: SettingServiceInterface
   let deviceDetector: UAParser
@@ -54,14 +54,14 @@ describe('SessionService', () => {
     )
 
   beforeEach(() => {
-    session = {} as jest.Mocked<Session>
-    session.uuid = '2e1e43'
-    session.userUuid = '1-2-3'
-    session.userAgent = 'Chrome'
-    session.apiVersion = ApiVersion.v20200115
-    session.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
-    session.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
-    session.readonlyAccess = false
+    existingSession = {} as jest.Mocked<Session>
+    existingSession.uuid = '2e1e43'
+    existingSession.userUuid = '1-2-3'
+    existingSession.userAgent = 'Chrome'
+    existingSession.apiVersion = ApiVersion.v20200115
+    existingSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
+    existingSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
+    existingSession.readonlyAccess = false
 
     revokedSession = {} as jest.Mocked<RevokedSession>
     revokedSession.uuid = '2e1e43'
@@ -69,9 +69,7 @@ describe('SessionService', () => {
     sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
     sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
     sessionRepository.deleteOneByUuid = jest.fn()
-    sessionRepository.save = jest.fn().mockReturnValue(session)
-    sessionRepository.updateHashedTokens = jest.fn()
-    sessionRepository.updatedTokenExpirationDates = jest.fn()
+    sessionRepository.save = jest.fn().mockReturnValue(existingSession)
 
     settingService = {} as jest.Mocked<SettingServiceInterface>
     settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
@@ -79,17 +77,18 @@ describe('SessionService', () => {
     ephemeralSessionRepository = {} as jest.Mocked<EphemeralSessionRepositoryInterface>
     ephemeralSessionRepository.save = jest.fn()
     ephemeralSessionRepository.findOneByUuid = jest.fn()
-    ephemeralSessionRepository.updateTokensAndExpirationDates = jest.fn()
     ephemeralSessionRepository.deleteOne = jest.fn()
 
     revokedSessionRepository = {} as jest.Mocked<RevokedSessionRepositoryInterface>
     revokedSessionRepository.save = jest.fn()
 
-    ephemeralSession = {} as jest.Mocked<EphemeralSession>
-    ephemeralSession.uuid = '2-3-4'
-    ephemeralSession.userAgent = 'Mozilla Firefox'
-    ephemeralSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
-    ephemeralSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
+    existingEphemeralSession = {} as jest.Mocked<EphemeralSession>
+    existingEphemeralSession.uuid = '2-3-4'
+    existingEphemeralSession.userUuid = '1-2-3'
+    existingEphemeralSession.userAgent = 'Mozilla Firefox'
+    existingEphemeralSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
+    existingEphemeralSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
+    existingEphemeralSession.readonlyAccess = false
 
     timer = {} as jest.Mocked<TimerInterface>
     timer.convertStringDateToMilliseconds = jest.fn().mockReturnValue(123)
@@ -138,7 +137,7 @@ describe('SessionService', () => {
   })
 
   it('should refresh access and refresh tokens for a session', async () => {
-    expect(await createService().refreshTokens(session)).toEqual({
+    expect(await createService().refreshTokens({ session: existingSession, isEphemeral: false })).toEqual({
       access_expiration: 123,
       access_token: expect.any(String),
       refresh_token: expect.any(String),
@@ -146,8 +145,21 @@ describe('SessionService', () => {
       readonly_access: false,
     })
 
-    expect(sessionRepository.updateHashedTokens).toHaveBeenCalled()
-    expect(sessionRepository.updatedTokenExpirationDates).toHaveBeenCalled()
+    expect(sessionRepository.save).toHaveBeenCalled()
+    expect(ephemeralSessionRepository.save).not.toHaveBeenCalled()
+  })
+
+  it('should refresh access and refresh tokens for an ephemeral session', async () => {
+    expect(await createService().refreshTokens({ session: existingEphemeralSession, isEphemeral: true })).toEqual({
+      access_expiration: 123,
+      access_token: expect.any(String),
+      refresh_token: expect.any(String),
+      refresh_expiration: 123,
+      readonly_access: false,
+    })
+
+    expect(sessionRepository.save).not.toHaveBeenCalled()
+    expect(ephemeralSessionRepository.save).toHaveBeenCalled()
   })
 
   it('should create new session for a user', async () => {
@@ -420,7 +432,7 @@ describe('SessionService', () => {
   it('should delete a session by token', async () => {
     sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
       if (uuid === '2') {
-        return session
+        return existingSession
       }
 
       return null
@@ -429,13 +441,28 @@ describe('SessionService', () => {
     await createService().deleteSessionByToken('1:2:3')
 
     expect(sessionRepository.deleteOneByUuid).toHaveBeenCalledWith('2e1e43')
-    expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2e1e43', '1-2-3')
+    expect(ephemeralSessionRepository.deleteOne).not.toHaveBeenCalled()
+  })
+
+  it('should delete an ephemeral session by token', async () => {
+    ephemeralSessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
+      if (uuid === '2') {
+        return existingEphemeralSession
+      }
+
+      return null
+    })
+
+    await createService().deleteSessionByToken('1:2:3')
+
+    expect(sessionRepository.deleteOneByUuid).not.toHaveBeenCalled()
+    expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2-3-4', '1-2-3')
   })
 
   it('should not delete a session by token if session is not found', async () => {
     sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
       if (uuid === '2') {
-        return session
+        return existingSession
       }
 
       return null
@@ -448,13 +475,13 @@ describe('SessionService', () => {
   })
 
   it('should determine if a refresh token is valid', async () => {
-    expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2:3')).toBeTruthy()
-    expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2:4')).toBeFalsy()
-    expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2')).toBeFalsy()
+    expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2:3')).toBeTruthy()
+    expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2:4')).toBeFalsy()
+    expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2')).toBeFalsy()
   })
 
   it('should return device info based on user agent', () => {
-    expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0 on Mac 10.13')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on Mac 10.13')
   })
 
   it('should return device info based on undefined user agent', () => {
@@ -463,7 +490,7 @@ describe('SessionService', () => {
       browser: { name: undefined, version: undefined },
       os: { name: undefined, version: undefined },
     })
-    expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
   })
 
   it('should return a shorter info based on lack of client in user agent', () => {
@@ -473,7 +500,7 @@ describe('SessionService', () => {
       os: { name: 'iOS', version: '10.3' },
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('iOS 10.3')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('iOS 10.3')
   })
 
   it('should return a shorter info based on lack of os in user agent', () => {
@@ -483,13 +510,13 @@ describe('SessionService', () => {
       os: { name: '', version: '' },
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0')
   })
 
   it('should return unknown client and os if user agent is cleaned out', () => {
-    session.userAgent = null
+    existingSession.userAgent = null
 
-    expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
   })
 
   it('should return a shorter info based on partial os in user agent', () => {
@@ -499,7 +526,7 @@ describe('SessionService', () => {
       os: { name: 'Windows', version: '' },
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0 on Windows')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on Windows')
 
     deviceDetector.getResult = jest.fn().mockReturnValue({
       ua: 'dummy-data',
@@ -507,7 +534,7 @@ describe('SessionService', () => {
       os: { name: '', version: '7' },
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0 on 7')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on 7')
   })
 
   it('should return a shorter info based on partial client in user agent', () => {
@@ -517,7 +544,7 @@ describe('SessionService', () => {
       os: { name: 'Windows', version: '7' },
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('69.0 on Windows 7')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('69.0 on Windows 7')
 
     deviceDetector.getResult = jest.fn().mockReturnValue({
       ua: 'dummy-data',
@@ -525,7 +552,7 @@ describe('SessionService', () => {
       os: { name: 'Windows', version: '7' },
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('Chrome on Windows 7')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome on Windows 7')
   })
 
   it('should return a shorter info based on iOS agent', () => {
@@ -538,7 +565,7 @@ describe('SessionService', () => {
       cpu: { architecture: undefined },
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('iOS')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('iOS')
   })
 
   it('should return a shorter info based on partial client and partial os in user agent', () => {
@@ -548,7 +575,7 @@ describe('SessionService', () => {
       os: { name: 'Windows', version: '' },
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('69.0 on Windows')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('69.0 on Windows')
 
     deviceDetector.getResult = jest.fn().mockReturnValue({
       ua: 'dummy-data',
@@ -556,7 +583,7 @@ describe('SessionService', () => {
       os: { name: '', version: '7' },
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('Chrome on 7')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome on 7')
   })
 
   it('should return only Android os for okHttp client', () => {
@@ -569,7 +596,7 @@ describe('SessionService', () => {
       cpu: { architecture: undefined },
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('Android')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('Android')
   })
 
   it('should detect the StandardNotes app in user agent', () => {
@@ -582,7 +609,7 @@ describe('SessionService', () => {
       cpu: { architecture: undefined },
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('Standard Notes Desktop 3.5.18 on Mac OS 10.16.0')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('Standard Notes Desktop 3.5.18 on Mac OS 10.16.0')
   })
 
   it('should return unknown device info as fallback', () => {
@@ -590,70 +617,72 @@ describe('SessionService', () => {
       throw new Error('something bad happened')
     })
 
-    expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
+    expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
   })
 
   it('should retrieve a session from a session token', async () => {
     sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
       if (uuid === '2') {
-        return session
+        return existingSession
       }
 
       return null
     })
 
-    const result = await createService().getSessionFromToken('1:2:3')
+    const { session, isEphemeral } = await createService().getSessionFromToken('1:2:3')
 
-    expect(result).toEqual(session)
+    expect(session).toEqual(session)
+    expect(isEphemeral).toBeFalsy()
   })
 
   it('should retrieve an ephemeral session from a session token', async () => {
-    ephemeralSessionRepository.findOneByUuid = jest.fn().mockReturnValue(ephemeralSession)
+    ephemeralSessionRepository.findOneByUuid = jest.fn().mockReturnValue(existingEphemeralSession)
     sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
 
-    const result = await createService().getSessionFromToken('1:2:3')
+    const { session, isEphemeral } = await createService().getSessionFromToken('1:2:3')
 
-    expect(result).toEqual(ephemeralSession)
+    expect(session).toEqual(existingEphemeralSession)
+    expect(isEphemeral).toBeTruthy()
   })
 
   it('should not retrieve a session from a session token that has access token missing', async () => {
     sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
       if (uuid === '2') {
-        return session
+        return existingSession
       }
 
       return null
     })
 
-    const result = await createService().getSessionFromToken('1:2')
+    const { session } = await createService().getSessionFromToken('1:2')
 
-    expect(result).toBeUndefined()
+    expect(session).toBeUndefined()
   })
 
   it('should not retrieve a session that is missing', async () => {
     sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
 
-    const result = await createService().getSessionFromToken('1:2:3')
+    const { session } = await createService().getSessionFromToken('1:2:3')
 
-    expect(result).toBeUndefined()
+    expect(session).toBeUndefined()
   })
 
   it('should not retrieve a session from a session token that has invalid access token', async () => {
     sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
       if (uuid === '2') {
-        return session
+        return existingSession
       }
 
       return null
     })
 
-    const result = await createService().getSessionFromToken('1:2:4')
+    const { session } = await createService().getSessionFromToken('1:2:4')
 
-    expect(result).toBeUndefined()
+    expect(session).toBeUndefined()
   })
 
   it('should revoked a session', async () => {
-    await createService().createRevokedSession(session)
+    await createService().createRevokedSession(existingSession)
 
     expect(revokedSessionRepository.save).toHaveBeenCalledWith({
       uuid: '2e1e43',

+ 26 - 28
packages/auth/src/Domain/Session/SessionService.ts

@@ -100,24 +100,14 @@ export class SessionService implements SessionServiceInterface {
     }
   }
 
-  async refreshTokens(session: Session): Promise<SessionBody> {
-    const sessionPayload = await this.createTokens(session)
-
-    await this.sessionRepository.updateHashedTokens(session.uuid, session.hashedAccessToken, session.hashedRefreshToken)
-
-    await this.sessionRepository.updatedTokenExpirationDates(
-      session.uuid,
-      session.accessExpiration,
-      session.refreshExpiration,
-    )
+  async refreshTokens(dto: { session: Session; isEphemeral: boolean }): Promise<SessionBody> {
+    const sessionPayload = await this.createTokens(dto.session)
 
-    await this.ephemeralSessionRepository.updateTokensAndExpirationDates(
-      session.uuid,
-      session.hashedAccessToken,
-      session.hashedRefreshToken,
-      session.accessExpiration,
-      session.refreshExpiration,
-    )
+    if (dto.isEphemeral) {
+      await this.ephemeralSessionRepository.save(dto.session)
+    } else {
+      await this.sessionRepository.save(dto.session)
+    }
 
     return sessionPayload
   }
@@ -196,25 +186,25 @@ export class SessionService implements SessionServiceInterface {
     return `${browserInfo} on ${osInfo}`
   }
 
-  async getSessionFromToken(token: string): Promise<Session | undefined> {
+  async getSessionFromToken(token: string): Promise<{ session: Session | undefined; isEphemeral: boolean }> {
     const tokenParts = token.split(':')
     const sessionUuid = tokenParts[1]
     const accessToken = tokenParts[2]
     if (!accessToken) {
-      return undefined
+      return { session: undefined, isEphemeral: false }
     }
 
-    const session = await this.getSession(sessionUuid)
+    const { session, isEphemeral } = await this.getSession(sessionUuid)
     if (!session) {
-      return undefined
+      return { session: undefined, isEphemeral: false }
     }
 
     const hashedAccessToken = crypto.createHash('sha256').update(accessToken).digest('hex')
     if (crypto.timingSafeEqual(Buffer.from(session.hashedAccessToken), Buffer.from(hashedAccessToken))) {
-      return session
+      return { session, isEphemeral }
     }
 
-    return undefined
+    return { session: undefined, isEphemeral: false }
   }
 
   async getRevokedSessionFromToken(token: string): Promise<RevokedSession | null> {
@@ -235,11 +225,14 @@ export class SessionService implements SessionServiceInterface {
   }
 
   async deleteSessionByToken(token: string): Promise<string | null> {
-    const session = await this.getSessionFromToken(token)
+    const { session, isEphemeral } = await this.getSessionFromToken(token)
 
     if (session) {
-      await this.sessionRepository.deleteOneByUuid(session.uuid)
-      await this.ephemeralSessionRepository.deleteOne(session.uuid, session.userUuid)
+      if (isEphemeral) {
+        await this.ephemeralSessionRepository.deleteOne(session.uuid, session.userUuid)
+      } else {
+        await this.sessionRepository.deleteOneByUuid(session.uuid)
+      }
 
       return session.userUuid
     }
@@ -284,14 +277,19 @@ export class SessionService implements SessionServiceInterface {
     return session
   }
 
-  private async getSession(uuid: string): Promise<Session | null> {
+  private async getSession(uuid: string): Promise<{
+    session: Session | null
+    isEphemeral: boolean
+  }> {
     let session = await this.ephemeralSessionRepository.findOneByUuid(uuid)
+    let isEphemeral = true
 
     if (!session) {
       session = await this.sessionRepository.findOneByUuid(uuid)
+      isEphemeral = false
     }
 
-    return session
+    return { session, isEphemeral }
   }
 
   private async createTokens(session: Session): Promise<SessionBody> {

+ 2 - 2
packages/auth/src/Domain/Session/SessionServiceInterface.ts

@@ -16,8 +16,8 @@ export interface SessionServiceInterface {
     userAgent: string
     readonlyAccess: boolean
   }): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }>
-  refreshTokens(session: Session): Promise<SessionBody>
-  getSessionFromToken(token: string): Promise<Session | undefined>
+  refreshTokens(dto: { session: Session; isEphemeral: boolean }): Promise<SessionBody>
+  getSessionFromToken(token: string): Promise<{ session: Session | undefined; isEphemeral: boolean }>
   getRevokedSessionFromToken(token: string): Promise<RevokedSession | null>
   markRevokedSessionAsReceived(revokedSession: RevokedSession): Promise<RevokedSession>
   deleteSessionByToken(token: string): Promise<string | null>

+ 9 - 4
packages/auth/src/Domain/UseCase/RefreshSessionToken.spec.ts

@@ -26,7 +26,7 @@ describe('RefreshSessionToken', () => {
 
     sessionService = {} as jest.Mocked<SessionServiceInterface>
     sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(true)
-    sessionService.getSessionFromToken = jest.fn().mockReturnValue(session)
+    sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
     sessionService.refreshTokens = jest.fn().mockReturnValue({
       access_token: 'token1',
       refresh_token: 'token2',
@@ -51,9 +51,10 @@ describe('RefreshSessionToken', () => {
     const result = await createUseCase().execute({
       accessToken: '123',
       refreshToken: '234',
+      userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
     })
 
-    expect(sessionService.refreshTokens).toHaveBeenCalledWith(session)
+    expect(sessionService.refreshTokens).toHaveBeenCalledWith({ session, isEphemeral: false })
 
     expect(result).toEqual({
       success: true,
@@ -74,9 +75,10 @@ describe('RefreshSessionToken', () => {
     const result = await createUseCase().execute({
       accessToken: '123',
       refreshToken: '234',
+      userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
     })
 
-    expect(sessionService.refreshTokens).toHaveBeenCalledWith(session)
+    expect(sessionService.refreshTokens).toHaveBeenCalledWith({ session, isEphemeral: false })
 
     expect(result).toEqual({
       success: true,
@@ -90,11 +92,12 @@ describe('RefreshSessionToken', () => {
   })
 
   it('should not refresh a session token if session is not found', async () => {
-    sessionService.getSessionFromToken = jest.fn().mockReturnValue(null)
+    sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session: undefined, isEphemeral: false })
 
     const result = await createUseCase().execute({
       accessToken: '123',
       refreshToken: '234',
+      userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
     })
 
     expect(result).toEqual({
@@ -110,6 +113,7 @@ describe('RefreshSessionToken', () => {
     const result = await createUseCase().execute({
       accessToken: '123',
       refreshToken: '234',
+      userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
     })
 
     expect(result).toEqual({
@@ -125,6 +129,7 @@ describe('RefreshSessionToken', () => {
     const result = await createUseCase().execute({
       accessToken: '123',
       refreshToken: '234',
+      userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
     })
 
     expect(result).toEqual({

+ 4 - 2
packages/auth/src/Domain/UseCase/RefreshSessionToken.ts

@@ -21,7 +21,7 @@ export class RefreshSessionToken {
   ) {}
 
   async execute(dto: RefreshSessionTokenDTO): Promise<RefreshSessionTokenResponse> {
-    const session = await this.sessionService.getSessionFromToken(dto.accessToken)
+    const { session, isEphemeral } = await this.sessionService.getSessionFromToken(dto.accessToken)
     if (!session) {
       return {
         success: false,
@@ -46,7 +46,9 @@ export class RefreshSessionToken {
       }
     }
 
-    const sessionPayload = await this.sessionService.refreshTokens(session)
+    session.userAgent = dto.userAgent
+
+    const sessionPayload = await this.sessionService.refreshTokens({ session, isEphemeral })
 
     try {
       await this.domainEventPublisher.publish(

+ 1 - 0
packages/auth/src/Domain/UseCase/RefreshSessionTokenDTO.ts

@@ -1,4 +1,5 @@
 export type RefreshSessionTokenDTO = {
   accessToken: string
   refreshToken: string
+  userAgent: string
 }

+ 2 - 0
packages/auth/src/Infra/InversifyExpressUtils/AnnotatedSessionController.spec.ts

@@ -30,6 +30,7 @@ describe('AnnotatedSessionController', () => {
 
     request = {
       body: {},
+      headers: {},
     } as jest.Mocked<express.Request>
 
     response = {
@@ -70,6 +71,7 @@ describe('AnnotatedSessionController', () => {
   it('should return bad request upon failed tokens refreshing', async () => {
     request.body.access_token = '123'
     request.body.refresh_token = '234'
+    request.headers['user-agent'] = 'Google Chrome'
 
     refreshSessionToken.execute = jest.fn().mockReturnValue({
       success: false,

+ 1 - 0
packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSessionController.ts

@@ -132,6 +132,7 @@ export class BaseSessionController extends BaseHttpController {
     const result = await this.refreshSessionToken.execute({
       accessToken: request.body.access_token,
       refreshToken: request.body.refresh_token,
+      userAgent: <string>request.headers['user-agent'],
     })
 
     if (!result.success) {

+ 2 - 20
packages/auth/src/Infra/TypeORM/TypeORMEphemeralSessionRepository.ts

@@ -29,26 +29,6 @@ export class TypeORMEphemeralSessionRepository implements EphemeralSessionReposi
     }
   }
 
-  async updateTokensAndExpirationDates(
-    uuid: string,
-    hashedAccessToken: string,
-    hashedRefreshToken: string,
-    accessExpiration: Date,
-    refreshExpiration: Date,
-  ): Promise<void> {
-    const session = await this.findOneByUuid(uuid)
-    if (!session) {
-      return
-    }
-
-    session.hashedAccessToken = hashedAccessToken
-    session.hashedRefreshToken = hashedRefreshToken
-    session.accessExpiration = accessExpiration
-    session.refreshExpiration = refreshExpiration
-
-    await this.save(session)
-  }
-
   async findAllByUserUuid(userUuid: string): Promise<Array<EphemeralSession>> {
     const ephemeralSessionUuidsJSON = await this.cacheEntryRepository.findUnexpiredOneByKey(
       `${this.USER_SESSIONS_PREFIX}:${userUuid}`,
@@ -94,6 +74,8 @@ export class TypeORMEphemeralSessionRepository implements EphemeralSessionReposi
   async save(ephemeralSession: EphemeralSession): Promise<void> {
     const ttl = this.ephemeralSessionAge
 
+    ephemeralSession.updatedAt = this.timer.getUTCDate()
+
     const stringifiedSession = JSON.stringify(ephemeralSession)
 
     await this.cacheEntryRepository.save(

+ 2 - 26
packages/auth/src/Infra/TypeORM/TypeORMSessionRepository.ts

@@ -18,6 +18,8 @@ export class TypeORMSessionRepository implements SessionRepositoryInterface {
   ) {}
 
   async save(session: Session): Promise<Session> {
+    session.updatedAt = this.timer.getUTCDate()
+
     return this.ormRepository.save(session)
   }
 
@@ -41,32 +43,6 @@ export class TypeORMSessionRepository implements SessionRepositoryInterface {
       .execute()
   }
 
-  async updateHashedTokens(uuid: string, hashedAccessToken: string, hashedRefreshToken: string): Promise<void> {
-    await this.ormRepository
-      .createQueryBuilder('session')
-      .update()
-      .set({
-        hashedAccessToken,
-        hashedRefreshToken,
-        updatedAt: this.timer.getUTCDate(),
-      })
-      .where('uuid = :uuid', { uuid })
-      .execute()
-  }
-
-  async updatedTokenExpirationDates(uuid: string, accessExpiration: Date, refreshExpiration: Date): Promise<void> {
-    await this.ormRepository
-      .createQueryBuilder('session')
-      .update()
-      .set({
-        accessExpiration,
-        refreshExpiration,
-        updatedAt: this.timer.getUTCDate(),
-      })
-      .where('uuid = :uuid', { uuid })
-      .execute()
-  }
-
   async findAllByRefreshExpirationAndUserUuid(userUuid: string): Promise<Session[]> {
     return this.ormRepository
       .createQueryBuilder('session')