Skip to content
This repository has been archived by the owner on Oct 24, 2024. It is now read-only.

Commit

Permalink
feat[react-native]: support primary auth and expose auth client - OKT…
Browse files Browse the repository at this point in the history
…A-291767 (#751)

* feat: support primary auth and expose auth client - OKTA-291767
  • Loading branch information
shuowu-okta authored Apr 21, 2020
1 parent 8030421 commit 9d53b35
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 16 deletions.
8 changes: 8 additions & 0 deletions packages/okta-react-native/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#1.4.0

### Features
- [#751](https://github.com/okta/okta-oidc-js/pull/751)
- Support primary authentication flow
- Add `getAuthClient` method to expose `@okta/okta-auth-js` client instance
- Add `Promise` support for `authenticate` method

# 1.3.0

### Features
Expand Down
31 changes: 30 additions & 1 deletion packages/okta-react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This library follows the current best practice for native apps using:
* [OAuth 2.0 Authorization Code Flow](https://tools.ietf.org/html/rfc6749#section-1.3.1)
* [Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636)

This library also exposes APIs to interact with [Authentication API](https://developer.okta.com/docs/api/resources/authn) directly to implement native UI for authentication.

You can learn more on the [Okta + ReactNative](https://developer.okta.com/code/react-native/) page in our documentation. You can also download our [sample application](https://github.com/okta/samples-js-react-native/tree/master/browser-sign-in)

## Prerequisites
Expand Down Expand Up @@ -180,8 +182,11 @@ import { createConfig, signIn, signOut, getAccessToken } from '@okta/okta-react-

This method will create a configured client on the native modules. Resolves `true` if successfully configures a client. Note: `requireHardwareBackedKeyStore` is a configurable setting only on android devices. If you're a developer testing on android emulators, set this field to `false`.

**Note**: `issuer` is an optional field in config, for more information please refer to [About the Issuer](https://github.com/okta/okta-auth-js/tree/master#about-the-issuer)

```javascript
await createConfig({
issuer: "https://{yourOktaDomain}/oauth2/default", // optional
clientId: "{clientId}",
redirectUri: "{redirectUri}",
endSessionRedirectUri: "{endSessionRedirectUri}",
Expand All @@ -191,14 +196,38 @@ await createConfig({
});
```

### `getAuthClient`

This method will return an instance of [`@okta/okta-auth-js`](https://github.com/okta/okta-auth-js) client to communicate with Okta Authentication API. For more information, please checkout [Okta AuthJs Node JS and React Native Usage](https://github.com/okta/okta-auth-js#node-js-and-react-native-usage) section.

### `signIn`

This async method will automatically redirect users to your Okta organziation for authentication. It will emit an event once a user successfully signs in. Make sure your event listeners are mounted and unmounted. Note: on iOS there isn't a `onCancelled` event. If the sign in process is cancelled, `onError` will be triggered.
This method will handle both `browser-sign-in` and `custom-sign-in` scenarios based on provided options.

This async method will automatically redirect users to your Okta organziation for authentication. It will emit an event once a user successfully signs in. Make sure your event listeners are mounted and unmounted. Note: on iOS there isn't a `onCancelled` event. If the sign in process is cancelled, `onError` will be triggered.

#### `browser-sign-in`
`browser-sign-in` leverages device's native browser to automatically redirect users to your Okta organziation for authentication. By providing no argument, this method will trigger the `browser-sign-in` flow. It will emit an event once a user successfully signs in. Make sure your event listeners are mounted and unmounted. **Note**: on iOS there isn't a `onCancelled` event. If the sign in process is cancelled, `onError` will be triggered.

```javascript
signIn();
```

#### `custom-sign-in`
`custom-sign-in` provides the way to authenticate the user within the native application. By providing `options` object with username and password fields, this method will retrieve `sessionToken` then exchange it for `accessToken`.
Both `Promise` and `Event listeners` are supported. This method is leveraging `@okta/okta-auth-js` SDK to perform authentication API request. For more information, please checkout [Okta AuthJs signIn options](https://github.com/okta/okta-auth-js#signinoptions) section.

##### Sample Usage
```javascript
signIn({ username: "{username}", password: "{password}" })
.then(token => {
// consume accessToken from token.access_token
})
.catch(error => {
// handle error
})
```

##### Sample Usage

```javascript
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,13 @@ public void signIn() {
}

@ReactMethod
public void authenticate(String sessionToken) {
public void authenticate(String sessionToken, final Promise promise) {
if (authClient == null) {
final WritableMap params = Arguments.createMap();
params.putString(OktaSdkConstant.ERROR_CODE_KEY, OktaSdkError.NOT_CONFIGURED.getErrorCode());
params.putString(OktaSdkConstant.ERROR_MSG_KEY, OktaSdkError.NOT_CONFIGURED.getErrorMessage());
sendEvent(reactContext, OktaSdkConstant.ON_ERROR, params);
promise.reject(OktaSdkError.NOT_CONFIGURED.getErrorCode(), OktaSdkError.NOT_CONFIGURED.getErrorMessage());
return;
}

Expand All @@ -146,22 +147,31 @@ public void onSuccess(@NonNull Result result) {
if (result.isSuccess()) {
try {
SessionClient sessionClient = authClient.getSessionClient();
WritableMap params = Arguments.createMap();
Tokens tokens = sessionClient.getTokens();
String token = tokens.getAccessToken();

WritableMap params = Arguments.createMap();
params.putString(OktaSdkConstant.RESOLVE_TYPE_KEY, OktaSdkConstant.AUTHORIZED);
params.putString(OktaSdkConstant.ACCESS_TOKEN_KEY, tokens.getAccessToken());
params.putString(OktaSdkConstant.ACCESS_TOKEN_KEY, token);
sendEvent(reactContext, OktaSdkConstant.SIGN_IN_SUCCESS, params);

params = Arguments.createMap();
params.putString(OktaSdkConstant.RESOLVE_TYPE_KEY, OktaSdkConstant.AUTHORIZED);
params.putString(OktaSdkConstant.ACCESS_TOKEN_KEY, token);
promise.resolve(params);
} catch (AuthorizationException e) {
WritableMap params = Arguments.createMap();
params.putString(OktaSdkConstant.ERROR_CODE_KEY, OktaSdkError.SIGN_IN_FAILED.getErrorCode());
params.putString(OktaSdkConstant.ERROR_MSG_KEY, OktaSdkError.SIGN_IN_FAILED.getErrorMessage());
sendEvent(reactContext, OktaSdkConstant.ON_ERROR, params);
promise.reject(OktaSdkError.SIGN_IN_FAILED.getErrorCode(), OktaSdkError.SIGN_IN_FAILED.getErrorMessage());
}
} else {
WritableMap params = Arguments.createMap();
params.putString(OktaSdkConstant.ERROR_CODE_KEY, OktaSdkError.SIGN_IN_FAILED.getErrorCode());
params.putString(OktaSdkConstant.ERROR_MSG_KEY, OktaSdkError.SIGN_IN_FAILED.getErrorMessage());
sendEvent(reactContext, OktaSdkConstant.ON_ERROR, params);
promise.reject(OktaSdkError.SIGN_IN_FAILED.getErrorCode(), OktaSdkError.SIGN_IN_FAILED.getErrorMessage());
}
}

Expand All @@ -171,6 +181,7 @@ public void onError(String error, AuthorizationException exception) {
params.putString(OktaSdkConstant.ERROR_CODE_KEY, OktaSdkError.OKTA_OIDC_ERROR.getErrorCode());
params.putString(OktaSdkConstant.ERROR_MSG_KEY, error);
sendEvent(reactContext, OktaSdkConstant.ON_ERROR, params);
promise.reject(OktaSdkError.OKTA_OIDC_ERROR.getErrorCode(), OktaSdkError.OKTA_OIDC_ERROR.getErrorMessage());
}
});
}
Expand Down
71 changes: 69 additions & 2 deletions packages/okta-react-native/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,23 @@
import { NativeModules, Platform, DeviceEventEmitter, NativeEventEmitter } from 'react-native';
import { assertIssuer, assertClientId, assertRedirectUri } from '@okta/configuration-validation';
import jwt from 'jwt-lite';
import OktaAuth from '@okta/okta-auth-js';
import Url from 'url-parse';
import { version } from './package.json';

let authClient;

class OktaAuthError extends Error {
constructor(code, message, detail) {
super(message);

this.code = code;
this.detail = detail;
}
}

export const createConfig = async({
issuer,
clientId,
redirectUri,
endSessionRedirectUri,
Expand All @@ -28,6 +43,14 @@ export const createConfig = async({
assertRedirectUri(redirectUri);
assertRedirectUri(endSessionRedirectUri);

const { origin } = Url(discoveryUri);
authClient = new OktaAuth({
issuer: issuer || origin,
userAgent: {
template: `@okta/okta-react-native/${version} $OKTA_AUTH_JS react-native/${version} ${Platform.OS}/${Platform.Version}`
}
});

if (Platform.OS === 'ios') {
scopes = scopes.join(' ');
return NativeModules.OktaSdkBridge.createConfig(
Expand All @@ -49,7 +72,40 @@ export const createConfig = async({
);
}

export const signIn = async() => {
export const getAuthClient = () => {
if (!authClient) {
throw new OktaAuthError(
'-100',
'OktaOidc client isn\'t configured, check if you have created a configuration with createConfig'
);
}
return authClient;
}

export const signIn = async(options) => {
// Custom sign in
if (options && typeof options === 'object') {
return authClient.signIn(options)
.then((transaction) => {
const { status, sessionToken } = transaction;
if (status !== 'SUCCESS') {
throw new Error('Transaction status other than "SUCCESS" has been return, please handle it properly by calling "authClient.tx.resume()"');
}
return authenticate({ sessionToken });
})
.then(token => {
if (!token) {
throw new Error('Failed to get accessToken');
}

return token;
})
.catch(error => {
throw new OktaAuthError('-1000', 'Sign in was not authorized', error);
});
}

// Browser sign in
return NativeModules.OktaSdkBridge.signIn();
}

Expand All @@ -70,7 +126,18 @@ export const getIdToken = async() => {
}

export const getUser = async() => {
return NativeModules.OktaSdkBridge.getUser();
return NativeModules.OktaSdkBridge.getUser()
.then(data => {
if (typeof data === 'string') {
try {
return JSON.parse(data);
} catch (e) {
throw new OktaAuthError('-600', 'Okta Oidc error', e);
}
}

return data;
});
}

export const getUserFromIdToken = async() => {
Expand Down
12 changes: 11 additions & 1 deletion packages/okta-react-native/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ describe('OktaReactNative', () => {
});

it('gets id token successfully', async () => {
mockGetUser.mockReturnValueOnce({
mockGetUser.mockResolvedValue({
'name': 'Joe Doe',
'sub': '00uid4BxXw6I6TV4m0g3',
});
Expand All @@ -195,6 +195,16 @@ describe('OktaReactNative', () => {
'sub': '00uid4BxXw6I6TV4m0g3',
});
});

it('gets id token successfully from json string result', async () => {
mockGetUser.mockResolvedValue("{\"name\":\"Joe Doe\",\"sub\":\"00uid4BxXw6I6TV4m0g3\"}");
expect.assertions(1);
const data = await getUser();
expect(data).toEqual({
'name': 'Joe Doe',
'sub': '00uid4BxXw6I6TV4m0g3',
});
});
});

describe('getUserFromIdToken', () => {
Expand Down
11 changes: 10 additions & 1 deletion packages/okta-react-native/ios/OktaSdkBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,18 @@ class OktaSdkBridge: RCTEventEmitter {
}

@objc
func authenticate(_ sessionToken: String) {
func authenticate(_ sessionToken: String,
promiseResolver: @escaping RCTPromiseResolveBlock,
promiseRejecter: @escaping RCTPromiseRejectBlock) {
guard let _ = config, let currOktaOidc = oktaOidc else {
let error = OktaReactNativeError.notConfigured
let errorDic = [
OktaSdkConstant.ERROR_CODE_KEY: error.errorCode,
OktaSdkConstant.ERROR_MSG_KEY: error.errorDescription
]
sendEvent(withName: OktaSdkConstant.ON_ERROR, body: errorDic)
promiseRejecter(errorDic[OktaSdkConstant.ERROR_CODE_KEY]!,
errorDic[OktaSdkConstant.ERROR_MSG_KEY]!, error)
return
}

Expand All @@ -164,6 +168,8 @@ class OktaSdkBridge: RCTEventEmitter {
OktaSdkConstant.ERROR_MSG_KEY: error.localizedDescription
]
self.sendEvent(withName: OktaSdkConstant.ON_ERROR, body: errorDic)
promiseRejecter(errorDic[OktaSdkConstant.ERROR_CODE_KEY]!,
errorDic[OktaSdkConstant.ERROR_MSG_KEY]!, error)
return
}

Expand All @@ -174,6 +180,8 @@ class OktaSdkBridge: RCTEventEmitter {
OktaSdkConstant.ERROR_MSG_KEY: error.errorDescription
]
self.sendEvent(withName: OktaSdkConstant.ON_ERROR, body: errorDic)
promiseRejecter(errorDic[OktaSdkConstant.ERROR_CODE_KEY]!,
errorDic[OktaSdkConstant.ERROR_MSG_KEY]!, error)
return
}

Expand All @@ -184,6 +192,7 @@ class OktaSdkBridge: RCTEventEmitter {
]

self.sendEvent(withName: OktaSdkConstant.SIGN_IN_SUCCESS, body: dic)
promiseResolver(dic)
}
}

Expand Down
7 changes: 6 additions & 1 deletion packages/okta-react-native/ios/ReactNativeOktaSdkBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ @interface RCT_EXTERN_MODULE(OktaSdkBridge, RCTEventEmitter)

RCT_EXTERN_METHOD(signIn)

RCT_EXTERN_METHOD(authenticate:(NSString *)sessionToken)
RCT_EXTERN_METHOD(
authenticate:
(NSString *)sessionToken
promiseResolver:(RCTPromiseResolveBlock *)promiseResolver
promiseRejecter:(RCTPromiseRejectBlock *)promiseRejecter
)

RCT_EXTERN_METHOD(signOut)

Expand Down
7 changes: 5 additions & 2 deletions packages/okta-react-native/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@okta/okta-react-native",
"title": "React Native Okta Sdk Bridge",
"version": "1.3.0",
"version": "1.4.0",
"description": "Okta OIDC for React Native",
"main": "index.js",
"podname": "OktaSdkBridgeReactNative",
Expand Down Expand Up @@ -34,6 +34,7 @@
"license": "Apache-2.0",
"readmeFilename": "README.md",
"peerDependencies": {
"events": "^3.1.0",
"react": "^16.5.0",
"react-native": "^0.60.0"
},
Expand All @@ -48,7 +49,9 @@
},
"dependencies": {
"@okta/configuration-validation": "^0.3.0",
"jwt-lite": "^1.1.2"
"@okta/okta-auth-js": "3.1.0",
"jwt-lite": "^1.1.2",
"url-parse": "^1.4.7"
},
"jest": {
"automock": false,
Expand Down
Loading

0 comments on commit 9d53b35

Please sign in to comment.