diff --git a/library/java/net/openid/appauth/AuthorizationException.java b/library/java/net/openid/appauth/AuthorizationException.java index e2057ce7..60604adf 100644 --- a/library/java/net/openid/appauth/AuthorizationException.java +++ b/library/java/net/openid/appauth/AuthorizationException.java @@ -401,6 +401,63 @@ public static AuthorizationException byString(String error) { } } + /** + * Error codes related to failed revoke token requests. + * + * @see "OAuth 2.0 Token Revocation" (RFC 7009), Section 2.2.1 + * " + */ + public static final class RevokeTokenRequestErrors { + // codes in this group should be between 3000-3999 + + /** + * An `invalid_request` OAuth2 error response. + */ + public static final AuthorizationException INVALID_REQUEST = + tokenEx(3000, "invalid_request"); + + /** + * An `unsupported_token_type` OAuth2 error response. + */ + public static final AuthorizationException UNSUPPORTED_TOKEN_TYPE = + tokenEx(3001, "unsupported_token_type"); + + /** + * An authorization error occurring on the client rather than the server. For example, + * due to client misconfiguration. This error should be treated as unrecoverable. + */ + public static final AuthorizationException CLIENT_ERROR = + tokenEx(3002, null); + + /** + * Indicates an OAuth error as per RFC 7009, but the error code is not known to the + * AppAuth for Android library. It could be a custom error or code, or one from an + * OAuth extension. The {@link #error} field provides the exact error string returned by + * the server. + */ + public static final AuthorizationException OTHER = + tokenEx(3003, null); + + private static final Map STRING_TO_EXCEPTION = + exceptionMapByString( + INVALID_REQUEST, + UNSUPPORTED_TOKEN_TYPE, + CLIENT_ERROR, + OTHER); + + /** + * Returns the matching exception type for the provided OAuth2 error string, or + * {@link #OTHER} if unknown. + */ + public static AuthorizationException byString(String error) { + AuthorizationException ex = STRING_TO_EXCEPTION.get(error); + if (ex != null) { + return ex; + } + return OTHER; + } + } + /** * Error codes related to failed registration requests. */ diff --git a/library/java/net/openid/appauth/AuthorizationService.java b/library/java/net/openid/appauth/AuthorizationService.java index 14e3438c..f0bca234 100644 --- a/library/java/net/openid/appauth/AuthorizationService.java +++ b/library/java/net/openid/appauth/AuthorizationService.java @@ -33,6 +33,7 @@ import net.openid.appauth.AuthorizationException.GeneralErrors; import net.openid.appauth.AuthorizationException.RegistrationRequestErrors; +import net.openid.appauth.AuthorizationException.RevokeTokenRequestErrors; import net.openid.appauth.AuthorizationException.TokenRequestErrors; import net.openid.appauth.IdToken.IdTokenException; import net.openid.appauth.browser.BrowserDescriptor; @@ -511,6 +512,34 @@ public void performRegistrationRequest( .execute(); } + /** + * Sends a request to the authorization service to revoke a token. + * The result of this request will be sent to the provided callback handler. + */ + public void performRevokeToken( + @NonNull RevokeTokenRequest request, + @NonNull RevokeTokenResponseCallback callback) { + performRevokeToken(request, NoClientAuthentication.INSTANCE, callback); + } + + /** + * Sends a request to the authorization service to revoke a token. + * The result of this request will be sent to the provided callback handler. + */ + public void performRevokeToken( + @NonNull RevokeTokenRequest request, + @NonNull ClientAuthentication clientAuthentication, + @NonNull RevokeTokenResponseCallback callback) { + checkNotDisposed(); + Logger.debug("Initiating token revocation"); + new RevokeTokenRequestTask( + request, + clientAuthentication, + mClientConfiguration.getConnectionBuilder(), + callback) + .execute(); + } + /** * Disposes state that will not normally be handled by garbage collection. This should be * called when the authorization service is no longer required, including when any owning @@ -859,4 +888,131 @@ public interface RegistrationResponseCallback { void onRegistrationRequestCompleted(@Nullable RegistrationResponse response, @Nullable AuthorizationException ex); } + + private static class RevokeTokenRequestTask + extends AsyncTask { + private RevokeTokenRequest mRequest; + private final ClientAuthentication mClientAuthentication; + private final ConnectionBuilder mConnectionBuilder; + private RevokeTokenResponseCallback mCallback; + + private AuthorizationException mException; + + RevokeTokenRequestTask(RevokeTokenRequest request, + @NonNull ClientAuthentication clientAuthentication, + @NonNull ConnectionBuilder connectionBuilder, + RevokeTokenResponseCallback callback) { + mRequest = request; + mClientAuthentication = clientAuthentication; + mConnectionBuilder = connectionBuilder; + mCallback = callback; + } + + + @Override + protected JSONObject doInBackground(Void... voids) { + InputStream is = null; + try { + HttpURLConnection conn = mConnectionBuilder.openConnection( + mRequest.configuration.revocationEndpoint); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setDoOutput(true); + + Map headers = mClientAuthentication + .getRequestHeaders(mRequest.clientId); + if (headers != null) { + for (Map.Entry header : headers.entrySet()) { + conn.setRequestProperty(header.getKey(), header.getValue()); + } + } + + Map parameters = mRequest.getRequestParameters(); + Map clientAuthParams = mClientAuthentication + .getRequestParameters(mRequest.clientId); + if (clientAuthParams != null) { + parameters.putAll(clientAuthParams); + } + + String queryData = UriUtil.formUrlEncode(parameters); + conn.setRequestProperty("Content-Length", String.valueOf(queryData.length())); + OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); + + wr.write(queryData); + wr.flush(); + + if (conn.getResponseCode() >= HttpURLConnection.HTTP_MULT_CHOICE) { + is = conn.getErrorStream(); + String response = Utils.readInputStream(is); + return new JSONObject(response); + } + return null; + } catch (IOException ex) { + Logger.debugWithStack(ex, "Failed to complete revocation request"); + mException = AuthorizationException.fromTemplate( + GeneralErrors.NETWORK_ERROR, ex); + } catch (JSONException ex) { + Logger.debugWithStack(ex, "Failed to complete revocation request"); + mException = AuthorizationException.fromTemplate( + GeneralErrors.JSON_DESERIALIZATION_ERROR, ex); + } finally { + Utils.closeQuietly(is); + } + return null; + } + + @Override + protected void onPostExecute(JSONObject json) { + if (mException != null) { + mCallback.onRevokeTokenRequestCompleted(null, mException); + return; + } + + if (json != null && json.has(AuthorizationException.PARAM_ERROR)) { + AuthorizationException ex; + try { + String error = json.getString(AuthorizationException.PARAM_ERROR); + ex = AuthorizationException.fromOAuthTemplate( + RevokeTokenRequestErrors.byString(error), + error, + json.optString(AuthorizationException.PARAM_ERROR_DESCRIPTION, null), + UriUtil.parseUriIfAvailable( + json.optString(AuthorizationException.PARAM_ERROR_URI))); + } catch (JSONException jsonEx) { + ex = AuthorizationException.fromTemplate( + GeneralErrors.JSON_DESERIALIZATION_ERROR, + jsonEx); + } + mCallback.onRevokeTokenRequestCompleted(null, ex); + return; + } + + RevokeTokenResponse response = new RevokeTokenResponse.Builder(mRequest).build(); + Logger.debug("Token revocation with %s completed", + mRequest.configuration.revocationEndpoint); + mCallback.onRevokeTokenRequestCompleted(response, null); + } + } + + /** + * Callback interface for token revocation requests. + * + * @see AuthorizationService#performRevokeToken + */ + public interface RevokeTokenResponseCallback { + /** + * Invoked when the request completes successfully or fails. + * + * Exactly one of `response` or `ex` will be non-null. If `response` is `null`, a failure + * occurred during the request. This can happen if an invalid URI was provided, no + * connection to the server could be established, or the response JSON was incomplete or + * incorrectly formatted. + * + * @param response the retrieved token revocation response, if successful; `null` otherwise. + * @param ex a description of the failure, if one occurred: `null` otherwise. + * @see AuthorizationException.RevokeTokenRequestErrors + */ + void onRevokeTokenRequestCompleted(@Nullable RevokeTokenResponse response, + @Nullable AuthorizationException ex); + } } diff --git a/library/java/net/openid/appauth/AuthorizationServiceConfiguration.java b/library/java/net/openid/appauth/AuthorizationServiceConfiguration.java index b962e68e..4458ac6a 100644 --- a/library/java/net/openid/appauth/AuthorizationServiceConfiguration.java +++ b/library/java/net/openid/appauth/AuthorizationServiceConfiguration.java @@ -62,6 +62,7 @@ public class AuthorizationServiceConfiguration { private static final String KEY_REGISTRATION_ENDPOINT = "registrationEndpoint"; private static final String KEY_DISCOVERY_DOC = "discoveryDoc"; private static final String KEY_END_SESSION_ENPOINT = "endSessionEndpoint"; + private static final String KEY_REVOCATION_ENDPOINT = "revocationEndpoint"; /** * The authorization service's endpoint. @@ -81,6 +82,12 @@ public class AuthorizationServiceConfiguration { @Nullable public final Uri endSessionEndpoint; + /** + * The authorization service's token revocation endpoint; + */ + @Nullable + public final Uri revocationEndpoint; + /** * The authorization service's client registration endpoint. */ @@ -146,10 +153,36 @@ public AuthorizationServiceConfiguration( @NonNull Uri tokenEndpoint, @Nullable Uri registrationEndpoint, @Nullable Uri endSessionEndpoint) { + this(authorizationEndpoint, tokenEndpoint, registrationEndpoint, endSessionEndpoint, null); + } + + /** + * Creates a service configuration for a basic OAuth2 provider. + * @param authorizationEndpoint The + * [authorization endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.1) + * for the service. + * @param tokenEndpoint The + * [token endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.2) + * for the service. + * @param registrationEndpoint The optional + * [client registration endpoint URI](https://tools.ietf.org/html/rfc7591#section-3) + * @param endSessionEndpoint The optional + * [end session endpoint URI](https://tools.ietf.org/html/rfc6749#section-2.2) + * for the service. + * @param revocationEndpoint The optional + * [revocation endpoint URI](https://datatracker.ietf.org/doc/html/rfc7009) + * for the service. */ + public AuthorizationServiceConfiguration( + @NonNull Uri authorizationEndpoint, + @NonNull Uri tokenEndpoint, + @Nullable Uri registrationEndpoint, + @Nullable Uri endSessionEndpoint, + @Nullable Uri revocationEndpoint) { this.authorizationEndpoint = checkNotNull(authorizationEndpoint); this.tokenEndpoint = checkNotNull(tokenEndpoint); this.registrationEndpoint = registrationEndpoint; this.endSessionEndpoint = endSessionEndpoint; + this.revocationEndpoint = revocationEndpoint; this.discoveryDoc = null; } @@ -167,6 +200,8 @@ public AuthorizationServiceConfiguration( this.tokenEndpoint = discoveryDoc.getTokenEndpoint(); this.registrationEndpoint = discoveryDoc.getRegistrationEndpoint(); this.endSessionEndpoint = discoveryDoc.getEndSessionEndpoint(); + this.revocationEndpoint = discoveryDoc.getRevocationEndpoint(); + } /** @@ -183,6 +218,9 @@ public JSONObject toJson() { if (endSessionEndpoint != null) { JsonUtil.put(json, KEY_END_SESSION_ENPOINT, endSessionEndpoint.toString()); } + if (revocationEndpoint != null) { + JsonUtil.put(json, KEY_REVOCATION_ENDPOINT, revocationEndpoint.toString()); + } if (discoveryDoc != null) { JsonUtil.put(json, KEY_DISCOVERY_DOC, discoveryDoc.docJson); } @@ -224,7 +262,8 @@ public static AuthorizationServiceConfiguration fromJson(@NonNull JSONObject jso JsonUtil.getUri(json, KEY_AUTHORIZATION_ENDPOINT), JsonUtil.getUri(json, KEY_TOKEN_ENDPOINT), JsonUtil.getUriIfDefined(json, KEY_REGISTRATION_ENDPOINT), - JsonUtil.getUriIfDefined(json, KEY_END_SESSION_ENPOINT)); + JsonUtil.getUriIfDefined(json, KEY_END_SESSION_ENPOINT), + JsonUtil.getUriIfDefined(json, KEY_REVOCATION_ENDPOINT)); } } diff --git a/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java b/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java index f0d02263..9930424b 100644 --- a/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java +++ b/library/java/net/openid/appauth/AuthorizationServiceDiscovery.java @@ -53,6 +53,9 @@ public class AuthorizationServiceDiscovery { @VisibleForTesting static final UriField END_SESSION_ENDPOINT = uri("end_session_endpoint"); + @VisibleForTesting + static final UriField REVOCATION_ENDPOINT = uri("revocation_endpoint"); + @VisibleForTesting static final UriField USERINFO_ENDPOINT = uri("userinfo_endpoint"); @@ -268,6 +271,13 @@ public Uri getEndSessionEndpoint() { return get(END_SESSION_ENDPOINT); } + /** + * The OAuth 2 revocation endpoint URI. Not specified test OAuth implementation + */ + public Uri getRevocationEndpoint() { + return get(REVOCATION_ENDPOINT); + } + /** * The OpenID Connect UserInfo endpoint URI. */ diff --git a/library/java/net/openid/appauth/RevokeTokenRequest.java b/library/java/net/openid/appauth/RevokeTokenRequest.java new file mode 100644 index 00000000..748114d0 --- /dev/null +++ b/library/java/net/openid/appauth/RevokeTokenRequest.java @@ -0,0 +1,320 @@ +/* + * Copyright 2021 The AppAuth for Android Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openid.appauth; + +import static net.openid.appauth.AdditionalParamsProcessor.checkAdditionalParams; +import static net.openid.appauth.Preconditions.checkNotEmpty; +import static net.openid.appauth.Preconditions.checkNotNull; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * An OAuth2 token revocation request. The request is used to revoke both refresh and access tokens. + * + * @see "OAuth 2.0 Token Revocation (RFC 7009), Section 2.1 + * " + */ +public class RevokeTokenRequest { + + @VisibleForTesting + static final String KEY_CONFIGURATION = "configuration"; + @VisibleForTesting + static final String KEY_CLIENT_ID = "clientId"; + @VisibleForTesting + static final String KEY_TOKEN_TYPE_HINT = "tokenTypeHint"; + @VisibleForTesting + static final String KEY_TOKEN = "token"; + @VisibleForTesting + static final String KEY_ADDITIONAL_PARAMETERS = "additionalParameters"; + + public static final String PARAM_CLIENT_ID = "client_id"; + + @VisibleForTesting + static final String PARAM_TOKEN = "token"; + + @VisibleForTesting + static final String PARAM_TOKEN_TYPE_HINT = "token_type_hint"; + + private static final Set BUILT_IN_PARAMS = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + PARAM_CLIENT_ID, + PARAM_TOKEN, + PARAM_TOKEN_TYPE_HINT))); + + /** + * The token type used to revoke an access token. + * + * @see "OAuth 2.0 Token Revocation (RFC 7009), Section 2.1 + * " + */ + public static final String TOKEN_TYPE_ACCESS = "access_token"; + + /** + * The token type used to revoke a refresh token. + * + * @see "OAuth 2.0 Token Revocation (RFC 7009), Section 2.1 + * " + */ + public static final String TOKEN_TYPE_REFRESH = "refresh_token"; + + /** + * The service's {@link AuthorizationServiceConfiguration configuration}. + * This configuration specifies how to connect to a particular OAuth provider. + * Configurations may be + * {@link + * AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri, Uri, Uri, Uri) + * created manually}, or + * {@link AuthorizationServiceConfiguration#fetchFromUrl(Uri, + * AuthorizationServiceConfiguration.RetrieveConfigurationCallback) + * via an OpenID Connect Discovery Document}. + */ + @NonNull + public final AuthorizationServiceConfiguration configuration; + + /** + * The client identifier. + * + * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4 + * " + * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.1 + * " + */ + @NonNull + public final String clientId; + + /** + * The (optional) token type hint of the token to revoke. + * + * @see "OAuth 2.0 Token Revocation (RFC 7009), Section 2.1 + * " + */ + @Nullable + public final String tokenTypeHint; + + /** + * The token to be revoked. + * + * @see "OAuth 2.0 Token Revocation (RFC 7009), Section 2.1 + * " + */ + @NonNull + public final String token; + + /** + * Additional parameters to be passed as part of the request. + */ + @NonNull + public final Map additionalParameters; + + /** + * Creates instances of {@link RevokeTokenRequest}. + */ + public static final class Builder { + + @NonNull + private AuthorizationServiceConfiguration mConfiguration; + + @NonNull + private String mClientId; + + @Nullable + private String mTokenTypeHint; + + @NonNull + private String mToken; + + @NonNull + private Map mAdditionalParameters; + + /** + * Creates a token revocation request builder with the specified mandatory properties. + */ + public Builder( + @NonNull AuthorizationServiceConfiguration configuration, + @NonNull String clientId, + @NonNull String token) { + setConfiguration(configuration); + setClientId(clientId); + setToken(token); + mAdditionalParameters = new LinkedHashMap<>(); + } + + /** + * Specifies the authorization service configuration for the request, which must not + * be null or empty. + */ + @NonNull + public Builder setConfiguration(@NonNull AuthorizationServiceConfiguration configuration) { + mConfiguration = checkNotNull(configuration); + return this; + } + + /** + * Specifies the client ID for the token revocation request, which must not be null or + * empty. + */ + @NonNull + public Builder setClientId(@NonNull String clientId) { + mClientId = checkNotEmpty(clientId, "clientId cannot be null or empty"); + return this; + } + + /** + * Specifies the (optional) token type hint for the token to revoke. + */ + @NonNull + public Builder setTokenTypeHint(@Nullable String tokenTypeHint) { + mTokenTypeHint = tokenTypeHint; + return this; + } + + /** + * Specifies the token to be revoked. + */ + @NonNull + public Builder setToken(@NonNull String token) { + mToken = checkNotEmpty(token, "token cannot be null or empty");; + return this; + } + + /** + * Specifies an additional set of parameters to be sent as part of the request. + */ + @NonNull + public Builder setAdditionalParameters(@Nullable Map additionalParameters) { + mAdditionalParameters = checkAdditionalParams(additionalParameters, BUILT_IN_PARAMS); + return this; + } + + /** + * Produces a {@link RevokeTokenRequest} instance. + */ + @NonNull + public RevokeTokenRequest build() { + return new RevokeTokenRequest( + mConfiguration, + mClientId, + mTokenTypeHint, + mToken, + Collections.unmodifiableMap(mAdditionalParameters)); + } + } + + private RevokeTokenRequest( + @NonNull AuthorizationServiceConfiguration configuration, + @NonNull String clientId, + @Nullable String tokenTypeHint, + @NonNull String token, + @NonNull Map additionalParameters) { + this.configuration = configuration; + this.clientId = clientId; + this.tokenTypeHint = tokenTypeHint; + this.token = token; + this.additionalParameters = additionalParameters; + } + + /** + * Produces the set of request parameters for this query, which can be further + * processed into a request body. + */ + @NonNull + public Map getRequestParameters() { + Map params = new HashMap<>(); + params.put(PARAM_TOKEN, token); + putIfNotNull(params, PARAM_TOKEN_TYPE_HINT, tokenTypeHint); + + for (Map.Entry param : additionalParameters.entrySet()) { + params.put(param.getKey(), param.getValue()); + } + + return params; + } + + private void putIfNotNull(Map map, String key, Object value) { + if (value != null) { + map.put(key, value.toString()); + } + } + + /** + * Produces a JSON string representation of the token revocation request for persistent storage + * or local transmission (e.g. between activities). + */ + @NonNull + public JSONObject jsonSerialize() { + JSONObject json = new JSONObject(); + JsonUtil.put(json, KEY_CONFIGURATION, configuration.toJson()); + JsonUtil.put(json, KEY_CLIENT_ID, clientId); + JsonUtil.putIfNotNull(json, KEY_TOKEN_TYPE_HINT, tokenTypeHint); + JsonUtil.put(json, KEY_TOKEN, token); + JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS, + JsonUtil.mapToJsonObject(additionalParameters)); + return json; + } + + /** + * Produces a JSON string representation of the token revocation request for persistent storage + * or local transmission (e.g. between activities). This method is just a convenience wrapper + * for {@link #jsonSerialize()}, converting the JSON object to its string form. + */ + @NonNull + public String jsonSerializeString() { + return jsonSerialize().toString(); + } + + /** + * Reads a token revocation request from a JSON string representation produced by + * {@link #jsonSerialize()}. + * @throws JSONException if the provided JSON does not match the expected structure. + */ + @NonNull + public static RevokeTokenRequest jsonDeserialize(JSONObject json) throws JSONException { + checkNotNull(json, "json object cannot be null"); + + return new RevokeTokenRequest( + AuthorizationServiceConfiguration.fromJson(json.getJSONObject(KEY_CONFIGURATION)), + JsonUtil.getString(json, KEY_CLIENT_ID), + JsonUtil.getStringIfDefined(json, KEY_TOKEN_TYPE_HINT), + JsonUtil.getString(json, KEY_TOKEN), + JsonUtil.getStringMap(json, KEY_ADDITIONAL_PARAMETERS)); + } + + /** + * Reads a token revocation request from a JSON string representation produced by + * {@link #jsonSerializeString()}. This method is just a convenience wrapper for + * {@link #jsonDeserialize(JSONObject)}, converting the JSON string to its JSON object form. + * @throws JSONException if the provided JSON does not match the expected structure. + */ + @NonNull + public static RevokeTokenRequest jsonDeserialize(@NonNull String json) throws JSONException { + checkNotNull(json, "json string cannot be null"); + return jsonDeserialize(new JSONObject(json)); + } +} diff --git a/library/java/net/openid/appauth/RevokeTokenResponse.java b/library/java/net/openid/appauth/RevokeTokenResponse.java new file mode 100644 index 00000000..b98d1b1b --- /dev/null +++ b/library/java/net/openid/appauth/RevokeTokenResponse.java @@ -0,0 +1,128 @@ +/* + * Copyright 2021 The AppAuth for Android Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openid.appauth; + +import static net.openid.appauth.Preconditions.checkNotEmpty; +import static net.openid.appauth.Preconditions.checkNotNull; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * A response to a token revocation request. + * + * @see RevokeTokenRequest + * @see "OAuth 2.0 Token Revocation (RFC 7009), Section 2.2 + * " + */ +public class RevokeTokenResponse { + + @VisibleForTesting + static final String KEY_REQUEST = "request"; + + /** + * The token revocation request associated with this response. + */ + @NonNull + public final RevokeTokenRequest request; + + /** + * Creates instances of {@link TokenResponse}. + */ + public static final class Builder { + @NonNull + private RevokeTokenRequest mRequest; + + /** + * Creates a token response associated with the specified request. + */ + public Builder(@NonNull RevokeTokenRequest request) { + setRequest(request); + } + + /** + * Specifies the request associated with this response. Must not be null. + */ + @NonNull + public Builder setRequest(@NonNull RevokeTokenRequest request) { + mRequest = checkNotNull(request, "request cannot be null"); + return this; + } + + /** + * Creates the token revocation response instance. + */ + public RevokeTokenResponse build() { + return new RevokeTokenResponse(mRequest); + } + } + + RevokeTokenResponse( + @NonNull RevokeTokenRequest request) { + this.request = request; + } + + /** + * Produces a JSON string representation of the token revocation response for persistent storage + * or local transmission (e.g. between activities). + */ + public JSONObject jsonSerialize() { + JSONObject json = new JSONObject(); + JsonUtil.put(json, KEY_REQUEST, request.jsonSerialize()); + return json; + } + + /** + * Produces a JSON string representation of the token revocation response for persistent storage + * or local transmission (e.g. between activities). This method is just a convenience wrapper + * for {@link #jsonSerialize()}, converting the JSON object to its string form. + */ + public String jsonSerializeString() { + return jsonSerialize().toString(); + } + + /** + * Reads a token revocation response from a JSON string, and associates it with the provided + * request. If a request is not provided, its serialized form is expected to be found in the + * JSON (as if produced by a prior call to {@link #jsonSerialize()}. + * @throws JSONException if the JSON is malformed or missing required fields. + */ + @NonNull + public static RevokeTokenResponse jsonDeserialize(@NonNull JSONObject json) + throws JSONException { + if (!json.has(KEY_REQUEST)) { + throw new IllegalArgumentException( + "token revocation request not provided and not found in JSON"); + } + return new RevokeTokenResponse( + RevokeTokenRequest.jsonDeserialize(json.getJSONObject(KEY_REQUEST))); + } + + /** + * Reads a token revocation response from a JSON string, and associates it with the provided + * request. If a request is not provided, its serialized form is expected to be found in the + * JSON (as if produced by a prior call to {@link #jsonSerialize()}. + * @throws JSONException if the JSON is malformed or missing required fields. + */ + @NonNull + public static RevokeTokenResponse jsonDeserialize(@NonNull String jsonStr) + throws JSONException { + checkNotEmpty(jsonStr, "jsonStr cannot be null or empty"); + return jsonDeserialize(new JSONObject(jsonStr)); + } +} diff --git a/library/javatests/net/openid/appauth/AuthorizationServiceConfigurationTest.java b/library/javatests/net/openid/appauth/AuthorizationServiceConfigurationTest.java index 00d5c8f2..1004bee1 100644 --- a/library/javatests/net/openid/appauth/AuthorizationServiceConfigurationTest.java +++ b/library/javatests/net/openid/appauth/AuthorizationServiceConfigurationTest.java @@ -59,6 +59,7 @@ public class AuthorizationServiceConfigurationTest { private static final String TEST_AUTH_ENDPOINT = "https://test.openid.com/o/oauth/auth"; private static final String TEST_TOKEN_ENDPOINT = "https://test.openid.com/o/oauth/token"; private static final String TEST_END_SESSION_ENDPOINT = "https://test.openid.com/o/oauth/logout"; + private static final String TEST_REVOCATION_ENDPOINT = "https://test.openid.com/o/oauth/revoke"; private static final String TEST_REGISTRATION_ENDPOINT = "https://test.openid.com/o/oauth/registration"; private static final String TEST_USERINFO_ENDPOINT = "https://test.openid.com/o/oauth/userinfo"; private static final String TEST_JWKS_URI = "https://test.openid.com/o/oauth/jwks"; @@ -77,6 +78,7 @@ public class AuthorizationServiceConfigurationTest { + " \"token_endpoint\": \"" + TEST_TOKEN_ENDPOINT + "\",\n" + " \"registration_endpoint\": \"" + TEST_REGISTRATION_ENDPOINT + "\",\n" + " \"end_session_endpoint\": \"" + TEST_END_SESSION_ENDPOINT + "\",\n" + + " \"revocation_endpoint\": \"" + TEST_REVOCATION_ENDPOINT + "\",\n" + " \"userinfo_endpoint\": \"" + TEST_USERINFO_ENDPOINT + "\",\n" + " \"jwks_uri\": \"" + TEST_JWKS_URI + "\",\n" + " \"response_types_supported\": " + toJson(TEST_RESPONSE_TYPE_SUPPORTED) + ",\n" @@ -127,7 +129,8 @@ public void setUp() throws Exception { Uri.parse(TEST_AUTH_ENDPOINT), Uri.parse(TEST_TOKEN_ENDPOINT), Uri.parse(TEST_REGISTRATION_ENDPOINT), - Uri.parse(TEST_END_SESSION_ENDPOINT)); + Uri.parse(TEST_END_SESSION_ENDPOINT), + Uri.parse(TEST_REVOCATION_ENDPOINT)); when(mConnectionBuilder.openConnection(any(Uri.class))).thenReturn(mHttpConnection); mPausedExecutorService = new PausedExecutorService(); @@ -164,6 +167,7 @@ public void testSerializationWithoutRegistrationEndpoint() throws Exception { assertThat(deserialized.tokenEndpoint).isEqualTo(config.tokenEndpoint); assertThat(deserialized.registrationEndpoint).isNull(); assertThat(deserialized.endSessionEndpoint).isEqualTo(config.endSessionEndpoint); + assertThat(deserialized.revocationEndpoint).isEqualTo(config.revocationEndpoint); } @Test @@ -177,6 +181,23 @@ public void testSerializationWithoutRegistrationEndpointAndEndSessionEndpoint() assertThat(deserialized.tokenEndpoint).isEqualTo(config.tokenEndpoint); assertThat(deserialized.endSessionEndpoint).isNull(); assertThat(deserialized.registrationEndpoint).isNull(); + assertThat(deserialized.revocationEndpoint).isNull(); + } + + @Test + public void testSerializationWithoutRevocationEndpoint() throws Exception { + AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration( + Uri.parse(TEST_AUTH_ENDPOINT), + Uri.parse(TEST_TOKEN_ENDPOINT), + Uri.parse(TEST_REGISTRATION_ENDPOINT), + Uri.parse(TEST_END_SESSION_ENDPOINT)); + AuthorizationServiceConfiguration deserialized = AuthorizationServiceConfiguration + .fromJson(config.toJson()); + assertThat(deserialized.authorizationEndpoint).isEqualTo(config.authorizationEndpoint); + assertThat(deserialized.tokenEndpoint).isEqualTo(config.tokenEndpoint); + assertThat(deserialized.registrationEndpoint).isEqualTo(config.registrationEndpoint); + assertThat(deserialized.endSessionEndpoint).isEqualTo(config.endSessionEndpoint); + assertThat(deserialized.revocationEndpoint).isNull(); } @Test @@ -202,6 +223,8 @@ private void assertMembers(AuthorizationServiceConfiguration config) { assertEquals(TEST_TOKEN_ENDPOINT, config.tokenEndpoint.toString()); assertEquals(TEST_REGISTRATION_ENDPOINT, config.registrationEndpoint.toString()); assertEquals(TEST_END_SESSION_ENDPOINT, config.endSessionEndpoint.toString()); + assertEquals(TEST_REVOCATION_ENDPOINT, config.revocationEndpoint.toString()); + } @Test diff --git a/library/javatests/net/openid/appauth/AuthorizationServiceDiscoveryTest.java b/library/javatests/net/openid/appauth/AuthorizationServiceDiscoveryTest.java index 3e2e2788..4d4cd835 100644 --- a/library/javatests/net/openid/appauth/AuthorizationServiceDiscoveryTest.java +++ b/library/javatests/net/openid/appauth/AuthorizationServiceDiscoveryTest.java @@ -39,6 +39,7 @@ public class AuthorizationServiceDiscoveryTest { static final String TEST_USERINFO_ENDPOINT = "http://test.openid.com/o/oauth/userinfo"; static final String TEST_REGISTRATION_ENDPOINT = "http://test.openid.com/o/oauth/register"; static final String TEST_END_SESSION_ENDPOINT = "http://test.openid.com/o/oauth/logout"; + static final String TEST_REVOCATION_ENDPOINT = "http://test.openid.com/o/oauth/revoke"; static final String TEST_JWKS_URI = "http://test.openid.com/o/oauth/jwks"; static final List TEST_RESPONSE_TYPES_SUPPORTED = Arrays.asList("code", "token"); static final List TEST_SUBJECT_TYPES_SUPPORTED = Arrays.asList("public"); @@ -55,6 +56,7 @@ public class AuthorizationServiceDiscoveryTest { TEST_USERINFO_ENDPOINT, TEST_REGISTRATION_ENDPOINT, TEST_END_SESSION_ENDPOINT, + TEST_REVOCATION_ENDPOINT, TEST_JWKS_URI, TEST_RESPONSE_TYPES_SUPPORTED, TEST_SUBJECT_TYPES_SUPPORTED, diff --git a/library/javatests/net/openid/appauth/AuthorizationServiceTest.java b/library/javatests/net/openid/appauth/AuthorizationServiceTest.java index 2010f836..14d26bf0 100644 --- a/library/javatests/net/openid/appauth/AuthorizationServiceTest.java +++ b/library/javatests/net/openid/appauth/AuthorizationServiceTest.java @@ -29,6 +29,7 @@ import net.openid.appauth.AppAuthConfiguration.Builder; import net.openid.appauth.AuthorizationException.GeneralErrors; +import net.openid.appauth.AuthorizationException.RevokeTokenRequestErrors; import net.openid.appauth.browser.BrowserDescriptor; import net.openid.appauth.browser.Browsers; import net.openid.appauth.browser.CustomTabManager; @@ -78,6 +79,7 @@ import static net.openid.appauth.TestValues.getTestEndSessionRequestBuilder; import static net.openid.appauth.TestValues.getTestIdTokenWithNonce; import static net.openid.appauth.TestValues.getTestRegistrationRequest; +import static net.openid.appauth.TestValues.getTestRevokeTokenRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -113,6 +115,11 @@ public class AuthorizationServiceTest { + " \"error_description\": \"invalid_grant description\"\n" + "}"; + private static final String UNSUPPORTED_TOKEN_TYPE_RESPONSE_JSON = "{\n" + + " \"error\": \"unsupported_token_type\",\n" + + " \"error_description\": \"unsupported_token_type description\"\n" + + "}"; + private static final String INVALID_GRANT_NO_DESC_RESPONSE_JSON = "{\n" + " \"error\": \"invalid_grant\"\n" + "}"; @@ -122,6 +129,7 @@ public class AuthorizationServiceTest { private AutoCloseable mMockitoCloseable; private AuthorizationCallback mAuthCallback; private RegistrationCallback mRegistrationCallback; + private RevokeTokenCallback mRevokeTokenCallback; private AuthorizationService mService; private OutputStream mOutputStream; private BrowserDescriptor mBrowserDescriptor; @@ -139,6 +147,7 @@ public void setUp() throws Exception { mMockitoCloseable = MockitoAnnotations.openMocks(this); mAuthCallback = new AuthorizationCallback(); mRegistrationCallback = new RegistrationCallback(); + mRevokeTokenCallback = new RevokeTokenCallback(); mBrowserDescriptor = Browsers.Chrome.customTab("46"); mService = new AuthorizationService( mContext, @@ -231,6 +240,51 @@ public void testEndSessionRequest_customization() throws Exception { assertColorMatch(intent, Color.GREEN); } + @Test + public void testRevokeTokenRequest() throws Exception { + InputStream is = new ByteArrayInputStream("\n".getBytes()); + when(mHttpConnection.getInputStream()).thenReturn(is); + RevokeTokenRequest request = getTestRevokeTokenRequest(); + mService.performRevokeToken(request, mRevokeTokenCallback); + mPausedExecutorService.runAll(); + shadowOf(getMainLooper()).idle(); + assertRevokeTokenResponse(mRevokeTokenCallback.response, request); + String postBody = mOutputStream.toString(); + + Map params = UriUtil.formUrlDecodeUnique(postBody); + + for (Map.Entry requestParam : request.getRequestParameters().entrySet()) { + assertThat(params).containsEntry(requestParam.getKey(), requestParam.getValue()); + } + + assertThat(params).containsEntry(TokenRequest.PARAM_CLIENT_ID, request.clientId); + } + + @Test + public void testRevokeTokenRequest_IoException() throws Exception { + Exception ex = new IOException(); + // revoke token request has no input stream body so use response code to test + when(mHttpConnection.getResponseCode()).thenThrow(ex); + mService.performRevokeToken(getTestRevokeTokenRequest(), mRevokeTokenCallback); + mPausedExecutorService.runAll(); + shadowOf(getMainLooper()).idle(); + assertNotNull(mRevokeTokenCallback.error); + assertEquals(GeneralErrors.NETWORK_ERROR, mRevokeTokenCallback.error); + } + + @Test + public void testRevokeTokenRequest_withUnsupportedTokenType() throws Exception { + InputStream is = new ByteArrayInputStream(UNSUPPORTED_TOKEN_TYPE_RESPONSE_JSON.getBytes()); + when(mHttpConnection.getErrorStream()).thenReturn(is); + when(mHttpConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST); + mService.performRevokeToken(getTestRevokeTokenRequest(), mRevokeTokenCallback); + mPausedExecutorService.runAll(); + shadowOf(getMainLooper()).idle(); + assertNotNull(mRevokeTokenCallback.error); + assertThat(mRevokeTokenCallback.error) + .isEqualTo(RevokeTokenRequestErrors.UNSUPPORTED_TOKEN_TYPE); + } + @Test(expected = IllegalStateException.class) public void testAuthorizationRequest_afterDispose() throws Exception { mService.dispose(); @@ -537,6 +591,12 @@ private void assertRegistrationResponse(RegistrationResponse response, assertThat(response.clientSecretExpiresAt).isEqualTo(TEST_CLIENT_SECRET_EXPIRES_AT); } + private void assertRevokeTokenResponse(RevokeTokenResponse response, + RevokeTokenRequest expectedRequest) { + assertThat(response).isNotNull(); + assertThat(response.request).isEqualTo(expectedRequest); + } + private void assertTokenRequestBody( String requestBody, Map expectedParameters) { Uri postBody = new Uri.Builder().encodedQuery(requestBody).build(); @@ -560,6 +620,20 @@ public void onTokenRequestCompleted( } } + private static class RevokeTokenCallback implements + AuthorizationService.RevokeTokenResponseCallback { + public RevokeTokenResponse response; + public AuthorizationException error; + + @Override + public void onRevokeTokenRequestCompleted(@Nullable RevokeTokenResponse revokeTokenResponse, + @Nullable AuthorizationException ex) { + assertTrue((revokeTokenResponse == null) ^ (ex == null)); + this.response = revokeTokenResponse; + this.error = ex; + } + } + private static class RegistrationCallback implements AuthorizationService.RegistrationResponseCallback { public RegistrationResponse response; diff --git a/library/javatests/net/openid/appauth/IdTokenTest.java b/library/javatests/net/openid/appauth/IdTokenTest.java index 5014650c..db4644db 100644 --- a/library/javatests/net/openid/appauth/IdTokenTest.java +++ b/library/javatests/net/openid/appauth/IdTokenTest.java @@ -23,6 +23,7 @@ import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_JWKS_URI; import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_REGISTRATION_ENDPOINT; import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_RESPONSE_TYPES_SUPPORTED; +import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_REVOCATION_ENDPOINT; import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_SCOPES_SUPPORTED; import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_SUBJECT_TYPES_SUPPORTED; import static net.openid.appauth.AuthorizationServiceDiscoveryTest.TEST_TOKEN_ENDPOINT; @@ -463,6 +464,7 @@ private String getDiscoveryDocJsonWithIssuer(String issuer) { TEST_USERINFO_ENDPOINT, TEST_REGISTRATION_ENDPOINT, TEST_END_SESSION_ENDPOINT, + TEST_REVOCATION_ENDPOINT, TEST_JWKS_URI, TEST_RESPONSE_TYPES_SUPPORTED, TEST_SUBJECT_TYPES_SUPPORTED, diff --git a/library/javatests/net/openid/appauth/RevokeTokenRequestTest.java b/library/javatests/net/openid/appauth/RevokeTokenRequestTest.java new file mode 100644 index 00000000..687f13ea --- /dev/null +++ b/library/javatests/net/openid/appauth/RevokeTokenRequestTest.java @@ -0,0 +1,162 @@ +/* + * Copyright 2021 The AppAuth for Android Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openid.appauth; + +import static net.openid.appauth.TestValues.TEST_CLIENT_ID; +import static net.openid.appauth.TestValues.TEST_ID_TOKEN; +import static net.openid.appauth.TestValues.getTestServiceConfig; +import static org.assertj.core.api.Assertions.assertThat; + +import org.json.JSONException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.HashMap; +import java.util.Map; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 16) +public class RevokeTokenRequestTest { + + private static final Map TEST_ADDITIONAL_PARAMS; + + static { + TEST_ADDITIONAL_PARAMS = new HashMap<>(); + TEST_ADDITIONAL_PARAMS.put("test_key1", "test_value1"); + TEST_ADDITIONAL_PARAMS.put("test_key2", "test_value2"); + } + + private RevokeTokenRequest.Builder mRequestBuilder; + + @Before + public void setUp() throws JSONException { + mRequestBuilder = new RevokeTokenRequest.Builder( + getTestServiceConfig(), + TEST_CLIENT_ID, + TEST_ID_TOKEN); + } + + /* ********************************** Builder() ***********************************************/ + + @Test(expected = NullPointerException.class) + @SuppressWarnings("ConstantConditions") + public void testBuild_nullConfiguration() { + new RevokeTokenRequest.Builder(null, TEST_CLIENT_ID, TEST_ID_TOKEN).build(); + } + + @Test(expected = NullPointerException.class) + @SuppressWarnings("ConstantConditions") + public void testBuild_nullClientId() { + new RevokeTokenRequest.Builder(getTestServiceConfig(), null, TEST_ID_TOKEN); + } + + @Test(expected = NullPointerException.class) + @SuppressWarnings("ConstantConditions") + public void testBuild_nullToken() { + new RevokeTokenRequest.Builder(getTestServiceConfig(), TEST_CLIENT_ID, null); + } + + /* ********************************** tokenTypeHint *******************************************/ + + @Test + public void testBuilder_setTokenTypeHint() { + String tokenTypeHint = RevokeTokenRequest.TOKEN_TYPE_ACCESS; + mRequestBuilder.setTokenTypeHint(tokenTypeHint); + RevokeTokenRequest request = mRequestBuilder.build(); + assertThat(request.tokenTypeHint).isEqualTo(tokenTypeHint); + } + + /* ******************************* additionalParams *******************************************/ + + @Test(expected = IllegalArgumentException.class) + public void testBuilder_setAdditionalParams_withBuiltInParam() { + Map additionalParams = new HashMap<>(); + additionalParams.put(RevokeTokenRequest.PARAM_TOKEN_TYPE_HINT, + RevokeTokenRequest.TOKEN_TYPE_ACCESS); + mRequestBuilder.setAdditionalParameters(additionalParams); + } + + /* ******************************* getRequestParameters() *************************************/ + + @Test + public void testGetRequestParameters_forToken() { + RevokeTokenRequest request = mRequestBuilder.build(); + Map params = request.getRequestParameters(); + assertThat(params).containsEntry( + RevokeTokenRequest.PARAM_TOKEN, + TEST_ID_TOKEN); + } + + @Test + public void testGetRequestParameters_forTokenTypeHint() { + String tokenTypeHint = RevokeTokenRequest.TOKEN_TYPE_ACCESS; + RevokeTokenRequest request = mRequestBuilder + .setTokenTypeHint(tokenTypeHint) + .build(); + Map params = request.getRequestParameters(); + assertThat(params).containsEntry( + RevokeTokenRequest.PARAM_TOKEN_TYPE_HINT, + tokenTypeHint); + } + + @Test + public void testGetRequestParameters_forAdditionalParameters() { + RevokeTokenRequest request = mRequestBuilder + .setAdditionalParameters(TEST_ADDITIONAL_PARAMS) + .build(); + Map params = request.getRequestParameters(); + assertThat(params).containsEntry("test_key1", "test_value1"); + assertThat(params).containsEntry("test_key2", "test_value2"); + } + + /* ************************** jsonSerialize() / jsonDeserialize() *****************************/ + + @Test + public void testJsonSerialize_clientId() throws Exception { + RevokeTokenRequest copy = serializeDeserialize( + mRequestBuilder.setClientId(TEST_CLIENT_ID).build()); + assertThat(copy.clientId).isEqualTo(TEST_CLIENT_ID); + } + + @Test + public void testJsonSerialize_token() throws Exception { + RevokeTokenRequest copy = serializeDeserialize( + mRequestBuilder.setClientId(TEST_ID_TOKEN).build()); + assertThat(copy.token).isEqualTo(TEST_ID_TOKEN); + } + + @Test + public void testJsonSerialize_tokenTypeHint() throws Exception { + String tokenTypeHint = RevokeTokenRequest.TOKEN_TYPE_ACCESS; + RevokeTokenRequest copy = serializeDeserialize( + mRequestBuilder.setTokenTypeHint(tokenTypeHint).build()); + assertThat(copy.tokenTypeHint).isEqualTo(tokenTypeHint); + } + + @Test + public void testJsonSerialize_additionalParams() throws JSONException { + RevokeTokenRequest copy = serializeDeserialize( + mRequestBuilder.setAdditionalParameters(TEST_ADDITIONAL_PARAMS).build()); + assertThat(copy.additionalParameters).isEqualTo(TEST_ADDITIONAL_PARAMS); + } + + private RevokeTokenRequest serializeDeserialize(RevokeTokenRequest request) + throws JSONException { + return RevokeTokenRequest.jsonDeserialize(request.jsonSerializeString()); + } +} diff --git a/library/javatests/net/openid/appauth/RevokeTokenResponseTest.java b/library/javatests/net/openid/appauth/RevokeTokenResponseTest.java new file mode 100644 index 00000000..633b8986 --- /dev/null +++ b/library/javatests/net/openid/appauth/RevokeTokenResponseTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2021 The AppAuth for Android Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openid.appauth; + +import static net.openid.appauth.TestValues.getTestRevokeTokenRequest; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 16) +public class RevokeTokenResponseTest { + + private RevokeTokenResponse.Builder mRevokeTokenResponseBuilder; + private RevokeTokenResponse mRevokeTokenResponse; + + @Before + public void setUp() { + mRevokeTokenResponseBuilder = new RevokeTokenResponse.Builder( + getTestRevokeTokenRequest()); + + mRevokeTokenResponse = mRevokeTokenResponseBuilder.build(); + } + + @Test + public void testSerialization() throws Exception { + String json = mRevokeTokenResponse.jsonSerializeString(); + RevokeTokenResponse revokeTokenResponse = RevokeTokenResponse + .jsonDeserialize(json); + assertThat(revokeTokenResponse.request.jsonSerializeString()) + .isEqualTo(mRevokeTokenResponse.request.jsonSerializeString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testDeserialization_empty() throws Exception { + RevokeTokenResponse.jsonDeserialize(""); + } + + @Test(expected = IllegalArgumentException.class) + public void testDeserialization_withoutRequest() throws Exception { + RevokeTokenResponse.jsonDeserialize("{}"); + } +} diff --git a/library/javatests/net/openid/appauth/TestValues.java b/library/javatests/net/openid/appauth/TestValues.java index 27e57a59..8d6a7e5c 100644 --- a/library/javatests/net/openid/appauth/TestValues.java +++ b/library/javatests/net/openid/appauth/TestValues.java @@ -72,6 +72,7 @@ static String getDiscoveryDocumentJson( String userInfoEndpoint, String registrationEndpoint, String endSessionEndpoint, + String revocationEndpoint, String jwksUri, List responseTypesSupported, List subjectTypesSupported, @@ -86,6 +87,7 @@ static String getDiscoveryDocumentJson( + " \"token_endpoint\": \"" + tokenEndpoint + "\",\n" + " \"userinfo_endpoint\": \"" + userInfoEndpoint + "\",\n" + " \"end_session_endpoint\": \"" + endSessionEndpoint + "\",\n" + + " \"revocation_endpoint\": \"" + revocationEndpoint + "\",\n" + " \"registration_endpoint\": \"" + registrationEndpoint + "\",\n" + " \"jwks_uri\": \"" + jwksUri + "\",\n" + " \"response_types_supported\": " + toJson(responseTypesSupported) + ",\n" @@ -132,6 +134,11 @@ public static EndSessionRequest.Builder getTestEndSessionRequestBuilder() { .setPostLogoutRedirectUri(TEST_APP_REDIRECT_URI); } + public static RevokeTokenRequest.Builder getTestRevokeTokenRequestBuilder() { + return new RevokeTokenRequest.Builder(getTestServiceConfig(), TEST_CLIENT_ID, TEST_ID_TOKEN) + .setTokenTypeHint(RevokeTokenRequest.TOKEN_TYPE_ACCESS); + } + public static AuthorizationRequest getTestAuthRequest() { return getTestAuthRequestBuilder() .setNonce(null) @@ -143,6 +150,11 @@ public static EndSessionRequest getTestEndSessionRequest() { .build(); } + public static RevokeTokenRequest getTestRevokeTokenRequest() { + return getTestRevokeTokenRequestBuilder() + .build(); + } + public static AuthorizationResponse.Builder getTestAuthResponseBuilder() { AuthorizationRequest req = getTestAuthRequest(); return new AuthorizationResponse.Builder(req)