From 166e00f2e8b466c71360f32ab2adfec56350e787 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Sun, 23 Feb 2014 11:30:32 -0500 Subject: [PATCH 1/2] remember me --- .../resources/admin/js/controllers/realm.js | 2 +- .../main/resources/theme/login/base/login.ftl | 9 +++++- .../login/base/messages/messages.properties | 1 + .../login/freemarker/model/RealmBean.java | 4 +++ .../services/managers/AccessCodeEntry.java | 9 ++++++ .../managers/AuthenticationManager.java | 30 ++++++++++++++----- .../services/resources/TokenService.java | 14 ++++++++- .../services/resources/flows/OAuthFlows.java | 17 +++++++++-- 8 files changed, 73 insertions(+), 13 deletions(-) mode change 100644 => 100755 forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js index 9c5e974392..760d501b3a 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js @@ -549,7 +549,7 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $scope.realm.accessTokenLifespan = TimeUnit.convert($scope.realm.accessTokenLifespan, from, to); }); - $scope.realm.centralLoginLifespanUnit = TimeUnit.autoUnit(realm.accessTokenLifespan); + $scope.realm.centralLoginLifespanUnit = TimeUnit.autoUnit(realm.centralLoginLifespan); $scope.realm.centralLoginLifespan = TimeUnit.toUnit(realm.centralLoginLifespan, $scope.realm.centralLoginLifespanUnit); $scope.$watch('realm.centralLoginLifespanUnit', function(to, from) { $scope.realm.centralLoginLifespan = TimeUnit.convert($scope.realm.centralLoginLifespan, from, to); diff --git a/forms/common-themes/src/main/resources/theme/login/base/login.ftl b/forms/common-themes/src/main/resources/theme/login/base/login.ftl index 4fd6f63aec..b609784dc1 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login.ftl @@ -28,6 +28,13 @@
+ <#if realm.rememberMe> +
+ +
+
<#if realm.registrationAllowed> ${rb.noAccount} ${rb.register} @@ -43,7 +50,7 @@
-
+
<#elseif section = "info" > diff --git a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties old mode 100644 new mode 100755 index 79c8e7b19b..9aa907ee23 --- a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties +++ b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties @@ -16,6 +16,7 @@ firstName=First name lastName=Last name email=Email password=Password +rememberMe=Remember me passwordConfirm=Confirm password passwordNew=New Password passwordNewConfirm=New Password confirmation diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java index 8a71244ffd..a6b52f0786 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java @@ -49,5 +49,9 @@ public class RealmBean { public boolean isResetPasswordAllowed() { return realm.isResetPasswordAllowed(); } + + public boolean isRememberMe() { + return realm.isRememberMe(); + } } 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 e3268cf06a..a94cb1b268 100755 --- a/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java +++ b/services/src/main/java/org/keycloak/services/managers/AccessCodeEntry.java @@ -22,6 +22,7 @@ public class AccessCodeEntry { protected String code; protected String state; protected String redirectUri; + protected boolean rememberMe; protected long expiration; protected RealmModel realm; @@ -119,4 +120,12 @@ public class AccessCodeEntry { public void setRedirectUri(String redirectUri) { this.redirectUri = redirectUri; } + + public boolean isRememberMe() { + return rememberMe; + } + + public void setRememberMe(boolean rememberMe) { + this.rememberMe = rememberMe; + } } 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 237b7dccff..b82f585c79 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -39,6 +39,7 @@ public class AuthenticationManager { protected static Logger logger = Logger.getLogger(AuthenticationManager.class); public static final String FORM_USERNAME = "username"; public static final String KEYCLOAK_IDENTITY_COOKIE = "KEYCLOAK_IDENTITY"; + public static final String KEYCLOAK_REMEMBER_ME = "KEYCLOAK_REMEMBER_ME"; public AccessToken createIdentityToken(RealmModel realm, UserModel user) { AccessToken token = new AccessToken(); @@ -52,26 +53,26 @@ public class AuthenticationManager { return token; } - public NewCookie createLoginCookie(RealmModel realm, UserModel user, UriInfo uriInfo) { + public NewCookie createLoginCookie(RealmModel realm, UserModel user, UriInfo uriInfo, boolean rememberMe) { String cookieName = KEYCLOAK_IDENTITY_COOKIE; String cookiePath = getIdentityCookiePath(realm, uriInfo); - return createLoginCookie(realm, user, null, cookieName, cookiePath); + return createLoginCookie(realm, user, null, cookieName, cookiePath, rememberMe); } public NewCookie createSaasIdentityCookie(RealmModel realm, UserModel user, UriInfo uriInfo) { String cookieName = AdminService.SAAS_IDENTITY_COOKIE; URI uri = AdminService.saasCookiePath(uriInfo).build(); String cookiePath = uri.getRawPath(); - return createLoginCookie(realm, user, null, cookieName, cookiePath); + return createLoginCookie(realm, user, null, cookieName, cookiePath, false); } public NewCookie createAccountIdentityCookie(RealmModel realm, UserModel user, UserModel client, URI uri) { String cookieName = AccountService.ACCOUNT_IDENTITY_COOKIE; String cookiePath = uri.getRawPath(); - return createLoginCookie(realm, user, client, cookieName, cookiePath); + return createLoginCookie(realm, user, client, cookieName, cookiePath, false); } - protected NewCookie createLoginCookie(RealmModel realm, UserModel user, UserModel client, String cookieName, String cookiePath) { + protected NewCookie createLoginCookie(RealmModel realm, UserModel user, UserModel client, String cookieName, String cookiePath, boolean rememberMe) { AccessToken identityToken = createIdentityToken(realm, user); if (client != null) { identityToken.issuedFor(client.getLoginName()); @@ -80,15 +81,22 @@ public class AuthenticationManager { boolean secureOnly = !realm.isSslNotRequired(); logger.debug("creatingLoginCookie - name: {0} path: {1}", cookieName, cookiePath); int maxAge = NewCookie.DEFAULT_MAX_AGE; - /* - if (realm.isRememberMe()) { + if (rememberMe) { maxAge = realm.getCentralLoginLifespan(); + logger.info("createLoginCookie maxAge: " + maxAge); } - */ NewCookie cookie = new NewCookie(cookieName, encoded, cookiePath, null, null, maxAge, secureOnly, true); return cookie; } + public NewCookie createRememberMeCookie(RealmModel realm, UriInfo uriInfo) { + String path = getIdentityCookiePath(realm, uriInfo); + boolean secureOnly = !realm.isSslNotRequired(); + // remember me cookie should be persistent + NewCookie cookie = new NewCookie(KEYCLOAK_REMEMBER_ME, "true", path, null, null, realm.getCentralLoginLifespan(), secureOnly, true); + return cookie; + } + protected String encodeToken(RealmModel realm, Object token) { String encodedToken = new JWSBuilder() .jsonContent(token) @@ -103,6 +111,12 @@ public class AuthenticationManager { String cookieName = KEYCLOAK_IDENTITY_COOKIE; expireCookie(cookieName, path); } + public void expireRememberMeCookie(RealmModel realm, UriInfo uriInfo) { + logger.debug("Expiring remember me cookie"); + String path = getIdentityCookiePath(realm, uriInfo); + String cookieName = KEYCLOAK_REMEMBER_ME; + expireCookie(cookieName, path); + } protected String getIdentityCookiePath(RealmModel realm, UriInfo uriInfo) { URI uri = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()); 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 4983d37ced..24d80907db 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -45,6 +45,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriBuilder; @@ -235,10 +236,20 @@ public class TokenService { AuthenticationStatus status = authManager.authenticateForm(realm, user, formData); + String rememberMe = formData.getFirst("rememberMe"); + boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on"); + logger.debug("*** Remember me: " + remember); + if (remember) { + NewCookie cookie = authManager.createRememberMeCookie(realm, uriInfo); + response.addNewCookie(cookie); + } else { + authManager.expireRememberMeCookie(realm, uriInfo); + } + switch (status) { case SUCCESS: case ACTIONS_REQUIRED: - return oauth.processAccessCode(scopeParam, state, redirect, client, user); + return oauth.processAccessCode(scopeParam, state, redirect, client, user, remember); case ACCOUNT_DISABLED: return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin(); case MISSING_TOTP: @@ -544,6 +555,7 @@ public class TokenService { if (user != null) { logger.info("Logging out: {0}", user.getLoginName()); authManager.expireIdentityCookie(realm, uriInfo); + authManager.expireRememberMeCookie(realm, uriInfo); resourceAdminManager.singleLogOut(realm, user.getId()); } else { logger.info("No user logged in for logout"); 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 626aab8822..8524807eeb 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 @@ -35,6 +35,7 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.resources.TokenService; +import javax.ws.rs.core.Cookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; @@ -69,13 +70,20 @@ public class OAuthFlows { } public Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect) { + return redirectAccessCode(accessCode, state, redirect, false); + } + + + public Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect, boolean rememberMe) { String code = accessCode.getCode(); UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", code); log.debug("redirectAccessCode: state: {0}", state); if (state != null) redirectUri.queryParam("state", state); Response.ResponseBuilder location = Response.status(302).location(redirectUri.build()); - location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), uriInfo)); + Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME); + rememberMe = rememberMe || remember != null; + location.cookie(authManager.createLoginCookie(realm, accessCode.getUser(), uriInfo, rememberMe)); return location.build(); } @@ -89,6 +97,11 @@ public class OAuthFlows { } public Response processAccessCode(String scopeParam, String state, String redirect, UserModel client, UserModel user) { + return processAccessCode(scopeParam, state, redirect, client, user, false); + } + + + public Response processAccessCode(String scopeParam, String state, String redirect, UserModel client, UserModel user, boolean rememberMe) { isTotpConfigurationRequired(user); isEmailVerificationRequired(user); @@ -121,7 +134,7 @@ public class OAuthFlows { } if (redirect != null) { - return redirectAccessCode(accessCode, state, redirect); + return redirectAccessCode(accessCode, state, redirect, rememberMe); } else { return null; } From 4b78857b5138ae0e18607721fe2e511d78c1c44f Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Mon, 24 Feb 2014 17:16:45 -0500 Subject: [PATCH 2/2] as7 adapter refresh token support --- .../resources/admin/js/controllers/realm.js | 1 + .../KeycloakAuthenticatedSession.java | 3 + .../java/org/keycloak/KeycloakPrincipal.java | 3 +- .../keycloak-adapter-core/main/module.xml | 1 + integration/adapter-core/pom.xml | 6 ++ .../adapters/RefreshableKeycloakSession.java | 84 +++++++++++++++++++ .../as7/KeycloakAuthenticatorValve.java | 34 +++++++- .../adapters/as7/ServletOAuthLogin.java | 6 ++ .../KeycloakAuthenticationMechanism.java | 7 +- .../undertow/KeycloakIdentityManager.java | 20 ++--- .../undertow/KeycloakServletExtension.java | 2 +- .../undertow/KeycloakUndertowAccount.java | 78 +++++------------ .../ServletPropagateSessionHandler.java | 6 +- .../undertow/UndertowKeycloakSession.java | 43 ---------- 14 files changed, 175 insertions(+), 119 deletions(-) create mode 100755 integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSession.java delete mode 100755 integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakSession.java diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js index 760d501b3a..f1a4f49422 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js @@ -592,6 +592,7 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, delete realmCopy["accessCodeLifespanUserActionUnit"]; realmCopy.accessTokenLifespan = TimeUnit.toSeconds($scope.realm.accessTokenLifespan, $scope.realm.accessTokenLifespanUnit) + realmCopy.centralLoginLifespan = TimeUnit.toSeconds($scope.realm.centralLoginLifespan, $scope.realm.centralLoginLifespanUnit) realmCopy.refreshTokenLifespan = TimeUnit.toSeconds($scope.realm.refreshTokenLifespan, $scope.realm.refreshTokenLifespanUnit) realmCopy.accessCodeLifespan = TimeUnit.toSeconds($scope.realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit) realmCopy.accessCodeLifespanUserAction = TimeUnit.toSeconds($scope.realm.accessCodeLifespanUserAction, $scope.realm.accessCodeLifespanUserActionUnit) diff --git a/core/src/main/java/org/keycloak/KeycloakAuthenticatedSession.java b/core/src/main/java/org/keycloak/KeycloakAuthenticatedSession.java index 33c87d9ac8..658e19adff 100755 --- a/core/src/main/java/org/keycloak/KeycloakAuthenticatedSession.java +++ b/core/src/main/java/org/keycloak/KeycloakAuthenticatedSession.java @@ -35,4 +35,7 @@ public class KeycloakAuthenticatedSession implements Serializable { return metadata; } + public void setMetadata(ResourceMetadata metadata) { + this.metadata = metadata; + } } diff --git a/core/src/main/java/org/keycloak/KeycloakPrincipal.java b/core/src/main/java/org/keycloak/KeycloakPrincipal.java index 07a9322d9c..e4aab9c71f 100755 --- a/core/src/main/java/org/keycloak/KeycloakPrincipal.java +++ b/core/src/main/java/org/keycloak/KeycloakPrincipal.java @@ -1,12 +1,13 @@ package org.keycloak; +import java.io.Serializable; import java.security.Principal; /** * @author Bill Burke * @version $Revision: 1 $ */ -public class KeycloakPrincipal implements Principal { +public class KeycloakPrincipal implements Principal, Serializable { protected String name; protected String surrogate; diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml index 0aa0a29d7d..5323ea3207 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml @@ -32,6 +32,7 @@ + diff --git a/integration/adapter-core/pom.xml b/integration/adapter-core/pom.xml index be1e49a0f2..ae0fc816bc 100755 --- a/integration/adapter-core/pom.xml +++ b/integration/adapter-core/pom.xml @@ -13,6 +13,12 @@ + + org.jboss.logging + jboss-logging + 3.1.2.GA + provided + org.keycloak keycloak-core diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSession.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSession.java new file mode 100755 index 0000000000..8716e0ef5c --- /dev/null +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSession.java @@ -0,0 +1,84 @@ +package org.keycloak.adapters; + +import org.keycloak.KeycloakAuthenticatedSession; +import org.keycloak.RSATokenVerifier; +import org.keycloak.VerificationException; +import org.keycloak.adapters.config.RealmConfiguration; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessTokenResponse; +import org.jboss.logging.Logger; + +import java.io.IOException; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RefreshableKeycloakSession extends KeycloakAuthenticatedSession { + + protected static Logger log = Logger.getLogger(RefreshableKeycloakSession.class); + + protected transient RealmConfiguration realmConfiguration; + protected String refreshToken; + + public RefreshableKeycloakSession() { + } + + public RefreshableKeycloakSession(String tokenString, AccessToken token, ResourceMetadata metadata, RealmConfiguration realmConfiguration, String refreshToken) { + super(tokenString, token, metadata); + this.realmConfiguration = realmConfiguration; + this.refreshToken = refreshToken; + } + + @Override + public AccessToken getToken() { + refreshExpiredToken(); + return super.getToken(); + } + + @Override + public String getTokenString() { + refreshExpiredToken(); + return super.getTokenString(); + } + + public boolean isActive() { + return this.token.isActive(); + } + + public void setRealmConfiguration(RealmConfiguration realmConfiguration) { + this.realmConfiguration = realmConfiguration; + } + + public void refreshExpiredToken() { + if (this.token.isActive()) return; + if (this.realmConfiguration == null || refreshToken == null) return; // Might be serialized in HttpSession? + + log.info("Doing refresh"); + AccessTokenResponse response = null; + try { + response = TokenGrantRequest.invokeRefresh(realmConfiguration, refreshToken); + } catch (IOException e) { + log.error("Refresh token failure", e); + return; + } catch (TokenGrantRequest.HttpFailure httpFailure) { + log.error("Refresh token failure status: " + httpFailure.getStatus() + " " + httpFailure.getError()); + return; + } + log.info("received refresh response"); + String tokenString = response.getToken(); + AccessToken token = null; + try { + token = RSATokenVerifier.verifyToken(tokenString, realmConfiguration.getMetadata().getRealmKey(), realmConfiguration.getMetadata().getRealm()); + log.info("Token Verification succeeded!"); + } catch (VerificationException e) { + log.error("failed verification of token"); + } + this.token = token; + this.refreshToken = response.getRefreshToken(); + this.tokenString = tokenString; + + } + + +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java index ced0ea2327..430b3bd506 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java @@ -16,6 +16,7 @@ import org.jboss.logging.Logger; import org.keycloak.KeycloakAuthenticatedSession; import org.keycloak.KeycloakPrincipal; import org.keycloak.adapters.AdapterConstants; +import org.keycloak.adapters.RefreshableKeycloakSession; import org.keycloak.adapters.ResourceMetadata; import org.keycloak.adapters.as7.config.CatalinaAdapterConfigLoader; import org.keycloak.representations.AccessToken; @@ -92,6 +93,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif remoteLogout(input, response); return; } + checkKeycloakSession(request); super.invoke(request, response); } finally { } @@ -184,13 +186,39 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif return false; } + /** + * Checks that access token is still valid. Will attempt refresh of token if it is not. + * + * @param request + */ + protected void checkKeycloakSession(Request request) { + if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) return; + RefreshableKeycloakSession session = (RefreshableKeycloakSession)request.getSessionInternal().getNote(KeycloakAuthenticatedSession.class.getName()); + if (session == null) return; + // just in case session got serialized + session.setRealmConfiguration(realmConfiguration); + session.setMetadata(resourceMetadata); + if (session.isActive()) return; + + // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will + // not be updated + session.refreshExpiredToken(); + if (session.isActive()) return; + + request.getSessionInternal().removeNote(KeycloakAuthenticatedSession.class.getName()); + request.setUserPrincipal(null); + request.setAuthType(null); + request.getSessionInternal().setPrincipal(null); + request.getSessionInternal().setAuthType(null); + } + protected boolean checkLoggedIn(Request request, HttpServletResponse response) { - if (request.getSessionInternal() == null || request.getSessionInternal().getPrincipal() == null) + if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) return false; log.debug("remote logged in already"); GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal(); request.setUserPrincipal(principal); - request.setAuthType("OAUTH"); + request.setAuthType("KEYCLOAK"); Session session = request.getSessionInternal(); if (session != null) { KeycloakAuthenticatedSession skSession = (KeycloakAuthenticatedSession) session.getNote(KeycloakAuthenticatedSession.class.getName()); @@ -234,7 +262,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif Session session = request.getSessionInternal(true); session.setPrincipal(principal); session.setAuthType("OAUTH"); - KeycloakAuthenticatedSession skSession = new KeycloakAuthenticatedSession(oauth.getTokenString(), token, realmConfiguration.getMetadata()); + KeycloakAuthenticatedSession skSession = new RefreshableKeycloakSession(oauth.getTokenString(), oauth.getToken(), resourceMetadata, realmConfiguration, oauth.getRefreshToken()); session.setNote(KeycloakAuthenticatedSession.class.getName(), skSession); String username = token.getSubject(); diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/ServletOAuthLogin.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/ServletOAuthLogin.java index 2f9ca0742b..35cb609396 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/ServletOAuthLogin.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/ServletOAuthLogin.java @@ -29,6 +29,7 @@ public class ServletOAuthLogin { protected int redirectPort; protected String tokenString; protected AccessToken token; + protected String refreshToken; public ServletOAuthLogin(RealmConfiguration realmInfo, HttpServletRequest request, HttpServletResponse response, int redirectPort) { this.request = request; @@ -45,6 +46,10 @@ public class ServletOAuthLogin { return token; } + public String getRefreshToken() { + return refreshToken; + } + public RealmConfiguration getRealmInfo() { return realmInfo; } @@ -249,6 +254,7 @@ public class ServletOAuthLogin { sendError(HttpServletResponse.SC_FORBIDDEN); return false; } + refreshToken = tokenResponse.getRefreshToken(); // redirect to URL without oauth query parameters sendRedirect(redirectUri); return true; diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakAuthenticationMechanism.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakAuthenticationMechanism.java index 33fd421f5e..5b9ef86d34 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakAuthenticationMechanism.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakAuthenticationMechanism.java @@ -8,6 +8,7 @@ import io.undertow.util.AttachmentKey; import org.jboss.logging.Logger; import org.keycloak.KeycloakAuthenticatedSession; import org.keycloak.KeycloakPrincipal; +import org.keycloak.adapters.RefreshableKeycloakSession; import org.keycloak.adapters.config.RealmConfiguration; import org.keycloak.adapters.ResourceMetadata; import org.keycloak.representations.AccessToken; @@ -93,7 +94,8 @@ public class KeycloakAuthenticationMechanism implements AuthenticationMechanism protected void completeAuthentication(HttpServerExchange exchange, SecurityContext securityContext, OAuthAuthenticator oauth) { final KeycloakPrincipal principal = new KeycloakPrincipal(oauth.getToken().getSubject(), null); - KeycloakUndertowAccount account = new KeycloakUndertowAccount(principal, oauth.getToken(), oauth.getTokenString(), oauth.getRefreshToken(), realmConfig, resourceMetadata, adapterConfig); + RefreshableKeycloakSession session = new RefreshableKeycloakSession(oauth.getTokenString(), oauth.getToken(), resourceMetadata, realmConfig, oauth.getRefreshToken()); + KeycloakUndertowAccount account = new KeycloakUndertowAccount(principal, session, adapterConfig, resourceMetadata); securityContext.authenticationComplete(account, "KEYCLOAK", true); login(exchange, account); } @@ -105,7 +107,8 @@ public class KeycloakAuthenticationMechanism implements AuthenticationMechanism protected void completeAuthentication(SecurityContext securityContext, BearerTokenAuthenticator bearer) { final KeycloakPrincipal principal = new KeycloakPrincipal(bearer.getToken().getSubject(), bearer.getSurrogate()); - KeycloakUndertowAccount account = new KeycloakUndertowAccount(principal, bearer.getToken(), bearer.getTokenString(), null, realmConfig, resourceMetadata, adapterConfig); + RefreshableKeycloakSession session = new RefreshableKeycloakSession(bearer.getTokenString(), bearer.getToken(), resourceMetadata, realmConfig, null); + KeycloakUndertowAccount account = new KeycloakUndertowAccount(principal, session, adapterConfig, resourceMetadata); securityContext.authenticationComplete(account, "KEYCLOAK", false); } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakIdentityManager.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakIdentityManager.java index 00d4a8f072..34406da4b4 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakIdentityManager.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakIdentityManager.java @@ -23,29 +23,29 @@ import java.io.IOException; */ class KeycloakIdentityManager implements IdentityManager { protected static Logger log = Logger.getLogger(KeycloakIdentityManager.class); + protected AdapterConfig adapterConfig; + protected RealmConfiguration realmConfiguration; + + KeycloakIdentityManager(AdapterConfig adapterConfig, RealmConfiguration realmConfiguration) { + this.adapterConfig = adapterConfig; + this.realmConfiguration = realmConfiguration; + } @Override public Account verify(Account account) { log.info("Verifying account in IdentityManager"); KeycloakUndertowAccount keycloakAccount = (KeycloakUndertowAccount)account; - if (keycloakAccount.getAccessToken().isActive()) { - log.info("account is still active. Time left: " + (keycloakAccount.getAccessToken().getExpiration() - (System.currentTimeMillis()/1000)) ); - return account; - } - keycloakAccount.refreshExpiredToken(); - if (!keycloakAccount.getAccessToken().isActive()) return null; + if (!keycloakAccount.isActive(realmConfiguration, adapterConfig)) return null; return account; } @Override public Account verify(String id, Credential credential) { - KeycloakServletExtension.log.warn("Shouldn't call verify!!!"); - throw new IllegalStateException("Not allowed"); + throw new IllegalStateException("Unsupported verify method"); } @Override public Account verify(Credential credential) { - KeycloakServletExtension.log.warn("Shouldn't call verify!!!"); - throw new IllegalStateException("Not allowed"); + throw new IllegalStateException("Unsupported verify method"); } } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java index c7948eed17..02aff6c1d6 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java @@ -95,7 +95,7 @@ public class KeycloakServletExtension implements ServletExtension { deploymentInfo.addInnerHandlerChainWrapper(ServletPropagateSessionHandler.WRAPPER); // propagates SkeletonKeySession deploymentInfo.addInnerHandlerChainWrapper(actions); // handles authenticated actions and cors. - deploymentInfo.setIdentityManager(new KeycloakIdentityManager()); + deploymentInfo.setIdentityManager(new KeycloakIdentityManager(keycloakConfig, realmConfiguration)); log.info("Setting jsession cookie path to: " + deploymentInfo.getContextPath()); ServletSessionConfig cookieConfig = new ServletSessionConfig(); diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakUndertowAccount.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakUndertowAccount.java index dd2b1743b3..8f0268939d 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakUndertowAccount.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakUndertowAccount.java @@ -3,16 +3,13 @@ package org.keycloak.adapters.undertow; import io.undertow.security.idm.Account; import org.jboss.logging.Logger; import org.keycloak.KeycloakPrincipal; -import org.keycloak.RSATokenVerifier; -import org.keycloak.VerificationException; +import org.keycloak.adapters.RefreshableKeycloakSession; import org.keycloak.adapters.ResourceMetadata; -import org.keycloak.adapters.TokenGrantRequest; import org.keycloak.adapters.config.RealmConfiguration; import org.keycloak.representations.AccessToken; -import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.adapters.config.AdapterConfig; -import java.io.IOException; +import java.io.Serializable; import java.security.Principal; import java.util.Collections; import java.util.Set; @@ -21,30 +18,19 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public class KeycloakUndertowAccount implements Account { +public class KeycloakUndertowAccount implements Account, Serializable { protected static Logger log = Logger.getLogger(KeycloakUndertowAccount.class); - protected AccessToken accessToken; - protected String encodedAccessToken; - protected String refreshToken; + protected RefreshableKeycloakSession session; protected KeycloakPrincipal principal; protected Set accountRoles; - protected RealmConfiguration realmConfiguration; - protected ResourceMetadata resourceMetadata; - protected AdapterConfig adapterConfig; - public KeycloakUndertowAccount(KeycloakPrincipal principal, AccessToken accessToken, String encodedAccessToken, String refreshToken, - RealmConfiguration realmConfiguration, ResourceMetadata resourceMetadata, AdapterConfig adapterConfig) { + public KeycloakUndertowAccount(KeycloakPrincipal principal, RefreshableKeycloakSession session, AdapterConfig config, ResourceMetadata metadata) { this.principal = principal; - this.accessToken = accessToken; - this.encodedAccessToken = encodedAccessToken; - this.refreshToken = refreshToken; - this.realmConfiguration = realmConfiguration; - this.resourceMetadata = resourceMetadata; - this.adapterConfig = adapterConfig; - setRoles(accessToken); + this.session = session; + setRoles(session.getToken(), config, metadata); } - protected void setRoles(AccessToken accessToken) { + protected void setRoles(AccessToken accessToken, AdapterConfig adapterConfig, ResourceMetadata resourceMetadata) { Set roles = null; if (adapterConfig.isUseResourceRoleMappings()) { AccessToken.Access access = accessToken.getResourceAccess(resourceMetadata.getResourceName()); @@ -68,48 +54,30 @@ public class KeycloakUndertowAccount implements Account { } public AccessToken getAccessToken() { - return accessToken; + return session.getToken(); } public String getEncodedAccessToken() { - return encodedAccessToken; + return session.getTokenString(); } - public String getRefreshToken() { - return refreshToken; + public RefreshableKeycloakSession getSession() { + return session; } - public ResourceMetadata getResourceMetadata() { - return resourceMetadata; + public boolean isActive(RealmConfiguration realmConfiguration, AdapterConfig config) { + // this object may have been serialized, so we need to reset realm config/metadata + session.setRealmConfiguration(realmConfiguration); + session.setMetadata(realmConfiguration.getMetadata()); + if (session.isActive()) return true; + + session.refreshExpiredToken(); + if (!session.isActive()) return false; + + setRoles(session.getToken(), config, realmConfiguration.getMetadata()); + return true; } - public void refreshExpiredToken() { - if (accessToken.isActive()) return; - log.info("Doing refresh"); - AccessTokenResponse response = null; - try { - response = TokenGrantRequest.invokeRefresh(realmConfiguration, getRefreshToken()); - } catch (IOException e) { - log.error("Refresh token failure", e); - return; - } catch (TokenGrantRequest.HttpFailure httpFailure) { - log.error("Refresh token failure status: " + httpFailure.getStatus() + " " + httpFailure.getError()); - return; - } - log.info("received refresh response"); - String tokenString = response.getToken(); - AccessToken token = null; - try { - token = RSATokenVerifier.verifyToken(tokenString, realmConfiguration.getMetadata().getRealmKey(), realmConfiguration.getMetadata().getRealm()); - log.info("Token Verification succeeded!"); - } catch (VerificationException e) { - log.error("failed verification of token"); - } - this.accessToken = token; - this.refreshToken = response.getRefreshToken(); - this.encodedAccessToken = tokenString; - setRoles(this.accessToken); - } } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPropagateSessionHandler.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPropagateSessionHandler.java index 9baa829953..efd5662df8 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPropagateSessionHandler.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPropagateSessionHandler.java @@ -40,12 +40,10 @@ public class ServletPropagateSessionHandler implements HttpHandler { next.handleRequest(exchange); return; } - UndertowKeycloakSession skSession = new UndertowKeycloakSession(account); - final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest(); - req.setAttribute(KeycloakAuthenticatedSession.class.getName(), skSession); + req.setAttribute(KeycloakAuthenticatedSession.class.getName(), account.getSession()); HttpSession session = req.getSession(false); if (session == null) { @@ -53,7 +51,7 @@ public class ServletPropagateSessionHandler implements HttpHandler { return; } log.debug("propagating to HTTP Session"); - session.setAttribute(KeycloakAuthenticatedSession.class.getName(), skSession); + session.setAttribute(KeycloakAuthenticatedSession.class.getName(), account.getSession()); next.handleRequest(exchange); } } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakSession.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakSession.java deleted file mode 100755 index 22f79d946b..0000000000 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakSession.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.keycloak.adapters.undertow; - -import org.keycloak.KeycloakAuthenticatedSession; -import org.keycloak.adapters.ResourceMetadata; -import org.keycloak.representations.AccessToken; - -import java.io.Serializable; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class UndertowKeycloakSession extends KeycloakAuthenticatedSession { - - private transient KeycloakUndertowAccount account; - - public UndertowKeycloakSession(KeycloakUndertowAccount account) { - super(account.getEncodedAccessToken(), account.getAccessToken(), account.getResourceMetadata()); - this.account = account; - } - - @Override - public AccessToken getToken() { - checkExpiration(); - return super.getToken(); - } - - private void checkExpiration() { - if (token.isExpired() && account != null) { - account.refreshExpiredToken(); - this.token = account.getAccessToken(); - this.tokenString = account.getEncodedAccessToken(); - - } - } - - @Override - public String getTokenString() { - checkExpiration(); - return super.getTokenString(); - } - -}