From 56e011dce45f302285c1ae928b2071e6df228f7c Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 21 Jul 2016 18:18:05 +0200 Subject: [PATCH] KEYCLOAK-3318 Adapter support for prompt and max_age. Refactoring to not hardcode OIDC specifics to CookieAuthenticator --- .../adapters/OAuthRequestAuthenticator.java | 12 ++++++ .../oidc/js/src/main/resources/keycloak.js | 4 ++ .../java/org/keycloak/OAuth2Constants.java | 4 ++ .../org/keycloak/protocol/LoginProtocol.java | 7 ++++ .../browser/CookieAuthenticator.java | 37 +++---------------- .../protocol/oidc/OIDCLoginProtocol.java | 37 ++++++++++++++++++- .../keycloak/protocol/saml/SamlProtocol.java | 6 +++ .../keycloak/testsuite/page/AbstractPage.java | 5 +++ .../AbstractDemoServletsAdapterTest.java | 30 +++++++++++++++ 9 files changed, 108 insertions(+), 34 deletions(-) diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java index 25969a6d68..73aa0f52e7 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java @@ -160,6 +160,12 @@ public class OAuthRequestAuthenticator { String scope = getQueryParamValue(OAuth2Constants.SCOPE); url = UriUtils.stripQueryParam(url, OAuth2Constants.SCOPE); + String prompt = getQueryParamValue(OAuth2Constants.PROMPT); + url = UriUtils.stripQueryParam(url, OAuth2Constants.PROMPT); + + String maxAge = getQueryParamValue(OAuth2Constants.MAX_AGE); + url = UriUtils.stripQueryParam(url, OAuth2Constants.MAX_AGE); + KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone() .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName()) @@ -172,6 +178,12 @@ public class OAuthRequestAuthenticator { if (idpHint != null && idpHint.length() > 0) { redirectUriBuilder.queryParam(AdapterConstants.KC_IDP_HINT,idpHint); } + if (prompt != null && prompt.length() > 0) { + redirectUriBuilder.queryParam(OAuth2Constants.PROMPT, prompt); + } + if (maxAge != null && maxAge.length() > 0) { + redirectUriBuilder.queryParam(OAuth2Constants.MAX_AGE, maxAge); + } scope = TokenUtil.attachOIDCScope(scope); redirectUriBuilder.queryParam(OAuth2Constants.SCOPE, scope); diff --git a/adapters/oidc/js/src/main/resources/keycloak.js b/adapters/oidc/js/src/main/resources/keycloak.js index a67852866c..563f7cacc5 100755 --- a/adapters/oidc/js/src/main/resources/keycloak.js +++ b/adapters/oidc/js/src/main/resources/keycloak.js @@ -224,6 +224,10 @@ url += '&prompt=' + encodeURIComponent(options.prompt); } + if (options && options.maxAge) { + url += '&max_age=' + encodeURIComponent(options.maxAge); + } + if (options && options.loginHint) { url += '&login_hint=' + encodeURIComponent(options.loginHint); } diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index 6c00831320..188d7593b9 100644 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -73,6 +73,10 @@ public interface OAuth2Constants { String UI_LOCALES_PARAM = "ui_locales"; + String PROMPT = "prompt"; + + String MAX_AGE = "max_age"; + } diff --git a/server-spi/src/main/java/org/keycloak/protocol/LoginProtocol.java b/server-spi/src/main/java/org/keycloak/protocol/LoginProtocol.java index 6cc1be481f..086a8edc48 100755 --- a/server-spi/src/main/java/org/keycloak/protocol/LoginProtocol.java +++ b/server-spi/src/main/java/org/keycloak/protocol/LoginProtocol.java @@ -74,4 +74,11 @@ public interface LoginProtocol extends Provider { Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession); Response finishLogout(UserSessionModel userSession); + /** + * @param userSession + * @param clientSession + * @return true if SSO cookie authentication can't be used. User will need to "actively" reauthenticate + */ + boolean requireReauthentication(UserSessionModel userSession, ClientSessionModel clientSession); + } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java index 24d708acab..6c961e1d70 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java @@ -25,6 +25,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.ServicesLogger; import org.keycloak.services.managers.AuthenticationManager; @@ -36,8 +37,6 @@ import org.keycloak.util.TokenUtil; */ public class CookieAuthenticator implements Authenticator { - private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER; - @Override public boolean requiresUser() { return false; @@ -50,11 +49,13 @@ public class CookieAuthenticator implements Authenticator { if (authResult == null) { context.attempted(); } else { + ClientSessionModel clientSession = context.getClientSession(); + LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, clientSession.getAuthMethod()); + // Cookie re-authentication is skipped if re-authentication is required - if (requireReauthentication(authResult.getSession(), context.getClientSession())) { + if (protocol.requireReauthentication(authResult.getSession(), clientSession)) { context.attempted(); } else { - ClientSessionModel clientSession = context.getClientSession(); clientSession.setNote(AuthenticationManager.SSO_AUTH, "true"); context.setUser(authResult.getUser()); @@ -83,32 +84,4 @@ public class CookieAuthenticator implements Authenticator { public void close() { } - - protected boolean requireReauthentication(UserSessionModel userSession, ClientSessionModel clientSession) { - return isPromptLogin(clientSession) || isAuthTimeExpired(userSession, clientSession); - } - - protected boolean isPromptLogin(ClientSessionModel clientSession) { - String prompt = clientSession.getNote(OIDCLoginProtocol.PROMPT_PARAM); - return TokenUtil.hasPrompt(prompt, OIDCLoginProtocol.PROMPT_VALUE_LOGIN); - } - - protected boolean isAuthTimeExpired(UserSessionModel userSession, ClientSessionModel clientSession) { - String authTime = userSession.getNote(AuthenticationManager.AUTH_TIME); - String maxAge = clientSession.getNote(OIDCLoginProtocol.MAX_AGE_PARAM); - if (maxAge == null) { - return false; - } - - int authTimeInt = authTime==null ? 0 : Integer.parseInt(authTime); - int maxAgeInt = Integer.parseInt(maxAge); - - if (authTimeInt + maxAgeInt < Time.currentTime()) { - logger.debugf("Authentication time is expired in CookieAuthenticator. userSession=%s, clientId=%s, maxAge=%d, authTime=%d", userSession.getId(), - clientSession.getClient().getId(), maxAgeInt, authTimeInt); - return true; - } - - return false; - } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index 39dc288d8c..4391fbad1a 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -18,6 +18,7 @@ package org.keycloak.protocol.oidc; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; +import org.keycloak.common.util.Time; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; @@ -33,8 +34,10 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseMode; import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.services.ServicesLogger; +import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ResourceAdminManager; +import org.keycloak.util.TokenUtil; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; @@ -57,8 +60,8 @@ public class OIDCLoginProtocol implements LoginProtocol { public static final String REDIRECT_URI_PARAM = "redirect_uri"; public static final String CLIENT_ID_PARAM = "client_id"; public static final String NONCE_PARAM = "nonce"; - public static final String MAX_AGE_PARAM = "max_age"; - public static final String PROMPT_PARAM = "prompt"; + public static final String MAX_AGE_PARAM = OAuth2Constants.MAX_AGE; + public static final String PROMPT_PARAM = OAuth2Constants.PROMPT; public static final String LOGIN_HINT_PARAM = "login_hint"; public static final String LOGOUT_REDIRECT_URI = "OIDC_LOGOUT_REDIRECT_URI"; public static final String ISSUER = "iss"; @@ -242,6 +245,36 @@ public class OIDCLoginProtocol implements LoginProtocol { } } + + @Override + public boolean requireReauthentication(UserSessionModel userSession, ClientSessionModel clientSession) { + return isPromptLogin(clientSession) || isAuthTimeExpired(userSession, clientSession); + } + + protected boolean isPromptLogin(ClientSessionModel clientSession) { + String prompt = clientSession.getNote(OIDCLoginProtocol.PROMPT_PARAM); + return TokenUtil.hasPrompt(prompt, OIDCLoginProtocol.PROMPT_VALUE_LOGIN); + } + + protected boolean isAuthTimeExpired(UserSessionModel userSession, ClientSessionModel clientSession) { + String authTime = userSession.getNote(AuthenticationManager.AUTH_TIME); + String maxAge = clientSession.getNote(OIDCLoginProtocol.MAX_AGE_PARAM); + if (maxAge == null) { + return false; + } + + int authTimeInt = authTime==null ? 0 : Integer.parseInt(authTime); + int maxAgeInt = Integer.parseInt(maxAge); + + if (authTimeInt + maxAgeInt < Time.currentTime()) { + logger.debugf("Authentication time is expired, needs to reauthenticate. userSession=%s, clientId=%s, maxAge=%d, authTime=%d", userSession.getId(), + clientSession.getClient().getId(), maxAgeInt, authTimeInt); + return true; + } + + return false; + } + @Override public void close() { diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 042779f237..85e316fb15 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -629,6 +629,12 @@ public class SamlProtocol implements LoginProtocol { return logoutBuilder; } + @Override + public boolean requireReauthentication(UserSessionModel userSession, ClientSessionModel clientSession) { + // Not yet supported + return false; + } + private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient) { JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(); if (samlClient.requiresRealmSignature()) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/AbstractPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/AbstractPage.java index 62510e0469..5ed6afd27a 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/AbstractPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/AbstractPage.java @@ -72,6 +72,11 @@ public abstract class AbstractPage { return this; } + public AbstractPage removeUriParameter(String name) { + uriParameters.remove(name); + return this; + } + public Object getUriParameter(String name) { return uriParameters.get(name); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java index 3b67027fe2..5191e55869 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java @@ -25,7 +25,9 @@ import org.junit.Ignore; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.common.Version; +import org.keycloak.common.util.Time; import org.keycloak.constants.AdapterConstants; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.AccessToken; import org.keycloak.representations.VersionRepresentation; @@ -33,6 +35,7 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter; import org.keycloak.testsuite.adapter.page.*; +import org.keycloak.testsuite.util.URLUtils; import org.keycloak.util.BasicAuthHelper; import javax.ws.rs.client.Client; @@ -448,5 +451,32 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd setAdapterAndServerTimeOffset(0, tokenMinTTLPage.toString()); } + // Tests forwarding of parameters like "prompt" + @Test + public void testOIDCParamsForwarding() { + // test login to customer-portal which does a bearer request to customer-db + securePortal.navigateTo(); + assertCurrentUrlStartsWithLoginUrlOf(testRealmPage); + testRealmLoginPage.form().login("bburke@redhat.com", "password"); + assertCurrentUrlEquals(securePortal); + String pageSource = driver.getPageSource(); + assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen")); + + int currentTime = Time.currentTime(); + setAdapterAndServerTimeOffset(10, securePortal.toString()); + + // Test I need to reauthenticate with prompt=login + String appUri = tokenMinTTLPage.getUriBuilder().queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN).build().toString(); + URLUtils.navigateToUri(driver, appUri, true); + assertCurrentUrlStartsWithLoginUrlOf(testRealmPage); + testRealmLoginPage.form().login("bburke@redhat.com", "password"); + AccessToken token = tokenMinTTLPage.getAccessToken(); + int authTime = token.getAuthTime(); + Assert.assertTrue(currentTime + 10 <= authTime); + + // Revert times + setAdapterAndServerTimeOffset(0, tokenMinTTLPage.toString()); + } + }