001/*
002 * Copyright 2015 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.checkAdditionalParams;
018import static net.openid.appauth.AdditionalParamsProcessor.extractAdditionalParams;
019import static net.openid.appauth.Preconditions.checkNotNull;
020import static net.openid.appauth.Preconditions.checkNullOrNotEmpty;
021
022import android.content.Intent;
023import android.net.Uri;
024import android.text.TextUtils;
025import androidx.annotation.NonNull;
026import androidx.annotation.Nullable;
027import androidx.annotation.VisibleForTesting;
028
029import net.openid.appauth.internal.UriUtil;
030import org.json.JSONException;
031import org.json.JSONObject;
032
033import java.util.Arrays;
034import java.util.Collections;
035import java.util.HashSet;
036import java.util.LinkedHashMap;
037import java.util.Map;
038import java.util.Set;
039import java.util.concurrent.TimeUnit;
040
041/**
042 * A response to an authorization request.
043 *
044 * @see AuthorizationRequest
045 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.2
046 * <https://tools.ietf.org/html/rfc6749#section-4.1.2>"
047 */
048public class AuthorizationResponse extends AuthorizationManagementResponse {
049
050    /**
051     * The extra string used to store an {@link AuthorizationResponse} in an intent by
052     * {@link #toIntent()}.
053     */
054    public static final String EXTRA_RESPONSE = "net.openid.appauth.AuthorizationResponse";
055
056    /**
057     * Indicates that a provided access token is a bearer token.
058     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 7.1 <https://tools.ietf.org/html/rfc6749#section-7.1>"
059     */
060    public static final String TOKEN_TYPE_BEARER = "bearer";
061
062    @VisibleForTesting
063    static final String KEY_REQUEST = "request";
064
065    @VisibleForTesting
066    static final String KEY_ADDITIONAL_PARAMETERS = "additional_parameters";
067
068    @VisibleForTesting
069    static final String KEY_EXPIRES_AT = "expires_at";
070
071    // TODO: rename all KEY_* below to PARAM_* - they are standard OAuth2 parameters
072    @VisibleForTesting
073    static final String KEY_STATE = "state";
074    @VisibleForTesting
075    static final String KEY_TOKEN_TYPE = "token_type";
076    @VisibleForTesting
077    static final String KEY_AUTHORIZATION_CODE = "code";
078    @VisibleForTesting
079    static final String KEY_ACCESS_TOKEN = "access_token";
080    @VisibleForTesting
081    static final String KEY_EXPIRES_IN = "expires_in";
082    @VisibleForTesting
083    static final String KEY_ID_TOKEN = "id_token";
084    @VisibleForTesting
085    static final String KEY_SCOPE = "scope";
086
087    private static final Set<String> BUILT_IN_PARAMS = Collections.unmodifiableSet(
088            new HashSet<>(Arrays.asList(
089                    KEY_TOKEN_TYPE,
090                    KEY_STATE,
091                    KEY_AUTHORIZATION_CODE,
092                    KEY_ACCESS_TOKEN,
093                    KEY_EXPIRES_IN,
094                    KEY_ID_TOKEN,
095                    KEY_SCOPE)));
096
097    /**
098     * The authorization request associated with this response.
099     */
100    @NonNull
101    public final AuthorizationRequest request;
102
103    /**
104     * The returned state parameter, which must match the value specified in the request.
105     * AppAuth for Android ensures that this is the case.
106     */
107    @Nullable
108    public final String state;
109
110    /**
111     * The type of the retrieved token. Typically this is "Bearer" when present. Otherwise,
112     * another token_type value that the Client has negotiated with the Authorization Server.
113     *
114     * @see "OpenID Connect Core 1.0, Section 3.2.2.5
115     * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.2.2.5>"
116     */
117    @Nullable
118    public final String tokenType;
119
120    /**
121     * The authorization code generated by the authorization server.
122     * Set when the response_type requested includes 'code'.
123     */
124    @Nullable
125    public final String authorizationCode;
126
127    /**
128     * The access token retrieved as part of the authorization flow.
129     * This is available when the {@link AuthorizationRequest#responseType response_type}
130     * of the request included 'token'.
131     *
132     * @see "OpenID Connect Core 1.0, Section 3.2.2.5
133     * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.2.2.5>"
134     */
135    @Nullable
136    public final String accessToken;
137
138    /**
139     * The approximate expiration time of the access token, as milliseconds from the UNIX epoch.
140     * Set when the requested {@link AuthorizationRequest#responseType response_type}
141     * included 'token'.
142     *
143     * @see "OpenID Connect Core 1.0, Section 3.2.2.5
144     * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.2.2.5>"
145     */
146    @Nullable
147    public final Long accessTokenExpirationTime;
148
149    /**
150     * The id token retrieved as part of the authorization flow.
151     * This is available when the {@link  AuthorizationRequest#responseType response_type}
152     * of the request included 'id_token'.
153     *
154     * @see "OpenID Connect Core 1.0, Section 2
155     * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.2>"
156     * @see "OpenID Connect Core 1.0, Section 3.2.2.5
157     * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.2.2.5>"
158     */
159    @Nullable
160    public final String idToken;
161
162    /**
163     * The scope of the returned access token. If this is not specified, the scope is assumed
164     * to be the same as what was originally requested.
165     */
166    @Nullable
167    public final String scope;
168
169    /**
170     * The additional, non-standard parameters in the response.
171     */
172    @NonNull
173    public final Map<String, String> additionalParameters;
174
175    /**
176     * Creates instances of {@link AuthorizationResponse}.
177     */
178    public static final class Builder {
179
180        @NonNull
181        private AuthorizationRequest mRequest;
182
183        @Nullable
184        private String mState;
185
186        @Nullable
187        private String mTokenType;
188
189        @Nullable
190        private String mAuthorizationCode;
191
192        @Nullable
193        private String mAccessToken;
194
195        @Nullable
196        private Long mAccessTokenExpirationTime;
197
198        @Nullable
199        private String mIdToken;
200
201        @Nullable
202        private String mScope;
203
204        @NonNull
205        private Map<String, String> mAdditionalParameters;
206
207        /**
208         * Creates an authorization builder with the specified mandatory properties.
209         */
210        public Builder(@NonNull AuthorizationRequest request) {
211            mRequest = checkNotNull(request, "authorization request cannot be null");
212            mAdditionalParameters = new LinkedHashMap<>();
213        }
214
215        /**
216         * Extracts authorization response parameters from the query portion of a redirect URI.
217         */
218        @NonNull
219        public Builder fromUri(@NonNull Uri uri) {
220            return fromUri(uri, SystemClock.INSTANCE);
221        }
222
223        @NonNull
224        @VisibleForTesting
225        Builder fromUri(@NonNull Uri uri, @NonNull Clock clock) {
226            setState(uri.getQueryParameter(KEY_STATE));
227            setTokenType(uri.getQueryParameter(KEY_TOKEN_TYPE));
228            setAuthorizationCode(uri.getQueryParameter(KEY_AUTHORIZATION_CODE));
229            setAccessToken(uri.getQueryParameter(KEY_ACCESS_TOKEN));
230            setAccessTokenExpiresIn(UriUtil.getLongQueryParameter(uri, KEY_EXPIRES_IN), clock);
231            setIdToken(uri.getQueryParameter(KEY_ID_TOKEN));
232            setScope(uri.getQueryParameter(KEY_SCOPE));
233            setAdditionalParameters(extractAdditionalParams(uri, BUILT_IN_PARAMS));
234            return this;
235        }
236
237        /**
238         * Specifies the OAuth 2 state.
239         */
240        @NonNull
241        public Builder setState(@Nullable String state) {
242            checkNullOrNotEmpty(state, "state must not be empty");
243            mState = state;
244            return this;
245        }
246
247        /**
248         * Specifies the OAuth 2 token type.
249         */
250        @NonNull
251        public Builder setTokenType(@Nullable String tokenType) {
252            checkNullOrNotEmpty(tokenType, "tokenType must not be empty");
253            mTokenType = tokenType;
254            return this;
255        }
256
257        /**
258         * Specifies the OAuth 2 authorization code.
259         */
260        @NonNull
261        public Builder setAuthorizationCode(@Nullable String authorizationCode) {
262            checkNullOrNotEmpty(authorizationCode, "authorizationCode must not be empty");
263            mAuthorizationCode = authorizationCode;
264            return this;
265        }
266
267        /**
268         * Specifies the OAuth 2 access token.
269         */
270        @NonNull
271        public Builder setAccessToken(@Nullable String accessToken) {
272            checkNullOrNotEmpty(accessToken, "accessToken must not be empty");
273            mAccessToken = accessToken;
274            return this;
275        }
276
277        /**
278         * Specifies the expiration period of the OAuth 2 access token.
279         */
280        @NonNull
281        public Builder setAccessTokenExpiresIn(@Nullable Long expiresIn) {
282            return setAccessTokenExpiresIn(expiresIn, SystemClock.INSTANCE);
283        }
284
285        /**
286         * Specifies the relative expiration time of the access token, in seconds, using the
287         * provided clock as the source of the current time.
288         */
289        @NonNull
290        @VisibleForTesting
291        public Builder setAccessTokenExpiresIn(@Nullable Long expiresIn, @NonNull Clock clock) {
292            if (expiresIn == null) {
293                mAccessTokenExpirationTime = null;
294            } else {
295                mAccessTokenExpirationTime = clock.getCurrentTimeMillis()
296                        + TimeUnit.SECONDS.toMillis(expiresIn);
297            }
298            return this;
299        }
300
301        /**
302         * Specifies the expiration time of the OAuth 2 access token.
303         */
304        @NonNull
305        public Builder setAccessTokenExpirationTime(@Nullable Long expirationTime) {
306            mAccessTokenExpirationTime = expirationTime;
307            return this;
308        }
309
310        /**
311         * Specifies the OAuth 2 Id token.
312         */
313        @NonNull
314        public Builder setIdToken(@Nullable String idToken) {
315            checkNullOrNotEmpty(idToken, "idToken cannot be empty");
316            mIdToken = idToken;
317            return this;
318        }
319
320        /**
321         * Specifies the encoded scope string, which is a space-delimited set of
322         * case-sensitive scope identifiers. Replaces any previously specified scope.
323         *
324         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3
325         * <https://tools.ietf.org/html/rfc6749#section-3.3>"
326         */
327        @NonNull
328        public Builder setScope(@Nullable String scope) {
329            if (TextUtils.isEmpty(scope)) {
330                mScope = null;
331            } else {
332                setScopes(scope.split(" +"));
333            }
334            return this;
335        }
336
337        /**
338         * Specifies the set of case-sensitive scopes. Replaces any previously specified set of
339         * scopes. Individual scope strings cannot be null or empty.
340         *
341         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3
342         * <https://tools.ietf.org/html/rfc6749#section-3.3>"
343         */
344        @NonNull
345        public Builder setScopes(String... scopes) {
346            if (scopes == null) {
347                mScope = null;
348            } else {
349                setScopes(Arrays.asList(scopes));
350            }
351            return this;
352        }
353
354        /**
355         * Specifies the set of case-sensitive scopes. Replaces any previously specified set of
356         * scopes. Individual scope strings cannot be null or empty.
357         *
358         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3
359         * <https://tools.ietf.org/html/rfc6749#section-3.3>"
360         */
361        @NonNull
362        public Builder setScopes(@Nullable Iterable<String> scopes) {
363            mScope = AsciiStringListUtil.iterableToString(scopes);
364            return this;
365        }
366
367        /**
368         * Specifies the additional set of parameters received as part of the response.
369         */
370        @NonNull
371        public Builder setAdditionalParameters(@Nullable Map<String, String> additionalParameters) {
372            mAdditionalParameters = checkAdditionalParams(additionalParameters, BUILT_IN_PARAMS);
373            return this;
374        }
375
376        /**
377         * Builds the Authorization object.
378         */
379        @NonNull
380        public AuthorizationResponse build() {
381            return new AuthorizationResponse(
382                    mRequest,
383                    mState,
384                    mTokenType,
385                    mAuthorizationCode,
386                    mAccessToken,
387                    mAccessTokenExpirationTime,
388                    mIdToken,
389                    mScope,
390                    Collections.unmodifiableMap(mAdditionalParameters));
391        }
392    }
393
394    private AuthorizationResponse(
395            @NonNull AuthorizationRequest request,
396            @Nullable String state,
397            @Nullable String tokenType,
398            @Nullable String authorizationCode,
399            @Nullable String accessToken,
400            @Nullable Long accessTokenExpirationTime,
401            @Nullable String idToken,
402            @Nullable String scope,
403            @NonNull Map<String, String> additionalParameters) {
404        this.request = request;
405        this.state = state;
406        this.tokenType = tokenType;
407        this.authorizationCode = authorizationCode;
408        this.accessToken = accessToken;
409        this.accessTokenExpirationTime = accessTokenExpirationTime;
410        this.idToken = idToken;
411        this.scope = scope;
412        this.additionalParameters = additionalParameters;
413    }
414
415    /**
416     * Determines whether the returned access token has expired.
417     */
418    public boolean hasAccessTokenExpired() {
419        return hasAccessTokenExpired(SystemClock.INSTANCE);
420    }
421
422    @VisibleForTesting
423    boolean hasAccessTokenExpired(@NonNull Clock clock) {
424        return accessTokenExpirationTime != null
425                && checkNotNull(clock).getCurrentTimeMillis() > accessTokenExpirationTime;
426    }
427
428    /**
429     * Derives the set of scopes from the consolidated, space-delimited scopes in the
430     * {@link #scope} field. If no scopes were specified on this response, the method will
431     * return `null`.
432     */
433    @Nullable
434    public Set<String> getScopeSet() {
435        return AsciiStringListUtil.stringToSet(scope);
436    }
437
438    /**
439     * Creates a follow-up request to exchange a received authorization code for tokens.
440     */
441    @NonNull
442    public TokenRequest createTokenExchangeRequest() {
443        return createTokenExchangeRequest(Collections.<String, String>emptyMap());
444    }
445
446    /**
447     * Creates a follow-up request to exchange a received authorization code for tokens, including
448     * the provided additional parameters.
449     */
450    @NonNull
451    public TokenRequest createTokenExchangeRequest(
452            @NonNull Map<String, String> additionalExchangeParameters) {
453        checkNotNull(additionalExchangeParameters,
454                "additionalExchangeParameters cannot be null");
455
456        if (authorizationCode == null) {
457            throw new IllegalStateException("authorizationCode not available for exchange request");
458        }
459
460        return new TokenRequest.Builder(
461                request.configuration,
462                request.clientId)
463                .setGrantType(GrantTypeValues.AUTHORIZATION_CODE)
464                .setRedirectUri(request.redirectUri)
465                .setCodeVerifier(request.codeVerifier)
466                .setAuthorizationCode(authorizationCode)
467                .setAdditionalParameters(additionalExchangeParameters)
468                .setNonce(request.nonce)
469                .build();
470    }
471
472    @Override
473    @Nullable
474    public String getState() {
475        return state;
476    }
477
478    /**
479     * Produces a JSON representation of the authorization response for persistent storage or local
480     * transmission (e.g. between activities).
481     */
482    @Override
483    @NonNull
484    public JSONObject jsonSerialize() {
485        JSONObject json = new JSONObject();
486        JsonUtil.put(json, KEY_REQUEST, request.jsonSerialize());
487        JsonUtil.putIfNotNull(json, KEY_STATE, state);
488        JsonUtil.putIfNotNull(json, KEY_TOKEN_TYPE, tokenType);
489        JsonUtil.putIfNotNull(json, KEY_AUTHORIZATION_CODE, authorizationCode);
490        JsonUtil.putIfNotNull(json, KEY_ACCESS_TOKEN, accessToken);
491        JsonUtil.putIfNotNull(json, KEY_EXPIRES_AT, accessTokenExpirationTime);
492        JsonUtil.putIfNotNull(json, KEY_ID_TOKEN, idToken);
493        JsonUtil.putIfNotNull(json, KEY_SCOPE, scope);
494        JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS,
495                JsonUtil.mapToJsonObject(additionalParameters));
496        return json;
497    }
498
499    /**
500     * Reads an authorization response from a JSON string representation produced by
501     * {@link #jsonSerialize()}.
502     *
503     * @throws JSONException if the provided JSON does not match the expected structure.
504     */
505    @NonNull
506    public static AuthorizationResponse jsonDeserialize(@NonNull JSONObject json)
507            throws JSONException {
508        if (!json.has(KEY_REQUEST)) {
509            throw new IllegalArgumentException(
510                "authorization request not provided and not found in JSON");
511        }
512
513        return new AuthorizationResponse(
514                AuthorizationRequest.jsonDeserialize(json.getJSONObject(KEY_REQUEST)),
515                JsonUtil.getStringIfDefined(json, KEY_STATE),
516                JsonUtil.getStringIfDefined(json, KEY_TOKEN_TYPE),
517                JsonUtil.getStringIfDefined(json, KEY_AUTHORIZATION_CODE),
518                JsonUtil.getStringIfDefined(json, KEY_ACCESS_TOKEN),
519                JsonUtil.getLongIfDefined(json, KEY_EXPIRES_AT),
520                JsonUtil.getStringIfDefined(json, KEY_ID_TOKEN),
521                JsonUtil.getStringIfDefined(json, KEY_SCOPE),
522                JsonUtil.getStringMap(json, KEY_ADDITIONAL_PARAMETERS));
523    }
524
525    /**
526     * Reads an authorization request from a JSON string representation produced by
527     * {@link #jsonSerializeString()}. This method is just a convenience wrapper for
528     * {@link #jsonDeserialize(JSONObject)}, converting the JSON string to its JSON object form.
529     *
530     * @throws JSONException if the provided JSON does not match the expected structure.
531     */
532    @NonNull
533    public static AuthorizationResponse jsonDeserialize(@NonNull String jsonStr)
534            throws JSONException {
535        return jsonDeserialize(new JSONObject(jsonStr));
536    }
537
538    /**
539     * Produces an intent containing this authorization response. This is used to deliver the
540     * authorization response to the registered handler after a call to
541     * {@link AuthorizationService#performAuthorizationRequest}.
542     */
543    @Override
544    @NonNull
545    public Intent toIntent() {
546        Intent data = new Intent();
547        data.putExtra(EXTRA_RESPONSE, this.jsonSerializeString());
548        return data;
549    }
550
551    /**
552     * Extracts an authorization response from an intent produced by {@link #toIntent()}. This is
553     * used to extract the response from the intent data passed to an activity registered as the
554     * handler for {@link AuthorizationService#performAuthorizationRequest}.
555     */
556    @Nullable
557    public static AuthorizationResponse fromIntent(@NonNull Intent dataIntent) {
558        checkNotNull(dataIntent, "dataIntent must not be null");
559        if (!dataIntent.hasExtra(EXTRA_RESPONSE)) {
560            return null;
561        }
562
563        try {
564            return AuthorizationResponse.jsonDeserialize(dataIntent.getStringExtra(EXTRA_RESPONSE));
565        } catch (JSONException ex) {
566            throw new IllegalArgumentException("Intent contains malformed auth response", ex);
567        }
568    }
569
570    static boolean containsAuthorizationResponse(@NonNull Intent intent) {
571        return intent.hasExtra(EXTRA_RESPONSE);
572    }
573}