Skip to content

Commit 6bb6710

Browse files
authored
OpenID Connect RP Initiated Logout (#693)
* Add OIDC RP Initiated Logout * Rename taskId variable to comply with PR #697 * Add logout example for Okta * Fix error in Okta logout example Co-authored-by: Gianluca Spada <gianluca.spada@spindox.it> Co-authored-by: Gianluca Spada <gianluca.spada@stackhouse.it>
1 parent 15b4399 commit 6bb6710

File tree

8 files changed

+489
-57
lines changed

8 files changed

+489
-57
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ with optional overrides.
118118
- **tokenEndpoint** - (`string`) _REQUIRED_ fully formed url to the OAuth token exchange endpoint
119119
- **revocationEndpoint** - (`string`) fully formed url to the OAuth token revocation endpoint. If you want to be able to revoke a token and no `issuer` is specified, this field is mandatory.
120120
- **registrationEndpoint** - (`string`) fully formed url to your OAuth/OpenID Connect registration endpoint. Only necessary for servers that require client registration.
121+
- **endSessionEndpoint** - (`string`) fully formed url to your OpenID Connect end session endpoint. If you want to be able to end a user's session and no `issuer` is specified, this field is mandatory.
121122
- **clientId** - (`string`) _REQUIRED_ your client id on the auth server
122123
- **clientSecret** - (`string`) client secret to pass to token exchange requests. :warning: Read more about [client secrets](#note-about-client-secrets)
123124
- **redirectUrl** - (`string`) _REQUIRED_ the url that links back to your app with the auth code
@@ -192,6 +193,23 @@ const result = await revoke(config, {
192193
});
193194
```
194195

196+
### `logout`
197+
198+
This method will logout a user, as per the [OpenID Connect RP Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) specification. It requires an `idToken`, obtained after successfully authenticating with OpenID Connect, and a URL to redirect back after the logout has been performed.
199+
200+
```js
201+
import { logout } from 'react-native-app-auth';
202+
203+
const config = {
204+
issuer: '<YOUR_ISSUER_URL>'
205+
};
206+
207+
const result = await logout(config, {
208+
idToken: '<ID_TOKEN>',
209+
postLogoutRedirectUrl: '<POST_LOGOUT_URL>'
210+
});
211+
```
212+
195213
### `register`
196214

197215
This will perform [dynamic client registration](https://openid.net/specs/openid-connect-registration-1_0.html) on the given provider.

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

Lines changed: 169 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.rnappauth.utils.UnsafeConnectionBuilder;
2929
import com.rnappauth.utils.RegistrationResponseFactory;
3030
import com.rnappauth.utils.TokenResponseFactory;
31+
import com.rnappauth.utils.EndSessionResponseFactory;
3132
import com.rnappauth.utils.CustomConnectionBuilder;
3233

3334
import net.openid.appauth.AppAuthConfiguration;
@@ -45,6 +46,8 @@
4546
import net.openid.appauth.ResponseTypeValues;
4647
import net.openid.appauth.TokenResponse;
4748
import net.openid.appauth.TokenRequest;
49+
import net.openid.appauth.EndSessionRequest;
50+
import net.openid.appauth.EndSessionResponse;
4851
import net.openid.appauth.connectivity.ConnectionBuilder;
4952
import net.openid.appauth.connectivity.DefaultConnectionBuilder;
5053

@@ -84,15 +87,15 @@ public RNAppAuthModule(ReactApplicationContext reactContext) {
8487

8588
@ReactMethod
8689
public void prefetchConfiguration(
87-
final Boolean warmAndPrefetchChrome,
88-
final String issuer,
89-
final String redirectUrl,
90-
final String clientId,
91-
final ReadableArray scopes,
92-
final ReadableMap serviceConfiguration,
93-
final boolean dangerouslyAllowInsecureHttpRequests,
94-
final ReadableMap headers,
95-
final Promise promise
90+
final Boolean warmAndPrefetchChrome,
91+
final String issuer,
92+
final String redirectUrl,
93+
final String clientId,
94+
final ReadableArray scopes,
95+
final ReadableMap serviceConfiguration,
96+
final boolean dangerouslyAllowInsecureHttpRequests,
97+
final ReadableMap headers,
98+
final Promise promise
9699
) {
97100
if (warmAndPrefetchChrome) {
98101
warmChromeCustomTab(reactContext, issuer);
@@ -145,17 +148,17 @@ public void onFetchConfigurationCompleted(
145148

146149
@ReactMethod
147150
public void register(
148-
String issuer,
149-
final ReadableArray redirectUris,
150-
final ReadableArray responseTypes,
151-
final ReadableArray grantTypes,
152-
final String subjectType,
153-
final String tokenEndpointAuthMethod,
154-
final ReadableMap additionalParameters,
155-
final ReadableMap serviceConfiguration,
156-
final boolean dangerouslyAllowInsecureHttpRequests,
157-
final ReadableMap headers,
158-
final Promise promise
151+
String issuer,
152+
final ReadableArray redirectUris,
153+
final ReadableArray responseTypes,
154+
final ReadableArray grantTypes,
155+
final String subjectType,
156+
final String tokenEndpointAuthMethod,
157+
final ReadableMap additionalParameters,
158+
final ReadableMap serviceConfiguration,
159+
final boolean dangerouslyAllowInsecureHttpRequests,
160+
final ReadableMap headers,
161+
final Promise promise
159162
) {
160163
this.parseHeaderMap(headers);
161164
final ConnectionBuilder builder = createConnectionBuilder(dangerouslyAllowInsecureHttpRequests, this.registrationRequestHeaders);
@@ -394,6 +397,72 @@ public void onFetchConfigurationCompleted(
394397

395398
}
396399

400+
@ReactMethod
401+
public void logout(
402+
String issuer,
403+
final String idTokenHint,
404+
final String postLogoutRedirectUri,
405+
final ReadableMap serviceConfiguration,
406+
final ReadableMap additionalParameters,
407+
final boolean dangerouslyAllowInsecureHttpRequests,
408+
final Promise promise
409+
) {
410+
final ConnectionBuilder builder = createConnectionBuilder(dangerouslyAllowInsecureHttpRequests, null);
411+
final AppAuthConfiguration appAuthConfiguration = this.createAppAuthConfiguration(builder, dangerouslyAllowInsecureHttpRequests);
412+
final HashMap<String, String> additionalParametersMap = MapUtil.readableMapToHashMap(additionalParameters);
413+
414+
this.promise = promise;
415+
416+
if (serviceConfiguration != null || hasServiceConfiguration(issuer)) {
417+
try {
418+
final AuthorizationServiceConfiguration serviceConfig = hasServiceConfiguration(issuer) ? getServiceConfiguration(issuer) : createAuthorizationServiceConfiguration(serviceConfiguration);
419+
endSessionWithConfiguration(
420+
serviceConfig,
421+
appAuthConfiguration,
422+
idTokenHint,
423+
postLogoutRedirectUri,
424+
additionalParametersMap
425+
);
426+
} catch (ActivityNotFoundException e) {
427+
promise.reject("browser_not_found", e.getMessage());
428+
} catch (Exception e) {
429+
promise.reject("end_session_failed", e.getMessage());
430+
}
431+
} else {
432+
final Uri issuerUri = Uri.parse(issuer);
433+
AuthorizationServiceConfiguration.fetchFromUrl(
434+
buildConfigurationUriFromIssuer(issuerUri),
435+
new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() {
436+
public void onFetchConfigurationCompleted(
437+
@Nullable AuthorizationServiceConfiguration fetchedConfiguration,
438+
@Nullable AuthorizationException ex) {
439+
if (ex != null) {
440+
promise.reject("service_configuration_fetch_error", ex.getLocalizedMessage(), ex);
441+
return;
442+
}
443+
444+
setServiceConfiguration(issuer, fetchedConfiguration);
445+
446+
try {
447+
endSessionWithConfiguration(
448+
fetchedConfiguration,
449+
appAuthConfiguration,
450+
idTokenHint,
451+
postLogoutRedirectUri,
452+
additionalParametersMap
453+
);
454+
} catch (ActivityNotFoundException e) {
455+
promise.reject("browser_not_found", e.getMessage());
456+
} catch (Exception e) {
457+
promise.reject("end_session_failed", e.getMessage());
458+
}
459+
}
460+
},
461+
builder
462+
);
463+
}
464+
}
465+
397466
/*
398467
* Called when the OAuth browser activity completes
399468
*/
@@ -468,32 +537,52 @@ public void onTokenRequestCompleted(
468537
}
469538

470539
}
540+
541+
if (requestCode == 53) {
542+
if (data == null) {
543+
if (promise != null) {
544+
promise.reject("end_session_failed", "Data intent is null" );
545+
}
546+
return;
547+
}
548+
EndSessionResponse response = EndSessionResponse.fromIntent(data);
549+
AuthorizationException ex = AuthorizationException.fromIntent(data);
550+
if (ex != null) {
551+
if (promise != null) {
552+
handleAuthorizationException("end_session_failed", ex, promise);
553+
}
554+
return;
555+
}
556+
final Promise endSessionPromise = this.promise;
557+
WritableMap map = EndSessionResponseFactory.endSessionResponseToMap(response);
558+
endSessionPromise.resolve(map);
559+
}
471560
}
472561

473562
/*
474563
* Perform dynamic client registration with the provided configuration
475564
*/
476565
private void registerWithConfiguration(
477-
final AuthorizationServiceConfiguration serviceConfiguration,
478-
final AppAuthConfiguration appAuthConfiguration,
479-
final ReadableArray redirectUris,
480-
final ReadableArray responseTypes,
481-
final ReadableArray grantTypes,
482-
final String subjectType,
483-
final String tokenEndpointAuthMethod,
484-
final Map<String, String> additionalParametersMap,
485-
final Promise promise
566+
final AuthorizationServiceConfiguration serviceConfiguration,
567+
final AppAuthConfiguration appAuthConfiguration,
568+
final ReadableArray redirectUris,
569+
final ReadableArray responseTypes,
570+
final ReadableArray grantTypes,
571+
final String subjectType,
572+
final String tokenEndpointAuthMethod,
573+
final Map<String, String> additionalParametersMap,
574+
final Promise promise
486575
) {
487576
final Context context = this.reactContext;
488577

489578
AuthorizationService authService = new AuthorizationService(context, appAuthConfiguration);
490579

491580
RegistrationRequest.Builder registrationRequestBuilder =
492581
new RegistrationRequest.Builder(
493-
serviceConfiguration,
494-
arrayToUriList(redirectUris)
582+
serviceConfiguration,
583+
arrayToUriList(redirectUris)
495584
)
496-
.setAdditionalParameters(additionalParametersMap);
585+
.setAdditionalParameters(additionalParametersMap);
497586

498587
if (responseTypes != null) {
499588
registrationRequestBuilder.setResponseTypeValues(arrayToList(responseTypes));
@@ -677,6 +766,47 @@ public void onTokenRequestCompleted(@Nullable TokenResponse response, @Nullable
677766
}
678767
}
679768

769+
/*
770+
* End user session with provided configuration
771+
*/
772+
private void endSessionWithConfiguration(
773+
final AuthorizationServiceConfiguration serviceConfiguration,
774+
final AppAuthConfiguration appAuthConfiguration,
775+
final String idTokenHint,
776+
final String postLogoutRedirectUri,
777+
final Map<String, String> additionalParametersMap
778+
) {
779+
final Context context = this.reactContext;
780+
final Activity currentActivity = getCurrentActivity();
781+
782+
EndSessionRequest.Builder endSessionRequestBuilder =
783+
new EndSessionRequest.Builder(
784+
serviceConfiguration,
785+
idTokenHint,
786+
Uri.parse(postLogoutRedirectUri)
787+
);
788+
789+
if (additionalParametersMap != null) {
790+
if (additionalParametersMap.containsKey("state")) {
791+
endSessionRequestBuilder.setState(additionalParametersMap.get("state"));
792+
}
793+
}
794+
795+
EndSessionRequest endSessionRequest = endSessionRequestBuilder.build();
796+
797+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
798+
AuthorizationService authService = new AuthorizationService(context, appAuthConfiguration);
799+
Intent endSessionIntent = authService.getEndSessionRequestIntent(endSessionRequest);
800+
801+
currentActivity.startActivityForResult(endSessionIntent, 53);
802+
} else {
803+
AuthorizationService authService = new AuthorizationService(currentActivity, appAuthConfiguration);
804+
PendingIntent pendingIntent = currentActivity.createPendingResult(53, new Intent(), 0);
805+
806+
authService.performEndSessionRequest(endSessionRequest, pendingIntent);
807+
}
808+
}
809+
680810
private void parseHeaderMap (ReadableMap headerMap) {
681811
if (headerMap == null) {
682812
return;
@@ -793,14 +923,19 @@ private AuthorizationServiceConfiguration createAuthorizationServiceConfiguratio
793923
Uri authorizationEndpoint = Uri.parse(serviceConfiguration.getString("authorizationEndpoint"));
794924
Uri tokenEndpoint = Uri.parse(serviceConfiguration.getString("tokenEndpoint"));
795925
Uri registrationEndpoint = null;
926+
Uri endSessionEndpoint = null;
796927
if (serviceConfiguration.hasKey("registrationEndpoint")) {
797928
registrationEndpoint = Uri.parse(serviceConfiguration.getString("registrationEndpoint"));
798929
}
930+
if (serviceConfiguration.hasKey("endSessionEndpoint")) {
931+
endSessionEndpoint = Uri.parse(serviceConfiguration.getString("endSessionEndpoint"));
932+
}
799933

800934
return new AuthorizationServiceConfiguration(
801935
authorizationEndpoint,
802936
tokenEndpoint,
803-
registrationEndpoint
937+
registrationEndpoint,
938+
endSessionEndpoint
804939
);
805940
}
806941

@@ -837,7 +972,7 @@ private AuthorizationServiceConfiguration getServiceConfiguration(@Nullable Stri
837972
}
838973

839974
private void handleAuthorizationException(final String fallbackErrorCode, final AuthorizationException ex, final Promise promise) {
840-
if (ex.getLocalizedMessage() == null) {
975+
if (ex.getLocalizedMessage() == null) {
841976
promise.reject(fallbackErrorCode, ex.error, ex);
842977
} else {
843978
promise.reject(ex.error != null ? ex.error: fallbackErrorCode, ex.getLocalizedMessage(), ex);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.EndSessionResponse;
7+
8+
public final class EndSessionResponseFactory {
9+
/*
10+
* Read raw end session response into a React Native map to be passed down the bridge
11+
*/
12+
public static final WritableMap endSessionResponseToMap(EndSessionResponse response) {
13+
WritableMap map = Arguments.createMap();
14+
15+
map.putString("state", response.state);
16+
map.putString("idTokenHint", response.request.idToken);
17+
map.putString("postLogoutRedirectUri", response.request.redirectUri.toString());
18+
19+
return map;
20+
}
21+
}

docs/config-examples/okta.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Full support out of the box.
77
> Log in to your Okta Developer account and navigate to **Applications** > **Add Application**. Click **Native** and click the **Next** button. Give the app a name you’ll remember (e.g., `React Native`), select `Refresh Token` as a grant type, in addition to the default `Authorization Code`. Copy the **Login redirect URI** (e.g., `com.oktapreview.dev-158606:/callback`) and save it somewhere. You'll need this value when configuring your app.
88
>
99
> Click **Done** and you'll see a client ID on the next screen. Copy the redirect URI and clientId values into your App Auth config.
10+
>
11+
> To end the session, `postLogoutRedirectUrl` has to be one of the **Sign-out redirect URIs** defined in the **General Settings** > **LOGIN** section of the application page previously created.
1012
1113
```js
1214
const config = {
@@ -28,4 +30,10 @@ const refreshedState = await refresh(config, {
2830
await revoke(config, {
2931
tokenToRevoke: refreshedState.refreshToken
3032
});
33+
34+
// End session
35+
await logout(config, {
36+
idToken: authState.idToken,
37+
postLogoutRedirectUrl: 'com.{yourReversedOktaDomain}:/logout'
38+
});
3139
```

0 commit comments

Comments
 (0)