Skip to content

Commit

Permalink
feat: added support for domain switching (#931)
Browse files Browse the repository at this point in the history
  • Loading branch information
desusai7 authored Aug 2, 2024
1 parent 0bf7a6b commit 1aa5e70
Show file tree
Hide file tree
Showing 21 changed files with 1,165 additions and 486 deletions.
95 changes: 95 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- [Log in to an organization](#log-in-to-an-organization)
- [Accept user invitations](#accept-user-invitations)
- [Bot Protection](#bot-protection)
- [Domain Switching](#domain-switching)

## Authentication API

Expand Down Expand Up @@ -263,3 +264,97 @@ auth0.webAuth.authorize({
screen_hint: 'signup', // 👈🏻
});
```

### Domain Switching

To switch between two different domains for authentication in your Android and iOS applications, follow these steps:

#### Android

To switch between two different domains for authentication in your Android application, you need to manually update your `AndroidManifest.xml` file. This involves adding an intent filter for the activity `com.auth0.android.provider.RedirectActivity`. Unlike using a single domain where you can add the domain and scheme values within the `manifestPlaceholders` of your app's `build.gradle` file, you need to add a `<data>` tag for each domain along with its scheme within the intent filter.

Here is an example:

```xml
<activity
android:name="com.auth0.android.provider.RedirectActivity"
tools:node="replace"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${domain1}"
android:pathPrefix="/android/${applicationId}/callback"
android:scheme="${applicationId}.auth0" />
<data
android:host="${domain2}"
android:pathPrefix="/android/${applicationId}/callback"
android:scheme="${applicationId}.auth0" />
</intent-filter>
</activity>
```

If you customize the scheme by removing the default value of `${applicationId}.auth0`, you will also need to pass it as the `customScheme` option parameter of the `authorize` and `clearSession` methods.

#### iOS

For iOS, if you are not customizing the scheme, adding `$(PRODUCT_BUNDLE_IDENTIFIER).auth0` as an entry to the `CFBundleURLSchemes` array in your `Info.plist` file should be sufficient. However, if you want to customize the scheme for the domains, you need to add the customized scheme for each domain as an entry to the `CFBundleURLSchemes` array.

Here is an example:

```
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>None</string>
<key>CFBundleURLName</key>
<string>auth0</string>
<key>CFBundleURLSchemes</key>
<array>
<string>$(customScheme1)</string>
<string>$(customScheme2)</string>
</array>
</dict>
</array>
```

By following these steps, you can configure your Android and iOS applications to handle authentication for multiple domains.

#### Expo

If using a single domain, you can simply pass an object in the format to the `react-native-auth0` plugin in your `app.json` as shown below:

```json
"plugins": [
"expo-router",
["react-native-auth0",
{
"domain": "sample.auth0.com",
"customScheme": "sampleScheme"
}
]
]
```

If you want to support multiple domains, you would have to pass an array of objects as shown below:

```json
"plugins": [
"expo-router",
["react-native-auth0",
[{
"domain": "sample.auth0.com",
"customScheme": "sampleScheme"
},
{
"domain": "sample2.auth0.com",
"customScheme": "sampleScheme2"
}]
]
]
```

You can skip sending the `customScheme` property if you do not want to customize it.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ To use the SDK with Expo, configure the app at build time by providing the `doma
}
```

> :info: If you want to switch between multiple domains in your app, refer [here](https://github.com/auth0/react-native-auth0/blob/master/EXAMPLES.md#domain-switching)
| API | Description |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| domain | Mandatory: Provide the Auth0 domain that can be found at the [Application Settings](https://manage.auth0.com/#/applications) |
Expand Down
110 changes: 57 additions & 53 deletions android/src/main/java/com/auth0/react/A0Auth0Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

import android.app.Activity;
import android.content.Intent;
import android.content.ActivityNotFoundException;
import android.net.Uri;

import androidx.annotation.NonNull;
import android.util.Base64;

import com.auth0.android.Auth0;
import com.auth0.android.authentication.AuthenticationAPIClient;
Expand All @@ -16,27 +14,21 @@
import com.auth0.android.provider.WebAuthProvider;
import com.auth0.android.result.Credentials;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import static android.app.Activity.RESULT_OK;

public class A0Auth0Module extends ReactContextBaseJavaModule implements ActivityEventListener {

private static final String ERROR_CODE = "a0.invalid_state.credential_manager_exception";
private static final String CREDENTIAL_MANAGER_ERROR_CODE = "a0.invalid_state.credential_manager_exception";
private static final String INVALID_DOMAIN_URL_ERROR_CODE = "a0.invalid_domain_url";
private static final int LOCAL_AUTH_REQUEST_CODE = 150;
public static final int UNKNOWN_ERROR_RESULT_CODE = 1405;

Expand All @@ -52,7 +44,7 @@ public A0Auth0Module(ReactApplicationContext reactContext) {
}

@ReactMethod
public void initializeAuth0(String clientId, String domain) {
public void initializeAuth0WithConfiguration(String clientId, String domain) {
this.auth0 = new Auth0(clientId, domain);
AuthenticationAPIClient authenticationAPIClient = new AuthenticationAPIClient(auth0);
this.secureCredentialsManager = new SecureCredentialsManager(
Expand All @@ -63,13 +55,25 @@ public void initializeAuth0(String clientId, String domain) {
}

@ReactMethod
public void hasValidAuth0Instance(Promise promise) {
promise.resolve(this.auth0 != null && this.secureCredentialsManager != null);
public void hasValidAuth0InstanceWithConfiguration(String clientId, String domain, Promise promise) {
if(this.auth0 == null) {
promise.resolve(false);
return;
}
String currentDomain;
try {
URL domainUrl = new URL(this.auth0.getDomainUrl());
currentDomain = domainUrl.getHost();
} catch (MalformedURLException e) {
promise.reject(INVALID_DOMAIN_URL_ERROR_CODE, "Invalid domain URL", e);
return;
}
promise.resolve(this.auth0.getClientId().equals(clientId) && currentDomain.equals(domain));
}

@ReactMethod
public void getCredentials(String scope, double minTtl, ReadableMap parameters, boolean forceRefresh, Promise promise) {
Map<String,String> cleanedParameters = new HashMap<>();
Map<String, String> cleanedParameters = new HashMap<>();
for (Map.Entry<String, Object> entry : parameters.toHashMap().entrySet()) {
if (entry.getValue() != null) {
cleanedParameters.put(entry.getKey(), entry.getValue().toString());
Expand All @@ -85,7 +89,7 @@ public void onSuccess(Credentials credentials) {

@Override
public void onFailure(@NonNull CredentialsManagerException e) {
promise.reject(ERROR_CODE, e.getMessage(), e);
promise.reject(CREDENTIAL_MANAGER_ERROR_CODE, e.getMessage(), e);
}
});
}
Expand All @@ -96,23 +100,23 @@ public void saveCredentials(ReadableMap credentials, Promise promise) {
this.secureCredentialsManager.saveCredentials(CredentialsParser.fromMap(credentials));
promise.resolve(true);
} catch (CredentialsManagerException e) {
promise.reject(ERROR_CODE, e.getMessage(), e);
promise.reject(CREDENTIAL_MANAGER_ERROR_CODE, e.getMessage(), e);
}
}

@ReactMethod
public void enableLocalAuthentication(String title, String description, Promise promise) {
Activity activity = reactContext.getCurrentActivity();
if (activity == null) {
promise.reject(ERROR_CODE, "No current activity present");
promise.reject(CREDENTIAL_MANAGER_ERROR_CODE, "No current activity present");
return;
}
activity.runOnUiThread(() -> {
try {
A0Auth0Module.this.secureCredentialsManager.requireAuthentication(activity, LOCAL_AUTH_REQUEST_CODE, title, description);
promise.resolve(true);
} catch (CredentialsManagerException e){
promise.reject(ERROR_CODE, e.getMessage(), e);
} catch (CredentialsManagerException e) {
promise.reject(CREDENTIAL_MANAGER_ERROR_CODE, e.getMessage(), e);
}
});
}
Expand Down Expand Up @@ -144,69 +148,69 @@ 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, int safariViewControllerPresentationStyle, ReadableMap additionalParameters, Promise promise) {
this.webAuthPromise = promise;
Map<String,String> cleanedParameters = new HashMap<>();
Map<String, String> cleanedParameters = new HashMap<>();
for (Map.Entry<String, Object> entry : additionalParameters.toHashMap().entrySet()) {
if (entry.getValue() != null) {
cleanedParameters.put(entry.getKey(), entry.getValue().toString());
}
}
WebAuthProvider.Builder builder = WebAuthProvider.login(this.auth0)
.withScheme(scheme);
if(state != null) {
if (state != null) {
builder.withState(state);
}
if(nonce != null) {
if (nonce != null) {
builder.withNonce(nonce);
}
if(audience != null) {
if (audience != null) {
builder.withAudience(audience);
}
if(scope != null) {
if (scope != null) {
builder.withScope(scope);
}
if(connection != null) {
if (connection != null) {
builder.withConnection(connection);
}
if(maxAge != 0) {
if (maxAge != 0) {
builder.withMaxAge(maxAge);
}
if(organization != null) {
if (organization != null) {
builder.withOrganization(organization);
}
if(invitationUrl != null) {
if (invitationUrl != null) {
builder.withInvitationUrl(invitationUrl);
}
if(leeway != 0) {
if (leeway != 0) {
builder.withIdTokenVerificationLeeway(leeway);
}
if(redirectUri != null) {
if (redirectUri != null) {
builder.withRedirectUri(redirectUri);
}
builder.withParameters(cleanedParameters);
builder.start(reactContext.getCurrentActivity(), new com.auth0.android.callback.Callback<Credentials, AuthenticationException>() {
@Override
public void onSuccess(Credentials result) {
ReadableMap map = CredentialsParser.toMap(result);
promise.resolve(map);
webAuthPromise = null;
}
@Override
public void onSuccess(Credentials result) {
ReadableMap map = CredentialsParser.toMap(result);
promise.resolve(map);
webAuthPromise = null;
}

@Override
public void onFailure(@NonNull AuthenticationException error) {
handleError(error, promise);
webAuthPromise = null;
}
});
@Override
public void onFailure(@NonNull AuthenticationException error) {
handleError(error, promise);
webAuthPromise = null;
}
});
}

@ReactMethod
public void webAuthLogout(String scheme, boolean federated, String redirectUri, Promise promise) {
WebAuthProvider.LogoutBuilder builder = WebAuthProvider.logout(this.auth0)
.withScheme(scheme);
if(federated) {
if (federated) {
builder.withFederated();
}
if(redirectUri != null) {
if (redirectUri != null) {
builder.withReturnToUrl(redirectUri);
}
builder.start(reactContext.getCurrentActivity(), new com.auth0.android.callback.Callback<Void, AuthenticationException>() {
Expand All @@ -223,19 +227,19 @@ public void onFailure(AuthenticationException e) {
}

private void handleError(AuthenticationException error, Promise promise) {
if(error.isBrowserAppNotAvailable()) {
if (error.isBrowserAppNotAvailable()) {
promise.reject("a0.browser_not_available", "No Browser application is installed.", error);
return;
}
if(error.isCanceled()) {
if (error.isCanceled()) {
promise.reject("a0.session.user_cancelled", "User cancelled the Auth", error);
return;
}
if(error.isNetworkError()) {
if (error.isNetworkError()) {
promise.reject("a0.network_error", "Network error", error);
return;
}
if(error.isIdTokenValidationError()) {
if (error.isIdTokenValidationError()) {
promise.reject("a0.session.invalid_idtoken", "Error validating ID Token", error);
return;
}
Expand All @@ -245,14 +249,14 @@ private void handleError(AuthenticationException error, Promise promise) {

@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if(requestCode == LOCAL_AUTH_REQUEST_CODE) {
if (requestCode == LOCAL_AUTH_REQUEST_CODE) {
secureCredentialsManager.checkAuthenticationResult(requestCode, resultCode);
}
}

@Override
public void onNewIntent(Intent intent) {
if(webAuthPromise != null) {
if (webAuthPromise != null) {
webAuthPromise.reject("a0.session.browser_terminated", "The browser window was closed by a new instance of the application");
webAuthPromise = null;
}
Expand Down
1 change: 0 additions & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ android {
namespace "com.auth0example"
defaultConfig {
applicationId "com.auth0example"
manifestPlaceholders = [auth0Domain: "brucke.auth0.com", auth0Scheme: "${applicationId}.auth0"]
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
Expand Down
Loading

0 comments on commit 1aa5e70

Please sign in to comment.