1

We are trying to use Expo authentication with Okta as stated here:

https://docs.expo.dev/guides/authentication/#okta

Expo has very good documentation for lot's of stuff, but for the Okta authentication unfortunately we could not sort out how to use the library in a correct way.

Currently, with lot's of suffering (mostly because of the ambiguity in Okta's configuration pages), we came to a certain point where the following code correctly responds the code parameter. This is the exact same part from Expo documentation:

React.useEffect(() => {
    if (response?.type === 'success') {
      const { code } = response.params;
    }
  }, [response]);

But unfortunately we could not find any method how we can use the parameter code to get the scope information, email, name, etc...

Can anybody guide us how we can use the object code to retrieve these data? (The Okta documentation is not clear for this either, so we are stuck.)

Edit 1:

The response has the following structure:

response: {
    "type": "success",
    "error": null,
    "url": "http://localhost:19006/?code=fUMjE4kBX2QZXXXXXX_XXXXXXXMQ084kEPrTqDa9FTs&state=3XXXXXXXXz",
    "params": {
        "code": "fUMjE4kBX2QZXXXXXX_XXXXXXXMQ084kEPrTqDa9FTs",
        "state": "3XXXXXXXXz"
    },
    "authentication": null,
    "errorCode": null
}

Edit 2:

Calling exchangeCodeAsync also yields errors.

Code:

    const tokenRequestParams = {
        code: code,
        clientId: config.okta.clientId,
        redirectUri: oktaRedirectUri,
        extraParams: {
            code_verifier: authRequest.codeVerifier
        },
    }

    const tokenResult = await exchangeCodeAsync(tokenRequestParams, discovery);

Error:

TokenRequest.ts:205 Uncaught (in promise) Error: Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method).  The authorization server MAY return an HTTP 401 (Unauthorized) status code to indicate which HTTP authentication schemes are supported.  If the client attempted to authenticate via the "Authorization" request header field, the authorization server MUST respond with an HTTP 401 (Unauthorized) status code and include the "WWW-Authenticate" response header field matching the authentication scheme used by the client.
More info: Client authentication failed. Either the client or the client credentials are invalid.
    at AccessTokenRequest.<anonymous> (TokenRequest.ts:205:1)
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (asyncToGenerator.js:3:1)
    at _next (asyncToGenerator.js:22:1)

PS: I asked the same question to the Expo forums also here. If we can solve there, I plan to reflect to here also for wider audience. (The method can be related with Okta rather than Expo itself.)

Mehmet Kaplan
  • 1,723
  • 2
  • 20
  • 43
  • I've never worked with Okta or Expo, but with the brief look I had at the Okta website I assume it's some kind of OAuth / OpenID auth? If this is the case the "code" you receive from the authentication would be part of an Authorization Code Flow. The flow should roughly be Login -> Receive Code -> Use Code to hit Token Endpoint -> Receive ID/Refresh/Access Token(s) -> Use Access Token to hit User Info Endpoint (if claims on tokens not enough) -> Receive User Info. This is a simplified high level of the flow, but it should give you a rough idea. – Jacob Smit Dec 08 '22 at 22:55
  • are you using bare workflow or managed? – Muhammad Numan Dec 09 '22 at 06:40
  • You definitely want to award the bounty now before it expires. – Robert Bradley Dec 15 '22 at 16:41

1 Answers1

1

there are two ways to use Okta in React Native

1. By Restful APIs, using fetch/Axios

2. By Using Native SDK

By Restful APIs, using fetch/Axios

here is the full code of okta using restful

import React, { useState } from "react";

import {
  ScrollView,
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  Platform,
} from "react-native";

import {
  useAutoDiscovery,
  useAuthRequest,
  makeRedirectUri,
  exchangeCodeAsync,
} from "expo-auth-session";
import { maybeCompleteAuthSession } from "expo-web-browser";
import axios from "axios";

const oktaConfig = {
  okta_issuer_url: "",
  okta_client_id: "",
  okta_callback_url: "com.okta.<OKTA_DOMAIN>:/callback",
};
export default App = (props) => {
  const useProxy = true;

  if (Platform.OS === "web") {
    maybeCompleteAuthSession();
  }

  const discovery = useAutoDiscovery(oktaConfig.okta_issuer_url);

  // When promptAsync is invoked we will get back an Auth Code
  // This code can be exchanged for an Access/ID token as well as
  // User Info by making calls to the respective endpoints

  const [authRequest, response, promptAsync] = useAuthRequest(
    {
      clientId: oktaConfig.okta_client_id,
      scopes: ["openid", "profile"],
      redirectUri: makeRedirectUri({
        native: oktaConfig.okta_callback_url,
        useProxy,
      }),
    },
    discovery
  );

  async function oktaCognitoLogin() {
    const loginResult = await promptAsync({ useProxy });
    ExchangeForToken(loginResult, authRequest, discovery);
  }

  return (
    <View style={styles.container}>
      <View style={styles.buttonContainer}>
        <TouchableOpacity
          style={styles.equalSizeButtons}
          onPress={() => oktaCognitoLogin()}
        >
          <Text style={styles.buttonText}>Okta Login</Text>
        </TouchableOpacity>
      </View>
      <ScrollView>
        {response && <Text>{JSON.stringify(response, null, 2)}</Text>}
      </ScrollView>
    </View>
  );
};

this is how, we can get exchange token and then get user info by using restful api


//After getting the Auth Code we need to exchange it for credentials
async function ExchangeForToken(response, authRequest, discovery) {
  // React hooks must be used within functions
  const useProxy = true;
  const expoRedirectURI = makeRedirectUri({
    native: oktaConfig.okta_callback_url,
    useProxy,
  })

  const tokenRequestParams = {
    code: response.params.code,
    clientId: oktaConfig.okta_client_id,
    redirectUri: expoRedirectURI,
    extraParams: {
      code_verifier: authRequest.codeVerifier
    },
  }
  
  const tokenResult = await exchangeCodeAsync(
      tokenRequestParams,
      discovery
  )

  const creds = ExchangeForUser(tokenResult)

  const finalAuthResult = {
    token_res : tokenResult,
    user_creds : creds
  }
  console.log("Final Result: ", finalAuthResult)
}

this is how we can get user info by using restful api

async function ExchangeForUser(tokenResult) {
  const accessToken = tokenResult.accessToken;
  const idToken = tokenResult.idToken;

  //make an HTTP direct call to the Okta User Info endpoint of our domain
  const usersRequest = `${oktaConfig.okta_issuer_url}/v1/userinfo`
  const userPromise = await axios.get(usersRequest, {
    headers: {
      'Authorization': `Bearer ${accessToken}`
    }
  });
    
  console.log(userPromise, "user Info");
}



const styles = StyleSheet.create({
  container: {
    margin: 10,
    marginTop: 20,
  },
  buttonContainer: {
    flexDirection: "row",
    alignItems: "center",
    margin: 5,
  },
  equalSizeButtons: {
    width: "50%",
    backgroundColor: "#023788",
    borderColor: "#6df1d8",
    flexDirection: "row",
    justifyContent: "center",
    alignItems: "center",
    padding: 9,
    borderWidth: 1,
    shadowColor: "#6df1d8",
    shadowOpacity: 8,
    shadowRadius: 3,
    shadowOffset: {
      height: 0,
      width: 0,
    },
  },
  buttonText: {
    color: "#ffffff",
    fontSize: 16,
  },
});

Reference Code

By Using Native SDK

for native SDK, you can use okta-react-native package like this

Login Screen

import React from 'react';
import { 
  Alert,
  Button, 
  StyleSheet, 
  TextInput,
  View,  
  ActivityIndicator 
} from 'react-native';

import {
  signIn,
  introspectIdToken
} from '@okta/okta-react-native';

export default class CustomLogin extends React.Component {
  
  constructor(props) {
    super(props);
    this.state = { 
      isLoading: false,
      username: '',
      password: '',
    };  
  }
  
  async componentDidMount() {
    
  }

  signInCustom = () => {
    this.setState({ isLoading: true });
    signIn({ username: this.state.username, password: this.state.password })
      .then(() => {
        introspectIdToken()
          .then(idToken => {
            this.props.navigation.navigate('ProfilePage', { idToken: idToken, isBrowserScenario: false });
          }).finally(() => {
            this.setState({ 
              isLoading: false,
              username: '', 
              password: '',
            });
          });
      })
      .catch(error => {
        // For some reason the app crashes when only one button exist (only with loaded bundle, debug is OK) ‍♂️
        Alert.alert(
          "Error",
          error.message,
          [
            {
              text: "Cancel",
              onPress: () => console.log("Cancel Pressed"),
              style: "cancel"
            },
            { text: "OK", onPress: () => console.log("OK Pressed") }
          ]
        );
    

        this.setState({
          isLoading: false
        });
      });
  }

  render() {
    if (this.state.isLoading) {
      return (
        <View style={styles.container}>
          <ActivityIndicator size="large" />
        </View>
      );
    }

    return (
      <View style={styles.container}>
        <TextInput 
          style={styles.input}
          placeholder='Username'
          onChangeText={input => this.setState({ username: input })}
          testID="username_input"
        />
        <TextInput 
          style={styles.input}
          placeholder='Password'
          onChangeText={input => this.setState({ password: input })}
          testID="password_input"
        />
        <Button 
          onPress={this.signInCustom} 
          title="Sign in" 
          testID='sign_in_button' 
        />
        <View style={styles.flexible}></View>
      </View>  
    ); 
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  input: {
    height: 40,
    width: '80%',
    margin: 12,
    borderWidth: 1,
    padding: 10,
  },
  flexible: {
    flex: 1,
  }
});

Profile Screen

import React from 'react';
import { 
  Text,
  Button, 
  StyleSheet, 
  TextInput,
  View,
} from 'react-native';

import {
  signOut,
  revokeAccessToken,
  revokeIdToken,
  clearTokens,
} from '@okta/okta-react-native';

export default class ProfilePage extends React.Component {
  constructor(props) {
    super(props);

    this.state = { 
      idToken: props.route.params.idToken,
      isBrowserScenario: props.route.params.isBrowserScenario
    };
  }

  logout = () => {
    if (this.state.isBrowserScenario == true) {
      signOut().then(() => {
        this.props.navigation.popToTop();
      }).catch(error => {
        console.log(error);
      });
    }

    Promise.all([revokeAccessToken(), revokeIdToken(), clearTokens()])
      .then(() => {
        this.props.navigation.popToTop();
      }).catch(error => {
        console.log(error);
      });
  }

  render() {
    return (
      <View style={styles.container}>
        <Text testID="welcome_text">Welcome back, {this.state.idToken.preferred_username}!</Text>
        <Button 
          onPress={this.logout}
          title="Logout"
          testID="logout_button"
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

Reference Code

Muhammad Numan
  • 23,222
  • 6
  • 63
  • 80
  • Thanks for the answer Muhammad. We are trying to go with the first method (fetch where the final fetch is to be done at the backend). But this part `const tokenResult = await exchangeCodeAsync(tokenRequestParams, discovery);` still returns error that we could not solve. Editing the original question accordingly. – Mehmet Kaplan Dec 13 '22 at 11:57
  • Maybe the problem can be because we are trying to use react-native-web and expo. – Mehmet Kaplan Dec 13 '22 at 13:14
  • Actually what we need is converting `params.code` and `params.state` to scope data. I am afraid we could not achieve this using `ExchangeForToken` and `ExchangeForUser` in your example. Maybe it can be because of some details missing in the configuration, I am not sure. (We are using `react-native-web` and `Expo`, these may have an impact.) – Mehmet Kaplan Dec 13 '22 at 15:17
  • @MehmetKaplan are you testing it on web or mobile? – Muhammad Numan Dec 14 '22 at 06:43
  • Now testing on web. (Mobile will be the next step after web is completed.) – Mehmet Kaplan Dec 14 '22 at 10:32
  • @MehmetKaplan can you verify if it is correct or not `okta_issuer_url`, `okta_client_id` – Muhammad Numan Dec 14 '22 at 12:56
  • We are using the exact code provided by Expo, here: https://docs.expo.dev/guides/authentication/#okta . As you may observe it does does not have okta_issuer_url. That code gets `response.params.code` and `response.params.state` correctly. The need is a fetch query to convert `response.params.code` and `response.params.state` to scope information. An atomic function would be very useful for that purpose. – Mehmet Kaplan Dec 14 '22 at 17:02
  • but for for restful apis we need to use issuer_url and client_id this is how you can get client_id https://pasteboard.co/KLrUMfCtL2Jl.png and issuer_url https://pasteboard.co/p79xwXI3zE3D.png – Muhammad Numan Dec 15 '22 at 07:00