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

Support SFSafariViewController #800

Merged
merged 31 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
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 {
}

}

poovamraj marked this conversation as resolved.
Show resolved Hide resolved
@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