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

[SDK-3537] Credential manager for React Native #501

Merged
merged 19 commits into from
Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions A0Auth0.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ Pod::Spec.new do |s|
s.homepage = package['repository']['baseUrl']
s.license = package['license']
s.authors = package['author']
s.platforms = { :ios => '9.0' }
s.platforms = { :ios => '12.0' }
s.source = { :git => 'https://github.com/auth0/react-native-auth0.git', :tag => "v#{s.version}" }

s.source_files = ['ios/A0Auth0.h', 'ios/A0Auth0.m']
s.source_files = 'ios/**/*.{h,m,mm,swift}'
s.requires_arc = true

s.dependency 'React-Core'
s.dependency 'Auth0', '~> 2.3'
end
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ repositories {
dependencies {
implementation "com.facebook.react:react-native:${safeExtGet('reactnativeVersion', '+')}"
implementation "androidx.browser:browser:1.2.0"
implementation 'com.auth0.android:auth0:2.8.0'
}

def configureReactNativePom(def pom) {
Expand Down
93 changes: 92 additions & 1 deletion android/src/main/java/com/auth0/react/A0Auth0Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@
import androidx.annotation.NonNull;
import android.util.Base64;

import com.auth0.android.Auth0;
import com.auth0.android.authentication.AuthenticationAPIClient;
import com.auth0.android.authentication.storage.CredentialsManagerException;
import com.auth0.android.authentication.storage.SecureCredentialsManager;
import com.auth0.android.authentication.storage.SharedPreferencesStorage;
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;
Expand All @@ -28,16 +36,94 @@ public class A0Auth0Module extends ReactContextBaseJavaModule implements Activit

private static final String US_ASCII = "US-ASCII";
private static final String SHA_256 = "SHA-256";
private static final String ERROR_CODE = "a0.invalid_state.credential_manager_exception";
private static final int LOCAL_AUTH_REQUEST_CODE = 150;

private final ReactApplicationContext reactContext;
private Callback callback;

private SecureCredentialsManager secureCredentialsManager;
public A0Auth0Module(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
this.reactContext.addActivityEventListener(this);
}

@ReactMethod
public void initializeCredentialManager(String clientId, String domain) {
Auth0 auth0 = new Auth0(clientId, domain);
AuthenticationAPIClient authenticationAPIClient = new AuthenticationAPIClient(auth0);
this.secureCredentialsManager = new SecureCredentialsManager(
reactContext,
authenticationAPIClient,
new SharedPreferencesStorage(reactContext)
);
}

@ReactMethod
public void hasValidCredentialManagerInstance(Promise promise) {
promise.resolve(this.secureCredentialsManager != null);
}

@ReactMethod
public void getCredentials(String scope, double minTtl, ReadableMap parameters, Promise promise) {
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());
}
}

this.secureCredentialsManager.getCredentials(scope, (int) minTtl, cleanedParameters, new com.auth0.android.callback.Callback<Credentials, CredentialsManagerException>() {
@Override
public void onSuccess(Credentials credentials) {
ReadableMap map = CredentialsParser.toMap(credentials);
promise.resolve(map);
}

@Override
public void onFailure(@NonNull CredentialsManagerException e) {
promise.reject(ERROR_CODE, e.getMessage());
}
});
}

@ReactMethod
public void saveCredentials(ReadableMap credentials, Promise promise) {
try {
this.secureCredentialsManager.saveCredentials(CredentialsParser.fromMap(credentials));
promise.resolve(true);
} catch (CredentialsManagerException e) {
promise.reject(ERROR_CODE, e.getMessage());
}
}

@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");
return;
}
try {
this.secureCredentialsManager.requireAuthentication(activity, LOCAL_AUTH_REQUEST_CODE, title, description);
promise.resolve(true);
} catch (CredentialsManagerException e){
promise.reject(ERROR_CODE, e.getMessage());
}
}

@ReactMethod
public void clearCredentials(Promise promise) {
this.secureCredentialsManager.clearCredentials();
promise.resolve(true);
}

@ReactMethod
public void hasValidCredentials(double minTtl, Promise promise) {
promise.resolve(this.secureCredentialsManager.hasValidCredentials((long) minTtl));
}

@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = new HashMap<>();
Expand Down Expand Up @@ -133,6 +219,11 @@ String generateCodeChallenge(@NonNull String codeVerifier) {
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
Callback cb = A0Auth0Module.this.callback;

if(requestCode == LOCAL_AUTH_REQUEST_CODE) {
secureCredentialsManager.checkAuthenticationResult(requestCode, resultCode);
return;
}

if (cb == null) {
return;
}
Expand All @@ -157,4 +248,4 @@ public void onNewIntent(Intent intent) {
// NO OP
}

}
}
68 changes: 68 additions & 0 deletions android/src/main/java/com/auth0/react/CredentialsParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.auth0.react;

import com.auth0.android.authentication.storage.CredentialsManagerException;
import com.auth0.android.result.Credentials;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableNativeMap;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class CredentialsParser {

private static final String ACCESS_TOKEN_KEY = "accessToken";
private static final String ID_TOKEN_KEY = "idToken";
private static final String EXPIRES_AT_KEY = "expiresAt";
private static final String SCOPE = "scope";
private static final String REFRESH_TOKEN_KEY = "refreshToken";
private static final String TYPE_KEY = "type";
private static final String TOKEN_TYPE_KEY = "tokenType";
private static final String EXPIRES_IN_KEY = "expiresIn";
private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";

public static ReadableMap toMap(Credentials credentials) {
WritableNativeMap map = new WritableNativeMap();
map.putString(ACCESS_TOKEN_KEY, credentials.getAccessToken());
map.putDouble(EXPIRES_AT_KEY, credentials.getExpiresAt().getTime());
map.putString(ID_TOKEN_KEY, credentials.getIdToken());
map.putString(SCOPE, credentials.getScope());
map.putString(REFRESH_TOKEN_KEY, credentials.getRefreshToken());
map.putString(TYPE_KEY, credentials.getType());
return map;
}

public static Credentials fromMap(ReadableMap map) {
String idToken = map.getString(ID_TOKEN_KEY);
String accessToken = map.getString(ACCESS_TOKEN_KEY);
String tokenType = map.getString(TOKEN_TYPE_KEY);
String refreshToken = map.getString(REFRESH_TOKEN_KEY);
String scope = map.getString(SCOPE);
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT, Locale.US);
Date expiresAt = null;
String expiresAtText = map.getString(EXPIRES_AT_KEY);
if(expiresAtText != null) {
try {
expiresAt = sdf.parse(expiresAtText);
} catch (ParseException e) {
throw new CredentialsManagerException("Invalid date format - "+expiresAtText, e);
}
}
double expiresIn = 0;
if(map.hasKey(EXPIRES_IN_KEY)) {
expiresIn = map.getDouble(EXPIRES_IN_KEY);
}
if (expiresAt == null && expiresIn != 0) {
expiresAt = new Date((long) (System.currentTimeMillis() + expiresIn * 1000));
}
return new Credentials(
idToken,
accessToken,
tokenType,
refreshToken,
expiresAt,
scope
);
}
}
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Auth from './src/auth';
import CredentialsManager from './src/credentials-manager';
import Users from './src/management/users';
import WebAuth from './src/webauth';
export {TimeoutError} from './src/utils/fetchWithTimeout';
Expand All @@ -22,6 +23,7 @@ export default class Auth0 {
const {domain, clientId, ...extras} = options;
this.auth = new Auth({baseUrl: domain, clientId, ...extras});
this.webAuth = new WebAuth(this.auth);
this.credentialsManager = new CredentialsManager(domain, clientId);
this.options = options;
}

Expand Down
5 changes: 5 additions & 0 deletions ios/A0Auth0-Bridging-Header.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//

#import <React/RCTBridgeModule.h>
2 changes: 2 additions & 0 deletions ios/A0Auth0.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
@interface A0Auth0 : NSObject <RCTBridgeModule>

@end

@class CredentialsManagerBridge;
43 changes: 43 additions & 0 deletions ios/A0Auth0.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#import <React/RCTUtils.h>

#import "A0Auth0-Swift.h"
#define ERROR_CANCELLED @{@"error": @"a0.session.user_cancelled",@"error_description": @"User cancelled the Auth"}
#define ERROR_FAILED_TO_LOAD @{@"error": @"a0.session.failed_load",@"error_description": @"Failed to load url"}

Expand All @@ -22,6 +23,7 @@ @interface A0Auth0 () <SFSafariViewControllerDelegate>
@interface A0Auth0 () <SFSafariViewControllerDelegate>
@property (weak, nonatomic) SFSafariViewController *last;
@property (strong, nonatomic) NSObject *authenticationSession;
@property (strong, nonatomic) CredentialsManagerBridge *credentialsManagerBridge;
@property (copy, nonatomic) RCTResponseSenderBlock sessionCallback;
@property (assign, nonatomic) BOOL closeOnLoad;
@end
Expand All @@ -39,6 +41,37 @@ - (dispatch_queue_t)methodQueue
[self terminateWithError:nil dismissing:YES animated:YES];
}

RCT_EXPORT_METHOD(hasValidCredentialManagerInstance:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
BOOL valid = [self checkHasValidCredentialManagerInstance];
resolve(@(valid));
}


RCT_EXPORT_METHOD(initializeCredentialManager:(NSString *)clientId domain:(NSString *)domain) {
[self tryAndInitializeCredentialManager:clientId domain:domain];
}

RCT_EXPORT_METHOD(saveCredentials:(NSDictionary *)credentials resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
[self.credentialsManagerBridge saveCredentialsWithCredentialsDict:credentials resolve:resolve reject:reject];
}

RCT_EXPORT_METHOD(getCredentials:(NSString *)scope minTTL:(NSInteger)minTTL parameters:(NSDictionary *)parameters resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
[self.credentialsManagerBridge getCredentialsWithScope:scope minTTL:minTTL parameters:parameters resolve:resolve reject:reject];
}

RCT_EXPORT_METHOD(hasValidCredentials:(NSInteger)minTTL resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
[self.credentialsManagerBridge hasValidCredentialsWithMinTTL:minTTL resolve:resolve];
}

RCT_EXPORT_METHOD(clearCredentials:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
[self.credentialsManagerBridge clearCredentialsWithResolve:resolve reject:reject];
}

RCT_EXPORT_METHOD(enableLocalAuthentication:(NSString *)title cancelTitle:(NSString *)cancelTitle fallbackTitle:(NSString *)fallbackTitle) {
[self.credentialsManagerBridge enableLocalAuthenticationWithTitle:title cancelTitle:title fallbackTitle:title];
}

RCT_EXPORT_METHOD(showUrl:(NSString *)urlString
usingEphemeralSession:(BOOL)ephemeralSession
closeOnLoad:(BOOL)closeOnLoad
Expand Down Expand Up @@ -70,6 +103,16 @@ + (BOOL)requiresMainQueueSetup {

UIBackgroundTaskIdentifier taskId;

- (BOOL)checkHasValidCredentialManagerInstance {
BOOL valid = self.credentialsManagerBridge != nil;
return valid;
}

- (void)tryAndInitializeCredentialManager:(NSString *)clientId domain:(NSString *)domain {
CredentialsManagerBridge *bridge = [[CredentialsManagerBridge alloc] initWithClientId: clientId domain: domain];
self.credentialsManagerBridge = bridge;
}

- (void)presentSafariWithURL:(NSURL *)url {
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
SFSafariViewController *controller = [[SFSafariViewController alloc] initWithURL:url];
Expand Down
Loading