Skip to content
This repository has been archived by the owner on Apr 3, 2023. It is now read-only.

Set access/refresh token manually #172

Open
StevenMarrocco opened this issue Dec 22, 2021 · 5 comments
Open

Set access/refresh token manually #172

StevenMarrocco opened this issue Dec 22, 2021 · 5 comments

Comments

@StevenMarrocco
Copy link

Is your feature request related to a problem? Please describe.
Currently, we've implement a "partial" authenticated flow, where (in specific scenarios) we have our backend provide us the access/refresh token that it receives from keycloak.

Describe the solution you'd like
Setting the token and have it be used in initConfig (ideally triggering a token refresh)

Describe alternatives you've considered
After reviewing this issue would it be possible to set this.tokens after keycloak has been initialized?

@TreeOfLearning
Copy link

We are currently in the process of doing the exact same thing. We're almost there with a hacky workaround implementation that wraps the SSRCookie persistor and sets the kcToken and kcIdToken cookies manually, but we're not 100% towards a working solution as we can't get react-keycloak to trigger the refresh and do all the behind the scenes magic that entails.

So, +1

@StevenMarrocco
Copy link
Author

We are currently in the process of doing the exact same thing. We're almost there with a hacky workaround implementation that wraps the SSRCookie persistor and sets the kcToken and kcIdToken cookies manually, but we're not 100% towards a working solution as we can't get react-keycloak to trigger the refresh and do all the behind the scenes magic that entails.

So, +1

Glad we're not alone! I kind of got a working-ish solution also using this implementation (using keycloak-js without react-keycloak).

Following this implementation I was able to create a simple setter

const setTokens = (access: string, refresh: string) => {
_kc.accessToken = access;
_kc.refreshToken = refresh;
}

and then following this axios implementation to set the token refresh.

Unfortunately, only the first call to refresh/create the token worked. All others failed because for some reason, updateToken was marking the token as expired (when it wasn't) and then making a request to keycloak to refresh but for some odd reason it would provide undefined for the refreshToken.

@ashleyww93
Copy link

@StevenMarrocco I work with @Julian-Cloudshelf and this is the solution we have come up with so far, which is far from ideal.

We are using the SSR package, and we had to do quite a few hacky things to get this "working". It should be noted that we have two domains for our project. One for the frontend, and one for the keycloak instance. When we access our application in a usual way, we can use the normal keycloak authentication, however we need to support logging in via an access key provided by our backend(using token exchange).

We need to write out own persistor, and use it in the SSRKeycloakProvider like this:

         export const cloudshelfCookieProvider = new CloudshelfCookieProvider();
         
         <SSRKeycloakProvider
                persistor={cloudshelfCookieProvider}
                keycloakConfig={keycloakCfg}
                initOptions={{
                    silentCheckSsoRedirectUri: `${process.env.HOST}/silent-check-sso.html`,
                    enableLogging: true,
                }}
            >

The perisitor looks like this:

export class CloudshelfCookieProvider implements TokenPersistor {
    setTokens({ idToken, token, refreshToken }: AuthClientTokens) {
        !!token && setCookie('kcToken', btoa(token));
        !!idToken && setCookie('kcIdToken', btoa(idToken));
        !!refreshToken && setCookie('cloudshelfRefreshToken', btoa(refreshToken));
        
    }

    getTokens() {
        const tknStr = getCookie('kcToken');
        const idTknStr = getCookie('kcIdToken');
        const refreshTknStr = getCookie('cloudshelfRefreshToken');

        return {
            idToken: idTknStr ? atob(idTknStr) : undefined,
            refreshToken: refreshTknStr ? atob(refreshTknStr) : undefined,
            token: tknStr ? atob(tknStr) : undefined,
        };
    }

    resetTokens() {
        removeCookie('kcToken');
        removeCookie('kcIdToken');
        removeCookie('cloudshelfRefreshToken');
    }
}

We then need to write a wrapper around the useKeycloak hook which is provided by @react-keycloak/ssr. I called this wrapped useAuthentication, and that decides if we use the default keycloak/ssr stuff, or if we are accessing it via the method which directly provides the tokens.


export function useAuthentication(router: NextRouter) {
    const keycloakResults = useKeycloak<KeycloakInstance>();
    const { keycloak, initialized } = keycloakResults;
    const [keycloakIsReady, setKeycloakIsReady] = useState(false);
    const [authState, setAuthState] = useState<AuthenticationState | undefined>(undefined);
    const keycloakAsAuthClient = keycloak as KeycloakInstance;
    const accessingViaManager = router.query.shop === undefined;
    
    if (!accessingViaManager) {
        if (keycloakAsAuthClient?.onReady) {
            const tokens = cloudshelfCookieProvider.getTokens();
            keycloakAsAuthClient.token = tokens.token;
            keycloakAsAuthClient.idToken = tokens.idToken;
            keycloakAsAuthClient.refreshToken = tokens.refreshToken;

            Promise.allSettled([
                keycloakAsAuthClient?.updateToken(5),
                keycloakAsAuthClient?.onReady(false),
                keycloakAsAuthClient?.loadUserInfo(),
                keycloakAsAuthClient?.loadUserProfile(),
            ]).finally(() => {
                setKeycloakIsReady(true);
            });
        }
    }

    useEffect(() => {
        const tempAuthState = getAuthState(keycloakAsAuthClient, router, initialized, accessingViaManager);

        setAuthState(tempAuthState);
    }, [keycloakIsReady]);

    return !accessingViaManager
        ? authState
        : getAuthState(keycloakAsAuthClient, router, initialized, accessingViaManager);
}

Finally, we have an authstate which looks like this:

function getAuthState(
    authClient: KeycloakInstance,
    router: NextRouter,
    initialized: boolean,
    accessingViaManager: boolean,
): AuthenticationState {
    return {
        keycloak: authClient,
        initialized,
        userInfo: useUserInfoNew(router, authClient, initialized, accessingViaManager),
    };
}

useUserInfoNew just pulls some info about the user from the tokens.

While this seems like it is going to work for us (we haven't finished the impmentation, but hardcoded access tokens we provided in the CloudshelfCookieProvider worked), it would be so much better if there was an offical way to use the tokens provided by the token exchange functions.

@StevenMarrocco
Copy link
Author

StevenMarrocco commented Jan 4, 2022

@ashleyww93 thank you for the detail and happy new year!

This will come in handy. Looks like we're trying to accomplish the exact same thing. Really appreciate it

I do like the use of the Persistor and useAuthentication wrapper. I haven't done much reading into custom Persistor will the token now be stored in the cookie as implement for both keycloak authentication and back end authentication?

That said, I hope this ticket gets some eyes from some maintainers also as an official way would be awesome;

edit: sorry follow up question; const accessingViaManager = router.query.shop === undefined did you folks configure keycloak to add this query param?

@NaridaL
Copy link

NaridaL commented Feb 17, 2022

A feature like this would be useful for testing locally too. I.e. "ignore the config and just use this token".

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants