Last active
November 7, 2025 15:43
-
-
Save lemmensaxel/72ece5cd00026cc05888701d7d65fbe0 to your computer and use it in GitHub Desktop.
React-native expo + keycloak PKCE flow implemented using expo AuthSession
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | |
| ); | |
| } |
@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
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 errorI 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.