Skip to content

Commit

Permalink
Support SFSafariViewController (#800)
Browse files Browse the repository at this point in the history
Co-authored-by: Rita Zerrizuela <zeta@widcket.com>
  • Loading branch information
poovamraj and Widcket authored Nov 28, 2023
1 parent 1942482 commit 42783ac
Show file tree
Hide file tree
Showing 12 changed files with 512 additions and 52 deletions.
29 changes: 24 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,22 @@ 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.
### 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 +117,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
2 changes: 1 addition & 1 deletion android/src/main/java/com/auth0/react/A0Auth0Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public String getName() {
}

@ReactMethod
public void webAuth(String scheme, String redirectUri, String state, String nonce, String audience, String scope, String connection, int maxAge, String organization, String invitationUrl, int leeway, boolean ephemeralSession, ReadableMap additionalParameters, Promise promise) {
public void webAuth(String scheme, String redirectUri, String state, String nonce, String audience, String scope, String connection, int maxAge, String organization, String invitationUrl, int leeway, boolean ephemeralSession, int safariViewControllerPresentationStyle, ReadableMap additionalParameters, Promise promise) {
this.webAuthPromise = promise;
Map<String,String> cleanedParameters = new HashMap<>();
for (Map.Entry<String, Object> entry : additionalParameters.toHashMap().entrySet()) {
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
8 changes: 6 additions & 2 deletions ios/A0Auth0.m
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,18 @@ - (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 safariViewControllerPresentationStyle:(NSInteger)safariViewControllerPresentationStyle 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 safariViewControllerPresentationStyle:safariViewControllerPresentationStyle 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(resumeWebAuth:(NSString *)url resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
[self.nativeBridge resumeWebAuthWithUrl:url resolve:resolve reject:reject];
}

- (NSDictionary *)constantsToExport {
return @{ @"bundleIdentifier": [[NSBundle mainBundle] bundleIdentifier] };
}
Expand Down
19 changes: 16 additions & 3 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, safariViewControllerPresentationStyle: Int, 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,12 @@ public class NativeBridge: NSObject {
if(ephemeralSession) {
let _ = builder.useEphemeralSession()
}
let _ = builder.parameters(additionalParameters)
//Since we cannot have a null value here, the JS layer sends 99 if we have to ignore setting this value
if let presentationStyle = UIModalPresentationStyle(rawValue: safariViewControllerPresentationStyle), safariViewControllerPresentationStyle != 99 {
let _ = builder.provider(WebAuthentication.safariProvider(style: presentationStyle))
}
let _ = builder
.parameters(additionalParameters)
builder.start { result in
switch result {
case .success(let credentials):
Expand All @@ -84,7 +89,7 @@ public class NativeBridge: NSObject {
}

}

@objc public func webAuthLogout(federated: Bool, redirectUri: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
let builder = Auth0.webAuth(clientId: self.clientId, domain: self.domain)
if let value = URL(string: redirectUri) {
Expand All @@ -99,6 +104,14 @@ public class NativeBridge: NSObject {
}
}
}

@objc public func resumeWebAuth(url: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
if let value = URL(string: url), WebAuthentication.resume(with: value) {
resolve(true)
} 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
3 changes: 3 additions & 0 deletions src/internal-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@ export type Auth0Module = {
invitationUrl?: string,
leeway?: number,
ephemeralSession?: boolean,
safariViewControllerPresentationStyle?: number,
additionalParameters?: { [key: string]: string }
) => Promise<Credentials>;
webAuthLogout: (
scheme: string,
federated: boolean,
redirectUri: string
) => Promise<void>;
resumeWebAuth: (url: string) => Promise<void>;
saveCredentials: (credentials: Credentials) => Promise<void>;
getCredentials: (
scope?: string,
Expand Down Expand Up @@ -133,6 +135,7 @@ export interface AgentLoginOptions {
customScheme?: string;
leeway?: number;
ephemeralSession?: boolean;
safariViewControllerPresentationStyle?: number;
additionalParameters?: { [key: string]: string };
useLegacyCallbackUrl?: boolean;
}
45 changes: 45 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,32 @@ 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`. If empty object is set, the presentationStyle defaults to {@link SafariViewControllerPresentationStyle.fullScreen}
*
* This can be used as a boolean value or as an object which sets the `presentationStyle`. See the examples below for reference
*
* @example
* ```typescript
* await authorize({}, {useSFSafariViewController: true});
* ```
*
* or
*
* @example
* ```typescript
* await authorize({}, {useSFSafariViewController: {presentationStyle: SafariViewControllerPresentationStyle.fullScreen}});
* ```
*/
useSFSafariViewController?:
| {
presentationStyle?: SafariViewControllerPresentationStyle;
}
| boolean;
}

/**
Expand All @@ -133,6 +158,9 @@ 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;
}

Expand Down Expand Up @@ -541,3 +569,20 @@ export type MultifactorChallengeResponse =
| MultifactorChallengeOTPResponse
| MultifactorChallengeOOBResponse
| MultifactorChallengeOOBWithBindingResponse;

/**
* Presentation styles for when using SFSafariViewController on iOS.
* For the full description of what each option does, please see {@link https://developer.apple.com/documentation/uikit/uimodalpresentationstyle} for more details
*/
export enum SafariViewControllerPresentationStyle {
automatic = -2,
none,
fullScreen,
pageSheet,
formSheet,
currentContext,
custom,
overFullScreen,
overCurrentContext,
popover,
}
41 changes: 41 additions & 0 deletions src/webauth/__tests__/__snapshots__/webauth.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,44 @@ exports[`WebAuth authorize should authorize with provided parameters 1`] = `
"tokenType": "token type",
}
`;

exports[`WebAuth authorize should set presentation style to 0 if set as empty 1`] = `
{
"accessToken": "access token",
"idToken": "id token",
"refreshToken": "refresh token",
"scope": "scope",
"tokenType": "token type",
}
`;


exports[`WebAuth authorize should set presentation style to 0 if value is true 1`] = `
{
"accessToken": "access token",
"idToken": "id token",
"refreshToken": "refresh token",
"scope": "scope",
"tokenType": "token type",
}
`;

exports[`WebAuth authorize should set presentation style to undefined if object is undefined 1`] = `
{
"accessToken": "access token",
"idToken": "id token",
"refreshToken": "refresh token",
"scope": "scope",
"tokenType": "token type",
}
`;

exports[`WebAuth authorize should set presentation style to undefined if value is false 1`] = `
{
"accessToken": "access token",
"idToken": "id token",
"refreshToken": "refresh token",
"scope": "scope",
"tokenType": "token type",
}
`;
Loading

0 comments on commit 42783ac

Please sign in to comment.