{{announcement.body}}
{{announcement.title}}

Add a Login Feature to a React Native Application

DZone 's Guide to

Add a Login Feature to a React Native Application

In this article, we demonstrate how to implement a login feature with OpenID Connect in a React Native application.

· Web Dev Zone ·
Free Resource

React Native is an open-source mobile application framework for developing native Android and iOS apps. React components translate to native platform components, as opposed to using JavaScript/HTML and a web view. making React Native is ready to use in your current iOS and Android apps. 

In this tutorial, I’ll show you how to use React Native with Okta to implement a login feature in an app within ten minutes. For authorization, we’ll use OAuth 2.0, and for authentication, we’ll use OpenID Connect. Authentication allows us to verify who the user is, and authorization gives the user access to what they’re allowed to do. Okta makes authentication implementation super easy and even more so with React Native and Okta’s React Native SDK.

React Native 0.61 was just released a couple of weeks ago. One of its biggest features is Fast Refresh — a unification of live reloading (reload on save) and hot reloading. Fast Refreshfully supports modern React’s functional components and hooks and recovers after typos and other mistakes. In previous versions of React Native, a common complaint was that “hot reloading” was broken.

Prerequisites:

To install these prerequisites on a Mac, Linux, or Windows Subsystem for Linux (WSL), I recommend using Homebrew.

Plain Text




x


 
1
brew install node
2
brew install yarn
3
brew install watchman
4
brew tap AdoptOpenJDK/openjdk
5
brew cask install adoptopenjdk8



If you’re not using WSL for Windows, you can use Chocolatey to install everything from the command line:

Shell




xxxxxxxxxx
1


 
1
choco install -y nodejs.install python2 jdk8



You’ll also need to download and install IDEs for Android and iOS:

If you’d rather watch a video, I created a screencast of this tutorial.

Create a React Native Application

The React Native CLI is a popular way to get started with React Native development.

Shell




xxxxxxxxxx
1


 
1
npm install -g react-native-cli@2.0.1



Once you have React Native CLI installed, you can create a new application using the init command.

Shell




xxxxxxxxxx
1


 
1
react-native init ReactNativeLogin



Add Login With OIDC

Okta provides a React Native SDK which conveniently wraps the Okta native Android OIDC and iOS OIDC libraries.

I’m going to show you two ways to add OIDC-based login with Okta: The fast way with a tool I created and the step-by-step instructions.

This tool is based on Schematics and manipulates your project to install and configure everything.

Install Schematics globally.

Shell




xxxxxxxxxx
1


 
1
npm install -g @angular-devkit/schematics-cli@0.803.7



Create a Native App in Okta

Log in to your Okta Developer account (or sign up if you don’t have an account).

  • From the Applications page, choose Add Application.
  • On the Create New Application page, select Native as the platform, and click Next.
  • Give your app a memorable name, select Refresh Token as a grant type, and click Done.
  • Click the Edit button and add a Logout redirect URI that matches the default Login redirect URI (e.g., com.okta.dev-123456:/callback).
  • Click Save.

Install React Native OIDC Login

In a terminal, navigate into your ReactNativeLogin directory and install OktaDev Schematics:

Shell




xxxxxxxxxx
1


 
1
npm i @oktadev/schematics@1.0.0



Note: If you have a React Native 0.60.x app, use @oktadev/schematics@0.9.0. The only difference between the two is the tests.

Run the add-auth schematic in your ReactNativeLogin project.

Shell




xxxxxxxxxx
1


 
1
schematics @oktadev/schematics:add-auth



You will be prompted for an issuer and a clientId. You can find your issuer under API > Authorization Servers on Okta.


The client ID will be on your application screen.


This process will take a minute to complete.

Configure Your iOS Project to use Swift

React Native uses Objective-C, but the Okta React Native library uses Swift. Because of this, you have to add a Swift file in your iOS project for it to compile. Run the following command to open your native iOS project in Xcode.

Shell




xxxxxxxxxx
1


 
1
open ios/ReactNativeLogin.xcworkspace



To add a Swift file, complete the following steps:

  1. Right-click on your project and select New File….
  2. Select Swift File, and click Next.
  3. Enter a name (e.g., Polyfill) and click Create.
  4. If prompted for a header file, it is not required to create one.

Then, cd into ReactNativeLogin/ios and run pod install.

Tip: If you don’t have CocoaPoads installed, you can install it with gem install cocoapods.

Run Your React Native App on iOS

Navigate back to the root directory of your app. Start your app, and you should be able to authenticate with Okta. 

Plain Text




xxxxxxxxxx
1


 
1
react-native run-ios



Once you’re signed in, you’ll see options to log out, get the user’s information from an ID token, and get the user’s information from the React Native SDK’s  getUser() method (a.k.a. the request).



Note: The prompt when you click Login cannot be avoided. This is an iOS safety mechanism. It also pops up when you log out. See this issue for more information.

Run Your React Native App on Android

The schematic you ran modifies all the necessary files for Android; there are no code modifications required!

You will need to run an AVD (Android Virtual Device) before starting your app, or you can plug in your Android phone and use that. If you have neither, launch Android Studio and go to Tools > AVD Manager. Click Create Virtual Device at the bottom and select a phone of your choice. I chose a Pixel 3 XL with Android 10.

Start your AVD, then your app, and authenticate with Okta. 

Plain Text




xxxxxxxxxx
1


 
1
react-native run-android




Click the Get User from ID Token button to confirm you can retrieve the user’s information.

Run Installed React Native Authentication Tests

In addition to integrating login, OktaDev Schematics also installed some tests that verify login and authentication work. Run npm test to see these tests run in your terminal.

Plain Text




xxxxxxxxxx
1


 
1
Snapshot Summary
2
  1 snapshot written from 1 test suite.
3
 
          
4
Test Suites: 2 passed, 2 total
5
Tests:       12 passed, 12 total
6
Snapshots:   1 written, 1 total
7
Time:        8.952s
8
Ran all test suites.



Note: OktaDev Schematics puts tests in a tests directory rather than the default __tests__ directory because Angular Schematics uses double underscore as a placeholder.

Using a Custom Login Screen with Okta

This example showed you how to add an OIDC flow that opens a browser when a user logs in and logs out. If you require a smoother login experience that doesn’t pop open a browser, see Okta’s Custom Sign-In Example, for example, code that shows you how to implement that type of flow.

Add OIDC Login the Hard Way

The previous section showed you how to use OktaDev Schematics to quickly add a login feature (and tests!) to a React Native application. However, you might have an existing React Native application that doesn’t have the same structure as a brand new React Native application.

This section shows you everything that OktaDev Schematics does for you, in detail.

Create a project with React Native CLI and install Okta’s SDK.

Shell




xxxxxxxxxx
1


 
1
react-native init ReactNativeLogin
2
cd ReactNativeLogin
3
npm install @okta/okta-react-native@1.2.1



For iOS, modify ios/Podfile to change it from iOS 9 to iOS 11.

Shell




xxxxxxxxxx
1


 
1
platform :ios, '11.0'



Open your project in Xcode.

Shell




xxxxxxxxxx
1


 
1
open ios/ReactNativeLogin.xcworkspace



Add a Swift file.

  1. Right-click on your project and select New File….
  2. Select Swift File, and click Next.
  3. Enter a name (e.g., Polyfill) and click Create.
  4. If prompted for a header file, it is not required to create one.

Install iOS native dependencies with CocoaPods.

Shell




xxxxxxxxxx
1


 
1
cd ios
2
pod install


Add Jest and Enzyme to Test Your React Native Login

Jest is a library for testing JavaScript apps, and Enzyme is a library that makes it easier to select and query elements in tests. They’re often used alongside one another.

Install testing dependencies with npm.

Shell




xxxxxxxxxx
1


 
1
npm i enzyme@3.10.0 enzyme-adapter-react-16@1.14.0 enzyme-async-helpers@0.9.1 react-dom@16.9.0



Then, change your jest key in package.json to match the following:

JSON




xxxxxxxxxx
1
10


 
1
"jest": {
2
  "preset": "react-native",
3
  "automock": false,
4
  "transformIgnorePatterns": [
5
    "node_modules/(?!@okta|react-native)"
6
  ],
7
  "setupFiles": [
8
    "./setupJest.js"
9
  ]
10
}



Create setupJest.js to polyfill React Native for Okta.

JavaScript




xxxxxxxxxx
1
32


 
1
// Required to correctly polyfill React-Native
2
 
          
3
import { configure } from 'enzyme';
4
import Adapter from 'enzyme-adapter-react-16';
5
import { NativeModules } from 'react-native';
6
 
          
7
configure({ adapter: new Adapter() });
8
 
          
9
global.XMLHttpRequest = jest.fn();
10
global.fetch = jest.fn();
11
 
          
12
if (typeof window !== 'object') {
13
  global.window = global;
14
  global.window.navigator = {};
15
}
16
 
          
17
NativeModules.OktaSdkBridge = {
18
  createConfig: jest.fn(),
19
  signIn: jest.fn(),
20
  signOut: jest.fn(),
21
  getAccessToken: jest.fn(),
22
  getIdToken: jest.fn(),
23
  getUser: jest.fn(),
24
  isAuthenticated: jest.fn(),
25
  revokeAccessToken: jest.fn(),
26
  revokeIdToken: jest.fn(),
27
  revokeRefreshToken: jest.fn(),
28
  introspectAccessToken: jest.fn(),
29
  introspectIdToken: jest.fn(),
30
  introspectRefreshToken: jest.fn(),
31
  refreshTokens: jest.fn(),
32
};



Create Auth.js to handle your authentication code.

Java




xxxxxxxxxx
1
154


 
1
import React, { Component, Fragment } from 'react';
2
 
          
3
import { SafeAreaView, ScrollView, Button, StyleSheet, Text, View } from 'react-native';
4
import { createConfig, signIn, signOut, isAuthenticated, getUser, getUserFromIdToken, EventEmitter } from '@okta/okta-react-native';
5
import configFile from './auth.config';
6
 
          
7
export default class Auth extends Component {
8
  constructor() {
9
    super();
10
    this.state = {
11
      authenticated: false,
12
      context: null,
13
    };
14
    this.checkAuthentication = this.checkAuthentication.bind(this);
15
  }
16
 
          
17
  async componentDidMount() {
18
    let that = this;
19
    EventEmitter.addListener('signInSuccess', function (e: Event) {
20
      that.setState({authenticated: true});
21
      that.setContext('Logged in!');
22
    });
23
    EventEmitter.addListener('signOutSuccess', function (e: Event) {
24
      that.setState({authenticated: false});
25
      that.setContext('Logged out!');
26
    });
27
    EventEmitter.addListener('onError', function (e: Event) {
28
      console.warn(e);
29
      that.setContext(e.error_message);
30
    });
31
    EventEmitter.addListener('onCancelled', function (e: Event) {
32
      console.warn(e);
33
    });
34
    await createConfig({
35
      clientId: configFile.oidc.clientId,
36
      redirectUri: configFile.oidc.redirectUri,
37
      endSessionRedirectUri: configFile.oidc.endSessionRedirectUri,
38
      discoveryUri: configFile.oidc.discoveryUri,
39
      scopes: configFile.oidc.scopes,
40
      requireHardwareBackedKeyStore: configFile.oidc.requireHardwareBackedKeyStore,
41
    });
42
    this.checkAuthentication();
43
  }
44
 
          
45
  componentWillUnmount() {
46
    EventEmitter.removeAllListeners('signInSuccess');
47
    EventEmitter.removeAllListeners('signOutSuccess');
48
    EventEmitter.removeAllListeners('onError');
49
    EventEmitter.removeAllListeners('onCancelled');
50
  }
51
 
          
52
  async componentDidUpdate() {
53
    this.checkAuthentication();
54
  }
55
 
          
56
  async checkAuthentication() {
57
    const result = await isAuthenticated();
58
    if (result.authenticated !== this.state.authenticated) {
59
      this.setState({authenticated: result.authenticated});
60
    }
61
  }
62
 
          
63
  async login() {
64
    signIn();
65
  }
66
 
          
67
  async logout() {
68
    signOut();
69
  }
70
 
          
71
  async getUserIdToken() {
72
    let user = await getUserFromIdToken();
73
    this.setContext(JSON.stringify(user, null, 2));
74
  }
75
 
          
76
  async getMyUser() {
77
    let user = await getUser();
78
    this.setContext(JSON.stringify(user, null, 2));
79
  }
80
 
          
81
  setContext = message => {
82
    this.setState({
83
      context: message,
84
    });
85
  };
86
 
          
87
  renderButtons() {
88
    if (this.state.authenticated) {
89
      return (
90
        <View style={styles.buttonContainer}>
91
          <View style={styles.button}>
92
            <Button
93
              onPress={async () => {
94
                this.getUserIdToken();
95
              }}
96
              title="Get User From Id Token"
97
            />
98
          </View>
99
        </View>
100
      );
101
    }
102
  }
103
 
          
104
  render() {
105
    return (
106
      <Fragment>
107
        <SafeAreaView style={styles.container}>
108
          <View style={styles.buttonContainer}>
109
            <View style={styles.button}>
110
              {this.state.authenticated ? (
111
                <Button
112
                  style={styles.button}
113
                  testID="logoutButton"
114
                  onPress={async () => { this.logout() }}
115
                  title="Logout"
116
                />
117
              ) : (
118
                <Button
119
                  style={styles.button}
120
                  testID="loginButton"
121
                  onPress={async () => { this.login() }}
122
                  title="Login"
123
                />
124
              )}
125
            </View>
126
          </View>
127
          {this.renderButtons()}
128
          <ScrollView
129
            contentInsetAdjustmentBehavior="automatic"
130
            style={styles.context}>
131
            <Text>{this.state.context}</Text>
132
          </ScrollView>
133
        </SafeAreaView>
134
      </Fragment>
135
    );
136
  }
137
}
138
 
          
139
const styles = StyleSheet.create({
140
  buttonContainer: {
141
    flexDirection: 'column',
142
    justifyContent: 'space-between',
143
  },
144
  button: {
145
    width: 300,
146
    height: 40,
147
    marginTop: 10,
148
  },
149
  container: {
150
    flex: 1,
151
    flexDirection: 'column',
152
    alignItems: 'center',
153
  }
154
});



You might notice it imports a config file at the top.

JavaScript




xxxxxxxxxx
1


 
1
import configFile from './auth.config';



Create auth.config with your OIDC settings from Okta.

JSON




xxxxxxxxxx
1
10


 
1
export default {
2
  oidc: {
3
    clientId: '$yourClientId',
4
    redirectUri: 'com.okta.dev-#######:/callback',
5
    endSessionRedirectUri: 'com.okta.dev-#######:/callback',
6
    discoveryUri: 'https://dev-#######.okta.com/oauth2/default',
7
    scopes: ['openid', 'profile', 'offline_access'],
8
    requireHardwareBackedKeyStore: false,
9
  },
10
};



Create an app on Okta to get the values for $yourClientId and ######.

  • From the Applications page, choose Add Application.
  • On the Create New Application page, select Native as the platform, and click Next.
  • Give your app a memorable name, select Refresh Token as a grant type, and click Done.
  • Click the Edit button and add a Logout redirect URI that matches the default Login redirect URI (e.g., com.okta.dev-123456:/callback).
  • Click Save.

In App.js, import Auth.

JavaScript




xxxxxxxxxx
1


 
1
import Auth from './Auth';



Use it in a new <View /> after the Hermes logic.

Java




xxxxxxxxxx
1
19


 
1
 
          
2
<ScrollView
3
  contentInsetAdjustmentBehavior="automatic"
4
  style={styles.scrollView}>
5
  <Header />
6
  {global.HermesInternal == null ? null : (
7
    <View style={styles.engine}>
8
      <Text style={styles.footer}>Engine: Hermes</Text>
9
    </View>
10
  )}
11
  <View style={styles.body}>
12
    <View style={styles.sectionContainer}>
13
      <Text style={styles.sectionTitle}>Step Zero</Text>
14
      <Text style={styles.sectionDescription}>
15
        Use <Text style={styles.highlight}>Okta</Text> for
16
        authentication.
17
      </Text>
18
      <Auth />
19
    </View>



At this point, your tests will not pass because Okta uses an EventEmitter to communicate between components.

Add React Native Authentication Tests

To mock the native event emitter that Okta uses, add a mock for it in __tests__/App-test.js.

Java




xxxxxxxxxx
1
16


 
1
/**
2
 * @format
3
 */
4
 
          
5
import 'react-native';
6
import React from 'react';
7
import renderer from 'react-test-renderer';
8
import App from '../App';
9
 
          
10
jest.mock(
11
  '../node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter',
12
);
13
 
          
14
it('renders correctly', () => {
15
  renderer.create(<App />);
16
});



To make sure all the login and authentication logic works, create __tests__/Auth-test.js.

Java




xxxxxxxxxx
1
138


 
1
import React from 'react';
2
import Auth from '../Auth';
3
import { shallow } from 'enzyme';
4
import renderer from 'react-test-renderer';
5
import { waitForState } from 'enzyme-async-helpers';
6
import { NativeEventEmitter } from 'react-native';
7
 
          
8
const nativeEmitter = new NativeEventEmitter();
9
 
          
10
jest
11
  .mock(
12
    '../node_modules/react-native/Libraries/Components/StatusBar/StatusBar',
13
    () => 'StatusBar',
14
  )
15
  .mock(
16
    '../node_modules/react-native/Libraries/Components/ScrollView/ScrollView',
17
    () => 'ScrollView',
18
  )
19
  .mock(
20
    '../node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter',
21
  );
22
 
          
23
global.fetch = jest
24
  .fn()
25
  .mockImplementation(() => {
26
    return new Promise((resolve, reject) => {
27
      resolve({
28
        json: () => {
29
          return {
30
            user: [{ foo: 'foo', bar: 'bar' }],
31
          }
32
        },
33
        ok: true,
34
      });
35
    });
36
  })
37
  .mockImplementationOnce(() => {
38
    return new Promise((resolve, reject) => {
39
      resolve({
40
        json: () => {
41
          return {
42
            userinfo_endpoint: 'dummy_endpoint',
43
          }
44
        },
45
        ok: true,
46
      });
47
    });
48
  });
49
 
          
50
describe('auth setup', () => {
51
  it('should render without crashing', () => {
52
    const rendered = renderer.create(<Auth />).toJSON();
53
    expect(rendered).toBeTruthy();
54
  });
55
 
          
56
  it('should render correctly', () => {
57
    const rendered = renderer.create(<Auth />).toJSON();
58
    expect(rendered).toMatchSnapshot();
59
  });
60
 
          
61
  it('should initialize with default state', () => {
62
    const wrapper = shallow(<Auth />);
63
    expect(wrapper.state().authenticated).toBe(false);
64
    expect(wrapper.state().context).toBe(null);
65
  });
66
 
          
67
  it('should render login button if not authenticated', () => {
68
    const wrapper = shallow(<Auth />);
69
    const loginButton = wrapper.find('Button').get(0);
70
    expect(loginButton.props.title).toBe('Login');
71
  });
72
 
          
73
  it('should render logout and get user info buttons if authenticated', () => {
74
    const wrapper = shallow(<Auth />);
75
    wrapper.setState({authenticated: true});
76
    const logoutButton = wrapper.find('Button').get(0);
77
    const getUserFromIdButton = wrapper.find('Button').get(1);
78
    const getUserButton = wrapper.find('Button').get(2);
79
    expect(logoutButton.props.title).toBe('Logout');
80
    expect(getUserFromIdButton.props.title).toBe('Get User From Id Token');
81
    expect(getUserButton.props.title).toBe('Get User From Request');
82
  });
83
 
          
84
  it('should not render login button if authenticated', () => {
85
    const wrapper = shallow(<Auth />);
86
    wrapper.setState({authenticated: true});
87
    const loginButton = wrapper.find('Button').get(0);
88
    expect(loginButton.props.title).not.toBe('Login');
89
  });
90
 
          
91
  it('should not render logout and get user info buttons if not authenticated', () => {
92
    const wrapper = shallow(<Auth />);
93
    const logoutButton = wrapper.find('Button').get(0);
94
    const getUserFromIdButton = wrapper.find('Button').get(1);
95
    const getUserButton = wrapper.find('Button').get(2);
96
    expect(logoutButton.props.title).not.toBe('Logout');
97
    expect(getUserFromIdButton).toBe(undefined);
98
    expect(getUserButton).toBe(undefined);
99
  });
100
});
101
 
          
102
describe('authentication flow', () => {
103
  it('should detect when the user has logged in', async () => {
104
    const wrapper = shallow(<Auth />);
105
    const loginButton = wrapper.find('Button').get(0);
106
    await loginButton.props.onPress();
107
    expect(loginButton.props.title).toBe('Login');
108
    nativeEmitter.emit('signInSuccess');
109
    expect(wrapper.state().authenticated).toBe(true);
110
    expect(wrapper.state().context).toBe('Logged in!');
111
  });
112
 
          
113
  it('should detect when the user has signed out', async () => {
114
    const wrapper = shallow(<Auth />);
115
    wrapper.setState({authenticated: true});
116
    const logoutButton = wrapper.find('Button').get(0);
117
    await logoutButton.props.onPress();
118
    expect(logoutButton.props.title).toBe('Logout');
119
    nativeEmitter.emit('signOutSuccess');
120
    expect(wrapper.state().authenticated).toBe(false);
121
    expect(wrapper.state().context).toBe('Logged out!');
122
  });
123
 
          
124
  it('should return user profile information from id token' , async () => {
125
    const mockGetIdToken = require('react-native').NativeModules.OktaSdkBridge.getIdToken;
126
    mockGetIdToken.mockImplementationOnce(() => {
127
      // id_token returns { a: 'b' }
128
      return {'id_token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoiYiJ9.jiMyrsmD8AoHWeQgmxZ5yq8z0lXS67_QGs52AzC8Ru8'};
129
    });
130
    const wrapper = shallow(<Auth />);
131
    wrapper.setState({authenticated: true});
132
    const profileButton = wrapper.find('Button').get(1);
133
    await profileButton.props.onPress();
134
    await waitForState(wrapper, state => state.context !== null);
135
    expect(profileButton.props.title).toBe('Get User From Id Token');
136
    expect(wrapper.state().context).toContain('"a": "b"');
137
  });
138
});



Run npm test to bask in the fruits of your labor!

To run your app on iOS, use react-native run-ios.


To run it on Android, you’ll need to modify your Gradle build files.

Okta’s React Native SDK depends on the Okta OIDC Android library. You have to add this library through Gradle.

  1. Add Okta’s BinTray repo to android/build.gradle, under allprojects -> repositories.
    Groovy
    xxxxxxxxxx
    1
     
    1
    maven {
    2
       url  "https://dl.bintray.com/okta/com.okta.android"
    3
    }
  2. Make sure your minSdkVersion is 19 in android/build.gradle.
  3. Define a redirect scheme to capture the authorization redirect. In android/app/build.gradle, under android -> defaultConfig, add:
     manifestPlaceholders = [ appAuthRedirectScheme: 'com.okta.dev-###### ]
    

Finally, start a virtual device (or plug in your phone), and run react-native run-android.

Learn More About React Native and OIDC Login

This tutorial showed you how to add a login feature to a React Native application. You learned that OAuth 2.0 is an authorization protocol, and OIDC is an authentication layer on top of it. You also used PKCE (Public Key Code Exchange) in your implementation, which is the more secure way to implement OAuth 2.0 in mobile applications.

I hope you enjoy your React Native development journey and its fast refresh feature!

You can find the source code for this example on GitHub at oktadeveloper/okta-react-native-login-example.

To learn more about React Native, OIDC, and PKCE, check out these posts:

If you liked this tutorial, follow @oktadev on Twitter and subscribe to our YouTube channel.

Create a React Native App with Login in 10 Minutes was originally published on the Okta Developer Blog on November 14, 2019. 

Topics:
javascript, login, mobile, oidc, react, react-native, tutorial

Published at DZone with permission of Matt Raible , DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}