Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for ephemeral sessions [SDK-1412] #305

Merged
merged 7 commits into from
May 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Widcket marked this conversation as resolved.
Show resolved Hide resolved
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) {
lbalmaceda marked this conversation as resolved.
Show resolved Hide resolved
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.
lbalmaceda marked this conversation as resolved.
Show resolved Hide resolved
* @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);
}
}