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.Preconditions.checkNotEmpty;
019import static net.openid.appauth.Preconditions.checkNotNull;
020import static net.openid.appauth.Preconditions.checkNullOrNotEmpty;
021
022import android.net.Uri;
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.HashMap;
034import java.util.HashSet;
035import java.util.LinkedHashMap;
036import java.util.Map;
037import java.util.Map.Entry;
038import java.util.Set;
039
040/**
041 * An OAuth2 token request. These are used to exchange codes for tokens, or exchange a refresh
042 * token for updated tokens.
043 *
044 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.3
045 * <https://tools.ietf.org/html/rfc6749#section-4.1.3>"
046 */
047public class TokenRequest {
048
049    @VisibleForTesting
050    static final String KEY_CONFIGURATION = "configuration";
051    @VisibleForTesting
052    static final String KEY_CLIENT_ID = "clientId";
053    @VisibleForTesting
054    static final String KEY_NONCE = "nonce";
055    @VisibleForTesting
056    static final String KEY_GRANT_TYPE = "grantType";
057    @VisibleForTesting
058    static final String KEY_REDIRECT_URI = "redirectUri";
059    @VisibleForTesting
060    static final String KEY_SCOPE = "scope";
061    @VisibleForTesting
062    static final String KEY_AUTHORIZATION_CODE = "authorizationCode";
063    @VisibleForTesting
064    static final String KEY_REFRESH_TOKEN = "refreshToken";
065    @VisibleForTesting
066    static final String KEY_CODE_VERIFIER = "codeVerifier";
067    @VisibleForTesting
068    static final String KEY_ADDITIONAL_PARAMETERS = "additionalParameters";
069
070    public static final String PARAM_CLIENT_ID = "client_id";
071
072    @VisibleForTesting
073    static final String PARAM_CODE = "code";
074
075    @VisibleForTesting
076    static final String PARAM_CODE_VERIFIER = "code_verifier";
077
078    @VisibleForTesting
079    static final String PARAM_GRANT_TYPE = "grant_type";
080
081    @VisibleForTesting
082    static final String PARAM_REDIRECT_URI = "redirect_uri";
083
084    @VisibleForTesting
085    static final String PARAM_REFRESH_TOKEN = "refresh_token";
086
087    @VisibleForTesting
088    static final String PARAM_SCOPE = "scope";
089
090    private static final Set<String> BUILT_IN_PARAMS = Collections.unmodifiableSet(
091            new HashSet<>(Arrays.asList(
092                    PARAM_CLIENT_ID,
093                    PARAM_CODE,
094                    PARAM_CODE_VERIFIER,
095                    PARAM_GRANT_TYPE,
096                    PARAM_REDIRECT_URI,
097                    PARAM_REFRESH_TOKEN,
098                    PARAM_SCOPE)));
099
100
101    /**
102     * The grant type used when requesting an access token using a username and password.
103     * This grant type is not directly supported by this library.
104     *
105     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.3.2
106     * <https://tools.ietf.org/html/rfc6749#section-4.3.2>"
107     */
108    public static final String GRANT_TYPE_PASSWORD = "password";
109
110    /**
111     * The grant type used when requesting an access token using client credentials, typically
112     * TLS client certificates. This grant type is not directly supported by this library.
113     *
114     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.4.2
115     * <https://tools.ietf.org/html/rfc6749#section-4.4.2>"
116     */
117    public static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
118
119    /**
120     * The service's {@link AuthorizationServiceConfiguration configuration}.
121     * This configuration specifies how to connect to a particular OAuth provider.
122     * Configurations may be
123     * {@link
124     * AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri, Uri, Uri, Uri)
125     * created manually}, or
126     * {@link AuthorizationServiceConfiguration#fetchFromUrl(Uri,
127     * AuthorizationServiceConfiguration.RetrieveConfigurationCallback)
128     * via an OpenID Connect Discovery Document}.
129     */
130    @NonNull
131    public final AuthorizationServiceConfiguration configuration;
132
133    /**
134     * The (optional) nonce associated with the current session.
135     */
136    @Nullable
137    public final String nonce;
138
139    /**
140     * The client identifier.
141     *
142     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4
143     * <https://tools.ietf.org/html/rfc6749#section-4>"
144     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.1
145     * <https://tools.ietf.org/html/rfc6749#section-4.1.1>"
146     */
147    @NonNull
148    public final String clientId;
149
150    /**
151     * The type of token being sent to the token endpoint.
152     *
153     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.3
154     * <https://tools.ietf.org/html/rfc6749#section-4.1.3>"
155     */
156    @NonNull
157    public final String grantType;
158
159    /**
160     * The client's redirect URI. Required if this token request is to exchange an authorization
161     * code for one or more tokens, and must be identical to the value specified in the original
162     * authorization request.
163     *
164     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.1.2
165     * <https://tools.ietf.org/html/rfc6749#section-3.1.2>"
166     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.3
167     * <https://tools.ietf.org/html/rfc6749#section-4.1.3>"
168     */
169    @Nullable
170    public final Uri redirectUri;
171
172    /**
173     * An authorization code to be exchanged for one or more tokens.
174     *
175     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.3
176     * <https://tools.ietf.org/html/rfc6749#section-4.1.3>"
177     */
178    @Nullable
179    public final String authorizationCode;
180
181    /**
182     * A space-delimited set of scopes used to determine the scope of any returned tokens.
183     *
184     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3
185     * <https://tools.ietf.org/html/rfc6749#section-3.3>"
186     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6
187     * <https://tools.ietf.org/html/rfc6749#section-6>"
188     */
189    @Nullable
190    public final String scope;
191
192    /**
193     * A refresh token to be exchanged for a new token.
194     *
195     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6
196     * <https://tools.ietf.org/html/rfc6749#section-6>"
197     */
198    @Nullable
199    public final String refreshToken;
200
201    /**
202     * The code verifier that was used to generate the challenge in the original authorization
203     * request, if one was used.
204     *
205     * @see "Proof Key for Code Exchange by OAuth Public Clients (RFC 7636), Section 4
206     * <https://tools.ietf.org/html/rfc7636#section-4>"
207     */
208    @Nullable
209    public final String codeVerifier;
210
211    /**
212     * Additional parameters to be passed as part of the request.
213     */
214    @NonNull
215    public final Map<String, String> additionalParameters;
216
217    /**
218     * Creates instances of {@link TokenRequest}.
219     */
220    public static final class Builder {
221
222        @NonNull
223        private AuthorizationServiceConfiguration mConfiguration;
224
225        @NonNull
226        private String mClientId;
227
228        @Nullable
229        private String mNonce;
230
231        @Nullable
232        private String mGrantType;
233
234        @Nullable
235        private Uri mRedirectUri;
236
237        @Nullable
238        private String mScope;
239
240        @Nullable
241        private String mAuthorizationCode;
242
243        @Nullable
244        private String mRefreshToken;
245
246        @Nullable
247        private String mCodeVerifier;
248
249        @NonNull
250        private Map<String, String> mAdditionalParameters;
251
252        /**
253         * Creates a token request builder with the specified mandatory properties.
254         */
255        public Builder(
256                @NonNull AuthorizationServiceConfiguration configuration,
257                @NonNull String clientId) {
258            setConfiguration(configuration);
259            setClientId(clientId);
260            mAdditionalParameters = new LinkedHashMap<>();
261        }
262
263        /**
264         * Specifies the authorization service configuration for the request, which must not
265         * be null or empty.
266         */
267        @NonNull
268        public Builder setConfiguration(@NonNull AuthorizationServiceConfiguration configuration) {
269            mConfiguration = checkNotNull(configuration);
270            return this;
271        }
272
273        /**
274         * Specifies the client ID for the token request, which must not be null or empty.
275         */
276        @NonNull
277        public Builder setClientId(@NonNull String clientId) {
278            mClientId = checkNotEmpty(clientId, "clientId cannot be null or empty");
279            return this;
280        }
281
282        /**
283         * Specifies the (optional) nonce for the current session.
284         */
285        @NonNull
286        public Builder setNonce(@Nullable String nonce) {
287            if (TextUtils.isEmpty(nonce)) {
288                mNonce = null;
289            } else {
290                this.mNonce = nonce;
291            }
292            return this;
293        }
294
295        /**
296         * Specifies the grant type for the request, which must not be null or empty.
297         */
298        @NonNull
299        public Builder setGrantType(@NonNull String grantType) {
300            mGrantType = checkNotEmpty(grantType, "grantType cannot be null or empty");
301            return this;
302        }
303
304        /**
305         * Specifies the redirect URI for the request. This is required for authorization code
306         * exchanges, but otherwise optional. If specified, the redirect URI must have a scheme.
307         */
308        @NonNull
309        public Builder setRedirectUri(@Nullable Uri redirectUri) {
310            if (redirectUri != null) {
311                checkNotNull(redirectUri.getScheme(), "redirectUri must have a scheme");
312            }
313            mRedirectUri = redirectUri;
314            return this;
315        }
316
317        /**
318         * Specifies the encoded scope string, which is a space-delimited set of
319         * case-sensitive scope identifiers. Replaces any previously specified scope.
320         *
321         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3
322         * <https://tools.ietf.org/html/rfc6749#section-3.3>"
323         */
324        @NonNull
325        public Builder setScope(@Nullable String scope) {
326            if (TextUtils.isEmpty(scope)) {
327                mScope = null;
328            } else {
329                setScopes(scope.split(" +"));
330            }
331            return this;
332        }
333
334        /**
335         * Specifies the set of case-sensitive scopes. Replaces any previously specified set of
336         * scopes. Individual scope strings cannot be null or empty.
337         *
338         * Scopes specified here are used to obtain a "down-scoped" access token, where the
339         * set of scopes specified _must_ be a subset of those already granted in
340         * previous requests.
341         *
342         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3
343         * <https://tools.ietf.org/html/rfc6749#section-3.3>"
344         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6
345         * <https://tools.ietf.org/html/rfc6749#section-6>"
346         */
347        @NonNull
348        public Builder setScopes(String... scopes) {
349            if (scopes == null) {
350                scopes = new String[0];
351            }
352            setScopes(Arrays.asList(scopes));
353            return this;
354        }
355
356        /**
357         * Specifies the set of case-sensitive scopes. Replaces any previously specified set of
358         * scopes. Individual scope strings cannot be null or empty.
359         *
360         * Scopes specified here are used to obtain a "down-scoped" access token, where the
361         * set of scopes specified _must_ be a subset of those already granted in
362         * previous requests.
363         *
364         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.3
365         * <https://tools.ietf.org/html/rfc6749#section-3.3>"
366         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 6
367         * <https://tools.ietf.org/html/rfc6749#section-6>"
368         */
369        @NonNull
370        public Builder setScopes(@Nullable Iterable<String> scopes) {
371            mScope = AsciiStringListUtil.iterableToString(scopes);
372            return this;
373        }
374
375        /**
376         * Specifies the authorization code for the request. If provided, the authorization code
377         * must not be empty.
378         *
379         * Specifying an authorization code normally implies that this is a request to exchange
380         * this authorization code for one or more tokens. If this is not intended, the grant type
381         * should be explicitly set.
382         */
383        @NonNull
384        public Builder setAuthorizationCode(@Nullable String authorizationCode) {
385            checkNullOrNotEmpty(authorizationCode, "authorization code must not be empty");
386            mAuthorizationCode = authorizationCode;
387            return this;
388        }
389
390        /**
391         * Specifies the refresh token for the request. If a non-null value is provided, it must
392         * not be empty.
393         *
394         * Specifying a refresh token normally implies that this is a request to exchange the
395         * refresh token for a new token. If this is not intended, the grant type should be
396         * explicit set.
397         */
398        @NonNull
399        public Builder setRefreshToken(@Nullable String refreshToken) {
400            if (refreshToken != null) {
401                checkNotEmpty(refreshToken, "refresh token cannot be empty if defined");
402            }
403            mRefreshToken = refreshToken;
404            return this;
405        }
406
407        /**
408         * Specifies the code verifier for an authorization code exchange request. This must match
409         * the code verifier that was used to generate the challenge sent in the request that
410         * produced the authorization code.
411         */
412        public Builder setCodeVerifier(@Nullable String codeVerifier) {
413            if (codeVerifier != null) {
414                CodeVerifierUtil.checkCodeVerifier(codeVerifier);
415            }
416
417            mCodeVerifier = codeVerifier;
418            return this;
419        }
420
421        /**
422         * Specifies an additional set of parameters to be sent as part of the request.
423         */
424        @NonNull
425        public Builder setAdditionalParameters(@Nullable Map<String, String> additionalParameters) {
426            mAdditionalParameters = checkAdditionalParams(additionalParameters, BUILT_IN_PARAMS);
427            return this;
428        }
429
430        /**
431         * Produces a {@link TokenRequest} instance, if all necessary values have been provided.
432         */
433        @NonNull
434        public TokenRequest build() {
435            String grantType = inferGrantType();
436
437            if (GrantTypeValues.AUTHORIZATION_CODE.equals(grantType)) {
438                checkNotNull(mAuthorizationCode,
439                        "authorization code must be specified for grant_type = "
440                                + GrantTypeValues.AUTHORIZATION_CODE);
441            }
442
443            if (GrantTypeValues.REFRESH_TOKEN.equals(grantType)) {
444                checkNotNull(mRefreshToken,
445                        "refresh token must be specified for grant_type = "
446                                + GrantTypeValues.REFRESH_TOKEN);
447            }
448
449
450            if (grantType.equals(GrantTypeValues.AUTHORIZATION_CODE) && mRedirectUri == null) {
451                throw new IllegalStateException(
452                        "no redirect URI specified on token request for code exchange");
453            }
454
455            return new TokenRequest(
456                    mConfiguration,
457                    mClientId,
458                    mNonce,
459                    grantType,
460                    mRedirectUri,
461                    mScope,
462                    mAuthorizationCode,
463                    mRefreshToken,
464                    mCodeVerifier,
465                    Collections.unmodifiableMap(mAdditionalParameters));
466        }
467
468        private String inferGrantType() {
469            if (mGrantType != null) {
470                return mGrantType;
471            } else if (mAuthorizationCode != null) {
472                return GrantTypeValues.AUTHORIZATION_CODE;
473            } else if (mRefreshToken != null) {
474                return GrantTypeValues.REFRESH_TOKEN;
475            } else {
476                throw new IllegalStateException("grant type not specified and cannot be inferred");
477            }
478        }
479    }
480
481    private TokenRequest(
482            @NonNull AuthorizationServiceConfiguration configuration,
483            @NonNull String clientId,
484            @Nullable String nonce,
485            @NonNull String grantType,
486            @Nullable Uri redirectUri,
487            @Nullable String scope,
488            @Nullable String authorizationCode,
489            @Nullable String refreshToken,
490            @Nullable String codeVerifier,
491            @NonNull Map<String, String> additionalParameters) {
492        this.configuration = configuration;
493        this.clientId = clientId;
494        this.nonce = nonce;
495        this.grantType = grantType;
496        this.redirectUri = redirectUri;
497        this.scope = scope;
498        this.authorizationCode = authorizationCode;
499        this.refreshToken = refreshToken;
500        this.codeVerifier = codeVerifier;
501        this.additionalParameters = additionalParameters;
502    }
503
504    /**
505     * Derives the set of scopes from the consolidated, space-delimited scopes in the
506     * {@link #scope} field. If no scopes were specified for this request, the method will
507     * return `null`.
508     */
509    @Nullable
510    public Set<String> getScopeSet() {
511        return AsciiStringListUtil.stringToSet(scope);
512    }
513
514    /**
515     * Produces the set of request parameters for this query, which can be further
516     * processed into a request body.
517     */
518    @NonNull
519    public Map<String, String> getRequestParameters() {
520        Map<String, String> params = new HashMap<>();
521        params.put(PARAM_GRANT_TYPE, grantType);
522        putIfNotNull(params, PARAM_REDIRECT_URI, redirectUri);
523        putIfNotNull(params, PARAM_CODE, authorizationCode);
524        putIfNotNull(params, PARAM_REFRESH_TOKEN, refreshToken);
525        putIfNotNull(params, PARAM_CODE_VERIFIER, codeVerifier);
526        putIfNotNull(params, PARAM_SCOPE, scope);
527
528        for (Entry<String, String> param : additionalParameters.entrySet()) {
529            params.put(param.getKey(), param.getValue());
530        }
531
532        return params;
533    }
534
535    private void putIfNotNull(Map<String, String> map, String key, Object value) {
536        if (value != null) {
537            map.put(key, value.toString());
538        }
539    }
540
541    /**
542     * Produces a JSON string representation of the token request for persistent storage or
543     * local transmission (e.g. between activities).
544     */
545    @NonNull
546    public JSONObject jsonSerialize() {
547        JSONObject json = new JSONObject();
548        JsonUtil.put(json, KEY_CONFIGURATION, configuration.toJson());
549        JsonUtil.put(json, KEY_CLIENT_ID, clientId);
550        JsonUtil.putIfNotNull(json, KEY_NONCE, nonce);
551        JsonUtil.put(json, KEY_GRANT_TYPE, grantType);
552        JsonUtil.putIfNotNull(json, KEY_REDIRECT_URI, redirectUri);
553        JsonUtil.putIfNotNull(json, KEY_SCOPE, scope);
554        JsonUtil.putIfNotNull(json, KEY_AUTHORIZATION_CODE, authorizationCode);
555        JsonUtil.putIfNotNull(json, KEY_REFRESH_TOKEN, refreshToken);
556        JsonUtil.putIfNotNull(json, KEY_CODE_VERIFIER, codeVerifier);
557        JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS,
558                JsonUtil.mapToJsonObject(additionalParameters));
559        return json;
560    }
561
562    /**
563     * Produces a JSON string representation of the token request for persistent storage or
564     * local transmission (e.g. between activities). This method is just a convenience wrapper
565     * for {@link #jsonSerialize()}, converting the JSON object to its string form.
566     */
567    @NonNull
568    public String jsonSerializeString() {
569        return jsonSerialize().toString();
570    }
571
572    /**
573     * Reads a token request from a JSON string representation produced by
574     * {@link #jsonSerialize()}.
575     * @throws JSONException if the provided JSON does not match the expected structure.
576     */
577    @NonNull
578    public static TokenRequest jsonDeserialize(JSONObject json) throws JSONException {
579        checkNotNull(json, "json object cannot be null");
580
581        return new TokenRequest(
582                AuthorizationServiceConfiguration.fromJson(json.getJSONObject(KEY_CONFIGURATION)),
583                JsonUtil.getString(json, KEY_CLIENT_ID),
584                JsonUtil.getStringIfDefined(json, KEY_NONCE),
585                JsonUtil.getString(json, KEY_GRANT_TYPE),
586                JsonUtil.getUriIfDefined(json, KEY_REDIRECT_URI),
587                JsonUtil.getStringIfDefined(json, KEY_SCOPE),
588                JsonUtil.getStringIfDefined(json, KEY_AUTHORIZATION_CODE),
589                JsonUtil.getStringIfDefined(json, KEY_REFRESH_TOKEN),
590                JsonUtil.getStringIfDefined(json, KEY_CODE_VERIFIER),
591                JsonUtil.getStringMap(json, KEY_ADDITIONAL_PARAMETERS));
592    }
593
594    /**
595     * Reads a token request from a JSON string representation produced by
596     * {@link #jsonSerializeString()}. This method is just a convenience wrapper for
597     * {@link #jsonDeserialize(JSONObject)}, converting the JSON string to its JSON object form.
598     * @throws JSONException if the provided JSON does not match the expected structure.
599     */
600    @NonNull
601    public static TokenRequest jsonDeserialize(@NonNull String json) throws JSONException {
602        checkNotNull(json, "json string cannot be null");
603        return jsonDeserialize(new JSONObject(json));
604    }
605}