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.checkAdditionalParams;
018import static net.openid.appauth.AdditionalParamsProcessor.extractAdditionalParams;
019import static net.openid.appauth.Preconditions.checkNotEmpty;
020import static net.openid.appauth.Preconditions.checkNotNull;
021
022import android.net.Uri;
023import androidx.annotation.NonNull;
024import androidx.annotation.Nullable;
025import androidx.annotation.VisibleForTesting;
026
027import org.json.JSONException;
028import org.json.JSONObject;
029
030import java.util.Arrays;
031import java.util.Collections;
032import java.util.HashSet;
033import java.util.Map;
034import java.util.Set;
035import java.util.concurrent.TimeUnit;
036
037public class RegistrationResponse {
038    static final String PARAM_CLIENT_ID = "client_id";
039    static final String PARAM_CLIENT_SECRET = "client_secret";
040    static final String PARAM_CLIENT_SECRET_EXPIRES_AT = "client_secret_expires_at";
041    static final String PARAM_REGISTRATION_ACCESS_TOKEN = "registration_access_token";
042    static final String PARAM_REGISTRATION_CLIENT_URI = "registration_client_uri";
043    static final String PARAM_CLIENT_ID_ISSUED_AT = "client_id_issued_at";
044    static final String PARAM_TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method";
045
046    static final String KEY_REQUEST = "request";
047    static final String KEY_ADDITIONAL_PARAMETERS = "additionalParameters";
048
049    private static final Set<String> BUILT_IN_PARAMS = new HashSet<>(Arrays.asList(
050            PARAM_CLIENT_ID,
051            PARAM_CLIENT_SECRET,
052            PARAM_CLIENT_SECRET_EXPIRES_AT,
053            PARAM_REGISTRATION_ACCESS_TOKEN,
054            PARAM_REGISTRATION_CLIENT_URI,
055            PARAM_CLIENT_ID_ISSUED_AT,
056            PARAM_TOKEN_ENDPOINT_AUTH_METHOD
057    ));
058
059    /**
060     * The registration request associated with this response.
061     */
062    @NonNull
063    public final RegistrationRequest request;
064
065    /**
066     * The registered client identifier.
067     *
068     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4
069     * <https://tools.ietf.org/html/rfc6749#section-4>"
070     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.1 <https://tools.ietf.org/html/rfc6749#section-4.1.1>"
071     */
072    @NonNull
073    public final String clientId;
074
075    /**
076     * Timestamp of when the client identifier was issued, if provided.
077     *
078     * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2
079     * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>"
080     */
081    @Nullable
082    public final Long clientIdIssuedAt;
083
084    /**
085     * The client secret, which is part of the client credentials, if provided.
086     *
087     * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2
088     * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>"
089     */
090    @Nullable
091    public final String clientSecret;
092
093    /**
094     * Timestamp of when the client credentials expires, if provided.
095     *
096     * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2
097     * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>"
098     */
099    @Nullable
100    public final Long clientSecretExpiresAt;
101
102    /**
103     * Client registration access token that can be used for subsequent operations upon the client
104     * registration.
105     *
106     * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2
107     * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>"
108     */
109    @Nullable
110    public final String registrationAccessToken;
111
112    /**
113     * Location of the client configuration endpoint, if provided.
114     *
115     * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2
116     * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>"
117     */
118    @Nullable
119    public final Uri registrationClientUri;
120
121    /**
122     * Client authentication method to use at the token endpoint, if provided.
123     *
124     * @see "OpenID Connect Core 1.0, Section 9
125     * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.9>"
126     */
127    @Nullable
128    public final String tokenEndpointAuthMethod;
129
130    /**
131     * Additional, non-standard parameters in the response.
132     */
133    @NonNull
134    public final Map<String, String> additionalParameters;
135
136    /**
137     * Thrown when a mandatory property is missing from the registration response.
138     */
139    public static class MissingArgumentException extends Exception {
140        private String mMissingField;
141
142        /**
143         * Indicates that the specified mandatory field is missing from the registration response.
144         */
145        public MissingArgumentException(String field) {
146            super("Missing mandatory registration field: " + field);
147            mMissingField = field;
148        }
149
150        public String getMissingField() {
151            return mMissingField;
152        }
153    }
154
155    public static final class Builder {
156        @NonNull
157        private RegistrationRequest mRequest;
158        @NonNull
159        private String mClientId;
160
161        @Nullable
162        private Long mClientIdIssuedAt;
163        @Nullable
164        private String mClientSecret;
165        @Nullable
166        private Long mClientSecretExpiresAt;
167        @Nullable
168        private String mRegistrationAccessToken;
169        @Nullable
170        private Uri mRegistrationClientUri;
171        @Nullable
172        private String mTokenEndpointAuthMethod;
173
174        @NonNull
175        private Map<String, String> mAdditionalParameters = Collections.emptyMap();
176
177        /**
178         * Creates a token response associated with the specified request.
179         */
180        public Builder(@NonNull RegistrationRequest request) {
181            setRequest(request);
182        }
183
184        /**
185         * Specifies the request associated with this response. Must not be null.
186         */
187        @NonNull
188        public Builder setRequest(@NonNull RegistrationRequest request) {
189            mRequest = checkNotNull(request, "request cannot be null");
190            return this;
191        }
192
193        /**
194         * Specifies the client identifier.
195         *
196         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4
197         * <https://tools.ietf.org/html/rfc6749#section-4>"
198         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.1
199         * <https://tools.ietf.org/html/rfc6749#section-4.1.1>"
200         */
201        public Builder setClientId(@NonNull String clientId) {
202            checkNotEmpty(clientId, "client ID cannot be null or empty");
203            mClientId = clientId;
204            return this;
205        }
206
207        /**
208         * Specifies the timestamp for when the client identifier was issued.
209         *
210         * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2
211         * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>"
212         */
213        public Builder setClientIdIssuedAt(@Nullable Long clientIdIssuedAt) {
214            mClientIdIssuedAt = clientIdIssuedAt;
215            return this;
216        }
217
218        /**
219         * Specifies the client secret.
220         *
221         * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2
222         * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>"
223         */
224        public Builder setClientSecret(@Nullable String clientSecret) {
225            mClientSecret = clientSecret;
226            return this;
227        }
228
229        /**
230         * Specifies the expiration time of the client secret.
231         *
232         * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2
233         * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>"
234         */
235        public Builder setClientSecretExpiresAt(@Nullable Long clientSecretExpiresAt) {
236            mClientSecretExpiresAt = clientSecretExpiresAt;
237            return this;
238        }
239
240        /**
241         * Specifies the registration access token.
242         *
243         * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2
244         * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>"
245         */
246        public Builder setRegistrationAccessToken(@Nullable String registrationAccessToken) {
247            mRegistrationAccessToken = registrationAccessToken;
248            return this;
249        }
250
251        /**
252         * Specifies the client authentication method to use at the token endpoint.
253         */
254        public Builder setTokenEndpointAuthMethod(@Nullable String tokenEndpointAuthMethod) {
255            mTokenEndpointAuthMethod = tokenEndpointAuthMethod;
256            return this;
257        }
258
259        /**
260         * Specifies the client configuration endpoint.
261         *
262         * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2
263         * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>"
264         */
265        public Builder setRegistrationClientUri(@Nullable Uri registrationClientUri) {
266            mRegistrationClientUri = registrationClientUri;
267            return this;
268        }
269
270        /**
271         * Specifies the additional, non-standard parameters received as part of the response.
272         */
273        public Builder setAdditionalParameters(Map<String, String> additionalParameters) {
274            mAdditionalParameters = checkAdditionalParams(additionalParameters, BUILT_IN_PARAMS);
275            return this;
276        }
277
278        /**
279         * Creates the token response instance.
280         */
281        public RegistrationResponse build() {
282            return new RegistrationResponse(
283                    mRequest,
284                    mClientId,
285                    mClientIdIssuedAt,
286                    mClientSecret,
287                    mClientSecretExpiresAt,
288                    mRegistrationAccessToken,
289                    mRegistrationClientUri,
290                    mTokenEndpointAuthMethod,
291                    mAdditionalParameters);
292        }
293
294        /**
295         * Extracts registration response fields from a JSON string.
296         *
297         * @throws JSONException if the JSON is malformed or has incorrect value types for fields.
298         * @throws MissingArgumentException if the JSON is missing fields required by the
299         *     specification.
300         */
301        @NonNull
302        public Builder fromResponseJsonString(@NonNull String jsonStr)
303                throws JSONException, MissingArgumentException {
304            checkNotEmpty(jsonStr, "json cannot be null or empty");
305            return fromResponseJson(new JSONObject(jsonStr));
306        }
307
308        /**
309         * Extracts token response fields from a JSON object.
310         *
311         * @throws JSONException if the JSON is malformed or has incorrect value types for fields.
312         * @throws MissingArgumentException if the JSON is missing fields required by the
313         *     specification.
314         */
315        @NonNull
316        public Builder fromResponseJson(@NonNull JSONObject json)
317                throws JSONException, MissingArgumentException {
318            setClientId(JsonUtil.getString(json, PARAM_CLIENT_ID));
319            setClientIdIssuedAt(JsonUtil.getLongIfDefined(json, PARAM_CLIENT_ID_ISSUED_AT));
320
321            if (json.has(PARAM_CLIENT_SECRET)) {
322                if (!json.has(PARAM_CLIENT_SECRET_EXPIRES_AT)) {
323                    /*
324                     * From OpenID Connect Dynamic Client Registration, Section 3.2:
325                     * client_secret_expires_at: "REQUIRED if client_secret is issued"
326                     */
327                    throw new MissingArgumentException(PARAM_CLIENT_SECRET_EXPIRES_AT);
328                }
329                setClientSecret(json.getString(PARAM_CLIENT_SECRET));
330                setClientSecretExpiresAt(json.getLong(PARAM_CLIENT_SECRET_EXPIRES_AT));
331            }
332
333            if (json.has(PARAM_REGISTRATION_ACCESS_TOKEN)
334                    != json.has(PARAM_REGISTRATION_CLIENT_URI)) {
335                /*
336                 * From OpenID Connect Dynamic Client Registration, Section 3.2:
337                 * "Implementations MUST either return both a Client Configuration Endpoint and a
338                 * Registration Access Token or neither of them."
339                 */
340                String missingParameter = json.has(PARAM_REGISTRATION_ACCESS_TOKEN)
341                        ? PARAM_REGISTRATION_CLIENT_URI : PARAM_REGISTRATION_ACCESS_TOKEN;
342                throw new MissingArgumentException(missingParameter);
343            }
344
345            setRegistrationAccessToken(JsonUtil.getStringIfDefined(json,
346                    PARAM_REGISTRATION_ACCESS_TOKEN));
347            setRegistrationClientUri(JsonUtil.getUriIfDefined(json, PARAM_REGISTRATION_CLIENT_URI));
348            setTokenEndpointAuthMethod(JsonUtil.getStringIfDefined(json,
349                    PARAM_TOKEN_ENDPOINT_AUTH_METHOD));
350
351            setAdditionalParameters(extractAdditionalParams(json, BUILT_IN_PARAMS));
352            return this;
353        }
354    }
355
356    private RegistrationResponse(
357            @NonNull RegistrationRequest request,
358            @NonNull String clientId,
359            @Nullable Long clientIdIssuedAt,
360            @Nullable String clientSecret,
361            @Nullable Long clientSecretExpiresAt,
362            @Nullable String registrationAccessToken,
363            @Nullable Uri registrationClientUri,
364            @Nullable String tokenEndpointAuthMethod,
365            @NonNull Map<String, String> additionalParameters) {
366        this.request = request;
367        this.clientId = clientId;
368        this.clientIdIssuedAt = clientIdIssuedAt;
369        this.clientSecret = clientSecret;
370        this.clientSecretExpiresAt = clientSecretExpiresAt;
371        this.registrationAccessToken = registrationAccessToken;
372        this.registrationClientUri = registrationClientUri;
373        this.tokenEndpointAuthMethod = tokenEndpointAuthMethod;
374        this.additionalParameters = additionalParameters;
375    }
376
377    /**
378     * Reads a registration response JSON string received from an authorization server,
379     * and associates it with the provided request.
380     *
381     * @throws JSONException if the JSON is malformed or missing required fields.
382     * @throws MissingArgumentException if the JSON is missing fields required by the specification.
383     */
384    @NonNull
385    public static RegistrationResponse fromJson(
386            @NonNull RegistrationRequest request, @NonNull String jsonStr)
387            throws JSONException, MissingArgumentException {
388        checkNotEmpty(jsonStr, "jsonStr cannot be null or empty");
389        return fromJson(request, new JSONObject(jsonStr));
390    }
391
392    /**
393     * Reads a registration response JSON object received from an authorization server,
394     * and associates it with the provided request.
395     *
396     * @throws JSONException if the JSON is malformed or missing required fields.
397     * @throws MissingArgumentException if the JSON is missing fields required by the specification.
398     */
399    @NonNull
400    public static RegistrationResponse fromJson(
401            @NonNull RegistrationRequest request,
402            @NonNull JSONObject json)
403            throws JSONException, MissingArgumentException {
404        checkNotNull(request, "registration request cannot be null");
405        return new RegistrationResponse.Builder(request)
406                .fromResponseJson(json)
407                .build();
408    }
409
410    /**
411     * Produces a JSON representation of the registration response for persistent storage or
412     * local transmission (e.g. between activities).
413     */
414    @NonNull
415    public JSONObject jsonSerialize() {
416        JSONObject json = new JSONObject();
417        JsonUtil.put(json, KEY_REQUEST, request.jsonSerialize());
418        JsonUtil.put(json, PARAM_CLIENT_ID, clientId);
419        JsonUtil.putIfNotNull(json, PARAM_CLIENT_ID_ISSUED_AT, clientIdIssuedAt);
420        JsonUtil.putIfNotNull(json, PARAM_CLIENT_SECRET, clientSecret);
421        JsonUtil.putIfNotNull(json, PARAM_CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt);
422        JsonUtil.putIfNotNull(json, PARAM_REGISTRATION_ACCESS_TOKEN, registrationAccessToken);
423        JsonUtil.putIfNotNull(json, PARAM_REGISTRATION_CLIENT_URI, registrationClientUri);
424        JsonUtil.putIfNotNull(json, PARAM_TOKEN_ENDPOINT_AUTH_METHOD, tokenEndpointAuthMethod);
425        JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS,
426                JsonUtil.mapToJsonObject(additionalParameters));
427        return json;
428    }
429
430    /**
431     * Produces a JSON string representation of the registration response for persistent storage or
432     * local transmission (e.g. between activities). This method is just a convenience wrapper
433     * for {@link #jsonSerialize()}, converting the JSON object to its string form.
434     */
435    @NonNull
436    public String jsonSerializeString() {
437        return jsonSerialize().toString();
438    }
439
440    /**
441     * Reads a registration response from a JSON string representation produced by
442     * {@link #jsonSerialize()}.
443     *
444     * @throws JSONException if the provided JSON does not match the expected structure.
445     */
446    public static RegistrationResponse jsonDeserialize(@NonNull JSONObject json)
447            throws JSONException {
448        checkNotNull(json, "json cannot be null");
449        if (!json.has(KEY_REQUEST)) {
450            throw new IllegalArgumentException("registration request not found in JSON");
451        }
452
453        return new RegistrationResponse(
454                RegistrationRequest.jsonDeserialize(json.getJSONObject(KEY_REQUEST)),
455                JsonUtil.getString(json, PARAM_CLIENT_ID),
456                JsonUtil.getLongIfDefined(json, PARAM_CLIENT_ID_ISSUED_AT),
457                JsonUtil.getStringIfDefined(json, PARAM_CLIENT_SECRET),
458                JsonUtil.getLongIfDefined(json, PARAM_CLIENT_SECRET_EXPIRES_AT),
459                JsonUtil.getStringIfDefined(json, PARAM_REGISTRATION_ACCESS_TOKEN),
460                JsonUtil.getUriIfDefined(json, PARAM_REGISTRATION_CLIENT_URI),
461                JsonUtil.getStringIfDefined(json, PARAM_TOKEN_ENDPOINT_AUTH_METHOD),
462                JsonUtil.getStringMap(json, KEY_ADDITIONAL_PARAMETERS));
463    }
464
465    /**
466     * Reads a registration response from a JSON string representation produced by
467     * {@link #jsonSerializeString()}. This method is just a convenience wrapper for
468     * {@link #jsonDeserialize(JSONObject)}, converting the JSON string to its JSON object form.
469     *
470     * @throws JSONException if the provided JSON does not match the expected structure.
471     */
472    @NonNull
473    public static RegistrationResponse jsonDeserialize(@NonNull String jsonStr)
474            throws JSONException {
475        checkNotEmpty(jsonStr, "jsonStr cannot be null or empty");
476        return jsonDeserialize(new JSONObject(jsonStr));
477    }
478
479    /**
480     * Determines whether the returned access token has expired.
481     */
482    public boolean hasClientSecretExpired() {
483        return hasClientSecretExpired(SystemClock.INSTANCE);
484    }
485
486    @VisibleForTesting
487    boolean hasClientSecretExpired(@NonNull Clock clock) {
488        Long now = TimeUnit.MILLISECONDS.toSeconds(checkNotNull(clock).getCurrentTimeMillis());
489        return clientSecretExpiresAt != null && now > clientSecretExpiresAt;
490
491    }
492}