001/* 002 * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 005 * in compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the 010 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 011 * express or implied. See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014 015package net.openid.appauth; 016 017import static net.openid.appauth.AdditionalParamsProcessor.builtInParams; 018import static net.openid.appauth.AdditionalParamsProcessor.checkAdditionalParams; 019import static net.openid.appauth.Preconditions.checkNotNull; 020import static net.openid.appauth.Preconditions.checkNullOrNotEmpty; 021 022import android.net.Uri; 023import androidx.annotation.NonNull; 024import androidx.annotation.Nullable; 025import androidx.annotation.VisibleForTesting; 026 027import net.openid.appauth.internal.UriUtil; 028import org.json.JSONException; 029import org.json.JSONObject; 030 031import java.util.Arrays; 032import java.util.Collections; 033import java.util.HashMap; 034import java.util.Map; 035import java.util.Set; 036 037/** 038 * An OpenID end session request. 039 * 040 * @see "OpenID Connect RP-Initiated Logout 1.0 - draft 01 041 * <https://openid.net/specs/openid-connect-rpinitiated-1_0.html>" 042 */ 043public class EndSessionRequest implements AuthorizationManagementRequest { 044 045 @VisibleForTesting 046 static final String PARAM_ID_TOKEN_HINT = "id_token_hint"; 047 048 @VisibleForTesting 049 static final String PARAM_POST_LOGOUT_REDIRECT_URI = "post_logout_redirect_uri"; 050 051 @VisibleForTesting 052 static final String PARAM_STATE = "state"; 053 054 @VisibleForTesting 055 static final String PARAM_UI_LOCALES = "ui_locales"; 056 057 private static final Set<String> BUILT_IN_PARAMS = builtInParams( 058 PARAM_ID_TOKEN_HINT, 059 PARAM_POST_LOGOUT_REDIRECT_URI, 060 PARAM_STATE, 061 PARAM_UI_LOCALES); 062 063 private static final String KEY_CONFIGURATION = "configuration"; 064 private static final String KEY_ID_TOKEN_HINT = "id_token_hint"; 065 private static final String KEY_POST_LOGOUT_REDIRECT_URI = "post_logout_redirect_uri"; 066 private static final String KEY_STATE = "state"; 067 private static final String KEY_UI_LOCALES = "ui_locales"; 068 private static final String KEY_ADDITIONAL_PARAMETERS = "additionalParameters"; 069 070 /** 071 * The service's {@link AuthorizationServiceConfiguration configuration}. 072 * This configuration specifies how to connect to a particular OAuth provider. 073 * Configurations may be 074 * {@link 075 * AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri, Uri, Uri, Uri)} 076 * created manually}, or {@link AuthorizationServiceConfiguration#fetchFromUrl(Uri, 077 * AuthorizationServiceConfiguration.RetrieveConfigurationCallback)} via an OpenID Connect 078 * Discovery Document}. 079 */ 080 @NonNull 081 public final AuthorizationServiceConfiguration configuration; 082 083 /** 084 * Previously issued ID Token passed to the end session endpoint as a hint about the End-User's 085 * current authenticated session with the Client 086 * 087 * @see "OpenID Connect Session Management 1.0 - draft 28, 5 RP-Initiated Logout 088 * <https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout>" 089 * @see "OpenID Connect Core ID Token, Section 2 090 * <http://openid.net/specs/openid-connect-core-1_0.html#IDToken>" 091 */ 092 @Nullable 093 public final String idTokenHint; 094 095 /** 096 * The client's redirect URI. 097 * 098 * @see "OpenID Connect RP-Initiated Logout 1.0 - draft 1, 3. Redirection to RP After Logout 099 * <https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RedirectionAfterLogout>" 100 */ 101 @Nullable 102 public final Uri postLogoutRedirectUri; 103 104 /** 105 * An opaque value used by the client to maintain state between the request and callback. If 106 * this value is not explicitly set, this library will automatically add state and perform 107 * appropriate validation of the state in the authorization response. It is recommended that 108 * the default implementation of this parameter be used wherever possible. Typically used to 109 * prevent CSRF attacks, as recommended in 110 * 111 * @see "OpenID Connect RP-Initiated Logout 1.0 - draft 1, 2. RP-Initiated Logout 112 * <https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout>" 113 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 5.3.5 114 * <https://tools.ietf.org/html/rfc6749#section-5.3.5>" 115 */ 116 @Nullable 117 public final String state; 118 119 /** 120 * This is a space-separated list of BCP47 [RFC5646] language tag values, ordered by preference. 121 * It represents End-User's preferred languages and scripts for the user interface. 122 * 123 * @see "OpenID Connect RP-Initiated Logout 1.0 - draft 01 124 * <https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout>" 125 */ 126 @Nullable 127 public final String uiLocales; 128 129 /** 130 * Additional parameters to be passed as part of the request. 131 * 132 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.1 133 * <https://tools.ietf.org/html/rfc6749#section-3.1>" 134 */ 135 @NonNull 136 public final Map<String, String> additionalParameters; 137 138 /** 139 * Creates instances of {@link EndSessionRequest}. 140 */ 141 public static final class Builder { 142 143 @NonNull 144 private AuthorizationServiceConfiguration mConfiguration; 145 146 @Nullable 147 private String mIdTokenHint; 148 149 @Nullable 150 private Uri mPostLogoutRedirectUri; 151 152 @Nullable 153 private String mState; 154 155 @Nullable 156 private String mUiLocales; 157 158 @NonNull 159 private Map<String, String> mAdditionalParameters = new HashMap<>(); 160 161 /** 162 * Creates an end-session request builder with the specified mandatory properties 163 * and preset value for {@link AuthorizationRequest#state}. 164 */ 165 public Builder(@NonNull AuthorizationServiceConfiguration configuration) { 166 setAuthorizationServiceConfiguration(configuration); 167 setState(AuthorizationManagementUtil.generateRandomState()); 168 } 169 170 /** @see EndSessionRequest#configuration */ 171 @NonNull 172 public Builder setAuthorizationServiceConfiguration( 173 @NonNull AuthorizationServiceConfiguration configuration) { 174 mConfiguration = checkNotNull(configuration, "configuration cannot be null"); 175 return this; 176 } 177 178 /** @see EndSessionRequest#idTokenHint */ 179 @NonNull 180 public Builder setIdTokenHint(@Nullable String idTokenHint) { 181 mIdTokenHint = checkNullOrNotEmpty(idTokenHint, "idTokenHint must not be empty"); 182 return this; 183 } 184 185 /** @see EndSessionRequest#postLogoutRedirectUri */ 186 @NonNull 187 public Builder setPostLogoutRedirectUri(@Nullable Uri postLogoutRedirectUri) { 188 mPostLogoutRedirectUri = postLogoutRedirectUri; 189 return this; 190 } 191 192 /** @see EndSessionRequest#state */ 193 @NonNull 194 public Builder setState(@Nullable String state) { 195 mState = checkNullOrNotEmpty(state, "state must not be empty"); 196 return this; 197 } 198 199 /** @see EndSessionRequest#uiLocales */ 200 @NonNull 201 public Builder setUiLocales(@Nullable String uiLocales) { 202 mUiLocales = checkNullOrNotEmpty(uiLocales, "uiLocales must be null or not empty"); 203 return this; 204 } 205 206 /** @see EndSessionRequest#uiLocales */ 207 @NonNull 208 public Builder setUiLocalesValues(@Nullable String... uiLocalesValues) { 209 if (uiLocalesValues == null) { 210 mUiLocales = null; 211 return this; 212 } 213 214 return setUiLocalesValues(Arrays.asList(uiLocalesValues)); 215 } 216 217 /** @see EndSessionRequest#uiLocales */ 218 @NonNull 219 public Builder setUiLocalesValues( 220 @Nullable Iterable<String> uiLocalesValues) { 221 mUiLocales = AsciiStringListUtil.iterableToString(uiLocalesValues); 222 return this; 223 } 224 225 /** @see EndSessionRequest#additionalParameters */ 226 @NonNull 227 public Builder setAdditionalParameters(@Nullable Map<String, String> additionalParameters) { 228 mAdditionalParameters = checkAdditionalParams(additionalParameters, BUILT_IN_PARAMS); 229 return this; 230 } 231 232 /** 233 * Constructs an end session request. All fields must be set. 234 * Failure to specify any of these parameters will result in a runtime exception. 235 */ 236 @NonNull 237 public EndSessionRequest build() { 238 return new EndSessionRequest( 239 mConfiguration, 240 mIdTokenHint, 241 mPostLogoutRedirectUri, 242 mState, 243 mUiLocales, 244 Collections.unmodifiableMap(new HashMap<>(mAdditionalParameters))); 245 } 246 } 247 248 private EndSessionRequest( 249 @NonNull AuthorizationServiceConfiguration configuration, 250 @Nullable String idTokenHint, 251 @Nullable Uri postLogoutRedirectUri, 252 @Nullable String state, 253 @Nullable String uiLocales, 254 @NonNull Map<String, String> additionalParameters) { 255 this.configuration = configuration; 256 this.idTokenHint = idTokenHint; 257 this.postLogoutRedirectUri = postLogoutRedirectUri; 258 this.state = state; 259 this.uiLocales = uiLocales; 260 this.additionalParameters = additionalParameters; 261 } 262 263 @Override 264 @Nullable 265 public String getState() { 266 return state; 267 } 268 269 public Set<String> getUiLocales() { 270 return AsciiStringListUtil.stringToSet(uiLocales); 271 } 272 273 @Override 274 public Uri toUri() { 275 Uri.Builder uriBuilder = configuration.endSessionEndpoint.buildUpon(); 276 277 UriUtil.appendQueryParameterIfNotNull(uriBuilder, PARAM_ID_TOKEN_HINT, idTokenHint); 278 UriUtil.appendQueryParameterIfNotNull(uriBuilder, PARAM_STATE, state); 279 UriUtil.appendQueryParameterIfNotNull(uriBuilder, PARAM_UI_LOCALES, uiLocales); 280 281 if (postLogoutRedirectUri != null) { 282 uriBuilder.appendQueryParameter(PARAM_POST_LOGOUT_REDIRECT_URI, 283 postLogoutRedirectUri.toString()); 284 } 285 286 for (Map.Entry<String, String> entry : additionalParameters.entrySet()) { 287 uriBuilder.appendQueryParameter(entry.getKey(), entry.getValue()); 288 } 289 290 return uriBuilder.build(); 291 } 292 293 /** 294 * Produces a JSON representation of the end session request for persistent storage or local 295 * transmission (e.g. between activities). 296 */ 297 @Override 298 public JSONObject jsonSerialize() { 299 JSONObject json = new JSONObject(); 300 JsonUtil.put(json, KEY_CONFIGURATION, configuration.toJson()); 301 JsonUtil.putIfNotNull(json, KEY_ID_TOKEN_HINT, idTokenHint); 302 JsonUtil.putIfNotNull(json, KEY_POST_LOGOUT_REDIRECT_URI, postLogoutRedirectUri); 303 JsonUtil.putIfNotNull(json, KEY_STATE, state); 304 JsonUtil.putIfNotNull(json, KEY_UI_LOCALES, uiLocales); 305 JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS, 306 JsonUtil.mapToJsonObject(additionalParameters)); 307 return json; 308 } 309 310 /** 311 * Produces a JSON string representation of the request for persistent storage or 312 * local transmission (e.g. between activities). This method is just a convenience wrapper 313 * for {@link #jsonSerialize()}, converting the JSON object to its string form. 314 */ 315 @Override 316 public String jsonSerializeString() { 317 return jsonSerialize().toString(); 318 } 319 320 /** 321 * Reads an authorization request from a JSON string representation produced by 322 * {@link #jsonSerialize()}. 323 * @throws JSONException if the provided JSON does not match the expected structure. 324 */ 325 public static EndSessionRequest jsonDeserialize(@NonNull JSONObject json) 326 throws JSONException { 327 checkNotNull(json, "json cannot be null"); 328 return new EndSessionRequest( 329 AuthorizationServiceConfiguration.fromJson(json.getJSONObject(KEY_CONFIGURATION)), 330 JsonUtil.getStringIfDefined(json, KEY_ID_TOKEN_HINT), 331 JsonUtil.getUriIfDefined(json, KEY_POST_LOGOUT_REDIRECT_URI), 332 JsonUtil.getStringIfDefined(json, KEY_STATE), 333 JsonUtil.getStringIfDefined(json, KEY_UI_LOCALES), 334 JsonUtil.getStringMap(json, KEY_ADDITIONAL_PARAMETERS)); 335 } 336 337 /** 338 * Reads an authorization request from a JSON string representation produced by 339 * {@link #jsonSerializeString()}. This method is just a convenience wrapper for 340 * {@link #jsonDeserialize(JSONObject)}, converting the JSON string to its JSON object form. 341 * @throws JSONException if the provided JSON does not match the expected structure. 342 */ 343 @NonNull 344 public static EndSessionRequest jsonDeserialize(@NonNull String jsonStr) 345 throws JSONException { 346 checkNotNull(jsonStr, "json string cannot be null"); 347 return jsonDeserialize(new JSONObject(jsonStr)); 348 } 349}