Merge pull request #579 from patriot1burke/master

remote token validation
This commit is contained in:
Bill Burke 2014-08-01 13:34:11 -04:00
commit 73b7fe6169
7 changed files with 194 additions and 0 deletions

View file

@ -17,6 +17,7 @@ public interface Details {
String REMEMBER_ME = "remember_me"; String REMEMBER_ME = "remember_me";
String TOKEN_ID = "token_id"; String TOKEN_ID = "token_id";
String REFRESH_TOKEN_ID = "refresh_token_id"; String REFRESH_TOKEN_ID = "refresh_token_id";
String VALIDATE_ACCESS_TOKEN = "validate_access_token";
String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id"; String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id";
} }

View file

@ -14,6 +14,8 @@ public enum EventType {
CODE_TO_TOKEN, CODE_TO_TOKEN,
CODE_TO_TOKEN_ERROR, CODE_TO_TOKEN_ERROR,
REFRESH_TOKEN, REFRESH_TOKEN,
VALIDATE_ACCESS_TOKEN,
VALIDATE_ACCESS_TOKEN_ERROR,
REFRESH_TOKEN_ERROR, REFRESH_TOKEN_ERROR,
SOCIAL_LINK, SOCIAL_LINK,
SOCIAL_LINK_ERROR, SOCIAL_LINK_ERROR,

View file

@ -10,5 +10,11 @@ import java.util.List;
*/ */
public interface UserFederationProviderFactory extends ProviderFactory<UserFederationProvider> { public interface UserFederationProviderFactory extends ProviderFactory<UserFederationProvider> {
UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model); UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model);
/**
* Config options to display in generic admin console page for federation
*
* @return
*/
List<String> getConfigurationOptions(); List<String> getConfigurationOptions();
} }

View file

@ -11,6 +11,8 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.spi.UnauthorizedException; import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
import org.keycloak.audit.Audit; import org.keycloak.audit.Audit;
import org.keycloak.audit.Details; import org.keycloak.audit.Details;
import org.keycloak.audit.Errors; import org.keycloak.audit.Errors;
@ -45,6 +47,7 @@ import org.keycloak.services.resources.flows.Urls;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.util.Base64Url; import org.keycloak.util.Base64Url;
import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.Time;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
@ -137,6 +140,11 @@ public class TokenService {
return uriBuilder.path(TokenService.class, "accessCodeToToken"); return uriBuilder.path(TokenService.class, "accessCodeToToken");
} }
public static UriBuilder validateAccessTokenUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(TokenService.class, "validateAccessToken");
}
public static UriBuilder grantAccessTokenUrl(UriInfo uriInfo) { public static UriBuilder grantAccessTokenUrl(UriInfo uriInfo) {
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
return grantAccessTokenUrl(baseUriBuilder); return grantAccessTokenUrl(baseUriBuilder);
@ -295,6 +303,105 @@ public class TokenService {
return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build(); return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build();
} }
/**
* Validate encoded access token.
*
* @param tokenString
* @return Unmarshalled token
*/
@Path("validate")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response validateAccessToken(@QueryParam("access_token") String tokenString) {
audit.event(EventType.VALIDATE_ACCESS_TOKEN);
AccessToken token = null;
try {
token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName());
} catch (Exception e) {
Map<String, String> err = new HashMap<String, String>();
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Token invalid");
audit.error(Errors.INVALID_TOKEN);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
.build();
}
audit.user(token.getSubject()).session(token.getSessionState()).detail(Details.VALIDATE_ACCESS_TOKEN, token.getId());
if (token.isExpired()
|| token.getIssuedAt() < realm.getNotBefore()
) {
Map<String, String> err = new HashMap<String, String>();
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Token expired");
audit.error(Errors.INVALID_TOKEN);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
.build();
}
UserModel user = session.users().getUserById(token.getSubject(), realm);
if (user == null) {
Map<String, String> err = new HashMap<String, String>();
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
err.put(OAuth2Constants.ERROR_DESCRIPTION, "User does not exist");
audit.error(Errors.USER_NOT_FOUND);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
.build();
}
if (!user.isEnabled()) {
Map<String, String> err = new HashMap<String, String>();
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
err.put(OAuth2Constants.ERROR_DESCRIPTION, "User disabled");
audit.error(Errors.USER_DISABLED);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
.build();
}
UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
Map<String, String> err = new HashMap<String, String>();
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT);
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Expired session");
audit.error(Errors.USER_SESSION_NOT_FOUND);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
.build();
}
ClientModel client = realm.findClient(token.getIssuedFor());
if (client == null) {
Map<String, String> err = new HashMap<String, String>();
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_CLIENT);
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Issued for client no longer exists");
audit.error(Errors.CLIENT_NOT_FOUND);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
.build();
}
if (token.getIssuedAt() < client.getNotBefore()) {
Map<String, String> err = new HashMap<String, String>();
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_CLIENT);
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Issued for client no longer exists");
audit.error(Errors.INVALID_TOKEN);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
.build();
}
try {
tokenManager.verifyAccess(token, realm, client, user);
} catch (OAuthErrorException e) {
Map<String, String> err = new HashMap<String, String>();
err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_SCOPE);
err.put(OAuth2Constants.ERROR_DESCRIPTION, "Role mappings have changed");
audit.error(Errors.INVALID_TOKEN);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
.build();
}
return Response.ok(token, MediaType.APPLICATION_JSON_TYPE).build();
}
/** /**
* URL for making refresh token requests. * URL for making refresh token requests.
* *

View file

@ -365,4 +365,6 @@ public class AdapterTest {
} }
} }

View file

@ -34,6 +34,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.TokenService;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.OAuthClient.AccessTokenResponse; import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
@ -41,8 +42,22 @@ import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule; import org.keycloak.testsuite.rule.WebRule;
import org.keycloak.util.BasicAuthHelper;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.lessThanOrEqualTo;
@ -108,6 +123,7 @@ public class AccessTokenTest {
Assert.assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID)); Assert.assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID));
Assert.assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID)); Assert.assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
Assert.assertEquals(sessionId, token.getSessionState()); Assert.assertEquals(sessionId, token.getSessionState());
} }
@Test @Test
@ -247,4 +263,63 @@ public class AccessTokenTest {
}); });
} }
@Test
public void testValidateAccessToken() throws Exception {
Client client = ClientBuilder.newClient();
UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
URI grantUri = TokenService.grantAccessTokenUrl(builder).build("test");
WebTarget grantTarget = client.target(grantUri);
builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
URI validateUri = TokenService.validateAccessTokenUrl(builder).build("test");
WebTarget validateTarget = client.target(validateUri);
{
Response response = validateTarget.queryParam("access_token", "bad token").request().get();
Assert.assertEquals(400, response.getStatus());
HashMap<String, String> error = response.readEntity(new GenericType <HashMap<String, String>>() {});
Assert.assertNotNull(error.get("error"));
}
org.keycloak.representations.AccessTokenResponse tokenResponse = null;
{
String header = BasicAuthHelper.createHeader("test-app", "password");
Form form = new Form();
form.param("username", "test-user@localhost")
.param("password", "password");
Response response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, header)
.post(Entity.form(form));
Assert.assertEquals(200, response.getStatus());
tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class);
response.close();
}
{
Response response = validateTarget.queryParam("access_token", tokenResponse.getToken()).request().get();
Assert.assertEquals(200, response.getStatus());
AccessToken token = response.readEntity(AccessToken.class);
Assert.assertNotNull(token);
response.close();
}
{
builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
URI logoutUri = TokenService.logoutUrl(builder).build("test");
Response response = client.target(logoutUri).queryParam("session_state", tokenResponse.getSessionState()).request().get();
Assert.assertEquals(200, response.getStatus());
response.close();
}
{
Response response = validateTarget.queryParam("access_token", tokenResponse.getToken()).request().get();
Assert.assertEquals(400, response.getStatus());
HashMap<String, String> error = response.readEntity(new GenericType <HashMap<String, String>>() {});
Assert.assertNotNull(error.get("error"));
}
client.close();
events.clear();
}
} }

View file

@ -5,6 +5,7 @@
"sslRequired": "external", "sslRequired": "external",
"registrationAllowed": true, "registrationAllowed": true,
"resetPasswordAllowed": true, "resetPasswordAllowed": true,
"passwordCredentialGrantAllowed": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ], "requiredCredentials": [ "password" ],