Expo Authentication with Amazon Cognito not working on web

108 views Asked by At

I've setup a basic login page following a guide from Expo that logs in a user from an AWS Cognito user pool that I've already setup. This basic login page appears to be working for both login and logout functions on Expo Go with my iPhone, but isn't working correctly on the browser on my desktop. I've pinpointed the problem down to an issue where the 'useAuthRequest' function returns a response of type 'dismiss' immediately upon opening up the hosted login page from AWS Cognito. Even if a user enters correct credentials and logs in, they simply get a new popup that opens up the expo app's home page.

This is the (misbehaving) login flow on web:

Phase 1: First, the user is shown the AWS Cognito hosted UI after clicking the blue login button.

Phase 2: After entering correct credentials, the popup page persists showing us a duplicate of the home page. This isn't supposed happen. Normally, once the user enters their credentials the hosted UI popup should get closed and we should have a 'successful' AuthSession response where we can exchange an authorization code from AWS for authTokens, all of which is triggered in a useEffect. As the image from Phase 2 shows, we do get back a redirect from AWS Cognito with 'code' and 'state' params, but the exchange doesn't happen. This is because the 'response' output in the 'useAuthRequest' gets set to response.type = 'dismiss' from the very moment the user opens the AWS Hosted UI. Normally, the response type should be 'successful' and we can grab the code from said response and do our exchange for the tokens (which works fine on Expo Go, but not on web).

I'm pretty confident this isn't an issue that needs fixing on the AWS Cognito side because we get proper authorization code from Cognito - we just don't trigger the token exchange because the useAuthRequest response gets set to 'dismiss'.

I want to understand why login with 'useAuthRequest' is failing on web, but still working on Expo Go. Also, this project doesn't use AWS Amplify whatsoever. Furthermore, while I'm not sure what impact this has on auth behavior, the project uses Expo Router.

I'll provide my code below: it's effectively a one-to-one replica of the Expo guide minus the logout function and written in Typescript instead:

import {
  AccessTokenRequestConfig,
  ResponseType,
  TokenResponse,
  exchangeCodeAsync,
  makeRedirectUri,
  revokeAsync,
  useAuthRequest,
} from "expo-auth-session";
import React from "react";
import { StyleSheet, Button, Alert } from "react-native";

const clientId = process.env.EXPO_PUBLIC_COGNITO_CLIENT_ID!;
const userPoolUrl = process.env.EXPO_PUBLIC_COGNITO_USER_POOL!;
const redirectUri = makeRedirectUri();

export default function App() {
  const [authTokens, setAuthTokens] = React.useState<TokenResponse | null>(
    null
  );
  const discoveryDocument = React.useMemo(
    () => ({
      authorizationEndpoint: userPoolUrl + "/oauth2/authorize",
      tokenEndpoint: userPoolUrl + "/oauth2/token",
      revocationEndpoint: userPoolUrl + "/oauth2/revoke",
    }),
    []
  );

  const [request, response, promptAsync] = useAuthRequest(
    {
      clientId,
      responseType: ResponseType.Code,
      redirectUri,
      usePKCE: true,
    },
    discoveryDocument
  );

  React.useEffect(() => {
    const exchangeFn = async (exchangeTokenReq: AccessTokenRequestConfig) => {
      try {
        const exchangeTokenResponse = await exchangeCodeAsync(
          exchangeTokenReq,
          discoveryDocument
        );
        setAuthTokens(exchangeTokenResponse);
      } catch (error) {
        console.error(error);
      }
    };
    if (response) {
      console.log("response type: " + response.type); // Web: response.type is always 'dismiss'
      if (response.type === "error") {
        Alert.alert(
          "Authentication error",
          response.params.error_description || "something went wrong"
        );
        return;
      }
      if (response.type === "success" && request?.codeVerifier) {
        exchangeFn({
          clientId,
          code: response.params.code,
          redirectUri,
          extraParams: {
            code_verifier: request.codeVerifier,
          },
        });
      }
    }
  }, [discoveryDocument, request, response]);

  console.log("authTokens: " + JSON.stringify(authTokens));

  return authTokens ? (
    <Button title="Logout" />
  ) : (
    <Button disabled={!request} title="Login" onPress={() => promptAsync()} />
  );
}
0

There are 0 answers