Skip to content
This repository was archived by the owner on Dec 12, 2018. It is now read-only.

Commit 77da61b

Browse files
author
mrioan
authored
Merge pull request #1261 from stormpath/1247_oauth_revoke
Oauth revoke endpoint
2 parents 77fe4b4 + f9f4887 commit 77da61b

File tree

18 files changed

+385
-67
lines changed

18 files changed

+385
-67
lines changed

extensions/servlet/src/main/java/com/stormpath/sdk/servlet/config/Config.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ public interface Config extends Map<String, String> {
109109

110110
String getAccessTokenUrl();
111111

112+
String getRevokeTokenUrl();
113+
112114
String getUnauthorizedUrl();
113115

114116
boolean isMeEnabled();

extensions/servlet/src/main/java/com/stormpath/sdk/servlet/config/filter/DefaultFilterChainManagerConfigurer.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ public FilterChainManager configure() throws ServletException {
107107
boolean accessTokenChainSpecified = false;
108108
boolean oauthEnabled = config.isOAuthEnabled();
109109

110+
String revokeTokenUrl = config.getRevokeTokenUrl();
111+
String revokeTokenUrlPattern = cleanUri(revokeTokenUrl);
112+
boolean revokeTokenChainSpecified = false;
113+
110114
String unauthorizedUrl = config.getUnauthorizedUrl();
111115
String unauthorizedUrlPattern = cleanUri(unauthorizedUrl);
112116
boolean unauthorizedChainSpecified = false;
@@ -240,6 +244,14 @@ public FilterChainManager configure() throws ServletException {
240244
chainDefinition += Strings.DEFAULT_DELIMITER_CHAR + filterName;
241245
}
242246

247+
} else if (uriPattern.startsWith(revokeTokenUrlPattern)) {
248+
revokeTokenChainSpecified = true;
249+
250+
String filterName = DefaultFilter.revokeToken.name();
251+
if (!chainDefinition.contains(filterName)) {
252+
chainDefinition += Strings.DEFAULT_DELIMITER_CHAR + filterName;
253+
}
254+
243255
} else if (uriPattern.startsWith(unauthorizedUrlPattern)) {
244256
unauthorizedChainSpecified = true;
245257

@@ -321,6 +333,9 @@ public FilterChainManager configure() throws ServletException {
321333
if (!accessTokenChainSpecified && oauthEnabled) {
322334
mgr.createChain(accessTokenUrlPattern, DefaultFilter.accessToken.name());
323335
}
336+
if (!revokeTokenChainSpecified && oauthEnabled) {
337+
mgr.createChain(revokeTokenUrlPattern, DefaultFilter.revokeToken.name());
338+
}
324339
if (!samlChainSpecified && callbackEnabled) {
325340
mgr.createChain(samlUrlPattern, DefaultFilter.saml.name());
326341
mgr.createChain(samlCallbackPattern, DefaultFilter.samlResult.name());
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2017 Stormpath, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.stormpath.sdk.servlet.config.filter;
17+
18+
import com.stormpath.sdk.servlet.config.Config;
19+
import com.stormpath.sdk.servlet.mvc.RevokeTokenController;
20+
21+
/**
22+
* https://github.com/stormpath/stormpath-sdk-java/issues/1247
23+
*
24+
* @since 1.5.0
25+
*/
26+
public class RevokeTokenFilterFactory extends ControllerFilterFactory<RevokeTokenController> {
27+
28+
@Override
29+
protected RevokeTokenController newController() {
30+
return new RevokeTokenController();
31+
}
32+
33+
@Override
34+
protected void configure(RevokeTokenController controller, Config config) throws Exception {
35+
controller.setApplicationResolver(config.getApplicationResolver());
36+
}
37+
}

extensions/servlet/src/main/java/com/stormpath/sdk/servlet/config/impl/DefaultConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public class DefaultConfig implements Config {
8080
public static final String UNAUTHORIZED_URL = "stormpath.web.unauthorized.uri";
8181
public static final String LOGOUT_INVALIDATE_HTTP_SESSION = "stormpath.web.logout.invalidateHttpSession";
8282
public static final String ACCESS_TOKEN_URL = "stormpath.web.oauth2.uri";
83+
public static final String REVOKE_TOKEN_URL = "stormpath.web.oauth2.revoke.uri";
8384
public static final String ACCESS_TOKEN_VALIDATION_STRATEGY = "stormpath.web.oauth2.password.validationStrategy";
8485

8586
protected static final String SERVER_URI_RESOLVER = "stormpath.web.oauth2.origin.authorizer.serverUriResolver";
@@ -274,6 +275,11 @@ public String getAccessTokenUrl() {
274275
return CFG.getString(ACCESS_TOKEN_URL);
275276
}
276277

278+
@Override
279+
public String getRevokeTokenUrl() {
280+
return CFG.getString(REVOKE_TOKEN_URL);
281+
}
282+
277283
@Override
278284
public String getUnauthorizedUrl() {
279285
return CFG.getString(UNAUTHORIZED_URL);

extensions/servlet/src/main/java/com/stormpath/sdk/servlet/event/TokenRevocationRequestEventListener.java

Lines changed: 23 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,28 @@
1515
*/
1616
package com.stormpath.sdk.servlet.event;
1717

18+
import com.stormpath.sdk.application.Application;
1819
import com.stormpath.sdk.client.Client;
1920
import com.stormpath.sdk.impl.ds.InternalDataStore;
21+
import com.stormpath.sdk.impl.error.DefaultError;
22+
import com.stormpath.sdk.lang.Strings;
2023
import com.stormpath.sdk.oauth.AccessToken;
24+
import com.stormpath.sdk.oauth.OAuthRequests;
25+
import com.stormpath.sdk.oauth.OAuthRevocationRequest;
26+
import com.stormpath.sdk.oauth.OAuthRevocationRequestBuilder;
27+
import com.stormpath.sdk.oauth.OAuthTokenRevocators;
2128
import com.stormpath.sdk.oauth.RefreshToken;
29+
import com.stormpath.sdk.oauth.TokenTypeHint;
2230
import com.stormpath.sdk.resource.ResourceException;
2331
import com.stormpath.sdk.servlet.account.event.RegisteredAccountRequestEvent;
2432
import com.stormpath.sdk.servlet.account.event.VerifiedAccountRequestEvent;
33+
import com.stormpath.sdk.servlet.application.ApplicationResolver;
2534
import com.stormpath.sdk.servlet.authc.FailedAuthenticationRequestEvent;
2635
import com.stormpath.sdk.servlet.authc.LogoutRequestEvent;
2736
import com.stormpath.sdk.servlet.authc.SuccessfulAuthenticationRequestEvent;
2837
import com.stormpath.sdk.servlet.client.ClientResolver;
38+
import com.stormpath.sdk.servlet.filter.oauth.OAuthErrorCode;
39+
import com.stormpath.sdk.servlet.filter.oauth.OAuthException;
2940
import com.stormpath.sdk.servlet.http.CookieResolver;
3041
import com.stormpath.sdk.servlet.oauth.impl.JwtTokenSigningKeyResolver;
3142
import io.jsonwebtoken.Claims;
@@ -37,6 +48,7 @@
3748
import org.slf4j.Logger;
3849
import org.slf4j.LoggerFactory;
3950

51+
import javax.servlet.http.HttpServletRequest;
4052
import java.security.Key;
4153
import java.util.LinkedHashMap;
4254
import java.util.Map;
@@ -51,9 +63,7 @@ public class TokenRevocationRequestEventListener implements RequestEventListener
5163
private final TokenExtractor tokenExtractor = new BearerHeaderTokenExtractor();
5264
private final CookieResolver accessTokenCookieResolver = new CookieResolver("access_token");
5365

54-
private final JwtTokenSigningKeyResolver jwtTokenSigningKeyResolver = new JwtTokenSigningKeyResolver();
55-
56-
private Client client = null;
66+
protected ApplicationResolver applicationResolver = ApplicationResolver.INSTANCE;
5767

5868
@Override
5969
public void on(SuccessfulAuthenticationRequestEvent event) {
@@ -78,30 +88,17 @@ public void on(VerifiedAccountRequestEvent event) {
7888
@Override
7989
public void on(LogoutRequestEvent event) {
8090
String jwt = getJwtFromLogoutRequestEvent(event);
81-
if (jwt != null) {
82-
if (this.client == null) {
83-
this.client = ClientResolver.INSTANCE.getClient(event.getRequest()); //will throw if not found
84-
}
85-
86-
Key signingKey = jwtTokenSigningKeyResolver.getSigningKey(event.getRequest(), event.getResponse(), null, SignatureAlgorithm.HS256);
87-
JwsHeader header = Jwts.parser().setSigningKey(signingKey.getEncoded()).parseClaimsJws(jwt).getHeader();
88-
Claims claims = Jwts.parser().setSigningKey(signingKey.getEncoded()).parseClaimsJws(jwt).getBody();
89-
90-
//Let's be sure this jwt is actually an access token otherwise we will have an error when trying to retrieve
91-
//a resource (in order to delete it) that actually is not what we expect
92-
if (isAccessToken(header)) {
93-
gracefullyDeleteRefreshToken((String) claims.get("rti"));
94-
gracefullyDeleteAccessToken(claims.getId());
91+
HttpServletRequest request = event.getRequest();
92+
Application application = applicationResolver.getApplication(request);
93+
if (application != null && jwt != null) {
94+
try {
95+
OAuthRevocationRequest revocationRequest = OAuthRequests.OAUTH_TOKEN_REVOCATION_REQUEST.builder().setToken(jwt).build();
96+
OAuthTokenRevocators.OAUTH_TOKEN_REVOCATOR.forApplication(application).revoke(revocationRequest);
97+
} catch (ResourceException e) {
98+
com.stormpath.sdk.error.Error error = e.getStormpathError();
99+
String message = error.getMessage();
100+
log.warn("There was an error trying to revoke a token", message);
95101
}
96-
//There should never be a refresh token here. Therefore we will not even try to identify if the received JWT is
97-
//a refresh token. That would be a bug in the filter chain as a refresh token should never be used to anything other than
98-
//obtaining a new access token
99-
100-
//Fix for https://github.com/stormpath/stormpath-sdk-java/issues/611
101-
log.debug(
102-
"The current access and refresh tokens for '{}' have been revoked.",
103-
(event.getAccount() != null) ? event.getAccount().getEmail() : "unknown user"
104-
);
105102
}
106103
}
107104

@@ -117,34 +114,4 @@ protected String getJwtFromLogoutRequestEvent(LogoutRequestEvent event) {
117114
return jwt;
118115
}
119116

120-
private boolean isAccessToken(JwsHeader header) {
121-
return header.get("stt").equals("access");
122-
}
123-
124-
private void gracefullyDeleteAccessToken(String accessTokenId) {
125-
try {
126-
String href = "/accessTokens/" + accessTokenId;
127-
Map<String, Object> map = new LinkedHashMap<String, Object>();
128-
map.put("href", href);
129-
AccessToken accessToken = ((InternalDataStore)client.getDataStore()).instantiate(AccessToken.class, map, true);
130-
accessToken.delete();
131-
} catch (ResourceException e) {
132-
//Let's prevent an error to allow the flow to continue
133-
log.warn("There was an error trying to delete access token with ID {}", accessTokenId, e);
134-
}
135-
}
136-
137-
private void gracefullyDeleteRefreshToken(String refreshTokenId) {
138-
try{
139-
String href = "/refreshTokens/" + refreshTokenId;
140-
Map<String, Object> map = new LinkedHashMap<String, Object>();
141-
map.put("href", href);
142-
RefreshToken refreshToken = ((InternalDataStore)client.getDataStore()).instantiate(RefreshToken.class, map, true);
143-
refreshToken.delete();
144-
} catch (ResourceException e) {
145-
//Let's prevent an error to allow the flow to continue, this component is basically a listener that tries to delete
146-
//the current access and refresh tokens on logout, we will only post this error in the log
147-
log.warn("There was an error trying to delete refresh token with ID {}", refreshTokenId, e);
148-
}
149-
}
150117
}

extensions/servlet/src/main/java/com/stormpath/sdk/servlet/filter/DefaultFilter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.stormpath.sdk.servlet.config.filter.LogoutFilterFactory;
3535
import com.stormpath.sdk.servlet.config.filter.MeFilterFactory;
3636
import com.stormpath.sdk.servlet.config.filter.RegisterFilterFactory;
37+
import com.stormpath.sdk.servlet.config.filter.RevokeTokenFilterFactory;
3738
import com.stormpath.sdk.servlet.config.filter.SamlFilterFactory;
3839
import com.stormpath.sdk.servlet.config.filter.SamlResultFilterFactory;
3940
import com.stormpath.sdk.servlet.config.filter.StaticResourceFilterFactory;
@@ -51,6 +52,7 @@
5152
public enum DefaultFilter {
5253

5354
accessToken(ControllerFilter.class, AccessTokenFilterFactory.class),
55+
revokeToken(ControllerFilter.class, RevokeTokenFilterFactory.class),
5456
account(AccountAuthorizationFilter.class, AccountAuthorizationFilterFactory.class),
5557
anon(AnonymousFilter.class, null),
5658
authc(AuthenticationFilter.class, AuthenticationFilterFactory.class),
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2017 Stormpath, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.stormpath.sdk.servlet.mvc;
17+
18+
import com.stormpath.sdk.application.Application;
19+
import com.stormpath.sdk.http.HttpMethod;
20+
import com.stormpath.sdk.impl.error.DefaultError;
21+
import com.stormpath.sdk.lang.Strings;
22+
import com.stormpath.sdk.oauth.OAuthRequests;
23+
import com.stormpath.sdk.oauth.OAuthRevocationRequest;
24+
import com.stormpath.sdk.oauth.OAuthRevocationRequestBuilder;
25+
import com.stormpath.sdk.oauth.OAuthTokenRevocators;
26+
import com.stormpath.sdk.oauth.TokenTypeHint;
27+
import com.stormpath.sdk.resource.ResourceException;
28+
import com.stormpath.sdk.servlet.filter.oauth.OAuthErrorCode;
29+
import com.stormpath.sdk.servlet.filter.oauth.OAuthException;
30+
import com.stormpath.sdk.servlet.http.MediaType;
31+
import org.slf4j.Logger;
32+
import org.slf4j.LoggerFactory;
33+
34+
import javax.servlet.http.HttpServletRequest;
35+
import javax.servlet.http.HttpServletResponse;
36+
37+
/**
38+
* https://github.com/stormpath/stormpath-sdk-java/issues/1247
39+
*
40+
* @since 1.5.0
41+
*/
42+
public class RevokeTokenController extends AbstractController {
43+
44+
private static final Logger log = LoggerFactory.getLogger(RevokeTokenController.class);
45+
46+
private final static String TOKEN = "token";
47+
private final static String TOKEN_TYPE_HINT = "token_type_hint";
48+
49+
public void init() {
50+
}
51+
52+
@Override
53+
public boolean isNotAllowedIfAuthenticated() {
54+
return false;
55+
}
56+
57+
@Override
58+
public ViewModel handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
59+
String method = request.getMethod();
60+
61+
if (HttpMethod.POST.name().equalsIgnoreCase(method)) {
62+
return doPost(request, response);
63+
}
64+
65+
return super.handleRequest(request, response);
66+
}
67+
68+
@Override
69+
protected ViewModel doPost(HttpServletRequest request, HttpServletResponse response) throws Exception {
70+
71+
OAuthRevocationRequestBuilder builder = OAuthRequests.OAUTH_TOKEN_REVOCATION_REQUEST.builder();
72+
73+
response.setHeader("Cache-Control", "no-store, no-cache");
74+
response.setHeader("Pragma", "no-cache");
75+
76+
try {
77+
78+
//Form media type is required: https://tools.ietf.org/html/rfc6749#section-4.3.2
79+
String contentType = Strings.clean(request.getContentType());
80+
if (contentType == null || !contentType.startsWith(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
81+
String msg = "Content-Type must be " + MediaType.APPLICATION_FORM_URLENCODED_VALUE;
82+
throw new OAuthException(OAuthErrorCode.INVALID_REQUEST, msg, null);
83+
}
84+
85+
String tokenTypeHint = request.getParameter(TOKEN_TYPE_HINT);
86+
87+
if (Strings.hasText(tokenTypeHint)) {
88+
builder.setTokenTypeHint(TokenTypeHint.fromValue(tokenTypeHint));
89+
}
90+
91+
String token = request.getParameter(TOKEN);
92+
93+
if (!Strings.hasText(token)) {
94+
throw new OAuthException(OAuthErrorCode.INVALID_REQUEST);
95+
}
96+
97+
this.revoke(getApplication(request), builder.setToken(token).build());
98+
99+
response.setStatus(HttpServletResponse.SC_OK);
100+
response.setHeader("Content-Length", "0");
101+
102+
} catch (OAuthException e) {
103+
104+
log.debug("Error occurred revoking token: {}", e.getMessage());
105+
106+
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
107+
108+
String json = e.toJson();
109+
110+
response.setHeader("Content-Length", String.valueOf(json.length()));
111+
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
112+
response.getWriter().print(json);
113+
response.getWriter().flush();
114+
}
115+
116+
//we rendered the response directly - no need for a view to be resolved, so return null:
117+
return null;
118+
}
119+
120+
private void revoke(Application application, OAuthRevocationRequest request) throws OAuthException {
121+
try {
122+
OAuthTokenRevocators.OAUTH_TOKEN_REVOCATOR.forApplication(application).revoke(request);
123+
} catch (ResourceException e) {
124+
com.stormpath.sdk.error.Error error = e.getStormpathError();
125+
String message = error.getMessage();
126+
127+
OAuthErrorCode oauthError = OAuthErrorCode.INVALID_REQUEST;
128+
if (error instanceof DefaultError) {
129+
Object errorObject = ((DefaultError) error).getProperty("error");
130+
oauthError = errorObject == null ? oauthError : new OAuthErrorCode(errorObject.toString());
131+
}
132+
133+
throw new OAuthException(oauthError, message);
134+
}
135+
}
136+
137+
}

extensions/servlet/src/main/resources/com/stormpath/sdk/servlet/config/web.stormpath.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ stormpath.web.oauth2.uri = /oauth/token
2525
stormpath.web.oauth2.client_credentials.enabled = true
2626
stormpath.web.oauth2.password.enabled = true
2727
stormpath.web.oauth2.password.validationStrategy = local
28+
stormpath.web.oauth2.revoke.uri = /oauth/revoke
2829
stormpath.web.accessTokenCookie.name = access_token
2930
stormpath.web.accessTokenCookie.httpOnly = true
3031
stormpath.web.accessTokenCookie.secure =

extensions/servlet/src/test/groovy/com/stormpath/sdk/servlet/config/SpecConfigVersusWebPropertiesTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class SpecConfigVersusWebPropertiesTest {
8585
specProperties.containsKey(k) ? null : k
8686
}
8787

88-
def expected_diff_size = 83
88+
def expected_diff_size = 84
8989

9090
if (diff.size != expected_diff_size) {
9191
println "It looks like a property was added or removed from the Framework Spec or web.stormpath.properties."

0 commit comments

Comments
 (0)