From c75d3736ce168b5983243fdac6a6484bab983330 Mon Sep 17 00:00:00 2001 From: Sergiy Mokiyenko Date: Thu, 29 Nov 2018 16:23:13 +0200 Subject: [PATCH] 374 Support End Session --- README.md | 71 +++ app/README-Okta.md | 4 +- app/README.md | 9 + app/build.gradle | 2 +- .../net/openid/appauthdemo/Configuration.java | 14 + .../net/openid/appauthdemo/LoginActivity.java | 1 + .../net/openid/appauthdemo/TokenActivity.java | 44 +- app/res/raw/auth_config.json | 13 +- .../AuthorizationManagementActivity.java | 88 ++-- .../AuthorizationManagementRequest.java | 77 +++ .../AuthorizationManagementResponse.java | 67 +++ .../openid/appauth/AuthorizationRequest.java | 26 +- .../openid/appauth/AuthorizationResponse.java | 13 +- .../openid/appauth/AuthorizationService.java | 163 ++++++- .../AuthorizationServiceConfiguration.java | 25 +- .../AuthorizationServiceDiscovery.java | 10 + .../net/openid/appauth/EndSessionRequest.java | 170 +++++++ .../openid/appauth/EndSessionResponse.java | 166 +++++++ .../openid/appauth/RegistrationRequest.java | 5 +- .../java/net/openid/appauth/TokenRequest.java | 5 +- .../AuthorizationManagementActivityTest.java | 450 ++++++++++++++++-- .../appauth/AuthorizationRequestTest.java | 2 +- ...AuthorizationServiceConfigurationTest.java | 18 +- .../AuthorizationServiceDiscoveryTest.java | 2 + .../appauth/AuthorizationServiceTest.java | 31 ++ .../openid/appauth/EndSessionRequestTest.java | 81 ++++ .../appauth/EndSessionResponseTest.java | 125 +++++ .../net/openid/appauth/IdTokenTest.java | 2 + .../net/openid/appauth/TestValues.java | 6 + 29 files changed, 1563 insertions(+), 127 deletions(-) create mode 100644 library/java/net/openid/appauth/AuthorizationManagementRequest.java create mode 100644 library/java/net/openid/appauth/AuthorizationManagementResponse.java create mode 100644 library/java/net/openid/appauth/EndSessionRequest.java create mode 100644 library/java/net/openid/appauth/EndSessionResponse.java create mode 100644 library/javatests/net/openid/appauth/EndSessionRequestTest.java create mode 100644 library/javatests/net/openid/appauth/EndSessionResponseTest.java diff --git a/README.md b/README.md index 67026d20..33114acb 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,77 @@ authState.performActionWithFreshTokens(service, new AuthStateAction() { }); ``` +### Ending current session (Draft) + +Given you have a logged in session and you want to end it. In that case you need to get: +- `AuthorizationServiceConfiguration` +- valid Open Id Token that you should get after authentication +- End of session URI that should be provided within you OpeId service config + +First you have to build EndSessionRequest + +```java +EndSessionRequest endSessionRequest = new EndSessionRequest( + authorizationServiceConfiguration, + idToken, + endSessionRedirectUri + ); +``` +This request can then be dispatched using one of two approaches. + +a `startActivityForResult` call using an Intent returned from the `AuthorizationService`, +or by calling `performEndSessionRequest` and providing pending intent for completion +and cancelation handling activities. + +The startActivityForResult approach is simpler to use but may require more processing of the result: + +```java +private void endSession() { + AuthorizationService authService = new AuthorizationService(this); + Intent endSessionItent = authService.getAuthorizationRequestIntent(endSessionRequest); + startActivityForResult(endSessionItent, RC_END_SESSION); +} + +@Override +protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == RC_END_SESSION) { + EndSessionResonse resp = EndSessionResonse.fromIntent(data); + AuthorizationException ex = AuthorizationException.fromIntent(data); + // ... process the response or exception ... + } else { + // ... + } +} +``` +If instead you wish to directly transition to another activity on completion or cancelation, +you can use `performEndSessionRequest`: + +```java +AuthorizationService authService = new AuthorizationService(this); + +authService.performEndSessionRequest( + endSessionRequest, + PendingIntent.getActivity(this, 0, new Intent(this, MyAuthCompleteActivity.class), 0), + PendingIntent.getActivity(this, 0, new Intent(this, MyAuthCanceledActivity.class), 0)); +``` + +End session flow will also work involving browser mechanism that is described in authorization +mechanism session. +Handling response mechanism with transition to another activity should be as follows: + + ```java +public void onCreate(Bundle b) { + EndSessionResponse resp = EndSessionResponse.fromIntent(getIntent()); + AuthorizationException ex = AuthorizationException.fromIntent(getIntent()); + if (resp != null) { + // authorization completed + } else { + // authorization failed, check ex for more details + } + // ... +} +``` + ### AuthState persistence Instances of `AuthState` keep track of the authorization and token diff --git a/app/README-Okta.md b/app/README-Okta.md index acfa2de2..d9777e6c 100644 --- a/app/README-Okta.md +++ b/app/README-Okta.md @@ -13,7 +13,8 @@ You can create an Okta developer account at [https://developer.okta.com/](https: | Setting | Value | | ------------------- | --------------------------------------------------- | | Application Name | OpenId Connect App *(must be unique)* | -| Redirect URIs | com.oktapreview.yoursubdomain://callback_url| +| Login redirect URIs | com.oktapreview.yoursubdomain://callback_url| +| Logout redirect URIs| com.oktapreview.yoursubdomain://callback_url| | Allowed grant types | Authorization Code | 1. Click **Finish** to redirect back to the *General Settings* of your application. @@ -27,6 +28,7 @@ You can create an Okta developer account at [https://developer.okta.com/](https: { "client_id": "{{YourClientID}}", "redirect_uri": "com.oktapreview.{{yourOrg}}:/oauth", +"end_session_uri": "com.oktapreview.{{yourOrg}}:/{logoutCallback}", "authorization_scope": "openid email profile", "discovery_uri": "https://{{yourOrg}}.okta.com/.well-known/openid-configuration" } diff --git a/app/README.md b/app/README.md index 48bc27ec..4b22c241 100644 --- a/app/README.md +++ b/app/README.md @@ -16,6 +16,15 @@ The configuration file MUST contain a JSON object. The following properties can The value specified here should match the value specified for `appAuthRedirectScheme` in the `build.gradle` (Module: app), so that the demo app can capture the response. + - `end_sesion_uri` (required): The redirect URI to use for receiving the end session response. + This should be a custom scheme URI (com.example.app:/oauth2redirect/example-provider). + Consult the documentation for your authorization server. + + The value specified here should match the value specified for `appAuthRedirectScheme` in the + `build.gradle` (Module: app), so that the demo app can capture the response. + + NOTE: Scheme of the URI should be the same as `redirect_uri` but callback should be different. + - `authorization_scope` (required): The scope string to use for the authorization request. For the purposes of the demo, we recommend the value "openid profile email", though any value understood by your authorization server can be used. diff --git a/app/build.gradle b/app/build.gradle index 1487398c..56303f85 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,7 +12,7 @@ android { // Make sure this is consistent with the redirect URI used in res/raw/auth_config.json, // or specify additional redirect URIs in AndroidManifest.xml manifestPlaceholders = [ - 'appAuthRedirectScheme': 'net.openid.appauthdemo' + 'appAuthRedirectScheme': 'com.lohika.android.test' ] } diff --git a/app/java/net/openid/appauthdemo/Configuration.java b/app/java/net/openid/appauthdemo/Configuration.java index 0be12e09..9e97a3a8 100644 --- a/app/java/net/openid/appauthdemo/Configuration.java +++ b/app/java/net/openid/appauthdemo/Configuration.java @@ -62,9 +62,11 @@ public final class Configuration { private String mClientId; private String mScope; private Uri mRedirectUri; + private Uri mEndSessionUri; private Uri mDiscoveryUri; private Uri mAuthEndpointUri; private Uri mTokenEndpointUri; + private Uri mEndSessionEndpoint; private Uri mRegistrationEndpointUri; private Uri mUserInfoEndpointUri; private boolean mHttpsRequired; @@ -142,6 +144,11 @@ public Uri getDiscoveryUri() { return mDiscoveryUri; } + @Nullable + public Uri getmEndSessionUri() { + return mEndSessionUri; + } + @Nullable public Uri getAuthEndpointUri() { return mAuthEndpointUri; @@ -152,6 +159,11 @@ public Uri getTokenEndpointUri() { return mTokenEndpointUri; } + @Nullable + public Uri getEndSessionEndpoint() { + return mEndSessionEndpoint; + } + @Nullable public Uri getRegistrationEndpointUri() { return mRegistrationEndpointUri; @@ -196,6 +208,7 @@ private void readConfiguration() throws InvalidConfigurationException { mClientId = getConfigString("client_id"); mScope = getRequiredConfigString("authorization_scope"); mRedirectUri = getRequiredConfigUri("redirect_uri"); + mEndSessionUri = getRequiredConfigUri("end_session_uri"); if (!isRedirectUriRegistered()) { throw new InvalidConfigurationException( @@ -210,6 +223,7 @@ private void readConfiguration() throws InvalidConfigurationException { mTokenEndpointUri = getRequiredConfigWebUri("token_endpoint_uri"); mUserInfoEndpointUri = getRequiredConfigWebUri("user_info_endpoint_uri"); + mEndSessionEndpoint = getRequiredConfigUri("end_session_endpoint"); if (mClientId == null) { mRegistrationEndpointUri = getRequiredConfigWebUri("registration_endpoint_uri"); diff --git a/app/java/net/openid/appauthdemo/LoginActivity.java b/app/java/net/openid/appauthdemo/LoginActivity.java index 02e0371d..75884d72 100644 --- a/app/java/net/openid/appauthdemo/LoginActivity.java +++ b/app/java/net/openid/appauthdemo/LoginActivity.java @@ -215,6 +215,7 @@ private void initializeAppAuth() { AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration( mConfiguration.getAuthEndpointUri(), mConfiguration.getTokenEndpointUri(), + mConfiguration.getEndSessionEndpoint(), mConfiguration.getRegistrationEndpointUri()); mAuthStateManager.replace(new AuthState(config)); diff --git a/app/java/net/openid/appauthdemo/TokenActivity.java b/app/java/net/openid/appauthdemo/TokenActivity.java index 74f27b79..78e8c792 100644 --- a/app/java/net/openid/appauthdemo/TokenActivity.java +++ b/app/java/net/openid/appauthdemo/TokenActivity.java @@ -14,6 +14,7 @@ package net.openid.appauthdemo; +import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -36,6 +37,7 @@ import net.openid.appauth.AuthorizationService; import net.openid.appauth.AuthorizationServiceDiscovery; import net.openid.appauth.ClientAuthentication; +import net.openid.appauth.EndSessionRequest; import net.openid.appauth.TokenRequest; import net.openid.appauth.TokenResponse; @@ -66,6 +68,8 @@ public class TokenActivity extends AppCompatActivity { private static final String KEY_USER_INFO = "userInfo"; + private static final int END_SESSION_REQUEST_CODE = 911; + private AuthorizationService mAuthService; private AuthStateManager mStateManager; private final AtomicReference mUserInfoJson = new AtomicReference<>(); @@ -187,17 +191,17 @@ private void displayAuthorized() { AuthState state = mStateManager.getCurrent(); - TextView refreshTokenInfoView = (TextView) findViewById(R.id.refresh_token_info); + TextView refreshTokenInfoView = findViewById(R.id.refresh_token_info); refreshTokenInfoView.setText((state.getRefreshToken() == null) ? R.string.no_refresh_token_returned : R.string.refresh_token_returned); - TextView idTokenInfoView = (TextView) findViewById(R.id.id_token_info); + TextView idTokenInfoView = findViewById(R.id.id_token_info); idTokenInfoView.setText((state.getIdToken()) == null ? R.string.no_id_token_returned : R.string.id_token_returned); - TextView accessTokenInfoView = (TextView) findViewById(R.id.access_token_info); + TextView accessTokenInfoView = findViewById(R.id.access_token_info); if (state.getAccessToken() == null) { accessTokenInfoView.setText(R.string.no_access_token_returned); } else { @@ -213,13 +217,13 @@ private void displayAuthorized() { } } - Button refreshTokenButton = (Button) findViewById(R.id.refresh_token); + Button refreshTokenButton = findViewById(R.id.refresh_token); refreshTokenButton.setVisibility(state.getRefreshToken() != null ? View.VISIBLE : View.GONE); refreshTokenButton.setOnClickListener((View view) -> refreshAccessToken()); - Button viewProfileButton = (Button) findViewById(R.id.view_profile); + Button viewProfileButton = findViewById(R.id.view_profile); AuthorizationServiceDiscovery discoveryDoc = state.getAuthorizationServiceConfiguration().discoveryDoc; @@ -231,7 +235,7 @@ private void displayAuthorized() { viewProfileButton.setOnClickListener((View view) -> fetchUserInfo()); } - ((Button)findViewById(R.id.sign_out)).setOnClickListener((View view) -> signOut()); + findViewById(R.id.sign_out).setOnClickListener((View view) -> endOfSession()); View userInfoCard = findViewById(R.id.userinfo_card); JSONObject userInfo = mUserInfoJson.get(); @@ -381,6 +385,24 @@ private void fetchUserInfo(String accessToken, String idToken, AuthorizationExce }); } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == END_SESSION_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + signOut(); + finish(); + } else { + displayEndSessionCancelled(); + } + } + + private void displayEndSessionCancelled() { + Snackbar.make(findViewById(R.id.coordinator), + "Sign out canceled", + Snackbar.LENGTH_SHORT) + .show(); + } + @MainThread private void showSnackbar(String message) { Snackbar.make(findViewById(R.id.coordinator), @@ -389,6 +411,16 @@ private void showSnackbar(String message) { .show(); } + @MainThread + private void endOfSession() { + Intent endSessionEnten = mAuthService.getEndSessionRequestIntent( + new EndSessionRequest( + mStateManager.getCurrent().getAuthorizationServiceConfiguration(), + mStateManager.getCurrent().getIdToken(), + mConfiguration.getmEndSessionUri())); + startActivityForResult(endSessionEnten, END_SESSION_REQUEST_CODE); + } + @MainThread private void signOut() { // discard the authorization and token state, but retain the configuration and diff --git a/app/res/raw/auth_config.json b/app/res/raw/auth_config.json index 6f39e249..cb12e56f 100644 --- a/app/res/raw/auth_config.json +++ b/app/res/raw/auth_config.json @@ -1,11 +1,12 @@ { - "client_id": "", - "redirect_uri": "net.openid.appauthdemo:/oauth2redirect", + "client_id": "0oahnzhsegzYjqETc0h7", + "redirect_uri": "com.lohika.android.test:/callback", + "end_session_uri":"com.lohika.android.test:/logout", "authorization_scope": "openid email profile", - "discovery_uri": "", - "authorization_endpoint_uri": "", - "token_endpoint_uri": "", - "registration_endpoint_uri": "", + "discovery_uri": "https://lohika-um.oktapreview.com/oauth2/default/.well-known/openid-configuration", + "authorization_endpoint_uri": "https://lohika-um.oktapreview.com/oauth2/default/v1/authorize", + "token_endpoint_uri": "https://lohika-um.oktapreview.com/oauth2/default/v1/token", + "registration_endpoint_uri": "https://lohika-um.oktapreview.com/oauth2/v1/clients", "user_info_endpoint_uri": "", "https_required": true } diff --git a/library/java/net/openid/appauth/AuthorizationManagementActivity.java b/library/java/net/openid/appauth/AuthorizationManagementActivity.java index 3eddac43..9b72c1fa 100644 --- a/library/java/net/openid/appauth/AuthorizationManagementActivity.java +++ b/library/java/net/openid/appauth/AuthorizationManagementActivity.java @@ -29,10 +29,10 @@ import org.json.JSONException; /** - * Stores state and handles events related to the authorization flow. The activity is - * started by {@link AuthorizationService#performAuthorizationRequest - * AuthorizationService.performAuthorizationRequest}, and records all state pertinent to - * the authorization request before invoking the authorization intent. It also functions + * Stores state and handles events related to the authorization management flow. The activity is + * started by {@link AuthorizationService#performAuthorizationRequest} or + * {@link AuthorizationService#performEndOfSessionRequest}, and records all state pertinent to + * the authorization management request before invoking the authorization intent. It also functions * to control the back stack, ensuring that the authorization activity will not be reachable * via the back button after the flow completes. * @@ -71,7 +71,8 @@ * ``` * * The process begins with an activity requesting that an authorization flow be started, - * using {@link AuthorizationService#performAuthorizationRequest}. + * using {@link AuthorizationService#performAuthorizationRequest} or + * {@link AuthorizationService#performEndOfSessionRequest}. * * - Step 1: Using an intent derived from {@link #createStartIntent}, this activity is * started. The state delivered in this intent is recorded for future use. @@ -85,12 +86,14 @@ * activity to finish, the AuthorizationManagementActivity will be recreated or restarted. * * - Step C2a: If a cancellation PendingIntent was provided in the call to - * {@link AuthorizationService#performAuthorizationRequest}, then this is + * {@link AuthorizationService#performAuthorizationRequest} or + * {@link AuthorizationService#performEndOfSessionRequest}, then this is * used to invoke a cancelation activity. * * - Step C2b: If no cancellation PendingIntent was provided (legacy behavior, or * AuthorizationManagementActivity was started with an intent from - * {@link AuthorizationService#getAuthorizationRequestIntent}), then the + * {@link AuthorizationService#getAuthorizationRequestIntent} or + * @link AuthorizationService#performEndOfSessionRequest}), then the * AuthorizationManagementActivity simply finishes after calling {@link Activity#setResult}, * with {@link Activity#RESULT_CANCELED}, returning control to the activity above * it in the back stack (typically, the initiating activity). @@ -107,9 +110,10 @@ * top of the back stack. * * - Step S3a: If this activity was invoked via - * {@link AuthorizationService#performAuthorizationRequest}, then the pending intent provided + * {@link AuthorizationService#performAuthorizationRequest} or + * {@link AuthorizationService#performEndOfSessionRequest}, then the pending intent provided * for completion of the authorization flow is invoked, providing the decoded - * {@link AuthorizationResponse} or {@link AuthorizationException} as appropriate. + * {@link AuthorizationManagementResponse} or {@link AuthorizationException} as appropriate. * The AuthorizationManagementActivity finishes, removing itself from the back stack. * * - Step S3b: If this activity was invoked via an intent returned by @@ -138,7 +142,7 @@ public class AuthorizationManagementActivity extends Activity { private boolean mAuthorizationStarted = false; private Intent mAuthIntent; - private AuthorizationRequest mAuthRequest; + private AuthorizationManagementRequest mAuthRequest; private PendingIntent mCompleteIntent; private PendingIntent mCancelIntent; @@ -152,7 +156,7 @@ public class AuthorizationManagementActivity extends Activity { */ public static Intent createStartIntent( Context context, - AuthorizationRequest request, + AuthorizationManagementRequest request, Intent authIntent, PendingIntent completeIntent, PendingIntent cancelIntent) { @@ -167,12 +171,12 @@ public static Intent createStartIntent( /** * Creates an intent to start an authorization flow. * @param context the package context for the app. - * @param request the authorization request which is to be sent. + * @param request the authorization management request which is to be sent. * @param authIntent the intent to be used to get authorization from the user. */ public static Intent createStartForResultIntent( Context context, - AuthorizationRequest request, + AuthorizationManagementRequest request, Intent authIntent) { return createStartIntent(context, request, authIntent, null, null); } @@ -263,16 +267,7 @@ private void handleAuthorizationComplete() { } responseData.setData(responseUri); - if (mCompleteIntent != null) { - Logger.debug("Authorization complete - invoking completion intent"); - try { - mCompleteIntent.send(this, 0, responseData); - } catch (CanceledException ex) { - Logger.error("Failed to send completion intent", ex); - } - } else { - setResult(RESULT_OK, responseData); - } + sendResult(mCompleteIntent, responseData, RESULT_OK); } private void handleAuthorizationCanceled() { @@ -281,16 +276,8 @@ private void handleAuthorizationCanceled() { AuthorizationException.GeneralErrors.USER_CANCELED_AUTH_FLOW, null) .toIntent(); - if (mCancelIntent != null) { - try { - mCancelIntent.send(this, 0, cancelData); - } catch (CanceledException ex) { - Logger.error("Failed to send cancel intent", ex); - } - } else { - setResult(RESULT_CANCELED, cancelData); - Logger.debug("No cancel intent set - will return to previous activity"); - } + + sendResult(mCancelIntent, cancelData, RESULT_CANCELED); } private void extractState(Bundle state) { @@ -302,33 +289,46 @@ private void extractState(Bundle state) { mAuthIntent = state.getParcelable(KEY_AUTH_INTENT); mAuthorizationStarted = state.getBoolean(KEY_AUTHORIZATION_STARTED, false); + mCompleteIntent = state.getParcelable(KEY_COMPLETE_INTENT); + mCancelIntent = state.getParcelable(KEY_CANCEL_INTENT); try { String authRequestJson = state.getString(KEY_AUTH_REQUEST, null); mAuthRequest = authRequestJson != null - ? AuthorizationRequest.jsonDeserialize(authRequestJson) + ? AuthorizationManagementRequest.jsonDeserialize(authRequestJson) : null; } catch (JSONException ex) { - throw new IllegalStateException("Unable to deserialize authorization request", ex); + sendResult(mCancelIntent,AuthorizationRequestErrors.INVALID_REQUEST.toIntent(), + RESULT_CANCELED); + } + } + + private void sendResult(PendingIntent callback, Intent cancelData, int resultCode) { + if (callback != null) { + try { + callback.send(this, 0, cancelData); + } catch (CanceledException e) { + Logger.error("Failed to send cancel intent", e); + } + } else { + setResult(resultCode, cancelData); } - mCompleteIntent = state.getParcelable(KEY_COMPLETE_INTENT); - mCancelIntent = state.getParcelable(KEY_CANCEL_INTENT); } private Intent extractResponseData(Uri responseUri) { if (responseUri.getQueryParameterNames().contains(AuthorizationException.PARAM_ERROR)) { return AuthorizationException.fromOAuthRedirect(responseUri).toIntent(); } else { - AuthorizationResponse response = new AuthorizationResponse.Builder(mAuthRequest) - .fromUri(responseUri) - .build(); + AuthorizationManagementResponse response = AuthorizationManagementResponse + .buildFromRequest(mAuthRequest, responseUri); - if (mAuthRequest.state == null && response.state != null - || (mAuthRequest.state != null && !mAuthRequest.state.equals(response.state))) { + if (mAuthRequest.getState() == null && response.getState() != null + || (mAuthRequest.getState() != null && !mAuthRequest.getState() + .equals(response.getState()))) { Logger.warn("State returned in authorization response (%s) does not match state " + "from request (%s) - discarding response", - response.state, - mAuthRequest.state); + response.getState(), + mAuthRequest.getState()); return AuthorizationRequestErrors.STATE_MISMATCH.toIntent(); } diff --git a/library/java/net/openid/appauth/AuthorizationManagementRequest.java b/library/java/net/openid/appauth/AuthorizationManagementRequest.java new file mode 100644 index 00000000..311f9611 --- /dev/null +++ b/library/java/net/openid/appauth/AuthorizationManagementRequest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openid.appauth; + +import android.net.Uri; +import android.util.Base64; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.security.SecureRandom; + +/** + * A base request for session management models + * {@link AuthorizationRequest} + * {@link EndSessionRequest} + */ +public abstract class AuthorizationManagementRequest { + + private static final int STATE_LENGTH = 16; + + /** + * Reads an authorization request from a JSON string representation produced by + * {@link #jsonSerialize()}. + * @throws JSONException if the provided JSON does not match the expected structure. + */ + public static AuthorizationManagementRequest jsonDeserialize(String jsonStr) + throws JSONException { + Preconditions.checkNotNull(jsonStr, "jsonStr can not be null"); + JSONObject json = new JSONObject(jsonStr); + if (AuthorizationRequest.isAuthorizationRequest(json)) { + return AuthorizationRequest.jsonDeserialize(json); + } + if (EndSessionRequest.isEndSessionRequest(json)) { + return EndSessionRequest.jsonDeserialize(json); + } + throw new IllegalArgumentException( + "No AuthorizationManagementRequest found maching to this json schema"); + } + + static String generateRandomState() { + SecureRandom sr = new SecureRandom(); + byte[] random = new byte[STATE_LENGTH]; + sr.nextBytes(random); + return Base64.encodeToString(random, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); + } + + /** + * Produces a JSON representation of the authorization request for persistent storage or local + * transmission (e.g. between activities). + */ + public abstract JSONObject jsonSerialize(); + + /** + * Produces a JSON string representation of the authorization request for persistent storage or + * local transmission (e.g. between activities). This method is just a convenience wrapper + * for {@link #jsonSerialize()}, converting the JSON object to its string form. + */ + public abstract String jsonSerializeString(); + + public abstract String getState(); + + public abstract Uri toUri(); + +} diff --git a/library/java/net/openid/appauth/AuthorizationManagementResponse.java b/library/java/net/openid/appauth/AuthorizationManagementResponse.java new file mode 100644 index 00000000..9c884652 --- /dev/null +++ b/library/java/net/openid/appauth/AuthorizationManagementResponse.java @@ -0,0 +1,67 @@ +/* + * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openid.appauth; + +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * A base response for session management models + * {@link AuthorizationResponse} + * {@link EndSessionResponse} + */ +public abstract class AuthorizationManagementResponse { + + /** + * Builds an AuthorizationManagementResponse from {@link AuthorizationManagementRequest} + * and response {@link Uri} + */ + public static AuthorizationManagementResponse buildFromRequest( + AuthorizationManagementRequest request, Uri uri) { + if (request instanceof AuthorizationRequest) { + return new AuthorizationResponse.Builder((AuthorizationRequest) request) + .fromUri(uri) + .build(); + } + if (request instanceof EndSessionRequest) { + return EndSessionResponse.fromRequestAndUri((EndSessionRequest)request,uri); + } + throw new IllegalArgumentException("Malformed request or uri"); + } + + + /** + * Extracts response from an intent produced by {@link #toIntent()}. This is + * used to extract the response from the intent data passed to an activity registered as the + * handler for {@link AuthorizationService#performEndOfSessionRequest} + * or {@link AuthorizationService#performAuthManagementRequest}. + */ + @Nullable + public static AuthorizationManagementResponse fromIntent(@NonNull Intent dataIntent) { + if (EndSessionResponse.containsEndSessionResoponse(dataIntent)) { + return EndSessionResponse.fromIntent(dataIntent); + } + if (AuthorizationResponse.containEndSessionResoponse(dataIntent)) { + return AuthorizationResponse.fromIntent(dataIntent); + } + throw new IllegalArgumentException("Malformed intent"); + } + + public abstract String getState(); + + public abstract Intent toIntent(); +} diff --git a/library/java/net/openid/appauth/AuthorizationRequest.java b/library/java/net/openid/appauth/AuthorizationRequest.java index 0f133eb8..e99690a2 100644 --- a/library/java/net/openid/appauth/AuthorizationRequest.java +++ b/library/java/net/openid/appauth/AuthorizationRequest.java @@ -26,14 +26,12 @@ import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; -import android.util.Base64; import net.openid.appauth.internal.UriUtil; import org.json.JSONException; import org.json.JSONObject; -import java.security.SecureRandom; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -49,7 +47,7 @@ * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.1 * " */ -public class AuthorizationRequest { +public class AuthorizationRequest extends AuthorizationManagementRequest { /** * SHA-256 based code verifier challenge method. @@ -333,13 +331,13 @@ public static final class ResponseMode { private static final String KEY_CODE_VERIFIER_CHALLENGE_METHOD = "codeVerifierChallengeMethod"; private static final String KEY_RESPONSE_MODE = "responseMode"; private static final String KEY_ADDITIONAL_PARAMETERS = "additionalParameters"; - private static final int STATE_LENGTH = 16; /** * The service's {@link AuthorizationServiceConfiguration configuration}. * This configuration specifies how to connect to a particular OAuth provider. * Configurations may be - * {@link AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri, Uri, Uri)} + * {@link + * AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri, Uri, Uri, Uri)} * created manually}, or {@link AuthorizationServiceConfiguration#fetchFromUrl(Uri, * AuthorizationServiceConfiguration.RetrieveConfigurationCallback)} via an OpenID Connect * Discovery Document}. @@ -589,8 +587,8 @@ public Builder( setClientId(clientId); setResponseType(responseType); setRedirectUri(redirectUri); - setState(AuthorizationRequest.generateRandomState()); - setNonce(AuthorizationRequest.generateRandomState()); + setState(AuthorizationManagementRequest.generateRandomState()); + setNonce(AuthorizationManagementRequest.generateRandomState()); setCodeVerifier(CodeVerifierUtil.generateRandomCodeVerifier()); } @@ -1044,6 +1042,12 @@ public String jsonSerializeString() { return jsonSerialize().toString(); } + @Override + @Nullable + public String getState() { + return state; + } + /** * Reads an authorization request from a JSON string representation produced by * {@link #jsonSerialize()}. @@ -1089,10 +1093,8 @@ public static AuthorizationRequest jsonDeserialize(@NonNull String jsonStr) return jsonDeserialize(new JSONObject(jsonStr)); } - private static String generateRandomState() { - SecureRandom sr = new SecureRandom(); - byte[] random = new byte[STATE_LENGTH]; - sr.nextBytes(random); - return Base64.encodeToString(random, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); + static boolean isAuthorizationRequest(JSONObject json) { + return json.has(KEY_REDIRECT_URI); } + } diff --git a/library/java/net/openid/appauth/AuthorizationResponse.java b/library/java/net/openid/appauth/AuthorizationResponse.java index 84aa6bd0..84df46fd 100644 --- a/library/java/net/openid/appauth/AuthorizationResponse.java +++ b/library/java/net/openid/appauth/AuthorizationResponse.java @@ -46,7 +46,7 @@ * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.2 * " */ -public class AuthorizationResponse { +public class AuthorizationResponse extends AuthorizationManagementResponse { /** * The extra string used to store an {@link AuthorizationResponse} in an intent by @@ -543,11 +543,18 @@ public static AuthorizationResponse jsonDeserialize(@NonNull String jsonStr) return jsonDeserialize(new JSONObject(jsonStr)); } + @Override + @Nullable + public String getState() { + return state; + } + /** * Produces an intent containing this authorization response. This is used to deliver the * authorization response to the registered handler after a call to * {@link AuthorizationService#performAuthorizationRequest}. */ + @Override @NonNull public Intent toIntent() { Intent data = new Intent(); @@ -573,4 +580,8 @@ public static AuthorizationResponse fromIntent(@NonNull Intent dataIntent) { throw new IllegalArgumentException("Intent contains malformed auth response", ex); } } + + static boolean containEndSessionResoponse(@NonNull Intent intent) { + return intent.hasExtra(EXTRA_RESPONSE); + } } diff --git a/library/java/net/openid/appauth/AuthorizationService.java b/library/java/net/openid/appauth/AuthorizationService.java index 3fd5c054..6e6e89d9 100644 --- a/library/java/net/openid/appauth/AuthorizationService.java +++ b/library/java/net/openid/appauth/AuthorizationService.java @@ -225,6 +225,103 @@ public void performAuthorizationRequest( @NonNull PendingIntent completedIntent, @Nullable PendingIntent canceledIntent, @NonNull CustomTabsIntent customTabsIntent) { + performAuthManagementRequest(request,completedIntent, canceledIntent, customTabsIntent); + } + + /** + * Sends an end session request to the authorization service, using a + * [custom tab](https://developer.chrome.com/multidevice/android/customtabs) + * if available, or a browser instance. + * The parameters of this request are determined by both the authorization service + * configuration and the provided {@link EndSessionRequest request object}. Upon completion + * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. + * If the user cancels the authorization request, the current activity will regain control. + */ + public void performEndOfSessionRequest( @NonNull EndSessionRequest request, + @NonNull PendingIntent completedIntent) { + performEndOfSessionRequest( + request, + completedIntent, + null, + createCustomTabsIntentBuilder().build()); + + } + + /** + * Sends an end session request to the authorization service, using a + * [custom tab](https://developer.chrome.com/multidevice/android/customtabs) + * if available, or a browser instance. + * The parameters of this request are determined by both the authorization service + * configuration and the provided {@link EndSessionRequest request object}. Upon completion + * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. + * If the user cancels the authorization request, the provided + * {@link PendingIntent cancel PendingIntent} will be invoked. + */ + public void performEndOfSessionRequest(@NonNull EndSessionRequest request, + @NonNull PendingIntent completedIntent, + @NonNull PendingIntent canceledIntent) { + performEndOfSessionRequest( + request, + completedIntent, + canceledIntent, + createCustomTabsIntentBuilder().build()); + } + + /** + * Sends an end session request to the authorization service, using a + * [custom tab](https://developer.chrome.com/multidevice/android/customtabs). + * The parameters of this request are determined by both the authorization service + * configuration and the provided {@link EndSessionRequest request object}. Upon completion + * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. + * If the user cancels the authorization request, the current activity will regain control. + * + * @param customTabsIntent + * The intent that will be used to start the custom tab. It is recommended that this intent + * be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will + * ensure that a warmed-up version of the browser will be used, minimizing latency. + */ + public void performEndOfSessionRequest(@NonNull EndSessionRequest request, + @NonNull PendingIntent completedIntent, + @NonNull CustomTabsIntent customTabsIntent) { + performEndOfSessionRequest( + request, + completedIntent, + null, + customTabsIntent); + + } + + /** + * Sends an end session request to the authorization service, using a + * [custom tab](https://developer.chrome.com/multidevice/android/customtabs). + * The parameters of this request are determined by both the authorization service + * configuration and the provided {@link EndSessionRequest request object}. Upon completion + * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked. + * If the user cancels the authorization request, the provided + * {@link PendingIntent cancel PendingIntent} will be invoked. + * + * @param customTabsIntent + * The intent that will be used to start the custom tab. It is recommended that this intent + * be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will + * ensure that a warmed-up version of the browser will be used, minimizing latency. + * + * @throws android.content.ActivityNotFoundException if no suitable browser is available to + * perform the authorization flow. + */ + public void performEndOfSessionRequest( + @NonNull EndSessionRequest request, + @NonNull PendingIntent completedIntent, + @Nullable PendingIntent canceledIntent, + @NonNull CustomTabsIntent customTabsIntent) { + performAuthManagementRequest(request,completedIntent, canceledIntent, customTabsIntent); + } + + private void performAuthManagementRequest( + @NonNull AuthorizationManagementRequest request, + @NonNull PendingIntent completedIntent, + @Nullable PendingIntent canceledIntent, + @NonNull CustomTabsIntent customTabsIntent) { + checkNotDisposed(); checkNotNull(request); checkNotNull(completedIntent); @@ -237,6 +334,7 @@ public void performAuthorizationRequest( authIntent, completedIntent, canceledIntent)); + } /** @@ -297,6 +395,64 @@ public Intent getAuthorizationRequestIntent( return getAuthorizationRequestIntent(request, createCustomTabsIntentBuilder().build()); } + /** + * Constructs an intent that encapsulates the provided request and custom tabs intent, + * and is intended to be launched via {@link Activity#startActivityForResult}. + * The parameters of this request are determined by both the authorization service + * configuration and the provided {@link AuthorizationRequest request object}. Upon completion + * of this request, the activity that gets launched will call {@link Activity#setResult} with + * {@link Activity#RESULT_OK} and an {@link Intent} containing authorization completion + * information. If the user presses the back button or closes the browser tab, the launched + * activity will call {@link Activity#setResult} with + * {@link Activity#RESULT_CANCELED} without a data {@link Intent}. Note that + * {@link Activity#RESULT_OK} indicates the authorization request completed, + * not necessarily that it was a successful authorization. + * + * @param customTabsIntent + * The intent that will be used to start the custom tab. It is recommended that this intent + * be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will + * ensure that a warmed-up version of the browser will be used, minimizing latency. + * + * @throws android.content.ActivityNotFoundException if no suitable browser is available to + * perform the authorization flow. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Intent getEndSessionRequestIntent( + @NonNull EndSessionRequest request, + @NonNull CustomTabsIntent customTabsIntent) { + + Intent authIntent = prepareAuthorizationRequestIntent(request, customTabsIntent); + return AuthorizationManagementActivity.createStartForResultIntent( + mContext, + request, + authIntent); + } + + /** + * Constructs an intent that encapsulates the provided request and a default custom tabs intent, + * and is intended to be launched via {@link Activity#startActivityForResult} + * When started, the intent launches an {@link Activity} that sends an authorization request + * to the authorization service, using a + * [custom tab](https://developer.chrome.com/multidevice/android/customtabs). + * The parameters of this request are determined by both the authorization service + * configuration and the provided {@link EndSessionRequest request object}. Upon completion + * of this request, the activity that gets launched will call {@link Activity#setResult} with + * {@link Activity#RESULT_OK} and an {@link Intent} containing authorization completion + * information. If the user presses the back button or closes the browser tab, the launched + * activity will call {@link Activity#setResult} with + * {@link Activity#RESULT_CANCELED} without a data {@link Intent}. Note that + * {@link Activity#RESULT_OK} indicates the authorization request completed, + * not necessarily that it was a successful authorization. + * + * @throws android.content.ActivityNotFoundException if no suitable browser is available to + * perform the authorization flow. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Intent getEndSessionRequestIntent( + @NonNull EndSessionRequest request) { + return getEndSessionRequestIntent(request, createCustomTabsIntentBuilder().build()); + } + /** * Sends a request to the authorization service to exchange a code granted as part of an * authorization request for a token. The result of this request will be sent to the provided @@ -366,7 +522,7 @@ private void checkNotDisposed() { } private Intent prepareAuthorizationRequestIntent( - AuthorizationRequest request, + AuthorizationManagementRequest request, CustomTabsIntent customTabsIntent) { checkNotDisposed(); @@ -388,8 +544,9 @@ private Intent prepareAuthorizationRequestIntent( intent.getPackage(), mBrowser.useCustomTab.toString()); - Logger.debug("Initiating authorization request to %s", - request.configuration.authorizationEndpoint); + //TODO fix logger for configuration + //Logger.debug("Initiating authorization request to %s" + //request.configuration.authorizationEndpoint); return intent; } diff --git a/library/java/net/openid/appauth/AuthorizationServiceConfiguration.java b/library/java/net/openid/appauth/AuthorizationServiceConfiguration.java index 5d2b0e9a..f73a7a2c 100644 --- a/library/java/net/openid/appauth/AuthorizationServiceConfiguration.java +++ b/library/java/net/openid/appauth/AuthorizationServiceConfiguration.java @@ -63,6 +63,7 @@ public class AuthorizationServiceConfiguration { private static final String KEY_TOKEN_ENDPOINT = "tokenEndpoint"; private static final String KEY_REGISTRATION_ENDPOINT = "registrationEndpoint"; private static final String KEY_DISCOVERY_DOC = "discoveryDoc"; + private static final String KEY_END_SESSION_ENPOINT = "endSessionEndpoint"; /** * The authorization service's endpoint. @@ -76,6 +77,12 @@ public class AuthorizationServiceConfiguration { @NonNull public final Uri tokenEndpoint; + /** + * The end session service's endpoint; + */ + @Nullable + public final Uri endSessionEndpoint; + /** * The authorization service's client registration endpoint. */ @@ -101,28 +108,31 @@ public class AuthorizationServiceConfiguration { public AuthorizationServiceConfiguration( @NonNull Uri authorizationEndpoint, @NonNull Uri tokenEndpoint) { - this(authorizationEndpoint, tokenEndpoint, null); + this(authorizationEndpoint, tokenEndpoint, null, null); } /** * Creates a service configuration for a basic OAuth2 provider. - * - * @param authorizationEndpoint The + * @param authorizationEndpoint The * [authorization endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.1) * for the service. * @param tokenEndpoint The * [token endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.2) * for the service. + * @param endSessionEndpoint The + * [end session endpoint URI](https://tools.ietf.org/html/rfc6749#section-2.2) + * for the service. * @param registrationEndpoint The optional * [client registration endpoint URI](https://tools.ietf.org/html/rfc7591#section-3) - * for the service. */ public AuthorizationServiceConfiguration( @NonNull Uri authorizationEndpoint, @NonNull Uri tokenEndpoint, + @Nullable Uri endSessionEndpoint, @Nullable Uri registrationEndpoint) { this.authorizationEndpoint = checkNotNull(authorizationEndpoint); this.tokenEndpoint = checkNotNull(tokenEndpoint); + this.endSessionEndpoint = endSessionEndpoint; this.registrationEndpoint = registrationEndpoint; this.discoveryDoc = null; } @@ -140,6 +150,7 @@ public AuthorizationServiceConfiguration( this.authorizationEndpoint = discoveryDoc.getAuthorizationEndpoint(); this.tokenEndpoint = discoveryDoc.getTokenEndpoint(); this.registrationEndpoint = discoveryDoc.getRegistrationEndpoint(); + this.endSessionEndpoint = discoveryDoc.getEndSessionEndpoint(); } /** @@ -155,6 +166,8 @@ public JSONObject toJson() { } if (discoveryDoc != null) { JsonUtil.put(json, KEY_DISCOVERY_DOC, discoveryDoc.docJson); + } if (endSessionEndpoint != null) { + JsonUtil.put(json, KEY_END_SESSION_ENPOINT, endSessionEndpoint.toString()); } return json; } @@ -193,7 +206,9 @@ public static AuthorizationServiceConfiguration fromJson(@NonNull JSONObject jso return new AuthorizationServiceConfiguration( JsonUtil.getUri(json, KEY_AUTHORIZATION_ENDPOINT), JsonUtil.getUri(json, KEY_TOKEN_ENDPOINT), - JsonUtil.getUriIfDefined(json, KEY_REGISTRATION_ENDPOINT)); + JsonUtil.getUriIfDefined(json, KEY_END_SESSION_ENPOINT), + JsonUtil.getUriIfDefined(json, + KEY_REGISTRATION_ENDPOINT)); } } diff --git a/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java b/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java index f74e0927..3c799643 100644 --- a/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java +++ b/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java @@ -51,6 +51,9 @@ public class AuthorizationServiceDiscovery { @VisibleForTesting static final UriField TOKEN_ENDPOINT = uri("token_endpoint"); + @VisibleForTesting + static final UriField END_SESSION_ENPDINT = uri("end_session_endpoint"); + @VisibleForTesting static final UriField USERINFO_ENDPOINT = uri("userinfo_endpoint"); @@ -259,6 +262,13 @@ public Uri getTokenEndpoint() { return get(TOKEN_ENDPOINT); } + /** + * The OAuth 2 emd session endpoint URI. Not specified test OAuth implementation + */ + public Uri getEndSessionEndpoint() { + return get(END_SESSION_ENPDINT); + } + /** * The OpenID Connect UserInfo endpoint URI. */ diff --git a/library/java/net/openid/appauth/EndSessionRequest.java b/library/java/net/openid/appauth/EndSessionRequest.java new file mode 100644 index 00000000..a642f24d --- /dev/null +++ b/library/java/net/openid/appauth/EndSessionRequest.java @@ -0,0 +1,170 @@ +/* + * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openid.appauth; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * An OpenID end session request. + * + * NOTE: That is a draft implementation + * + * @see "OpenID Connect Session Management 1.0 - draft 28, 5 RP-Initiated Logout + * " + */ +public class EndSessionRequest extends AuthorizationManagementRequest { + + private static final String PARAM_ID_TOKEN_HINT = "id_token_hint"; + private static final String PARAM_REDIECT_URI = "post_logout_redirect_uri"; + private static final String PARAM_STATE = "state"; + private static final String KEY_CONFIGURATION = "configuration"; + + @VisibleForTesting + static final String KEY_ID_TOKEN_HINT = "id_token_hint"; + @VisibleForTesting + static final String KEY_REDIECT_URI = "post_logout_redirect_uri"; + @VisibleForTesting + static final String KEY_STATE = "state"; + + /** + * The service's {@link AuthorizationServiceConfiguration configuration}. + * This configuration specifies how to connect to a particular OAuth provider. + * Configurations may be + * {@link + * AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri, Uri, Uri, Uri)} + * created manually}, or {@link AuthorizationServiceConfiguration#fetchFromUrl(Uri, + * AuthorizationServiceConfiguration.RetrieveConfigurationCallback)} via an OpenID Connect + * Discovery Document}. + */ + @NonNull + public final AuthorizationServiceConfiguration configuration; + + /** + * An OpenID Connect ID Token. Contains claims about the authentication of an End-User by an + * Authorization Server. + * + * @see "OpenID Connect Session Management 1.0 - draft 28, 5 RP-Initiated Logout + * " + * @see "OpenID Connect Core ID Token, Section 2 + * " + */ + @NonNull + public final String idToken; + + /** + * The client's redirect URI. + * + * @see "OpenID Connect Session Management 1.0 - draft 28, 5.1. Redirection to RP After Logout + * " + */ + @NonNull + public final Uri redirectUri; + + /** + * An opaque value used by the client to maintain state between the request and callback. If + * this value is not explicitly set, this library will automatically add state and perform + * appropriate validation of the state in the authorization response. It is recommended that + * the default implementation of this parameter be used wherever possible. Typically used to + * prevent CSRF attacks, as recommended in + * + * @see "OpenID Connect Session Management 1.0 - draft 28, 5 RP-Initiated Logout + * " + * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 5.3.5 + * " + */ + @NonNull + public final String state; + + @VisibleForTesting + EndSessionRequest(@NonNull AuthorizationServiceConfiguration configuration, + @NonNull String idToken, @NonNull Uri redirectUri, + @NonNull String state) { + Preconditions.checkNotNull(configuration, "Configuration can not be null"); + Preconditions.checkNotNull(idToken, "IdToken can not be null"); + Preconditions.checkNotNull(redirectUri, "RedirectUri can not be null"); + Preconditions.checkNotNull(state, "State can not be null"); + this.configuration = configuration; + this.idToken = idToken; + this.redirectUri = redirectUri; + this.state = state; + } + + /** + * Creates new EndSessionRequest with mandatory parameters + */ + public EndSessionRequest(@NonNull AuthorizationServiceConfiguration configuration, @NonNull + String idToken, @NonNull Uri redirectUri) { + this(configuration, idToken, redirectUri, + AuthorizationManagementRequest.generateRandomState()); + } + + @Override + public JSONObject jsonSerialize() { + JSONObject json = new JSONObject(); + JsonUtil.put(json, KEY_CONFIGURATION, configuration.toJson()); + JsonUtil.put(json, KEY_ID_TOKEN_HINT, idToken); + JsonUtil.put(json, KEY_REDIECT_URI, redirectUri.toString()); + JsonUtil.put(json, KEY_STATE, state); + return json; + } + + + @Override + public String jsonSerializeString() { + return jsonSerialize().toString(); + } + + @Override + public String getState() { + return state; + } + + @Override + public Uri toUri() { + Uri.Builder uriBuilder = configuration.endSessionEndpoint.buildUpon() + .appendQueryParameter(PARAM_REDIECT_URI, redirectUri.toString()) + .appendQueryParameter(PARAM_ID_TOKEN_HINT, idToken) + .appendQueryParameter(PARAM_STATE, state); + return uriBuilder.build(); + } + + public static EndSessionRequest jsonDeserialize(@NonNull JSONObject jsonObject) + throws JSONException { + Preconditions.checkNotNull(jsonObject, "json cannot be null"); + return new EndSessionRequest( + AuthorizationServiceConfiguration.fromJson(jsonObject.getJSONObject(KEY_CONFIGURATION)), + JsonUtil.getString(jsonObject, KEY_ID_TOKEN_HINT), + JsonUtil.getUri(jsonObject, KEY_REDIECT_URI), + JsonUtil.getString(jsonObject, KEY_STATE) + ); + } + + @NonNull + public static EndSessionRequest jsonDeserialize(@NonNull String jsonStr) + throws JSONException { + Preconditions.checkNotNull(jsonStr, "json string cannot be null"); + return jsonDeserialize(new JSONObject(jsonStr)); + } + + static boolean isEndSessionRequest(JSONObject json) { + return json.has(KEY_REDIECT_URI); + } + +} diff --git a/library/java/net/openid/appauth/EndSessionResponse.java b/library/java/net/openid/appauth/EndSessionResponse.java new file mode 100644 index 00000000..916531b0 --- /dev/null +++ b/library/java/net/openid/appauth/EndSessionResponse.java @@ -0,0 +1,166 @@ +/* + * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openid.appauth; + +import static net.openid.appauth.Preconditions.checkNotNull; + +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * A response to end session request. + * + * @see EndSessionRequest + * @see "OpenID Connect Session Management 1.0 - draft 28, 5 RP-Initiated Logout + * " + */ +public class EndSessionResponse extends AuthorizationManagementResponse { + + /** + * The extra string used to store an {@link EndSessionResponse} in an intent by + * {@link #toIntent()}. + */ + public static final String EXTRA_RESPONSE = "net.openid.appauth.EndSessionResponse"; + + @VisibleForTesting + static final String KEY_REQUEST = "request"; + + @VisibleForTesting + static final String KEY_STATE = "state"; + + /** + * The end session reques associated with this response. + */ + @NonNull + public final EndSessionRequest endSessionRequest; + + /** + * The returned state parameter, which must match the value specified in the request. + * AppAuth for Android ensures that this is the case. + */ + @NonNull + public final String state; + + EndSessionResponse(@NonNull EndSessionRequest endSessionRequest, @NonNull String state) { + Preconditions.checkNotNull(endSessionRequest); + Preconditions.checkNotNull(state); + this.endSessionRequest = endSessionRequest; + this.state = state; + } + + @Override + public String getState() { + return state; + } + + /** + * Produces an intent containing this end session response. This is used to deliver the + * end session response to the registered handler after a call to + * {@link AuthorizationService#performEndOfSessionRequest}. + */ + @Override + public Intent toIntent() { + Intent data = new Intent(); + data.putExtra(EXTRA_RESPONSE, this.jsonSerialize().toString()); + return data; + } + + /** + * Produces a JSON representation of the authorization response for persistent storage or local + * transmission (e.g. between activities). + */ + @NonNull + public JSONObject jsonSerialize() { + JSONObject json = new JSONObject(); + JsonUtil.put(json, KEY_REQUEST, endSessionRequest.jsonSerialize()); + JsonUtil.putIfNotNull(json, KEY_STATE, state); + return json; + } + + public static EndSessionResponse fromRequestAndUri(@NonNull EndSessionRequest request, + @NonNull Uri uri) { + checkNotNull(request, "request can not be null"); + checkNotNull(uri, "uri can not be null"); + return new EndSessionResponse(request, uri.getQueryParameter(EndSessionRequest.KEY_STATE)); + } + + /** + * Extracts an end session response from an intent produced by {@link #toIntent()}. This is + * used to extract the response from the intent data passed to an activity registered as the + * handler for {@link AuthorizationService#performEndOfSessionRequest}. + */ + @Nullable + public static EndSessionResponse fromIntent(@NonNull Intent dataIntent) { + checkNotNull(dataIntent, "dataIntent must not be null"); + if (!dataIntent.hasExtra(EXTRA_RESPONSE)) { + return null; + } + + try { + return EndSessionResponse + .jsonDeserializeString(dataIntent.getStringExtra(EXTRA_RESPONSE)); + } catch (JSONException ex) { + throw new IllegalArgumentException("Intent contains malformed auth response", ex); + } + } + + /** + * Reads an authorization response from a JSON string representation produced by + * {@link #jsonSerialize()}. + * + * @throws JSONException if the provided JSON does not match the expected structure. + */ + @NonNull + public static EndSessionResponse jsonDeserializeString(@NonNull String jsonStr) + throws JSONException { + return jsonDeserializeString(new JSONObject(jsonStr)); + } + + /** + * Reads an authorization request from a JSON representation produced by + * {@link #jsonSerialize()} converted to String. This method is just a convenience wrapper for + * {@link #jsonDeserializeString(JSONObject)}, + * converting the JSON string to its JSON object form. + * + * @throws JSONException if the provided JSON does not match the expected structure. + */ + @NonNull + public static EndSessionResponse jsonDeserializeString(@NonNull JSONObject json) + throws JSONException { + if (!json.has(KEY_REQUEST)) { + throw new IllegalArgumentException( + "authorization request not provided and not found in JSON"); + } + + EndSessionRequest request = + EndSessionRequest.jsonDeserialize(json.getJSONObject(KEY_REQUEST)); + + return new EndSessionResponse( + request, + JsonUtil.getString(json, KEY_STATE) + ); + } + + + static boolean containsEndSessionResoponse(@NonNull Intent intent) { + return intent.hasExtra(EXTRA_RESPONSE); + } +} diff --git a/library/java/net/openid/appauth/RegistrationRequest.java b/library/java/net/openid/appauth/RegistrationRequest.java index d2c577fc..acc09193 100644 --- a/library/java/net/openid/appauth/RegistrationRequest.java +++ b/library/java/net/openid/appauth/RegistrationRequest.java @@ -79,8 +79,9 @@ public class RegistrationRequest { * The service's {@link AuthorizationServiceConfiguration configuration}. * This configuration specifies how to connect to a particular OAuth provider. * Configurations may be - * {@link AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri, - * Uri, Uri) created manually}, or + * {@link + * AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri, Uri, Uri, Uri) + * created manually}, or * {@link AuthorizationServiceConfiguration#fetchFromUrl(Uri, * AuthorizationServiceConfiguration.RetrieveConfigurationCallback) * via an OpenID Connect Discovery Document}. diff --git a/library/java/net/openid/appauth/TokenRequest.java b/library/java/net/openid/appauth/TokenRequest.java index e86fcb0b..ab7aef4e 100644 --- a/library/java/net/openid/appauth/TokenRequest.java +++ b/library/java/net/openid/appauth/TokenRequest.java @@ -118,8 +118,9 @@ public class TokenRequest { * The service's {@link AuthorizationServiceConfiguration configuration}. * This configuration specifies how to connect to a particular OAuth provider. * Configurations may be - * {@link AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri, - * Uri, Uri) created manually}, or + * {@link + * AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri, Uri, Uri, Uri) + * created manually}, or * {@link AuthorizationServiceConfiguration#fetchFromUrl(Uri, * AuthorizationServiceConfiguration.RetrieveConfigurationCallback) * via an OpenID Connect Discovery Document}. diff --git a/library/javatests/net/openid/appauth/AuthorizationManagementActivityTest.java b/library/javatests/net/openid/appauth/AuthorizationManagementActivityTest.java index 84245036..f7c6f843 100644 --- a/library/javatests/net/openid/appauth/AuthorizationManagementActivityTest.java +++ b/library/javatests/net/openid/appauth/AuthorizationManagementActivityTest.java @@ -41,23 +41,30 @@ public class AuthorizationManagementActivityTest { private AuthorizationRequest mAuthRequest; + private EndSessionRequest mEndSessionRequest; private Intent mAuthIntent; private Intent mCompleteIntent; private PendingIntent mCompletePendingIntent; private Intent mCancelIntent; private PendingIntent mCancelPendingIntent; - private Intent mStartIntentWithPendings; - private Intent mStartIntentWithPendingsWithoutCancel; - private Intent mStartForResultIntent; + private Intent mStartAuthIntentWithPendings; + private Intent mStartAuthIntentWithPendingsWithoutCancel; + private Intent mStartAuthForResultIntent; + private Intent mEndSessionIntentWithPendings; + private Intent mEndSessionIntentWithPendingsWithoutCancel; + private Intent mEndSessionForResultIntent; private ActivityController mController; private AuthorizationManagementActivity mActivity; private ShadowActivity mActivityShadow; private Uri mSuccessAuthRedirect; private Uri mErrorAuthRedirect; + private Uri mSuccessEndSessionRedirect; + private Uri mErrorEndSessionRedirect; @Before public void setUp() { mAuthRequest = TestValues.getTestAuthRequest(); + mEndSessionRequest = TestValues.getTestEndSessionRequest(); mAuthIntent = new Intent("AUTH"); mCompleteIntent = new Intent("COMPLETE"); @@ -67,17 +74,28 @@ public void setUp() { mCancelPendingIntent = PendingIntent.getActivity(RuntimeEnvironment.application, 0, mCancelIntent, 0); - mStartIntentWithPendings = + mStartAuthIntentWithPendings = createStartIntentWithPendingIntents(mAuthRequest, mCancelPendingIntent); - mStartIntentWithPendingsWithoutCancel = + mStartAuthIntentWithPendingsWithoutCancel = createStartIntentWithPendingIntents(mAuthRequest, null); - mStartForResultIntent = createStartForResultIntent(mAuthRequest); + mStartAuthForResultIntent = createStartForResultIntent(mAuthRequest); + + mEndSessionIntentWithPendings = + createStartIntentWithPendingIntents(mEndSessionRequest, mCancelPendingIntent); + mEndSessionIntentWithPendingsWithoutCancel = + createStartIntentWithPendingIntents(mEndSessionRequest, null); + mEndSessionForResultIntent = + createStartForResultIntent(mEndSessionRequest); mSuccessAuthRedirect = mAuthRequest.redirectUri.buildUpon() - .appendQueryParameter(AuthorizationResponse.KEY_STATE, mAuthRequest.state) + .appendQueryParameter(AuthorizationResponse.KEY_STATE, mAuthRequest.getState()) .appendQueryParameter(AuthorizationResponse.KEY_AUTHORIZATION_CODE, "12345") .build(); + mSuccessEndSessionRedirect = mEndSessionRequest.redirectUri.buildUpon() + .appendQueryParameter(EndSessionRequest.KEY_STATE, mEndSessionRequest.getState()) + .build(); + mErrorAuthRedirect = mAuthRequest.redirectUri.buildUpon() .appendQueryParameter( AuthorizationException.PARAM_ERROR, @@ -87,11 +105,19 @@ public void setUp() { AuthorizationRequestErrors.ACCESS_DENIED.errorDescription) .build(); - instantiateActivity(mStartIntentWithPendings); + mErrorEndSessionRedirect = mEndSessionRequest.redirectUri.buildUpon() + .appendQueryParameter(AuthorizationException.PARAM_ERROR, + AuthorizationRequestErrors.ACCESS_DENIED.error) + .appendQueryParameter( + AuthorizationException.PARAM_ERROR_DESCRIPTION, + AuthorizationRequestErrors.ACCESS_DENIED.errorDescription) + .build(); + + instantiateActivity(mStartAuthIntentWithPendings); } private Intent createStartIntentWithPendingIntents( - AuthorizationRequest authRequest, + AuthorizationManagementRequest authRequest, PendingIntent cancelIntent) { return AuthorizationManagementActivity.createStartIntent( RuntimeEnvironment.application, @@ -102,7 +128,7 @@ private Intent createStartIntentWithPendingIntents( } private Intent createStartForResultIntent( - AuthorizationRequest authRequest) { + AuthorizationManagementRequest authRequest) { return AuthorizationManagementActivity.createStartForResultIntent( RuntimeEnvironment.application, authRequest, @@ -119,9 +145,9 @@ private void instantiateActivity(Intent managementIntent) { } @Test - public void testSuccessFlow_withPendingIntentsAndWithoutDestroy_shouldSendCompleteIntent() { + public void testLoginSuccessFlow_withPendingIntentsAndWithoutDestroy_shouldSendCompleteIntent() { // start the flow - instantiateActivity(mStartIntentWithPendings); + instantiateActivity(mStartAuthIntentWithPendings); mController.create().start().resume(); // an activity should be started for auth @@ -149,9 +175,38 @@ public void testSuccessFlow_withPendingIntentsAndWithoutDestroy_shouldSendComple } @Test - public void testSuccessFlow_withoutPendingIntentsAndWithoutDestroy_shouldSetResult() { + public void testEndOfSessionSuccessFlow_withPendingIntentsAndWithoutDestroy_shouldSendCompleteIntent() { + // start the flow + instantiateActivity(mEndSessionIntentWithPendings); + mController.create().start().resume(); + + // an activity should be started for auth + assertThat(mActivityShadow.getNextStartedActivity()).hasAction("AUTH"); + + // the management activity will be paused while the authorization flow is running. + // if there is no memory pressure, the activity will remain in the paused state. + mController.pause(); + + // on completion of the authorization activity, the result will be forwarded to the + // management activity via newIntent + mController.newIntent(AuthorizationManagementActivity.createResponseHandlingIntent( + RuntimeEnvironment.application, + mSuccessEndSessionRedirect)); + + // the management activity is then resumed + mController.resume(); + + // after which the completion intent should be fired + assertThat(mActivityShadow.getNextStartedActivity()) + .hasAction("COMPLETE") + .hasData(mSuccessEndSessionRedirect); + assertThat(mActivity).isFinishing(); + } + + @Test + public void testLoginSuccessFlow_withoutPendingIntentsAndWithoutDestroy_shouldSetResult() { // start the flow - instantiateActivity(mStartForResultIntent); + instantiateActivity(mStartAuthForResultIntent); mController.create().start().resume(); // an activity should be started for auth @@ -176,9 +231,36 @@ public void testSuccessFlow_withoutPendingIntentsAndWithoutDestroy_shouldSetResu } @Test - public void testSuccessFlow_withPendingIntentsAndWithDestroy_shouldSendCompleteIntent() { + public void testEndSessionSuccessFlow_withoutPendingIntentsAndWithoutDestroy_shouldSetResult() { // start the flow - instantiateActivity(mStartIntentWithPendings); + instantiateActivity(mEndSessionForResultIntent); + mController.create().start().resume(); + + // an activity should be started for auth + assertThat(mActivityShadow.getNextStartedActivity()).hasAction("AUTH"); + + // the management activity will be paused while the authorization flow is running. + // if there is no memory pressure, the activity will remain in the paused state. + mController.pause(); + + // on completion of the authorization activity, the result will be forwarded to the + // management activity via newIntent + mController.newIntent(AuthorizationManagementActivity.createResponseHandlingIntent( + RuntimeEnvironment.application, + mSuccessEndSessionRedirect)); + + // the management activity is then resumed + mController.resume(); + + // and then sets a result before finishing as there is no completion intent + assertThat(mActivityShadow.getResultCode()).isEqualTo(RESULT_OK); + assertThat(mActivity).isFinishing(); + } + + @Test + public void testLoginSuccessFlow_withPendingIntentsAndWithDestroy_shouldSendCompleteIntent() { + // start the flow + instantiateActivity(mStartAuthIntentWithPendings); mController.create().start().resume(); // an activity should be started for auth @@ -190,7 +272,7 @@ public void testSuccessFlow_withPendingIntentsAndWithDestroy_shouldSendCompleteI mController.pause().stop().saveInstanceState(savedState).destroy(); // on completion of the authorization activity, a new management activity will be created - instantiateActivity(mStartIntentWithPendings); + instantiateActivity(mStartAuthIntentWithPendings); mController.create(savedState).start(); // the authorization redirect will be forwarded via a new intent @@ -210,9 +292,42 @@ public void testSuccessFlow_withPendingIntentsAndWithDestroy_shouldSendCompleteI } @Test - public void testSuccessFlow_withoutPendingIntentsAndWithDestroy_shouldSetResult() { + public void testEndSessionSuccessFlow_withPendingIntentsAndWithDestroy_shouldSendCompleteIntent() { + // start the flow + instantiateActivity(mEndSessionIntentWithPendings); + mController.create().start().resume(); + + // an activity should be started for auth + assertThat(mActivityShadow.getNextStartedActivity()).hasAction("AUTH"); + + // on a device under memory pressure, the management activity will be destroyed, but + // it will be able to save its state + Bundle savedState = new Bundle(); + mController.pause().stop().saveInstanceState(savedState).destroy(); + + // on completion of the authorization activity, a new management activity will be created + instantiateActivity(mStartAuthIntentWithPendings); + mController.create(savedState).start(); + + // the authorization redirect will be forwarded via a new intent + mController.newIntent(AuthorizationManagementActivity.createResponseHandlingIntent( + RuntimeEnvironment.application, + mSuccessAuthRedirect)); + + // the management activity is then resumed + mController.resume(); + + // after which the completion intent should be fired + assertThat(mActivityShadow.getNextStartedActivity()) + .hasAction("COMPLETE") + .hasData(mSuccessAuthRedirect); + assertThat(mActivity).isFinishing(); + } + + @Test + public void testLoginSuccessFlow_withoutPendingIntentsAndWithDestroy_shouldSetResult() { // start the flow - instantiateActivity(mStartForResultIntent); + instantiateActivity(mStartAuthForResultIntent); mController.create().start().resume(); // an activity should be started for auth @@ -224,7 +339,7 @@ public void testSuccessFlow_withoutPendingIntentsAndWithDestroy_shouldSetResult( mController.pause().stop().saveInstanceState(savedState).destroy(); // on completion of the authorization activity, a new management activity will be created - instantiateActivity(mStartIntentWithPendings); + instantiateActivity(mStartAuthIntentWithPendings); mController.create(savedState).start(); // the authorization redirect will be forwarded via a new intent @@ -241,9 +356,40 @@ public void testSuccessFlow_withoutPendingIntentsAndWithDestroy_shouldSetResult( } @Test - public void testFailureFlow_withPendingIntentsAndWithoutDestroy_shouldSendCompleteIntent() { + public void testEndSessionSuccessFlow_withoutPendingIntentsAndWithDestroy_shouldSetResult() { + // start the flow + instantiateActivity(mEndSessionForResultIntent); + mController.create().start().resume(); + + // an activity should be started for auth + assertThat(mActivityShadow.getNextStartedActivity()).hasAction("AUTH"); + + // on a device under memory pressure, the management activity will be destroyed, but + // it will be able to save its state + Bundle savedState = new Bundle(); + mController.pause().stop().saveInstanceState(savedState).destroy(); + + // on completion of the authorization activity, a new management activity will be created + instantiateActivity(mStartAuthIntentWithPendings); + mController.create(savedState).start(); + + // the authorization redirect will be forwarded via a new intent + mController.newIntent(AuthorizationManagementActivity.createResponseHandlingIntent( + RuntimeEnvironment.application, + mSuccessEndSessionRedirect)); + + // the management activity is then resumed + mController.resume(); + + // and then sets a result before finishing as there is no completion intent + assertThat(mActivityShadow.getResultCode()).isEqualTo(RESULT_OK); + assertThat(mActivity).isFinishing(); + } + + @Test + public void testLoginFailureFlow_withPendingIntentsAndWithoutDestroy_shouldSendCompleteIntent() { // start the flow - instantiateActivity(mStartIntentWithPendings); + instantiateActivity(mStartAuthIntentWithPendings); mController.create().start().resume(); // an activity should be started for auth @@ -270,9 +416,38 @@ public void testFailureFlow_withPendingIntentsAndWithoutDestroy_shouldSendComple } @Test - public void testFailureFlow_withoutPendingIntentsAndWithoutDestroy_shouldSetResult() { + public void testEndSessionFailureFlow_withPendingIntentsAndWithoutDestroy_shouldSendCompleteIntent() { // start the flow - instantiateActivity(mStartForResultIntent); + instantiateActivity(mEndSessionIntentWithPendings); + mController.create().start().resume(); + + // an activity should be started for auth + assertThat(mActivityShadow.getNextStartedActivity()).hasAction("AUTH"); + + // the management activity will be paused while the authorization flow is running. + // if there is no memory pressure, the activity will remain in the paused state. + mController.pause(); + + // the authorization redirect will be forwarded via a new intent + mController.newIntent(AuthorizationManagementActivity.createResponseHandlingIntent( + RuntimeEnvironment.application, + mErrorEndSessionRedirect)); + + // the management activity is then resumed + mController.resume(); + + // after which the completion intent should be fired + assertThat(mActivityShadow.getNextStartedActivity()) + .hasAction("COMPLETE") + .hasData(mErrorEndSessionRedirect) + .hasExtra(AuthorizationException.EXTRA_EXCEPTION); + assertThat(mActivity).isFinishing(); + } + + @Test + public void testLoginFailureFlow_withoutPendingIntentsAndWithoutDestroy_shouldSetResult() { + // start the flow + instantiateActivity(mStartAuthForResultIntent); mController.create().start().resume(); // an activity should be started for auth @@ -298,8 +473,36 @@ public void testFailureFlow_withoutPendingIntentsAndWithoutDestroy_shouldSetResu } @Test - public void testMismatchedState_withPendingIntentsAndResponseDiffersFromRequest() { - emulateFlowToAuthorizationActivityLaunch(mStartIntentWithPendings); + public void testEndSessionFailureFlow_withoutPendingIntentsAndWithoutDestroy_shouldSetResult() { + // start the flow + instantiateActivity(mEndSessionForResultIntent); + mController.create().start().resume(); + + // an activity should be started for auth + assertThat(mActivityShadow.getNextStartedActivity()).hasAction("AUTH"); + + // the management activity will be paused while the authorization flow is running. + // if there is no memory pressure, the activity will remain in the paused state. + mController.pause(); + + // the authorization redirect will be forwarded via a new intent + mController.newIntent(AuthorizationManagementActivity.createResponseHandlingIntent( + RuntimeEnvironment.application, + mErrorAuthRedirect)); + + // the management activity is then resumed + mController.resume(); + + // after which the completion intent should be fired + assertThat(mActivityShadow.getResultCode()).isEqualTo(RESULT_OK); + Intent resultIntent = mActivityShadow.getResultIntent(); + assertThat(resultIntent.getData()).isEqualTo(mErrorEndSessionRedirect); + assertThat(mActivity).isFinishing(); + } + + @Test + public void testLoginMismatchedState_withPendingIntentsAndResponseDiffersFromRequest() { + emulateFlowToAuthorizationActivityLaunch(mStartAuthIntentWithPendings); Uri authResponseUri = mAuthRequest.redirectUri.buildUpon() .appendQueryParameter(AuthorizationResponse.KEY_STATE, "differentState") @@ -322,8 +525,31 @@ public void testMismatchedState_withPendingIntentsAndResponseDiffersFromRequest( } @Test - public void testMismatchedState_withoutPendingIntentsAndResponseDiffersFromRequest() { - emulateFlowToAuthorizationActivityLaunch(mStartForResultIntent); + public void testEndSessionnMismatchedState_withPendingIntentsAndResponseDiffersFromRequest() { + emulateFlowToAuthorizationActivityLaunch(mEndSessionIntentWithPendings); + + Uri authResponseUri = mEndSessionRequest.redirectUri.buildUpon() + .appendQueryParameter(EndSessionRequest.KEY_STATE, "differentState") + .build(); + + Intent nextStartedActivity = emulateAuthorizationResponseReceived( + AuthorizationManagementActivity.createResponseHandlingIntent( + RuntimeEnvironment.application, + authResponseUri)); + + // the next activity should be from the completion intent, carrying an error + assertThat(nextStartedActivity) + .hasAction("COMPLETE") + .hasData(authResponseUri) + .hasExtra(AuthorizationException.EXTRA_EXCEPTION); + + assertThat(AuthorizationException.fromIntent(nextStartedActivity)) + .isEqualTo(AuthorizationRequestErrors.STATE_MISMATCH); + } + + @Test + public void testLoginMismatchedState_withoutPendingIntentsAndResponseDiffersFromRequest() { + emulateFlowToAuthorizationActivityLaunch(mStartAuthForResultIntent); Uri authResponseUri = mAuthRequest.redirectUri.buildUpon() .appendQueryParameter(AuthorizationResponse.KEY_STATE, "differentState") @@ -351,7 +577,35 @@ public void testMismatchedState_withoutPendingIntentsAndResponseDiffersFromReque } @Test - public void testMismatchedState_withPendingIntentsAndNoStateInRequestWithStateInResponse() { + public void testEndSessionMismatchedState_withoutPendingIntentsAndResponseDiffersFromRequest() { + emulateFlowToAuthorizationActivityLaunch(mEndSessionForResultIntent); + + Uri authResponseUri = mEndSessionRequest.redirectUri.buildUpon() + .appendQueryParameter(AuthorizationResponse.KEY_STATE, "differentState") + .build(); + + // the authorization redirect will be forwarded via a new intent + mController.newIntent(AuthorizationManagementActivity.createResponseHandlingIntent( + RuntimeEnvironment.application, + authResponseUri)); + + // the management activity is then resumed + mController.resume(); + + // no completion intent, so exception should be passed to calling activity + // via the result intent supplied to setResult + Intent resultIntent = mActivityShadow.getResultIntent(); + + assertThat(resultIntent) + .hasData(authResponseUri) + .hasExtra(AuthorizationException.EXTRA_EXCEPTION); + + assertThat(AuthorizationException.fromIntent(resultIntent)) + .isEqualTo(AuthorizationRequestErrors.STATE_MISMATCH); + } + + @Test + public void testLoginMismatchedState_withPendingIntentsAndNoStateInRequestWithStateInResponse() { AuthorizationRequest request = new AuthorizationRequest.Builder( TestValues.getTestServiceConfig(), TestValues.TEST_CLIENT_ID, @@ -379,7 +633,33 @@ public void testMismatchedState_withPendingIntentsAndNoStateInRequestWithStateIn } @Test - public void testMismatchedState_withoutPendingIntentsAndNoStateInRequestWithStateInResponse() { + public void testEndSessionMismatchedState_withPendingIntentsAndNoStateInRequestWithStateInResponse() { + EndSessionRequest request = new EndSessionRequest( + TestValues.getTestServiceConfig(), + TestValues.TEST_ID_TOKEN, + TestValues.TEST_APP_REDIRECT_URI, + "state" + ); + + Intent startIntent = createStartIntentWithPendingIntents(request, mCancelPendingIntent); + emulateFlowToAuthorizationActivityLaunch(startIntent); + + Uri authResponseUri = mEndSessionRequest.redirectUri.buildUpon() + .appendQueryParameter(AuthorizationResponse.KEY_STATE, "differentState") + .build(); + + // the next activity should be from the completion intent, carrying an error + Intent nextStartedActivity = emulateAuthorizationResponseReceived( + AuthorizationManagementActivity.createResponseHandlingIntent( + RuntimeEnvironment.application, + authResponseUri)); + + assertThat(AuthorizationException.fromIntent(nextStartedActivity)) + .isEqualTo(AuthorizationRequestErrors.STATE_MISMATCH); + } + + @Test + public void testLoginMismatchedState_withoutPendingIntentsAndNoStateInRequestWithStateInResponse() { AuthorizationRequest request = new AuthorizationRequest.Builder( TestValues.getTestServiceConfig(), TestValues.TEST_CLIENT_ID, @@ -410,19 +690,58 @@ public void testMismatchedState_withoutPendingIntentsAndNoStateInRequestWithStat } @Test - public void testInvalidResponse() { - emulateFlowToAuthorizationActivityLaunch(mStartIntentWithPendings); + public void testEndSessionMismatchedState_withoutPendingIntentsAndNoStateInRequestWithStateInResponse() { + EndSessionRequest request = new EndSessionRequest( + TestValues.getTestServiceConfig(), + TestValues.TEST_ID_TOKEN, + TestValues.TEST_APP_REDIRECT_URI, + "state" + ); - Uri authResponseUri = mAuthRequest.redirectUri.buildUpon() - .appendQueryParameter(AuthorizationResponse.KEY_STATE, "differentState") - .appendQueryParameter(AuthorizationResponse.KEY_AUTHORIZATION_CODE, "12345") - .build(); + Intent startIntent = createStartForResultIntent(request); + emulateFlowToAuthorizationActivityLaunch(startIntent); + + Uri authResponseUri = mEndSessionRequest.redirectUri.buildUpon() + .appendQueryParameter(AuthorizationResponse.KEY_STATE, "differentState") + .build(); + + // the authorization redirect will be forwarded via a new intent + mController.newIntent(AuthorizationManagementActivity.createResponseHandlingIntent( + RuntimeEnvironment.application, + authResponseUri)); + + // the management activity is then resumed + mController.resume(); + + Intent resultIntent = mActivityShadow.getResultIntent(); + assertThat(AuthorizationException.fromIntent(resultIntent)) + .isEqualTo(AuthorizationRequestErrors.STATE_MISMATCH); + } + + @Test + public void testLoginCancelFlow_withPendingIntentsAndWithoutDestroy() { + // start the flow + instantiateActivity(mStartAuthIntentWithPendings); + mController.create().start().resume(); + + // an activity should be started for auth + assertThat(mActivityShadow.getNextStartedActivity()).isNotNull(); + + // the management activity will be paused while this auth intent is running + mController.pause(); + + // when the user cancels the auth intent, the management activity will be resumed + mController.resume(); + + // at which point the cancel intent should be fired + assertThat(mActivityShadow.getNextStartedActivity()).hasAction("CANCEL"); + assertThat(mActivity).isFinishing(); } @Test - public void testCancelFlow_withPendingIntentsAndWithoutDestroy() { + public void testEndSessionCancelFlow_withPendingIntentsAndWithoutDestroy() { // start the flow - instantiateActivity(mStartIntentWithPendings); + instantiateActivity(mEndSessionIntentWithPendings); mController.create().start().resume(); // an activity should be started for auth @@ -440,9 +759,9 @@ public void testCancelFlow_withPendingIntentsAndWithoutDestroy() { } @Test - public void testCancelFlow_withoutPendingIntentsAndWithoutDestroy() { + public void testLoginCancelFlow_withoutPendingIntentsAndWithoutDestroy() { // start the flow - instantiateActivity(mStartForResultIntent); + instantiateActivity(mStartAuthForResultIntent); mController.create().start().resume(); // an activity should be started for auth @@ -467,9 +786,56 @@ public void testCancelFlow_withoutPendingIntentsAndWithoutDestroy() { } @Test - public void testCancelFlow_withCompletionIntentButNoCancelIntent() { + public void testEndSessionCancelFlow_withoutPendingIntentsAndWithoutDestroy() { + // start the flow + instantiateActivity(mEndSessionForResultIntent); + mController.create().start().resume(); + + // an activity should be started for auth + assertThat(mActivityShadow.getNextStartedActivity()).isNotNull(); + + // the management activity will be paused while this auth intent is running + mController.pause(); + + // when the user cancels the auth intent, the management activity will be resumed + mController.resume(); + + // at which point the cancel intent should be fired + assertThat(mActivityShadow.getResultCode()).isEqualTo(RESULT_CANCELED); + + Intent resultIntent = mActivityShadow.getResultIntent(); + assertThat(resultIntent).hasExtra(AuthorizationException.EXTRA_EXCEPTION); + + assertThat(AuthorizationException.fromIntent(resultIntent)) + .isEqualTo(AuthorizationException.GeneralErrors.USER_CANCELED_AUTH_FLOW); + + assertThat(mActivity).isFinishing(); + } + + @Test + public void testLoginCancelFlow_withCompletionIntentButNoCancelIntent() { + // start the flow + instantiateActivity(mStartAuthIntentWithPendingsWithoutCancel); + mController.create().start().resume(); + + // an activity should be started for auth + assertThat(mActivityShadow.getNextStartedActivity()).isNotNull(); + + // the management activity will be paused while this auth intent is running + mController.pause(); + + // when the user cancels the auth intent, the management activity will be resumed + mController.resume(); + + // as there is no cancel intent, the activity simply finishes + assertThat(mActivityShadow.getNextStartedActivity()).isNull(); + assertThat(mActivity).isFinishing(); + } + + @Test + public void testEndSessionCancelFlow_withCompletionIntentButNoCancelIntent() { // start the flow - instantiateActivity(mStartIntentWithPendingsWithoutCancel); + instantiateActivity(mEndSessionIntentWithPendingsWithoutCancel); mController.create().start().resume(); // an activity should be started for auth diff --git a/library/javatests/net/openid/appauth/AuthorizationRequestTest.java b/library/javatests/net/openid/appauth/AuthorizationRequestTest.java index b45e7698..68426177 100644 --- a/library/javatests/net/openid/appauth/AuthorizationRequestTest.java +++ b/library/javatests/net/openid/appauth/AuthorizationRequestTest.java @@ -604,7 +604,7 @@ public void testToUri_additionalParams() throws Exception { .isEqualTo("5678"); } - /* ************************** jsonSerialize() / jsonDeserialize() *****************************/ + /* ************************** jsonSerialize() / jsonDeserializeString() *****************************/ @Test public void testJsonSerialize_clientId() throws Exception { diff --git a/library/javatests/net/openid/appauth/AuthorizationServiceConfigurationTest.java b/library/javatests/net/openid/appauth/AuthorizationServiceConfigurationTest.java index e3eb10de..9e99731f 100644 --- a/library/javatests/net/openid/appauth/AuthorizationServiceConfigurationTest.java +++ b/library/javatests/net/openid/appauth/AuthorizationServiceConfigurationTest.java @@ -52,6 +52,7 @@ public class AuthorizationServiceConfigurationTest { private static final String TEST_ISSUER = "test_issuer"; private static final String TEST_AUTH_ENDPOINT = "https://test.openid.com/o/oauth/auth"; private static final String TEST_TOKEN_ENDPOINT = "https://test.openid.com/o/oauth/token"; + private static final String TEST_END_SESSION_ENDPOINT = "https://test.openid.com/o/oauth/logout"; private static final String TEST_REGISTRATION_ENDPOINT = "https://test.openid.com/o/oauth/registration"; private static final String TEST_USERINFO_ENDPOINT = "https://test.openid.com/o/oauth/userinfo"; private static final String TEST_JWKS_URI = "https://test.openid.com/o/oauth/jwks"; @@ -116,6 +117,7 @@ public void setUp() throws Exception { mConfig = new AuthorizationServiceConfiguration( Uri.parse(TEST_AUTH_ENDPOINT), Uri.parse(TEST_TOKEN_ENDPOINT), + Uri.parse(TEST_END_SESSION_ENDPOINT), Uri.parse(TEST_REGISTRATION_ENDPOINT)); when(mConnectionBuilder.openConnection(any(Uri.class))).thenReturn(mHttpConnection); } @@ -137,11 +139,25 @@ public void testSerializationWithoutRegistrationEndpoint() throws Exception { AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration( Uri.parse(TEST_AUTH_ENDPOINT), Uri.parse(TEST_TOKEN_ENDPOINT), - null); + Uri.parse(TEST_END_SESSION_ENDPOINT), null); AuthorizationServiceConfiguration deserialized = AuthorizationServiceConfiguration .fromJson(config.toJson()); assertThat(deserialized.authorizationEndpoint).isEqualTo(config.authorizationEndpoint); assertThat(deserialized.tokenEndpoint).isEqualTo(config.tokenEndpoint); + assertThat(deserialized.endSessionEndpoint).isEqualTo(config.endSessionEndpoint); + assertThat(deserialized.registrationEndpoint).isNull(); + } + + @Test + public void testSerializationWithoutRegistrationEndpointAndEndSessionEndpoint() throws Exception { + AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration( + Uri.parse(TEST_AUTH_ENDPOINT), + Uri.parse(TEST_TOKEN_ENDPOINT)); + AuthorizationServiceConfiguration deserialized = AuthorizationServiceConfiguration + .fromJson(config.toJson()); + assertThat(deserialized.authorizationEndpoint).isEqualTo(config.authorizationEndpoint); + assertThat(deserialized.tokenEndpoint).isEqualTo(config.tokenEndpoint); + assertThat(deserialized.endSessionEndpoint).isNull(); assertThat(deserialized.registrationEndpoint).isNull(); } diff --git a/library/javatests/net/openid/appauth/AuthorizationServiceDiscoveryTest.java b/library/javatests/net/openid/appauth/AuthorizationServiceDiscoveryTest.java index 4441e064..b449b514 100644 --- a/library/javatests/net/openid/appauth/AuthorizationServiceDiscoveryTest.java +++ b/library/javatests/net/openid/appauth/AuthorizationServiceDiscoveryTest.java @@ -38,6 +38,7 @@ public class AuthorizationServiceDiscoveryTest { static final String TEST_TOKEN_ENDPOINT = "http://test.openid.com/o/oauth/token"; static final String TEST_USERINFO_ENDPOINT = "http://test.openid.com/o/oauth/userinfo"; static final String TEST_REGISTRATION_ENDPOINT = "http://test.openid.com/o/oauth/register"; + static final String TEST_END_OF_SESSION_ENDPOINT = "http://test.openid.com/o/oauth/logout"; static final String TEST_JWKS_URI = "http://test.openid.com/o/oauth/jwks"; static final List TEST_RESPONSE_TYPES_SUPPORTED = Arrays.asList("code", "token"); static final List TEST_SUBJECT_TYPES_SUPPORTED = Arrays.asList("public"); @@ -53,6 +54,7 @@ public class AuthorizationServiceDiscoveryTest { TEST_TOKEN_ENDPOINT, TEST_USERINFO_ENDPOINT, TEST_REGISTRATION_ENDPOINT, + TEST_END_OF_SESSION_ENDPOINT, TEST_JWKS_URI, TEST_RESPONSE_TYPES_SUPPORTED, TEST_SUBJECT_TYPES_SUPPORTED, diff --git a/library/javatests/net/openid/appauth/AuthorizationServiceTest.java b/library/javatests/net/openid/appauth/AuthorizationServiceTest.java index 078c5046..2c0c588d 100644 --- a/library/javatests/net/openid/appauth/AuthorizationServiceTest.java +++ b/library/javatests/net/openid/appauth/AuthorizationServiceTest.java @@ -60,6 +60,7 @@ import static net.openid.appauth.AuthorizationManagementActivity.KEY_CANCEL_INTENT; import static net.openid.appauth.AuthorizationManagementActivity.KEY_COMPLETE_INTENT; import static net.openid.appauth.TestValues.TEST_ACCESS_TOKEN; +import static net.openid.appauth.TestValues.TEST_APP_REDIRECT_URI; import static net.openid.appauth.TestValues.TEST_CLIENT_ID; import static net.openid.appauth.TestValues.TEST_CLIENT_SECRET; import static net.openid.appauth.TestValues.TEST_CLIENT_SECRET_EXPIRES_AT; @@ -70,8 +71,10 @@ import static net.openid.appauth.TestValues.getTestAuthCodeExchangeRequest; import static net.openid.appauth.TestValues.getTestAuthCodeExchangeRequestBuilder; import static net.openid.appauth.TestValues.getTestAuthRequestBuilder; +import static net.openid.appauth.TestValues.getTestEndSessionRequest; import static net.openid.appauth.TestValues.getTestIdTokenWithNonce; import static net.openid.appauth.TestValues.getTestRegistrationRequest; +import static net.openid.appauth.TestValues.getTestServiceConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -157,6 +160,15 @@ public void testAuthorizationRequest_withSpecifiedState() throws Exception { assertEquals(request.toUri().toString(), intent.getData().toString()); } + @Test + public void testEndSessionRequest_withSpecifiedState() throws Exception { + EndSessionRequest request = new EndSessionRequest(getTestServiceConfig(), TEST_ID_TOKEN, TEST_APP_REDIRECT_URI, TEST_STATE); + mService.performEndOfSessionRequest(request, mPendingIntent); + Intent intent = captureAuthRequestIntent(); + assertRequestIntent(intent, null); + assertEquals(request.toUri().toString(), intent.getData().toString()); + } + @Test public void testAuthorizationRequest_withSpecifiedNonce() throws Exception { AuthorizationRequest request = getTestAuthRequestBuilder() @@ -189,12 +201,31 @@ public void testAuthorizationRequest_customization() throws Exception { assertColorMatch(intent, Color.GREEN); } + @Test + public void testEndSessionRequest_customization() throws Exception { + CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder() + .setToolbarColor(Color.GREEN) + .build(); + mService.performEndOfSessionRequest( + getTestEndSessionRequest(), + mPendingIntent, + customTabsIntent); + Intent intent = captureAuthRequestIntent(); + assertColorMatch(intent, Color.GREEN); + } + @Test(expected = IllegalStateException.class) public void testAuthorizationRequest_afterDispose() throws Exception { mService.dispose(); mService.performAuthorizationRequest(getTestAuthRequestBuilder().build(), mPendingIntent); } + @Test(expected = IllegalStateException.class) + public void testEndSessionRequest_afterDispose() throws Exception { + mService.dispose(); + mService.performEndOfSessionRequest(getTestEndSessionRequest(), mPendingIntent); + } + @Test public void testGetAuthorizationRequestIntent_preservesRequest() { AuthorizationRequest request = getTestAuthRequestBuilder().build(); diff --git a/library/javatests/net/openid/appauth/EndSessionRequestTest.java b/library/javatests/net/openid/appauth/EndSessionRequestTest.java new file mode 100644 index 00000000..137fad8d --- /dev/null +++ b/library/javatests/net/openid/appauth/EndSessionRequestTest.java @@ -0,0 +1,81 @@ +package net.openid.appauth; + +import android.net.Uri; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk=16) +public class EndSessionRequestTest { + + + @Test(expected = NullPointerException.class) + public void buildWithNulConfiguration_NullPointerException() { + new EndSessionRequest(null, TestValues.TEST_ID_TOKEN, TestValues.TEST_APP_REDIRECT_URI); + } + + @Test(expected = NullPointerException.class) + public void buildWithNulIdToken_NullPointerException() { + new EndSessionRequest(TestValues.getTestServiceConfig(), null, TestValues.TEST_APP_REDIRECT_URI); + } + + @Test(expected = NullPointerException.class) + public void buildWithNullRedirectUri_NullPointerException() { + new EndSessionRequest(TestValues.getTestServiceConfig(), TestValues.TEST_ID_TOKEN, null); + } + + @Test + public void getState_notNull() { + EndSessionRequest request = new EndSessionRequest(TestValues.getTestServiceConfig(), TestValues.TEST_ID_TOKEN, TestValues.TEST_APP_REDIRECT_URI); + assertNotNull(request.getState()); + } + + @Test + public void testToUri() { + EndSessionRequest request = TestValues.getTestEndSessionRequest(); + Uri requestUri = request.toUri(); + + assertThat(requestUri.getQueryParameter(EndSessionRequest.KEY_ID_TOKEN_HINT)) + .isEqualTo(request.idToken); + assertThat(requestUri.getQueryParameter(EndSessionRequest.KEY_REDIECT_URI)) + .isEqualTo(request.redirectUri.toString()); + assertThat(requestUri.getQueryParameter(EndSessionRequest.KEY_STATE)) + .isEqualTo(request.state); + + } + + @Test + public void testJsonSerialize() throws Exception { + EndSessionRequest resquest = TestValues.getTestEndSessionRequest(); + EndSessionRequest copy = serializeDeserialize(resquest); + assertThat(copy.idToken).isEqualTo(TestValues.TEST_ID_TOKEN); + assertThat(copy.state).isEqualTo(resquest.state); + assertThat(copy.redirectUri).isEqualTo(resquest.redirectUri); + } + + @Test + public void testIsEndSessionRequestSuccess() { + JSONObject json = TestValues.getTestEndSessionRequest().jsonSerialize(); + assertTrue(EndSessionRequest.isEndSessionRequest(json)); + } + + @Test + public void testIsEndSessionRequestFailure() { + JSONObject json = TestValues.getTestAuthRequestBuilder().build().jsonSerialize(); + assertFalse(EndSessionRequest.isEndSessionRequest(json)); + } + + private EndSessionRequest serializeDeserialize(EndSessionRequest request) + throws JSONException { + return EndSessionRequest.jsonDeserialize(request.jsonSerializeString()); + } + +} diff --git a/library/javatests/net/openid/appauth/EndSessionResponseTest.java b/library/javatests/net/openid/appauth/EndSessionResponseTest.java new file mode 100644 index 00000000..02b4e98b --- /dev/null +++ b/library/javatests/net/openid/appauth/EndSessionResponseTest.java @@ -0,0 +1,125 @@ +package net.openid.appauth; + +import android.content.Intent; +import android.net.Uri; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk=16) +public class EndSessionResponseTest { + + private static final EndSessionRequest TEST_REQUEST = TestValues.getTestEndSessionRequest(); + + + @Test(expected = NullPointerException.class) + public void testEndSessionNew_NullRequest(){ + new EndSessionResponse(null, TEST_REQUEST.state); + } + + @Test(expected = NullPointerException.class) + public void testEndSessionNew_NullState(){ + new EndSessionResponse(TEST_REQUEST, null); + } + + @Test + public void testIntentSerializeDeserialize(){ + EndSessionResponse endSessionResponse = new EndSessionResponse(TEST_REQUEST, TEST_REQUEST.state); + + Intent endSessionIntent = endSessionResponse.toIntent(); + + EndSessionResponse deserializeResponse = EndSessionResponse.fromIntent(endSessionIntent); + + assertThat(deserializeResponse).isNotNull(); + assertThat(deserializeResponse.state).isEqualTo(endSessionResponse.endSessionRequest.state); + assertThat(deserializeResponse.endSessionRequest.redirectUri) + .isEqualTo(endSessionResponse.endSessionRequest.redirectUri); + assertThat(deserializeResponse.endSessionRequest.configuration.endSessionEndpoint) + .isEqualTo(endSessionResponse.endSessionRequest.configuration.endSessionEndpoint); + assertThat(deserializeResponse.endSessionRequest.state) + .isEqualTo(endSessionResponse.endSessionRequest.state); + assertThat(deserializeResponse.endSessionRequest.idToken) + .isEqualTo(endSessionResponse.endSessionRequest.idToken); + } + + @Test + public void testIntentSerializeNull(){ + EndSessionResponse deserializeResponse = EndSessionResponse.fromIntent(new Intent()); + assertThat(deserializeResponse).isNull(); + } + + @Test (expected = IllegalArgumentException.class) + public void testIntentDeserializeError(){ + Intent intent = new Intent(); + intent.putExtra(EndSessionResponse.EXTRA_RESPONSE, ""); + EndSessionResponse.fromIntent(intent); + } + + @Test + public void testJsonSerializeDeserialize() throws JSONException { + EndSessionResponse endSessionResponse = + new EndSessionResponse(TEST_REQUEST, TEST_REQUEST.state); + + JSONObject endSessionResponseJson = endSessionResponse.jsonSerialize(); + + EndSessionResponse deserializeResponse = + EndSessionResponse.jsonDeserializeString(endSessionResponseJson); + + assertThat(deserializeResponse).isNotNull(); + assertThat(deserializeResponse.state).isEqualTo(endSessionResponse.state); + assertThat(deserializeResponse.endSessionRequest.redirectUri) + .isEqualTo(endSessionResponse.endSessionRequest.redirectUri); + assertThat(deserializeResponse.endSessionRequest.configuration.endSessionEndpoint) + .isEqualTo(endSessionResponse.endSessionRequest.configuration.endSessionEndpoint); + assertThat(deserializeResponse.endSessionRequest.state) + .isEqualTo(endSessionResponse.endSessionRequest.state); + assertThat(deserializeResponse.endSessionRequest.idToken) + .isEqualTo(endSessionResponse.endSessionRequest.idToken); + } + + @Test + public void testFromRequestAndUri_Success(){ + EndSessionResponse endSessionResponse = + new EndSessionResponse(TEST_REQUEST, TEST_REQUEST.state); + + Uri endSessionUri = new Uri.Builder() + .appendQueryParameter(EndSessionResponse.KEY_STATE, TEST_REQUEST.state) + .build(); + + EndSessionResponse endSessionResponseDeserialized = + EndSessionResponse.fromRequestAndUri(TEST_REQUEST, endSessionUri); + assertThat(endSessionResponseDeserialized).isNotNull(); + assertThat(endSessionResponseDeserialized.state).isEqualTo(endSessionResponse.state); + assertThat(endSessionResponseDeserialized.endSessionRequest.redirectUri) + .isEqualTo(endSessionResponse.endSessionRequest.redirectUri); + assertThat(endSessionResponseDeserialized.endSessionRequest.configuration.endSessionEndpoint) + .isEqualTo(endSessionResponse.endSessionRequest.configuration.endSessionEndpoint); + assertThat(endSessionResponseDeserialized.endSessionRequest.state) + .isEqualTo(endSessionResponse.endSessionRequest.state); + assertThat(endSessionResponseDeserialized.endSessionRequest.idToken) + .isEqualTo(endSessionResponse.endSessionRequest.idToken); + } + + @Test + public void testIntent_containsEndSessionResponse_True() { + EndSessionResponse endSessionResponse = new EndSessionResponse(TEST_REQUEST, TEST_REQUEST.state); + + Intent endSessionIntent = endSessionResponse.toIntent(); + + assertThat(EndSessionResponse.containsEndSessionResoponse(endSessionIntent)).isTrue(); + } + + @Test + public void testIntent_containsEndSessionResponse_False() { + Intent intent = new Intent(); + assertThat(EndSessionResponse.containsEndSessionResoponse(intent)).isFalse(); + } + +} diff --git a/library/javatests/net/openid/appauth/IdTokenTest.java b/library/javatests/net/openid/appauth/IdTokenTest.java index 3579bbb7..46457183 100644 --- a/library/javatests/net/openid/appauth/IdTokenTest.java +++ b/library/javatests/net/openid/appauth/IdTokenTest.java @@ -18,6 +18,7 @@ import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_AUTHORIZATION_ENDPOINT; import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_CLAIMS_SUPPORTED; +import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_END_OF_SESSION_ENDPOINT; import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_ID_TOKEN_SIGNING_ALG_VALUES; import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_JWKS_URI; import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_REGISTRATION_ENDPOINT; @@ -369,6 +370,7 @@ private String getDiscoveryDocJsonWithIssuer(String issuer) { TEST_TOKEN_ENDPOINT, TEST_USERINFO_ENDPOINT, TEST_REGISTRATION_ENDPOINT, + TEST_END_OF_SESSION_ENDPOINT, TEST_JWKS_URI, TEST_RESPONSE_TYPES_SUPPORTED, TEST_SUBJECT_TYPES_SUPPORTED, diff --git a/library/javatests/net/openid/appauth/TestValues.java b/library/javatests/net/openid/appauth/TestValues.java index 8e8b10f7..363d8bf1 100644 --- a/library/javatests/net/openid/appauth/TestValues.java +++ b/library/javatests/net/openid/appauth/TestValues.java @@ -71,6 +71,7 @@ static String getDiscoveryDocumentJson( String tokenEndpoint, String userInfoEndpoint, String registrationEndpoint, + String endSessionEndpont, String jwksUri, List responseTypesSupported, List subjectTypesSupported, @@ -84,6 +85,7 @@ static String getDiscoveryDocumentJson( + " \"authorization_endpoint\": \"" + authorizationEndpoint + "\",\n" + " \"token_endpoint\": \"" + tokenEndpoint + "\",\n" + " \"userinfo_endpoint\": \"" + userInfoEndpoint + "\",\n" + + " \"end_session_endpoint\": \"" + endSessionEndpont + "\",\n" + " \"registration_endpoint\": \"" + registrationEndpoint + "\",\n" + " \"jwks_uri\": \"" + jwksUri + "\",\n" + " \"response_types_supported\": " + toJson(responseTypesSupported) + ",\n" @@ -130,6 +132,10 @@ public static AuthorizationRequest getTestAuthRequest() { build(); } + public static EndSessionRequest getTestEndSessionRequest() { + return new EndSessionRequest(getTestServiceConfig(),TEST_ID_TOKEN, TEST_APP_REDIRECT_URI); + } + public static AuthorizationResponse.Builder getTestAuthResponseBuilder() { AuthorizationRequest req = getTestAuthRequest(); return new AuthorizationResponse.Builder(req)