Skip to content

Token Revocation #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions library/java/net/openid/appauth/AuthorizationException.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <https://datatracker.ietf.org/doc/html/rfc7009#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, AuthorizationException> 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.
*/
Expand Down
156 changes: 156 additions & 0 deletions library/java/net/openid/appauth/AuthorizationService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -859,4 +888,131 @@ public interface RegistrationResponseCallback {
void onRegistrationRequestCompleted(@Nullable RegistrationResponse response,
@Nullable AuthorizationException ex);
}

private static class RevokeTokenRequestTask
extends AsyncTask<Void, Void, JSONObject> {
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<String, String> headers = mClientAuthentication
.getRequestHeaders(mRequest.clientId);
if (headers != null) {
for (Map.Entry<String,String> header : headers.entrySet()) {
conn.setRequestProperty(header.getKey(), header.getValue());
}
}

Map<String, String> parameters = mRequest.getRequestParameters();
Map<String, String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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;
}

Expand All @@ -167,6 +200,8 @@ public AuthorizationServiceConfiguration(
this.tokenEndpoint = discoveryDoc.getTokenEndpoint();
this.registrationEndpoint = discoveryDoc.getRegistrationEndpoint();
this.endSessionEndpoint = discoveryDoc.getEndSessionEndpoint();
this.revocationEndpoint = discoveryDoc.getRevocationEndpoint();

}

/**
Expand All @@ -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);
}
Expand Down Expand Up @@ -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));
}
}

Expand Down
10 changes: 10 additions & 0 deletions library/java/net/openid/appauth/AuthorizationServiceDiscovery.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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.
*/
Expand Down
Loading