Skip to content

Commit

Permalink
Merge pull request #1 from mvanroon/feature/support-rotating-refresh-…
Browse files Browse the repository at this point in the history
…tokens

feat: support rotating refresh tokens
  • Loading branch information
mvanroon authored Apr 10, 2021
2 parents c257f92 + 72d6449 commit a671e52
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 6 deletions.
14 changes: 10 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,16 @@ const refreshToken = async (requestRefresh: TokenRefreshRequest): Promise<Token>

try {
// Refresh and store access token using the supplied refresh function
const newToken = await requestRefresh(refreshToken)
await setAccessToken(newToken)
const newTokens = await requestRefresh(refreshToken)
if (typeof newTokens === 'object' && newTokens?.accessToken) {
await setAuthTokens(newTokens)
return newTokens.accessToken
} else if (typeof newTokens === 'string') {
await setAccessToken(newTokens)
return newTokens
}

return newToken
throw new Error('requestRefresh must either return a string or an object with an accessToken')
} catch (error) {
// Failed to refresh token
const status = error?.response?.status
Expand All @@ -200,7 +206,7 @@ const refreshToken = async (requestRefresh: TokenRefreshRequest): Promise<Token>
}
}

export type TokenRefreshRequest = (refreshToken: string) => Promise<Token>
export type TokenRefreshRequest = (refreshToken: string) => Promise<Token | AuthTokens>

export interface AuthTokenInterceptorConfig {
header?: string
Expand Down
4 changes: 2 additions & 2 deletions tests/authTokenInterceptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,10 @@ describe('authTokenInterceptor', () => {

it('puts requests in the queue while tokens are being refreshed', async () => {
// GIVEN
// We are count the number of times a token is being refreshed
// We are counting the number of times a token is being refreshed
let refreshes = 0

// I have an access token that expired an hour ago
// and I have an access token that expired an hour ago
const expiredToken = jwt.sign(
{
exp: Math.floor(Date.now() / 1000) - 60 * 60,
Expand Down
65 changes: 65 additions & 0 deletions tests/refreshTokenIfNeeded.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,69 @@ describe('refreshTokenIfNeeded', () => {
// and the result to be the new access token
expect(result).toEqual('newaccesstoken')
})

it('updates both tokens if they are provided', async () => {
// GIVEN
// I have an access token that expired an hour ago
const expiredToken = jwt.sign(
{
exp: Math.floor(Date.now() / 1000) - 60 * 60,
data: 'foobar',
},
'secret'
)

// and this token is stored in local storage
const tokens = { accessToken: expiredToken, refreshToken: 'refreshtoken' }
localStorage.setItem(STORAGE_KEY, JSON.stringify(tokens))

// and I have a requestRefresh function that returns both tokens
const requestRefresh = async () => ({ accessToken: 'newaccesstoken', refreshToken: 'newrefreshtoken' })

// WHEN
// I call refreshTokenIfNeeded
const result = await refreshTokenIfNeeded(requestRefresh)

// THEN
// I expect both the stord tokens to have been updated
const storedTokens = localStorage.getItem(STORAGE_KEY) as string
expect(JSON.parse(storedTokens)).toEqual({ accessToken: 'newaccesstoken', refreshToken: 'newrefreshtoken' })

// and the result to be the new access token
expect(result).toEqual('newaccesstoken')
})

it('throws an error if requestRefresh returns an invalid response', async () => {
// GIVEN
// I have an access token that expired an hour ago
const expiredToken = jwt.sign(
{
exp: Math.floor(Date.now() / 1000) - 60 * 60,
data: 'foobar',
},
'secret'
)

// and this token is stored in local storage
const tokens = { accessToken: expiredToken, refreshToken: 'refreshtoken' }
localStorage.setItem(STORAGE_KEY, JSON.stringify(tokens))

// and I have a requestRefresh function that returns an access token
const requestRefresh = async () => ({ access_token: 'wrongkey!', refresh_token: 'anotherwrongkey!' })

// and I have an error handler
const errorHandler = jest.fn()

// WHEN
// I call refreshTokenIfNeeded
await refreshTokenIfNeeded(requestRefresh as any).catch(errorHandler)

// THEN
// I expect the error handler to have been called with the right error
expect(errorHandler).toHaveBeenLastCalledWith(
new Error(
'Failed to refresh auth token: requestRefresh must either return a string or an object with an accessToken'
)
)
})
})

0 comments on commit a671e52

Please sign in to comment.