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.AuthorizationException.AuthorizationRequestErrors;
018import static net.openid.appauth.Preconditions.checkArgument;
019import static net.openid.appauth.Preconditions.checkNotEmpty;
020import static net.openid.appauth.Preconditions.checkNotNull;
021
022import androidx.annotation.NonNull;
023import androidx.annotation.Nullable;
024import androidx.annotation.VisibleForTesting;
025
026import net.openid.appauth.internal.Logger;
027import org.json.JSONException;
028import org.json.JSONObject;
029
030import java.util.ArrayList;
031import java.util.Collections;
032import java.util.List;
033import java.util.Map;
034import java.util.Set;
035
036/**
037 * Collects authorization state from authorization requests and responses. This facilitates
038 * the creation of subsequent requests based on this state, and allows for this state to be
039 * persisted easily.
040 */
041public class AuthState {
042
043    /**
044     * Tokens which have less time than this value left before expiry will be considered to be
045     * expired for the purposes of calls to
046     * {@link #performActionWithFreshTokens(AuthorizationService, AuthStateAction)
047     * performActionWithFreshTokens}.
048     */
049    public static final int EXPIRY_TIME_TOLERANCE_MS = 60000;
050
051    private static final String KEY_CONFIG = "config";
052    private static final String KEY_REFRESH_TOKEN = "refreshToken";
053    private static final String KEY_SCOPE = "scope";
054    private static final String KEY_LAST_AUTHORIZATION_RESPONSE = "lastAuthorizationResponse";
055    private static final String KEY_LAST_TOKEN_RESPONSE = "mLastTokenResponse";
056    private static final String KEY_AUTHORIZATION_EXCEPTION = "mAuthorizationException";
057    private static final String KEY_LAST_REGISTRATION_RESPONSE = "lastRegistrationResponse";
058
059    @Nullable
060    private String mRefreshToken;
061
062    @Nullable
063    private String mScope;
064
065    @Nullable
066    private AuthorizationServiceConfiguration mConfig;
067
068    @Nullable
069    private AuthorizationResponse mLastAuthorizationResponse;
070
071    @Nullable
072    private TokenResponse mLastTokenResponse;
073
074    @Nullable
075    private RegistrationResponse mLastRegistrationResponse;
076
077    @Nullable
078    private AuthorizationException mAuthorizationException;
079
080    private final Object mPendingActionsSyncObject = new Object();
081    private List<AuthStateAction> mPendingActions;
082    private boolean mNeedsTokenRefreshOverride;
083
084    /**
085     * Creates an empty, unauthenticated {@link AuthState}.
086     */
087    public AuthState() {}
088
089    /**
090     * Creates an unauthenticated {@link AuthState}, with the service configuration retained
091     * for convenience.
092     */
093    public AuthState(@NonNull AuthorizationServiceConfiguration config) {
094        mConfig = config;
095    }
096
097    /**
098     * Creates an {@link AuthState} based on an authorization exchange.
099     */
100    public AuthState(@Nullable AuthorizationResponse authResponse,
101            @Nullable AuthorizationException authError) {
102        checkArgument(authResponse != null ^ authError != null,
103                "exactly one of authResponse or authError should be non-null");
104        mPendingActions = null;
105        update(authResponse, authError);
106    }
107
108    /**
109     * Creates an {@link AuthState} based on a dynamic registration client registration request.
110     */
111    public AuthState(@NonNull RegistrationResponse regResponse) {
112        update(regResponse);
113    }
114
115    /**
116     * Creates an {@link AuthState} based on an authorization exchange and subsequent token
117     * exchange.
118     */
119    public AuthState(
120            @NonNull AuthorizationResponse authResponse,
121            @Nullable TokenResponse tokenResponse,
122            @Nullable AuthorizationException authException) {
123        this(authResponse, null);
124        update(tokenResponse, authException);
125    }
126
127    /**
128     * The most recent refresh token received from the server, if available. Rather than using
129     * this property directly as part of any request depending on authorization state, it is
130     * recommended to call {@link #performActionWithFreshTokens(AuthorizationService,
131     * AuthStateAction) performActionWithFreshTokens} to ensure that fresh tokens are available.
132     */
133    @Nullable
134    public String getRefreshToken() {
135        return mRefreshToken;
136    }
137
138    /**
139     * The scope of the current authorization grant. This represents the latest scope returned by
140     * the server and may be a subset of the scope that was initially granted.
141     */
142    @Nullable
143    public String getScope() {
144        return mScope;
145    }
146
147    /**
148     * A set representation of {@link #getScope()}, for convenience.
149     */
150    @Nullable
151    public Set<String> getScopeSet() {
152        return AsciiStringListUtil.stringToSet(mScope);
153    }
154
155    /**
156     * The most recent authorization response used to update the authorization state. For the
157     * implicit flow, this will contain the latest access token. It is rarely necessary to
158     * directly use the response; instead convenience methods are provided to retrieve the
159     * {@link #getAccessToken() access token},
160     * {@link #getAccessTokenExpirationTime() access token expiration},
161     * {@link #getIdToken() ID token}
162     * and {@link #getScopeSet() scope} regardless of the flow used to retrieve them.
163     */
164    @Nullable
165    public AuthorizationResponse getLastAuthorizationResponse() {
166        return mLastAuthorizationResponse;
167    }
168
169    /**
170     * The most recent token response used to update this authorization state. For the
171     * authorization code flow, this will contain the latest access token. It is rarely necessary
172     * to directly use the response; instead convenience methods are provided to retrieve the
173     * {@link #getAccessToken() access token},
174     * {@link #getAccessTokenExpirationTime() access token expiration},
175     * {@link #getIdToken() ID token}
176     * and {@link #getScopeSet() scope} regardless of the flow used to retrieve them.
177     */
178    @Nullable
179    public TokenResponse getLastTokenResponse() {
180        return mLastTokenResponse;
181    }
182
183    /**
184     * The most recent client registration response used to update this authorization state.
185     *
186     * <p>
187     * It is rarely necessary to directly use the response; instead convenience methods are provided
188     * to retrieve the {@link #getClientSecret() client secret} and
189     * {@link #getClientSecretExpirationTime() client secret expiration}.
190     * </p>
191     */
192    @Nullable
193    public RegistrationResponse getLastRegistrationResponse() {
194        return mLastRegistrationResponse;
195    }
196
197    /**
198     * The configuration of the authorization service associated with this authorization state.
199     */
200    @Nullable
201    public AuthorizationServiceConfiguration getAuthorizationServiceConfiguration() {
202        if (mLastAuthorizationResponse != null) {
203            return mLastAuthorizationResponse.request.configuration;
204        }
205
206        return mConfig;
207    }
208
209    /**
210     * The current access token, if available. Rather than using
211     * this property directly as part of any request depending on authorization state, it s
212     * recommended to call {@link #performActionWithFreshTokens(AuthorizationService,
213     * AuthStateAction) performActionWithFreshTokens} to ensure that fresh tokens are available.
214     */
215    @Nullable
216    public String getAccessToken() {
217        if (mAuthorizationException != null) {
218            return null;
219        }
220
221        if (mLastTokenResponse != null && mLastTokenResponse.accessToken != null) {
222            return mLastTokenResponse.accessToken;
223        }
224
225        if (mLastAuthorizationResponse != null) {
226            return mLastAuthorizationResponse.accessToken;
227        }
228
229        return null;
230    }
231
232    /**
233     * The expiration time of the current access token (if available), as milliseconds from the
234     * UNIX epoch (consistent with {@link System#currentTimeMillis()}).
235     */
236    @Nullable
237    public Long getAccessTokenExpirationTime() {
238        if (mAuthorizationException != null) {
239            return null;
240        }
241
242        if (mLastTokenResponse != null && mLastTokenResponse.accessToken != null) {
243            return mLastTokenResponse.accessTokenExpirationTime;
244        }
245
246        if (mLastAuthorizationResponse != null && mLastAuthorizationResponse.accessToken != null) {
247            return mLastAuthorizationResponse.accessTokenExpirationTime;
248        }
249
250        return null;
251    }
252
253    /**
254     * The current ID token, if available.
255     */
256    @Nullable
257    public String getIdToken() {
258        if (mAuthorizationException != null) {
259            return null;
260        }
261
262        if (mLastTokenResponse != null && mLastTokenResponse.idToken != null) {
263            return mLastTokenResponse.idToken;
264        }
265
266        if (mLastAuthorizationResponse != null) {
267            return mLastAuthorizationResponse.idToken;
268        }
269
270        return null;
271    }
272
273    /**
274     * The current parsed ID token, if available.
275     */
276    @Nullable
277    public IdToken getParsedIdToken() {
278        String stringToken = getIdToken();
279        IdToken token;
280
281        if (stringToken != null) {
282            try {
283                token = IdToken.from(stringToken);
284            } catch (JSONException | IdToken.IdTokenException ex) {
285                token = null;
286            }
287        } else {
288            token = null;
289        }
290
291        return token;
292    }
293
294    /**
295     * The current client secret, if available.
296     */
297    public String getClientSecret() {
298        if (mLastRegistrationResponse != null) {
299            return mLastRegistrationResponse.clientSecret;
300        }
301
302        return null;
303    }
304
305    /**
306     * The expiration time of the current client credentials (if available), as milliseconds from
307     * the UNIX epoch (consistent with {@link System#currentTimeMillis()}). If the value is 0, the
308     * client credentials will not expire.
309     */
310    @Nullable
311    public Long getClientSecretExpirationTime() {
312        if (mLastRegistrationResponse != null) {
313            return mLastRegistrationResponse.clientSecretExpiresAt;
314        }
315
316        return null;
317    }
318
319    /**
320     * Determines whether the current state represents a successful authorization,
321     * from which at least either an access token or an ID token have been retrieved.
322     */
323    public boolean isAuthorized() {
324        return mAuthorizationException == null
325                && (getAccessToken() != null || getIdToken() != null);
326    }
327
328    /**
329     * If the last response was an OAuth related failure, this returns the exception describing
330     * the failure.
331     */
332    @Nullable
333    public AuthorizationException getAuthorizationException() {
334        return mAuthorizationException;
335    }
336
337    /**
338     * Determines whether the access token is considered to have expired. If no refresh token
339     * has been acquired, then this method will always return `false`. A token refresh
340     * can be forced, regardless of the validity of any currently acquired access token, by
341     * calling {@link #setNeedsTokenRefresh(boolean) setNeedsTokenRefresh(true)}.
342     */
343    public boolean getNeedsTokenRefresh() {
344        return getNeedsTokenRefresh(SystemClock.INSTANCE);
345    }
346
347    @VisibleForTesting
348    boolean getNeedsTokenRefresh(Clock clock) {
349        if (mNeedsTokenRefreshOverride) {
350            return true;
351        }
352
353        if (getAccessTokenExpirationTime() == null) {
354            // if there is no expiration but we have an access token, it is assumed
355            // to never expire.
356            return getAccessToken() == null;
357        }
358
359        return getAccessTokenExpirationTime()
360                <= clock.getCurrentTimeMillis() + EXPIRY_TIME_TOLERANCE_MS;
361    }
362
363    /**
364     * Sets whether to force an access token refresh, regardless of the current access token's
365     * expiration time.
366     */
367    public void setNeedsTokenRefresh(boolean needsTokenRefresh) {
368        mNeedsTokenRefreshOverride = needsTokenRefresh;
369    }
370
371    /**
372    * Determines whether the client credentials is considered to have expired. If no client
373    * credentials have been acquired, then this method will always return `false`
374    */
375    public boolean hasClientSecretExpired() {
376        return hasClientSecretExpired(SystemClock.INSTANCE);
377    }
378
379    @VisibleForTesting
380    boolean hasClientSecretExpired(Clock clock) {
381        if (getClientSecretExpirationTime() == null || getClientSecretExpirationTime() == 0) {
382            // no explicit expiration time, and 0 means it will not expire
383            return false;
384        }
385
386        return getClientSecretExpirationTime() <= clock.getCurrentTimeMillis();
387    }
388
389    /**
390     * Updates the authorization state based on a new authorization response.
391     */
392    public void update(
393            @Nullable AuthorizationResponse authResponse,
394            @Nullable AuthorizationException authException) {
395        checkArgument(authResponse != null ^ authException != null,
396                "exactly one of authResponse or authException should be non-null");
397        if (authException != null) {
398            if (authException.type == AuthorizationException.TYPE_OAUTH_AUTHORIZATION_ERROR) {
399                mAuthorizationException = authException;
400            }
401            return;
402        }
403
404        // the last token response and refresh token are now stale, as they are associated with
405        // any previous authorization response
406        mLastAuthorizationResponse = authResponse;
407        mConfig = null;
408        mLastTokenResponse = null;
409        mRefreshToken = null;
410        mAuthorizationException = null;
411
412        // if the response's mScope is null, it means that it equals that of the request
413        // see: https://tools.ietf.org/html/rfc6749#section-5.1
414        mScope = (authResponse.scope != null) ? authResponse.scope : authResponse.request.scope;
415    }
416
417    /**
418     * Updates the authorization state based on a new token response.
419     */
420    public void update(
421            @Nullable TokenResponse tokenResponse,
422            @Nullable AuthorizationException authException) {
423        checkArgument(tokenResponse != null ^ authException != null,
424                "exactly one of tokenResponse or authException should be non-null");
425
426        if (mAuthorizationException != null) {
427            // Calling updateFromTokenResponse while in an error state probably means the developer
428            // obtained a new token and did the exchange without also calling
429            // updateFromAuthorizationResponse. Attempt to handle this gracefully, but warn the
430            // developer that this is unexpected.
431            Logger.warn(
432                    "AuthState.update should not be called in an error state (%s), call update"
433                            + "with the result of the fresh authorization response first",
434                    mAuthorizationException);
435            mAuthorizationException = null;
436        }
437
438        if (authException != null) {
439            if (authException.type == AuthorizationException.TYPE_OAUTH_TOKEN_ERROR) {
440                mAuthorizationException = authException;
441            }
442            return;
443        }
444
445        mLastTokenResponse = tokenResponse;
446        if (tokenResponse.scope != null) {
447            mScope = tokenResponse.scope;
448        }
449        if (tokenResponse.refreshToken != null) {
450            mRefreshToken = tokenResponse.refreshToken;
451        }
452    }
453
454    /**
455     * Updates the authorization state based on a new client registration response.
456     */
457    public void update(@Nullable RegistrationResponse regResponse) {
458        mLastRegistrationResponse = regResponse;
459
460        // a new client registration will have a new client id, so invalidate the current session.
461        // Note however that we do not discard the configuration; this is likely still applicable.
462        mConfig = getAuthorizationServiceConfiguration();
463
464        mRefreshToken = null;
465        mScope = null;
466        mLastAuthorizationResponse = null;
467        mLastTokenResponse = null;
468        mAuthorizationException = null;
469    }
470
471    /**
472     * Ensures that a non-expired access token is available before invoking the provided action.
473     */
474    public void performActionWithFreshTokens(
475            @NonNull AuthorizationService service,
476            @NonNull AuthStateAction action) {
477        performActionWithFreshTokens(
478                service,
479                NoClientAuthentication.INSTANCE,
480                Collections.<String, String>emptyMap(),
481                SystemClock.INSTANCE,
482                action);
483    }
484
485    /**
486     * Ensures that a non-expired access token is available before invoking the provided action.
487     */
488    public void performActionWithFreshTokens(
489            @NonNull AuthorizationService service,
490            @NonNull ClientAuthentication clientAuth,
491            @NonNull AuthStateAction action) {
492        performActionWithFreshTokens(
493                service,
494                clientAuth,
495                Collections.<String, String>emptyMap(),
496                SystemClock.INSTANCE,
497                action);
498    }
499
500    /**
501     * Ensures that a non-expired access token is available before invoking the provided action.
502     * If a token refresh is required, the provided additional parameters will be included in this
503     * refresh request.
504     */
505    public void performActionWithFreshTokens(
506            @NonNull AuthorizationService service,
507            @NonNull Map<String, String> refreshTokenAdditionalParams,
508            @NonNull AuthStateAction action) {
509        try {
510            performActionWithFreshTokens(
511                    service,
512                    getClientAuthentication(),
513                    refreshTokenAdditionalParams,
514                    SystemClock.INSTANCE,
515                    action);
516        } catch (ClientAuthentication.UnsupportedAuthenticationMethod ex) {
517            action.execute(null, null,
518                    AuthorizationException.fromTemplate(
519                            AuthorizationException.TokenRequestErrors.CLIENT_ERROR, ex));
520        }
521    }
522
523    /**
524     * Ensures that a non-expired access token is available before invoking the provided action.
525     * If a token refresh is required, the provided additional parameters will be included in this
526     * refresh request.
527     */
528    public void performActionWithFreshTokens(
529            @NonNull AuthorizationService service,
530            @NonNull ClientAuthentication clientAuth,
531            @NonNull Map<String, String> refreshTokenAdditionalParams,
532            @NonNull AuthStateAction action) {
533        performActionWithFreshTokens(
534                service,
535                clientAuth,
536                refreshTokenAdditionalParams,
537                SystemClock.INSTANCE,
538                action);
539    }
540
541    @VisibleForTesting
542    void performActionWithFreshTokens(
543            @NonNull final AuthorizationService service,
544            @NonNull final ClientAuthentication clientAuth,
545            @NonNull final Map<String, String> refreshTokenAdditionalParams,
546            @NonNull final Clock clock,
547            @NonNull final AuthStateAction action) {
548        checkNotNull(service, "service cannot be null");
549        checkNotNull(clientAuth, "client authentication cannot be null");
550        checkNotNull(refreshTokenAdditionalParams,
551                "additional params cannot be null");
552        checkNotNull(clock, "clock cannot be null");
553        checkNotNull(action, "action cannot be null");
554
555        if (!getNeedsTokenRefresh(clock)) {
556            action.execute(getAccessToken(), getIdToken(), null);
557            return;
558        }
559
560        if (mRefreshToken == null) {
561            AuthorizationException ex = AuthorizationException.fromTemplate(
562                    AuthorizationRequestErrors.CLIENT_ERROR,
563                    new IllegalStateException("No refresh token available and token have expired"));
564            action.execute(null, null, ex);
565            return;
566        }
567
568        checkNotNull(mPendingActionsSyncObject, "pending actions sync object cannot be null");
569        synchronized (mPendingActionsSyncObject) {
570            //if a token request is currently executing, queue the actions instead
571            if (mPendingActions != null) {
572                mPendingActions.add(action);
573                return;
574            }
575
576            //creates a list of pending actions, starting with the current action
577            mPendingActions = new ArrayList<>();
578            mPendingActions.add(action);
579        }
580
581        service.performTokenRequest(
582                createTokenRefreshRequest(refreshTokenAdditionalParams),
583                clientAuth,
584                new AuthorizationService.TokenResponseCallback() {
585                    @Override
586                    public void onTokenRequestCompleted(
587                            @Nullable TokenResponse response,
588                            @Nullable AuthorizationException ex) {
589                        update(response, ex);
590
591                        String accessToken = null;
592                        String idToken = null;
593                        AuthorizationException exception = null;
594
595                        if (ex == null) {
596                            mNeedsTokenRefreshOverride = false;
597                            accessToken = getAccessToken();
598                            idToken = getIdToken();
599                        } else {
600                            exception = ex;
601                        }
602
603                        //sets pending queue to null and processes all actions in the queue
604                        List<AuthStateAction> actionsToProcess;
605                        synchronized (mPendingActionsSyncObject) {
606                            actionsToProcess = mPendingActions;
607                            mPendingActions = null;
608                        }
609                        for (AuthStateAction action : actionsToProcess) {
610                            action.execute(accessToken, idToken, exception);
611                        }
612                    }
613                });
614    }
615
616    /**
617     * Creates a token request for new tokens using the current refresh token.
618     */
619    @NonNull
620    public TokenRequest createTokenRefreshRequest() {
621        return createTokenRefreshRequest(Collections.<String, String>emptyMap());
622    }
623
624    /**
625     * Creates a token request for new tokens using the current refresh token, adding the
626     * specified additional parameters.
627     */
628    @NonNull
629    public TokenRequest createTokenRefreshRequest(
630            @NonNull Map<String, String> additionalParameters) {
631        if (mRefreshToken == null) {
632            throw new IllegalStateException("No refresh token available for refresh request");
633        }
634        if (mLastAuthorizationResponse == null) {
635            throw new IllegalStateException(
636                    "No authorization configuration available for refresh request");
637        }
638
639        return new TokenRequest.Builder(
640                mLastAuthorizationResponse.request.configuration,
641                mLastAuthorizationResponse.request.clientId)
642                .setGrantType(GrantTypeValues.REFRESH_TOKEN)
643                .setScope(null)
644                .setRefreshToken(mRefreshToken)
645                .setAdditionalParameters(additionalParameters)
646                .build();
647    }
648
649    /**
650     * Produces a JSON representation of the authorization state for persistent storage or local
651     * transmission (e.g. between activities).
652     */
653    public JSONObject jsonSerialize() {
654        JSONObject json = new JSONObject();
655        JsonUtil.putIfNotNull(json, KEY_REFRESH_TOKEN, mRefreshToken);
656        JsonUtil.putIfNotNull(json, KEY_SCOPE, mScope);
657
658        if (mConfig != null) {
659            JsonUtil.put(json, KEY_CONFIG, mConfig.toJson());
660        }
661
662        if (mAuthorizationException != null) {
663            JsonUtil.put(json, KEY_AUTHORIZATION_EXCEPTION, mAuthorizationException.toJson());
664        }
665
666        if (mLastAuthorizationResponse != null) {
667            JsonUtil.put(
668                    json,
669                    KEY_LAST_AUTHORIZATION_RESPONSE,
670                    mLastAuthorizationResponse.jsonSerialize());
671        }
672
673        if (mLastTokenResponse != null) {
674            JsonUtil.put(
675                    json,
676                    KEY_LAST_TOKEN_RESPONSE,
677                    mLastTokenResponse.jsonSerialize());
678        }
679
680        if (mLastRegistrationResponse != null) {
681            JsonUtil.put(
682                    json,
683                    KEY_LAST_REGISTRATION_RESPONSE,
684                    mLastRegistrationResponse.jsonSerialize());
685        }
686
687        return json;
688    }
689
690    /**
691     * Produces a JSON string representation of the authorization state for persistent storage or
692     * local transmission (e.g. between activities). This method is just a convenience wrapper
693     * for {@link #jsonSerialize()}, converting the JSON object to its string form.
694     */
695    public String jsonSerializeString() {
696        return jsonSerialize().toString();
697    }
698
699    /**
700     * Reads an authorization state instance from a JSON string representation produced by
701     * {@link #jsonSerialize()}.
702     * @throws JSONException if the provided JSON does not match the expected structure.
703     */
704    public static AuthState jsonDeserialize(@NonNull JSONObject json) throws JSONException {
705        checkNotNull(json, "json cannot be null");
706
707        AuthState state = new AuthState();
708        state.mRefreshToken = JsonUtil.getStringIfDefined(json, KEY_REFRESH_TOKEN);
709        state.mScope = JsonUtil.getStringIfDefined(json, KEY_SCOPE);
710
711        if (json.has(KEY_CONFIG)) {
712            state.mConfig = AuthorizationServiceConfiguration.fromJson(
713                    json.getJSONObject(KEY_CONFIG));
714        }
715
716        if (json.has(KEY_AUTHORIZATION_EXCEPTION)) {
717            state.mAuthorizationException = AuthorizationException.fromJson(
718                    json.getJSONObject(KEY_AUTHORIZATION_EXCEPTION));
719        }
720
721        if (json.has(KEY_LAST_AUTHORIZATION_RESPONSE)) {
722            state.mLastAuthorizationResponse = AuthorizationResponse.jsonDeserialize(
723                    json.getJSONObject(KEY_LAST_AUTHORIZATION_RESPONSE));
724        }
725
726        if (json.has(KEY_LAST_TOKEN_RESPONSE)) {
727            state.mLastTokenResponse = TokenResponse.jsonDeserialize(
728                    json.getJSONObject(KEY_LAST_TOKEN_RESPONSE));
729        }
730
731        if (json.has(KEY_LAST_REGISTRATION_RESPONSE)) {
732            state.mLastRegistrationResponse = RegistrationResponse.jsonDeserialize(
733                    json.getJSONObject(KEY_LAST_REGISTRATION_RESPONSE));
734        }
735
736        return state;
737    }
738
739    /**
740     * Reads an authorization state instance from a JSON string representation produced by
741     * {@link #jsonSerializeString()}. This method is just a convenience wrapper for
742     * {@link #jsonDeserialize(JSONObject)}, converting the JSON string to its JSON object form.
743     * @throws JSONException if the provided JSON does not match the expected structure.
744     */
745    public static AuthState jsonDeserialize(@NonNull String jsonStr) throws JSONException {
746        checkNotEmpty(jsonStr, "jsonStr cannot be null or empty");
747        return jsonDeserialize(new JSONObject(jsonStr));
748    }
749
750    /**
751     * Interface for actions executed in the context of fresh (non-expired) tokens.
752     * @see #performActionWithFreshTokens(AuthorizationService, AuthStateAction)
753     */
754    public interface AuthStateAction {
755        /**
756         * Executed in the context of fresh (non-expired) tokens. If new tokens were
757         * required to execute the action and could not be acquired, an authorization
758         * exception is provided instead. One or both of the access token and ID token will be
759         * provided, dependent upon the token types previously negotiated.
760         */
761        void execute(
762                @Nullable String accessToken,
763                @Nullable String idToken,
764                @Nullable AuthorizationException ex);
765    }
766
767    /**
768     * Creates the required client authentication for the token endpoint based on information
769     * in the most recent registration response (if it is set).
770     *
771     * @throws ClientAuthentication.UnsupportedAuthenticationMethod if the expected client
772     *     authentication method is unsupported by this client library.
773     */
774    public ClientAuthentication getClientAuthentication() throws
775            ClientAuthentication.UnsupportedAuthenticationMethod {
776        if (getClientSecret() == null) {
777            /* Without client credentials, or unspecified 'token_endpoint_auth_method',
778             * we can never authenticate */
779            return NoClientAuthentication.INSTANCE;
780        } else if (mLastRegistrationResponse.tokenEndpointAuthMethod == null) {
781            /* 'token_endpoint_auth_method': "If omitted, the default is client_secret_basic",
782             * "OpenID Connect Dynamic Client Registration 1.0", Section 2 */
783            return new ClientSecretBasic(getClientSecret());
784        }
785
786        switch (mLastRegistrationResponse.tokenEndpointAuthMethod) {
787            case ClientSecretBasic.NAME:
788                return new ClientSecretBasic(getClientSecret());
789            case ClientSecretPost.NAME:
790                return new ClientSecretPost(getClientSecret());
791            case "none":
792                return NoClientAuthentication.INSTANCE;
793            default:
794                throw new ClientAuthentication.UnsupportedAuthenticationMethod(
795                        mLastRegistrationResponse.tokenEndpointAuthMethod);
796
797        }
798    }
799}