10
votes

I'm trying to create an authentication flow where the user's access token is kept in a server-side session along with the refresh token, and when the token expires it is renewed if the session is still valid. However, the token I get back from Azure AD after refresh has an invalid signature, when verifying it with the same method as the original token.

Here's a runnable gist that illustrates the problem: https://gist.github.com/tlycken/fdaf47dc31e03de43a1a07fbbea2ab91

What I'm doing is basically this:

  1. When the user requests a page, check for a session. If none exists, redirect to /auth which redirects to Azure AD, and when I'm returned I have a valid token which I store in the session.

  2. Verify the token from the session using jwks-rsa. (This normally works fine, so I'm purposely adding something to the token string to make the signature invalid in the test code.)

  3. If token verification failed, and there is a refresh token on the session, try to fetch a new token using that refresh token. This request normally returns with status 200 OK and a new set of access/refresh tokens.

  4. Verify the new access token using the same code as was used to verify the old one (now without garbling the token). This should work, IIUC, but it fails with the error invalid signature.

Why does my newly refreshed token not pass verification?

Update: I was able to create a simpler flow for reproducing this; the gist has been updated. It now does the following (printing these messages, along the way):

no session, redirecting to /auth
successful auth callback, redirecting to /
verifying old token
decoded user id e7f02a6e-510c-430d-905c-f8a0e63206c2
refreshing
fetching /me with renewed token
got user id e7f02a6e-510c-430d-905c-f8a0e63206c2
verifying new token
token verification failed: invalid signature

In addition to validating the token myself, I now also send a request to Azure with it, hoping that such a request would fail for an invalid token. But it passes!

1
My guess is refreshed access tokens are not signed. Can you verify this ? - Kavindu Dodanduwa
The refreshed token has three parts separated by ., and IIUC the last of them is the signature (the first being the header and payload). If I decode just the header, I get the following info, { "typ": "JWT", "nonce": "AQABAAAAAADX8GCi6Js6SK82TsD2Pb7rCCqFemr3qpUpj3EJja8SpJatcDYA51CdCJueMlRpYfOo8_wofd4aSRhZ4EX0MWALK62rwaGf8oDPsIB9rXCRyiAA", "alg": "RS256", "x5t": "TioGywwlhvdFbXZ813WpPay9AlU", "kid": "TioGywwlhvdFbXZ813WpPay9AlU" }, with info about which signing key etc is used, so I'm pretty sure my interpretation is correct. - Tomas Aschan
They have the same kid value. (Since I'm getting the keys from a JWKS endpoint, I would have expected this to work even if they did not - the verification code should be able to choose the correct signing certificate for each token anyway. But they do, so that's not the problem...) - Tomas Aschan
@KavinduDodanduwa: Even if I skip token verification for the original access token, and verify the token returned from the refresh endpoint only, it still fails with the same message. - Tomas Aschan
Just in case. I noticed that you are using the AAD v2 well-known openid-configuration (login.microsoftonline.com/${AZURE_TENANT}/v2.0/.well-known/openid-configuration). Shouldn't be using the v1? (login.microsoftonline.com/${AZURE_TENANT}/.well-known/openid-configuration) - andresm53

1 Answers

2
votes

You're code is using the v1 Endpoint to obtain the initial access token but the v2 Endpoint to exorcise the refresh token. These two endpoints operate differently. In particular, the v1 Endpoint uses "resource" while v2 uses "scopes".

The reason this is happening is your calling v1 explicitly but relying on the v2 /openid-configuration for the Refresh Token endpoint.

To correct this, change line 19 of refresh-auth-token.js to

const configResponse = 
   await fetch(`https://login.microsoftonline.com/${AZURE_TENANT}/.well-known/openid-configuration`)