Skip to content

Instantly share code, notes, and snippets.

@lemmensaxel
Last active June 19, 2024 05:41
Show Gist options
  • Save lemmensaxel/72ece5cd00026cc05888701d7d65fbe0 to your computer and use it in GitHub Desktop.
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>
);
}
@gabgagnon
Copy link

Thank you for your example, super cool as the Expo documentation is lacking important sections of the process.

@luizeduardogiampaoli
Copy link

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!

@luizeduardogiampaoli
Copy link

luizeduardogiampaoli commented Apr 17, 2024

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. 😄 😃 💯

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