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

feat: added support for domain switching #931

Merged
merged 22 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c5bb94e
chore: updated sample app to contain mutliple screens and each screen…
desusai7 Jul 19, 2024
29e1b73
feat: added support for domain switching
desusai7 Jul 19, 2024
f89b686
feat: added support for domain switching via expo too
desusai7 Jul 26, 2024
ab41786
chore: improved the interactions with expo config plugins for domain …
desusai7 Jul 29, 2024
329abf9
test: added unit tests for supporting domain switching via expo
desusai7 Jul 30, 2024
df039a6
chore: minor changes in A0Auth0Module
desusai7 Jul 30, 2024
8260321
chore: updating clients in the auth0 provider whenever clientId & dom…
desusai7 Jul 31, 2024
b21dec6
chore: minor refactoring
desusai7 Jul 31, 2024
606cf91
test: fixed failing unit tests after the refactor
desusai7 Jul 31, 2024
cd92afb
chore: changed to useMemo for auth0 instance of combining useState & …
desusai7 Aug 2, 2024
6839b8b
chore: bumped up version of expo to 51.0.24 & react-native to 0.74.4 …
desusai7 Aug 2, 2024
395bc62
chore(deps-dev): bump braces from 3.0.2 to 3.0.3 (#939)
dependabot[bot] Aug 2, 2024
61c9dcd
chore(deps): bump fast-xml-parser from 4.2.7 to 4.4.1 in /example (#936)
dependabot[bot] Aug 2, 2024
f410bcb
chore(deps-dev): bump @testing-library/jest-dom from 6.4.5 to 6.4.8 (…
dependabot[bot] Aug 2, 2024
56ceea7
chore(deps): bump actions/setup-node from 4.0.2 to 4.0.3 (#929)
dependabot[bot] Aug 2, 2024
7c4d608
chore(deps): bump ws from 6.2.2 to 6.2.3 in /example (#925)
dependabot[bot] Aug 2, 2024
318d515
chore(deps-dev): bump ws from 6.2.2 to 6.2.3 (#924)
dependabot[bot] Aug 2, 2024
246a19b
chore(deps): bump braces and react-native-codegen in /example (#922)
dependabot[bot] Aug 2, 2024
77f84ba
chore(deps-dev): bump typedoc-plugin-missing-exports from 2.1.0 to 2.…
dependabot[bot] Aug 2, 2024
ff9ed70
docs: updated examples and readme for supporting domain switching
desusai7 Aug 2, 2024
b6d8aad
Update codeowner file with new GitHub team name (#935)
stevenwong-okta Aug 2, 2024
b4b58d5
Merge branch 'master' into feat/domain_switching
desusai7 Aug 2, 2024
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
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
Loading