diff --git a/audit/api/src/main/java/org/keycloak/audit/Details.java b/audit/api/src/main/java/org/keycloak/audit/Details.java old mode 100644 new mode 100755 index 1a8df0ad40..fdb344816e --- a/audit/api/src/main/java/org/keycloak/audit/Details.java +++ b/audit/api/src/main/java/org/keycloak/audit/Details.java @@ -17,6 +17,7 @@ public interface Details { String REMEMBER_ME = "remember_me"; String TOKEN_ID = "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"; } diff --git a/audit/api/src/main/java/org/keycloak/audit/EventType.java b/audit/api/src/main/java/org/keycloak/audit/EventType.java old mode 100644 new mode 100755 index 707f5fd9be..0913ef33c9 --- a/audit/api/src/main/java/org/keycloak/audit/EventType.java +++ b/audit/api/src/main/java/org/keycloak/audit/EventType.java @@ -14,6 +14,8 @@ public enum EventType { CODE_TO_TOKEN, CODE_TO_TOKEN_ERROR, REFRESH_TOKEN, + VALIDATE_ACCESS_TOKEN, + VALIDATE_ACCESS_TOKEN_ERROR, REFRESH_TOKEN_ERROR, SOCIAL_LINK, SOCIAL_LINK_ERROR, diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java b/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java index 7b3c38835a..39a51efdd2 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java @@ -10,5 +10,11 @@ import java.util.List; */ public interface UserFederationProviderFactory extends ProviderFactory { UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model); + + /** + * Config options to display in generic admin console page for federation + * + * @return + */ List getConfigurationOptions(); } diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index ee2282ff2d..625055f894 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -11,6 +11,8 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.UnauthorizedException; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; +import org.keycloak.RSATokenVerifier; +import org.keycloak.VerificationException; import org.keycloak.audit.Audit; import org.keycloak.audit.Details; 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.util.Base64Url; import org.keycloak.util.BasicAuthHelper; +import org.keycloak.util.Time; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -137,6 +140,11 @@ public class TokenService { 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) { UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); return grantAccessTokenUrl(baseUriBuilder); @@ -295,6 +303,105 @@ public class TokenService { 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 err = new HashMap(); + 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 err = new HashMap(); + 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 err = new HashMap(); + 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 err = new HashMap(); + 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 err = new HashMap(); + 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 err = new HashMap(); + 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 err = new HashMap(); + 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 err = new HashMap(); + 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. * diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java index 3b57ca3cfc..7bba7c9a97 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java @@ -365,4 +365,6 @@ public class AdapterTest { } + + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index ba7c41d218..980879c9e5 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -34,6 +34,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.representations.AccessToken; import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.resources.TokenService; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; 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.WebResource; import org.keycloak.testsuite.rule.WebRule; +import org.keycloak.util.BasicAuthHelper; 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.greaterThanOrEqualTo; 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(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID)); Assert.assertEquals(sessionId, token.getSessionState()); + } @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 error = response.readEntity(new GenericType >() {}); + 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 error = response.readEntity(new GenericType >() {}); + Assert.assertNotNull(error.get("error")); + } + + client.close(); + events.clear(); + + } + + } diff --git a/testsuite/integration/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json index ced8c26c54..cc2a6143d3 100755 --- a/testsuite/integration/src/test/resources/testrealm.json +++ b/testsuite/integration/src/test/resources/testrealm.json @@ -5,6 +5,7 @@ "sslRequired": "external", "registrationAllowed": true, "resetPasswordAllowed": true, + "passwordCredentialGrantAllowed": true, "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", "requiredCredentials": [ "password" ],