-
-
Save lemmensaxel/72ece5cd00026cc05888701d7d65fbe0 to your computer and use it in GitHub Desktop.
| 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> | |
| ); | |
| } |
Hi @SHUHAIB-T, a screenshots of my client configuration:

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
}
}
thank you @lemmensaxel
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.
@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
Answering my own question :
prompt: AuthSession.Prompt.Loginat line 51, according to OpenId documentation here it will make the server prompt for reauthentication. It says: "The Authorization Server SHOULD prompt the End-User for reauthentication". Keycloak is doing exactly that. To solve my case, I just had to not send the prompt parameter. Now it works as I expect: it will only promt for login again in case of complete timeout without refresh. π π π―