AxaFrance / oidc-client

Light, Secure, Pure Javascript OIDC (Open ID Connect) Client. We provide also a REACT wrapper (compatible NextJS, etc.).
MIT License
596 stars 160 forks source link

Refresh token is not properly set when using service worker #826

Closed Thodor12 closed 2 years ago

Thodor12 commented 2 years ago

Issue and Steps to Reproduce

When using the service worker, the access token is translated to a protected value for security reasons, and then converted back into the actual token when sending them to the backend. However this doesn't seem to be happening with the refresh token, this one is sent as the protected value itself and not reset by the service worker.

I have the follow oidc configuration:

import { Environment } from "./env";
import { OidcConfiguration } from "@axa-fr/react-oidc/dist/vanilla/oidc";

function getAuthority() {
    if (Environment.OIDC_AUTHORITY !== undefined) {
        return Environment.OIDC_AUTHORITY;
    } else {
        return `${window.location.protocol}//auth.${window.location.host}`;
    }
}

export function GetOIDCConfiguration(): OidcConfiguration {
    const data: OidcConfiguration = {
        client_id: Environment.OIDC_CLIENT_ID,
        authority: getAuthority(),
        redirect_uri: window.location.origin + "/authentication/callback",
        scope: "openid profile email roles offline_access full.access",
        service_worker_relative_url: "/OidcServiceWorker.js",
        service_worker_only: false,
    };
    return data;
}

On the backend I've set my access token to expire within 90 seconds to trigger refreshing very fast for testing purposes.

Versions

react-oidc: 5.13.5

Screenshots

2022-07-22 09_51_01-IPMWebsite (Debugging) - Microsoft Visual Studio

Expected

The refresh token should be received in the backend as the actual refresh token value, not the service worker protected value.

Actual

The refresh token comes in as the service worker protected value.

Additional Details

Dependencies:

    "@axa-fr/react-oidc": "^5.7.6-alpha1",
    "@emotion/react": "^11.9.0",
    "@emotion/styled": "^11.8.1",
    "@mui/icons-material": "^5.6.2",
    "@mui/lab": "^5.0.0-alpha.87",
    "@mui/material": "^5.6.4",
    "@mui/x-data-grid": "^5.12.3",
    "@mui/x-date-pickers": "^5.0.0-alpha.6",
    "@openapi-contrib/openapi-schema-to-json-schema": "^3.1.1",
    "axios": "^0.27.2",
    "date-fns": "^2.28.0",
    "formik": "^2.2.9",
    "i18next": "^21.8.9",
    "js-file-download": "^0.4.12",
    "jwt-decode": "^3.1.2",
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "react-helmet-async": "^1.3.0",
    "react-i18next": "^11.17.1",
    "react-router-dom": "^6.3.0",
    "react-scripts": "5.0.1",
    "sass": "^1.50.1",
    "swr": "^2.0.0-beta.6",
    "typescript": "^4.6.3",
    "web-vitals": "^2.1.4",
    "yup": "^0.32.11"
guillaume-chervet commented 2 years ago

Hi @Thodor12 , thank you for the issue.

Have you configured TrustesDomains.js? For security access token are only send to domains declared in that file.

// OidcTrustedDomains.js // Add here trusted domains, access tokens will be send const trustedDomains = { default:["http://localhost:4200"], auth0:[] };

Thodor12 commented 2 years ago

Yes, I have set that one, I have separated the backend by authorization server and resource server, in development they run on port 7068 and 7200 respectively, both are added to the trusted domains.

The screenshot I provided is the error I get from the authorization server (port 7068)

Thodor12 commented 2 years ago

Btw, just in case the doubt maybe with the backend, I am using OpenIddict (for ASP.NET) and the response does correctly return a refresh token.

guillaume-chervet commented 2 years ago

@Thodor12 if you update to v6.0.0beta3 whithout any code chang,e it should fix your problem. Before v6 oidc server domain should not be declared in trusteddomain.js.

Thodor12 commented 2 years ago

I updated to 6.0.0-beta3 but the service worker in that version seems not to function. It crashes with the error:

OidcServiceWorker.js:238 Uncaught TypeError: Cannot read properties of undefined (reading 'startsWith')
    at OidcServiceWorker.js:238:57
    at Array.find (<anonymous>)
    at checkDomain (OidcServiceWorker.js:238:28)
    at OidcServiceWorker.js:279:13

At the function where it wants to validate the userInfoEndpoint, however my system does not expose a userInfoEndpoint because I rely on JWT tokens that already include all the user information.

As per the OpenId discovery document specification the userinfo_endpoint is RECOMMENDED, not REQUIRED. (https://openid.net/specs/openid-connect-discovery-1_0.html, section 3)

Could this be changed so that the userinfo_endpoint is only checked when it's actually provided?

Should I make a different issue to fix this or can you pick this up?

guillaume-chervet commented 2 years ago

Sorry again @Thodor12 for that bug. No need another issue i will fix this for beta4. Thank you for your feedback!

Thodor12 commented 2 years ago

I commented the offending line to be able to test it, but right now the library doesn't even attempt to refresh the token, previously it at least attempted to make a refresh token request, now it does nothing?

After the token expiry time rolls over all of the requests just follow up with a 401 error and nothing happens.

Also I don't even get the "session lost" component shown

guillaume-chervet commented 2 years ago

Hi @Thodor12 , have you tried the beta4?

guillaume-chervet commented 2 years ago

This is strange if the timer do not start :/

Thodor12 commented 2 years ago

The service worker is functional now, however the refreshing still doesn't happen

guillaume-chervet commented 2 years ago

If you run the demo with your configuration, what does look like your logs ?

Thodor12 commented 2 years ago

On the demo, by default it works and it continuously rolls my token over and over (because it's expiration is currently set to 60 seconds), so that works as expected.

I then updated the demo to use 6.0.0-beta4 and everythings stopped working

guillaume-chervet commented 2 years ago

Hum, i have an idea where it can come from.

Do you have a sample an idtoken?

Thodor12 commented 2 years ago
CfDJ8OpP9CaZdrhJkH68OTXlDjyZTaYry8DbdHQHLNqeH3yDg27CfsqvM28tzIkGlQM_7Vn0aLwoX7brP3USzJQaNBFoy4BwfBYyGy4LIColEg4JqF3jQ5jo4dKxmooCXJfKU87TYAaTEYIDmVROdtBONvkLrYDAJYFdSGQPvviblYxuOslq9qegIx_5SN7inBMjOIcqavZ2t-_ONX2yOVD-I0dsr5L1zwl6mtyY9Xbx03K0tGcKyhmpIdGDAVH89PElo6xOWbS7Yh75qO4rPMzi7xLBqD_lVOOHYWpKUPD4mLDv9T-gIb37N_toZ9sHrz3qscFtoVngmNBna-XAcLQzmrcDgVl9qYvKvE3-nzMHgQ6Tw8c8jUo-6C0EAf9ws6y1BcHevjSFDFqla4QaZoLCJchx2ES5uO3ALzQslGbXBINCZYiBBu6-o3FgxSm7UUKSrfUT0OSDG_i3xcutKmBDoQIxgU130LKtaJIioxdxWTjD8h5yDoLp3954tYLemBwJwLUm0UVw916JS0cXaARsNERQzaHoMZSebovsGYPaXas2ImKsQkUDrQqwy1SqAlPF8S-DKHWw4cQBs7xLMhlm_1AyAh4QS8MrBe6hIDggHaAgF8ETuM4T3ClDIdnNeXllZ64VMM-BUhMS7oZSf2vRfhZwlSY3B11XxI6S-Y32xMQF6GDTQXAJbWeJSKy9OROzqep8SpWD5rb9B9TEbCYovnNF9FAQ9I8TUjo7AE6gpt-KiWaO0wGur-25quc1tkPiJXufPA4TOSx2nncFaUcMK42ixhUp94A2MxoWzjNUlZV7v17OeNX_1Sa1vcu_4_trslHY2nHTll3TD_bwcxVUqhJMtHLis0WmarTidfz0X1nRXCZ3cTRKhl6oed5JIvYLNdZqX3iIMfeMWCpqxppr3MLL_fXD_S6e0FlRTgnVr_posg48wMquakCZbixwoeuVVZxA_wfevTbeVurSrpSvccT9WeI5851hX8jsimJQBWeIQQPe1hTYP57d3jbcgYb_juppkVh7whoYLTLIQjf5TuIRacROOyxYMsXMNi5QCN0sPMibna_7-VLfW9DH_pOKYMvmR10hrq-GYS2tM63Wy8ILM9XksmIXjnmTi-_aMq4dTqHVS6lv2RXcBLAiP__nI62_KfMfenDzAy7c-O5Y2qQ

This token format is identical to my access token. They're encrypted using ASP.NET Core Data Protection stack. You can only unprotect these through the data protection system, provided you also have the correct certificate, is that usable for you?

(This was the refresh_token btw)

Thodor12 commented 2 years ago

Oh sorry, you said idToken:

eyJhbGciOiJSUzI1NiIsImtpZCI6IkNFNjQ1QUMzNkZCRDUxQzkzNzI1Rjk2MjIwN0ZDNTUxRjlCRTM3N0EiLCJ4NXQiOiJ6bVJhdzItOVVjazNKZmxpSUhfRlVmbS1OM28iLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiI1IiwiZW1haWwiOiJ0aG9tLnZhbmRlbmFra2VyQG91dGxvb2suY29tIiwibmFtZSI6IkFkbWluIiwicm9sZSI6IkFkbWluIiwib2lfYXVfaWQiOiI4NzMiLCJhenAiOiJyZWFjdC1zcGEiLCJhdF9oYXNoIjoiSzhDM0lGTnVXZE5lWERRQUVjcWdrUSIsIm9pX3Rrbl9pZCI6IjU1NiIsImF1ZCI6InJlYWN0LXNwYSIsImV4cCI6MTY1ODU4OTU5NiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NzA2OC8iLCJpYXQiOjE2NTg1ODgzOTZ9.h-VaxYT1MPeYaQsXkhn8uyKEuIAajWkTRyaXbIUREmy6tRGJFCFpJaGIkPTjAjZ5FGUFYVCHKnGyu4QN76BckZb2vQqRh_Ff74froKMdfedQs2yLFuJqfGj-6dWKY4FmwSnMk4Ibz58v0Mhm9ONe_UejatVOIoSi942UNQMAL0n8MDP2Vn5q8STWwnF8BAuIQ3nrfrOxOJYmvOjvBjMWe_5L7lPabxTPPCgxgQAHIrMM7Bdi-hOLU8Obbcd7b10liR59g2lh-PYpw-4yItOMn_bX5OOYhSVzM-mTyyB-C-W_Vdy7e0x7KmrhXl3SjWBbvpN7pHaoKT-Xbu90_r2WNQ
guillaume-chervet commented 2 years ago

Thank you very much @Thodor12 . I understand what append. Do you have a sample of the first token call after login.

You may remove refresh token value and set an old expired access token. I need to find information to know when access token expire.

Thodor12 commented 2 years ago

This is the initial token call:

{
  "access_token": "CfDJ8OpP9CaZdrhJkH68OTXlDjw10gomah53FkxI3XnCIO7QHl-gUCxDvQgfAX9paDUEaS2ENTtALZlymYD2yNXB-LW_Fc_4mP5Tv9MO0nz5q7er2l-nwWA0JAU_sd1w_-v0w_-nfOhvw2NC_l5l8JvD0wjmXIygrBxEKT9SsMTcTcjI-_eX1dT12Lt-nI1t5CaPDn32YYpbNdpp0N7BXYY0lxpHhVvVJU0EYlt2Rl3uBbzD8YS3J7zcrPLJ_jNdEMYEyL1ziRvdXlDdgz4PNVZ4QIpPPKyISKcEeyOIjnSkoJSMC_9de2ddvo6b0f06iCahLJAQz6WXS4RcIy2Z5O9ykVik8w0h4bH5CKBC8HzYSAFrc9_Zq2c4ISbpLRplZRBp0yQEqC6mJ1nRdEtLr1iDitAUBAD6fZRQvwwiCsC3WK8hCPdPLkBPWn2RYojDLED_7Jxtz-AB3UhTOC_qVPbRWxS2SqSUq_Guqf-S4FwNtMjzo39CeF9J0WJtsd3AMBI73986iqJ-4j0idgjEnMWD4QiY_h7GosegPrYb8Iia9HJK8YMpq-PE9QFEyrgbQQKqs_0cGMRwUYF7sdO3dbhBTZ_p_eMKF4vxbhUHKLzEmcbmjZdVdojt2CCS8BjmtmSSCScaQU4CnDSfFOalpsIevHhY1NqQ7QUPCdPJTLZXsuQ4nC2wDxI2FSKFkmwk0ilFlJUDmQi5QLhbu1fuDkNWb5E",
  "token_type": "Bearer",
  "expires_in": 60,
  "scope": "openid profile email roles offline_access full.access",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkNFNjQ1QUMzNkZCRDUxQzkzNzI1Rjk2MjIwN0ZDNTUxRjlCRTM3N0EiLCJ4NXQiOiJ6bVJhdzItOVVjazNKZmxpSUhfRlVmbS1OM28iLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiI1IiwiZW1haWwiOiJ0aG9tLnZhbmRlbmFra2VyQG91dGxvb2suY29tIiwibmFtZSI6IkFkbWluIiwicm9sZSI6IkFkbWluIiwib2lfYXVfaWQiOiI4NzMiLCJhenAiOiJyZWFjdC1zcGEiLCJhdF9oYXNoIjoidTFkZVpGZmxsbUVQLU8wZ1FXU2RsUSIsIm9pX3Rrbl9pZCI6IjU2MCIsImF1ZCI6InJlYWN0LXNwYSIsImV4cCI6MTY1ODU5MDIzNiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NzA2OC8iLCJpYXQiOjE2NTg1ODkwMzZ9.XVrj0t7m4Xpf4xfQ6z-ZUD4PXes63sQD_Yz4tDtqxiTgb_MdNNJgByT2gJlWoBgo1m_OphR8q4DGVvjc8r3e4yzuCf3mjpiMD4-K9g3Og5AmP_C-cknMUe0zAOo5To1tn8FghEd0mY-gzJo70x5KXKsL5wl45dqRvXfVjlmAR8u-BqMhGmKLOO7BZCETftyUNAICqfJPe-eEkkTo5RzCYuEo6N4trdRmpbap0W3PK3GOLyLEzYGmkxwG4LqfEs09hzSCrpk_Y4GCbQH0X-NNXC4xLev7rXw58R5vv7K1RRFRRN_19PlooDVTjNwOBrSfHop3mr8KeLwcEbPPH8h5_A",
  "refresh_token": "CfDJ8OpP9CaZdrhJkH68OTXlDjz5wKJ8uZfoRgBiUfHIh9KcYOIyiClfOJzFW2hpDh5PjggQU6FmkOjuau5xhMohp3XqnuucAjkfCFcV-nbIUxYh0mSl_IyEkA0XkLL55qkm-sLkM56dbd0wIp2lrb2iv79Lo7UK90ceI-5IdlTuuW1sX2hQzZkLUd6yjIKCxXF2l90UxJ0geQ8TTKM1iusavoOvVxdnYVeIy5ZqBs7iyjQlqWAW5IMHzbsH4tGUe5Uzpud2aNNIRxyfDrYfVQvfhTzCeCl1p0XJr6zqoq6vFkLzbei6K7IoVwl9v8s-oiwJQeEtO3JnKe-eUYpESJfYocJ2TODom13IReT37-VKMbuy82xVuABv5JIesster8AbJrq2hFfe35qaqIjL7-syuRCphy4Uo3v9Kb0WqXIwZqHrfwR6ZC0UXoe1BqjDLTQFoQzRCYR1ldquA5IW0HdaHDe1j-_kA7KTNDPiGC5IzIWKd_p_Q6Lx_7J1O44GqmELT1V2o16Zz9D4BHl4LjpN0xgrBM9t2_J7kBLEiO1hoddJpWgQh12BDGdj80VzHxUMveK3HcVlKdpPQsN7CuvnsEUuZ2zcGZLh9LWSFW8l1LoIqxl_mGt1h5izqdkVMAdEaTKLx_kajy9eRer0tx66x13xCggJ59zb3dJoZhq0v6oZX9ulvTg8K8FdVjTNyJ6NYS-JqqT6enL3Tc8AHXTQGEbc6Ic70KmZLMnyFPLskoieBiGdllC4df7ZXXTONlbFfCLepZ7OPPsp5VL3BN9aarB0Wfh699xYt_YbgP6fHFGQO1wGYSVRMzk_KThJK5C6SAqRI_L_4xDfooBLysx34wzWORgkroK6MZGxwKHOMLY8KvKdmnJbLOybYze3UT5LR-d3JcesjvkfvoeLOJGwsKVtamgwoV_C3eRSbhksh3hLGgnZmsWHNS8kFhcOo9db3BV7FD06BEcn5TpHNg2vm3miR2-wfQobUGXFc0caCrLld6DUJ9C1po3J1qpTQnSxMkAnfstmpMcWNfD0HPlsRQ3E4e1yNU_Y-d4Bf4ahUnEJkzOf_-DbwZGykt_ISgv4ZiQJbcls632Om__HgVVNEKESJmIdfnEmHfB23JuEdphVWGqLWf0FVDyQYbfoviqdljWMu2CbW2bemW-sZJCPxPU"
}

I don't care about any of the tokens, this is just a testing environment

Thodor12 commented 2 years ago

Sorry, that was the response, this is the form data input:

grant_type: authorization_code
client_id: react-spa
redirect_uri: http://localhost:3000/authentication/callback
code: mCbfs6Zmtb4Au-IRja9vKrD-Ov0ZQQjZv8HQiONt2Yw
code_verifier: 30Aux9eTElGqOOp901gt9KRA5YpaE2lRXn7veEaHw6QF5HflN5OUqCa67GQBXxCtCOrBiHCBG6cyQrAPQNlwTdlaozYmMovzDHE8QUf1NFkVTGxD3nd37OTLuu60XC9O
Thodor12 commented 2 years ago

I just noticed that requests that are being made to the OidcKeepAliveServiceWorker.json are resulting in errors (infinitely pending, then failing). Could that have something to do with it?

This only happens seemingly right after the initial token connection, if I hard reload after that it's resulting in a 304

guillaume-chervet commented 2 years ago

Hi @Thodor12 , thank you very much for all your information and your help. OidcKeepAliveServiceWorker.json is a fake request in order to keepalive the service worker. It fail when window is reloading, but it does not matter.

I just publshed version 6.0.0-beta7 which should fix your issue.

guillaume-chervet commented 2 years ago

with that commit https://github.com/AxaGuilDEv/react-oidc/pull/817/commits/6ef4da5d12442f9709a700c38185da4c4cce74ce

Thodor12 commented 2 years ago

Checked the commit out, looks good, however for people that do use JWT tokens, and not encrypted tokens you probably want to make use of the iat claim: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6 This won't be for me since I have that encrypted token but could be useful for others.

Edit: Actually you can get the iat claim from the id_token since that one, per the specification, is mandatory to be a JWT token and thus always can have an iat claim, if it doesn't have one you could still fall back on the current datetime (because the iat claim is considered optional)

Edit 2: Actually, as per the OpenID Connect specification, the iat claim is mandatory: https://openid.net/specs/openid-connect-core-1_0.html (section 2)

Thodor12 commented 2 years ago

@guillaume-chervet Unfortunately beta7 still had no effect, the tokens still won't roll over. I have attached the onEvent method like the demo application does too, I can in fact see the token_timer, however it's timeLeft is really high despite only having a 60 second expiry time on my token, I don't think the expiresAt calculation is going completely correctly if I'm being honest.

Thodor12 commented 2 years ago

@guillaume-chervet I found the issue (finally), turns out the backend decouples the lifetime of the access_token and id_token into 2 separate things, useful but not well documented. The default lifetime for the id_token turned out to be 20 minutes, and I set the access_token only 1 minute. So this isn't really a problem with react-oidc anymore, I have managed to solve it now.

Howver the following question, we can still see if a request ends in a 401 through our own code, is there a way we can manually trigger a refresh using the refresh_token and retry?

guillaume-chervet commented 2 years ago

Thank you again @Thodor12 for your information. Beta 6 does not use idtoken information. It may work for your use case. You may set     refresh_time_before_tokens_expiration_in_second: 20 In your configuration.

I will study how to retrieve well all token expiration date.

Thodor12 commented 2 years ago

On a different note, in v5 I could use Axios using:

    const { accessToken } = useOidcAccessToken();

    const axiosInst = useMemo(() => {
        const config: AxiosRequestConfig = {
            baseURL: Environment.API_HOST + "/api",
        };

        if (accessToken) {
            config.headers = {
                Authorization: `Bearer ${accessToken}`,
            };
        }

        const client = axios.create(config);

        client.interceptors.response.use(originalResponse => {
            handleDates(originalResponse.data);
            return originalResponse;
        });

        return client;
    }, [accessToken]);

    return axiosInst;

The accessToken here would be the service worker protected value but the service worker would replace it with the correct token. However this functionality seems to be broken in v6, the service worker no longer intercepts the requests? Resulting in all requests ending in a 401, how do you properly use Axios?

I dislike fetch and don't want to use it because the API is quite bad.

guillaume-chervet commented 2 years ago

Hi @Thodor12 , the same behavior should happen in v6.

Do you have somewhere on github the code of your oidc dev server?

It may be quicker to reproduce all the problems.

guillaume-chervet commented 2 years ago

I set up a more robust algorithm in beta 8 : https://github.com/AxaGuilDEv/react-oidc/pull/817/commits/5e76b6dbf25f1cce6dd6ff9f65643cf04b217033

Thodor12 commented 2 years ago

I will grant you access to the 2 repositores, backend and frontend so you can take a look. Can I contact you elsewhere which is a bit faster in terms of communication?

Slack, Teams, Discord?

guillaume-chervet commented 2 years ago

Hi @Thodor12 , thank you very much. I will try to create a slack and look a your code.

Thodor12 commented 2 years ago

Just write down your email and I can invite you into a slack group, then we can continue discussion in there, alright?

I'll need to give you some information on how to set up the project since I work on it alone and thus provided no README etc

guillaume-chervet commented 2 years ago

My mail is guillaume.chervet at gmail.com. I am in holiday, i won't be a lot in front my computer before next week.

Thodor12 commented 2 years ago

@guillaume-chervet Hey, you managed to take another look at Slack to check if you could manage to get my application running? If you're busy that's alright, still attempting to see if I can get it fixed myself but no progress yet.

guillaume-chervet commented 2 years ago

Hi @Thodor12 , I'am sorry i will be 2 weeks more in holiday (I have worked that previous week) on the beach :)

Did you tried last version 6 ? I think it should fixes many things.

guillaume-chervet commented 2 years ago

Your code you give me access at front side looks good.

Thodor12 commented 2 years ago

Hey, I updated to 6.0.12 and it seems to be properly working now. The token now automatically rolls if it expires, so that's good.

However 1 odd thing that I noticed, if I refresh the page normally, the page reloads properly and the backend serves the content directly. But if I hard reload (CTRL + Shift + R) the page that it loads results in a 401, because the token is not loaded. But when I then reload the page normally again, it works again??

It looks like on a hard reload the service worker does not get initialized on time because I do not see the service worker modifying any requests in the network tab, but on a soft reload it does modify them properly

Thodor12 commented 2 years ago

Oh, nevermind, turns out this is intended behaviour: https://web.dev/service-worker-lifecycle/#shift-reload 👀

Thodor12 commented 2 years ago

In that case everything works like a charm now, thank you for all your support and effort and enjoy the rest of your holiday 😄