AxaFrance / oidc-client

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

Restore connection between client and ServiceWorker after failures #1397

Open wermanoid opened 5 days ago

wermanoid commented 5 days ago

Issue and Steps to Reproduce

You need to run some auth mock server to control responses.

Versions

"@axa-fr/oidc-client": "7.22.8",

Expected

If I open one more tab and login correctly, old tabs should update with correct tokens

Actual

2 tabs are broken, 1 is sometimes functional

Additional Details

I got a workaround, like next:

Tab that hangs with outdate tokens:

I have no ideas why there is such difference in renewTokensAsync and tryKeepExistingSessionAsync. Just found manually, that these functions helps to recover client in apps, when auth was renewed in some new tab.

Probably it would be a good feature to have smth like that in OIDC client by default? So OIDC client in apps could recover automatically, in case if SW was recreated/updated by working or new tab.

guillaume-chervet commented 3 days ago

hi @wermanoid thank you for your issue.

I'am not sure to understand what you did but I'am interested to understand well. Do you have a full exemple of code or something like that ?

wermanoid commented 2 days ago

Hi @guillaume-chervet,

There is no special app code example here, because in code it looks like regular oidc client usage. Whole magic happened when auth server fails (like session ended/lost or server errors)

I was using oauth-mock-server to simulate some errors

here is implementation:

// index.ts
import { OAuth2Server } from 'oauth2-mock-server';

import { createStateServer } from './state-manager';

// docs https://github.com/axa-group/oauth2-mock-server/blob/master/README.md

const run = async () => {
    const { state, responseByResultHandlers } = createStateServer(); // http://localhost:8081
    // unfortunately OAuth2Server does not give access to express app instance, so I had to find a way, that allow to modify
    // auth responses in runtime, without instant server restarts. This can be achieved by simple get requests from browser
    // based on defined query params

    const server = new OAuth2Server();

    // Generate a new RSA key and add it to the keystore
    await server.issuer.keys.generate('RS256');

    // Start the server
    await server.start(8082, 'localhost');

    console.log('Issuer URL:', server.issuer.url); // -> http://localhost:8082

    server.service.on('beforeUserinfo', userInfoResponse => {
        userInfoResponse.body = {
            ...userInfoResponse.body,
            email: 'who.cares@email.net',
            name: 'JD',
        };
    });

    server.service.on('beforeTokenSigning', (token, req) => {
        const timestamp = Math.floor(Date.now() / 1000);

        token.payload.exp = timestamp + state.expireTimeSec;
        token.payload.scope =
            'offline_access groups tenant_id openid profile email';
        token.payload.scp = token.payload.scope.split(' ');

        req.body.scope = 'offline_access groups tenant_id openid profile email';
    });

    server.service.on('beforeResponse', response => {
        console.log('Response of type:', state.currentResponse);
        // modify token response based on predefined state, so we could modify desired responses in runtime, when needed
        responseByResultHandlers[state.currentResponse](response);
    });
};

run();

and code to manage responses in real-time:

import express from 'express';

let requestCounter = 0;

const state = {
    expireTimeSec: 180,
    currentResponse: 'e3', // every third token request will fail with random 400 or 401 error
};

const responseByResultHandlers = {
    e3: (response: any) => {
        if (requestCounter < 2) {
            responseByResultHandlers[200](response);
            requestCounter += 1;
        } else {
            responseByResultHandlers[Math.random() > 0.5 ? 401 : 400](response);
            requestCounter = 0;
        }
    },
    // always success
    200: (response: any) => {
        response.body.expires_in = state.expireTimeSec;
        response.body.scope =
            'offline_access groups tenant_id openid profile email';
        response.body.issued_at = response.body.issuedAt;
    },
    // always invalid_grant
    400: (response: any) => {
        response.body = {
            error: 'invalid_grant',
        };
        response.statusCode = 400;
    },
    // always unauthorized
    401: (response: any) => {
        response.body.error = 'Unauthorized';
        response.body.error_message = 'User is unauthorized';
        response.statusCode = 401;
    },
} as const;

export const createStateServer = (
    port = 8081
): {
    state: typeof state;
    responseByResultHandlers: typeof responseByResultHandlers;
} => {
    const app = express();

    app.get('/config', (req, res) => {
        state.expireTimeSec = Number(req.query?.expireTime) || 180;
        state.currentResponse = String(req.query?.responseType) || 'e3';

        console.log('New state:', state);

        return res.json({ ok: true });
    });

    app.listen(port, () => {
        console.log(
            `Auth mock configuration api runnin at http://localhost:${port}`
        );
    });

    return { state, responseByResultHandlers };
};

App to test - anything with oidc client that expects to update tokens on regular basis via SW. I've used offline_access in scope for client initialization in app. And tokens expiry is reduced to 180 sec oob, so there is no need to wait hours until oidc decided to refresh tokens.

So when you have few tabs (at least 3) opened and they all are connected to same SW everything is ok, until auth server returns first failed request.

Hope that helps.

wermanoid commented 1 day ago

Interesting extension related to error handling in general, a bit off-topic, but: In case if /.well-known/openid-configuration happens to fail any time, client crashes so hard, that it fails into infinite loop and does not recover even if auth server is back to live. This can even consume whole PC RAM and require system restart 🤣 (which I had once, actually).

image

guillaume-chervet commented 1 day ago

Thank you @wermanoid it is a very nice feedback. I will look at what i can do about it !