Skip to content

Instantly share code, notes, and snippets.

@lemmensaxel
Last active November 7, 2025 15:43
Show Gist options
  • Select an option

  • Save lemmensaxel/72ece5cd00026cc05888701d7d65fbe0 to your computer and use it in GitHub Desktop.

Select an option

Save lemmensaxel/72ece5cd00026cc05888701d7d65fbe0 to your computer and use it in GitHub Desktop.
React-native expo + keycloak PKCE flow implemented using expo AuthSession
import {
ActivityIndicator,
Button,
ScrollView,
Text,
View,
} from "react-native";
import * as AuthSession from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
import { useEffect, useState } from "react";
WebBrowser.maybeCompleteAuthSession();
const redirectUri = AuthSession.makeRedirectUri({
useProxy: true,
});
// Keycloak details
const keycloakUri = "";
const keycloakRealm = "";
const clientId = "";
export function generateShortUUID() {
return Math.random().toString(36).substring(2, 15);
}
export default function App() {
const [accessToken, setAccessToken] = useState<string>();
const [idToken, setIdToken] = useState<string>();
const [refreshToken, setRefreshToken] = useState<string>();
const [discoveryResult, setDiscoveryResult] =
useState<AuthSession.DiscoveryDocument>();
// Fetch OIDC discovery document once
useEffect(() => {
const getDiscoveryDocument = async () => {
const discoveryDocument = await AuthSession.fetchDiscoveryAsync(
`${keycloakUri}/realms/${keycloakRealm}`
);
setDiscoveryResult(discoveryDocument);
};
getDiscoveryDocument();
}, []);
const login = async () => {
const state = generateShortUUID();
// Get Authorization code
const authRequestOptions: AuthSession.AuthRequestConfig = {
responseType: AuthSession.ResponseType.Code,
clientId,
redirectUri: redirectUri,
prompt: AuthSession.Prompt.Login,
scopes: ["openid", "profile", "email", "offline_access"],
state: state,
usePKCE: true,
};
const authRequest = new AuthSession.AuthRequest(authRequestOptions);
const authorizeResult = await authRequest.promptAsync(discoveryResult!, {
useProxy: true,
});
if (authorizeResult.type === "success") {
// If successful, get tokens
const tokenResult = await AuthSession.exchangeCodeAsync(
{
code: authorizeResult.params.code,
clientId: clientId,
redirectUri: redirectUri,
extraParams: {
code_verifier: authRequest.codeVerifier || "",
},
},
discoveryResult!
);
setAccessToken(tokenResult.accessToken);
setIdToken(tokenResult.idToken);
setRefreshToken(tokenResult.refreshToken);
}
};
const refresh = async () => {
const refreshTokenObject: AuthSession.RefreshTokenRequestConfig = {
clientId: clientId,
refreshToken: refreshToken,
};
const tokenResult = await AuthSession.refreshAsync(
refreshTokenObject,
discoveryResult!
);
setAccessToken(tokenResult.accessToken);
setIdToken(tokenResult.idToken);
setRefreshToken(tokenResult.refreshToken);
};
const logout = async () => {
if (!accessToken) return;
const redirectUrl = AuthSession.makeRedirectUri({ useProxy: false });
const revoked = await AuthSession.revokeAsync(
{ token: accessToken },
discoveryResult!
);
if (!revoked) return;
// The default revokeAsync method doesn't work for Keycloak, we need to explicitely invoke the OIDC endSessionEndpoint with the correct parameters
const logoutUrl = `${discoveryResult!
.endSessionEndpoint!}?client_id=${clientId}&post_logout_redirect_uri=${redirectUrl}&id_token_hint=${idToken}`;
const res = await WebBrowser.openAuthSessionAsync(logoutUrl, redirectUrl);
if (res.type === "success") {
setAccessToken(undefined);
setIdToken(undefined);
setRefreshToken(undefined);
}
};
if (!discoveryResult) return <ActivityIndicator />;
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
{refreshToken ? (
<View
style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
>
<View>
<ScrollView style={{ flex: 1 }}>
<Text>AccessToken: {accessToken}</Text>
<Text>idToken: {idToken}</Text>
<Text>refreshToken: {refreshToken}</Text>
</ScrollView>
</View>
<View>
<Button title="Refresh" onPress={refresh} />
<Button title="Logout" onPress={logout} />
</View>
</View>
) : (
<Button title="Login" onPress={login} />
)}
</View>
);
}
@lemmensaxel
Copy link
Author

Hi @SHUHAIB-T, a screenshots of my client configuration:
image

This is the JSON export:

{
    "clientId": "app",
    "name": "AlertCore Mobiele Applicatie",
    "surrogateAuthRequired": false,
    "enabled": true,
    "alwaysDisplayInConsole": false,
    "clientAuthenticatorType": "client-secret",
    "redirectUris": [
        "alert-core://*",
        "https://auth.expo.io/@lemmensaxel/*",
        "exp://*"
    ],
    "webOrigins": [],
    "notBefore": 0,
    "bearerOnly": false,
    "consentRequired": false,
    "standardFlowEnabled": true,
    "implicitFlowEnabled": false,
    "directAccessGrantsEnabled": true,
    "serviceAccountsEnabled": false,
    "publicClient": true,
    "frontchannelLogout": false,
    "protocol": "openid-connect",
    "attributes": {
        "saml.multivalued.roles": "false",
        "saml.force.post.binding": "false",
        "frontchannel.logout.session.required": "false",
        "oauth2.device.authorization.grant.enabled": "true",
        "backchannel.logout.revoke.offline.tokens": "false",
        "saml.server.signature.keyinfo.ext": "false",
        "use.refresh.tokens": "true",
        "oidc.ciba.grant.enabled": "false",
        "backchannel.logout.session.required": "true",
        "client_credentials.use_refresh_token": "false",
        "saml.client.signature": "false",
        "require.pushed.authorization.requests": "false",
        "saml.allow.ecp.flow": "false",
        "saml.assertion.signature": "false",
        "id.token.as.detached.signature": "false",
        "saml.encrypt": "false",
        "saml.server.signature": "false",
        "exclude.session.state.from.auth.response": "false",
        "saml.artifact.binding": "false",
        "saml_force_name_id_format": "false",
        "tls.client.certificate.bound.access.tokens": "false",
        "acr.loa.map": "{}",
        "saml.authnstatement": "false",
        "display.on.consent.screen": "false",
        "token.response.type.bearer.lower-case": "false",
        "saml.onetimeuse.condition": "false"
    },
    "authenticationFlowBindingOverrides": {},
    "fullScopeAllowed": true,
    "nodeReRegistrationTimeout": -1,
    "protocolMappers": [
        {
            "name": "Realm mapper",
            "protocol": "openid-connect",
            "protocolMapper": "oidc-hardcoded-claim-mapper",
            "consentRequired": false,
            "config": {
                "claim.value": "alert-core-tenant1",
                "userinfo.token.claim": "true",
                "id.token.claim": "true",
                "access.token.claim": "true",
                "claim.name": "realm",
                "jsonType.label": "String",
                "access.tokenResponse.claim": "false"
            }
        },
        {
            "name": "Phone mapper",
            "protocol": "openid-connect",
            "protocolMapper": "oidc-usermodel-attribute-mapper",
            "consentRequired": false,
            "config": {
                "userinfo.token.claim": "true",
                "user.attribute": "phone",
                "id.token.claim": "true",
                "access.token.claim": "true",
                "claim.name": "phone",
                "jsonType.label": "String"
            }
        },
        {
            "name": "Competences mapper",
            "protocol": "openid-connect",
            "protocolMapper": "oidc-usermodel-attribute-mapper",
            "consentRequired": false,
            "config": {
                "userinfo.token.claim": "true",
                "multivalued": "true",
                "user.attribute": "competences",
                "id.token.claim": "true",
                "access.token.claim": "true",
                "claim.name": "competences",
                "jsonType.label": "String"
            }
        }
    ],
    "defaultClientScopes": [
        "web-origins",
        "acr",
        "roles",
        "profile",
        "email"
    ],
    "optionalClientScopes": [
        "address",
        "phone",
        "offline_access",
        "microprofile-jwt"
    ],
    "access": {
        "view": true,
        "configure": true,
        "manage": true
    }
}

@SHUHAIB-T
Copy link

thank you @lemmensaxel

@AntoninBarbier
Copy link

AntoninBarbier commented Sep 3, 2025

Thanks for the example, it has been very useful, but i'm still scratching my head on redirect URIs, trying to make my authentication worflow work on Android and IOS simulators and also Web, using expo managed workflow and development builds :
First, it seems using { useProxy : true } like you do in your code is deprecated now, as stated here : https://auth.expo.io/

Then, when building my redirectUri like that :
const redirectUri = AuthSession.makeRedirectUri();
When doing the promptAsync(), I'm getting the keycloak error : Invalid parameter : redirect_uri error

I found out that I don't get any error redirect_uri error when specify a path, like :
const redirectUri = AuthSession.makeRedirectUri({ path: "redirect", });

And then everything works fine on IOS and Web (login, logout...).

But on Android, because of what I think is called Deep Linking, this path: 'redirect' makes the app to be redirected to the /redirect screen which does not exist on my app.
This solves it for IOS and web, and everything is working fine, but it crashes on Android because of the missing screen.

So i had this idea :
const redirectUri = AuthSession.makeRedirectUri({ path: "/", });

So that i'm redirected to the home screen after keycloak login is complete. And it works ! But when trying to logout, i get an error in the keycloak popup : We are sorry... An internal server error has occured. The same error occurs on IOS.

So I'm still stuck and any help would be very useful.

@datmt
Copy link

datmt commented Sep 26, 2025

@AntoninBarbier I've configured successfully and made the post here: https://datmt.com/mobile-development/configure-expo-login-with-keycloak/
Also, my demo repo is here so you can take a look https://github.com/datmt/expo-keycloak
Hope that helps

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment