Search code examples
expooktareact-native-webokta-apiokta-signin-widget

Okta authentication, how to use the response.params?


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.)


Solution

  • 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