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.checkNotEmpty;
020import static net.openid.appauth.Preconditions.checkNotNull;
021import static net.openid.appauth.Preconditions.checkNullOrNotEmpty;
022
023import android.text.TextUtils;
024import androidx.annotation.NonNull;
025import androidx.annotation.Nullable;
026import androidx.annotation.VisibleForTesting;
027
028import org.json.JSONException;
029import org.json.JSONObject;
030
031import java.util.Arrays;
032import java.util.Collections;
033import java.util.HashSet;
034import java.util.Map;
035import java.util.Set;
036import java.util.concurrent.TimeUnit;
037
038/**
039 * A response to a token request.
040 *
041 * @see TokenRequest
042 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.4
043 * <https://tools.ietf.org/html/rfc6749#section-4.1.4>"
044 */
045public class TokenResponse {
046
047    /**
048     * Indicates that a provided access token is a bearer token.
049     *
050     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 7.1
051     * <https://tools.ietf.org/html/rfc6749#section-7.1>"
052     */
053    public static final String TOKEN_TYPE_BEARER = "Bearer";
054
055    @VisibleForTesting
056    static final String KEY_REQUEST = "request";
057
058    @VisibleForTesting
059    static final String KEY_EXPIRES_AT = "expires_at";
060
061    // TODO: rename all KEY_* below to PARAM_*
062    @VisibleForTesting
063    static final String KEY_TOKEN_TYPE = "token_type";
064
065    @VisibleForTesting
066    static final String KEY_ACCESS_TOKEN = "access_token";
067
068    @VisibleForTesting
069    static final String KEY_EXPIRES_IN = "expires_in";
070
071    @VisibleForTesting
072    static final String KEY_REFRESH_TOKEN = "refresh_token";
073
074    @VisibleForTesting
075    static final String KEY_ID_TOKEN = "id_token";
076
077    @VisibleForTesting
078    static final String KEY_SCOPE = "scope";
079
080    @VisibleForTesting
081    static final String KEY_ADDITIONAL_PARAMETERS = "additionalParameters";
082
083    private static final Set<String> BUILT_IN_PARAMS = new HashSet<>(Arrays.asList(
084            KEY_TOKEN_TYPE,
085            KEY_ACCESS_TOKEN,
086            KEY_EXPIRES_IN,
087            KEY_REFRESH_TOKEN,
088            KEY_ID_TOKEN,
089            KEY_SCOPE
090    ));
091
092    /**
093     * The token request associated with this response.
094     */
095    @NonNull
096    public final TokenRequest request;
097
098    /**
099     * The type of the token returned. Typically this is {@link #TOKEN_TYPE_BEARER}, or some
100     * other token type that the client has negotiated with the authorization service.
101     *
102     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.4
103     * <https://tools.ietf.org/html/rfc6749#section-4.1.4>"
104     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 5.1
105     * <https://tools.ietf.org/html/rfc6749#section-5.1>"
106     */
107    @Nullable
108    public final String tokenType;
109
110    /**
111     * The access token, if provided.
112     *
113     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 5.1
114     * <https://tools.ietf.org/html/rfc6749#section-5.1>"
115     */
116    @Nullable
117    public final String accessToken;
118
119    /**
120     * The expiration time of the access token, if provided. If an access token is provided but the
121     * expiration time is not, then the expiration time is typically some default value specified
122     * by the identity provider through some other means, such as documentation or an additional
123     * non-standard field.
124     */
125    @Nullable
126    public final Long accessTokenExpirationTime;
127
128    /**
129     * The ID token describing the authenticated user, if provided.
130     *
131     * @see "OpenID Connect Core 1.0, Section 2
132     * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.2>"
133     */
134    @Nullable
135    public final String idToken;
136
137    /**
138     * The refresh token, if provided.
139     *
140     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 5.1
141     * <https://tools.ietf.org/html/rfc6749#section-5.1>"
142     */
143    @Nullable
144    public final String refreshToken;
145
146    /**
147     * The scope of the access token. If the scope is identical to that originally
148     * requested, then this value is optional.
149     *
150     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 5.1
151     * <https://tools.ietf.org/html/rfc6749#section-5.1>"
152     */
153    @Nullable
154    public final String scope;
155
156    /**
157     * Additional, non-standard parameters in the response.
158     */
159    @NonNull
160    public final Map<String, String> additionalParameters;
161
162    /**
163     * Creates instances of {@link TokenResponse}.
164     */
165    public static final class Builder {
166        @NonNull
167        private TokenRequest mRequest;
168
169        @Nullable
170        private String mTokenType;
171
172        @Nullable
173        private String mAccessToken;
174
175        @Nullable
176        private Long mAccessTokenExpirationTime;
177
178        @Nullable
179        private String mIdToken;
180
181        @Nullable
182        private String mRefreshToken;
183
184        @Nullable
185        private String mScope;
186
187        @NonNull
188        private Map<String, String> mAdditionalParameters;
189
190        /**
191         * Creates a token response associated with the specified request.
192         */
193        public Builder(@NonNull TokenRequest request) {
194            setRequest(request);
195            mAdditionalParameters = Collections.emptyMap();
196        }
197
198        /**
199         * Extracts token response fields from a JSON string.
200         *
201         * @throws JSONException if the JSON is malformed or has incorrect value types for fields.
202         */
203        @NonNull
204        public Builder fromResponseJsonString(@NonNull String jsonStr) throws JSONException {
205            checkNotEmpty(jsonStr, "json cannot be null or empty");
206            return fromResponseJson(new JSONObject(jsonStr));
207        }
208
209        /**
210         * Extracts token response fields from a JSON object.
211         *
212         * @throws JSONException if the JSON is malformed or has incorrect value types for fields.
213         */
214        @NonNull
215        public Builder fromResponseJson(@NonNull JSONObject json) throws JSONException {
216            setTokenType(JsonUtil.getString(json, KEY_TOKEN_TYPE));
217            setAccessToken(JsonUtil.getStringIfDefined(json, KEY_ACCESS_TOKEN));
218            setAccessTokenExpirationTime(JsonUtil.getLongIfDefined(json, KEY_EXPIRES_AT));
219            if (json.has(KEY_EXPIRES_IN)) {
220                setAccessTokenExpiresIn(json.getLong(KEY_EXPIRES_IN));
221            }
222            setRefreshToken(JsonUtil.getStringIfDefined(json, KEY_REFRESH_TOKEN));
223            setIdToken(JsonUtil.getStringIfDefined(json, KEY_ID_TOKEN));
224            setScope(JsonUtil.getStringIfDefined(json, KEY_SCOPE));
225            setAdditionalParameters(extractAdditionalParams(json, BUILT_IN_PARAMS));
226
227            return this;
228        }
229
230        /**
231         * Specifies the request associated with this response. Must not be null.
232         */
233        @NonNull
234        public Builder setRequest(@NonNull TokenRequest request) {
235            mRequest = checkNotNull(request, "request cannot be null");
236            return this;
237        }
238
239        /**
240         * Specifies the token type of the access token in this response. If not null, the value
241         * must be non-empty.
242         */
243        @NonNull
244        public Builder setTokenType(@Nullable String tokenType) {
245            mTokenType = checkNullOrNotEmpty(tokenType, "token type must not be empty if defined");
246            return this;
247        }
248
249        /**
250         * Specifies the access token. If not null, the value must be non-empty.
251         */
252        @NonNull
253        public Builder setAccessToken(@Nullable String accessToken) {
254            mAccessToken = checkNullOrNotEmpty(accessToken,
255                    "access token cannot be empty if specified");
256            return this;
257        }
258
259        /**
260         * Sets the relative expiration time of the access token, in seconds, using the default
261         * system clock as the source of the current time.
262         */
263        @NonNull
264        public Builder setAccessTokenExpiresIn(@NonNull Long expiresIn) {
265            return setAccessTokenExpiresIn(expiresIn, SystemClock.INSTANCE);
266        }
267
268        /**
269         * Sets the relative expiration time of the access token, in seconds, using the provided
270         * clock as the source of the current time.
271         */
272        @NonNull
273        @VisibleForTesting
274        Builder setAccessTokenExpiresIn(@Nullable Long expiresIn, @NonNull Clock clock) {
275            if (expiresIn == null) {
276                mAccessTokenExpirationTime = null;
277            } else {
278                mAccessTokenExpirationTime = clock.getCurrentTimeMillis()
279                        + TimeUnit.SECONDS.toMillis(expiresIn);
280            }
281            return this;
282        }
283
284        /**
285         * Sets the exact expiration time of the access token, in milliseconds since the UNIX epoch.
286         */
287        @NonNull
288        public Builder setAccessTokenExpirationTime(@Nullable Long expiresAt) {
289            mAccessTokenExpirationTime = expiresAt;
290            return this;
291        }
292
293        /**
294         * Specifies the ID token. If not null, the value must be non-empty.
295         */
296        public Builder setIdToken(@Nullable String idToken) {
297            mIdToken = checkNullOrNotEmpty(idToken, "id token must not be empty if defined");
298            return this;
299        }
300
301        /**
302         * Specifies the refresh token. If not null, the value must be non-empty.
303         */
304        public Builder setRefreshToken(@Nullable String refreshToken) {
305            mRefreshToken = checkNullOrNotEmpty(refreshToken,
306                    "refresh token must not be empty if defined");
307            return this;
308        }
309
310        /**
311         * Specifies the encoded scope string, which is a space-delimited set of
312         * case-sensitive scope identifiers. Replaces any previously specified scope.
313         *
314         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3
315         * <https://tools.ietf.org/html/rfc6749#section-3.3>"
316         */
317        @NonNull
318        public Builder setScope(@Nullable String scope) {
319            if (TextUtils.isEmpty(scope)) {
320                mScope = null;
321            } else {
322                setScopes(scope.split(" +"));
323            }
324            return this;
325        }
326
327        /**
328         * Specifies the set of case-sensitive scopes. Replaces any previously specified set of
329         * scopes. Individual scope strings cannot be null or empty.
330         *
331         * <p>Scopes specified here are used to obtain a "down-scoped" access token, where the
332         * set of scopes specified _must_ be a subset of those already granted in
333         * previous requests.
334         *
335         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3
336         * <https://tools.ietf.org/html/rfc6749#section-3.3>"
337         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6
338         * <https://tools.ietf.org/html/rfc6749#section-6>"
339         */
340        @NonNull
341        public Builder setScopes(String... scopes) {
342            if (scopes == null) {
343                scopes = new String[0];
344            }
345            setScopes(Arrays.asList(scopes));
346            return this;
347        }
348
349        /**
350         * Specifies the set of case-sensitive scopes. Replaces any previously specified set of
351         * scopes. Individual scope strings cannot be null or empty.
352         *
353         * <p>Scopes specified here are used to obtain a "down-scoped" access token, where the
354         * set of scopes specified _must_ be a subset of those already granted in
355         * previous requests.
356         *
357         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3
358         * <https://tools.ietf.org/html/rfc6749#section-3.3>"
359         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6
360         * <https://tools.ietf.org/html/rfc6749#section-6>"
361         */
362        @NonNull
363        public Builder setScopes(@Nullable Iterable<String> scopes) {
364            mScope = AsciiStringListUtil.iterableToString(scopes);
365            return this;
366        }
367
368        /**
369         * Specifies the additional, non-standard parameters received as part of the response.
370         */
371        @NonNull
372        public Builder setAdditionalParameters(@Nullable Map<String, String> additionalParameters) {
373            mAdditionalParameters = checkAdditionalParams(additionalParameters, BUILT_IN_PARAMS);
374            return this;
375        }
376
377        /**
378         * Creates the token response instance.
379         */
380        public TokenResponse build() {
381            return new TokenResponse(
382                    mRequest,
383                    mTokenType,
384                    mAccessToken,
385                    mAccessTokenExpirationTime,
386                    mIdToken,
387                    mRefreshToken,
388                    mScope,
389                    mAdditionalParameters);
390        }
391    }
392
393    TokenResponse(
394            @NonNull TokenRequest request,
395            @Nullable String tokenType,
396            @Nullable String accessToken,
397            @Nullable Long accessTokenExpirationTime,
398            @Nullable String idToken,
399            @Nullable String refreshToken,
400            @Nullable String scope,
401            @NonNull Map<String, String> additionalParameters) {
402        this.request = request;
403        this.tokenType = tokenType;
404        this.accessToken = accessToken;
405        this.accessTokenExpirationTime = accessTokenExpirationTime;
406        this.idToken = idToken;
407        this.refreshToken = refreshToken;
408        this.scope = scope;
409        this.additionalParameters = additionalParameters;
410    }
411
412    /**
413     * Derives the set of scopes from the consolidated, space-delimited scopes in the
414     * {@link #scope} field. If no scopes were specified on this response, the method will
415     * return `null`.
416     */
417    @Nullable
418    public Set<String> getScopeSet() {
419        return AsciiStringListUtil.stringToSet(scope);
420    }
421
422    /**
423     * Produces a JSON string representation of the token response for persistent storage or
424     * local transmission (e.g. between activities).
425     */
426    public JSONObject jsonSerialize() {
427        JSONObject json = new JSONObject();
428        JsonUtil.put(json, KEY_REQUEST, request.jsonSerialize());
429        JsonUtil.putIfNotNull(json, KEY_TOKEN_TYPE, tokenType);
430        JsonUtil.putIfNotNull(json, KEY_ACCESS_TOKEN, accessToken);
431        JsonUtil.putIfNotNull(json, KEY_EXPIRES_AT, accessTokenExpirationTime);
432        JsonUtil.putIfNotNull(json, KEY_ID_TOKEN, idToken);
433        JsonUtil.putIfNotNull(json, KEY_REFRESH_TOKEN, refreshToken);
434        JsonUtil.putIfNotNull(json, KEY_SCOPE, scope);
435        JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS,
436                JsonUtil.mapToJsonObject(additionalParameters));
437        return json;
438    }
439
440    /**
441     * Produces a JSON string representation of the token response for persistent storage or
442     * local transmission (e.g. between activities). This method is just a convenience wrapper
443     * for {@link #jsonSerialize()}, converting the JSON object to its string form.
444     */
445    public String jsonSerializeString() {
446        return jsonSerialize().toString();
447    }
448
449    /**
450     * Reads a token response from a JSON string, and associates it with the provided request.
451     * If a request is not provided, its serialized form is expected to be found in the JSON
452     * (as if produced by a prior call to {@link #jsonSerialize()}.
453     * @throws JSONException if the JSON is malformed or missing required fields.
454     */
455    @NonNull
456    public static TokenResponse jsonDeserialize(@NonNull JSONObject json) throws JSONException {
457        if (!json.has(KEY_REQUEST)) {
458            throw new IllegalArgumentException(
459                    "token request not provided and not found in JSON");
460        }
461        return new TokenResponse(
462                TokenRequest.jsonDeserialize(json.getJSONObject(KEY_REQUEST)),
463                JsonUtil.getStringIfDefined(json, KEY_TOKEN_TYPE),
464                JsonUtil.getStringIfDefined(json, KEY_ACCESS_TOKEN),
465                JsonUtil.getLongIfDefined(json, KEY_EXPIRES_AT),
466                JsonUtil.getStringIfDefined(json, KEY_ID_TOKEN),
467                JsonUtil.getStringIfDefined(json, KEY_REFRESH_TOKEN),
468                JsonUtil.getStringIfDefined(json, KEY_SCOPE),
469                JsonUtil.getStringMap(json, KEY_ADDITIONAL_PARAMETERS));
470    }
471
472    /**
473     * Reads a token response from a JSON string, and associates it with the provided request.
474     * If a request is not provided, its serialized form is expected to be found in the JSON
475     * (as if produced by a prior call to {@link #jsonSerialize()}.
476     * @throws JSONException if the JSON is malformed or missing required fields.
477     */
478    @NonNull
479    public static TokenResponse jsonDeserialize(@NonNull String jsonStr) throws JSONException {
480        checkNotEmpty(jsonStr, "jsonStr cannot be null or empty");
481        return jsonDeserialize(new JSONObject(jsonStr));
482    }
483}