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

Add dynamic client registration #418

Merged
merged 4 commits into from
Jan 6, 2020
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: 5 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ rules:
prettier/prettier:
- error
- trailingComma: es5
eqeqeq:
- error
- smart
max-statements:
- off
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ with optional overrides.
* **customHeaders** - (`object`) _ANDROID_ you can specify custom headers to pass during authorize request and/or token request.
* **authorize** - (`{ [key: string]: value }`) headers to be passed during authorization request.
* **token** - (`{ [key: string]: value }`) headers to be passed during token retrieval request.
* **register** - (`{ [key: string]: value }`) headers to be passed during registration request.
* **useNonce** - (`boolean`) _IOS_ (default: true) optionally allows not sending the nonce parameter, to support non-compliant providers
* **usePKCE** - (`boolean`) (default: true) optionally allows not sending the code_challenge parameter and skipping PKCE code verification, to support non-compliant providers.

Expand Down Expand Up @@ -179,6 +180,49 @@ const result = await revoke(config, {
});
```


### `register`

This will perform [dynamic client registration](https://openid.net/specs/openid-connect-registration-1_0.html) on the given provider.
If the provider supports dynamic client registration, it will generate a `clientId` for you to use in subsequent calls to this library.

```js
import { register } from 'react-native-app-auth';

const registerConfig = {
issuer: '<YOUR_ISSUER_URL>',
redirectUrls: ['<YOUR_REDIRECT_URL>', '<YOUR_OTHER_REDIRECT_URL>'],
};

const registerResult = await register(registerConfig);
```

#### registerConfig

* **issuer** - (`string`) same as in authorization config
* **serviceConfiguration** - (`object`) same as in authorization config
* **redirectUrls** - (`array<string>`) _REQUIRED_ specifies all of the redirect urls that your client will use for authentication
* **responseTypes** - (`array<string>`) an array that specifies which [OAuth 2.0 response types](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html) your client will use. The default value is `['code']`
* **grantTypes** - (`array<string>`) an array that specifies which [OAuth 2.0 grant types](https://oauth.net/2/grant-types/) your client will use. The default value is `['authorization_code']`
* **subjectType** - (`string`) requests a specific [subject type](https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes) for your client
* **tokenEndpointAuthMethod** (`string`) specifies which `clientAuthMethod` your client will use for authentication. The default value is `'client_secret_basic'`
* **additionalParameters** - (`object`) additional parameters that will be passed in the registration request.
Must be string values! E.g. setting `additionalParameters: { hello: 'world', foo: 'bar' }` would add
`hello=world&foo=bar` to the authorization request.
* **dangerouslyAllowInsecureHttpRequests** - (`boolean`) _ANDROID_ same as in authorization config
* **customHeaders** - (`object`) _ANDROID_ same as in authorization config

#### registerResult

This is the result from the auth server

* **clientId** - (`string`) the assigned client id
* **clientIdIssuedAt** - (`string`) _OPTIONAL_ date string of when the client id was issued
* **clientSecret** - (`string`) _OPTIONAL_ the assigned client secret
* **clientSecretExpiresAt** - (`string`) date string of when the client secret expires, which will be provided if `clientSecret` is provided. If `new Date(clientSecretExpiresAt).getTime() === 0`, then the secret never expires
* **registrationClientUri** - (`string`) _OPTIONAL_ uri that can be used to perform subsequent operations on the registration
* **registrationAccessToken** - (`string`) token that can be used at the endpoint given by `registrationClientUri` to perform subsequent operations on the registration. Will be provided if `registrationClientUri` is provided

## Getting started

```sh
Expand Down
162 changes: 158 additions & 4 deletions android/src/main/java/com/rnappauth/RNAppAuthModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import com.rnappauth.utils.MapUtil;
import com.rnappauth.utils.UnsafeConnectionBuilder;
import com.rnappauth.utils.RegistrationResponseFactory;
import com.rnappauth.utils.TokenResponseFactory;
import com.rnappauth.utils.CustomConnectionBuilder;

Expand All @@ -36,14 +37,18 @@
import net.openid.appauth.ClientAuthentication;
import net.openid.appauth.ClientSecretBasic;
import net.openid.appauth.ClientSecretPost;
import net.openid.appauth.RegistrationRequest;
import net.openid.appauth.RegistrationResponse;
import net.openid.appauth.ResponseTypeValues;
import net.openid.appauth.TokenResponse;
import net.openid.appauth.TokenRequest;
import net.openid.appauth.connectivity.ConnectionBuilder;
import net.openid.appauth.connectivity.DefaultConnectionBuilder;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.CountDownLatch;
Expand All @@ -56,6 +61,7 @@ public class RNAppAuthModule extends ReactContextBaseJavaModule implements Activ
private Promise promise;
private Boolean dangerouslyAllowInsecureHttpRequests;
private String clientAuthMethod = "basic";
private Map<String, String> registrationRequestHeaders = null;
private Map<String, String> authorizationRequestHeaders = null;
private Map<String, String> tokenRequestHeaders = null;
private Map<String, String> additionalParametersMap;
Expand Down Expand Up @@ -130,6 +136,75 @@ public void onFetchConfigurationCompleted(
}
}

@ReactMethod
public void register(
String issuer,
final ReadableArray redirectUris,
final ReadableArray responseTypes,
final ReadableArray grantTypes,
final String subjectType,
final String tokenEndpointAuthMethod,
final ReadableMap additionalParameters,
final ReadableMap serviceConfiguration,
final Boolean dangerouslyAllowInsecureHttpRequests,
final ReadableMap headers,
final Promise promise
) {
this.parseHeaderMap(headers);
final ConnectionBuilder builder = createConnectionBuilder(dangerouslyAllowInsecureHttpRequests, this.registrationRequestHeaders);
final AppAuthConfiguration appAuthConfiguration = this.createAppAuthConfiguration(builder);
final HashMap<String, String> additionalParametersMap = MapUtil.readableMapToHashMap(additionalParameters);

// when serviceConfiguration is provided, we don't need to hit up the OpenID well-known id endpoint
if (serviceConfiguration != null || mServiceConfiguration.get() != null) {
try {
final AuthorizationServiceConfiguration serviceConfig = mServiceConfiguration.get() != null ? mServiceConfiguration.get() : createAuthorizationServiceConfiguration(serviceConfiguration);
registerWithConfiguration(
serviceConfig,
appAuthConfiguration,
redirectUris,
responseTypes,
grantTypes,
subjectType,
tokenEndpointAuthMethod,
additionalParametersMap,
promise
);
} catch (Exception e) {
promise.reject("registration_failed", e.getMessage());
}
} else {
final Uri issuerUri = Uri.parse(issuer);
AuthorizationServiceConfiguration.fetchFromUrl(
buildConfigurationUriFromIssuer(issuerUri),
new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() {
public void onFetchConfigurationCompleted(
@Nullable AuthorizationServiceConfiguration fetchedConfiguration,
@Nullable AuthorizationException ex) {
if (ex != null) {
promise.reject("service_configuration_fetch_error", getErrorMessage(ex));
return;
}

mServiceConfiguration.set(fetchedConfiguration);

registerWithConfiguration(
fetchedConfiguration,
appAuthConfiguration,
redirectUris,
responseTypes,
grantTypes,
subjectType,
tokenEndpointAuthMethod,
additionalParametersMap,
promise
);
}
},
builder);
}
}

@ReactMethod
public void authorize(
String issuer,
Expand All @@ -150,10 +225,6 @@ public void authorize(
final AppAuthConfiguration appAuthConfiguration = this.createAppAuthConfiguration(builder);
final HashMap<String, String> additionalParametersMap = MapUtil.readableMapToHashMap(additionalParameters);

if (clientSecret != null) {
additionalParametersMap.put("client_secret", clientSecret);
}

// store args in private fields for later use in onActivityResult handler
this.promise = promise;
this.dangerouslyAllowInsecureHttpRequests = dangerouslyAllowInsecureHttpRequests;
Expand Down Expand Up @@ -345,6 +416,64 @@ public void onTokenRequestCompleted(
}
}

/*
* Perform dynamic client registration with the provided configuration
*/
private void registerWithConfiguration(
final AuthorizationServiceConfiguration serviceConfiguration,
final AppAuthConfiguration appAuthConfiguration,
final ReadableArray redirectUris,
final ReadableArray responseTypes,
final ReadableArray grantTypes,
final String subjectType,
final String tokenEndpointAuthMethod,
final Map<String, String> additionalParametersMap,
final Promise promise
) {
final Context context = this.reactContext;

AuthorizationService authService = new AuthorizationService(context, appAuthConfiguration);

RegistrationRequest.Builder registrationRequestBuilder =
new RegistrationRequest.Builder(
serviceConfiguration,
arrayToUriList(redirectUris)
)
.setAdditionalParameters(additionalParametersMap);

if (responseTypes != null) {
registrationRequestBuilder.setResponseTypeValues(arrayToList(responseTypes));
}

if (grantTypes != null) {
registrationRequestBuilder.setGrantTypeValues(arrayToList(grantTypes));
}

if (subjectType != null) {
registrationRequestBuilder.setSubjectType(subjectType);
}

if (tokenEndpointAuthMethod != null) {
registrationRequestBuilder.setTokenEndpointAuthenticationMethod(tokenEndpointAuthMethod);
}

RegistrationRequest registrationRequest = registrationRequestBuilder.build();

AuthorizationService.RegistrationResponseCallback registrationResponseCallback = new AuthorizationService.RegistrationResponseCallback() {
@Override
public void onRegistrationRequestCompleted(@Nullable RegistrationResponse response, @Nullable AuthorizationException ex) {
if (response != null) {
WritableMap map = RegistrationResponseFactory.registrationResponseToMap(response);
promise.resolve(map);
} else {
promise.reject("registration_failed", getErrorMessage(ex));
}
}
};

authService.performRegistrationRequest(registrationRequest, registrationResponseCallback);
}

/*
* Authorize user with the provided configuration
*/
Expand Down Expand Up @@ -487,6 +616,9 @@ private void parseHeaderMap (ReadableMap headerMap) {
if (headerMap == null) {
return;
}
if (headerMap.hasKey("register") && headerMap.getType("register") == ReadableType.Map) {
this.registrationRequestHeaders = MapUtil.readableMapToHashMap(headerMap.getMap("register"));
}
if (headerMap.hasKey("authorize") && headerMap.getType("authorize") == ReadableType.Map) {
this.authorizationRequestHeaders = MapUtil.readableMapToHashMap(headerMap.getMap("authorize"));
}
Expand Down Expand Up @@ -527,6 +659,28 @@ private String arrayToString(ReadableArray array) {
return strBuilder.toString();
}

/*
* Create a string list from an array of strings
*/
private List<String> arrayToList(ReadableArray array) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < array.size(); i++) {
list.add(array.getString(i));
}
return list;
}

/*
* Create a Uri list from an array of strings
*/
private List<Uri> arrayToUriList(ReadableArray array) {
ArrayList<Uri> list = new ArrayList<>();
for (int i = 0; i < array.size(); i++) {
list.add(Uri.parse(array.getString(i)));
}
return list;
}

/*
* Create an App Auth configuration using the provided connection builder
*/
Expand Down
20 changes: 20 additions & 0 deletions android/src/main/java/com/rnappauth/utils/MapUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

import androidx.annotation.Nullable;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.WritableMap;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class MapUtil {

Expand All @@ -22,4 +26,20 @@ public static HashMap<String, String> readableMapToHashMap(@Nullable ReadableMap

return hashMap;
}

public static final WritableMap createAdditionalParametersMap(Map<String, String> additionalParameters) {
WritableMap additionalParametersMap = Arguments.createMap();

if (!additionalParameters.isEmpty()) {

Iterator<String> iterator = additionalParameters.keySet().iterator();

while(iterator.hasNext()) {
String key = iterator.next();
additionalParametersMap.putString(key, additionalParameters.get(key));
}
}

return additionalParametersMap;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.rnappauth.utils;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;

import net.openid.appauth.RegistrationResponse;

public final class RegistrationResponseFactory {
/*
* Read raw registration response into a React Native map to be passed down the bridge
*/
public static final WritableMap registrationResponseToMap(RegistrationResponse response) {
WritableMap map = Arguments.createMap();

map.putString("clientId", response.clientId);
map.putMap("additionalParameters", MapUtil.createAdditionalParametersMap(response.additionalParameters));

if (response.clientIdIssuedAt != null) {
map.putString("clientIdIssuedAt", DateUtil.formatTimestamp(response.clientIdIssuedAt));
}

if (response.clientSecret != null) {
map.putString("clientSecret", response.clientSecret);
}

if (response.clientSecretExpiresAt != null) {
map.putString("clientSecretExpiresAt", DateUtil.formatTimestamp(response.clientSecretExpiresAt));
}

if (response.registrationAccessToken != null) {
map.putString("registrationAccessToken", response.registrationAccessToken);
}

if (response.registrationClientUri != null) {
map.putString("registrationClientUri", response.registrationClientUri.toString());
}

if (response.tokenEndpointAuthMethod != null) {
map.putString("tokenEndpointAuthMethod", response.tokenEndpointAuthMethod);
}

return map;
}
}
Loading