Last active
April 27, 2025 05:26
-
-
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> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
thank you @lemmensaxel