From f9aaa16cfe8386a96656b029ecde506e7aac5041 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Sat, 15 Mar 2014 10:15:10 +0000 Subject: [PATCH] KEYCLOAK-378 KEYCLOAK-379 KEYCLOAK-381 Fix refresh token if token contains app roles. Changed long time fields in AccessCode and AccessToken to int --- .../keycloak/representations/AccessToken.java | 4 +- .../representations/JsonWebToken.java | 20 ++-- .../adapters/action/AdminAction.java | 4 +- .../src/main/java/org/keycloak/util/Time.java | 12 ++ .../java/org/keycloak/RSAVerifierTest.java | 9 +- pom.xml | 8 ++ .../services/managers/AccessCodeEntry.java | 9 +- .../managers/AuthenticationManager.java | 3 +- .../managers/ResourceAdminManager.java | 13 ++- .../services/managers/TokenManager.java | 16 +-- .../resources/RequiredActionsService.java | 7 +- .../services/resources/TokenService.java | 3 +- .../resources/admin/UsersResource.java | 5 +- .../services/resources/flows/OAuthFlows.java | 5 +- testsuite/integration/pom.xml | 4 + .../org/keycloak/testsuite/OAuthClient.java | 52 +++++++++ .../testsuite/oauth/AccessTokenTest.java | 9 +- .../testsuite/oauth/RefreshTokenTest.java | 110 ++++++++++++++++++ .../src/test/resources/testrealm.json | 3 - 19 files changed, 244 insertions(+), 52 deletions(-) create mode 100644 core/src/main/java/org/keycloak/util/Time.java create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java diff --git a/core/src/main/java/org/keycloak/representations/AccessToken.java b/core/src/main/java/org/keycloak/representations/AccessToken.java index 8279214213..262d07dc1f 100755 --- a/core/src/main/java/org/keycloak/representations/AccessToken.java +++ b/core/src/main/java/org/keycloak/representations/AccessToken.java @@ -128,12 +128,12 @@ public class AccessToken extends IDToken { } @Override - public AccessToken expiration(long expiration) { + public AccessToken expiration(int expiration) { return (AccessToken) super.expiration(expiration); } @Override - public AccessToken notBefore(long notBefore) { + public AccessToken notBefore(int notBefore) { return (AccessToken) super.notBefore(notBefore); } diff --git a/core/src/main/java/org/keycloak/representations/JsonWebToken.java b/core/src/main/java/org/keycloak/representations/JsonWebToken.java index 56ac51c3e6..42e9328110 100755 --- a/core/src/main/java/org/keycloak/representations/JsonWebToken.java +++ b/core/src/main/java/org/keycloak/representations/JsonWebToken.java @@ -2,6 +2,7 @@ package org.keycloak.representations; import org.codehaus.jackson.annotate.JsonIgnore; import org.codehaus.jackson.annotate.JsonProperty; +import org.keycloak.util.Time; import java.io.Serializable; @@ -13,9 +14,9 @@ public class JsonWebToken implements Serializable { @JsonProperty("jti") protected String id; @JsonProperty("exp") - protected long expiration; + protected int expiration; @JsonProperty("nbf") - protected long notBefore; + protected int notBefore; @JsonProperty("iat") protected int issuedAt; @JsonProperty("iss") @@ -39,26 +40,25 @@ public class JsonWebToken implements Serializable { } - public long getExpiration() { + public int getExpiration() { return expiration; } - public JsonWebToken expiration(long expiration) { + public JsonWebToken expiration(int expiration) { this.expiration = expiration; return this; } @JsonIgnore public boolean isExpired() { - long time = System.currentTimeMillis() / 1000; - return time > expiration; + return Time.currentTime() > expiration; } - public long getNotBefore() { + public int getNotBefore() { return notBefore; } - public JsonWebToken notBefore(long notBefore) { + public JsonWebToken notBefore(int notBefore) { this.notBefore = notBefore; return this; } @@ -66,7 +66,7 @@ public class JsonWebToken implements Serializable { @JsonIgnore public boolean isNotBefore() { - return (System.currentTimeMillis() / 1000) >= notBefore; + return Time.currentTime() >= notBefore; } @@ -89,7 +89,7 @@ public class JsonWebToken implements Serializable { */ @JsonIgnore public JsonWebToken issuedNow() { - issuedAt = (int)(System.currentTimeMillis() / 1000); + issuedAt = Time.currentTime(); return this; } diff --git a/core/src/main/java/org/keycloak/representations/adapters/action/AdminAction.java b/core/src/main/java/org/keycloak/representations/adapters/action/AdminAction.java index 7fcc3a006a..1708594cdc 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/action/AdminAction.java +++ b/core/src/main/java/org/keycloak/representations/adapters/action/AdminAction.java @@ -1,6 +1,7 @@ package org.keycloak.representations.adapters.action; import org.codehaus.jackson.annotate.JsonIgnore; +import org.keycloak.util.Time; /** * Posted to managed client from admin server. @@ -34,8 +35,7 @@ public abstract class AdminAction { @JsonIgnore public boolean isExpired() { - long time = System.currentTimeMillis() / 1000; - return time > expiration; + return Time.currentTime() > expiration; } /** diff --git a/core/src/main/java/org/keycloak/util/Time.java b/core/src/main/java/org/keycloak/util/Time.java new file mode 100644 index 0000000000..3b3aef4b31 --- /dev/null +++ b/core/src/main/java/org/keycloak/util/Time.java @@ -0,0 +1,12 @@ +package org.keycloak.util; + +/** + * @author Stian Thorgersen + */ +public class Time { + + public static int currentTime() { + return (int) (System.currentTimeMillis() / 1000); + } + +} diff --git a/core/src/test/java/org/keycloak/RSAVerifierTest.java b/core/src/test/java/org/keycloak/RSAVerifierTest.java index b9e2848537..ac10bbf41c 100755 --- a/core/src/test/java/org/keycloak/RSAVerifierTest.java +++ b/core/src/test/java/org/keycloak/RSAVerifierTest.java @@ -9,6 +9,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.representations.AccessToken; +import org.keycloak.util.Time; import javax.security.auth.x500.X500Principal; import java.io.IOException; @@ -145,7 +146,7 @@ public class RSAVerifierTest { @Test public void testNotBeforeGood() throws Exception { - token.notBefore((System.currentTimeMillis() / 1000) - 100); + token.notBefore(Time.currentTime() - 100); String encoded = new JWSBuilder() .jsonContent(token) @@ -161,7 +162,7 @@ public class RSAVerifierTest { @Test public void testNotBeforeBad() throws Exception { - token.notBefore((System.currentTimeMillis() / 1000) + 100); + token.notBefore(Time.currentTime() + 100); String encoded = new JWSBuilder() .jsonContent(token) @@ -178,7 +179,7 @@ public class RSAVerifierTest { @Test public void testExpirationGood() throws Exception { - token.expiration((System.currentTimeMillis() / 1000) + 100); + token.expiration(Time.currentTime() + 100); String encoded = new JWSBuilder() .jsonContent(token) @@ -194,7 +195,7 @@ public class RSAVerifierTest { @Test public void testExpirationBad() throws Exception { - token.expiration((System.currentTimeMillis() / 1000) - 100); + token.expiration(Time.currentTime() - 100); String encoded = new JWSBuilder() .jsonContent(token) diff --git a/pom.xml b/pom.xml index 5a629b786d..00cdfeb9f4 100755 --- a/pom.xml +++ b/pom.xml @@ -229,6 +229,13 @@ junit junit 4.11 + test + + + org.hamcrest + hamcrest-all + 1.3 + test org.hibernate.javax.persistence @@ -310,6 +317,7 @@ org.seleniumhq.selenium selenium-chrome-driver 2.35.0 + test org.mongodb diff --git a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java index 79da6b7e8d..51ea351048 100755 --- a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java +++ b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java @@ -6,6 +6,7 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.representations.AccessToken; +import org.keycloak.util.Time; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; @@ -25,7 +26,7 @@ public class AccessCodeEntry { protected String redirectUri; protected boolean rememberMe; - protected long expiration; + protected int expiration; protected RealmModel realm; protected AccessToken token; protected UserModel user; @@ -35,7 +36,7 @@ public class AccessCodeEntry { MultivaluedMap resourceRolesRequested = new MultivaluedHashMap(); public boolean isExpired() { - return expiration != 0 && (System.currentTimeMillis() / 1000) > expiration; + return expiration != 0 && Time.currentTime() > expiration; } public String getId() { @@ -58,11 +59,11 @@ public class AccessCodeEntry { this.code = code; } - public long getExpiration() { + public int getExpiration() { return expiration; } - public void setExpiration(long expiration) { + public void setExpiration(int expiration) { this.expiration = expiration; } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index b1a473a08c..e3c1e95ee0 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -16,6 +16,7 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.resources.RealmsResource; +import org.keycloak.util.Time; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; @@ -47,7 +48,7 @@ public class AuthenticationManager { token.subject(user.getId()); token.audience(realm.getName()); if (realm.getCentralLoginLifespan() > 0) { - token.expiration((System.currentTimeMillis() / 1000) + realm.getCentralLoginLifespan()); + token.expiration(Time.currentTime() + realm.getCentralLoginLifespan()); } return token; } diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java index 3c0e74121e..24f3ee41f9 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -14,6 +14,7 @@ import org.keycloak.representations.adapters.action.SessionStats; import org.keycloak.representations.adapters.action.SessionStatsAction; import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.adapters.action.UserStatsAction; +import org.keycloak.util.Time; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Response; @@ -44,7 +45,7 @@ public class ResourceAdminManager { public SessionStats getSessionStats(RealmModel realm, ApplicationModel application, boolean users, ResteasyClient client) { String managementUrl = application.getManagementUrl(); if (managementUrl != null) { - SessionStatsAction adminAction = new SessionStatsAction(TokenIdGenerator.generateId(), (int)(System.currentTimeMillis() / 1000) + 30, application.getName()); + SessionStatsAction adminAction = new SessionStatsAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, application.getName()); adminAction.setListUsers(users); String token = new TokenManager().encodeToken(realm, adminAction); logger.info("session stats for application: {0} url: {1}", application.getName(), managementUrl); @@ -91,7 +92,7 @@ public class ResourceAdminManager { public UserStats getUserStats(RealmModel realm, ApplicationModel application, UserModel user, ResteasyClient client) { String managementUrl = application.getManagementUrl(); if (managementUrl != null) { - UserStatsAction adminAction = new UserStatsAction(TokenIdGenerator.generateId(), (int)(System.currentTimeMillis() / 1000) + 30, application.getName(), user.getId()); + UserStatsAction adminAction = new UserStatsAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, application.getName(), user.getId()); String token = new TokenManager().encodeToken(realm, adminAction); logger.info("session stats for application: {0} url: {1}", application.getName(), managementUrl); Response response = client.target(managementUrl).path(AdapterConstants.K_GET_USER_STATS).request().post(Entity.text(token)); @@ -130,7 +131,7 @@ public class ResourceAdminManager { .build(); try { - realm.setNotBefore((int)(System.currentTimeMillis()/1000)); + realm.setNotBefore(Time.currentTime()); List resources = realm.getApplications(); logger.debug("logging out {0} resources ", resources.size()); for (ApplicationModel resource : resources) { @@ -147,7 +148,7 @@ public class ResourceAdminManager { .build(); try { - resource.setNotBefore((int)(System.currentTimeMillis()/1000)); + resource.setNotBefore(Time.currentTime()); logoutApplication(realm, resource, user, client, resource.getNotBefore()); } finally { client.close(); @@ -159,7 +160,7 @@ public class ResourceAdminManager { protected boolean logoutApplication(RealmModel realm, ApplicationModel resource, String user, ResteasyClient client, int notBefore) { String managementUrl = resource.getManagementUrl(); if (managementUrl != null) { - LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), (int)(System.currentTimeMillis() / 1000) + 30, resource.getName(), user, notBefore); + LogoutAction adminAction = new LogoutAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), user, notBefore); String token = new TokenManager().encodeToken(realm, adminAction); logger.info("logout user: {0} resource: {1} url: {2}", user, resource.getName(), managementUrl); Response response = client.target(managementUrl).path(AdapterConstants.K_LOGOUT).request().post(Entity.text(token)); @@ -204,7 +205,7 @@ public class ResourceAdminManager { if (notBefore <= 0) return false; String managementUrl = resource.getManagementUrl(); if (managementUrl != null) { - PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), (int)(System.currentTimeMillis() / 1000) + 30, resource.getName(), notBefore); + PushNotBeforeAction adminAction = new PushNotBeforeAction(TokenIdGenerator.generateId(), Time.currentTime() + 30, resource.getName(), notBefore); String token = new TokenManager().encodeToken(realm, adminAction); logger.info("pushRevocation resource: {0} url: {1}", resource.getName(), managementUrl); Response response = client.target(managementUrl).path(AdapterConstants.K_PUSH_NOT_BEFORE).request().post(Entity.text(token)); diff --git a/services/src/main/java/org/keycloak/services/managers/TokenManager.java b/services/src/main/java/org/keycloak/services/managers/TokenManager.java index beb916efe6..a9e6bcfea1 100755 --- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java +++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java @@ -16,6 +16,7 @@ import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; import org.keycloak.representations.RefreshToken; +import org.keycloak.util.Time; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; @@ -82,7 +83,7 @@ public class TokenManager { code.setToken(token); code.setRealm(realm); - code.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan()); + code.setExpiration(Time.currentTime() + realm.getAccessCodeLifespan()); code.setClient(client); code.setUser(user); code.setState(state); @@ -158,7 +159,7 @@ public class TokenManager { if (app == null) { throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "Application no longer exists", "Application no longer exists: " + app.getName()); } - for (String roleName : refreshToken.getRealmAccess().getRoles()) { + for (String roleName : entry.getValue().getRoles()) { RoleModel role = app.getRole(roleName); if (role == null) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown application role: " + roleName); @@ -257,7 +258,7 @@ public class TokenManager { token.issuedFor(client.getLoginName()); token.issuer(realm.getName()); if (realm.getAccessTokenLifespan() > 0) { - token.expiration((System.currentTimeMillis() / 1000) + realm.getAccessTokenLifespan()); + token.expiration(Time.currentTime() + realm.getAccessTokenLifespan()); } initClaims(token, claimer, user); return token; @@ -274,7 +275,7 @@ public class TokenManager { token.issuedFor(client.getClientId()); token.issuer(realm.getName()); if (realm.getAccessTokenLifespan() > 0) { - token.expiration((System.currentTimeMillis() / 1000) + realm.getAccessTokenLifespan()); + token.expiration(Time.currentTime() + realm.getAccessTokenLifespan()); } Set allowedOrigins = client.getWebOrigins(); if (allowedOrigins != null) { @@ -356,7 +357,7 @@ public class TokenManager { refreshToken = new RefreshToken(accessToken); refreshToken.id(KeycloakModelUtils.generateId()); refreshToken.issuedNow(); - refreshToken.expiration((System.currentTimeMillis() / 1000) + realm.getRefreshTokenLifespan()); + refreshToken.expiration(Time.currentTime() + realm.getRefreshTokenLifespan()); return this; } @@ -372,7 +373,7 @@ public class TokenManager { idToken.issuedFor(accessToken.getIssuedFor()); idToken.issuer(accessToken.getIssuer()); if (realm.getAccessTokenLifespan() > 0) { - idToken.expiration((System.currentTimeMillis() / 1000) + realm.getAccessTokenLifespan()); + idToken.expiration(Time.currentTime() + realm.getAccessTokenLifespan()); } idToken.setPreferredUsername(accessToken.getPreferredUsername()); idToken.setGivenName(accessToken.getGivenName()); @@ -412,8 +413,7 @@ public class TokenManager { res.setToken(encodedToken); res.setTokenType("bearer"); if (accessToken.getExpiration() != 0) { - long time = accessToken.getExpiration() - (System.currentTimeMillis() / 1000); - res.setExpiresIn(time); + res.setExpiresIn(accessToken.getExpiration() - Time.currentTime()); } } if (refreshToken != null) { diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java index b8eb4dba5a..5f5e25289f 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -41,6 +41,7 @@ import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.validation.Validation; +import org.keycloak.util.Time; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -268,7 +269,7 @@ public class RequiredActionsService { AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user); accessCode.setRequiredActions(requiredActions); - accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); + accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction()); try { new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo); @@ -312,7 +313,7 @@ public class RequiredActionsService { if (accessCodeEntry.isExpired()) { logger.debug("getAccessCodeEntry: access code id: {0}", accessCodeEntry.getId()); logger.debug("getAccessCodeEntry access code entry expired: {0}", accessCodeEntry.getExpiration()); - logger.debug("getAccessCodeEntry current time: {0}", (System.currentTimeMillis() / 1000)); + logger.debug("getAccessCodeEntry current time: {0}", Time.currentTime()); return null; } @@ -339,7 +340,7 @@ public class RequiredActionsService { .createResponse(requiredActions.iterator().next()); } else { logger.debug("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri()); - accessCode.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan()); + accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespan()); return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, accessCode.getState(), accessCode.getRedirectUri()); } 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 b9b16739c5..5a3b239c26 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -28,6 +28,7 @@ import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.OAuthFlows; import org.keycloak.services.validation.Validation; import org.keycloak.util.BasicAuthHelper; +import org.keycloak.util.Time; import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; @@ -622,7 +623,7 @@ public class TokenService { return redirectAccessDenied(redirect, state); } - accessCodeEntry.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan()); + accessCodeEntry.setExpiration(Time.currentTime() + realm.getAccessCodeLifespan()); return oauth.redirectAccessCode(accessCodeEntry, state, redirect); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index c548c78b76..038a9de98c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -25,6 +25,7 @@ import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Urls; +import org.keycloak.util.Time; import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; @@ -181,7 +182,7 @@ public class UsersResource { throw new NotFoundException(); } // set notBefore so that user will be forced to log in. - user.setNotBefore((int) (System.currentTimeMillis() / 1000)); + user.setNotBefore(Time.currentTime()); new ResourceAdminManager().logoutUser(realm, user); } @@ -514,7 +515,7 @@ public class UsersResource { AccessCodeEntry accessCode = tokenManager.createAccessCode(scope, state, redirect, realm, client, user); accessCode.setRequiredActions(requiredActions); - accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); + accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction()); try { new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo); diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java index eeb6674d66..0a463e3646 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java @@ -37,6 +37,7 @@ import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.resources.TokenService; +import org.keycloak.util.Time; import javax.ws.rs.Path; import javax.ws.rs.core.Cookie; @@ -127,14 +128,14 @@ public class OAuthFlows { Set requiredActions = user.getRequiredActions(); if (!requiredActions.isEmpty()) { accessCode.setRequiredActions(new HashSet(requiredActions)); - accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); + accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction()); return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user) .createResponse(user.getRequiredActions().iterator().next()); } if (!isResource && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) { - accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction()); + accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction()); return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()). setAccessRequest(accessCode.getRealmRolesRequested(), accessCode.getResourceRolesRequested()). setClient(client).createOAuthGrant(); diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index ea61388294..1591c7f5ff 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -236,6 +236,10 @@ junit junit + + org.hamcrest + hamcrest-all + org.hibernate.javax.persistence hibernate-jpa-2.0-api diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java index 5b262bb1e6..a44991879d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java @@ -38,6 +38,7 @@ import org.keycloak.VerificationException; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.RefreshToken; import org.keycloak.util.BasicAuthHelper; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; @@ -141,6 +142,40 @@ public class OAuthClient { } } + public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String password) { + HttpClient client = new DefaultHttpClient(); + HttpPost post = new HttpPost(getRefreshTokenUrl()); + + List parameters = new LinkedList(); + if (grantType != null) { + parameters.add(new BasicNameValuePair("grant_type", grantType)); + } + if (refreshToken != null) { + parameters.add(new BasicNameValuePair("refresh_token", refreshToken)); + } + if (clientId != null && password != null) { + String authorization = BasicAuthHelper.createHeader(clientId, password); + post.setHeader("Authorization", authorization); + } + else if (clientId != null) { + parameters.add(new BasicNameValuePair("client_id", clientId)); + } + + UrlEncodedFormEntity formEntity = null; + try { + formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + post.setEntity(formEntity); + + try { + return new AccessTokenResponse(client.execute(post)); + } catch (Exception e) { + throw new RuntimeException("Failed to retrieve access token", e); + } + } + public AccessToken verifyToken(String token) { try { return RSATokenVerifier.verifyToken(token, realmPublicKey, realm); @@ -155,6 +190,18 @@ public class OAuthClient { } } + public RefreshToken verifyRefreshToken(String refreshToken) { + try { + JWSInput jws = new JWSInput(refreshToken); + if (!RSAProvider.verify(jws, realmPublicKey)) { + throw new RuntimeException("Invalid refresh token"); + } + return jws.readJsonContent(RefreshToken.class); + } catch (Exception e) { + throw new RuntimeException("Invalid refresh token", e); + } + } + public String getClientId() { return clientId; } @@ -218,6 +265,11 @@ public class OAuthClient { return b.build().toString(); } + public String getRefreshTokenUrl() { + UriBuilder b = UriBuilder.fromUri(baseUrl + "/realms/" + realm + "/tokens/refresh"); + return b.build().toString(); + } + public OAuthClient realm(String realm) { this.realm = realm; return this; 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 b894e2c890..db57c43a62 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 @@ -25,10 +25,7 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; -import org.keycloak.models.RealmModel; import org.keycloak.representations.AccessToken; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient.AccessTokenResponse; import org.keycloak.testsuite.pages.LoginPage; @@ -37,6 +34,10 @@ import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.WebDriver; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + /** * @author Stian Thorgersen */ @@ -66,7 +67,7 @@ public class AccessTokenTest { Assert.assertEquals(200, response.getStatusCode()); - Assert.assertTrue(response.getExpiresIn() <= 600 && response.getExpiresIn() >= 550); + Assert.assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300))); Assert.assertEquals("bearer", response.getTokenType()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java new file mode 100755 index 0000000000..8e70fb8725 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java @@ -0,0 +1,110 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.testsuite.oauth; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.RefreshToken; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.OAuthClient.AccessTokenResponse; +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.Time; +import org.openqa.selenium.WebDriver; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +/** + * @author Stian Thorgersen + */ +public class RefreshTokenTest { + + @ClassRule + public static KeycloakRule keycloakRule = new KeycloakRule(); + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected WebDriver driver; + + @WebResource + protected OAuthClient oauth; + + @WebResource + protected LoginPage loginPage; + + @Test + public void refreshTokenRequest() throws Exception { + oauth.doLogin("test-user@localhost", "password"); + + String code = oauth.getCurrentQuery().get("code"); + + AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password"); + AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); + String refreshTokenString = tokenResponse.getRefreshToken(); + RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString); + + Assert.assertNotNull(refreshTokenString); + + Assert.assertEquals("bearer", tokenResponse.getTokenType()); + + Assert.assertThat(token.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300))); + Assert.assertThat(refreshToken.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(35950), lessThanOrEqualTo(36000))); + + Thread.sleep(2000); + + AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString, "password"); + AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken()); + RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(response.getRefreshToken()); + + Assert.assertEquals(200, response.getStatusCode()); + + Assert.assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300))); + Assert.assertThat(refreshedToken.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300))); + + Assert.assertThat(refreshedToken.getExpiration() - token.getExpiration(), allOf(greaterThanOrEqualTo(1), lessThanOrEqualTo(3))); + Assert.assertThat(refreshedRefreshToken.getExpiration() - refreshToken.getExpiration(), allOf(greaterThanOrEqualTo(1), lessThanOrEqualTo(3))); + + Assert.assertNotEquals(token.getId(), refreshedToken.getId()); + Assert.assertNotEquals(refreshToken.getId(), refreshedRefreshToken.getId()); + + Assert.assertEquals("bearer", response.getTokenType()); + + Assert.assertEquals(keycloakRule.getUser("test", "test-user@localhost").getId(), refreshedToken.getSubject()); + Assert.assertNotEquals("test-user@localhost", refreshedToken.getSubject()); + + Assert.assertEquals(1, refreshedToken.getRealmAccess().getRoles().size()); + Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user")); + + Assert.assertEquals(1, refreshedToken.getResourceAccess(oauth.getClientId()).getRoles().size()); + Assert.assertTrue(refreshedToken.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user")); + } + +} diff --git a/testsuite/integration/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json index ed504aa0f0..a7cf0d49e1 100755 --- a/testsuite/integration/src/test/resources/testrealm.json +++ b/testsuite/integration/src/test/resources/testrealm.json @@ -2,9 +2,6 @@ "id": "test", "realm": "test", "enabled": true, - "accessTokenLifespan": 600, - "accessCodeLifespan": 600, - "accessCodeLifespanUserAction": 600, "sslNotRequired": true, "registrationAllowed": true, "resetPasswordAllowed": true,