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}