Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f5d51ba
Support SFSafariViewController
poovamraj Nov 10, 2023
fd55191
Move try block below
poovamraj Nov 10, 2023
20b1d25
Merge branch 'master' into support-sfsafariviewcontroller
poovamraj Nov 10, 2023
83fda69
Merge branch 'master' into support-sfsafariviewcontroller
poovamraj Nov 13, 2023
73fa655
Fixed failing tests
poovamraj Nov 14, 2023
bed6bda
Update src/types.ts
poovamraj Nov 15, 2023
abe4ec4
Add equal parameters in Android
poovamraj Nov 19, 2023
13d0b02
Call remove only if `linkSubscription` exists
poovamraj Nov 19, 2023
ef66887
Add tests to check when SFSafariViewController is enabled
poovamraj Nov 20, 2023
24f2ebe
Update FAQ.md
poovamraj Nov 20, 2023
cccd117
Simplify resumeWebAuth native code
poovamraj Nov 20, 2023
5dc4f87
Update packageManager version
poovamraj Nov 20, 2023
91ed971
Remove `useSFSafariViewController` for logout
poovamraj Nov 20, 2023
e188c5c
Merge branch 'master' into support-sfsafariviewcontroller
poovamraj Nov 20, 2023
65dfe3a
Run tests with corepack enabled
poovamraj Nov 20, 2023
888b3df
Remove package manager tag from package.json
poovamraj Nov 21, 2023
f587412
Remove link subscription from logout
poovamraj Nov 21, 2023
ce73ef0
Add more unit tests
poovamraj Nov 21, 2023
24b56de
Merge branch 'master' into support-sfsafariviewcontroller
Widcket Nov 21, 2023
217d4be
Update FAQ.md
poovamraj Nov 21, 2023
be61ae3
Merge branch 'master' into support-sfsafariviewcontroller
poovamraj Nov 27, 2023
0c793f6
Implement SafariViewControllerPresentationStyle
poovamraj Nov 27, 2023
9c49f8a
Add tests to implement safariviewcontroller presenter style
poovamraj Nov 27, 2023
6f5e73c
Update documentation
poovamraj Nov 27, 2023
4f6a8e1
Implement review feedback
poovamraj Nov 28, 2023
511dc7c
Fix unit test case
poovamraj Nov 28, 2023
4da4f57
Add another unit test to test success case
poovamraj Nov 28, 2023
5170b3e
coalesce promises together
poovamraj Nov 28, 2023
2a85c9f
Support boolean values for useSFSafariViewController
poovamraj Nov 28, 2023
5c373f0
Update documentation
poovamraj Nov 28, 2023
07d45eb
Update documentation
poovamraj Nov 28, 2023
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
Next Next commit
Support SFSafariViewController
  • Loading branch information
poovamraj committed Nov 10, 2023
commit f5d51baf8032046697623a7252a79cace8480b6e
31 changes: 26 additions & 5 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,14 @@ Alternatively, you can re-declare the `RedirectActivity` in the `AndroidManifest

![ios-sso-alert](assets/ios-sso-alert.png)

Under the hood, react-native-auth0 uses `ASWebAuthenticationSession` to perform web-based authentication on iOS 12+, which is the [API provided by Apple](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) for such purpose.
Under the hood, react-native-auth0 uses `ASWebAuthenticationSession` by default to perform web-based authentication, which is the [API provided by Apple](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) for such purpose.

That alert box is displayed and managed by `ASWebAuthenticationSession`, not by react-native-auth0, because by default this API will store the session cookie in the shared Safari cookie jar. This makes Single Sign-On (SSO) possible. According to Apple, that requires user consent.
That alert box is displayed and managed by `ASWebAuthenticationSession`, not by react-native-auth0, because by default this API will store the session cookie in the shared Safari cookie jar. This makes single sign-on (SSO) possible. According to Apple, that requires user consent.

> :bulb: See [this blog post](https://developer.okta.com/blog/2022/01/13/mobile-sso) for a detailed overview of SSO on iOS.
> **Note**
> See [this blog post](https://developer.okta.com/blog/2022/01/13/mobile-sso) for a detailed overview of SSO on iOS.

### Use ephemeral sessions

If you don't need SSO, you can disable this behavior by adding `ephemeralSession: true` to the login call. This will configure `ASWebAuthenticationSession` to not store the session cookie in the shared cookie jar, as if using an incognito browser window. With no shared cookie, `ASWebAuthenticationSession` will not prompt the user for consent.

Expand All @@ -77,7 +80,24 @@ Note that with `ephemeralSession: true` you don't need to call `clearSession` at

You still need to call `clearSession` on Android, though, as `ephemeralSession` is iOS-only.

> :bulb: `ephemeralSession` relies on the `prefersEphemeralWebBrowserSession` configuration option of `ASWebAuthenticationSession`. This option is only available on [iOS 13+](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/3237231-prefersephemeralwebbrowsersessio), so `ephemeralSession` will have no effect on older iOS versions. To improve the experience for users on older iOS versions, see the approach described below.
> **Note** > `ephemeralSession: true` relies on the `prefersEphemeralWebBrowserSession` configuration option of `ASWebAuthenticationSession`. This option is only available on [iOS 13+ and macOS](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/3237231-prefersephemeralwebbrowsersessio), so `ephemeralSession: true` will have no effect on iOS 12. To improve the experience for iOS 12 users, see the approach described below.

### Use `SFSafariViewController`

An alternative is to use `SFSafariViewController` instead of `ASWebAuthenticationSession`. You can do so with the built-in `SFSafariViewController` Web Auth provider:

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

> **Note**
> Since `SFSafariViewController` does not share cookies with the Safari app, SSO will not work either. But it will keep its own cookies, so you can use it to perform SSO between your app and your website as long as you open it inside your app using `SFSafariViewController`. This also means that any feature that relies on the persistence of cookies will work as expected.

## 3. How can I disable the iOS _logout_ alert box?

Expand All @@ -99,7 +119,8 @@ auth0.webAuth

Otherwise, the browser modal will close right away and the user will be automatically logged in again, as the cookie will still be there.

> :warning: Keeping the shared session cookie may not be an option if you have strong privacy and/or security requirements, for example in the case of a banking app.
> **Warning**
> Keeping the shared session cookie may not be an option if you have strong privacy and/or security requirements, for example in the case of a banking app.

## 4. Is there a way to disable the iOS _login_ alert box without `ephemeralSession`?

Expand Down
7 changes: 7 additions & 0 deletions example/ios/Auth0Example/AppDelegate.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>

@implementation AppDelegate

Expand All @@ -14,6 +15,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options
{
return [RCTLinkingManager application:app openURL:url options:options];
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
Expand Down
12 changes: 8 additions & 4 deletions ios/A0Auth0.m
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,16 @@ - (dispatch_queue_t)methodQueue
[self.nativeBridge enableLocalAuthenticationWithTitle:title cancelTitle:cancelTitle fallbackTitle:fallbackTitle evaluationPolicy: evaluationPolicy];
}

RCT_EXPORT_METHOD(webAuth:(NSString *)scheme redirectUri:(NSString *)redirectUri state:(NSString *)state nonce:(NSString *)nonce audience:(NSString *)audience scope:(NSString *)scope connection:(NSString *)connection maxAge:(NSInteger)maxAge organization:(NSString *)organization invitationUrl:(NSString *)invitationUrl leeway:(NSInteger)leeway ephemeralSession:(BOOL)ephemeralSession additionalParameters:(NSDictionary *)additionalParameters resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
[self.nativeBridge webAuthWithState:state redirectUri:redirectUri nonce:nonce audience:audience scope:scope connection:connection maxAge:maxAge organization:organization invitationUrl:invitationUrl leeway:leeway ephemeralSession:ephemeralSession additionalParameters:additionalParameters resolve:resolve reject:reject];
RCT_EXPORT_METHOD(webAuth:(NSString *)scheme redirectUri:(NSString *)redirectUri state:(NSString *)state nonce:(NSString *)nonce audience:(NSString *)audience scope:(NSString *)scope connection:(NSString *)connection maxAge:(NSInteger)maxAge organization:(NSString *)organization invitationUrl:(NSString *)invitationUrl leeway:(NSInteger)leeway ephemeralSession:(BOOL)ephemeralSession useSFSafariViewController:(BOOL)useSFSafariViewController additionalParameters:(NSDictionary *)additionalParameters resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
[self.nativeBridge webAuthWithState:state redirectUri:redirectUri nonce:nonce audience:audience scope:scope connection:connection maxAge:maxAge organization:organization invitationUrl:invitationUrl leeway:leeway ephemeralSession:ephemeralSession useSFSafariViewController:useSFSafariViewController additionalParameters:additionalParameters resolve:resolve reject:reject];
}

RCT_EXPORT_METHOD(webAuthLogout:(NSString *)scheme federated:(BOOL)federated redirectUri:(NSString *)redirectUri resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
[self.nativeBridge webAuthLogoutWithFederated:federated redirectUri:redirectUri resolve:resolve reject:reject];
RCT_EXPORT_METHOD(webAuthLogout:(NSString *)scheme federated:(BOOL)federated redirectUri:(NSString *)redirectUri useSFSafariViewController:(BOOL)useSFSafariViewController resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
[self.nativeBridge webAuthLogoutWithFederated:federated redirectUri:redirectUri useSFSafariViewController:useSFSafariViewController resolve:resolve reject:reject];
}

RCT_EXPORT_METHOD(resumeWebAuth:(NSString *)url resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
[self.nativeBridge resumeWebAuthWithUrl:url resolve:resolve reject:reject];
}

- (NSDictionary *)constantsToExport {
Expand Down
25 changes: 21 additions & 4 deletions ios/NativeBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class NativeBridge: NSObject {
super.init()
}

@objc public func webAuth(state: String?, redirectUri: String, nonce: String?, audience: String?, scope: String?, connection: String?, maxAge: Int, organization: String?, invitationUrl: String?, leeway: Int, ephemeralSession: Bool, additionalParameters: [String: String], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
@objc public func webAuth(state: String?, redirectUri: String, nonce: String?, audience: String?, scope: String?, connection: String?, maxAge: Int, organization: String?, invitationUrl: String?, leeway: Int, ephemeralSession: Bool, useSFSafariViewController: Bool, additionalParameters: [String: String], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
let builder = Auth0.webAuth(clientId: self.clientId, domain: self.domain)
if let value = URL(string: redirectUri) {
let _ = builder.redirectURL(value)
Expand Down Expand Up @@ -73,7 +73,11 @@ public class NativeBridge: NSObject {
if(ephemeralSession) {
let _ = builder.useEphemeralSession()
}
let _ = builder.parameters(additionalParameters)
if(useSFSafariViewController) {
let _ = builder.provider(WebAuthentication.safariProvider())
}
let _ = builder
.parameters(additionalParameters)
builder.start { result in
switch result {
case .success(let credentials):
Expand All @@ -84,12 +88,15 @@ public class NativeBridge: NSObject {
}

}
@objc public func webAuthLogout(federated: Bool, redirectUri: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {

@objc public func webAuthLogout(federated: Bool, redirectUri: String, useSFSafariViewController: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
let builder = Auth0.webAuth(clientId: self.clientId, domain: self.domain)
if let value = URL(string: redirectUri) {
let _ = builder.redirectURL(value)
}
if(useSFSafariViewController) {
let _ = builder.provider(WebAuthentication.safariProvider())
}
builder.clearSession(federated: federated) { result in
switch result {
case .success:
Expand All @@ -99,6 +106,16 @@ public class NativeBridge: NSObject {
}
}
}

@objc public func resumeWebAuth(url: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
if let value = URL(string: url) {
if (!WebAuthentication.resume(with: value)) {
reject("ERROR_PARSING_URL", "The callback url \(url) is invalid", nil)
}
} else {
reject("ERROR_PARSING_URL", "The callback url \(url) is invalid", nil)
}
}

@objc public func saveCredentials(credentialsDict: [String: Any], resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {

Expand Down
7 changes: 6 additions & 1 deletion src/internal-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,16 @@ export type Auth0Module = {
invitationUrl?: string,
leeway?: number,
ephemeralSession?: boolean,
useSFSafariViewController?: boolean,
additionalParameters?: { [key: string]: string }
) => Promise<Credentials>;
webAuthLogout: (
scheme: string,
federated: boolean,
redirectUri: string
redirectUri: string,
useSFSafariViewController?: boolean
) => Promise<void>;
resumeWebAuth: (url: string) => Promise<void>;
saveCredentials: (credentials: Credentials) => Promise<void>;
getCredentials: (
scope?: string,
Expand Down Expand Up @@ -118,6 +121,7 @@ export type AgentParameters = {
export type AgentLogoutOptions = {
customScheme?: string;
federated?: boolean;
useSFSafariViewController?: boolean;
useLegacyCallbackUrl?: boolean;
};

Expand All @@ -133,6 +137,7 @@ export interface AgentLoginOptions {
customScheme?: string;
leeway?: number;
ephemeralSession?: boolean;
useSFSafariViewController?: boolean;
additionalParameters?: { [key: string]: string };
useLegacyCallbackUrl?: boolean;
}
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,14 @@ export interface WebAuthorizeOptions {
* **Android only:** Custom scheme to build the callback URL with.
*/
customScheme?: string;
/**
* This will use older callback URL. See {@link https://github.com/auth0/react-native-auth0/blob/master/MIGRATION_GUIDE.md#callback-url-migration} for more details.
*/
useLegacyCallbackUrl?: boolean;
/**
* **iOS only:** Uses SFSafariViewController instead of ASWebAuthenticationSession.
*/
useSFSafariViewController?: boolean;
}

/**
Expand All @@ -133,7 +140,14 @@ export interface ClearSessionOptions {
* **Android only:** Custom scheme to build the callback URL with.
*/
customScheme?: string;
/**
* This will use older callback URL. See {@link https://github.com/auth0/react-native-auth0/blob/master/MIGRATION_GUIDE.md#callback-url-migration} for more details.
*/
useLegacyCallbackUrl?: boolean;
/**
* **iOS only:** Uses SFSafariViewController instead of ASWebAuthenticationSession.
*/
useSFSafariViewController?: boolean;
}

export interface GetUserOptions {
Expand Down
125 changes: 87 additions & 38 deletions src/webauth/agent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { NativeModules, Platform } from 'react-native';
import {
NativeModules,
Platform,
Linking,
EmitterSubscription,
} from 'react-native';
import { Credentials } from 'src/types';
import { _ensureNativeModuleIsInitialized } from '../utils/nativeHelper';
import {
Expand All @@ -14,64 +19,108 @@ class Agent {
parameters: AgentParameters,
options: AgentLoginOptions
): Promise<Credentials> {
let linkSubscription: EmitterSubscription;
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.'
)
);
}
await _ensureNativeModuleIsInitialized(
A0Auth0,
parameters.clientId,
parameters.domain
);
let scheme = this.getScheme(
options.useLegacyCallbackUrl ?? false,
options.customScheme
);
let redirectUri = this.callbackUri(parameters.domain, scheme);
return A0Auth0.webAuth(
scheme,
redirectUri,
options.state,
options.nonce,
options.audience,
options.scope,
options.connection,
options.maxAge ?? 0,
options.organization,
options.invitationUrl,
options.leeway ?? 0,
options.ephemeralSession ?? false,
options.additionalParameters ?? {}
);
return new Promise(async (resolve, reject) => {
try {
if (Platform.OS === 'ios' && options.useSFSafariViewController) {
linkSubscription = Linking.addEventListener('url', async (event) => {
linkSubscription.remove();
try {
await A0Auth0.resumeWebAuth(event.url);
} catch (error) {
reject(error);
}
});
}
await _ensureNativeModuleIsInitialized(
A0Auth0,
parameters.clientId,
parameters.domain
);
let scheme = this.getScheme(
options.useLegacyCallbackUrl ?? false,
options.customScheme
);
let redirectUri = this.callbackUri(parameters.domain, scheme);
let credentials = await A0Auth0.webAuth(
scheme,
redirectUri,
options.state,
options.nonce,
options.audience,
options.scope,
options.connection,
options.maxAge ?? 0,
options.organization,
options.invitationUrl,
options.leeway ?? 0,
options.ephemeralSession ?? false,
options.useSFSafariViewController ?? false,
options.additionalParameters ?? {}
);
resolve(credentials);
} catch (error) {
linkSubscription.remove();
reject(error);
}
});
}

async logout(
parameters: AgentParameters,
options: AgentLogoutOptions
): Promise<void> {
let linkSubscription: EmitterSubscription;
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.'
)
);
}
let federated = options.federated ?? false;
let scheme = this.getScheme(
options.useLegacyCallbackUrl ?? false,
options.customScheme
);
let redirectUri = this.callbackUri(parameters.domain, scheme);
await _ensureNativeModuleIsInitialized(
NativeModules.A0Auth0,
parameters.clientId,
parameters.domain
);
return new Promise(async (resolve, reject) => {
if (Platform.OS === 'ios' && options.useSFSafariViewController) {
linkSubscription = Linking.addEventListener('url', async (event) => {
linkSubscription.remove();
try {
await A0Auth0.resumeWebAuth(event.url);
} catch (error) {
reject(error);
}
});
}
try {
let federated = options.federated ?? false;
let scheme = this.getScheme(
options.useLegacyCallbackUrl ?? false,
options.customScheme
);
let redirectUri = this.callbackUri(parameters.domain, scheme);
await _ensureNativeModuleIsInitialized(
NativeModules.A0Auth0,
parameters.clientId,
parameters.domain
);

return A0Auth0.webAuthLogout(scheme, federated, redirectUri);
resolve(
await A0Auth0.webAuthLogout(
scheme,
federated,
redirectUri,
options.useSFSafariViewController ?? false
)
);
} catch (error) {
reject(error);
}
});
}

private getScheme(
Expand Down
6 changes: 0 additions & 6 deletions src/webauth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ class WebAuth {
/**
* Starts the AuthN/AuthZ transaction against the AS in the in-app browser.
*
* In iOS <11 it will use `SFSafariViewController`, in iOS 11 `SFAuthenticationSession` and in iOS >11 `ASWebAuthenticationSession`.
* In Android it will use Chrome Custom Tabs.
*
* To learn more about how to customize the authorize call, check the Universal Login Page
* article at https://auth0.com/docs/hosted-pages/login
*
Expand All @@ -56,9 +53,6 @@ class WebAuth {
/**
* Removes Auth0 session and optionally remove the Identity Provider session.
*
* In iOS <11 it will use `SFSafariViewController`, in iOS 11 `SFAuthenticationSession` and in iOS >11 `ASWebAuthenticationSession`.
* In Android it will use Chrome Custom Tabs.
*
* @see https://auth0.com/docs/logout
*/
clearSession(
Expand Down