Skip to content

Commit 5bc2f93

Browse files
author
Victor Solevic
committed
Add Token Revocation implementation
1 parent 5692d83 commit 5bc2f93

13 files changed

+1046
-2
lines changed

library/java/net/openid/appauth/AuthorizationException.java

+57
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,63 @@ public static AuthorizationException byString(String error) {
401401
}
402402
}
403403

404+
/**
405+
* Error codes related to failed revoke token requests.
406+
*
407+
* @see "OAuth 2.0 Token Revocation" (RFC 7009), Section 2.2.1
408+
* <https://datatracker.ietf.org/doc/html/rfc7009#section-2.2.1>"
409+
*/
410+
public static final class RevokeTokenRequestErrors {
411+
// codes in this group should be between 3000-3999
412+
413+
/**
414+
* An `invalid_request` OAuth2 error response.
415+
*/
416+
public static final AuthorizationException INVALID_REQUEST =
417+
tokenEx(3000, "invalid_request");
418+
419+
/**
420+
* An `unsupported_token_type` OAuth2 error response.
421+
*/
422+
public static final AuthorizationException UNSUPPORTED_TOKEN_TYPE =
423+
tokenEx(3001, "unsupported_token_type");
424+
425+
/**
426+
* An authorization error occurring on the client rather than the server. For example,
427+
* due to client misconfiguration. This error should be treated as unrecoverable.
428+
*/
429+
public static final AuthorizationException CLIENT_ERROR =
430+
tokenEx(3002, null);
431+
432+
/**
433+
* Indicates an OAuth error as per RFC 7009, but the error code is not known to the
434+
* AppAuth for Android library. It could be a custom error or code, or one from an
435+
* OAuth extension. The {@link #error} field provides the exact error string returned by
436+
* the server.
437+
*/
438+
public static final AuthorizationException OTHER =
439+
tokenEx(3003, null);
440+
441+
private static final Map<String, AuthorizationException> STRING_TO_EXCEPTION =
442+
exceptionMapByString(
443+
INVALID_REQUEST,
444+
UNSUPPORTED_TOKEN_TYPE,
445+
CLIENT_ERROR,
446+
OTHER);
447+
448+
/**
449+
* Returns the matching exception type for the provided OAuth2 error string, or
450+
* {@link #OTHER} if unknown.
451+
*/
452+
public static AuthorizationException byString(String error) {
453+
AuthorizationException ex = STRING_TO_EXCEPTION.get(error);
454+
if (ex != null) {
455+
return ex;
456+
}
457+
return OTHER;
458+
}
459+
}
460+
404461
/**
405462
* Error codes related to failed registration requests.
406463
*/

library/java/net/openid/appauth/AuthorizationService.java

+156
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
import net.openid.appauth.AuthorizationException.GeneralErrors;
3535
import net.openid.appauth.AuthorizationException.RegistrationRequestErrors;
36+
import net.openid.appauth.AuthorizationException.RevokeTokenRequestErrors;
3637
import net.openid.appauth.AuthorizationException.TokenRequestErrors;
3738
import net.openid.appauth.IdToken.IdTokenException;
3839
import net.openid.appauth.browser.BrowserDescriptor;
@@ -511,6 +512,34 @@ public void performRegistrationRequest(
511512
.execute();
512513
}
513514

515+
/**
516+
* Sends a request to the authorization service to revoke a token.
517+
* The result of this request will be sent to the provided callback handler.
518+
*/
519+
public void performRevokeToken(
520+
@NonNull RevokeTokenRequest request,
521+
@NonNull RevokeTokenResponseCallback callback) {
522+
performRevokeToken(request, NoClientAuthentication.INSTANCE, callback);
523+
}
524+
525+
/**
526+
* Sends a request to the authorization service to revoke a token.
527+
* The result of this request will be sent to the provided callback handler.
528+
*/
529+
public void performRevokeToken(
530+
@NonNull RevokeTokenRequest request,
531+
@NonNull ClientAuthentication clientAuthentication,
532+
@NonNull RevokeTokenResponseCallback callback) {
533+
checkNotDisposed();
534+
Logger.debug("Initiating token revocation");
535+
new RevokeTokenRequestTask(
536+
request,
537+
clientAuthentication,
538+
mClientConfiguration.getConnectionBuilder(),
539+
callback)
540+
.execute();
541+
}
542+
514543
/**
515544
* Disposes state that will not normally be handled by garbage collection. This should be
516545
* called when the authorization service is no longer required, including when any owning
@@ -859,4 +888,131 @@ public interface RegistrationResponseCallback {
859888
void onRegistrationRequestCompleted(@Nullable RegistrationResponse response,
860889
@Nullable AuthorizationException ex);
861890
}
891+
892+
private static class RevokeTokenRequestTask
893+
extends AsyncTask<Void, Void, JSONObject> {
894+
private RevokeTokenRequest mRequest;
895+
private final ClientAuthentication mClientAuthentication;
896+
private final ConnectionBuilder mConnectionBuilder;
897+
private RevokeTokenResponseCallback mCallback;
898+
899+
private AuthorizationException mException;
900+
901+
RevokeTokenRequestTask(RevokeTokenRequest request,
902+
@NonNull ClientAuthentication clientAuthentication,
903+
@NonNull ConnectionBuilder connectionBuilder,
904+
RevokeTokenResponseCallback callback) {
905+
mRequest = request;
906+
mClientAuthentication = clientAuthentication;
907+
mConnectionBuilder = connectionBuilder;
908+
mCallback = callback;
909+
}
910+
911+
912+
@Override
913+
protected JSONObject doInBackground(Void... voids) {
914+
InputStream is = null;
915+
try {
916+
HttpURLConnection conn = mConnectionBuilder.openConnection(
917+
mRequest.configuration.revocationEndpoint);
918+
conn.setRequestMethod("POST");
919+
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
920+
conn.setDoOutput(true);
921+
922+
Map<String, String> headers = mClientAuthentication
923+
.getRequestHeaders(mRequest.clientId);
924+
if (headers != null) {
925+
for (Map.Entry<String,String> header : headers.entrySet()) {
926+
conn.setRequestProperty(header.getKey(), header.getValue());
927+
}
928+
}
929+
930+
Map<String, String> parameters = mRequest.getRequestParameters();
931+
Map<String, String> clientAuthParams = mClientAuthentication
932+
.getRequestParameters(mRequest.clientId);
933+
if (clientAuthParams != null) {
934+
parameters.putAll(clientAuthParams);
935+
}
936+
937+
String queryData = UriUtil.formUrlEncode(parameters);
938+
conn.setRequestProperty("Content-Length", String.valueOf(queryData.length()));
939+
OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
940+
941+
wr.write(queryData);
942+
wr.flush();
943+
944+
if (conn.getResponseCode() >= HttpURLConnection.HTTP_MULT_CHOICE) {
945+
is = conn.getErrorStream();
946+
String response = Utils.readInputStream(is);
947+
return new JSONObject(response);
948+
}
949+
return null;
950+
} catch (IOException ex) {
951+
Logger.debugWithStack(ex, "Failed to complete revocation request");
952+
mException = AuthorizationException.fromTemplate(
953+
GeneralErrors.NETWORK_ERROR, ex);
954+
} catch (JSONException ex) {
955+
Logger.debugWithStack(ex, "Failed to complete revocation request");
956+
mException = AuthorizationException.fromTemplate(
957+
GeneralErrors.JSON_DESERIALIZATION_ERROR, ex);
958+
} finally {
959+
Utils.closeQuietly(is);
960+
}
961+
return null;
962+
}
963+
964+
@Override
965+
protected void onPostExecute(JSONObject json) {
966+
if (mException != null) {
967+
mCallback.onRevokeTokenRequestCompleted(null, mException);
968+
return;
969+
}
970+
971+
if (json != null && json.has(AuthorizationException.PARAM_ERROR)) {
972+
AuthorizationException ex;
973+
try {
974+
String error = json.getString(AuthorizationException.PARAM_ERROR);
975+
ex = AuthorizationException.fromOAuthTemplate(
976+
RevokeTokenRequestErrors.byString(error),
977+
error,
978+
json.optString(AuthorizationException.PARAM_ERROR_DESCRIPTION, null),
979+
UriUtil.parseUriIfAvailable(
980+
json.optString(AuthorizationException.PARAM_ERROR_URI)));
981+
} catch (JSONException jsonEx) {
982+
ex = AuthorizationException.fromTemplate(
983+
GeneralErrors.JSON_DESERIALIZATION_ERROR,
984+
jsonEx);
985+
}
986+
mCallback.onRevokeTokenRequestCompleted(null, ex);
987+
return;
988+
}
989+
990+
RevokeTokenResponse response = new RevokeTokenResponse.Builder(mRequest).build();
991+
Logger.debug("Token revocation with %s completed",
992+
mRequest.configuration.revocationEndpoint);
993+
mCallback.onRevokeTokenRequestCompleted(response, null);
994+
}
995+
}
996+
997+
/**
998+
* Callback interface for token revocation requests.
999+
*
1000+
* @see AuthorizationService#performRevokeToken
1001+
*/
1002+
public interface RevokeTokenResponseCallback {
1003+
/**
1004+
* Invoked when the request completes successfully or fails.
1005+
*
1006+
* Exactly one of `response` or `ex` will be non-null. If `response` is `null`, a failure
1007+
* occurred during the request. This can happen if an invalid URI was provided, no
1008+
* connection to the server could be established, or the response JSON was incomplete or
1009+
* incorrectly formatted.
1010+
*
1011+
* @param response the retrieved token revocation response, if successful; `null` otherwise.
1012+
* @param ex a description of the failure, if one occurred: `null` otherwise.
1013+
* @see AuthorizationException.RevokeTokenRequestErrors
1014+
*/
1015+
void onRevokeTokenRequestCompleted(@Nullable RevokeTokenResponse response,
1016+
@Nullable AuthorizationException ex);
1017+
}
8621018
}

library/java/net/openid/appauth/AuthorizationServiceConfiguration.java

+40-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public class AuthorizationServiceConfiguration {
6262
private static final String KEY_REGISTRATION_ENDPOINT = "registrationEndpoint";
6363
private static final String KEY_DISCOVERY_DOC = "discoveryDoc";
6464
private static final String KEY_END_SESSION_ENPOINT = "endSessionEndpoint";
65+
private static final String KEY_REVOCATION_ENDPOINT = "revocationEndpoint";
6566

6667
/**
6768
* The authorization service's endpoint.
@@ -81,6 +82,12 @@ public class AuthorizationServiceConfiguration {
8182
@Nullable
8283
public final Uri endSessionEndpoint;
8384

85+
/**
86+
* The authorization service's token revocation endpoint;
87+
*/
88+
@Nullable
89+
public final Uri revocationEndpoint;
90+
8491
/**
8592
* The authorization service's client registration endpoint.
8693
*/
@@ -146,10 +153,36 @@ public AuthorizationServiceConfiguration(
146153
@NonNull Uri tokenEndpoint,
147154
@Nullable Uri registrationEndpoint,
148155
@Nullable Uri endSessionEndpoint) {
156+
this(authorizationEndpoint, tokenEndpoint, registrationEndpoint, endSessionEndpoint, null);
157+
}
158+
159+
/**
160+
* Creates a service configuration for a basic OAuth2 provider.
161+
* @param authorizationEndpoint The
162+
* [authorization endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.1)
163+
* for the service.
164+
* @param tokenEndpoint The
165+
* [token endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.2)
166+
* for the service.
167+
* @param registrationEndpoint The optional
168+
* [client registration endpoint URI](https://tools.ietf.org/html/rfc7591#section-3)
169+
* @param endSessionEndpoint The optional
170+
* [end session endpoint URI](https://tools.ietf.org/html/rfc6749#section-2.2)
171+
* for the service.
172+
* @param revocationEndpoint The optional
173+
* [revocation endpoint URI](https://datatracker.ietf.org/doc/html/rfc7009)
174+
* for the service. */
175+
public AuthorizationServiceConfiguration(
176+
@NonNull Uri authorizationEndpoint,
177+
@NonNull Uri tokenEndpoint,
178+
@Nullable Uri registrationEndpoint,
179+
@Nullable Uri endSessionEndpoint,
180+
@Nullable Uri revocationEndpoint) {
149181
this.authorizationEndpoint = checkNotNull(authorizationEndpoint);
150182
this.tokenEndpoint = checkNotNull(tokenEndpoint);
151183
this.registrationEndpoint = registrationEndpoint;
152184
this.endSessionEndpoint = endSessionEndpoint;
185+
this.revocationEndpoint = revocationEndpoint;
153186
this.discoveryDoc = null;
154187
}
155188

@@ -167,6 +200,8 @@ public AuthorizationServiceConfiguration(
167200
this.tokenEndpoint = discoveryDoc.getTokenEndpoint();
168201
this.registrationEndpoint = discoveryDoc.getRegistrationEndpoint();
169202
this.endSessionEndpoint = discoveryDoc.getEndSessionEndpoint();
203+
this.revocationEndpoint = discoveryDoc.getRevocationEndpoint();
204+
170205
}
171206

172207
/**
@@ -183,6 +218,9 @@ public JSONObject toJson() {
183218
if (endSessionEndpoint != null) {
184219
JsonUtil.put(json, KEY_END_SESSION_ENPOINT, endSessionEndpoint.toString());
185220
}
221+
if (revocationEndpoint != null) {
222+
JsonUtil.put(json, KEY_REVOCATION_ENDPOINT, revocationEndpoint.toString());
223+
}
186224
if (discoveryDoc != null) {
187225
JsonUtil.put(json, KEY_DISCOVERY_DOC, discoveryDoc.docJson);
188226
}
@@ -224,7 +262,8 @@ public static AuthorizationServiceConfiguration fromJson(@NonNull JSONObject jso
224262
JsonUtil.getUri(json, KEY_AUTHORIZATION_ENDPOINT),
225263
JsonUtil.getUri(json, KEY_TOKEN_ENDPOINT),
226264
JsonUtil.getUriIfDefined(json, KEY_REGISTRATION_ENDPOINT),
227-
JsonUtil.getUriIfDefined(json, KEY_END_SESSION_ENPOINT));
265+
JsonUtil.getUriIfDefined(json, KEY_END_SESSION_ENPOINT),
266+
JsonUtil.getUriIfDefined(json, KEY_REVOCATION_ENDPOINT));
228267
}
229268
}
230269

library/java/net/openid/appauth/AuthorizationServiceDiscovery.java

+10
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public class AuthorizationServiceDiscovery {
5353
@VisibleForTesting
5454
static final UriField END_SESSION_ENDPOINT = uri("end_session_endpoint");
5555

56+
@VisibleForTesting
57+
static final UriField REVOCATION_ENDPOINT = uri("revocation_endpoint");
58+
5659
@VisibleForTesting
5760
static final UriField USERINFO_ENDPOINT = uri("userinfo_endpoint");
5861

@@ -268,6 +271,13 @@ public Uri getEndSessionEndpoint() {
268271
return get(END_SESSION_ENDPOINT);
269272
}
270273

274+
/**
275+
* The OAuth 2 revocation endpoint URI. Not specified test OAuth implementation
276+
*/
277+
public Uri getRevocationEndpoint() {
278+
return get(REVOCATION_ENDPOINT);
279+
}
280+
271281
/**
272282
* The OpenID Connect UserInfo endpoint URI.
273283
*/

0 commit comments

Comments
 (0)