Skip to content

Commit 1396514

Browse files
author
Kadi Kraman
authored
Merge pull request #418 from jasonpaulos/registration
Add dynamic client registration
2 parents 1e0d420 + 7bd9345 commit 1396514

File tree

10 files changed

+727
-36
lines changed

10 files changed

+727
-36
lines changed

.eslintrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,8 @@ rules:
2424
prettier/prettier:
2525
- error
2626
- trailingComma: es5
27+
eqeqeq:
28+
- error
29+
- smart
30+
max-statements:
31+
- off

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ with optional overrides.
123123
* **customHeaders** - (`object`) _ANDROID_ you can specify custom headers to pass during authorize request and/or token request.
124124
* **authorize** - (`{ [key: string]: value }`) headers to be passed during authorization request.
125125
* **token** - (`{ [key: string]: value }`) headers to be passed during token retrieval request.
126+
* **register** - (`{ [key: string]: value }`) headers to be passed during registration request.
126127
* **useNonce** - (`boolean`) _IOS_ (default: true) optionally allows not sending the nonce parameter, to support non-compliant providers
127128
* **usePKCE** - (`boolean`) (default: true) optionally allows not sending the code_challenge parameter and skipping PKCE code verification, to support non-compliant providers.
128129

@@ -179,6 +180,49 @@ const result = await revoke(config, {
179180
});
180181
```
181182

183+
184+
### `register`
185+
186+
This will perform [dynamic client registration](https://openid.net/specs/openid-connect-registration-1_0.html) on the given provider.
187+
If the provider supports dynamic client registration, it will generate a `clientId` for you to use in subsequent calls to this library.
188+
189+
```js
190+
import { register } from 'react-native-app-auth';
191+
192+
const registerConfig = {
193+
issuer: '<YOUR_ISSUER_URL>',
194+
redirectUrls: ['<YOUR_REDIRECT_URL>', '<YOUR_OTHER_REDIRECT_URL>'],
195+
};
196+
197+
const registerResult = await register(registerConfig);
198+
```
199+
200+
#### registerConfig
201+
202+
* **issuer** - (`string`) same as in authorization config
203+
* **serviceConfiguration** - (`object`) same as in authorization config
204+
* **redirectUrls** - (`array<string>`) _REQUIRED_ specifies all of the redirect urls that your client will use for authentication
205+
* **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']`
206+
* **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']`
207+
* **subjectType** - (`string`) requests a specific [subject type](https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes) for your client
208+
* **tokenEndpointAuthMethod** (`string`) specifies which `clientAuthMethod` your client will use for authentication. The default value is `'client_secret_basic'`
209+
* **additionalParameters** - (`object`) additional parameters that will be passed in the registration request.
210+
Must be string values! E.g. setting `additionalParameters: { hello: 'world', foo: 'bar' }` would add
211+
`hello=world&foo=bar` to the authorization request.
212+
* **dangerouslyAllowInsecureHttpRequests** - (`boolean`) _ANDROID_ same as in authorization config
213+
* **customHeaders** - (`object`) _ANDROID_ same as in authorization config
214+
215+
#### registerResult
216+
217+
This is the result from the auth server
218+
219+
* **clientId** - (`string`) the assigned client id
220+
* **clientIdIssuedAt** - (`string`) _OPTIONAL_ date string of when the client id was issued
221+
* **clientSecret** - (`string`) _OPTIONAL_ the assigned client secret
222+
* **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
223+
* **registrationClientUri** - (`string`) _OPTIONAL_ uri that can be used to perform subsequent operations on the registration
224+
* **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
225+
182226
## Getting started
183227

184228
```sh

android/src/main/java/com/rnappauth/RNAppAuthModule.java

Lines changed: 158 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import com.rnappauth.utils.MapUtil;
2626
import com.rnappauth.utils.UnsafeConnectionBuilder;
27+
import com.rnappauth.utils.RegistrationResponseFactory;
2728
import com.rnappauth.utils.TokenResponseFactory;
2829
import com.rnappauth.utils.CustomConnectionBuilder;
2930

@@ -36,14 +37,18 @@
3637
import net.openid.appauth.ClientAuthentication;
3738
import net.openid.appauth.ClientSecretBasic;
3839
import net.openid.appauth.ClientSecretPost;
40+
import net.openid.appauth.RegistrationRequest;
41+
import net.openid.appauth.RegistrationResponse;
3942
import net.openid.appauth.ResponseTypeValues;
4043
import net.openid.appauth.TokenResponse;
4144
import net.openid.appauth.TokenRequest;
4245
import net.openid.appauth.connectivity.ConnectionBuilder;
4346
import net.openid.appauth.connectivity.DefaultConnectionBuilder;
4447

48+
import java.util.ArrayList;
4549
import java.util.Collections;
4650
import java.util.HashMap;
51+
import java.util.List;
4752
import java.util.Map;
4853
import java.util.concurrent.atomic.AtomicReference;
4954
import java.util.concurrent.CountDownLatch;
@@ -56,6 +61,7 @@ public class RNAppAuthModule extends ReactContextBaseJavaModule implements Activ
5661
private Promise promise;
5762
private Boolean dangerouslyAllowInsecureHttpRequests;
5863
private String clientAuthMethod = "basic";
64+
private Map<String, String> registrationRequestHeaders = null;
5965
private Map<String, String> authorizationRequestHeaders = null;
6066
private Map<String, String> tokenRequestHeaders = null;
6167
private Map<String, String> additionalParametersMap;
@@ -130,6 +136,75 @@ public void onFetchConfigurationCompleted(
130136
}
131137
}
132138

139+
@ReactMethod
140+
public void register(
141+
String issuer,
142+
final ReadableArray redirectUris,
143+
final ReadableArray responseTypes,
144+
final ReadableArray grantTypes,
145+
final String subjectType,
146+
final String tokenEndpointAuthMethod,
147+
final ReadableMap additionalParameters,
148+
final ReadableMap serviceConfiguration,
149+
final Boolean dangerouslyAllowInsecureHttpRequests,
150+
final ReadableMap headers,
151+
final Promise promise
152+
) {
153+
this.parseHeaderMap(headers);
154+
final ConnectionBuilder builder = createConnectionBuilder(dangerouslyAllowInsecureHttpRequests, this.registrationRequestHeaders);
155+
final AppAuthConfiguration appAuthConfiguration = this.createAppAuthConfiguration(builder);
156+
final HashMap<String, String> additionalParametersMap = MapUtil.readableMapToHashMap(additionalParameters);
157+
158+
// when serviceConfiguration is provided, we don't need to hit up the OpenID well-known id endpoint
159+
if (serviceConfiguration != null || mServiceConfiguration.get() != null) {
160+
try {
161+
final AuthorizationServiceConfiguration serviceConfig = mServiceConfiguration.get() != null ? mServiceConfiguration.get() : createAuthorizationServiceConfiguration(serviceConfiguration);
162+
registerWithConfiguration(
163+
serviceConfig,
164+
appAuthConfiguration,
165+
redirectUris,
166+
responseTypes,
167+
grantTypes,
168+
subjectType,
169+
tokenEndpointAuthMethod,
170+
additionalParametersMap,
171+
promise
172+
);
173+
} catch (Exception e) {
174+
promise.reject("registration_failed", e.getMessage());
175+
}
176+
} else {
177+
final Uri issuerUri = Uri.parse(issuer);
178+
AuthorizationServiceConfiguration.fetchFromUrl(
179+
buildConfigurationUriFromIssuer(issuerUri),
180+
new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() {
181+
public void onFetchConfigurationCompleted(
182+
@Nullable AuthorizationServiceConfiguration fetchedConfiguration,
183+
@Nullable AuthorizationException ex) {
184+
if (ex != null) {
185+
promise.reject("service_configuration_fetch_error", getErrorMessage(ex));
186+
return;
187+
}
188+
189+
mServiceConfiguration.set(fetchedConfiguration);
190+
191+
registerWithConfiguration(
192+
fetchedConfiguration,
193+
appAuthConfiguration,
194+
redirectUris,
195+
responseTypes,
196+
grantTypes,
197+
subjectType,
198+
tokenEndpointAuthMethod,
199+
additionalParametersMap,
200+
promise
201+
);
202+
}
203+
},
204+
builder);
205+
}
206+
}
207+
133208
@ReactMethod
134209
public void authorize(
135210
String issuer,
@@ -150,10 +225,6 @@ public void authorize(
150225
final AppAuthConfiguration appAuthConfiguration = this.createAppAuthConfiguration(builder);
151226
final HashMap<String, String> additionalParametersMap = MapUtil.readableMapToHashMap(additionalParameters);
152227

153-
if (clientSecret != null) {
154-
additionalParametersMap.put("client_secret", clientSecret);
155-
}
156-
157228
// store args in private fields for later use in onActivityResult handler
158229
this.promise = promise;
159230
this.dangerouslyAllowInsecureHttpRequests = dangerouslyAllowInsecureHttpRequests;
@@ -345,6 +416,64 @@ public void onTokenRequestCompleted(
345416
}
346417
}
347418

419+
/*
420+
* Perform dynamic client registration with the provided configuration
421+
*/
422+
private void registerWithConfiguration(
423+
final AuthorizationServiceConfiguration serviceConfiguration,
424+
final AppAuthConfiguration appAuthConfiguration,
425+
final ReadableArray redirectUris,
426+
final ReadableArray responseTypes,
427+
final ReadableArray grantTypes,
428+
final String subjectType,
429+
final String tokenEndpointAuthMethod,
430+
final Map<String, String> additionalParametersMap,
431+
final Promise promise
432+
) {
433+
final Context context = this.reactContext;
434+
435+
AuthorizationService authService = new AuthorizationService(context, appAuthConfiguration);
436+
437+
RegistrationRequest.Builder registrationRequestBuilder =
438+
new RegistrationRequest.Builder(
439+
serviceConfiguration,
440+
arrayToUriList(redirectUris)
441+
)
442+
.setAdditionalParameters(additionalParametersMap);
443+
444+
if (responseTypes != null) {
445+
registrationRequestBuilder.setResponseTypeValues(arrayToList(responseTypes));
446+
}
447+
448+
if (grantTypes != null) {
449+
registrationRequestBuilder.setGrantTypeValues(arrayToList(grantTypes));
450+
}
451+
452+
if (subjectType != null) {
453+
registrationRequestBuilder.setSubjectType(subjectType);
454+
}
455+
456+
if (tokenEndpointAuthMethod != null) {
457+
registrationRequestBuilder.setTokenEndpointAuthenticationMethod(tokenEndpointAuthMethod);
458+
}
459+
460+
RegistrationRequest registrationRequest = registrationRequestBuilder.build();
461+
462+
AuthorizationService.RegistrationResponseCallback registrationResponseCallback = new AuthorizationService.RegistrationResponseCallback() {
463+
@Override
464+
public void onRegistrationRequestCompleted(@Nullable RegistrationResponse response, @Nullable AuthorizationException ex) {
465+
if (response != null) {
466+
WritableMap map = RegistrationResponseFactory.registrationResponseToMap(response);
467+
promise.resolve(map);
468+
} else {
469+
promise.reject("registration_failed", getErrorMessage(ex));
470+
}
471+
}
472+
};
473+
474+
authService.performRegistrationRequest(registrationRequest, registrationResponseCallback);
475+
}
476+
348477
/*
349478
* Authorize user with the provided configuration
350479
*/
@@ -487,6 +616,9 @@ private void parseHeaderMap (ReadableMap headerMap) {
487616
if (headerMap == null) {
488617
return;
489618
}
619+
if (headerMap.hasKey("register") && headerMap.getType("register") == ReadableType.Map) {
620+
this.registrationRequestHeaders = MapUtil.readableMapToHashMap(headerMap.getMap("register"));
621+
}
490622
if (headerMap.hasKey("authorize") && headerMap.getType("authorize") == ReadableType.Map) {
491623
this.authorizationRequestHeaders = MapUtil.readableMapToHashMap(headerMap.getMap("authorize"));
492624
}
@@ -527,6 +659,28 @@ private String arrayToString(ReadableArray array) {
527659
return strBuilder.toString();
528660
}
529661

662+
/*
663+
* Create a string list from an array of strings
664+
*/
665+
private List<String> arrayToList(ReadableArray array) {
666+
ArrayList<String> list = new ArrayList<>();
667+
for (int i = 0; i < array.size(); i++) {
668+
list.add(array.getString(i));
669+
}
670+
return list;
671+
}
672+
673+
/*
674+
* Create a Uri list from an array of strings
675+
*/
676+
private List<Uri> arrayToUriList(ReadableArray array) {
677+
ArrayList<Uri> list = new ArrayList<>();
678+
for (int i = 0; i < array.size(); i++) {
679+
list.add(Uri.parse(array.getString(i)));
680+
}
681+
return list;
682+
}
683+
530684
/*
531685
* Create an App Auth configuration using the provided connection builder
532686
*/

android/src/main/java/com/rnappauth/utils/MapUtil.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
import androidx.annotation.Nullable;
44

5+
import com.facebook.react.bridge.Arguments;
56
import com.facebook.react.bridge.ReadableMap;
67
import com.facebook.react.bridge.ReadableMapKeySetIterator;
8+
import com.facebook.react.bridge.WritableMap;
79

810
import java.util.HashMap;
11+
import java.util.Iterator;
12+
import java.util.Map;
913

1014
public class MapUtil {
1115

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

2327
return hashMap;
2428
}
29+
30+
public static final WritableMap createAdditionalParametersMap(Map<String, String> additionalParameters) {
31+
WritableMap additionalParametersMap = Arguments.createMap();
32+
33+
if (!additionalParameters.isEmpty()) {
34+
35+
Iterator<String> iterator = additionalParameters.keySet().iterator();
36+
37+
while(iterator.hasNext()) {
38+
String key = iterator.next();
39+
additionalParametersMap.putString(key, additionalParameters.get(key));
40+
}
41+
}
42+
43+
return additionalParametersMap;
44+
}
2545
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.rnappauth.utils;
2+
3+
import com.facebook.react.bridge.Arguments;
4+
import com.facebook.react.bridge.WritableMap;
5+
6+
import net.openid.appauth.RegistrationResponse;
7+
8+
public final class RegistrationResponseFactory {
9+
/*
10+
* Read raw registration response into a React Native map to be passed down the bridge
11+
*/
12+
public static final WritableMap registrationResponseToMap(RegistrationResponse response) {
13+
WritableMap map = Arguments.createMap();
14+
15+
map.putString("clientId", response.clientId);
16+
map.putMap("additionalParameters", MapUtil.createAdditionalParametersMap(response.additionalParameters));
17+
18+
if (response.clientIdIssuedAt != null) {
19+
map.putString("clientIdIssuedAt", DateUtil.formatTimestamp(response.clientIdIssuedAt));
20+
}
21+
22+
if (response.clientSecret != null) {
23+
map.putString("clientSecret", response.clientSecret);
24+
}
25+
26+
if (response.clientSecretExpiresAt != null) {
27+
map.putString("clientSecretExpiresAt", DateUtil.formatTimestamp(response.clientSecretExpiresAt));
28+
}
29+
30+
if (response.registrationAccessToken != null) {
31+
map.putString("registrationAccessToken", response.registrationAccessToken);
32+
}
33+
34+
if (response.registrationClientUri != null) {
35+
map.putString("registrationClientUri", response.registrationClientUri.toString());
36+
}
37+
38+
if (response.tokenEndpointAuthMethod != null) {
39+
map.putString("tokenEndpointAuthMethod", response.tokenEndpointAuthMethod);
40+
}
41+
42+
return map;
43+
}
44+
}

0 commit comments

Comments
 (0)