-
-
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> | |
); | |
} |
Hello, thanks for the example! I am using and it is working. This is my first contact with Expo AuthSession. But there is a problem: everytime I try to login again Keycloak remebers my e-mail, but asks for the password again. This does not happens with Postman, or other web front-ends... I think this is related to the following section in Expo AuthSession documentation:
"Note: the web browser should share cookies with your system web browser so that users do not need to sign in again if they are already authenticated on the system browser -- Expo's WebBrowser API takes care of this."
If the API takes care of this, something is wrong and I did not found a way to tune... Anyone experiencing this? Maybe this is related to a development build? Maybe Chrome in development build is restricting cookies?
Thanks!
Answering my own question : prompt: AuthSession.Prompt.Login
at 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. π π π―
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
Thank you for your example, super cool as the Expo documentation is lacking important sections of the process.