Handling authentication in React Native using Okta
You will need the React Native CLI installed on your machine.
While building applications, authentication is usually a very important aspect because this is how you identify every user and it can sometimes be tedious. This problem is what Authentication Service Providers solve. They provide authentication and user management services for applications and sometimes easily configurable functionalities such as the login, log out, social media integration and they support authentication protocols such as OpenID Connect
and SMAL
.
Okta (formerly Stormpath)
Okta recently gained Stormpath and a couple of things have changed. Stormpath API was shut down fully in August 2017.
Okta is a third party authentication provider that adds authentication, authorization, and user management to your web or mobile app within minutes.
Okta provides token authentication, single sign-on, multi-factor authentication, and social logins.
Okta account setup
To get started with using Okta, create an account on the registration page and if you’ve already got an account, log in. After login, see a screen similar to this.
After login is successful, we need to add an application and then configure the app. To add an application, navigate through Shortcut > Add Application > Create new app. Then select Native apps from the drop-down and click create
This will take you to a Create OpenID Connect Integration page. See image below for an example:
Replace developer
in the redirect URI with your Okta account company name and then save.
You will need the ClientID
and the redirect_uri
when setting up our React Native app.
Project setup
To set up our React Native application
$ react-native init react_native_okta_app
This will initialize our project. To be sure everything went well, build the app:
$ react-native run-ios
For Android, we run:
$ react-native run-android
Install dependencies
$ npm install react-native-app-auth --save
$ react-native link
Setup for iOS
Navigate to the iOS folder.
sudo gem install cocoapods
Then create a Podfile
and paste
platform :ios, '11.0'
target 'react_native_okta_app' do
pod 'AppAuth', '>= 0.95'
end
Run pod install
. If you encounter any error, run pod repo update
.
If there are no errors, open the react_native_okta_app.xcworkspace
and edit the AppDelegate.h
file.
#import <UIKit/UIKit.h>
#import "RNAppAuthAuthorizationFlowManager.h"
@interface AppDelegate : UIResponder <UIApplicationDelegate, RNAppAuthAuthorizationFlowManager>
@property (nonatomic, weak) id<RNAppAuthAuthorizationFlowManagerDelegate>authorizationFlowManagerDelegate;
@property (nonatomic, strong) UIWindow *window;
@end
Setup for Android
To get started on Android devices, from the project root directory, navigate to the android directory and upgrade Gradle to the latest version
$ cd android
$ ./gradlew wrapper --gradle-version 4.10.2
If you get an error that says: "compile" is obsolete and has been replaced with "implementation"
, you must edit the app/src/build.gradle
file and make the change under the react-native-app-auth
dependency:
dependencies {
implementation project(':react-native-app-auth')
...
}
We also need to add appAuthRedirectScheme
to the defaultConfig
section.
defaultConfig {
...
manifestPlaceholders = [
appAuthRedirectScheme: '{yourOktaCompanyUsername}'
]
}
Save the changes and run Android react-native run-android
Build the React Native app
We’ll split our application into components. to get started, we need to install styled-components
in order to style our application.
We also need a background image for our application. Save your image of choice in a folder called assets
. I will be using the stock image available here.
$ npm install styled-components --save
$ mkdir assets/
Create a new folder called components
, create the following files inside the folder:
$ touch index.js Header.js Button.js ButtonContainer.js Page.js Form.js
Replace the contents of the files above with the following:
// /components/index.js
export { default as Button } from './Button';
export { default as ButtonContainer } from './ButtonContainer';
export { default as Form } from './Form';
export { default as Header } from './Header';
export { default as Page } from './Page';
// /components/Header.js
import { Platform } from 'react-native';
import styled from 'styled-components/native';
export default styled.Text`
color: white;
font-size: 32px;
margin-top: 120px;
background-color: transparent;
text-align: center;
`;
// /components/Button.js
import React, { Component } from 'react';
import { Platform } from 'react-native';
import styled from 'styled-components/native';
type Props = {
text: string,
color: string,
onPress: () => any
};
const ButtonBox = styled.TouchableOpacity.attrs({ activeOpacity: 0.8 })`
height: 50px;
flex: 1;
margin: 5px;
align-items: center;
justify-content: center;
background-color: ${props => props.color};
`;
const ButtonText = styled.Text`
color: white;
`;
const Button = ({ text, color, onPress }: Props) => (
<ButtonBox onPress={onPress} color={color}>
<ButtonText>{text}</ButtonText>
</ButtonBox>
);
export default Button;
// /components/ButtonContainer.js
import styled from 'styled-components/native';
const ButtonContainer = styled.View`
position: absolute;
left: 0;
right: 0;
bottom: 0;
align-self: flex-end;
flex-direction: row;
margin: 5px;
`;
export default ButtonContainer;
// /components/Page.js
import styled from 'styled-components/native';
export default styled.ImageBackground.attrs({
source: require('../assets/image1.jpeg')
})`
flex: 1;
background-color: white;
padding: 40px 10px 10px 10px;
`;
// /components/Form.js
import styled from 'styled-components/native';
const Form = styled.View`
flex: 1;
`;
Form.Label = styled.Text`
font-size: 14px;
font-weight: bold;
background-color: transparent;
margin-bottom: 10px;
`;
Form.Value = styled.Text.attrs({ numberOfLines: 10, ellipsizeMode: 'tail' })`
font-size: 14px;
background-color: transparent;
margin-bottom: 20px;
`;
export default Form;
The components created above, are all exported and can be accessed in our app.js
file.
Import all dependencies and components
Let us replace the import statements in our app.js
file with this:
// App.js
import React, { Component } from 'react';
import { Alert, UIManager, LayoutAnimation } from 'react-native';
import { authorize, refresh, revoke } from 'react-native-app-auth';
import { Page, Button, ButtonContainer, Form, Heading } from './components';
Then we need to define some properties in our state object:
type State = {
hasLoggedInOnce: boolean,
accessToken: ?string,
accessTokenExpirationDate: ?string,
refreshToken: ?string
};
The most important part is where we define our config
object to be used for Okta authentication. You will need your issuer
, clientId
, redirectUrl
, and so on, and they all can be gotten from your Okta application dashboard:
const config = {
issuer: 'https://<oktausername>.okta.com',
clientId: '<your okta application client ID>',
redirectUrl: 'com.okta.<oktausername>:/callback',
serviceConfiguration: {
authorizationEndpoint: 'https://<oktausername>.okta.com/oauth2/v1/authorize',
tokenEndpoint: 'https://<oktausername>.okta.com/oauth2/v1/token',
registrationEndpoint: 'https://<oktausername>.okta.com/oauth2/v1/clients'
},
additionalParameters: {
prompt: 'login'
},
scopes: ['openid', 'profile', 'email', 'offline_access']
};
We need to create our authorize
function to enable users to log in and also create functions to refresh our authentication token or revoke it.
authorize = async () => {
try {
const authState = await authorize(config);
this.animateState(
{
hasLoggedInOnce: true,
accessToken: authState.accessToken,
accessTokenExpirationDate: authState.accessTokenExpirationDate,
refreshToken: authState.refreshToken
},
500
);
} catch (error) {
Alert.alert('Failed to log in', error.message);
}
};
refresh = async () => {
try {
const authState = await refresh(config, {
refreshToken: this.state.refreshToken
});
this.animateState({
accessToken: authState.accessToken || this.state.accessToken,
accessTokenExpirationDate:
authState.accessTokenExpirationDate || this.state.accessTokenExpirationDate,
refreshToken: authState.refreshToken || this.state.refreshToken
});
} catch (error) {
Alert.alert('Failed to refresh token', error.message);
}
};
revoke = async () => {
try {
await revoke(config, {
tokenToRevoke: this.state.accessToken,
sendClientId: true
});
this.animateState({
accessToken: '',
accessTokenExpirationDate: '',
refreshToken: ''
});
} catch (error) {
Alert.alert('Failed to revoke token', error.message);
}
};
In the authorize
method, we pass the config
object and try to fetch the access token, access token expiration date, and if there are errors, we’ll send alert the user with a pop-up.
In the refresh
method, we pass the config
object and try to refresh the token. When an authentication token is refreshed, the expiration date is also refreshed alongside.
The revoke method deactivates a token. This means that the user will be required to log in again or refresh the token to get a new token.
To finally wrap up the code, when a user opens our application, we first need to check if the user has been logged in, before deciding what to render. If the user has logged in before, we display the token and expiration date, else a button for the user to authenticate from.
render() {
const {state} = this;
return (
<Page>
{!!state.accessToken ? (
<Form>
<Form.Label>accessToken</Form.Label>
<Form.Value>{state.accessToken}</Form.Value>
<Form.Label>accessTokenExpirationDate</Form.Label>
<Form.Value>{state.accessTokenExpirationDate}</Form.Value>
<Form.Label>refreshToken</Form.Label>
<Form.Value>{state.refreshToken}</Form.Value>
</Form>
) : (
<Header>{state.hasLoggedInOnce ? 'Goodbye.' : 'Okta and React Native!'}</Header>
)}
<ButtonContainer>
{!state.accessToken && (
<Button onPress={this.authorize} text="Login" color="#017CC0"/>
)}
{!!state.refreshToken && <Button onPress={this.refresh} text="Refresh" color="#24C2CB"/>}
{!!state.accessToken && <Button onPress={this.revoke} text="Revoke" color="#EF525B"/>}
</ButtonContainer>
</Page>
);
}
Putting it all together, App.js
should look like this:
import React, { Component } from 'react';
import { Alert, UIManager, LayoutAnimation } from 'react-native';
import { authorize, refresh, revoke } from 'react-native-app-auth';
import { Page, Button, ButtonContainer, Form, Header } from './components';
UIManager.setLayoutAnimationEnabledExperimental &&
UIManager.setLayoutAnimationEnabledExperimental(true);
type State = {
hasLoggedInOnce: boolean,
accessToken: ?string,
accessTokenExpirationDate: ?string,
refreshToken: ?string
};
const config = {
issuer: 'https://<oktausername>.okta.com',
clientId: '<your okta application client ID>',
redirectUrl: 'com.okta.<oktausername>:/callback',
serviceConfiguration: {
authorizationEndpoint: 'https://<oktausername>.okta.com/oauth2/v1/authorize',
tokenEndpoint: 'https://<oktausername>.okta.com/oauth2/v1/token',
registrationEndpoint: 'https://<oktausername>.okta.com/oauth2/v1/clients'
},
additionalParameters: {
prompt: 'login'
},
scopes: ['openid', 'profile', 'email', 'offline_access']
};
export default class App extends Component<{}, State> {
state = {
hasLoggedInOnce: false,
accessToken: '',
accessTokenExpirationDate: '',
refreshToken: ''
};
animateState(nextState: $Shape<State>, delay: number = 0) {
setTimeout(() => {
this.setState(() => {
LayoutAnimation.easeInEaseOut();
return nextState;
});
}, delay);
}
authorize = async () => {
try {
const authState = await authorize(config);
this.animateState(
{
hasLoggedInOnce: true,
accessToken: authState.accessToken,
accessTokenExpirationDate: authState.accessTokenExpirationDate,
refreshToken: authState.refreshToken
},
500
);
} catch (error) {
Alert.alert('Failed to log in', error.message);
}
};
refresh = async () => {
try {
const authState = await refresh(config, {
refreshToken: this.state.refreshToken
});
this.animateState({
accessToken: authState.accessToken || this.state.accessToken,
accessTokenExpirationDate:
authState.accessTokenExpirationDate || this.state.accessTokenExpirationDate,
refreshToken: authState.refreshToken || this.state.refreshToken
});
} catch (error) {
Alert.alert('Failed to refresh token', error.message);
}
};
revoke = async () => {
try {
await revoke(config, {
tokenToRevoke: this.state.accessToken,
sendClientId: true
});
this.animateState({
accessToken: '',
accessTokenExpirationDate: '',
refreshToken: ''
});
} catch (error) {
Alert.alert('Failed to revoke token', error.message);
}
};
render() {
const {state} = this;
return (
<Page>
{!!state.accessToken ? (
<Form>
<Form.Label>accessToken</Form.Label>
<Form.Value>{state.accessToken}</Form.Value>
<Form.Label>accessTokenExpirationDate</Form.Label>
<Form.Value>{state.accessTokenExpirationDate}</Form.Value>
<Form.Label>refreshToken</Form.Label>
<Form.Value>{state.refreshToken}</Form.Value>
</Form>
) : (
<Header>{state.hasLoggedInOnce ? 'Goodbye.' : 'Okta and React Native!'}</Header>
)}
<ButtonContainer>
{!state.accessToken && (
<Button onPress={this.authorize} text="Login" color="#017CC0"/>
)}
{!!state.refreshToken && <Button onPress={this.refresh} text="Refresh" color="#24C2CB"/>}
{!!state.accessToken && <Button onPress={this.revoke} text="Revoke" color="#EF525B"/>}
</ButtonContainer>
</Page>
);
}
}
After receiving the access token, it can be used to send requests to your Okta based API to authenticate with clients.
Install on Android or iOS
To build for Android:
$ react-native run-android
To build for iOS:
$ react-native run-ios
On installation, your screen should be similar to the following:
Conclusion
With third-party authentication, we can easily build authentication into our applications and save time rather than build our own authentication system from scratch for every application every time.
The code base to this tutorial is available in a public GitHub Repository. Feel free to experiment around with it.
30 January 2019
by Samuel Ogundipe