Skip to content

Commit

Permalink
Added support for ephemeral sessions [SDK-1412] (#305)
Browse files Browse the repository at this point in the history
* Add support for ephemeral sessions

* Fix test

* Add unit tests

* Update src/webauth/index.js

Co-authored-by: Luciano Balmaceda <balmacedaluciano@gmail.com>

* Update README.md

* Update src/webauth/agent.js

Co-authored-by: Luciano Balmaceda <balmacedaluciano@gmail.com>

* Add unit test

Co-authored-by: Luciano Balmaceda <balmacedaluciano@gmail.com>
  • Loading branch information
Widcket and lbalmaceda authored May 22, 2020
1 parent b84b911 commit a24bb31
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 76 deletions.
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ The contents of previous release can be found on the [branch v1](https://github.

First install the native library module:

Using [npm](https://www.npmjs.com)
### With [npm](https://www.npmjs.com)

`$ npm install react-native-auth0 --save`

or [yarn](https://yarnpkg.com/en/)
### With [Yarn](https://yarnpkg.com/en/)

`$ yarn add react-native-auth0`

Then, you need to run the following command to install the ios app pods with Cocoapods. That will auto-link the iOS library.
Then, you need to run the following command to install the ios app pods with Cocoapods. That will auto-link the iOS library:

`$ cd ios && pod install`

Expand Down Expand Up @@ -105,11 +105,11 @@ android:windowSoftInputMode="adjustResize">

The `applicationId` value will be auto-replaced on runtime with the package name or id of your application (e.g. `com.example.app`). You can change this value from the `build.gradle` file. You can also check it at the top of your `AndroidManifest.xml` file. Take note of this value as you'll be requiring it to define the callback URLs below.

> For more info please read [react native docs](https://facebook.github.io/react-native/docs/linking.html)
> For more info please read the [React Native docs](https://facebook.github.io/react-native/docs/linking.html).
#### iOS

Inside the `ios` folder find the file `AppDelegate.[swift|m]` add the following to it
Inside the `ios` folder find the file `AppDelegate.[swift|m]` add the following to it:

```objc
#import <React/RCTLinkingManager.h>
Expand All @@ -129,7 +129,7 @@ Inside the `ios` folder open the `Info.plist` and locate the value for `CFBundle
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
```

and then below it register a URL type entry using the value of `CFBundleIdentifier` as the value for `CFBundleURLSchemes`
and then below it register a URL type entry using the value of `CFBundleIdentifier` as the value for `CFBundleURLSchemes`:

```xml
<key>CFBundleURLTypes</key>
Expand All @@ -154,7 +154,7 @@ If your application is generated using the React Native CLI, the default value o
- Replace the **Product Bundle Identifier** value with your desired application's bundle identifier name (e.g. `com.example.app`).
- If you've changed the project wide settings, make sure the same were applied to each of the targets your app has.

> For more info please read [react native docs](https://facebook.github.io/react-native/docs/linking.html)
> For more info please read the [React Native docs](https://facebook.github.io/react-native/docs/linking.html).
### Callback URL(s)

Expand Down Expand Up @@ -199,7 +199,7 @@ const auth0 = new Auth0({
### Web Authentication

#### Log in
#### Login

```js
auth0.webAuth
Expand All @@ -208,7 +208,18 @@ auth0.webAuth
.catch(error => console.log(error));
```

#### Log out
##### Disable Single Sign On (iOS 13+ only)

Use the `ephemeralSession` parameter to disable SSO on iOS 13+. This way iOS will not display the consent popup that otherwise shows up when SSO is enabled. It has no effect on older versions of iOS or Android.

```js
auth0.webAuth
.authorize({scope: 'openid email profile'}, {ephemeralSession: true})
.then(credentials => console.log(credentials))
.catch(error => console.log(error));
```

#### Logout

```js
auth0.webAuth.clearSession().catch(error => console.log(error));
Expand Down
10 changes: 7 additions & 3 deletions ios/A0Auth0.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,14 @@ - (dispatch_queue_t)methodQueue
[self terminateWithError:nil dismissing:YES animated:YES];
}

RCT_EXPORT_METHOD(showUrl:(NSString *)urlString closeOnLoad:(BOOL)closeOnLoad callback:(RCTResponseSenderBlock)callback) {
RCT_EXPORT_METHOD(showUrl:(NSString *)urlString
usingEphemeralSession:(BOOL)ephemeralSession
closeOnLoad:(BOOL)closeOnLoad
callback:(RCTResponseSenderBlock)callback) {
if (@available(iOS 11.0, *)) {
self.sessionCallback = callback;
self.closeOnLoad = closeOnLoad;
[self presentAuthenticationSession:[NSURL URLWithString:urlString]];
[self presentAuthenticationSession:[NSURL URLWithString:urlString] usingEphemeralSession:ephemeralSession];
} else {
[self presentSafariWithURL:[NSURL URLWithString:urlString]];
self.sessionCallback = callback;
Expand Down Expand Up @@ -80,7 +83,7 @@ - (void)presentSafariWithURL:(NSURL *)url {
self.last = controller;
}

- (void)presentAuthenticationSession:(NSURL *)url {
- (void)presentAuthenticationSession:(NSURL *)url usingEphemeralSession:(BOOL)ephemeralSession {

NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url
resolvingAgainstBaseURL:NO];
Expand Down Expand Up @@ -116,6 +119,7 @@ - (void)presentAuthenticationSession:(NSURL *)url {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
authenticationSession.presentationContextProvider = self;
authenticationSession.prefersEphemeralWebBrowserSession = ephemeralSession;
}
#endif
self.authenticationSession = authenticationSession;
Expand Down
14 changes: 12 additions & 2 deletions src/webauth/__mocks__/auth0.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
export default class A0Auth0 {
showUrl(url, closeOnLoad, callback) {
this.url = url;
showUrl(...args) {
let closeOnLoad;
let callback;
this.url = args[0];
if (args.length == 3) {
closeOnLoad = args[1];
callback = args[2];
} else {
this.ephemeralSession = args[1];
closeOnLoad = args[2];
callback = args[3];
}
this.hidden = false;
if (this.error || closeOnLoad) {
callback(this.error);
Expand Down
35 changes: 29 additions & 6 deletions src/webauth/__tests__/agent.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
jest.mock('react-native');
import Agent from '../agent';
import { NativeModules, Linking } from 'react-native';
import {NativeModules, Linking, Platform} from 'react-native';
const A0Auth0 = NativeModules.A0Auth0;

describe('Agent', () => {
Expand All @@ -21,14 +21,14 @@ describe('Agent', () => {
describe('complete web flow', () => {
beforeEach(() => {
A0Auth0.onUrl = () => {
Linking.emitter.emit('url', { url: 'https://auth0.com' });
Linking.emitter.emit('url', {url: 'https://auth0.com'});
};
});

it('should resolve promise with url result', async () => {
expect.assertions(1);
await expect(
agent.show('https://auth0.com')
agent.show('https://auth0.com'),
).resolves.toMatchSnapshot();
});

Expand All @@ -38,6 +38,29 @@ describe('Agent', () => {
await agent.show(url);
expect(A0Auth0.url).toEqual(url);
});

it('should not pass ephemeral session parameter', async () => {
expect.assertions(1);
const url = 'https://auth0.com';
await agent.show(url);
expect(A0Auth0.ephemeralSession).toBeUndefined();
});

it('should not use ephemeral session by default', async () => {
Platform.OS = 'ios';
expect.assertions(1);
const url = 'https://auth0.com';
await agent.show(url);
expect(A0Auth0.ephemeralSession).toEqual(false);
});

it('should set ephemeral session', async () => {
Platform.OS = 'ios';
expect.assertions(1);
const url = 'https://auth0.com';
await agent.show(url, true);
expect(A0Auth0.ephemeralSession).toEqual(true);
});
});

describe('listeners', () => {
Expand All @@ -50,7 +73,7 @@ describe('Agent', () => {

it('should remove url listeners when done', async () => {
A0Auth0.onUrl = () => {
Linking.emitter.emit('url', { url: 'https://auth0.com' });
Linking.emitter.emit('url', {url: 'https://auth0.com'});
};
expect.assertions(1);
const url = 'https://auth0.com/authorize';
Expand All @@ -69,7 +92,7 @@ describe('Agent', () => {
it('should remove url listeners on first load', async () => {
expect.assertions(1);
const url = 'https://auth0.com/authorize';
await agent.show(url, true);
await agent.show(url, false, true);
expect(Linking.emitter.listenerCount('url')).toEqual(0);
});
});
Expand All @@ -85,7 +108,7 @@ describe('Agent', () => {

describe('newTransaction', () => {
it('should call native integration', async () => {
const parameters = { state: 'state' };
const parameters = {state: 'state'};
A0Auth0.parameters = parameters;
expect.assertions(1);
await expect(agent.newTransaction()).resolves.toMatchSnapshot();
Expand Down
18 changes: 10 additions & 8 deletions src/webauth/agent.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { NativeModules, Linking } from 'react-native';
import {NativeModules, Linking, Platform} from 'react-native';

export default class Agent {
show(url, closeOnLoad = false) {
show(url, ephemeralSession = false, closeOnLoad = false) {
if (!NativeModules.A0Auth0) {
return Promise.reject(
new Error(
'Missing NativeModule. React Native versions 0.60 and up perform auto-linking. Please see https://github.com/react-native-community/cli/blob/master/docs/autolinking.md.'
)
),
);
}

Expand All @@ -16,14 +16,16 @@ export default class Agent {
Linking.removeEventListener('url', urlHandler);
resolve(event.url);
};
const params =
Platform.OS === 'ios' ? [ephemeralSession, closeOnLoad] : [closeOnLoad];
Linking.addEventListener('url', urlHandler);
NativeModules.A0Auth0.showUrl(url, closeOnLoad, (error, redirectURL) => {
NativeModules.A0Auth0.showUrl(url, ...params, (error, redirectURL) => {
Linking.removeEventListener('url', urlHandler);
if (error) {
reject(error);
} else if(redirectURL) {
} else if (redirectURL) {
resolve(redirectURL);
} else if(closeOnLoad) {
} else if (closeOnLoad) {
resolve();
}
});
Expand All @@ -34,8 +36,8 @@ export default class Agent {
if (!NativeModules.A0Auth0) {
return Promise.reject(
new Error(
'Missing NativeModule. React Native versions 0.60 and up perform auto-linking. Please see https://github.com/react-native-community/cli/blob/master/docs/autolinking.md.'
)
'Missing NativeModule. React Native versions 0.60 and up perform auto-linking. Please see https://github.com/react-native-community/cli/blob/master/docs/autolinking.md.',
),
);
}

Expand Down
99 changes: 51 additions & 48 deletions src/webauth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ export default class WebAuth {
* To learn more about how to customize the authorize call, check the Universal Login Page
* article at https://auth0.com/docs/hosted-pages/login
*
* @param {Object} parameters parameters to send on the AuthN/AuthZ request.
* @param {String} [parameters.state] random string to prevent CSRF attacks and used to discard unexepcted results. By default its a cryptographically secure random.
* @param {String} [parameters.nonce] random string to prevent replay attacks of id_tokens.
* @param {String} [parameters.audience] identifier of Resource Server (RS) to be included as audience (aud claim) of the issued access token
* @param {String} [parameters.scope] scopes requested for the issued tokens. e.g. `openid profile`
* @param {String} [parameters.connection] The name of the identity provider to use, e.g. "google-oauth2" or "facebook". When not set, it will display Auth0's Universal Login Page.
* @param {Number} [parameters.max_age] The allowable elapsed time in seconds since the last time the user was authenticated (optional).
* @param {Object} options options for ID token validation configuration.
* @param {Number} [options.leeway] The amount of leeway, in seconds, to accommodate potential clock skew when validating an ID token's claims. Defaults to 60 seconds if not specified.
* @param {Object} parameters Parameters to send on the AuthN/AuthZ request.
* @param {String} [parameters.state] Random string to prevent CSRF attacks and used to discard unexepcted results. By default its a cryptographically secure random.
* @param {String} [parameters.nonce] Random string to prevent replay attacks of id_tokens.
* @param {String} [parameters.audience] Identifier of Resource Server (RS) to be included as audience (aud claim) of the issued access token
* @param {String} [parameters.scope] Scopes requested for the issued tokens. e.g. `openid profile`
* @param {String} [parameters.connection] The name of the identity provider to use, e.g. "google-oauth2" or "facebook". When not set, it will display Auth0's Universal Login Page.
* @param {Number} [parameters.max_age] The allowable elapsed time in seconds since the last time the user was authenticated (optional).
* @param {Object} options Other configuration options.
* @param {Number} [options.leeway] The amount of leeway, in seconds, to accommodate potential clock skew when validating an ID token's claims. Defaults to 60 seconds if not specified.
* @param {Boolean} [options.ephemeralSession] Disable Single-Sign-On (SSO). It only affects iOS with versions 13 and above.
* @returns {Promise}
* @see https://auth0.com/docs/api/authentication#authorize-client
*
Expand All @@ -69,44 +70,46 @@ export default class WebAuth {
...parameters,
};
const authorizeUrl = this.client.authorizeUrl(query);
return agent.show(authorizeUrl).then(redirectUrl => {
if (!redirectUrl || !redirectUrl.startsWith(redirectUri)) {
throw new AuthError({
json: {
error: 'a0.redirect_uri.not_expected',
error_description: `Expected ${redirectUri} but got ${redirectUrl}`,
},
status: 0,
});
}
const query = url.parse(redirectUrl, true).query;
const {code, state: resultState, error} = query;
if (error) {
throw new AuthError({json: query, status: 0});
}
if (resultState !== expectedState) {
throw new AuthError({
json: {
error: 'a0.state.invalid',
error_description: `Invalid state received in redirect url`,
},
status: 0,
});
}
return agent
.show(authorizeUrl, options.ephemeralSession)
.then(redirectUrl => {
if (!redirectUrl || !redirectUrl.startsWith(redirectUri)) {
throw new AuthError({
json: {
error: 'a0.redirect_uri.not_expected',
error_description: `Expected ${redirectUri} but got ${redirectUrl}`,
},
status: 0,
});
}
const query = url.parse(redirectUrl, true).query;
const {code, state: resultState, error} = query;
if (error) {
throw new AuthError({json: query, status: 0});
}
if (resultState !== expectedState) {
throw new AuthError({
json: {
error: 'a0.state.invalid',
error_description: `Invalid state received in redirect url`,
},
status: 0,
});
}

return client
.exchange({code, verifier, redirectUri})
.then(credentials => {
return verifyToken(credentials.idToken, {
domain,
clientId,
nonce: parameters.nonce,
maxAge: parameters.max_age,
scope: parameters.scope,
leeway: options.leeway,
}).then(() => Promise.resolve(credentials));
});
});
return client
.exchange({code, verifier, redirectUri})
.then(credentials => {
return verifyToken(credentials.idToken, {
domain,
clientId,
nonce: parameters.nonce,
maxAge: parameters.max_age,
scope: parameters.scope,
leeway: options.leeway,
}).then(() => Promise.resolve(credentials));
});
});
});
}

Expand All @@ -115,7 +118,7 @@ export default class WebAuth {
*
* In iOS it will use `SFSafariViewController` and in Android Chrome Custom Tabs.
*
* @param {Object} parameters parameters to send
* @param {Object} parameters Parameters to send
* @param {Bool} [parameters.federated] Optionally remove the IdP session.
* @returns {Promise}
* @see https://auth0.com/docs/logout
Expand All @@ -128,6 +131,6 @@ export default class WebAuth {
options.returnTo = callbackUri(domain);
options.federated = options.federated || false;
const logoutUrl = client.logoutUrl(options);
return agent.show(logoutUrl, true);
return agent.show(logoutUrl, false, true);
}
}

0 comments on commit a24bb31

Please sign in to comment.