keycloak-aplcache
Changes
adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java 2(+1 -1)
core/src/main/java/org/keycloak/TokenVerifier.java 380(+320 -60)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml 1(+1 -0)
examples/kerberos/README.md 2(+1 -1)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientPoliciesResource.java 1(+1 -0)
misc/Testsuite.md 6(+6 -0)
model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java 57(+55 -2)
model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java 9(+9 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/AddInvalidatedActionTokenEvent.java 50(+50 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java 12(+12 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java 53(+53 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RemoveActionTokensSpecificEvent.java 42(+42 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java 197(+197 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java 199(+119 -80)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java 104(+104 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java 76(+76 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java 64(+3 -61)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java 152(+152 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java 16(+10 -6)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java 98(+98 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java 100(+100 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java 162(+162 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java 113(+113 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java 218(+218 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProvider.java 78(+78 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java 58(+58 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 497(+105 -392)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java 8(+1 -7)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java 129(+0 -129)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java 77(+0 -77)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthenticationSessionPredicate.java 85(+38 -47)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java 15(+15 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java 38(+12 -26)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java 11(+11 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java 64(+50 -14)
model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory 1(+1 -0)
model/infinispan/src/main/resources/META-INF/services/org.keycloak.sessions.AuthenticationSessionProviderFactory 18(+18 -0)
model/infinispan/src/main/resources/META-INF/services/org.keycloak.sessions.StickySessionEncoderProviderFactory 18(+18 -0)
model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java 30(+15 -15)
model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java 1(+0 -1)
model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java 35(+17 -18)
server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java 22(+18 -4)
server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java 4(+2 -2)
server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java 12(+6 -6)
server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java 27(+27 -0)
server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java 6(+3 -3)
server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java 135(+12 -123)
server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java 8(+0 -8)
server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java 25(+17 -8)
server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java 6(+3 -3)
server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java 26(+26 -0)
server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProviderFactory.java 26(+26 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java 104(+104 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerFactory.java 27(+27 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerSpi.java 50(+50 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java 79(+79 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionToken.java 71(+71 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java 107(+107 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/ExplainedTokenVerificationException.java 60(+60 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java 65(+65 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java 98(+98 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionToken.java 36(+36 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java 108(+108 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java 51(+51 -0)
services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java 89(+89 -0)
services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java 22(+11 -11)
services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpConfirmLinkAuthenticator.java 16(+10 -6)
services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java 8(+4 -4)
services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java 145(+77 -68)
services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java 7(+3 -4)
services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java 6(+3 -3)
services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java 22(+11 -11)
services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java 6(+3 -3)
services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java 8(+4 -4)
services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java 2(+1 -1)
services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java 4(+2 -2)
services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java 3(+1 -2)
services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java 3(+1 -2)
services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java 2(+1 -1)
services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java 25(+17 -8)
services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java 78(+38 -40)
services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java 9(+1 -8)
services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java 2(+1 -1)
services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java 3(+1 -2)
services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java 4(+2 -2)
services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java 85(+28 -57)
services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java 4(+2 -2)
services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLAttributeStatementMapper.java 4(+2 -2)
services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java 4(+2 -2)
services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java 4(+2 -2)
services/src/main/java/org/keycloak/protocol/saml/mappers/UserSessionNoteStatementMapper.java 4(+2 -2)
services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java 2(+1 -1)
services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java 128(+128 -0)
services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory 4(+4 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java 333(+333 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java 32(+19 -13)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java 4(+2 -2)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java 4(+2 -2)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java 20(+7 -13)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java 1(+1 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java 281(+281 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java 4(+2 -2)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java 16(+9 -7)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java 30(+16 -14)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java 113(+42 -71)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java 362(+185 -177)
testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java 4(+2 -2)
testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/PersistSessionsCommand.java 27(+11 -16)
testsuite/integration-arquillian/HOW-TO-RUN.md 106(+91 -15)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughRegistration.java 10(+5 -5)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java 1(+1 -0)
testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java 51(+44 -7)
testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowArquillianExtension.java 2(+2 -0)
testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java 38(+37 -1)
testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java 298(+298 -0)
testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java 48(+48 -0)
testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java 87(+87 -0)
testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java 53(+53 -0)
testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java 4(+2 -2)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java 29(+26 -3)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/LoginPasswordUpdatePage.java 7(+7 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java 7(+7 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginExpiredPage.java 51(+51 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java 7(+7 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java 7(+4 -3)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java 34(+34 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java 31(+26 -5)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java 40(+21 -19)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java 2(+0 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java 57(+52 -5)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java 279(+209 -70)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java 38(+21 -17)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java 9(+2 -7)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java 21(+13 -8)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java 17(+8 -9)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java 6(+4 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java 5(+4 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java 5(+4 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java 98(+97 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java 14(+14 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractPolicyManagementTest.java 13(+9 -4)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClientPolicyManagementTest.java 3(+3 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java 3(+0 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java 10(+10 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java 120(+115 -5)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java 30(+16 -14)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java 3(+1 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RequireUmaAuthorizationScopeTest.java 14(+9 -5)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java 16(+13 -3)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractFailoverClusterTest.java 112(+112 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionFailoverClusterTest.java 171(+171 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/SessionFailoverClusterTest.java 86(+2 -84)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java 6(+5 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java 24(+19 -5)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java 376(+376 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java 98(+85 -13)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java 290(+290 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java 137(+128 -9)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java 315(+144 -171)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java 43(+42 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java 9(+2 -7)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java 14(+5 -9)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java 4(+3 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java 3(+3 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java 37(+34 -3)
testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json 5(+3 -2)
testsuite/integration-arquillian/tests/base/src/test/resources/scripts/client-session-test.js 2(+1 -1)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java
index e6d6588..38fa9d3 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java
@@ -170,8 +170,8 @@ public class HttpClientBuilder {
return this;
}
- public HttpClientBuilder disableCookieCache() {
- this.disableCookieCache = true;
+ public HttpClientBuilder disableCookieCache(boolean disable) {
+ this.disableCookieCache = disable;
return this;
}
@@ -334,7 +334,7 @@ public class HttpClientBuilder {
}
public HttpClient build(AdapterHttpClientConfig adapterConfig) {
- disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing
+ disableCookieCache(true); // disable cookie cache as we don't want sticky sessions for load balancing
String truststorePath = adapterConfig.getTruststore();
if (truststorePath != null) {
diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java
index e28500b..e03158f 100755
--- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java
+++ b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java
@@ -168,7 +168,7 @@ public class OIDCFilterSessionStore extends FilterSessionStore implements Adapte
HttpSession httpSession = request.getSession();
httpSession.setAttribute(KeycloakAccount.class.getName(), sAccount);
httpSession.setAttribute(KeycloakSecurityContext.class.getName(), sAccount.getKeycloakSecurityContext());
- if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getClientSession(), account.getPrincipal().getName(), httpSession.getId());
+ if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getSessionState(), account.getPrincipal().getName(), httpSession.getId());
//String username = securityContext.getToken().getSubject();
//log.fine("userSessionManagement.login: " + username);
}
diff --git a/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java b/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java
new file mode 100644
index 0000000..4740567
--- /dev/null
+++ b/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.exceptions;
+
+import org.keycloak.representations.JsonWebToken;
+
+/**
+ * Exception thrown for cases when token is invalid due to time constraints (expired, or not yet valid).
+ * Cf. {@link JsonWebToken#isActive()}.
+ * @author hmlnarik
+ */
+public class TokenNotActiveException extends TokenVerificationException {
+
+ public TokenNotActiveException(JsonWebToken token) {
+ super(token);
+ }
+
+ public TokenNotActiveException(JsonWebToken token, String message) {
+ super(token, message);
+ }
+
+ public TokenNotActiveException(JsonWebToken token, String message, Throwable cause) {
+ super(token, message, cause);
+ }
+
+ public TokenNotActiveException(JsonWebToken token, Throwable cause) {
+ super(token, cause);
+ }
+
+}
diff --git a/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java b/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java
new file mode 100644
index 0000000..4d389eb
--- /dev/null
+++ b/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.exceptions;
+
+import org.keycloak.representations.JsonWebToken;
+
+/**
+ * Thrown when token signature is invalid.
+ * @author hmlnarik
+ */
+public class TokenSignatureInvalidException extends TokenVerificationException {
+
+ public TokenSignatureInvalidException(JsonWebToken token) {
+ super(token);
+ }
+
+ public TokenSignatureInvalidException(JsonWebToken token, String message) {
+ super(token, message);
+ }
+
+ public TokenSignatureInvalidException(JsonWebToken token, String message, Throwable cause) {
+ super(token, message, cause);
+ }
+
+ public TokenSignatureInvalidException(JsonWebToken token, Throwable cause) {
+ super(token, cause);
+ }
+
+}
diff --git a/core/src/main/java/org/keycloak/exceptions/TokenVerificationException.java b/core/src/main/java/org/keycloak/exceptions/TokenVerificationException.java
new file mode 100644
index 0000000..4d6b7d0
--- /dev/null
+++ b/core/src/main/java/org/keycloak/exceptions/TokenVerificationException.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.exceptions;
+
+import org.keycloak.common.VerificationException;
+import org.keycloak.representations.JsonWebToken;
+
+/**
+ * Exception thrown on failed verification of a token.
+ *
+ * @author hmlnarik
+ */
+public class TokenVerificationException extends VerificationException {
+
+ private final JsonWebToken token;
+
+ public TokenVerificationException(JsonWebToken token) {
+ this.token = token;
+ }
+
+ public TokenVerificationException(JsonWebToken token, String message) {
+ super(message);
+ this.token = token;
+ }
+
+ public TokenVerificationException(JsonWebToken token, String message, Throwable cause) {
+ super(message, cause);
+ this.token = token;
+ }
+
+ public TokenVerificationException(JsonWebToken token, Throwable cause) {
+ super(cause);
+ this.token = token;
+ }
+
+ public JsonWebToken getToken() {
+ return token;
+ }
+
+}
diff --git a/core/src/main/java/org/keycloak/representations/AccessToken.java b/core/src/main/java/org/keycloak/representations/AccessToken.java
index 4ef6831..36778e1 100755
--- a/core/src/main/java/org/keycloak/representations/AccessToken.java
+++ b/core/src/main/java/org/keycloak/representations/AccessToken.java
@@ -97,9 +97,6 @@ public class AccessToken extends IDToken {
}
}
- @JsonProperty("client_session")
- protected String clientSession;
-
@JsonProperty("trusted-certs")
protected Set<String> trustedCertificates;
@@ -156,10 +153,6 @@ public class AccessToken extends IDToken {
return resourceAccess.get(resource);
}
- public String getClientSession() {
- return clientSession;
- }
-
public Access addAccess(String service) {
Access access = resourceAccess.get(service);
if (access != null) return access;
@@ -168,11 +161,6 @@ public class AccessToken extends IDToken {
return access;
}
- public AccessToken clientSession(String session) {
- this.clientSession = session;
- return this;
- }
-
@Override
public AccessToken id(String id) {
return (AccessToken) super.id(id);
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index b14e55b..670e1d8 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -46,6 +46,8 @@ public class RealmRepresentation {
protected Integer accessCodeLifespan;
protected Integer accessCodeLifespanUserAction;
protected Integer accessCodeLifespanLogin;
+ protected Integer actionTokenGeneratedByAdminLifespan;
+ protected Integer actionTokenGeneratedByUserLifespan;
protected Boolean enabled;
protected String sslRequired;
@Deprecated
@@ -338,6 +340,22 @@ public class RealmRepresentation {
this.accessCodeLifespanLogin = accessCodeLifespanLogin;
}
+ public Integer getActionTokenGeneratedByAdminLifespan() {
+ return actionTokenGeneratedByAdminLifespan;
+ }
+
+ public void setActionTokenGeneratedByAdminLifespan(Integer actionTokenGeneratedByAdminLifespan) {
+ this.actionTokenGeneratedByAdminLifespan = actionTokenGeneratedByAdminLifespan;
+ }
+
+ public Integer getActionTokenGeneratedByUserLifespan() {
+ return actionTokenGeneratedByUserLifespan;
+ }
+
+ public void setActionTokenGeneratedByUserLifespan(Integer actionTokenGeneratedByUserLifespan) {
+ this.actionTokenGeneratedByUserLifespan = actionTokenGeneratedByUserLifespan;
+ }
+
public List<String> getDefaultRoles() {
return defaultRoles;
}
diff --git a/core/src/main/java/org/keycloak/representations/RefreshToken.java b/core/src/main/java/org/keycloak/representations/RefreshToken.java
index 4b89cf6..3a7a951 100755
--- a/core/src/main/java/org/keycloak/representations/RefreshToken.java
+++ b/core/src/main/java/org/keycloak/representations/RefreshToken.java
@@ -40,7 +40,6 @@ public class RefreshToken extends AccessToken {
*/
public RefreshToken(AccessToken token) {
this();
- this.clientSession = token.getClientSession();
this.issuer = token.issuer;
this.subject = token.subject;
this.issuedFor = token.issuedFor;
diff --git a/core/src/main/java/org/keycloak/RSATokenVerifier.java b/core/src/main/java/org/keycloak/RSATokenVerifier.java
index db8fc5a..0e3c08b 100755
--- a/core/src/main/java/org/keycloak/RSATokenVerifier.java
+++ b/core/src/main/java/org/keycloak/RSATokenVerifier.java
@@ -29,10 +29,10 @@ import java.security.PublicKey;
*/
public class RSATokenVerifier {
- private TokenVerifier tokenVerifier;
+ private final TokenVerifier<AccessToken> tokenVerifier;
private RSATokenVerifier(String tokenString) {
- this.tokenVerifier = TokenVerifier.create(tokenString);
+ this.tokenVerifier = TokenVerifier.create(tokenString, AccessToken.class).withDefaultChecks();
}
public static RSATokenVerifier create(String tokenString) {
core/src/main/java/org/keycloak/TokenVerifier.java 380(+320 -60)
diff --git a/core/src/main/java/org/keycloak/TokenVerifier.java b/core/src/main/java/org/keycloak/TokenVerifier.java
index 9c30bfd..0c6e2db 100755
--- a/core/src/main/java/org/keycloak/TokenVerifier.java
+++ b/core/src/main/java/org/keycloak/TokenVerifier.java
@@ -18,7 +18,8 @@
package org.keycloak;
import org.keycloak.common.VerificationException;
-import org.keycloak.jose.jws.Algorithm;
+import org.keycloak.exceptions.TokenNotActiveException;
+import org.keycloak.exceptions.TokenSignatureInvalidException;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
@@ -26,67 +27,280 @@ import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.TokenUtil;
import javax.crypto.SecretKey;
import java.security.PublicKey;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class TokenVerifier {
+public class TokenVerifier<T extends JsonWebToken> {
+
+ private static final Logger LOG = Logger.getLogger(TokenVerifier.class.getName());
+
+ // This interface is here as JDK 7 is a requirement for this project.
+ // Once JDK 8 would become mandatory, java.util.function.Predicate would be used instead.
+
+ /**
+ * Functional interface of checks that verify some part of a JWT.
+ * @param <T> Type of the token handled by this predicate.
+ */
+ // @FunctionalInterface
+ public static interface Predicate<T extends JsonWebToken> {
+ /**
+ * Performs a single check on the given token verifier.
+ * @param t Token, guaranteed to be non-null.
+ * @return
+ * @throws VerificationException
+ */
+ boolean test(T t) throws VerificationException;
+ }
+
+ public static final Predicate<JsonWebToken> SUBJECT_EXISTS_CHECK = new Predicate<JsonWebToken>() {
+ @Override
+ public boolean test(JsonWebToken t) throws VerificationException {
+ String subject = t.getSubject();
+ if (subject == null) {
+ throw new VerificationException("Subject missing in token");
+ }
+
+ return true;
+ }
+ };
+
+ /**
+ * Check for token being neither expired nor used before it gets valid.
+ * @see JsonWebToken#isActive()
+ */
+ public static final Predicate<JsonWebToken> IS_ACTIVE = new Predicate<JsonWebToken>() {
+ @Override
+ public boolean test(JsonWebToken t) throws VerificationException {
+ if (! t.isActive()) {
+ throw new TokenNotActiveException(t, "Token is not active");
+ }
+
+ return true;
+ }
+ };
+
+ public static class RealmUrlCheck implements Predicate<JsonWebToken> {
+
+ private static final RealmUrlCheck NULL_INSTANCE = new RealmUrlCheck(null);
+
+ private final String realmUrl;
+
+ public RealmUrlCheck(String realmUrl) {
+ this.realmUrl = realmUrl;
+ }
+
+ @Override
+ public boolean test(JsonWebToken t) throws VerificationException {
+ if (this.realmUrl == null) {
+ throw new VerificationException("Realm URL not set");
+ }
+
+ if (! this.realmUrl.equals(t.getIssuer())) {
+ throw new VerificationException("Invalid token issuer. Expected '" + this.realmUrl + "', but was '" + t.getIssuer() + "'");
+ }
+
+ return true;
+ }
+ };
+
+ public static class TokenTypeCheck implements Predicate<JsonWebToken> {
- private final String tokenString;
+ private static final TokenTypeCheck INSTANCE_BEARER = new TokenTypeCheck(TokenUtil.TOKEN_TYPE_BEARER);
+
+ private final String tokenType;
+
+ public TokenTypeCheck(String tokenType) {
+ this.tokenType = tokenType;
+ }
+
+ @Override
+ public boolean test(JsonWebToken t) throws VerificationException {
+ if (! tokenType.equalsIgnoreCase(t.getType())) {
+ throw new VerificationException("Token type is incorrect. Expected '" + tokenType + "' but was '" + t.getType() + "'");
+ }
+ return true;
+ }
+ };
+
+ private String tokenString;
+ private Class<? extends T> clazz;
private PublicKey publicKey;
private SecretKey secretKey;
private String realmUrl;
+ private String expectedTokenType = TokenUtil.TOKEN_TYPE_BEARER;
private boolean checkTokenType = true;
- private boolean checkActive = true;
private boolean checkRealmUrl = true;
+ private final LinkedList<Predicate<? super T>> checks = new LinkedList<>();
private JWSInput jws;
- private AccessToken token;
+ private T token;
- protected TokenVerifier(String tokenString) {
+ protected TokenVerifier(String tokenString, Class<T> clazz) {
this.tokenString = tokenString;
+ this.clazz = clazz;
+ }
+
+ protected TokenVerifier(T token) {
+ this.token = token;
+ }
+
+ /**
+ * Creates an instance of {@code TokenVerifier} from the given string on a JWT of the given class.
+ * The token verifier has no checks defined. Note that the checks are only tested when
+ * {@link #verify()} method is invoked.
+ * @param <T> Type of the token
+ * @param tokenString String representation of JWT
+ * @param clazz Class of the token
+ * @return
+ */
+ public static <T extends JsonWebToken> TokenVerifier<T> create(String tokenString, Class<T> clazz) {
+ return new TokenVerifier(tokenString, clazz);
+ }
+
+ /**
+ * Creates an instance of {@code TokenVerifier} from the given string on a JWT of the given class.
+ * The token verifier has no checks defined. Note that the checks are only tested when
+ * {@link #verify()} method is invoked.
+ * @return
+ */
+ public static <T extends JsonWebToken> TokenVerifier<T> create(T token) {
+ return new TokenVerifier(token);
+ }
+
+ /**
+ * Adds default checks to the token verification:
+ * <ul>
+ * <li>Realm URL (JWT issuer field: {@code iss}) has to be defined and match realm set via {@link #realmUrl(java.lang.String)} method</li>
+ * <li>Subject (JWT subject field: {@code sub}) has to be defined</li>
+ * <li>Token type (JWT type field: {@code typ}) has to be {@code Bearer}. The type can be set via {@link #tokenType(java.lang.String)} method</li>
+ * <li>Token has to be active, ie. both not expired and not used before its validity (JWT issuer fields: {@code exp} and {@code nbf})</li>
+ * </ul>
+ * @return This token verifier.
+ */
+ public TokenVerifier<T> withDefaultChecks() {
+ return withChecks(
+ RealmUrlCheck.NULL_INSTANCE,
+ SUBJECT_EXISTS_CHECK,
+ TokenTypeCheck.INSTANCE_BEARER,
+ IS_ACTIVE
+ );
}
- public static TokenVerifier create(String tokenString) {
- return new TokenVerifier(tokenString);
+ private void removeCheck(Class<? extends Predicate<?>> checkClass) {
+ for (Iterator<Predicate<? super T>> it = checks.iterator(); it.hasNext();) {
+ if (it.next().getClass() == checkClass) {
+ it.remove();
+ }
+ }
}
- public TokenVerifier publicKey(PublicKey publicKey) {
+ private void removeCheck(Predicate<? super T> check) {
+ checks.remove(check);
+ }
+
+ private <P extends Predicate<? super T>> TokenVerifier<T> replaceCheck(Class<? extends Predicate<?>> checkClass, boolean active, P predicate) {
+ removeCheck(checkClass);
+ if (active) {
+ checks.add(predicate);
+ }
+ return this;
+ }
+
+ private <P extends Predicate<? super T>> TokenVerifier<T> replaceCheck(Predicate<? super T> check, boolean active, P predicate) {
+ removeCheck(check);
+ if (active) {
+ checks.add(predicate);
+ }
+ return this;
+ }
+
+ /**
+ * Will test the given checks in {@link #verify()} method in addition to already set checks.
+ * @param checks
+ * @return
+ */
+ public TokenVerifier<T> withChecks(Predicate<? super T>... checks) {
+ if (checks != null) {
+ this.checks.addAll(Arrays.asList(checks));
+ }
+ return this;
+ }
+
+ /**
+ * Sets the key for verification of RSA-based signature.
+ * @param publicKey
+ * @return
+ */
+ public TokenVerifier<T> publicKey(PublicKey publicKey) {
this.publicKey = publicKey;
return this;
}
- public TokenVerifier secretKey(SecretKey secretKey) {
+ /**
+ * Sets the key for verification of HMAC-based signature.
+ * @param secretKey
+ * @return
+ */
+ public TokenVerifier<T> secretKey(SecretKey secretKey) {
this.secretKey = secretKey;
return this;
}
- public TokenVerifier realmUrl(String realmUrl) {
+ /**
+ * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
+ * @return This token verifier
+ */
+ public TokenVerifier<T> realmUrl(String realmUrl) {
this.realmUrl = realmUrl;
- return this;
+ return replaceCheck(RealmUrlCheck.class, checkRealmUrl, new RealmUrlCheck(realmUrl));
}
- public TokenVerifier checkTokenType(boolean checkTokenType) {
+ /**
+ * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
+ * @return This token verifier
+ */
+ public TokenVerifier<T> checkTokenType(boolean checkTokenType) {
this.checkTokenType = checkTokenType;
- return this;
+ return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
}
- public TokenVerifier checkActive(boolean checkActive) {
- this.checkActive = checkActive;
- return this;
+ /**
+ * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
+ * @return This token verifier
+ */
+ public TokenVerifier<T> tokenType(String tokenType) {
+ this.expectedTokenType = tokenType;
+ return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
+ }
+
+ /**
+ * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
+ * @return This token verifier
+ */
+ public TokenVerifier<T> checkActive(boolean checkActive) {
+ return replaceCheck(IS_ACTIVE, checkActive, IS_ACTIVE);
}
- public TokenVerifier checkRealmUrl(boolean checkRealmUrl) {
+ /**
+ * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
+ * @return This token verifier
+ */
+ public TokenVerifier<T> checkRealmUrl(boolean checkRealmUrl) {
this.checkRealmUrl = checkRealmUrl;
- return this;
+ return replaceCheck(RealmUrlCheck.class, this.checkRealmUrl, new RealmUrlCheck(realmUrl));
}
- public TokenVerifier parse() throws VerificationException {
+ public TokenVerifier<T> parse() throws VerificationException {
if (jws == null) {
if (tokenString == null) {
throw new VerificationException("Token not set");
@@ -100,7 +314,7 @@ public class TokenVerifier {
try {
- token = jws.readJsonContent(AccessToken.class);
+ token = jws.readJsonContent(clazz);
} catch (JWSInputException e) {
throw new VerificationException("Failed to read access token from JWT", e);
}
@@ -108,8 +322,10 @@ public class TokenVerifier {
return this;
}
- public AccessToken getToken() throws VerificationException {
- parse();
+ public T getToken() throws VerificationException {
+ if (token == null) {
+ parse();
+ }
return token;
}
@@ -118,53 +334,97 @@ public class TokenVerifier {
return jws.getHeader();
}
- public TokenVerifier verify() throws VerificationException {
- parse();
-
- if (checkRealmUrl && realmUrl == null) {
- throw new VerificationException("Realm URL not set");
- }
-
+ public void verifySignature() throws VerificationException {
AlgorithmType algorithmType = getHeader().getAlgorithm().getType();
- if (AlgorithmType.RSA.equals(algorithmType)) {
- if (publicKey == null) {
- throw new VerificationException("Public key not set");
- }
-
- if (!RSAProvider.verify(jws, publicKey)) {
- throw new VerificationException("Invalid token signature");
- }
- } else if (AlgorithmType.HMAC.equals(algorithmType)) {
- if (secretKey == null) {
- throw new VerificationException("Secret key not set");
- }
-
- if (!HMACProvider.verify(jws, secretKey)) {
- throw new VerificationException("Invalid token signature");
- }
- } else {
- throw new VerificationException("Unknown or unsupported token algorith");
- }
-
- String user = token.getSubject();
- if (user == null) {
- throw new VerificationException("Subject missing in token");
+ if (null == algorithmType) {
+ throw new VerificationException("Unknown or unsupported token algorithm");
+ } else switch (algorithmType) {
+ case RSA:
+ if (publicKey == null) {
+ throw new VerificationException("Public key not set");
+ }
+ if (!RSAProvider.verify(jws, publicKey)) {
+ throw new TokenSignatureInvalidException(token, "Invalid token signature");
+ } break;
+ case HMAC:
+ if (secretKey == null) {
+ throw new VerificationException("Secret key not set");
+ }
+ if (!HMACProvider.verify(jws, secretKey)) {
+ throw new TokenSignatureInvalidException(token, "Invalid token signature");
+ } break;
+ default:
+ throw new VerificationException("Unknown or unsupported token algorithm");
}
+ }
- if (checkRealmUrl && !realmUrl.equals(token.getIssuer())) {
- throw new VerificationException("Invalid token issuer. Expected '" + realmUrl + "', but was '" + token.getIssuer() + "'");
+ public TokenVerifier<T> verify() throws VerificationException {
+ if (getToken() == null) {
+ parse();
}
-
- if (checkTokenType && !TokenUtil.TOKEN_TYPE_BEARER.equalsIgnoreCase(token.getType())) {
- throw new VerificationException("Token type is incorrect. Expected '" + TokenUtil.TOKEN_TYPE_BEARER + "' but was '" + token.getType() + "'");
+ if (jws != null) {
+ verifySignature();
}
- if (checkActive && !token.isActive()) {
- throw new VerificationException("Token is not active");
+ for (Predicate<? super T> check : checks) {
+ if (! check.test(getToken())) {
+ throw new VerificationException("JWT check failed for check " + check);
+ }
}
return this;
}
+ /**
+ * Creates an optional predicate from a predicate that will proceed with check but always pass.
+ * @param <T>
+ * @param mandatoryPredicate
+ * @return
+ */
+ public static <T extends JsonWebToken> Predicate<T> optional(final Predicate<T> mandatoryPredicate) {
+ return new Predicate<T>() {
+ @Override
+ public boolean test(T t) throws VerificationException {
+ try {
+ if (! mandatoryPredicate.test(t)) {
+ LOG.finer("[optional] predicate failed: " + mandatoryPredicate);
+ }
+
+ return true;
+ } catch (VerificationException ex) {
+ LOG.log(Level.FINER, "[optional] predicate " + mandatoryPredicate + " failed.", ex);
+ return true;
+ }
+ }
+ };
+ }
+
+ /**
+ * Creates a predicate that will proceed with checks of the given predicates
+ * and will pass if and only if at least one of the given predicates passes.
+ * @param <T>
+ * @param predicates
+ * @return
+ */
+ public static <T extends JsonWebToken> Predicate<T> alternative(final Predicate<? super T>... predicates) {
+ return new Predicate<T>() {
+ @Override
+ public boolean test(T t) throws VerificationException {
+ for (Predicate<? super T> predicate : predicates) {
+ try {
+ if (predicate.test(t)) {
+ return true;
+ }
+
+ LOG.finer("[alternative] predicate failed: " + predicate);
+ } catch (VerificationException ex) {
+ LOG.log(Level.FINER, "[alternative] predicate " + predicate + " failed.", ex);
+ }
+ }
+
+ return false;
+ }
+ };
+ }
}
diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl
index 882d1b1..d78ff75 100755
--- a/distribution/demo-dist/src/main/xslt/standalone.xsl
+++ b/distribution/demo-dist/src/main/xslt/standalone.xsl
@@ -89,9 +89,11 @@
<eviction max-entries="10000" strategy="LRU"/>
</local-cache>
<local-cache name="sessions"/>
+ <local-cache name="authenticationSessions"/>
<local-cache name="offlineSessions"/>
<local-cache name="loginFailures"/>
<local-cache name="authorization"/>
+ <local-cache name="actionTokens"/>
<local-cache name="work"/>
<local-cache name="keys">
<eviction max-entries="1000" strategy="LRU"/>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml
index e7fdb8a..4388f83 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml
@@ -33,6 +33,7 @@
<module name="org.infinispan.commons"/>
<module name="org.infinispan.cachestore.remote"/>
<module name="org.infinispan.client.hotrod"/>
+ <module name="org.jgroups"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>
diff --git a/distribution/server-overlay/src/main/cli/keycloak-install-base.cli b/distribution/server-overlay/src/main/cli/keycloak-install-base.cli
index f24502a..5ce3122 100644
--- a/distribution/server-overlay/src/main/cli/keycloak-install-base.cli
+++ b/distribution/server-overlay/src/main/cli/keycloak-install-base.cli
@@ -6,6 +6,7 @@ embed-server --server-config=standalone.xml
/subsystem=infinispan/cache-container=keycloak/local-cache=users:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/local-cache=sessions:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=authenticationSessions:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=offlineSessions:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=loginFailures:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=work:add()
@@ -14,4 +15,7 @@ embed-server --server-config=standalone.xml
/subsystem=infinispan/cache-container=keycloak/local-cache=keys:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=keys/eviction=EVICTION:add(max-entries=1000,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/local-cache=keys/expiration=EXPIRATION:add(max-idle=3600000)
+/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/eviction=EVICTION:add(max-entries=-1,strategy=NONE)
+/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/expiration=EXPIRATION:add(max-idle=-1,interval=300000)
/extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem)
diff --git a/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli b/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli
index ec2b56f..4710eb8 100644
--- a/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli
+++ b/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli
@@ -7,6 +7,7 @@ embed-server --server-config=standalone-ha.xml
/subsystem=infinispan/cache-container=keycloak/local-cache=users:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1")
+/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:add(mode="SYNC",owners="1")
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add(mode="SYNC",owners="1")
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1")
/subsystem=infinispan/cache-container=keycloak/local-cache=authorization:add()
@@ -15,4 +16,7 @@ embed-server --server-config=standalone-ha.xml
/subsystem=infinispan/cache-container=keycloak/local-cache=keys:add()
/subsystem=infinispan/cache-container=keycloak/local-cache=keys/eviction=EVICTION:add(max-entries=1000,strategy=LRU)
/subsystem=infinispan/cache-container=keycloak/local-cache=keys/expiration=EXPIRATION:add(max-idle=3600000)
+/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/eviction=EVICTION:add(max-entries=-1,strategy=NONE)
+/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/expiration=EXPIRATION:add(max-idle=-1,interval=300000)
/extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem)
examples/kerberos/README.md 2(+1 -1)
diff --git a/examples/kerberos/README.md b/examples/kerberos/README.md
index 02bffdd..2c1d335 100644
--- a/examples/kerberos/README.md
+++ b/examples/kerberos/README.md
@@ -47,7 +47,7 @@ is in your `/etc/hosts` before other records for the 127.0.0.1 host to avoid iss
**5)** Configure Kerberos client (On linux it's in file `/etc/krb5.conf` ). You need to configure `KEYCLOAK.ORG` realm for host `localhost` and enable `forwardable` flag, which is needed
for credential delegation example, as application needs to forward Kerberos ticket and authenticate with it against LDAP server.
-See [this file](https://github.com/keycloak/keycloak/blob/master/testsuite/integration/src/test/resources/kerberos/test-krb5.conf) for inspiration.
+See [this file](https://github.com/keycloak/keycloak/blob/master/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/test-krb5.conf) for inspiration.
On OS X the file to edit (or create) is `/Library/Preferences/edu.mit.Kerberos` with the same syntax as `krb5.conf`.
On Windows the file to edit (or create) is `c:\Windows\krb5.ini` with the same syntax as `krb5.conf`.
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientPoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientPoliciesResource.java
index 3fd7778..ef09dde 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientPoliciesResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientPoliciesResource.java
@@ -27,6 +27,7 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
/**
misc/Testsuite.md 6(+6 -0)
diff --git a/misc/Testsuite.md b/misc/Testsuite.md
index 8403806..cb77ad7 100644
--- a/misc/Testsuite.md
+++ b/misc/Testsuite.md
@@ -132,6 +132,12 @@ kinit hnelson@KEYCLOAK.ORG
and provide password `secret`
Now when you access `http://localhost:8081/auth/realms/master/account` you should be logged in automatically as user `hnelson` .
+
+Simple loadbalancer
+-------------------
+
+You can run class `SimpleUndertowLoadBalancer` from IDE. By default, it executes the embedded undertow loadbalancer running on `http://localhost:8180`, which communicates with 2 backend Keycloak nodes
+running on `http://localhost:8181` and `http://localhost:8182` . See javadoc for more details.
Create many users or offline sessions
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index 916db65..1394d80 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -19,6 +19,8 @@ package org.keycloak.connections.infinispan;
import java.util.concurrent.TimeUnit;
+import org.infinispan.commons.util.FileLookup;
+import org.infinispan.commons.util.FileLookupFactory;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
@@ -27,12 +29,13 @@ import org.infinispan.eviction.EvictionStrategy;
import org.infinispan.eviction.EvictionType;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
-import org.infinispan.persistence.remote.configuration.ExhaustedAction;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
+import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.TransactionMode;
import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
import org.jboss.logging.Logger;
+import org.jgroups.JChannel;
import org.keycloak.Config;
import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
import org.keycloak.models.KeycloakSession;
@@ -119,7 +122,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(userRevisionsMaxEntries));
cacheManager.getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, true);
cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME, true);
+ cacheManager.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, true);
cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true);
+ cacheManager.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE, true);
logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup);
} catch (Exception e) {
@@ -138,7 +143,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true);
if (clustered) {
- gcb.transport().defaultTransport();
+ String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
+ configureTransport(gcb, nodeName);
}
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
@@ -181,6 +187,14 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME, sessionCacheConfiguration);
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfiguration);
+
+ // Retrieve caches to enforce rebalance
+ cacheManager.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME, true);
+ cacheManager.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true);
+ cacheManager.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, true);
+ cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME, true);
+ cacheManager.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, true);
ConfigurationBuilder replicationConfigBuilder = new ConfigurationBuilder();
if (clustered) {
@@ -219,6 +233,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, getKeysCacheConfig());
cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true);
+
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.ACTION_TOKEN_CACHE, getActionTokenCacheConfig());
+ cacheManager.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE, true);
}
private Configuration getRevisionCacheConfig(long maxEntries) {
@@ -269,4 +286,40 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
return cb.build();
}
+ private Configuration getActionTokenCacheConfig() {
+ ConfigurationBuilder cb = new ConfigurationBuilder();
+
+ cb.eviction()
+ .strategy(EvictionStrategy.NONE)
+ .type(EvictionType.COUNT)
+ .size(InfinispanConnectionProvider.ACTION_TOKEN_CACHE_DEFAULT_MAX);
+ cb.expiration()
+ .maxIdle(InfinispanConnectionProvider.ACTION_TOKEN_MAX_IDLE_SECONDS, TimeUnit.SECONDS)
+ .wakeUpInterval(InfinispanConnectionProvider.ACTION_TOKEN_WAKE_UP_INTERVAL_SECONDS, TimeUnit.SECONDS);
+
+ return cb.build();
+ }
+
+ protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName) {
+ if (nodeName == null) {
+ gcb.transport().defaultTransport();
+ } else {
+ FileLookup fileLookup = FileLookupFactory.newInstance();
+
+ try {
+ // Compatibility with Wildfly
+ JChannel channel = new JChannel(fileLookup.lookupFileLocation("default-configs/default-jgroups-udp.xml", this.getClass().getClassLoader()));
+ channel.setName(nodeName);
+ JGroupsTransport transport = new JGroupsTransport(channel);
+
+ gcb.transport().nodeName(nodeName);
+ gcb.transport().transport(transport);
+
+ logger.infof("Configured jgroups transport with the channel name: %s", nodeName);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index 7c255fd..8e190cd 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -36,13 +36,22 @@ public interface InfinispanConnectionProvider extends Provider {
String SESSION_CACHE_NAME = "sessions";
String OFFLINE_SESSION_CACHE_NAME = "offlineSessions";
String LOGIN_FAILURE_CACHE_NAME = "loginFailures";
+ String AUTHENTICATION_SESSIONS_CACHE_NAME = "authenticationSessions";
String WORK_CACHE_NAME = "work";
String AUTHORIZATION_CACHE_NAME = "authorization";
+ String ACTION_TOKEN_CACHE = "actionTokens";
+ int ACTION_TOKEN_CACHE_DEFAULT_MAX = -1;
+ int ACTION_TOKEN_MAX_IDLE_SECONDS = -1;
+ long ACTION_TOKEN_WAKE_UP_INTERVAL_SECONDS = 5 * 60 * 1000l;
+
String KEYS_CACHE_NAME = "keys";
int KEYS_CACHE_DEFAULT_MAX = 1000;
int KEYS_CACHE_MAX_IDLE_SECONDS = 3600;
+ // System property used on Wildfly to identify distributedCache address and sticky session route
+ String JBOSS_NODE_NAME = "jboss.node.name";
+
<K, V> Cache<K, V> getCache(String name);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/AddInvalidatedActionTokenEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/AddInvalidatedActionTokenEvent.java
new file mode 100644
index 0000000..37a1a21
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/AddInvalidatedActionTokenEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.cache.infinispan;
+
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey;
+import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
+
+/**
+ * Event requesting adding of an invalidated action token.
+ */
+public class AddInvalidatedActionTokenEvent implements ClusterEvent {
+
+ private final ActionTokenReducedKey key;
+ private final int expirationInSecs;
+ private final ActionTokenValueEntity tokenValue;
+
+ public AddInvalidatedActionTokenEvent(ActionTokenReducedKey key, int expirationInSecs, ActionTokenValueEntity tokenValue) {
+ this.key = key;
+ this.expirationInSecs = expirationInSecs;
+ this.tokenValue = tokenValue;
+ }
+
+ public ActionTokenReducedKey getKey() {
+ return key;
+ }
+
+ public int getExpirationInSecs() {
+ return expirationInSecs;
+ }
+
+ public ActionTokenValueEntity getTokenValue() {
+ return tokenValue;
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index fef0486..3668d97 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
@@ -83,6 +83,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected int accessCodeLifespan;
protected int accessCodeLifespanUserAction;
protected int accessCodeLifespanLogin;
+ protected int actionTokenGeneratedByAdminLifespan;
+ protected int actionTokenGeneratedByUserLifespan;
protected int notBefore;
protected PasswordPolicy passwordPolicy;
protected OTPPolicy otpPolicy;
@@ -175,6 +177,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
accessCodeLifespan = model.getAccessCodeLifespan();
accessCodeLifespanUserAction = model.getAccessCodeLifespanUserAction();
accessCodeLifespanLogin = model.getAccessCodeLifespanLogin();
+ actionTokenGeneratedByAdminLifespan = model.getActionTokenGeneratedByAdminLifespan();
+ actionTokenGeneratedByUserLifespan = model.getActionTokenGeneratedByUserLifespan();
notBefore = model.getNotBefore();
passwordPolicy = model.getPasswordPolicy();
otpPolicy = model.getOTPPolicy();
@@ -399,6 +403,14 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return accessCodeLifespanLogin;
}
+ public int getActionTokenGeneratedByAdminLifespan() {
+ return actionTokenGeneratedByAdminLifespan;
+ }
+
+ public int getActionTokenGeneratedByUserLifespan() {
+ return actionTokenGeneratedByUserLifespan;
+ }
+
public List<RequiredCredentialModel> getRequiredCredentials() {
return requiredCredentials;
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java
new file mode 100644
index 0000000..d7bdcdf
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.cache.infinispan.events;
+
+import org.keycloak.cluster.ClusterEvent;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
+
+ private String authSessionId;
+
+ private Map<String, String> authNotesFragment;
+
+ public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, Map<String, String> authNotesFragment) {
+ AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent();
+ event.authSessionId = authSessionId;
+ event.authNotesFragment = new LinkedHashMap<>(authNotesFragment);
+ return event;
+ }
+
+ public String getAuthSessionId() {
+ return authSessionId;
+ }
+
+ public Map<String, String> getAuthNotesFragment() {
+ return authNotesFragment;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, authNotesFragment=%s ]", authSessionId, authNotesFragment);
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 8350f0d..0bed826 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -475,6 +475,30 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
+ public int getActionTokenGeneratedByAdminLifespan() {
+ if (isUpdated()) return updated.getActionTokenGeneratedByAdminLifespan();
+ return cached.getActionTokenGeneratedByAdminLifespan();
+ }
+
+ @Override
+ public void setActionTokenGeneratedByAdminLifespan(int seconds) {
+ getDelegateForUpdate();
+ updated.setActionTokenGeneratedByAdminLifespan(seconds);
+ }
+
+ @Override
+ public int getActionTokenGeneratedByUserLifespan() {
+ if (isUpdated()) return updated.getActionTokenGeneratedByUserLifespan();
+ return cached.getActionTokenGeneratedByUserLifespan();
+ }
+
+ @Override
+ public void setActionTokenGeneratedByUserLifespan(int seconds) {
+ getDelegateForUpdate();
+ updated.setActionTokenGeneratedByUserLifespan(seconds);
+ }
+
+ @Override
public List<RequiredCredentialModel> getRequiredCredentials() {
if (isUpdated()) return updated.getRequiredCredentials();
return cached.getRequiredCredentials();
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RemoveActionTokensSpecificEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RemoveActionTokensSpecificEvent.java
new file mode 100644
index 0000000..0a4d858
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RemoveActionTokensSpecificEvent.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.cache.infinispan;
+
+import org.keycloak.cluster.ClusterEvent;
+
+/**
+ * Event requesting removal of the action tokens with the given user and action regardless of nonce.
+ */
+public class RemoveActionTokensSpecificEvent implements ClusterEvent {
+
+ private final String userId;
+ private final String actionId;
+
+ public RemoveActionTokensSpecificEvent(String userId, String actionId) {
+ this.userId = userId;
+ this.actionId = actionId;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public String getActionId() {
+ return actionId;
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java
new file mode 100644
index 0000000..13352df
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.infinispan.Cache;
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSessionModel {
+
+ private final AuthenticatedClientSessionEntity entity;
+ private final ClientModel client;
+ private final InfinispanUserSessionProvider provider;
+ private final Cache<String, SessionEntity> cache;
+ private UserSessionAdapter userSession;
+
+ public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache) {
+ this.provider = provider;
+ this.entity = entity;
+ this.client = client;
+ this.cache = cache;
+ this.userSession = userSession;
+ }
+
+ private void update() {
+ provider.getTx().replace(cache, userSession.getEntity().getId(), userSession.getEntity());
+ }
+
+
+ @Override
+ public void setUserSession(UserSessionModel userSession) {
+ String clientUUID = client.getId();
+ UserSessionEntity sessionEntity = this.userSession.getEntity();
+
+ // Dettach userSession
+ if (userSession == null) {
+ if (sessionEntity.getAuthenticatedClientSessions() != null) {
+ sessionEntity.getAuthenticatedClientSessions().remove(clientUUID);
+ update();
+ this.userSession = null;
+ }
+ } else {
+ this.userSession = (UserSessionAdapter) userSession;
+
+ if (sessionEntity.getAuthenticatedClientSessions() == null) {
+ sessionEntity.setAuthenticatedClientSessions(new HashMap<>());
+ }
+ sessionEntity.getAuthenticatedClientSessions().put(clientUUID, entity);
+ update();
+ }
+ }
+
+ @Override
+ public UserSessionModel getUserSession() {
+ return this.userSession;
+ }
+
+ @Override
+ public String getRedirectUri() {
+ return entity.getRedirectUri();
+ }
+
+ @Override
+ public void setRedirectUri(String uri) {
+ entity.setRedirectUri(uri);
+ update();
+ }
+
+ @Override
+ public String getId() {
+ return null;
+ }
+
+ @Override
+ public RealmModel getRealm() {
+ return userSession.getRealm();
+ }
+
+ @Override
+ public ClientModel getClient() {
+ return client;
+ }
+
+ @Override
+ public int getTimestamp() {
+ return entity.getTimestamp();
+ }
+
+ @Override
+ public void setTimestamp(int timestamp) {
+ entity.setTimestamp(timestamp);
+ update();
+ }
+
+ @Override
+ public String getAction() {
+ return entity.getAction();
+ }
+
+ @Override
+ public void setAction(String action) {
+ entity.setAction(action);
+ update();
+ }
+
+ @Override
+ public String getProtocol() {
+ return entity.getAuthMethod();
+ }
+
+ @Override
+ public void setProtocol(String method) {
+ entity.setAuthMethod(method);
+ update();
+ }
+
+ @Override
+ public Set<String> getRoles() {
+ return entity.getRoles();
+ }
+
+ @Override
+ public void setRoles(Set<String> roles) {
+ entity.setRoles(roles);
+ update();
+ }
+
+ @Override
+ public Set<String> getProtocolMappers() {
+ return entity.getProtocolMappers();
+ }
+
+ @Override
+ public void setProtocolMappers(Set<String> protocolMappers) {
+ entity.setProtocolMappers(protocolMappers);
+ update();
+ }
+
+ @Override
+ public String getNote(String name) {
+ return entity.getNotes()==null ? null : entity.getNotes().get(name);
+ }
+
+ @Override
+ public void setNote(String name, String value) {
+ if (entity.getNotes() == null) {
+ entity.setNotes(new HashMap<>());
+ }
+ entity.getNotes().put(name, value);
+ update();
+ }
+
+ @Override
+ public void removeNote(String name) {
+ if (entity.getNotes() != null) {
+ entity.getNotes().remove(name);
+ update();
+ }
+ }
+
+ @Override
+ public Map<String, String> getNotes() {
+ if (entity.getNotes() == null || entity.getNotes().isEmpty()) return Collections.emptyMap();
+ Map<String, String> copy = new HashMap<>();
+ copy.putAll(entity.getNotes());
+ return copy;
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java
index e55cf31..19cb7c7 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java
@@ -19,11 +19,9 @@ package org.keycloak.models.sessions.infinispan;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
-import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
-import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -41,21 +39,6 @@ public class Consumers {
return new UserSessionModelsConsumer(provider, realm, offline);
}
- public static class UserSessionIdAndTimestampConsumer implements Consumer<Map.Entry<String, SessionEntity>> {
-
- private Map<String, Integer> sessions = new HashMap<>();
-
- @Override
- public void accept(Map.Entry<String, SessionEntity> entry) {
- SessionEntity e = entry.getValue();
- if (e instanceof ClientSessionEntity) {
- ClientSessionEntity ce = (ClientSessionEntity) e;
- sessions.put(ce.getUserSession(), ce.getTimestamp());
- }
- }
-
- }
-
public static class UserSessionModelsConsumer implements Consumer<Map.Entry<String, SessionEntity>> {
private InfinispanUserSessionProvider provider;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java
new file mode 100644
index 0000000..173c434
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.sessions.infinispan.entities;
+
+import java.io.*;
+import java.util.Objects;
+import java.util.UUID;
+import org.infinispan.commons.marshall.Externalizer;
+import org.infinispan.commons.marshall.SerializeWith;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@SerializeWith(value = ActionTokenReducedKey.ExternalizerImpl.class)
+public class ActionTokenReducedKey implements Serializable {
+
+ private final String userId;
+ private final String actionId;
+
+ /**
+ * Nonce that must match.
+ */
+ private final UUID actionVerificationNonce;
+
+ public ActionTokenReducedKey(String userId, String actionId, UUID actionVerificationNonce) {
+ this.userId = userId;
+ this.actionId = actionId;
+ this.actionVerificationNonce = actionVerificationNonce;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public String getActionId() {
+ return actionId;
+ }
+
+ public UUID getActionVerificationNonce() {
+ return actionVerificationNonce;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 71 * hash + Objects.hashCode(this.userId);
+ hash = 71 * hash + Objects.hashCode(this.actionId);
+ hash = 71 * hash + Objects.hashCode(this.actionVerificationNonce);
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final ActionTokenReducedKey other = (ActionTokenReducedKey) obj;
+ return Objects.equals(this.userId, other.getUserId())
+ && Objects.equals(this.actionId, other.getActionId())
+ && Objects.equals(this.actionVerificationNonce, other.getActionVerificationNonce());
+ }
+
+ public static class ExternalizerImpl implements Externalizer<ActionTokenReducedKey> {
+
+ @Override
+ public void writeObject(ObjectOutput output, ActionTokenReducedKey t) throws IOException {
+ output.writeUTF(t.userId);
+ output.writeUTF(t.actionId);
+ output.writeLong(t.actionVerificationNonce.getMostSignificantBits());
+ output.writeLong(t.actionVerificationNonce.getLeastSignificantBits());
+ }
+
+ @Override
+ public ActionTokenReducedKey readObject(ObjectInput input) throws IOException, ClassNotFoundException {
+ return new ActionTokenReducedKey(
+ input.readUTF(),
+ input.readUTF(),
+ new UUID(input.readLong(), input.readLong())
+ );
+ }
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java
new file mode 100644
index 0000000..7c0f663
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.sessions.infinispan.entities;
+
+import org.keycloak.models.ActionTokenValueModel;
+
+import java.io.*;
+import java.util.*;
+import org.infinispan.commons.marshall.Externalizer;
+import org.infinispan.commons.marshall.SerializeWith;
+
+/**
+ * @author hmlnarik
+ */
+@SerializeWith(ActionTokenValueEntity.ExternalizerImpl.class)
+public class ActionTokenValueEntity implements ActionTokenValueModel {
+
+ private final Map<String, String> notes;
+
+ public ActionTokenValueEntity(Map<String, String> notes) {
+ this.notes = notes == null ? Collections.EMPTY_MAP : new HashMap<>(notes);
+ }
+
+ @Override
+ public Map<String, String> getNotes() {
+ return Collections.unmodifiableMap(notes);
+ }
+
+ @Override
+ public String getNote(String name) {
+ return notes.get(name);
+ }
+
+ public static class ExternalizerImpl implements Externalizer<ActionTokenValueEntity> {
+
+ private static final int VERSION_1 = 1;
+
+ @Override
+ public void writeObject(ObjectOutput output, ActionTokenValueEntity t) throws IOException {
+ output.writeByte(VERSION_1);
+
+ output.writeBoolean(! t.notes.isEmpty());
+ if (! t.notes.isEmpty()) {
+ output.writeObject(t.notes);
+ }
+ }
+
+ @Override
+ public ActionTokenValueEntity readObject(ObjectInput input) throws IOException, ClassNotFoundException {
+ byte version = input.readByte();
+
+ if (version != VERSION_1) {
+ throw new IOException("Invalid version: " + version);
+ }
+ boolean notesEmpty = input.readBoolean();
+
+ Map<String, String> notes = notesEmpty ? Collections.EMPTY_MAP : (Map<String, String>) input.readObject();
+
+ return new ActionTokenValueEntity(notes);
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java
new file mode 100644
index 0000000..0b99225
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan.entities;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AuthenticationSessionEntity extends SessionEntity {
+
+ private String clientUuid;
+ private String authUserId;
+
+ private String redirectUri;
+ private int timestamp;
+ private String action;
+ private Set<String> roles;
+ private Set<String> protocolMappers;
+
+ private Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus = new HashMap<>();;
+ private String protocol;
+
+ private Map<String, String> clientNotes;
+ private Map<String, String> authNotes;
+ private Set<String> requiredActions = new HashSet<>();
+ private Map<String, String> userSessionNotes;
+
+ public String getClientUuid() {
+ return clientUuid;
+ }
+
+ public void setClientUuid(String clientUuid) {
+ this.clientUuid = clientUuid;
+ }
+
+ public String getAuthUserId() {
+ return authUserId;
+ }
+
+ public void setAuthUserId(String authUserId) {
+ this.authUserId = authUserId;
+ }
+
+ public String getRedirectUri() {
+ return redirectUri;
+ }
+
+ public void setRedirectUri(String redirectUri) {
+ this.redirectUri = redirectUri;
+ }
+
+ public int getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(int timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public Set<String> getRoles() {
+ return roles;
+ }
+
+ public void setRoles(Set<String> roles) {
+ this.roles = roles;
+ }
+
+ public Set<String> getProtocolMappers() {
+ return protocolMappers;
+ }
+
+ public void setProtocolMappers(Set<String> protocolMappers) {
+ this.protocolMappers = protocolMappers;
+ }
+
+ public Map<String, AuthenticationSessionModel.ExecutionStatus> getExecutionStatus() {
+ return executionStatus;
+ }
+
+ public void setExecutionStatus(Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus) {
+ this.executionStatus = executionStatus;
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public void setProtocol(String protocol) {
+ this.protocol = protocol;
+ }
+
+ public Map<String, String> getClientNotes() {
+ return clientNotes;
+ }
+
+ public void setClientNotes(Map<String, String> clientNotes) {
+ this.clientNotes = clientNotes;
+ }
+
+ public Set<String> getRequiredActions() {
+ return requiredActions;
+ }
+
+ public void setRequiredActions(Set<String> requiredActions) {
+ this.requiredActions = requiredActions;
+ }
+
+ public Map<String, String> getUserSessionNotes() {
+ return userSessionNotes;
+ }
+
+ public void setUserSessionNotes(Map<String, String> userSessionNotes) {
+ this.userSessionNotes = userSessionNotes;
+ }
+
+ public Map<String, String> getAuthNotes() {
+ return authNotes;
+ }
+
+ public void setAuthNotes(Map<String, String> authNotes) {
+ this.authNotes = authNotes;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
index 538babf..54d182f 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
@@ -46,12 +46,12 @@ public class UserSessionEntity extends SessionEntity {
private int lastSessionRefresh;
- private Set<String> clientSessions = new CopyOnWriteArraySet<>();
-
private UserSessionModel.State state;
private Map<String, String> notes = new ConcurrentHashMap<>();
+ private Map<String, AuthenticatedClientSessionEntity> authenticatedClientSessions;
+
public String getUser() {
return user;
}
@@ -108,10 +108,6 @@ public class UserSessionEntity extends SessionEntity {
this.lastSessionRefresh = lastSessionRefresh;
}
- public Set<String> getClientSessions() {
- return clientSessions;
- }
-
public Map<String, String> getNotes() {
return notes;
}
@@ -120,6 +116,14 @@ public class UserSessionEntity extends SessionEntity {
this.notes = notes;
}
+ public Map<String, AuthenticatedClientSessionEntity> getAuthenticatedClientSessions() {
+ return authenticatedClientSessions;
+ }
+
+ public void setAuthenticatedClientSessions(Map<String, AuthenticatedClientSessionEntity> authenticatedClientSessions) {
+ this.authenticatedClientSessions = authenticatedClientSessions;
+ }
+
public UserSessionModel.State getState() {
return state;
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java
new file mode 100644
index 0000000..127879a
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.sessions.infinispan;
+
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.models.*;
+
+import org.keycloak.models.cache.infinispan.AddInvalidatedActionTokenEvent;
+import org.keycloak.models.cache.infinispan.RemoveActionTokensSpecificEvent;
+import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
+import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey;
+import java.util.*;
+import org.infinispan.Cache;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvider {
+
+ private final Cache<ActionTokenReducedKey, ActionTokenValueEntity> actionKeyCache;
+ private final InfinispanKeycloakTransaction tx;
+ private final KeycloakSession session;
+
+ public InfinispanActionTokenStoreProvider(KeycloakSession session, Cache<ActionTokenReducedKey, ActionTokenValueEntity> actionKeyCache) {
+ this.session = session;
+ this.actionKeyCache = actionKeyCache;
+ this.tx = new InfinispanKeycloakTransaction();
+
+ session.getTransactionManager().enlistAfterCompletion(tx);
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public void put(ActionTokenKeyModel key, Map<String, String> notes) {
+ if (key == null || key.getUserId() == null || key.getActionId() == null) {
+ return;
+ }
+
+ ActionTokenReducedKey tokenKey = new ActionTokenReducedKey(key.getUserId(), key.getActionId(), key.getActionVerificationNonce());
+ ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(notes);
+
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+ this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new AddInvalidatedActionTokenEvent(tokenKey, key.getExpiration(), tokenValue), false);
+ }
+
+ @Override
+ public ActionTokenValueModel get(ActionTokenKeyModel actionTokenKey) {
+ if (actionTokenKey == null || actionTokenKey.getUserId() == null || actionTokenKey.getActionId() == null) {
+ return null;
+ }
+
+ ActionTokenReducedKey key = new ActionTokenReducedKey(actionTokenKey.getUserId(), actionTokenKey.getActionId(), actionTokenKey.getActionVerificationNonce());
+ return this.actionKeyCache.getAdvancedCache().get(key);
+ }
+
+ @Override
+ public ActionTokenValueModel remove(ActionTokenKeyModel actionTokenKey) {
+ if (actionTokenKey == null || actionTokenKey.getUserId() == null || actionTokenKey.getActionId() == null) {
+ return null;
+ }
+
+ ActionTokenReducedKey key = new ActionTokenReducedKey(actionTokenKey.getUserId(), actionTokenKey.getActionId(), actionTokenKey.getActionVerificationNonce());
+ ActionTokenValueEntity value = this.actionKeyCache.get(key);
+
+ if (value != null) {
+ this.tx.remove(actionKeyCache, key);
+ }
+
+ return value;
+ }
+
+ public void removeAll(String userId, String actionId) {
+ if (userId == null || actionId == null) {
+ return;
+ }
+
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+ this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new RemoveActionTokensSpecificEvent(userId, actionId), false);
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java
new file mode 100644
index 0000000..a8c5e38
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.sessions.infinispan;
+
+import org.keycloak.Config;
+import org.keycloak.Config.Scope;
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.common.util.Time;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.*;
+
+import org.keycloak.models.cache.infinispan.AddInvalidatedActionTokenEvent;
+import org.keycloak.models.cache.infinispan.RemoveActionTokensSpecificEvent;
+import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
+import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import org.infinispan.Cache;
+import org.infinispan.context.Flag;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class InfinispanActionTokenStoreProviderFactory implements ActionTokenStoreProviderFactory {
+
+ public static final String ACTION_TOKEN_EVENTS = "ACTION_TOKEN_EVENTS";
+
+ /**
+ * If expiration is set to this value, no expiration is set on the corresponding cache entry (hence cache default is honored)
+ */
+ private static final int DEFAULT_CACHE_EXPIRATION = 0;
+
+ private Config.Scope config;
+
+ @Override
+ public ActionTokenStoreProvider create(KeycloakSession session) {
+ InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
+ Cache<ActionTokenReducedKey, ActionTokenValueEntity> actionTokenCache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE);
+
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+
+ cluster.registerListener(ACTION_TOKEN_EVENTS, event -> {
+ if (event instanceof RemoveActionTokensSpecificEvent) {
+ RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event;
+
+ actionTokenCache
+ .getAdvancedCache()
+ .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD)
+ .keySet()
+ .stream()
+ .filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId()))
+ .forEach(actionTokenCache::remove);
+ } else if (event instanceof AddInvalidatedActionTokenEvent) {
+ AddInvalidatedActionTokenEvent e = (AddInvalidatedActionTokenEvent) event;
+
+ if (e.getExpirationInSecs() == DEFAULT_CACHE_EXPIRATION) {
+ actionTokenCache.put(e.getKey(), e.getTokenValue());
+ } else {
+ actionTokenCache.put(e.getKey(), e.getTokenValue(), e.getExpirationInSecs() - Time.currentTime(), TimeUnit.SECONDS);
+ }
+ }
+ });
+
+ return new InfinispanActionTokenStoreProvider(session, actionTokenCache);
+ }
+
+ @Override
+ public void init(Scope config) {
+ this.config = config;
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return "infinispan";
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
new file mode 100644
index 0000000..5991f98
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan;
+
+import org.keycloak.cluster.ClusterProvider;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.infinispan.Cache;
+import org.infinispan.context.Flag;
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
+import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.RealmInfoUtil;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.AuthenticationSessionProvider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanAuthenticationSessionProvider implements AuthenticationSessionProvider {
+
+ private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProvider.class);
+
+ private final KeycloakSession session;
+ private final Cache<String, AuthenticationSessionEntity> cache;
+ protected final InfinispanKeycloakTransaction tx;
+
+ public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache<String, AuthenticationSessionEntity> cache) {
+ this.session = session;
+ this.cache = cache;
+
+ this.tx = new InfinispanKeycloakTransaction();
+ session.getTransactionManager().enlistAfterCompletion(tx);
+ }
+
+ @Override
+ public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client) {
+ String id = KeycloakModelUtils.generateId();
+ return createAuthenticationSession(id, realm, client);
+ }
+
+ @Override
+ public AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client) {
+ AuthenticationSessionEntity entity = new AuthenticationSessionEntity();
+ entity.setId(id);
+ entity.setRealm(realm.getId());
+ entity.setTimestamp(Time.currentTime());
+ entity.setClientUuid(client.getId());
+
+ tx.put(cache, id, entity);
+
+ AuthenticationSessionAdapter wrap = wrap(realm, entity);
+ return wrap;
+ }
+
+ private AuthenticationSessionAdapter wrap(RealmModel realm, AuthenticationSessionEntity entity) {
+ return entity==null ? null : new AuthenticationSessionAdapter(session, this, cache, realm, entity);
+ }
+
+ @Override
+ public AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId) {
+ AuthenticationSessionEntity entity = getAuthenticationSessionEntity(realm, authenticationSessionId);
+ return wrap(realm, entity);
+ }
+
+ private AuthenticationSessionEntity getAuthenticationSessionEntity(RealmModel realm, String authSessionId) {
+ // Chance created in this transaction
+ AuthenticationSessionEntity entity = tx.get(cache, authSessionId);
+
+ if (entity == null) {
+ entity = cache.get(authSessionId);
+ }
+
+ return entity;
+ }
+
+ @Override
+ public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authenticationSession) {
+ tx.remove(cache, authenticationSession.getId());
+ }
+
+ @Override
+ public void removeExpired(RealmModel realm) {
+ log.debugf("Removing expired sessions");
+
+ int expired = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
+
+
+ // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
+ Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
+ .entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired)).iterator();
+
+ int counter = 0;
+ while (itr.hasNext()) {
+ counter++;
+ AuthenticationSessionEntity entity = itr.next().getValue();
+ tx.remove(cache, entity.getId());
+ }
+
+ log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName());
+ }
+
+ // TODO: Should likely listen to "RealmRemovedEvent" received from cluster and clean just local sessions
+ @Override
+ public void onRealmRemoved(RealmModel realm) {
+ Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = cache.entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId())).iterator();
+ while (itr.hasNext()) {
+ cache.remove(itr.next().getKey());
+ }
+ }
+
+ // TODO: Should likely listen to "ClientRemovedEvent" received from cluster and clean just local sessions
+ @Override
+ public void onClientRemoved(RealmModel realm, ClientModel client) {
+ Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = cache.entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId()).client(client.getId())).iterator();
+ while (itr.hasNext()) {
+ cache.remove(itr.next().getKey());
+ }
+ }
+
+ @Override
+ public void updateNonlocalSessionAuthNotes(String authSessionId, Map<String, String> authNotesFragment) {
+ if (authSessionId == null) {
+ return;
+ }
+
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+ cluster.notify(
+ InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
+ AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment),
+ true
+ );
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
new file mode 100644
index 0000000..83e970d
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan;
+
+import org.infinispan.Cache;
+import org.keycloak.Config;
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
+import org.keycloak.sessions.AuthenticationSessionProvider;
+import org.keycloak.sessions.AuthenticationSessionProviderFactory;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import org.jboss.logging.Logger;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanAuthenticationSessionProviderFactory implements AuthenticationSessionProviderFactory {
+
+ private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProviderFactory.class);
+
+ private volatile Cache<String, AuthenticationSessionEntity> authSessionsCache;
+
+ public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public AuthenticationSessionProvider create(KeycloakSession session) {
+ lazyInit(session);
+ return new InfinispanAuthenticationSessionProvider(session, authSessionsCache);
+ }
+
+ private void updateAuthNotes(ClusterEvent clEvent) {
+ if (! (clEvent instanceof AuthenticationSessionAuthNoteUpdateEvent)) {
+ return;
+ }
+
+ AuthenticationSessionAuthNoteUpdateEvent event = (AuthenticationSessionAuthNoteUpdateEvent) clEvent;
+ AuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId());
+ updateAuthSession(authSession, event.getAuthNotesFragment());
+ }
+
+ private static void updateAuthSession(AuthenticationSessionEntity authSession, Map<String, String> authNotesFragment) {
+ if (authSession != null) {
+ if (authSession.getAuthNotes() == null) {
+ authSession.setAuthNotes(new ConcurrentHashMap<>());
+ }
+
+ for (Entry<String, String> me : authNotesFragment.entrySet()) {
+ String value = me.getValue();
+ if (value == null) {
+ authSession.getAuthNotes().remove(me.getKey());
+ } else {
+ authSession.getAuthNotes().put(me.getKey(), value);
+ }
+ }
+ }
+ }
+
+ private void lazyInit(KeycloakSession session) {
+ if (authSessionsCache == null) {
+ synchronized (this) {
+ if (authSessionsCache == null) {
+ InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
+ authSessionsCache = connections.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME);
+
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+ cluster.registerListener(AUTHENTICATION_SESSION_EVENTS, this::updateAuthNotes);
+
+ log.debug("Registered cluster listeners");
+ }
+ }
+ }
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return "infinispan";
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java
new file mode 100644
index 0000000..5471184
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.sessions.infinispan;
+
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.cluster.ClusterProvider;
+import org.infinispan.context.Flag;
+import org.keycloak.models.KeycloakTransaction;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.infinispan.Cache;
+import org.jboss.logging.Logger;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class InfinispanKeycloakTransaction implements KeycloakTransaction {
+
+ private final static Logger log = Logger.getLogger(InfinispanKeycloakTransaction.class);
+
+ public enum CacheOperation {
+ ADD, ADD_WITH_LIFESPAN, REMOVE, REPLACE, ADD_IF_ABSENT // ADD_IF_ABSENT throws an exception if there is existing value
+ }
+
+ private boolean active;
+ private boolean rollback;
+ private final Map<Object, CacheTask> tasks = new LinkedHashMap<>();
+
+ @Override
+ public void begin() {
+ active = true;
+ }
+
+ @Override
+ public void commit() {
+ if (rollback) {
+ throw new RuntimeException("Rollback only!");
+ }
+
+ tasks.values().forEach(CacheTask::execute);
+ }
+
+ @Override
+ public void rollback() {
+ tasks.clear();
+ }
+
+ @Override
+ public void setRollbackOnly() {
+ rollback = true;
+ }
+
+ @Override
+ public boolean getRollbackOnly() {
+ return rollback;
+ }
+
+ @Override
+ public boolean isActive() {
+ return active;
+ }
+
+ public <K, V> void put(Cache<K, V> cache, K key, V value) {
+ log.tracev("Adding cache operation: {0} on {1}", CacheOperation.ADD, key);
+
+ Object taskKey = getTaskKey(cache, key);
+ if (tasks.containsKey(taskKey)) {
+ throw new IllegalStateException("Can't add session: task in progress for session");
+ } else {
+ tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
+ @Override
+ public void execute() {
+ decorateCache(cache).put(key, value);
+ }
+ });
+ }
+ }
+
+ public <K, V> void put(Cache<K, V> cache, K key, V value, long lifespan, TimeUnit lifespanUnit) {
+ log.tracev("Adding cache operation: {0} on {1}", CacheOperation.ADD_WITH_LIFESPAN, key);
+
+ Object taskKey = getTaskKey(cache, key);
+ if (tasks.containsKey(taskKey)) {
+ throw new IllegalStateException("Can't add session: task in progress for session");
+ } else {
+ tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
+ @Override
+ public void execute() {
+ decorateCache(cache).put(key, value, lifespan, lifespanUnit);
+ }
+ });
+ }
+ }
+
+ public <K, V> void putIfAbsent(Cache<K, V> cache, K key, V value) {
+ log.tracev("Adding cache operation: {0} on {1}", CacheOperation.ADD_IF_ABSENT, key);
+
+ Object taskKey = getTaskKey(cache, key);
+ if (tasks.containsKey(taskKey)) {
+ throw new IllegalStateException("Can't add session: task in progress for session");
+ } else {
+ tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
+ @Override
+ public void execute() {
+ V existing = cache.putIfAbsent(key, value);
+ if (existing != null) {
+ throw new IllegalStateException("There is already existing value in cache for key " + key);
+ }
+ }
+ });
+ }
+ }
+
+ public <K, V> void replace(Cache<K, V> cache, K key, V value) {
+ log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REPLACE, key);
+
+ Object taskKey = getTaskKey(cache, key);
+ CacheTask current = tasks.get(taskKey);
+ if (current != null) {
+ if (current instanceof CacheTaskWithValue) {
+ ((CacheTaskWithValue<V>) current).setValue(value);
+ }
+ } else {
+ tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
+ @Override
+ public void execute() {
+ decorateCache(cache).replace(key, value);
+ }
+ });
+ }
+ }
+
+ public <K, V> void notify(ClusterProvider clusterProvider, String taskKey, ClusterEvent event, boolean ignoreSender) {
+ log.tracev("Adding cache operation SEND_EVENT: {0}", event);
+
+ String theTaskKey = taskKey;
+ int i = 1;
+ while (tasks.containsKey(theTaskKey)) {
+ theTaskKey = taskKey + "-" + (i++);
+ }
+
+ tasks.put(taskKey, () -> clusterProvider.notify(taskKey, event, ignoreSender));
+ }
+
+ public <K, V> void remove(Cache<K, V> cache, K key) {
+ log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REMOVE, key);
+
+ Object taskKey = getTaskKey(cache, key);
+ tasks.put(taskKey, () -> decorateCache(cache).remove(key));
+ }
+
+ // This is for possibility to lookup for session by id, which was created in this transaction
+ public <K, V> V get(Cache<K, V> cache, K key) {
+ Object taskKey = getTaskKey(cache, key);
+ CacheTask<V> current = tasks.get(taskKey);
+ if (current != null) {
+ if (current instanceof CacheTaskWithValue) {
+ return ((CacheTaskWithValue<V>) current).getValue();
+ }
+ return null;
+ }
+
+ // Should we have per-transaction cache for lookups?
+ return cache.get(key);
+ }
+
+ private static <K, V> Object getTaskKey(Cache<K, V> cache, K key) {
+ if (key instanceof String) {
+ return new StringBuilder(cache.getName())
+ .append("::")
+ .append(key).toString();
+ } else {
+ return key;
+ }
+ }
+
+ public interface CacheTask<V> {
+ void execute();
+ }
+
+ public abstract class CacheTaskWithValue<V> implements CacheTask<V> {
+ protected V value;
+
+ public CacheTaskWithValue(V value) {
+ this.value = value;
+ }
+
+ public V getValue() {
+ return value;
+ }
+
+ public void setValue(V value) {
+ this.value = value;
+ }
+ }
+
+ // Ignore return values. Should have better performance within cluster / cross-dc env
+ private static <K, V> Cache<K, V> decorateCache(Cache<K, V> cache) {
+ return cache.getAdvancedCache()
+ .withFlags(Flag.IGNORE_RETURN_VALUES, Flag.SKIP_REMOTE_LOOKUP);
+ }
+}
\ No newline at end of file
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProvider.java
new file mode 100644
index 0000000..0aca09f
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProvider.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan;
+
+import org.infinispan.Cache;
+import org.infinispan.distribution.DistributionManager;
+import org.infinispan.remoting.transport.Address;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.sessions.StickySessionEncoderProvider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanStickySessionEncoderProvider implements StickySessionEncoderProvider {
+
+ private final KeycloakSession session;
+ private final String myNodeName;
+
+ public InfinispanStickySessionEncoderProvider(KeycloakSession session, String myNodeName) {
+ this.session = session;
+ this.myNodeName = myNodeName;
+ }
+
+ @Override
+ public String encodeSessionId(String sessionId) {
+ String nodeName = getNodeName(sessionId);
+ if (nodeName != null) {
+ return sessionId + '.' + nodeName;
+ } else {
+ return sessionId;
+ }
+ }
+
+ @Override
+ public String decodeSessionId(String encodedSessionId) {
+ int index = encodedSessionId.indexOf('.');
+ return index == -1 ? encodedSessionId : encodedSessionId.substring(0, index);
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+
+ private String getNodeName(String sessionId) {
+ InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
+ Cache cache = ispnProvider.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME);
+ DistributionManager distManager = cache.getAdvancedCache().getDistributionManager();
+
+ if (distManager != null) {
+ // Sticky session to the node, who owns this authenticationSession
+ Address address = distManager.getPrimaryLocation(sessionId);
+ return address.toString();
+ } else {
+ // Fallback to jbossNodeName if authSession cache is local
+ return myNodeName;
+ }
+ }
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java
new file mode 100644
index 0000000..b8e6a71
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.sessions.infinispan;
+
+import org.keycloak.Config;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.sessions.StickySessionEncoderProvider;
+import org.keycloak.sessions.StickySessionEncoderProviderFactory;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanStickySessionEncoderProviderFactory implements StickySessionEncoderProviderFactory {
+
+ private String myNodeName;
+
+ @Override
+ public StickySessionEncoderProvider create(KeycloakSession session) {
+ return new InfinispanStickySessionEncoderProvider(session, myNodeName);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ myNodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "infinispan";
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index a7c8c31..0e50a73 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -23,10 +23,9 @@ import org.infinispan.context.Flag;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientInitialAccessModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
@@ -34,31 +33,29 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
-import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.stream.ClientInitialAccessPredicate;
-import org.keycloak.models.sessions.infinispan.stream.ClientSessionPredicate;
import org.keycloak.models.sessions.infinispan.stream.Comparators;
import org.keycloak.models.sessions.infinispan.stream.Mappers;
import org.keycloak.models.sessions.infinispan.stream.SessionPredicate;
import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate;
import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.models.utils.RealmInfoUtil;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@@ -90,28 +87,27 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
@Override
- public ClientSessionModel createClientSession(RealmModel realm, ClientModel client) {
- String id = KeycloakModelUtils.generateId();
+ public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) {
+ AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
+
+ AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, sessionCache);
+ adapter.setUserSession(userSession);
+ return adapter;
+ }
- ClientSessionEntity entity = new ClientSessionEntity();
+ @Override
+ public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
+ UserSessionEntity entity = new UserSessionEntity();
entity.setId(id);
- entity.setRealm(realm.getId());
- entity.setTimestamp(Time.currentTime());
- entity.setClient(client.getId());
+ updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
- tx.put(sessionCache, id, entity);
+ tx.putIfAbsent(sessionCache, id, entity);
- ClientSessionAdapter wrap = wrap(realm, entity, false);
- return wrap;
+ return wrap(realm, entity, false);
}
- @Override
- public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
- String id = KeycloakModelUtils.generateId();
-
- UserSessionEntity entity = new UserSessionEntity();
- entity.setId(id);
+ void updateSessionEntity(UserSessionEntity entity, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
entity.setRealm(realm.getId());
entity.setUser(user.getId());
entity.setLoginUsername(loginUsername);
@@ -126,42 +122,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.setStarted(currentTime);
entity.setLastSessionRefresh(currentTime);
- tx.put(sessionCache, id, entity);
-
- return wrap(realm, entity, false);
- }
-
- @Override
- public ClientSessionModel getClientSession(RealmModel realm, String id) {
- return getClientSession(realm, id, false);
- }
-
- protected ClientSessionModel getClientSession(RealmModel realm, String id, boolean offline) {
- Cache<String, SessionEntity> cache = getCache(offline);
- ClientSessionEntity entity = (ClientSessionEntity) cache.get(id);
-
- // Chance created in this transaction
- if (entity == null) {
- entity = (ClientSessionEntity) tx.get(cache, id);
- }
-
- return wrap(realm, entity, offline);
- }
-
- @Override
- public ClientSessionModel getClientSession(String id) {
- ClientSessionEntity entity = (ClientSessionEntity) sessionCache.get(id);
-
- // Chance created in this transaction
- if (entity == null) {
- entity = (ClientSessionEntity) tx.get(sessionCache, id);
- }
- if (entity != null) {
- RealmModel realm = session.realms().getRealm(entity.getRealm());
- return wrap(realm, entity, false);
- }
- return null;
}
@Override
@@ -171,11 +132,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) {
Cache<String, SessionEntity> cache = getCache(offline);
- UserSessionEntity entity = (UserSessionEntity) cache.get(id);
+ UserSessionEntity entity = (UserSessionEntity) tx.get(cache, id); // Chance created in this transaction
- // Chance created in this transaction
if (entity == null) {
- entity = (UserSessionEntity) tx.get(cache, id);
+ entity = (UserSessionEntity) cache.get(id);
}
return wrap(realm, entity, offline);
@@ -221,37 +181,50 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected List<UserSessionModel> getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) {
final Cache<String, SessionEntity> cache = getCache(offline);
- Iterator<UserSessionTimestamp> itr = cache.entrySet().stream()
- .filter(ClientSessionPredicate.create(realm.getId()).client(client.getId()).requireUserSession())
- .map(Mappers.clientSessionToUserSessionTimestamp())
- .iterator();
+ Stream<UserSessionEntity> stream = cache.entrySet().stream()
+ .filter(UserSessionPredicate.create(realm.getId()).client(client.getId()))
+ .map(Mappers.userSessionEntity())
+ .sorted(Comparators.userSessionLastSessionRefresh());
- Map<String, UserSessionTimestamp> m = new HashMap<>();
- while(itr.hasNext()) {
- UserSessionTimestamp next = itr.next();
- if (!m.containsKey(next.getUserSessionId()) || m.get(next.getUserSessionId()).getClientSessionTimestamp() < next.getClientSessionTimestamp()) {
- m.put(next.getUserSessionId(), next);
- }
- }
+ // Doesn't work due to ISPN-6575 . TODO Fix once infinispan upgraded to 8.2.2.Final or 9.0
+// if (firstResult > 0) {
+// stream = stream.skip(firstResult);
+// }
+//
+// if (maxResults > 0) {
+// stream = stream.limit(maxResults);
+// }
+//
+// List<UserSessionEntity> entities = stream.collect(Collectors.toList());
- Stream<UserSessionTimestamp> stream = new LinkedList<>(m.values()).stream().sorted(Comparators.userSessionTimestamp());
- if (firstResult > 0) {
- stream = stream.skip(firstResult);
+ // Workaround for ISPN-6575 TODO Fix once infinispan upgraded to 8.2.2.Final or 9.0 and replace with the more effective code above
+ if (firstResult < 0) {
+ firstResult = 0;
}
+ if (maxResults < 0) {
+ maxResults = Integer.MAX_VALUE;
+ }
+
+ int count = firstResult + maxResults;
+ if (count > 0) {
+ stream = stream.limit(count);
+ }
+ List<UserSessionEntity> entities = stream.collect(Collectors.toList());
- if (maxResults > 0) {
- stream = stream.limit(maxResults);
+ if (firstResult > entities.size()) {
+ return Collections.emptyList();
}
+ maxResults = Math.min(maxResults, entities.size() - firstResult);
+ entities = entities.subList(firstResult, firstResult + maxResults);
+
+
final List<UserSessionModel> sessions = new LinkedList<>();
- stream.forEach(new Consumer<UserSessionTimestamp>() {
+ entities.stream().forEach(new Consumer<UserSessionEntity>() {
@Override
- public void accept(UserSessionTimestamp userSessionTimestamp) {
- SessionEntity entity = cache.get(userSessionTimestamp.getUserSessionId());
- if (entity != null) {
- sessions.add(wrap(realm, (UserSessionEntity) entity, offline));
- }
+ public void accept(UserSessionEntity userSessionEntity) {
+ sessions.add(wrap(realm, userSessionEntity, offline));
}
});
@@ -264,7 +237,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) {
- return getCache(offline).entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).client(client.getId()).requireUserSession()).map(Mappers.clientSessionToUserSessionId()).distinct().count();
+ return getCache(offline).entrySet().stream()
+ .filter(UserSessionPredicate.create(realm.getId()).client(client.getId()))
+ .count();
}
@Override
@@ -294,9 +269,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void removeExpired(RealmModel realm) {
log.debugf("Removing expired sessions");
removeExpiredUserSessions(realm);
- removeExpiredClientSessions(realm);
removeExpiredOfflineUserSessions(realm);
- removeExpiredOfflineClientSessions(realm);
removeExpiredClientInitialAccess(realm);
}
@@ -313,33 +286,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
counter++;
UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
tx.remove(sessionCache, entity.getId());
-
- if (entity.getClientSessions() != null) {
- for (String clientSessionId : entity.getClientSessions()) {
- tx.remove(sessionCache, clientSessionId);
- }
- }
}
log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName());
}
- private void removeExpiredClientSessions(RealmModel realm) {
- int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
-
- // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
- Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
- .entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredDettachedClientSession).requireNullUserSession()).iterator();
-
- int counter = 0;
- while (itr.hasNext()) {
- counter++;
- tx.remove(sessionCache, itr.next().getKey());
- }
-
- log.debugf("Removed %d expired client sessions for realm '%s'", counter, realm.getName());
- }
-
private void removeExpiredOfflineUserSessions(RealmModel realm) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
@@ -357,33 +308,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
persister.removeUserSession(entity.getId(), true);
- for (String clientSessionId : entity.getClientSessions()) {
- tx.remove(offlineSessionCache, clientSessionId);
+ for (String clientUUID : entity.getAuthenticatedClientSessions().keySet()) {
+ persister.removeClientSession(entity.getId(), clientUUID, true);
}
}
log.debugf("Removed %d expired offline user sessions for realm '%s'", counter, realm.getName());
}
- private void removeExpiredOfflineClientSessions(RealmModel realm) {
- UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
- int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
-
- // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
- Iterator<String> itr = offlineSessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
- .entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredOffline)).map(Mappers.sessionId()).iterator();
-
- int counter = 0;
- while (itr.hasNext()) {
- counter++;
- String sessionId = itr.next();
- tx.remove(offlineSessionCache, sessionId);
- persister.removeClientSession(sessionId, true);
- }
-
- log.debugf("Removed %d expired offline client sessions for realm '%s'", counter, realm.getName());
- }
-
private void removeExpiredClientInitialAccess(RealmModel realm) {
Iterator<String> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
.entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId()).expired(Time.currentTime())).map(Mappers.sessionId()).iterator();
@@ -445,21 +377,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public void onClientRemoved(RealmModel realm, ClientModel client) {
- onClientRemoved(realm, client, true);
- onClientRemoved(realm, client, false);
- }
-
- private void onClientRemoved(RealmModel realm, ClientModel client, boolean offline) {
- Cache<String, SessionEntity> cache = getCache(offline);
-
- Iterator<Map.Entry<String, SessionEntity>> itr = cache.entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).client(client.getId())).iterator();
- while (itr.hasNext()) {
- ClientSessionEntity entity = (ClientSessionEntity) itr.next().getValue();
- ClientSessionAdapter adapter = wrap(realm, entity, offline);
- adapter.setUserSession(null);
-
- tx.remove(cache, entity.getId());
- }
+ // Nothing for now. userSession.getAuthenticatedClientSessions() will check lazily if particular client exists and update userSession on-the-fly.
}
@@ -475,55 +393,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void close() {
}
- void attachSession(UserSessionAdapter userSession, ClientSessionModel clientSession) {
- UserSessionEntity entity = userSession.getEntity();
- String clientSessionId = clientSession.getId();
- if (!entity.getClientSessions().contains(clientSessionId)) {
- entity.getClientSessions().add(clientSessionId);
- userSession.update();
- }
- }
-
- @Override
- public void removeClientSession(RealmModel realm, ClientSessionModel clientSession) {
- removeClientSession(realm, clientSession, false);
- }
-
- protected void removeClientSession(RealmModel realm, ClientSessionModel clientSession, boolean offline) {
- Cache<String, SessionEntity> cache = getCache(offline);
-
- UserSessionModel userSession = clientSession.getUserSession();
- if (userSession != null) {
- UserSessionEntity entity = ((UserSessionAdapter) userSession).getEntity();
- if (entity.getClientSessions() != null) {
- entity.getClientSessions().remove(clientSession.getId());
-
- }
- tx.replace(cache, entity.getId(), entity);
- }
- tx.remove(cache, clientSession.getId());
- }
-
-
- void dettachSession(UserSessionAdapter userSession, ClientSessionModel clientSession) {
- UserSessionEntity entity = userSession.getEntity();
- String clientSessionId = clientSession.getId();
- if (entity.getClientSessions() != null && entity.getClientSessions().contains(clientSessionId)) {
- entity.getClientSessions().remove(clientSessionId);
- userSession.update();
- }
- }
-
protected void removeUserSession(RealmModel realm, UserSessionEntity sessionEntity, boolean offline) {
Cache<String, SessionEntity> cache = getCache(offline);
tx.remove(cache, sessionEntity.getId());
-
- if (sessionEntity.getClientSessions() != null) {
- for (String clientSessionId : sessionEntity.getClientSessions()) {
- tx.remove(cache, clientSessionId);
- }
- }
}
InfinispanKeycloakTransaction getTx() {
@@ -551,11 +424,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return models;
}
- ClientSessionAdapter wrap(RealmModel realm, ClientSessionEntity entity, boolean offline) {
- Cache<String, SessionEntity> cache = getCache(offline);
- return entity != null ? new ClientSessionAdapter(session, this, cache, realm, entity, offline) : null;
- }
-
ClientInitialAccessAdapter wrap(RealmModel realm, ClientInitialAccessEntity entity) {
Cache<String, SessionEntity> cache = getCache(false);
return entity != null ? new ClientInitialAccessAdapter(session, this, cache, realm, entity) : null;
@@ -565,14 +433,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return entity != null ? new UserLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
}
- List<ClientSessionModel> wrapClientSessions(RealmModel realm, Collection<ClientSessionEntity> entities, boolean offline) {
- List<ClientSessionModel> models = new LinkedList<>();
- for (ClientSessionEntity e : entities) {
- models.add(wrap(realm, e, offline));
- }
- return models;
- }
-
UserSessionEntity getUserSessionEntity(UserSessionModel userSession, boolean offline) {
if (userSession instanceof UserSessionAdapter) {
return ((UserSessionAdapter) userSession).getEntity();
@@ -585,7 +445,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public UserSessionModel createOfflineUserSession(UserSessionModel userSession) {
- UserSessionAdapter offlineUserSession = importUserSession(userSession, true);
+ UserSessionAdapter offlineUserSession = importUserSession(userSession, true, false);
// started and lastSessionRefresh set to current time
int currentTime = Time.currentTime();
@@ -596,7 +456,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
@Override
- public UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId) {
+ public UserSessionAdapter getOfflineUserSession(RealmModel realm, String userSessionId) {
return getUserSession(realm, userSessionId, true);
}
@@ -608,9 +468,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
}
+
+
@Override
- public ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession) {
- ClientSessionAdapter offlineClientSession = importClientSession(clientSession, true);
+ public AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedClientSessionModel clientSession, UserSessionModel offlineUserSession) {
+ UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession :
+ getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId());
+
+ AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession);
// update timestamp to current time
offlineClientSession.setTimestamp(Time.currentTime());
@@ -619,38 +484,17 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
@Override
- public ClientSessionModel getOfflineClientSession(RealmModel realm, String clientSessionId) {
- return getClientSession(realm, clientSessionId, true);
- }
-
- @Override
- public List<ClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
+ public List<UserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
Iterator<Map.Entry<String, SessionEntity>> itr = offlineSessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).iterator();
- List<ClientSessionModel> clientSessions = new LinkedList<>();
+ List<UserSessionModel> userSessions = new LinkedList<>();
while(itr.hasNext()) {
UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
- Set<String> currClientSessions = entity.getClientSessions();
-
- if (currClientSessions == null) {
- continue;
- }
-
- for (String clientSessionId : currClientSessions) {
- ClientSessionEntity cls = (ClientSessionEntity) offlineSessionCache.get(clientSessionId);
- if (cls != null) {
- clientSessions.add(wrap(realm, cls, true));
- }
- }
+ UserSessionModel userSession = wrap(realm, entity, true);
+ userSessions.add(userSession);
}
- return clientSessions;
- }
-
- @Override
- public void removeOfflineClientSession(RealmModel realm, String clientSessionId) {
- ClientSessionModel clientSession = getOfflineClientSession(realm, clientSessionId);
- removeClientSession(realm, clientSession, true);
+ return userSessions;
}
@Override
@@ -664,7 +508,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
@Override
- public UserSessionAdapter importUserSession(UserSessionModel userSession, boolean offline) {
+ public UserSessionAdapter importUserSession(UserSessionModel userSession, boolean offline, boolean importAuthenticatedClientSessions) {
UserSessionEntity entity = new UserSessionEntity();
entity.setId(userSession.getId());
entity.setRealm(userSession.getRealm().getId());
@@ -682,34 +526,45 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.setStarted(userSession.getStarted());
entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
+
Cache<String, SessionEntity> cache = getCache(offline);
tx.put(cache, userSession.getId(), entity);
- return wrap(userSession.getRealm(), entity, offline);
+ UserSessionAdapter importedSession = wrap(userSession.getRealm(), entity, offline);
+
+ // Handle client sessions
+ if (importAuthenticatedClientSessions) {
+ for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
+ importClientSession(importedSession, clientSession);
+ }
+ }
+
+ return importedSession;
}
- @Override
- public ClientSessionAdapter importClientSession(ClientSessionModel clientSession, boolean offline) {
- ClientSessionEntity entity = new ClientSessionEntity();
- entity.setId(clientSession.getId());
- entity.setRealm(clientSession.getRealm().getId());
+
+ private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession) {
+ AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
entity.setAction(clientSession.getAction());
- entity.setAuthenticatorStatus(clientSession.getExecutionStatus());
- entity.setAuthMethod(clientSession.getAuthMethod());
- if (clientSession.getAuthenticatedUser() != null) {
- entity.setAuthUserId(clientSession.getAuthenticatedUser().getId());
- }
- entity.setClient(clientSession.getClient().getId());
+ entity.setAuthMethod(clientSession.getProtocol());
+
entity.setNotes(clientSession.getNotes());
entity.setProtocolMappers(clientSession.getProtocolMappers());
entity.setRedirectUri(clientSession.getRedirectUri());
entity.setRoles(clientSession.getRoles());
entity.setTimestamp(clientSession.getTimestamp());
- entity.setUserSessionNotes(clientSession.getUserSessionNotes());
- Cache<String, SessionEntity> cache = getCache(offline);
- tx.put(cache, clientSession.getId(), entity);
- return wrap(clientSession.getRealm(), entity, offline);
+ Map<String, AuthenticatedClientSessionEntity> clientSessions = importedUserSession.getEntity().getAuthenticatedClientSessions();
+ if (clientSessions == null) {
+ clientSessions = new HashMap<>();
+ importedUserSession.getEntity().setAuthenticatedClientSessions(clientSessions);
+ }
+
+ clientSessions.put(clientSession.getClient().getId(), entity);
+
+ importedUserSession.update();
+
+ return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, importedUserSession.getCache());
}
@Override
@@ -732,11 +587,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
Cache<String, SessionEntity> cache = getCache(false);
- ClientInitialAccessEntity entity = (ClientInitialAccessEntity) cache.get(id);
+ ClientInitialAccessEntity entity = (ClientInitialAccessEntity) tx.get(cache, id); // Chance created in this transaction
- // If created in this transaction
if (entity == null) {
- entity = (ClientInitialAccessEntity) tx.get(cache, id);
+ entity = (ClientInitialAccessEntity) cache.get(id);
}
return wrap(realm, entity);
@@ -757,145 +611,4 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return list;
}
-
- class InfinispanKeycloakTransaction implements KeycloakTransaction {
-
- private boolean active;
- private boolean rollback;
- private Map<Object, CacheTask> tasks = new HashMap<>();
-
- @Override
- public void begin() {
- active = true;
- }
-
- @Override
- public void commit() {
- if (rollback) {
- throw new RuntimeException("Rollback only!");
- }
-
- for (CacheTask task : tasks.values()) {
- task.execute();
- }
- }
-
- @Override
- public void rollback() {
- tasks.clear();
- }
-
- @Override
- public void setRollbackOnly() {
- rollback = true;
- }
-
- @Override
- public boolean getRollbackOnly() {
- return rollback;
- }
-
- @Override
- public boolean isActive() {
- return active;
- }
-
- public void put(Cache cache, Object key, Object value) {
- log.tracev("Adding cache operation: {0} on {1}", CacheOperation.ADD, key);
-
- Object taskKey = getTaskKey(cache, key);
- if (tasks.containsKey(taskKey)) {
- throw new IllegalStateException("Can't add session: task in progress for session");
- } else {
- tasks.put(taskKey, new CacheTask(cache, CacheOperation.ADD, key, value));
- }
- }
-
- public void replace(Cache cache, Object key, Object value) {
- log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REPLACE, key);
-
- Object taskKey = getTaskKey(cache, key);
- CacheTask current = tasks.get(taskKey);
- if (current != null) {
- switch (current.operation) {
- case ADD:
- case REPLACE:
- current.value = value;
- return;
- case REMOVE:
- return;
- }
- } else {
- tasks.put(taskKey, new CacheTask(cache, CacheOperation.REPLACE, key, value));
- }
- }
-
- public void remove(Cache cache, Object key) {
- log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REMOVE, key);
-
- Object taskKey = getTaskKey(cache, key);
- tasks.put(taskKey, new CacheTask(cache, CacheOperation.REMOVE, key, null));
- }
-
- // This is for possibility to lookup for session by id, which was created in this transaction
- public Object get(Cache cache, Object key) {
- Object taskKey = getTaskKey(cache, key);
- CacheTask current = tasks.get(taskKey);
- if (current != null) {
- switch (current.operation) {
- case ADD:
- case REPLACE:
- return current.value; }
- }
-
- return null;
- }
-
- private Object getTaskKey(Cache cache, Object key) {
- if (key instanceof String) {
- return new StringBuilder(cache.getName())
- .append("::")
- .append(key.toString()).toString();
- } else {
- // loginFailure cache
- return key;
- }
- }
-
- public class CacheTask {
- private Cache cache;
- private CacheOperation operation;
- private Object key;
- private Object value;
-
- public CacheTask(Cache cache, CacheOperation operation, Object key, Object value) {
- this.cache = cache;
- this.operation = operation;
- this.key = key;
- this.value = value;
- }
-
- public void execute() {
- log.tracev("Executing cache operation: {0} on {1}", operation, key);
-
- switch (operation) {
- case ADD:
- cache.put(key, value);
- break;
- case REMOVE:
- cache.remove(key);
- break;
- case REPLACE:
- cache.replace(key, value);
- break;
- }
- }
- }
-
- }
-
- public enum CacheOperation {
- ADD, REMOVE, REPLACE
- }
-
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java
index 83a3885..2b6fb71 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java
@@ -19,7 +19,6 @@ package org.keycloak.models.sessions.infinispan.initializer;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.session.UserSessionPersisterProvider;
@@ -64,12 +63,7 @@ public class OfflineUserSessionLoader implements SessionLoader {
for (UserSessionModel persistentSession : sessions) {
// Save to memory/infinispan
- UserSessionModel offlineUserSession = session.sessions().importUserSession(persistentSession, true);
-
- for (ClientSessionModel persistentClientSession : persistentSession.getClientSessions()) {
- ClientSessionModel offlineClientSession = session.sessions().importClientSession(persistentClientSession, true);
- offlineClientSession.setUserSession(offlineUserSession);
- }
+ UserSessionModel offlineUserSession = session.sessions().importUserSession(persistentSession, true, true);
}
return true;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java
index 4907ec1..ec2a2cb 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java
@@ -18,6 +18,8 @@
package org.keycloak.models.sessions.infinispan.stream;
import org.keycloak.models.sessions.infinispan.UserSessionTimestamp;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.io.Serializable;
import java.util.Comparator;
@@ -38,4 +40,17 @@ public class Comparators {
}
}
+
+ public static Comparator<UserSessionEntity> userSessionLastSessionRefresh() {
+ return new UserSessionLastSessionRefreshComparator();
+ }
+
+ private static class UserSessionLastSessionRefreshComparator implements Comparator<UserSessionEntity>, Serializable {
+
+ @Override
+ public int compare(UserSessionEntity u1, UserSessionEntity u2) {
+ return u1.getLastSessionRefresh() - u2.getLastSessionRefresh();
+ }
+ }
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
index 6bf1358..dd2db68 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
@@ -18,10 +18,10 @@
package org.keycloak.models.sessions.infinispan.stream;
import org.keycloak.models.sessions.infinispan.UserSessionTimestamp;
-import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.io.Serializable;
import java.util.Map;
@@ -33,10 +33,6 @@ import java.util.function.Function;
*/
public class Mappers {
- public static Function<Map.Entry<String, SessionEntity>, UserSessionTimestamp> clientSessionToUserSessionTimestamp() {
- return new ClientSessionToUserSessionTimestampMapper();
- }
-
public static Function<Map.Entry<String, Optional<UserSessionTimestamp>>, UserSessionTimestamp> userSessionTimestamp() {
return new UserSessionTimestampMapper();
}
@@ -49,21 +45,12 @@ public class Mappers {
return new SessionEntityMapper();
}
- public static Function<Map.Entry<LoginFailureKey, LoginFailureEntity>, LoginFailureKey> loginFailureId() {
- return new LoginFailureIdMapper();
+ public static Function<Map.Entry<String, SessionEntity>, UserSessionEntity> userSessionEntity() {
+ return new UserSessionEntityMapper();
}
- public static Function<Map.Entry<String, SessionEntity>, String> clientSessionToUserSessionId() {
- return new ClientSessionToUserSessionIdMapper();
- }
-
- private static class ClientSessionToUserSessionTimestampMapper implements Function<Map.Entry<String, SessionEntity>, UserSessionTimestamp>, Serializable {
- @Override
- public UserSessionTimestamp apply(Map.Entry<String, SessionEntity> entry) {
- SessionEntity e = entry.getValue();
- ClientSessionEntity entity = (ClientSessionEntity) e;
- return new UserSessionTimestamp(entity.getUserSession(), entity.getTimestamp());
- }
+ public static Function<Map.Entry<LoginFailureKey, LoginFailureEntity>, LoginFailureKey> loginFailureId() {
+ return new LoginFailureIdMapper();
}
private static class UserSessionTimestampMapper implements Function<Map.Entry<String, Optional<org.keycloak.models.sessions.infinispan.UserSessionTimestamp>>, org.keycloak.models.sessions.infinispan.UserSessionTimestamp>, Serializable {
@@ -87,19 +74,18 @@ public class Mappers {
}
}
- private static class LoginFailureIdMapper implements Function<Map.Entry<LoginFailureKey, LoginFailureEntity>, LoginFailureKey>, Serializable {
+ private static class UserSessionEntityMapper implements Function<Map.Entry<String, SessionEntity>, UserSessionEntity>, Serializable {
@Override
- public LoginFailureKey apply(Map.Entry<LoginFailureKey, LoginFailureEntity> entry) {
- return entry.getKey();
+ public UserSessionEntity apply(Map.Entry<String, SessionEntity> entry) {
+ return (UserSessionEntity) entry.getValue();
}
}
- private static class ClientSessionToUserSessionIdMapper implements Function<Map.Entry<String, SessionEntity>, String>, Serializable {
+ private static class LoginFailureIdMapper implements Function<Map.Entry<LoginFailureKey, LoginFailureEntity>, LoginFailureKey>, Serializable {
@Override
- public String apply(Map.Entry<String, SessionEntity> entry) {
- SessionEntity e = entry.getValue();
- ClientSessionEntity entity = (ClientSessionEntity) e;
- return entity.getUserSession();
+ public LoginFailureKey apply(Map.Entry<LoginFailureKey, LoginFailureEntity> entry) {
+ return entry.getKey();
}
}
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java
index 77ff572..0cc3fcc 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java
@@ -33,6 +33,8 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
private String user;
+ private String client;
+
private Integer expired;
private Integer expiredRefresh;
@@ -53,6 +55,11 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
return this;
}
+ public UserSessionPredicate client(String clientUUID) {
+ this.client = clientUUID;
+ return this;
+ }
+
public UserSessionPredicate expired(Integer expired, Integer expiredRefresh) {
this.expired = expired;
this.expiredRefresh = expiredRefresh;
@@ -87,6 +94,10 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
return false;
}
+ if (client != null && (entity.getAuthenticatedClientSessions() == null || !entity.getAuthenticatedClientSessions().containsKey(client))) {
+ return false;
+ }
+
if (brokerSessionId != null && !brokerSessionId.equals(entity.getBrokerSessionId())) {
return false;
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
index bf1e6fd..8ab15f7 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
@@ -18,11 +18,13 @@
package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
@@ -60,6 +62,36 @@ public class UserSessionAdapter implements UserSessionModel {
this.offline = offline;
}
+ @Override
+ public Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions() {
+ Map<String, AuthenticatedClientSessionEntity> clientSessionEntities = entity.getAuthenticatedClientSessions();
+ Map<String, AuthenticatedClientSessionModel> result = new HashMap<>();
+
+ List<String> removedClientUUIDS = new LinkedList<>();
+
+ if (clientSessionEntities != null) {
+ clientSessionEntities.forEach((String key, AuthenticatedClientSessionEntity value) -> {
+ // Check if client still exists
+ ClientModel client = realm.getClientById(key);
+ if (client != null) {
+ result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, cache));
+ } else {
+ removedClientUUIDS.add(key);
+ }
+ });
+ }
+
+ // Update user session
+ if (!removedClientUUIDS.isEmpty()) {
+ for (String clientUUID : removedClientUUIDS) {
+ entity.getAuthenticatedClientSessions().remove(clientUUID);
+ }
+ update();
+ }
+
+ return Collections.unmodifiableMap(result);
+ }
+
public String getId() {
return entity.getId();
}
@@ -83,6 +115,12 @@ public class UserSessionAdapter implements UserSessionModel {
}
@Override
+ public void setUser(UserModel user) {
+ entity.setUser(user.getId());
+ update();
+ }
+
+ @Override
public String getLoginUsername() {
return entity.getLoginUsername();
}
@@ -159,19 +197,14 @@ public class UserSessionAdapter implements UserSessionModel {
}
@Override
- public List<ClientSessionModel> getClientSessions() {
- if (entity.getClientSessions() != null) {
- List<ClientSessionModel> clientSessions = new LinkedList<>();
- for (String c : entity.getClientSessions()) {
- ClientSessionModel clientSession = provider.getClientSession(realm, c, offline);
- if (clientSession != null) {
- clientSessions.add(clientSession);
- }
- }
- return clientSessions;
- } else {
- return Collections.emptyList();
- }
+ public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
+ provider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
+
+ entity.setState(null);
+ entity.setNotes(null);
+ entity.setAuthenticatedClientSessions(null);
+
+ update();
}
@Override
@@ -196,4 +229,7 @@ public class UserSessionAdapter implements UserSessionModel {
provider.getTx().replace(cache, entity.getId(), entity);
}
+ Cache<String, SessionEntity> getCache() {
+ return cache;
+ }
}
diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory
new file mode 100644
index 0000000..4100ecc
--- /dev/null
+++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory
@@ -0,0 +1 @@
+org.keycloak.models.sessions.infinispan.InfinispanActionTokenStoreProviderFactory
diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.sessions.AuthenticationSessionProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.sessions.AuthenticationSessionProviderFactory
new file mode 100644
index 0000000..2c7b298
--- /dev/null
+++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.sessions.AuthenticationSessionProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory
\ No newline at end of file
diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.sessions.StickySessionEncoderProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.sessions.StickySessionEncoderProviderFactory
new file mode 100644
index 0000000..0436dde
--- /dev/null
+++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.sessions.StickySessionEncoderProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.models.sessions.infinispan.InfinispanStickySessionEncoderProviderFactory
\ No newline at end of file
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java
index 499a008..6ee1074 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java
@@ -26,4 +26,8 @@ public interface RealmAttributes {
String DISPLAY_NAME_HTML = "displayNameHtml";
+ String ACTION_TOKEN_GENERATED_BY_ADMIN_LIFESPAN = "actionTokenGeneratedByAdminLifespan";
+
+ String ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN = "actionTokenGeneratedByUserLifespan";
+
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 58aa424..b3b4db2 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -512,6 +512,26 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
em.flush();
}
+ @Override
+ public int getActionTokenGeneratedByAdminLifespan() {
+ return getAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_ADMIN_LIFESPAN, 12 * 60 * 60);
+ }
+
+ @Override
+ public void setActionTokenGeneratedByAdminLifespan(int actionTokenGeneratedByAdminLifespan) {
+ setAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_ADMIN_LIFESPAN, actionTokenGeneratedByAdminLifespan);
+ }
+
+ @Override
+ public int getActionTokenGeneratedByUserLifespan() {
+ return getAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN, getAccessCodeLifespanUserAction());
+ }
+
+ @Override
+ public void setActionTokenGeneratedByUserLifespan(int actionTokenGeneratedByUserLifespan) {
+ setAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN, actionTokenGeneratedByUserLifespan);
+ }
+
protected RequiredCredentialModel initRequiredCredentialModel(String type) {
RequiredCredentialModel model = RequiredCredentialModel.BUILT_IN.get(type);
if (model == null) {
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
index b3aea63..64246e8 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
@@ -17,14 +17,14 @@
package org.keycloak.models.jpa.session;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
-import org.keycloak.models.session.PersistentClientSessionAdapter;
+import org.keycloak.models.session.PersistentAuthenticatedClientSessionAdapter;
import org.keycloak.models.session.PersistentClientSessionModel;
import org.keycloak.models.session.PersistentUserSessionAdapter;
import org.keycloak.models.session.PersistentUserSessionModel;
@@ -34,8 +34,9 @@ import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
-import java.util.LinkedList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -68,12 +69,11 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
}
@Override
- public void createClientSession(ClientSessionModel clientSession, boolean offline) {
- PersistentClientSessionAdapter adapter = new PersistentClientSessionAdapter(clientSession);
+ public void createClientSession(AuthenticatedClientSessionModel clientSession, boolean offline) {
+ PersistentAuthenticatedClientSessionAdapter adapter = new PersistentAuthenticatedClientSessionAdapter(clientSession);
PersistentClientSessionModel model = adapter.getUpdatedModel();
PersistentClientSessionEntity entity = new PersistentClientSessionEntity();
- entity.setClientSessionId(clientSession.getId());
entity.setClientId(clientSession.getClient().getId());
entity.setTimestamp(clientSession.getTimestamp());
String offlineStr = offlineToString(offline);
@@ -121,9 +121,9 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
}
@Override
- public void removeClientSession(String clientSessionId, boolean offline) {
+ public void removeClientSession(String userSessionId, String clientUUID, boolean offline) {
String offlineStr = offlineToString(offline);
- PersistentClientSessionEntity sessionEntity = em.find(PersistentClientSessionEntity.class, new PersistentClientSessionEntity.Key(clientSessionId, offlineStr));
+ PersistentClientSessionEntity sessionEntity = em.find(PersistentClientSessionEntity.class, new PersistentClientSessionEntity.Key(userSessionId, clientUUID, offlineStr));
if (sessionEntity != null) {
em.remove(sessionEntity);
@@ -227,14 +227,14 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
int j = 0;
for (UserSessionModel ss : result) {
PersistentUserSessionAdapter userSession = (PersistentUserSessionAdapter) ss;
- List<ClientSessionModel> currentClientSessions = userSession.getClientSessions(); // This is empty now and we want to fill it
+ Map<String, AuthenticatedClientSessionModel> currentClientSessions = userSession.getAuthenticatedClientSessions(); // This is empty now and we want to fill it
boolean next = true;
while (next && j < clientSessions.size()) {
PersistentClientSessionEntity clientSession = clientSessions.get(j);
if (clientSession.getUserSessionId().equals(userSession.getId())) {
- PersistentClientSessionAdapter clientSessAdapter = toAdapter(userSession.getRealm(), userSession, clientSession);
- currentClientSessions.add(clientSessAdapter);
+ PersistentAuthenticatedClientSessionAdapter clientSessAdapter = toAdapter(userSession.getRealm(), userSession, clientSession);
+ currentClientSessions.put(clientSession.getClientId(), clientSessAdapter);
j++;
} else {
next = false;
@@ -243,6 +243,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
}
}
+
return result;
}
@@ -252,21 +253,20 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
model.setLastSessionRefresh(entity.getLastSessionRefresh());
model.setData(entity.getData());
- List<ClientSessionModel> clientSessions = new LinkedList<>();
+ Map<String, AuthenticatedClientSessionModel> clientSessions = new HashMap<>();
return new PersistentUserSessionAdapter(model, realm, user, clientSessions);
}
- private PersistentClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) {
+ private PersistentAuthenticatedClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) {
ClientModel client = realm.getClientById(entity.getClientId());
PersistentClientSessionModel model = new PersistentClientSessionModel();
- model.setClientSessionId(entity.getClientSessionId());
model.setClientId(entity.getClientId());
model.setUserSessionId(userSession.getId());
model.setUserId(userSession.getUser().getId());
model.setTimestamp(entity.getTimestamp());
model.setData(entity.getData());
- return new PersistentClientSessionAdapter(model, realm, client, userSession);
+ return new PersistentAuthenticatedClientSessionAdapter(model, realm, client, userSession);
}
@Override
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java
index 35265af..e12223d 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java
@@ -20,7 +20,6 @@ package org.keycloak.models.jpa.session;
import org.keycloak.Config;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.session.UserSessionPersisterProviderFactory;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
index 7250836..8910bca 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java
@@ -45,12 +45,10 @@ import java.io.Serializable;
public class PersistentClientSessionEntity {
@Id
- @Column(name="CLIENT_SESSION_ID", length = 36)
- protected String clientSessionId;
-
@Column(name = "USER_SESSION_ID", length = 36)
protected String userSessionId;
+ @Id
@Column(name="CLIENT_ID", length = 36)
protected String clientId;
@@ -64,14 +62,6 @@ public class PersistentClientSessionEntity {
@Column(name="DATA")
protected String data;
- public String getClientSessionId() {
- return clientSessionId;
- }
-
- public void setClientSessionId(String clientSessionId) {
- this.clientSessionId = clientSessionId;
- }
-
public String getUserSessionId() {
return userSessionId;
}
@@ -114,20 +104,27 @@ public class PersistentClientSessionEntity {
public static class Key implements Serializable {
- protected String clientSessionId;
+ protected String userSessionId;
+
+ protected String clientId;
protected String offline;
public Key() {
}
- public Key(String clientSessionId, String offline) {
- this.clientSessionId = clientSessionId;
+ public Key(String userSessionId, String clientId, String offline) {
+ this.userSessionId = userSessionId;
+ this.clientId = clientId;
this.offline = offline;
}
- public String getClientSessionId() {
- return clientSessionId;
+ public String getUserSessionId() {
+ return userSessionId;
+ }
+
+ public String getClientId() {
+ return clientId;
}
public String getOffline() {
@@ -141,7 +138,8 @@ public class PersistentClientSessionEntity {
Key key = (Key) o;
- if (this.clientSessionId != null ? !this.clientSessionId.equals(key.clientSessionId) : key.clientSessionId != null) return false;
+ if (this.userSessionId != null ? !this.userSessionId.equals(key.userSessionId) : key.userSessionId != null) return false;
+ if (this.clientId != null ? !this.clientId.equals(key.clientId) : key.clientId != null) return false;
if (this.offline != null ? !this.offline.equals(key.offline) : key.offline != null) return false;
return true;
@@ -149,7 +147,8 @@ public class PersistentClientSessionEntity {
@Override
public int hashCode() {
- int result = this.clientSessionId != null ? this.clientSessionId.hashCode() : 0;
+ int result = this.userSessionId != null ? this.userSessionId.hashCode() : 0;
+ result = 37 * result + (this.clientId != null ? this.clientId.hashCode() : 0);
result = 31 * result + (this.offline != null ? this.offline.hashCode() : 0);
return result;
}
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml
new file mode 100644
index 0000000..c453a2e
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ ~ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ ~ and other contributors as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+
+ <changeSet author="mposolda@redhat.com" id="3.2.0">
+ <dropPrimaryKey constraintName="CONSTRAINT_OFFLINE_CL_SES_PK2" tableName="OFFLINE_CLIENT_SESSION" />
+ <dropColumn tableName="OFFLINE_CLIENT_SESSION" columnName="CLIENT_SESSION_ID" />
+ <addPrimaryKey columnNames="USER_SESSION_ID,CLIENT_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK3" tableName="OFFLINE_CLIENT_SESSION"/>
+ </changeSet>
+
+</databaseChangeLog>
\ No newline at end of file
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
index 59855ec..ae7d98b 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
@@ -47,4 +47,5 @@
<include file="META-INF/jpa-changelog-2.5.0.xml"/>
<include file="META-INF/jpa-changelog-2.5.1.xml"/>
<include file="META-INF/jpa-changelog-3.0.0.xml"/>
+ <include file="META-INF/jpa-changelog-3.2.0.xml"/>
</databaseChangeLog>
diff --git a/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java b/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java
new file mode 100644
index 0000000..cf9d7d0
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models;
+
+import java.util.UUID;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public interface ActionTokenKeyModel {
+
+ /**
+ * @return ID of user which this token is for.
+ */
+ String getUserId();
+
+ /**
+ * @return Action identifier this token is for.
+ */
+ String getActionId();
+
+ /**
+ * Returns absolute number of seconds since the epoch in UTC timezone when the token expires.
+ */
+ int getExpiration();
+
+ /**
+ * @return Single-use random value used for verification whether the relevant action is allowed.
+ */
+ UUID getActionVerificationNonce();
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java b/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java
new file mode 100644
index 0000000..ba01cb6
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * This model represents contents of an action token shareable among Keycloak instances in the cluster.
+ * @author hmlnarik
+ */
+public interface ActionTokenValueModel {
+
+ /**
+ * Returns unmodifiable map of all notes.
+ * @return see description. Returns empty map if no note is set, never returns {@code null}.
+ */
+ Map<String,String> getNotes();
+
+ /**
+ * Returns value of the given note (or {@code null} when no note of this name is present)
+ * @return see description
+ */
+ String getNote(String name);
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java
new file mode 100644
index 0000000..099a39c
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models;
+
+
+import java.util.Map;
+
+import org.keycloak.sessions.CommonClientSessionModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface AuthenticatedClientSessionModel extends CommonClientSessionModel {
+
+ void setUserSession(UserSessionModel userSession);
+ UserSessionModel getUserSession();
+
+ String getNote(String name);
+ void setNote(String name, String value);
+ void removeNote(String name);
+ Map<String, String> getNotes();
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/ClientSessionModel.java b/server-spi/src/main/java/org/keycloak/models/ClientSessionModel.java
index 84fa64e..12abb10 100755
--- a/server-spi/src/main/java/org/keycloak/models/ClientSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/ClientSessionModel.java
@@ -20,14 +20,12 @@ package org.keycloak.models;
import java.util.Map;
import java.util.Set;
+import org.keycloak.sessions.CommonClientSessionModel;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public interface ClientSessionModel {
-
- public String getId();
- public RealmModel getRealm();
- public ClientModel getClient();
+public interface ClientSessionModel extends CommonClientSessionModel {
public UserSessionModel getUserSession();
public void setUserSession(UserSessionModel userSession);
@@ -35,41 +33,12 @@ public interface ClientSessionModel {
public String getRedirectUri();
public void setRedirectUri(String uri);
- public int getTimestamp();
-
- public void setTimestamp(int timestamp);
-
- public String getAction();
-
- public void setAction(String action);
-
- public Set<String> getRoles();
- public void setRoles(Set<String> roles);
-
- public Set<String> getProtocolMappers();
- public void setProtocolMappers(Set<String> protocolMappers);
-
public Map<String, ExecutionStatus> getExecutionStatus();
public void setExecutionStatus(String authenticator, ExecutionStatus status);
public void clearExecutionStatus();
public UserModel getAuthenticatedUser();
public void setAuthenticatedUser(UserModel user);
-
-
- /**
- * Authentication request type, i.e. OAUTH, SAML 2.0, SAML 1.1, etc.
- *
- * @return
- */
- public String getAuthMethod();
- public void setAuthMethod(String method);
-
- public String getNote(String name);
- public void setNote(String name, String value);
- public void removeNote(String name);
- public Map<String, String> getNotes();
-
/**
* Required actions that are attached to this client session.
*
@@ -103,28 +72,10 @@ public interface ClientSessionModel {
public void clearUserSessionNotes();
- public static enum Action {
- OAUTH_GRANT,
- CODE_TO_TOKEN,
- VERIFY_EMAIL,
- UPDATE_PROFILE,
- CONFIGURE_TOTP,
- UPDATE_PASSWORD,
- RECOVER_PASSWORD, // deprecated
- AUTHENTICATE,
- SOCIAL_CALLBACK,
- LOGGED_OUT,
- RESET_CREDENTIALS,
- EXECUTE_ACTIONS,
- REQUIRED_ACTIONS
- }
-
- public enum ExecutionStatus {
- FAILED,
- SUCCESS,
- SETUP_REQUIRED,
- ATTEMPTED,
- SKIPPED,
- CHALLENGED
- }
+ public String getNote(String name);
+ public void setNote(String name, String value);
+ public void removeNote(String name);
+ public Map<String, String> getNotes();
+
+
}
diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
index 766078d..0348b68 100755
--- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
+++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java
@@ -20,6 +20,7 @@ package org.keycloak.models;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.cache.UserCache;
import org.keycloak.provider.Provider;
+import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
import java.util.Set;
@@ -102,6 +103,9 @@ public interface KeycloakSession {
UserSessionProvider sessions();
+ AuthenticationSessionProvider authenticationSessions();
+
+
void close();
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index dc8bff5..f6484d6 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -191,6 +191,12 @@ public interface RealmModel extends RoleContainerModel {
void setAccessCodeLifespanLogin(int seconds);
+ int getActionTokenGeneratedByAdminLifespan();
+ void setActionTokenGeneratedByAdminLifespan(int seconds);
+
+ int getActionTokenGeneratedByUserLifespan();
+ void setActionTokenGeneratedByUserLifespan(int seconds);
+
List<RequiredCredentialModel> getRequiredCredentials();
void addRequiredCredential(String cred);
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
index d58c405..28a3145 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
@@ -17,7 +17,6 @@
package org.keycloak.models;
-import java.util.List;
import java.util.Map;
/**
@@ -53,7 +52,7 @@ public interface UserSessionModel {
void setLastSessionRefresh(int seconds);
- List<ClientSessionModel> getClientSessions();
+ Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions();
public String getNote(String name);
public void setNote(String name, String value);
@@ -63,8 +62,12 @@ public interface UserSessionModel {
State getState();
void setState(State state);
+ void setUser(UserModel user);
+
+ // Will completely restart whole state of user session. It will just keep same ID.
+ void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId);
+
public static enum State {
- LOGGING_IN,
LOGGED_IN,
LOGGING_OUT,
LOGGED_OUT
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
index 4102de1..d474e89 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -27,11 +27,9 @@ import java.util.List;
*/
public interface UserSessionProvider extends Provider {
- ClientSessionModel createClientSession(RealmModel realm, ClientModel client);
- ClientSessionModel getClientSession(RealmModel realm, String id);
- ClientSessionModel getClientSession(String id);
+ AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession);
- UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId);
+ UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId);
UserSessionModel getUserSession(RealmModel realm, String id);
List<UserSessionModel> getUserSessions(RealmModel realm, UserModel user);
List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client);
@@ -40,13 +38,14 @@ public interface UserSessionProvider extends Provider {
UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId);
long getActiveUserSessions(RealmModel realm, ClientModel client);
+
+ /** This will remove attached ClientLoginSessionModels too **/
void removeUserSession(RealmModel realm, UserSessionModel session);
void removeUserSessions(RealmModel realm, UserModel user);
- // Implementation should propagate removal of expired userSessions to userSessionPersister too
+ /** Implementation should propagate removal of expired userSessions to userSessionPersister too **/
void removeExpired(RealmModel realm);
void removeUserSessions(RealmModel realm);
- void removeClientSession(RealmModel realm, ClientSessionModel clientSession);
UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId);
UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId);
@@ -56,25 +55,22 @@ public interface UserSessionProvider extends Provider {
void onRealmRemoved(RealmModel realm);
void onClientRemoved(RealmModel realm, ClientModel client);
+ /** Newly created userSession won't contain attached AuthenticatedClientSessions **/
UserSessionModel createOfflineUserSession(UserSessionModel userSession);
UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId);
- // Removes the attached clientSessions as well
+ /** Removes the attached clientSessions as well **/
void removeOfflineUserSession(RealmModel realm, UserSessionModel userSession);
- ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession);
- ClientSessionModel getOfflineClientSession(RealmModel realm, String clientSessionId);
- List<ClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user);
-
- // Don't remove userSession even if it's last userSession
- void removeOfflineClientSession(RealmModel realm, String clientSessionId);
+ /** Will automatically attach newly created offline client session to the offlineUserSession **/
+ AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedClientSessionModel clientSession, UserSessionModel offlineUserSession);
+ List<UserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user);
long getOfflineSessionsCount(RealmModel realm, ClientModel client);
List<UserSessionModel> getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max);
- // Triggered by persister during pre-load
- UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline);
- ClientSessionModel importClientSession(ClientSessionModel persistentClientSession, boolean offline);
+ /** Triggered by persister during pre-load. It optionally imports authenticatedClientSessions too if requested. Otherwise the imported UserSession will have empty list of AuthenticationSessionModel **/
+ UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline, boolean importAuthenticatedClientSessions);
ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count);
ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id);
diff --git a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java
new file mode 100644
index 0000000..8e84f1a
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.sessions;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+
+/**
+ * Using class for now to avoid many updates among implementations
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface AuthenticationSessionModel extends CommonClientSessionModel {
+
+//
+// public UserSessionModel getUserSession();
+// public void setUserSession(UserSessionModel userSession);
+
+
+ Map<String, ExecutionStatus> getExecutionStatus();
+ void setExecutionStatus(String authenticator, ExecutionStatus status);
+ void clearExecutionStatus();
+ UserModel getAuthenticatedUser();
+ void setAuthenticatedUser(UserModel user);
+
+ /**
+ * Required actions that are attached to this client session.
+ *
+ * @return
+ */
+ Set<String> getRequiredActions();
+
+ void addRequiredAction(String action);
+
+ void removeRequiredAction(String action);
+
+ void addRequiredAction(UserModel.RequiredAction action);
+
+ void removeRequiredAction(UserModel.RequiredAction action);
+
+
+ /**
+ * Sets the given user session note to the given value. User session notes are notes
+ * you want be applied to the UserSessionModel when the client session is attached to it.
+ */
+ void setUserSessionNote(String name, String value);
+ /**
+ * Retrieves value of given user session note. User session notes are notes
+ * you want be applied to the UserSessionModel when the client session is attached to it.
+ */
+ Map<String, String> getUserSessionNotes();
+ /**
+ * Clears all user session notes. User session notes are notes
+ * you want be applied to the UserSessionModel when the client session is attached to it.
+ */
+ void clearUserSessionNotes();
+
+ /**
+ * Retrieves value of the given authentication note to the given value. Authentication notes are notes
+ * used typically by authenticators and authentication flows. They are cleared when
+ * authentication session is restarted
+ */
+ String getAuthNote(String name);
+ /**
+ * Sets the given authentication note to the given value. Authentication notes are notes
+ * used typically by authenticators and authentication flows. They are cleared when
+ * authentication session is restarted
+ */
+ void setAuthNote(String name, String value);
+ /**
+ * Removes the given authentication note. Authentication notes are notes
+ * used typically by authenticators and authentication flows. They are cleared when
+ * authentication session is restarted
+ */
+ void removeAuthNote(String name);
+ /**
+ * Clears all authentication note. Authentication notes are notes
+ * used typically by authenticators and authentication flows. They are cleared when
+ * authentication session is restarted
+ */
+ void clearAuthNotes();
+
+ /**
+ * Retrieves value of the given client note to the given value. Client notes are notes
+ * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ */
+ String getClientNote(String name);
+ /**
+ * Sets the given client note to the given value. Client notes are notes
+ * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ */
+ void setClientNote(String name, String value);
+ /**
+ * Removes the given client note. Client notes are notes
+ * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ */
+ void removeClientNote(String name);
+ /**
+ * Retrieves the (name, value) map of client notes. Client notes are notes
+ * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ */
+ Map<String, String> getClientNotes();
+ /**
+ * Clears all client notes. Client notes are notes
+ * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ */
+ void clearClientNotes();
+
+ void updateClient(ClientModel client);
+
+ // Will completely restart whole state of authentication session. It will just keep same ID. It will setup it with provided realm and client.
+ void restartSession(RealmModel realm, ClientModel client);
+}
diff --git a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
new file mode 100644
index 0000000..99806d4
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.sessions;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.Provider;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface AuthenticationSessionProvider extends Provider {
+
+ // Generates random ID
+ AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client);
+
+ AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client);
+
+ AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId);
+
+ void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authenticationSession);
+
+ void removeExpired(RealmModel realm);
+ void onRealmRemoved(RealmModel realm);
+ void onClientRemoved(RealmModel realm, ClientModel client);
+
+ /**
+ * Requests update of authNotes of an authentication session that is not owned
+ * by this instance but might exist somewhere in the cluster.
+ *
+ * @param authSessionId
+ * @param authNotesFragment Map with authNote values. Auth note is removed if the corresponding value in the map is {@code null}.
+ */
+ void updateNonlocalSessionAuthNotes(String authSessionId, Map<String, String> authNotesFragment);
+
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java
new file mode 100644
index 0000000..c47a6a5
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.sessions;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+
+/**
+ * Predecesor of AuthenticationSessionModel, ClientLoginSessionModel and ClientSessionModel (then action tickets). Maybe we will remove it later...
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface CommonClientSessionModel {
+
+ public String getRedirectUri();
+ public void setRedirectUri(String uri);
+
+ public String getId();
+ public RealmModel getRealm();
+ public ClientModel getClient();
+
+ public int getTimestamp();
+ public void setTimestamp(int timestamp);
+
+ public String getAction();
+ public void setAction(String action);
+
+ public String getProtocol();
+ public void setProtocol(String method);
+
+ // TODO: Not needed here...?
+ public Set<String> getRoles();
+ public void setRoles(Set<String> roles);
+
+ // TODO: Not needed here...?
+ public Set<String> getProtocolMappers();
+ public void setProtocolMappers(Set<String> protocolMappers);
+
+ public static enum Action {
+ OAUTH_GRANT,
+ CODE_TO_TOKEN,
+ AUTHENTICATE,
+ LOGGED_OUT,
+ REQUIRED_ACTIONS
+ }
+
+ public enum ExecutionStatus {
+ FAILED,
+ SUCCESS,
+ SETUP_REQUIRED,
+ ATTEMPTED,
+ SKIPPED,
+ CHALLENGED
+ }
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
index 98632fb..fb6e029 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
@@ -18,10 +18,10 @@
package org.keycloak.authentication;
import org.keycloak.forms.login.LoginFormsProvider;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
+import org.keycloak.sessions.AuthenticationSessionModel;
import java.net.URI;
@@ -62,7 +62,7 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
*
* @return
*/
- ClientSessionModel getClientSession();
+ AuthenticationSessionModel getAuthenticationSession();
/**
* Create a Freemarker form builder that presets the user, action URI, and a generated access code
@@ -80,11 +80,19 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
URI getActionUrl(String code);
/**
- * Get the action URL for the required action. This auto-generates the access code.
+ * Get the action URL for the action token executor.
*
+ * @param tokenString String representation (JWT) of action token
* @return
*/
- URI getActionUrl();
+ URI getActionTokenUrl(String tokenString);
+
+ /**
+ * Get the refresh URL for the required action.
+ *
+ * @return
+ */
+ URI getRefreshExecutionUrl();
/**
* End the flow and redirect browser based on protocol specific respones. This should only be executed
@@ -100,6 +108,12 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
void resetFlow();
/**
+ * Reset the current flow to the beginning and restarts it. Allows to add additional listener, which is triggered after flow restarted
+ *
+ */
+ void resetFlow(Runnable afterResetListener);
+
+ /**
* Fork the current flow. The client session will be cloned and set to point at the realm's browser login flow. The Response will be the result
* of this fork. The previous flow will still be set at the current execution. This is used by reset password when it sends an email.
* It sends an email linking to the current flow and redirects the browser to a new browser login flow.
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/FormContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/FormContext.java
index 7c7d143..2c1255d 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/FormContext.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/FormContext.java
@@ -22,10 +22,10 @@ import org.keycloak.common.ClientConnection;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.UriInfo;
@@ -79,11 +79,11 @@ public interface FormContext {
RealmModel getRealm();
/**
- * ClientSessionModel attached to this flow
+ * AuthenticationSessionModel attached to this flow
*
* @return
*/
- ClientSessionModel getClientSession();
+ AuthenticationSessionModel getAuthenticationSession();
/**
* Information about the IP address from the connecting HTTP client.
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java
index 3ece79e..caaa14e 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java
@@ -21,11 +21,10 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.EventBuilder;
import org.keycloak.forms.login.LoginFormsProvider;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
@@ -90,8 +89,7 @@ public interface RequiredActionContext {
*/
UserModel getUser();
RealmModel getRealm();
- ClientSessionModel getClientSession();
- UserSessionModel getUserSession();
+ AuthenticationSessionModel getAuthenticationSession();
ClientConnection getConnection();
UriInfo getUriInfo();
KeycloakSession getSession();
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionFactory.java b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionFactory.java
index 76dc582..517b5f4 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionFactory.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionFactory.java
@@ -35,4 +35,13 @@ public interface RequiredActionFactory extends ProviderFactory<RequiredActionPro
* @return
*/
String getDisplayText();
+
+ /**
+ * Flag indicating whether the execution of the required action by the same circumstances
+ * (e.g. by one and the same action token) should only be permitted once.
+ * @return
+ */
+ default boolean isOneTimeAction() {
+ return false;
+ }
}
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
index 8f57133..0320299 100755
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
@@ -17,12 +17,12 @@
package org.keycloak.broker.provider;
import org.keycloak.events.EventBuilder;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
@@ -75,7 +75,7 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
}
@Override
- public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) {
+ public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
}
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java
index 863decb..ba8276f 100644
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java
@@ -17,9 +17,9 @@
package org.keycloak.broker.provider;
import org.jboss.resteasy.spi.HttpRequest;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.UriInfo;
@@ -34,16 +34,16 @@ public class AuthenticationRequest {
private final HttpRequest httpRequest;
private final RealmModel realm;
private final String redirectUri;
- private final ClientSessionModel clientSession;
+ private final AuthenticationSessionModel authSession;
- public AuthenticationRequest(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession, HttpRequest httpRequest, UriInfo uriInfo, String state, String redirectUri) {
+ public AuthenticationRequest(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession, HttpRequest httpRequest, UriInfo uriInfo, String state, String redirectUri) {
this.session = session;
this.realm = realm;
this.httpRequest = httpRequest;
this.uriInfo = uriInfo;
this.state = state;
this.redirectUri = redirectUri;
- this.clientSession = clientSession;
+ this.authSession = authSession;
}
public KeycloakSession getSession() {
@@ -76,7 +76,7 @@ public class AuthenticationRequest {
return this.redirectUri;
}
- public ClientSessionModel getClientSession() {
- return this.clientSession;
+ public AuthenticationSessionModel getAuthenticationSession() {
+ return this.authSession;
}
}
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java
index f2c8a7a..a2b1dc5 100755
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java
@@ -16,9 +16,9 @@
*/
package org.keycloak.broker.provider;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.sessions.AuthenticationSessionModel;
import java.util.ArrayList;
import java.util.HashMap;
@@ -46,7 +46,7 @@ public class BrokeredIdentityContext {
private IdentityProviderModel idpConfig;
private IdentityProvider idp;
private Map<String, Object> contextData = new HashMap<>();
- private ClientSessionModel clientSession;
+ private AuthenticationSessionModel authenticationSession;
public BrokeredIdentityContext(String id) {
if (id == null) {
@@ -190,12 +190,12 @@ public class BrokeredIdentityContext {
this.lastName = lastName;
}
- public ClientSessionModel getClientSession() {
- return clientSession;
+ public AuthenticationSessionModel getAuthenticationSession() {
+ return authenticationSession;
}
- public void setClientSession(ClientSessionModel clientSession) {
- this.clientSession = clientSession;
+ public void setAuthenticationSession(AuthenticationSessionModel authenticationSession) {
+ this.authenticationSession = authenticationSession;
}
public void setName(String name) {
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java
index b14572e..c4f1c5c 100755
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java
@@ -17,7 +17,6 @@
package org.keycloak.broker.provider;
import org.keycloak.events.EventBuilder;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
@@ -25,6 +24,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.Provider;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
@@ -51,7 +51,7 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context);
- void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context);
+ void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context);
void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context);
void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context);
diff --git a/server-spi-private/src/main/java/org/keycloak/events/Details.java b/server-spi-private/src/main/java/org/keycloak/events/Details.java
index 0ef227d..5e3a9b3 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/Details.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/Details.java
@@ -25,6 +25,7 @@ public interface Details {
String EMAIL = "email";
String PREVIOUS_EMAIL = "previous_email";
String UPDATED_EMAIL = "updated_email";
+ String ACTION = "action";
String CODE_ID = "code_id";
String REDIRECT_URI = "redirect_uri";
String RESPONSE_TYPE = "response_type";
@@ -63,4 +64,6 @@ public interface Details {
String CLIENT_REGISTRATION_POLICY = "client_registration_policy";
+ String EXISTING_USER = "previous_user";
+
}
diff --git a/server-spi-private/src/main/java/org/keycloak/events/Errors.java b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
index e82421f..ab954bc 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/Errors.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
@@ -37,6 +37,7 @@ public interface Errors {
String USER_DISABLED = "user_disabled";
String USER_TEMPORARILY_DISABLED = "user_temporarily_disabled";
String INVALID_USER_CREDENTIALS = "invalid_user_credentials";
+ String DIFFERENT_USER_AUTHENTICATED = "different_user_authenticated";
String USERNAME_MISSING = "username_missing";
String USERNAME_IN_USE = "username_in_use";
diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
index f77137f..920646f 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
@@ -79,6 +79,9 @@ public enum EventType {
RESET_PASSWORD(true),
RESET_PASSWORD_ERROR(true),
+ RESTART_AUTHENTICATION(true),
+ RESTART_AUTHENTICATION_ERROR(true),
+
INVALID_SIGNATURE(false),
INVALID_SIGNATURE_ERROR(false),
REGISTER_NODE(false),
@@ -89,6 +92,8 @@ public enum EventType {
USER_INFO_REQUEST(false),
USER_INFO_REQUEST_ERROR(false),
+ IDENTITY_PROVIDER_LINK_ACCOUNT(true),
+ IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR(true),
IDENTITY_PROVIDER_LOGIN(false),
IDENTITY_PROVIDER_LOGIN_ERROR(false),
IDENTITY_PROVIDER_FIRST_LOGIN(true),
@@ -105,6 +110,8 @@ public enum EventType {
CUSTOM_REQUIRED_ACTION_ERROR(true),
EXECUTE_ACTIONS(true),
EXECUTE_ACTIONS_ERROR(true),
+ EXECUTE_ACTION_TOKEN(true),
+ EXECUTE_ACTION_TOKEN_ERROR(true),
CLIENT_INFO(false),
CLIENT_INFO_ERROR(false),
@@ -124,6 +131,10 @@ public enum EventType {
this.saveByDefault = saveByDefault;
}
+ /**
+ * Determines whether this event is stored when the admin has not set a specific set of event types to save.
+ * @return
+ */
public boolean isSaveByDefault() {
return saveByDefault;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java
index fcaff7a..476e3aa 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java
@@ -24,6 +24,6 @@ public enum LoginFormsPages {
LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL,
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
- OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE, CODE;
+ OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE, LOGIN_PAGE_EXPIRED, CODE;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
index f16f0c2..32195ef 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
@@ -17,12 +17,12 @@
package org.keycloak.forms.login;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.provider.Provider;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@@ -68,16 +68,16 @@ public interface LoginFormsProvider extends Provider {
public Response createIdpLinkEmailPage();
+ public Response createLoginExpiredPage();
+
public Response createErrorPage();
- public Response createOAuthGrant(ClientSessionModel clientSessionModel);
+ public Response createOAuthGrant();
public Response createCode();
public LoginFormsProvider setClientSessionCode(String accessCode);
- public LoginFormsProvider setClientSession(ClientSessionModel clientSession);
-
public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappers);
public LoginFormsProvider setAccessRequest(String message);
diff --git a/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProvider.java b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProvider.java
new file mode 100644
index 0000000..4e4a8db
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProvider.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models;
+
+import org.keycloak.provider.Provider;
+
+import java.util.Map;
+
+/**
+ * Internal action token store provider.
+ * @author hmlnarik
+ */
+public interface ActionTokenStoreProvider extends Provider {
+
+ /**
+ * Adds a given token to token store.
+ * @param actionTokenKey key
+ * @param notes Optional notes to be stored with the token. Can be {@code null} in which case it is treated as an empty map.
+ */
+ void put(ActionTokenKeyModel actionTokenKey, Map<String, String> notes);
+
+ /**
+ * Returns token corresponding to the given key from the internal action token store
+ * @param key key
+ * @return {@code null} if no token is found for given key and nonce, value otherwise
+ */
+ ActionTokenValueModel get(ActionTokenKeyModel key);
+
+ /**
+ * Removes token corresponding to the given key from the internal action token store, and returns the stored value
+ * @param key key
+ * @param nonce nonce that must match a given key
+ * @return {@code null} if no token is found for given key and nonce, value otherwise
+ */
+ ActionTokenValueModel remove(ActionTokenKeyModel key);
+
+ void removeAll(String userId, String actionId);
+
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java
new file mode 100644
index 0000000..26d086d
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public interface ActionTokenStoreProviderFactory extends ProviderFactory<ActionTokenStoreProvider> {
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreSpi.java b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreSpi.java
new file mode 100644
index 0000000..66ee518
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreSpi.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models;
+
+import org.keycloak.provider.*;
+
+/**
+ * SPI for action tokens.
+ *
+ * @author hmlnarik
+ */
+public class ActionTokenStoreSpi implements Spi {
+
+ public static final String NAME = "actionToken";
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return ActionTokenStoreProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return ActionTokenStoreProviderFactory.class;
+ }
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java b/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java
index f5e58d3..10b28a2 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java
@@ -18,8 +18,8 @@
package org.keycloak.models.session;
import org.keycloak.Config;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
@@ -70,7 +70,7 @@ public class DisabledUserSessionPersisterProvider implements UserSessionPersiste
}
@Override
- public void createClientSession(ClientSessionModel clientSession, boolean offline) {
+ public void createClientSession(AuthenticatedClientSessionModel clientSession, boolean offline) {
}
@@ -85,7 +85,7 @@ public class DisabledUserSessionPersisterProvider implements UserSessionPersiste
}
@Override
- public void removeClientSession(String clientSessionId, boolean offline) {
+ public void removeClientSession(String userSessionId, String clientUUID, boolean offline) {
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java
index 5990eea..ee33fed 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java
@@ -22,20 +22,12 @@ package org.keycloak.models.session;
*/
public class PersistentClientSessionModel {
- private String clientSessionId;
private String userSessionId;
private String clientId;
private String userId;
private int timestamp;
private String data;
- public String getClientSessionId() {
- return clientSessionId;
- }
-
- public void setClientSessionId(String clientSessionId) {
- this.clientSessionId = clientSessionId;
- }
public String getUserSessionId() {
return userSessionId;
diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
index 6047be2..170d381 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
@@ -18,7 +18,7 @@
package org.keycloak.models.session;
import com.fasterxml.jackson.annotation.JsonProperty;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@@ -27,7 +27,6 @@ import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
/**
@@ -38,7 +37,7 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
private final PersistentUserSessionModel model;
private final UserModel user;
private final RealmModel realm;
- private final List<ClientSessionModel> clientSessions;
+ private final Map<String, AuthenticatedClientSessionModel> authenticatedClientSessions;
private PersistentUserSessionData data;
@@ -59,14 +58,14 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
this.user = other.getUser();
this.realm = other.getRealm();
- this.clientSessions = other.getClientSessions();
+ this.authenticatedClientSessions = other.getAuthenticatedClientSessions();
}
- public PersistentUserSessionAdapter(PersistentUserSessionModel model, RealmModel realm, UserModel user, List<ClientSessionModel> clientSessions) {
+ public PersistentUserSessionAdapter(PersistentUserSessionModel model, RealmModel realm, UserModel user, Map<String, AuthenticatedClientSessionModel> clientSessions) {
this.model = model;
this.realm = realm;
this.user = user;
- this.clientSessions = clientSessions;
+ this.authenticatedClientSessions = clientSessions;
}
// Lazily init data
@@ -115,6 +114,11 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
}
@Override
+ public void setUser(UserModel user) {
+ throw new IllegalStateException("Not supported");
+ }
+
+ @Override
public RealmModel getRealm() {
return realm;
}
@@ -155,8 +159,8 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
}
@Override
- public List<ClientSessionModel> getClientSessions() {
- return clientSessions;
+ public Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions() {
+ return authenticatedClientSessions;
}
@Override
@@ -197,6 +201,11 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
}
@Override
+ public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
+ throw new IllegalStateException("Not supported");
+ }
+
+ @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof UserSessionModel)) return false;
diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java
index c0d033a..ba5a595 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java
@@ -17,8 +17,8 @@
package org.keycloak.models.session;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
@@ -35,7 +35,7 @@ public interface UserSessionPersisterProvider extends Provider {
void createUserSession(UserSessionModel userSession, boolean offline);
// Assuming that corresponding userSession is already persisted
- void createClientSession(ClientSessionModel clientSession, boolean offline);
+ void createClientSession(AuthenticatedClientSessionModel clientSession, boolean offline);
void updateUserSession(UserSessionModel userSession, boolean offline);
@@ -43,7 +43,7 @@ public interface UserSessionPersisterProvider extends Provider {
void removeUserSession(String userSessionId, boolean offline);
// Called during revoke. It will remove userSession too if this was last clientSession attached to it
- void removeClientSession(String clientSessionId, boolean offline);
+ void removeClientSession(String userSessionId, String clientUUID, boolean offline);
void onRealmRemoved(RealmModel realm);
void onClientRemoved(RealmModel realm, ClientModel client);
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index e901929..43e8ef8 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -45,8 +45,8 @@ import org.keycloak.events.admin.AuthDetails;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
@@ -303,6 +303,8 @@ public class ModelToRepresentation {
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
+ rep.setActionTokenGeneratedByAdminLifespan(realm.getActionTokenGeneratedByAdminLifespan());
+ rep.setActionTokenGeneratedByUserLifespan(realm.getActionTokenGeneratedByUserLifespan());
rep.setSmtpServer(new HashMap<>(realm.getSmtpConfig()));
rep.setBrowserSecurityHeaders(realm.getBrowserSecurityHeaders());
rep.setAccountTheme(realm.getAccountTheme());
@@ -485,7 +487,7 @@ public class ModelToRepresentation {
rep.setUsername(session.getUser().getUsername());
rep.setUserId(session.getUser().getId());
rep.setIpAddress(session.getIpAddress());
- for (ClientSessionModel clientSession : session.getClientSessions()) {
+ for (AuthenticatedClientSessionModel clientSession : session.getAuthenticatedClientSessions().values()) {
ClientModel client = clientSession.getClient();
rep.getClients().put(client.getId(), client.getClientId());
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index b427477..1c90b0f 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -189,6 +189,14 @@ public class RepresentationToModel {
newRealm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin());
else newRealm.setAccessCodeLifespanLogin(1800);
+ if (rep.getActionTokenGeneratedByAdminLifespan() != null)
+ newRealm.setActionTokenGeneratedByAdminLifespan(rep.getActionTokenGeneratedByAdminLifespan());
+ else newRealm.setActionTokenGeneratedByAdminLifespan(12 * 60 * 60);
+
+ if (rep.getActionTokenGeneratedByUserLifespan() != null)
+ newRealm.setActionTokenGeneratedByUserLifespan(rep.getActionTokenGeneratedByUserLifespan());
+ else newRealm.setActionTokenGeneratedByUserLifespan(newRealm.getAccessCodeLifespanUserAction());
+
if (rep.getSslRequired() != null)
newRealm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase()));
if (rep.isRegistrationAllowed() != null) newRealm.setRegistrationAllowed(rep.isRegistrationAllowed());
@@ -812,6 +820,10 @@ public class RepresentationToModel {
realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
if (rep.getAccessCodeLifespanLogin() != null)
realm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin());
+ if (rep.getActionTokenGeneratedByAdminLifespan() != null)
+ realm.setActionTokenGeneratedByAdminLifespan(rep.getActionTokenGeneratedByAdminLifespan());
+ if (rep.getActionTokenGeneratedByUserLifespan() != null)
+ realm.setActionTokenGeneratedByUserLifespan(rep.getActionTokenGeneratedByUserLifespan());
if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore());
if (rep.getRevokeRefreshToken() != null) realm.setRevokeRefreshToken(rep.getRevokeRefreshToken());
if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
diff --git a/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java b/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java
index 086a8ed..569a2c0 100755
--- a/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java
+++ b/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java
@@ -18,12 +18,12 @@
package org.keycloak.protocol;
import org.keycloak.events.EventBuilder;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.Provider;
-import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
@@ -66,19 +66,19 @@ public interface LoginProtocol extends Provider {
LoginProtocol setEventBuilder(EventBuilder event);
- Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode);
+ Response authenticated(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
- Response sendError(ClientSessionModel clientSession, Error error);
+ Response sendError(AuthenticationSessionModel authSession, Error error);
- void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession);
- Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession);
+ void backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
+ Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
Response finishLogout(UserSessionModel userSession);
/**
* @param userSession
- * @param clientSession
+ * @param authSession
* @return true if SSO cookie authentication can't be used. User will need to "actively" reauthenticate
*/
- boolean requireReauthentication(UserSessionModel userSession, ClientSessionModel clientSession);
+ boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession);
}
diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java
new file mode 100644
index 0000000..b182458
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.sessions;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface AuthenticationSessionProviderFactory extends ProviderFactory<AuthenticationSessionProvider> {
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionSpi.java b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionSpi.java
new file mode 100644
index 0000000..459350e
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionSpi.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.sessions;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AuthenticationSessionSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "authenticationSessions";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return AuthenticationSessionProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return AuthenticationSessionProviderFactory.class;
+ }
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProvider.java b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProvider.java
new file mode 100644
index 0000000..69dad56
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.sessions;
+
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface StickySessionEncoderProvider extends Provider {
+
+ String encodeSessionId(String sessionId);
+
+ String decodeSessionId(String encodedSessionId);
+
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProviderFactory.java
new file mode 100644
index 0000000..6b8c836
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProviderFactory.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.sessions;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface StickySessionEncoderProviderFactory extends ProviderFactory<StickySessionEncoderProvider> {
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderSpi.java b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderSpi.java
new file mode 100644
index 0000000..4e1fdff
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderSpi.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.sessions;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class StickySessionEncoderSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "stickySessionEncoder";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return StickySessionEncoderProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return StickySessionEncoderProviderFactory.class;
+ }
+}
diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 9397536..543ef25 100755
--- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -19,6 +19,7 @@ org.keycloak.provider.ExceptionConverterSpi
org.keycloak.storage.UserStorageProviderSpi
org.keycloak.storage.federated.UserFederatedStorageProviderSpi
org.keycloak.models.RealmSpi
+org.keycloak.models.ActionTokenStoreSpi
org.keycloak.models.UserSessionSpi
org.keycloak.models.UserSpi
org.keycloak.models.session.UserSessionPersisterSpi
@@ -32,6 +33,8 @@ org.keycloak.timer.TimerSpi
org.keycloak.scripting.ScriptingSpi
org.keycloak.services.managers.BruteForceProtectorSpi
org.keycloak.services.resource.RealmResourceSPI
+org.keycloak.sessions.AuthenticationSessionSpi
+org.keycloak.sessions.StickySessionEncoderSpi
org.keycloak.protocol.ClientInstallationSpi
org.keycloak.protocol.LoginProtocolSpi
org.keycloak.protocol.ProtocolMapperSpi
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java b/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java
new file mode 100644
index 0000000..52d94d9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.events.EventType;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public abstract class AbstractActionTokenHander<T extends DefaultActionToken> implements ActionTokenHandler<T>, ActionTokenHandlerFactory<T> {
+
+ private final String id;
+ private final Class<T> tokenClass;
+ private final String defaultErrorMessage;
+ private final EventType defaultEventType;
+ private final String defaultEventError;
+
+ public AbstractActionTokenHander(String id, Class<T> tokenClass, String defaultErrorMessage, EventType defaultEventType, String defaultEventError) {
+ this.id = id;
+ this.tokenClass = tokenClass;
+ this.defaultErrorMessage = defaultErrorMessage;
+ this.defaultEventType = defaultEventType;
+ this.defaultEventError = defaultEventError;
+ }
+
+ @Override
+ public ActionTokenHandler<T> create(KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public String getId() {
+ return this.id;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public Class<T> getTokenClass() {
+ return this.tokenClass;
+ }
+
+ @Override
+ public EventType eventType() {
+ return this.defaultEventType;
+ }
+
+ @Override
+ public String getDefaultErrorMessage() {
+ return this.defaultErrorMessage;
+ }
+
+ @Override
+ public String getDefaultEventError() {
+ return this.defaultEventError;
+ }
+
+ @Override
+ public String getAuthenticationSessionIdFromToken(T token) {
+ return token == null ? null : token.getAuthenticationSessionId();
+ }
+
+ @Override
+ public AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext<T> tokenContext) {
+ AuthenticationSessionModel authSession = tokenContext.createAuthenticationSessionForClient(token.getIssuedFor());
+ authSession.setAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
+ return authSession;
+ }
+
+ @Override
+ public boolean canUseTokenRepeatedly(T token, ActionTokenContext<T> tokenContext) {
+ return true;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
new file mode 100644
index 0000000..a55c587
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken;
+
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.*;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.services.Urls;
+import org.keycloak.services.managers.AuthenticationSessionManager;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import java.util.function.Function;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilderException;
+import javax.ws.rs.core.UriInfo;
+import org.jboss.resteasy.spi.HttpRequest;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ActionTokenContext<T extends JsonWebToken> {
+
+ @FunctionalInterface
+ public interface ProcessAuthenticateFlow {
+ Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor);
+ };
+
+ @FunctionalInterface
+ public interface ProcessBrokerFlow {
+ Response brokerLoginFlow(String code, String execution, String flowPath);
+ };
+
+ private final KeycloakSession session;
+ private final RealmModel realm;
+ private final UriInfo uriInfo;
+ private final ClientConnection clientConnection;
+ private final HttpRequest request;
+ private EventBuilder event;
+ private final ActionTokenHandler<T> handler;
+ private AuthenticationSessionModel authenticationSession;
+ private boolean authenticationSessionFresh;
+ private String executionId;
+ private final ProcessAuthenticateFlow processAuthenticateFlow;
+ private final ProcessBrokerFlow processBrokerFlow;
+
+ public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo,
+ ClientConnection clientConnection, HttpRequest request,
+ EventBuilder event, ActionTokenHandler<T> handler, String executionId,
+ ProcessAuthenticateFlow processFlow, ProcessBrokerFlow processBrokerFlow) {
+ this.session = session;
+ this.realm = realm;
+ this.uriInfo = uriInfo;
+ this.clientConnection = clientConnection;
+ this.request = request;
+ this.event = event;
+ this.handler = handler;
+ this.executionId = executionId;
+ this.processAuthenticateFlow = processFlow;
+ this.processBrokerFlow = processBrokerFlow;
+ }
+
+ public EventBuilder getEvent() {
+ return event;
+ }
+
+ public void setEvent(EventBuilder event) {
+ this.event = event;
+ }
+
+ public KeycloakSession getSession() {
+ return session;
+ }
+
+ public RealmModel getRealm() {
+ return realm;
+ }
+
+ public UriInfo getUriInfo() {
+ return uriInfo;
+ }
+
+ public ClientConnection getClientConnection() {
+ return clientConnection;
+ }
+
+ public HttpRequest getRequest() {
+ return request;
+ }
+
+ public AuthenticationSessionModel createAuthenticationSessionForClient(String clientId)
+ throws UriBuilderException, IllegalArgumentException {
+ AuthenticationSessionModel authSession;
+
+ // set up the account service as the endpoint to call.
+ ClientModel client = realm.getClientByClientId(clientId == null ? Constants.ACCOUNT_MANAGEMENT_CLIENT_ID : clientId);
+
+ authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
+ authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
+ authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
+ authSession.setRedirectUri(redirectUri);
+ authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
+ authSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
+ authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+
+ return authSession;
+ }
+
+ public boolean isAuthenticationSessionFresh() {
+ return authenticationSessionFresh;
+ }
+
+ public AuthenticationSessionModel getAuthenticationSession() {
+ return authenticationSession;
+ }
+
+ public void setAuthenticationSession(AuthenticationSessionModel authenticationSession, boolean isFresh) {
+ this.authenticationSession = authenticationSession;
+ this.authenticationSessionFresh = isFresh;
+ if (this.event != null) {
+ ClientModel client = authenticationSession == null ? null : authenticationSession.getClient();
+ this.event.client((String) (client == null ? null : client.getClientId()));
+ }
+ }
+
+ public ActionTokenHandler<T> getHandler() {
+ return handler;
+ }
+
+ public String getExecutionId() {
+ return executionId;
+ }
+
+ public void setExecutionId(String executionId) {
+ this.executionId = executionId;
+ }
+
+ public Response processFlow(boolean action, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
+ return processAuthenticateFlow.processFlow(action, getExecutionId(), getAuthenticationSession(), flowPath, flow, errorMessage, processor);
+ }
+
+ public Response brokerFlow(String code, String flowPath) {
+ return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), flowPath);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
new file mode 100644
index 0000000..f8d02d3
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken;
+
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.provider.Provider;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import javax.ws.rs.core.Response;
+
+/**
+ * Handler of the action token.
+ *
+ * @param <T> Class implementing the action token
+ *
+ * @author hmlnarik
+ */
+public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
+
+ /**
+ * Performs the action as per the token details. This method is only called if all verifiers
+ * returned in {@link #handleToken} succeed.
+ *
+ * @param token
+ * @param tokenContext
+ * @return
+ */
+ Response handleToken(T token, ActionTokenContext<T> tokenContext);
+
+ /**
+ * Returns the Java token class for use with deserialization.
+ * @return
+ */
+ Class<T> getTokenClass();
+
+ /**
+ * Returns an array of verifiers that are tested prior to handling the token. All verifiers have to pass successfully
+ * for token to be handled. The returned array must not be {@code null}.
+ * @param tokenContext
+ * @return Verifiers or an empty array. The returned array must not be {@code null}.
+ */
+ default Predicate<? super T>[] getVerifiers(ActionTokenContext<T> tokenContext) {
+ return new Predicate[] {};
+ }
+
+ /**
+ * Returns an authentication session ID requested from within the given token
+ * @param token Token. Can be {@code null}
+ * @return authentication session ID
+ */
+ String getAuthenticationSessionIdFromToken(T token);
+
+ /**
+ * Returns a event type logged with {@link EventBuilder} class.
+ * @return
+ */
+ EventType eventType();
+
+ /**
+ * Returns an error to be shown in the {@link EventBuilder} detail when token handling fails and
+ * no more specific error is provided.
+ * @return
+ */
+ String getDefaultEventError();
+
+ /**
+ * Returns an error to be shown in the response when token handling fails and no more specific
+ * error message is provided.
+ * @return
+ */
+ String getDefaultErrorMessage();
+
+ /**
+ * Creates a fresh authentication session according to the information from the token. The default
+ * implementation creates a new authentication session that requests termination after required actions.
+ * @param token
+ * @param tokenContext
+ * @return
+ */
+ AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext<T> tokenContext);
+
+ /**
+ * Returns {@code true} when the token can be used repeatedly to invoke the action, {@code false} when the token
+ * is intended to be for single use only.
+ * @return see above
+ */
+ boolean canUseTokenRepeatedly(T token, ActionTokenContext<T> tokenContext);
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerFactory.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerFactory.java
new file mode 100644
index 0000000..3ca3c17
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken;
+
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.representations.JsonWebToken;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public interface ActionTokenHandlerFactory<T extends JsonWebToken> extends ProviderFactory<ActionTokenHandler<T>> {
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerSpi.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerSpi.java
new file mode 100644
index 0000000..4a82ced
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerSpi.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ActionTokenHandlerSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+ private static final String NAME = "actionTokenHandler";
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return ActionTokenHandler.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return ActionTokenHandlerFactory.class;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java
new file mode 100644
index 0000000..ba44880
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.actiontoken;
+
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.common.VerificationException;
+
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.models.*;
+import org.keycloak.services.Urls;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.*;
+import javax.ws.rs.core.UriInfo;
+
+/**
+ * Part of action token that is intended to be used e.g. in link sent in password-reset email.
+ * The token encapsulates user, expected action and its time of expiry.
+ *
+ * @author hmlnarik
+ */
+public class DefaultActionToken extends DefaultActionTokenKey implements ActionTokenValueModel {
+
+ public static final String JSON_FIELD_AUTHENTICATION_SESSION_ID = "asid";
+
+ public static final Predicate<DefaultActionToken> ACTION_TOKEN_BASIC_CHECKS = t -> {
+ if (t.getActionVerificationNonce() == null) {
+ throw new VerificationException("Nonce not present.");
+ }
+
+ return true;
+ };
+
+ /**
+ * Single-use random value used for verification whether the relevant action is allowed.
+ */
+ public DefaultActionToken() {
+ super(null, null, 0, null);
+ }
+
+ /**
+ *
+ * @param userId User ID
+ * @param actionId Action ID
+ * @param absoluteExpirationInSecs Absolute expiration time in seconds in timezone of Keycloak.
+ * @param actionVerificationNonce
+ */
+ protected DefaultActionToken(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce) {
+ super(userId, actionId, absoluteExpirationInSecs, actionVerificationNonce);
+ }
+
+ /**
+ *
+ * @param userId User ID
+ * @param actionId Action ID
+ * @param absoluteExpirationInSecs Absolute expiration time in seconds in timezone of Keycloak.
+ * @param actionVerificationNonce
+ */
+ protected DefaultActionToken(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce, String authenticationSessionId) {
+ super(userId, actionId, absoluteExpirationInSecs, actionVerificationNonce);
+ setAuthenticationSessionId(authenticationSessionId);
+ }
+
+ @JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID)
+ public String getAuthenticationSessionId() {
+ return (String) getOtherClaims().get(JSON_FIELD_AUTHENTICATION_SESSION_ID);
+ }
+
+ @JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID)
+ public final void setAuthenticationSessionId(String authenticationSessionId) {
+ setOtherClaims(JSON_FIELD_AUTHENTICATION_SESSION_ID, authenticationSessionId);
+ }
+
+ @JsonIgnore
+ @Override
+ public Map<String, String> getNotes() {
+ Map<String, String> res = new HashMap<>();
+ if (getAuthenticationSessionId() != null) {
+ res.put(JSON_FIELD_AUTHENTICATION_SESSION_ID, getAuthenticationSessionId());
+ }
+ return res;
+ }
+
+ @Override
+ public String getNote(String name) {
+ Object res = getOtherClaims().get(name);
+ return res instanceof String ? (String) res : null;
+ }
+
+ /**
+ * Sets value of the given note
+ * @return original value (or {@code null} when no value was present)
+ */
+ public final String setNote(String name, String value) {
+ Object res = value == null
+ ? getOtherClaims().remove(name)
+ : getOtherClaims().put(name, value);
+ return res instanceof String ? (String) res : null;
+ }
+
+ /**
+ * Removes given note, and returns original value (or {@code null} when no value was present)
+ * @return see description
+ */
+ public final String removeNote(String name) {
+ Object res = getOtherClaims().remove(name);
+ return res instanceof String ? (String) res : null;
+ }
+
+ /**
+ * Updates the following fields and serializes this token into a signed JWT. The list of updated fields follows:
+ * <ul>
+ * <li>{@code id}: random nonce</li>
+ * <li>{@code issuedAt}: Current time</li>
+ * <li>{@code issuer}: URI of the given realm</li>
+ * <li>{@code audience}: URI of the given realm (same as issuer)</li>
+ * </ul>
+ *
+ * @param session
+ * @param realm
+ * @param uri
+ * @return
+ */
+ public String serialize(KeycloakSession session, RealmModel realm, UriInfo uri) {
+ String issuerUri = getIssuer(realm, uri);
+ KeyManager.ActiveHmacKey keys = session.keys().getActiveHmacKey(realm);
+
+ this
+ .issuedAt(Time.currentTime())
+ .id(getActionVerificationNonce().toString())
+ .issuer(issuerUri)
+ .audience(issuerUri);
+
+ return new JWSBuilder()
+ .kid(keys.getKid())
+ .jsonContent(this)
+ .hmac512(keys.getSecretKey());
+ }
+
+ private static String getIssuer(RealmModel realm, UriInfo uri) {
+ return Urls.realmIssuer(uri.getBaseUri(), realm.getName());
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
new file mode 100644
index 0000000..b41681f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken;
+
+import org.keycloak.models.ActionTokenKeyModel;
+import org.keycloak.representations.JsonWebToken;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.UUID;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class DefaultActionTokenKey extends JsonWebToken implements ActionTokenKeyModel {
+
+ /** The authenticationSession note with ID of the user authenticated via the action token */
+ public static final String ACTION_TOKEN_USER_ID = "ACTION_TOKEN_USER";
+
+ public static final String JSON_FIELD_ACTION_VERIFICATION_NONCE = "nonce";
+
+ @JsonProperty(value = JSON_FIELD_ACTION_VERIFICATION_NONCE, required = true)
+ private UUID actionVerificationNonce;
+
+ public DefaultActionTokenKey(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce) {
+ this.subject = userId;
+ this.type = actionId;
+ this.expiration = absoluteExpirationInSecs;
+ this.actionVerificationNonce = actionVerificationNonce == null ? UUID.randomUUID() : actionVerificationNonce;
+ }
+
+ @JsonIgnore
+ @Override
+ public String getUserId() {
+ return getSubject();
+ }
+
+ @JsonIgnore
+ @Override
+ public String getActionId() {
+ return getType();
+ }
+
+ @Override
+ public UUID getActionVerificationNonce() {
+ return actionVerificationNonce;
+ }
+
+ public String serializeKey() {
+ return String.format("%s.%d.%s.%s", getUserId(), getExpiration(), getActionVerificationNonce(), getActionId());
+ }
+
+ public static DefaultActionTokenKey from(String serializedKey) {
+ if (serializedKey == null) {
+ return null;
+ }
+ String[] parsed = serializedKey.split("\\.", 4);
+ if (parsed.length != 4) {
+ return null;
+ }
+
+ return new DefaultActionTokenKey(parsed[0], parsed[3], Integer.parseInt(parsed[1]), UUID.fromString(parsed[2]));
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionToken.java
new file mode 100644
index 0000000..7c32e2d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionToken.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken.execactions;
+
+import org.keycloak.authentication.actiontoken.DefaultActionToken;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ExecuteActionsActionToken extends DefaultActionToken {
+
+ public static final String TOKEN_TYPE = "execute-actions";
+ private static final String JSON_FIELD_REQUIRED_ACTIONS = "rqac";
+ private static final String JSON_FIELD_REDIRECT_URI = "reduri";
+
+ public ExecuteActionsActionToken(String userId, int absoluteExpirationInSecs, List<String> requiredActions, String redirectUri, String clientId) {
+ super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null);
+ setRequiredActions(requiredActions == null ? new LinkedList<>() : new LinkedList<>(requiredActions));
+ setRedirectUri(redirectUri);
+ this.issuedFor = clientId;
+ }
+
+ private ExecuteActionsActionToken() {
+ }
+
+ @JsonProperty(value = JSON_FIELD_REQUIRED_ACTIONS)
+ public List<String> getRequiredActions() {
+ return (List<String>) getOtherClaims().get(JSON_FIELD_REQUIRED_ACTIONS);
+ }
+
+ @JsonProperty(value = JSON_FIELD_REQUIRED_ACTIONS)
+ public final void setRequiredActions(List<String> requiredActions) {
+ if (requiredActions == null) {
+ getOtherClaims().remove(JSON_FIELD_REQUIRED_ACTIONS);
+ } else {
+ setOtherClaims(JSON_FIELD_REQUIRED_ACTIONS, requiredActions);
+ }
+ }
+
+ @JsonProperty(value = JSON_FIELD_REDIRECT_URI)
+ public String getRedirectUri() {
+ return (String) getOtherClaims().get(JSON_FIELD_REDIRECT_URI);
+ }
+
+ @JsonProperty(value = JSON_FIELD_REDIRECT_URI)
+ public final void setRedirectUri(String redirectUri) {
+ if (redirectUri == null) {
+ getOtherClaims().remove(JSON_FIELD_REDIRECT_URI);
+ } else {
+ setOtherClaims(JSON_FIELD_REDIRECT_URI, redirectUri);
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
new file mode 100644
index 0000000..9993ab7
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken.execactions;
+
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.authentication.actiontoken.*;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventType;
+import org.keycloak.models.*;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.utils.RedirectUtils;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import java.util.Objects;
+import javax.ws.rs.core.Response;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<ExecuteActionsActionToken> {
+
+ public ExecuteActionsActionTokenHandler() {
+ super(
+ ExecuteActionsActionToken.TOKEN_TYPE,
+ ExecuteActionsActionToken.class,
+ Messages.INVALID_CODE,
+ EventType.EXECUTE_ACTIONS,
+ Errors.NOT_ALLOWED
+ );
+ }
+
+ @Override
+ public Predicate<? super ExecuteActionsActionToken>[] getVerifiers(ActionTokenContext<ExecuteActionsActionToken> tokenContext) {
+ return TokenUtils.predicates(
+ TokenUtils.checkThat(
+ // either redirect URI is not specified or must be valid for the client
+ t -> t.getRedirectUri() == null
+ || RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), t.getRedirectUri(),
+ tokenContext.getRealm(), tokenContext.getAuthenticationSession().getClient()) != null,
+ Errors.INVALID_REDIRECT_URI,
+ Messages.INVALID_REDIRECT_URI
+ )
+ );
+ }
+
+ @Override
+ public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext<ExecuteActionsActionToken> tokenContext) {
+ AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
+
+ String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), token.getRedirectUri(),
+ tokenContext.getRealm(), authSession.getClient());
+
+ if (redirectUri != null) {
+ authSession.setAuthNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true");
+
+ authSession.setRedirectUri(redirectUri);
+ authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
+ }
+
+ token.getRequiredActions().stream().forEach(authSession::addRequiredAction);
+
+ UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
+ // verify user email as we know it is valid as this entry point would never have gotten here.
+ user.setEmailVerified(true);
+
+ String nextAction = AuthenticationManager.nextRequiredAction(tokenContext.getSession(), authSession, tokenContext.getClientConnection(), tokenContext.getRequest(), tokenContext.getUriInfo(), tokenContext.getEvent());
+ return AuthenticationManager.redirectToRequiredActions(tokenContext.getSession(), tokenContext.getRealm(), authSession, tokenContext.getUriInfo(), nextAction);
+ }
+
+ @Override
+ public boolean canUseTokenRepeatedly(ExecuteActionsActionToken token, ActionTokenContext<ExecuteActionsActionToken> tokenContext) {
+ RealmModel realm = tokenContext.getRealm();
+ KeycloakSessionFactory sessionFactory = tokenContext.getSession().getKeycloakSessionFactory();
+
+ return token.getRequiredActions().stream()
+ .map(actionName -> realm.getRequiredActionProviderByAlias(actionName)) // get realm-specific model from action name and filter out irrelevant
+ .filter(Objects::nonNull)
+ .filter(RequiredActionProviderModel::isEnabled)
+
+ .map(RequiredActionProviderModel::getProviderId) // get provider ID from model
+
+ .map(providerId -> (RequiredActionFactory) sessionFactory.getProviderFactory(RequiredActionProvider.class, providerId))
+ .filter(Objects::nonNull)
+
+ .noneMatch(RequiredActionFactory::isOneTimeAction);
+ }
+
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ExplainedTokenVerificationException.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ExplainedTokenVerificationException.java
new file mode 100644
index 0000000..271b2f2
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ExplainedTokenVerificationException.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken;
+
+import org.keycloak.authentication.ExplainedVerificationException;
+import org.keycloak.exceptions.TokenVerificationException;
+import org.keycloak.representations.JsonWebToken;
+
+/**
+ * Token verification exception that bears an error to be logged via event system
+ * and a message to show to the user e.g. via {@code ErrorPage.error()}.
+ *
+ * @author hmlnarik
+ */
+public class ExplainedTokenVerificationException extends TokenVerificationException {
+ private final String errorEvent;
+
+ public ExplainedTokenVerificationException(JsonWebToken token, ExplainedVerificationException cause) {
+ super(token, cause.getMessage(), cause);
+ this.errorEvent = cause.getErrorEvent();
+ }
+
+ public ExplainedTokenVerificationException(JsonWebToken token, String errorEvent) {
+ super(token);
+ this.errorEvent = errorEvent;
+ }
+
+ public ExplainedTokenVerificationException(JsonWebToken token, String errorEvent, String message) {
+ super(token, message);
+ this.errorEvent = errorEvent;
+ }
+
+ public ExplainedTokenVerificationException(JsonWebToken token, String errorEvent, String message, Throwable cause) {
+ super(token, message);
+ this.errorEvent = errorEvent;
+ }
+
+ public ExplainedTokenVerificationException(JsonWebToken token, String errorEvent, Throwable cause) {
+ super(token, cause);
+ this.errorEvent = errorEvent;
+ }
+
+ public String getErrorEvent() {
+ return errorEvent;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
new file mode 100644
index 0000000..7776634
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken.idpverifyemail;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.authentication.actiontoken.DefaultActionToken;
+
+/**
+ * Representation of a token that represents a time-limited verify e-mail action.
+ *
+ * @author hmlnarik
+ */
+public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
+
+ public static final String TOKEN_TYPE = "idp-verify-account-via-email";
+
+ private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu";
+ private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa";
+
+ @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME)
+ private String identityProviderUsername;
+
+ @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_ALIAS)
+ private String identityProviderAlias;
+
+ public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId,
+ String identityProviderUsername, String identityProviderAlias) {
+ super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
+ this.identityProviderUsername = identityProviderUsername;
+ this.identityProviderAlias = identityProviderAlias;
+ }
+
+ private IdpVerifyAccountLinkActionToken() {
+ }
+
+ public String getIdentityProviderUsername() {
+ return identityProviderUsername;
+ }
+
+ public void setIdentityProviderUsername(String identityProviderUsername) {
+ this.identityProviderUsername = identityProviderUsername;
+ }
+
+ public String getIdentityProviderAlias() {
+ return identityProviderAlias;
+ }
+
+ public void setIdentityProviderAlias(String identityProviderAlias) {
+ this.identityProviderAlias = identityProviderAlias;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
new file mode 100644
index 0000000..389441e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken.idpverifyemail;
+
+import org.keycloak.authentication.actiontoken.AbstractActionTokenHander;
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.authentication.actiontoken.*;
+import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator;
+import org.keycloak.events.*;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.managers.AuthenticationSessionManager;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.AuthenticationSessionProvider;
+import java.util.Collections;
+import javax.ws.rs.core.Response;
+
+/**
+ * Action token handler for verification of e-mail address.
+ * @author hmlnarik
+ */
+public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenHander<IdpVerifyAccountLinkActionToken> {
+
+ public IdpVerifyAccountLinkActionTokenHandler() {
+ super(
+ IdpVerifyAccountLinkActionToken.TOKEN_TYPE,
+ IdpVerifyAccountLinkActionToken.class,
+ Messages.STALE_CODE,
+ EventType.IDENTITY_PROVIDER_LINK_ACCOUNT,
+ Errors.INVALID_TOKEN
+ );
+ }
+
+ @Override
+ public Predicate<? super IdpVerifyAccountLinkActionToken>[] getVerifiers(ActionTokenContext<IdpVerifyAccountLinkActionToken> tokenContext) {
+ return TokenUtils.predicates(
+ );
+ }
+
+ @Override
+ public Response handleToken(IdpVerifyAccountLinkActionToken token, ActionTokenContext<IdpVerifyAccountLinkActionToken> tokenContext) {
+ UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
+ EventBuilder event = tokenContext.getEvent();
+
+ event.event(EventType.IDENTITY_PROVIDER_LINK_ACCOUNT)
+ .detail(Details.EMAIL, user.getEmail())
+ .detail(Details.IDENTITY_PROVIDER, token.getIdentityProviderAlias())
+ .detail(Details.IDENTITY_PROVIDER_USERNAME, token.getIdentityProviderUsername())
+ .success();
+
+ // verify user email as we know it is valid as this entry point would never have gotten here.
+ user.setEmailVerified(true);
+
+ AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
+ if (tokenContext.isAuthenticationSessionFresh()) {
+ AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
+ asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
+
+ AuthenticationSessionProvider authSessProvider = tokenContext.getSession().authenticationSessions();
+ authSession = authSessProvider.getAuthenticationSession(tokenContext.getRealm(), token.getAuthenticationSessionId());
+
+ if (authSession != null) {
+ authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
+ } else {
+ authSessProvider.updateNonlocalSessionAuthNotes(
+ token.getAuthenticationSessionId(),
+ Collections.singletonMap(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername())
+ );
+ }
+
+ return tokenContext.getSession().getProvider(LoginFormsProvider.class)
+ .setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, token.getIdentityProviderAlias(), token.getIdentityProviderUsername())
+ .setAttribute("skipLink", true)
+ .createInfoPage();
+ }
+
+ authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
+
+ return tokenContext.brokerFlow(null, authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH));
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionToken.java
new file mode 100644
index 0000000..6cd0458
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionToken.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken.resetcred;
+
+import org.keycloak.authentication.actiontoken.DefaultActionToken;
+
+/**
+ * Representation of a token that represents a time-limited reset credentials action.
+ *
+ * @author hmlnarik
+ */
+public class ResetCredentialsActionToken extends DefaultActionToken {
+
+ public static final String TOKEN_TYPE = "reset-credentials";
+
+ public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId) {
+ super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
+ }
+
+ private ResetCredentialsActionToken() {
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java
new file mode 100644
index 0000000..0f08bd3
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken.resetcred;
+
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.authentication.actiontoken.*;
+import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
+import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventType;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.ErrorPage;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.resources.LoginActionsService;
+import org.keycloak.services.resources.LoginActionsServiceChecks.IsActionRequired;
+import org.keycloak.sessions.CommonClientSessionModel.Action;
+import javax.ws.rs.core.Response;
+import static org.keycloak.services.resources.LoginActionsService.RESET_CREDENTIALS_PATH;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ResetCredentialsActionTokenHandler extends AbstractActionTokenHander<ResetCredentialsActionToken> {
+
+ public ResetCredentialsActionTokenHandler() {
+ super(
+ ResetCredentialsActionToken.TOKEN_TYPE,
+ ResetCredentialsActionToken.class,
+ Messages.RESET_CREDENTIAL_NOT_ALLOWED,
+ EventType.RESET_PASSWORD,
+ Errors.NOT_ALLOWED
+ );
+
+ }
+
+ @Override
+ public Predicate<? super ResetCredentialsActionToken>[] getVerifiers(ActionTokenContext<ResetCredentialsActionToken> tokenContext) {
+ return new Predicate[] {
+ TokenUtils.checkThat(tokenContext.getRealm()::isResetPasswordAllowed, Errors.NOT_ALLOWED, Messages.RESET_CREDENTIAL_NOT_ALLOWED),
+
+ new IsActionRequired(tokenContext, Action.AUTHENTICATE)
+ };
+ }
+
+ @Override
+ public Response handleToken(ResetCredentialsActionToken token, ActionTokenContext tokenContext) {
+ AuthenticationProcessor authProcessor = new ResetCredsAuthenticationProcessor();
+
+ return tokenContext.processFlow(
+ false,
+ RESET_CREDENTIALS_PATH,
+ tokenContext.getRealm().getResetCredentialsFlow(),
+ null,
+ authProcessor
+ );
+ }
+
+ @Override
+ public boolean canUseTokenRepeatedly(ResetCredentialsActionToken token, ActionTokenContext tokenContext) {
+ return false;
+ }
+
+ public static class ResetCredsAuthenticationProcessor extends AuthenticationProcessor {
+
+ @Override
+ protected Response authenticationComplete() {
+ boolean firstBrokerLoginInProgress = (authenticationSession.getAuthNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null);
+ if (firstBrokerLoginInProgress) {
+
+ UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realm, authenticationSession);
+ if (!linkingUser.getId().equals(authenticationSession.getAuthenticatedUser().getId())) {
+ return ErrorPage.error(session,
+ Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE,
+ authenticationSession.getAuthenticatedUser().getUsername(),
+ linkingUser.getUsername()
+ );
+ }
+
+ SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authenticationSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
+ authenticationSession.setAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS, serializedCtx.getIdentityProviderId());
+
+ logger.debugf("Forget-password flow finished when authenticated user '%s' after first broker login with identity provider '%s'.",
+ linkingUser.getUsername(), serializedCtx.getIdentityProviderId());
+
+ return LoginActionsService.redirectToAfterBrokerLoginEndpoint(session, realm, uriInfo, authenticationSession, true);
+ } else {
+ return super.authenticationComplete();
+ }
+ }
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/TokenUtils.java b/services/src/main/java/org/keycloak/authentication/actiontoken/TokenUtils.java
new file mode 100644
index 0000000..bdaa804
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/TokenUtils.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken;
+
+import org.keycloak.TokenVerifier;
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.representations.JsonWebToken;
+import java.util.function.BooleanSupplier;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class TokenUtils {
+ /**
+ * Returns a predicate for use in {@link TokenVerifier} using the given boolean-returning function.
+ * When the function return {@code false}, this predicate throws a {@link ExplainedTokenVerificationException}
+ * with {@code message} and {@code errorEvent} set from {@code errorMessage} and {@code errorEvent}, .
+ *
+ * @param function
+ * @param errorEvent
+ * @param errorMessage
+ * @return
+ */
+ public static Predicate<JsonWebToken> checkThat(BooleanSupplier function, String errorEvent, String errorMessage) {
+ return (JsonWebToken t) -> {
+ if (! function.getAsBoolean()) {
+ throw new ExplainedTokenVerificationException(t, errorEvent, errorMessage);
+ }
+
+ return true;
+ };
+ }
+
+ /**
+ * Returns a predicate for use in {@link TokenVerifier} using the given boolean-returning function.
+ * When the function return {@code false}, this predicate throws a {@link ExplainedTokenVerificationException}
+ * with {@code message} and {@code errorEvent} set from {@code errorMessage} and {@code errorEvent}, .
+ *
+ * @param function
+ * @param errorEvent
+ * @param errorMessage
+ * @return
+ */
+ public static <T extends JsonWebToken> Predicate<T> checkThat(java.util.function.Predicate<T> function, String errorEvent, String errorMessage) {
+ return (T t) -> {
+ if (! function.test(t)) {
+ throw new ExplainedTokenVerificationException(t, errorEvent, errorMessage);
+ }
+
+ return true;
+ };
+ }
+
+
+ /**
+ * Returns a predicate that is applied only if the given {@code condition} evaluates to {@true}. In case
+ * it evaluates to {@code false}, the predicate passes.
+ * @param <T>
+ * @param condition Condition guarding execution of the predicate
+ * @param predicate Predicate that gets tested if the condition evaluates to {@code true}
+ * @return
+ */
+ public static <T extends JsonWebToken> Predicate<T> onlyIf(java.util.function.Predicate<T> condition, Predicate<T> predicate) {
+ return t -> (! condition.test(t)) || predicate.test(t);
+ }
+
+ public static <T extends JsonWebToken> Predicate<? super T>[] predicates(Predicate<? super T>... predicate) {
+ return predicate;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java
new file mode 100644
index 0000000..656c518
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken.verifyemail;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.authentication.actiontoken.DefaultActionToken;
+
+/**
+ * Representation of a token that represents a time-limited verify e-mail action.
+ *
+ * @author hmlnarik
+ */
+public class VerifyEmailActionToken extends DefaultActionToken {
+
+ public static final String TOKEN_TYPE = "verify-email";
+
+ private static final String JSON_FIELD_EMAIL = "eml";
+
+ @JsonProperty(value = JSON_FIELD_EMAIL)
+ private String email;
+
+ public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String email) {
+ super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
+ this.email = email;
+ }
+
+ private VerifyEmailActionToken() {
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
new file mode 100644
index 0000000..abe2127
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication.actiontoken.verifyemail;
+
+import org.keycloak.authentication.actiontoken.AbstractActionTokenHander;
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.authentication.actiontoken.*;
+import org.keycloak.events.*;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserModel.RequiredAction;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.AuthenticationSessionManager;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import java.util.Objects;
+import javax.ws.rs.core.Response;
+
+/**
+ * Action token handler for verification of e-mail address.
+ * @author hmlnarik
+ */
+public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<VerifyEmailActionToken> {
+
+ public VerifyEmailActionTokenHandler() {
+ super(
+ VerifyEmailActionToken.TOKEN_TYPE,
+ VerifyEmailActionToken.class,
+ Messages.STALE_VERIFY_EMAIL_LINK,
+ EventType.VERIFY_EMAIL,
+ Errors.INVALID_TOKEN
+ );
+ }
+
+ @Override
+ public Predicate<? super VerifyEmailActionToken>[] getVerifiers(ActionTokenContext<VerifyEmailActionToken> tokenContext) {
+ return TokenUtils.predicates(
+ TokenUtils.checkThat(
+ t -> Objects.equals(t.getEmail(), tokenContext.getAuthenticationSession().getAuthenticatedUser().getEmail()),
+ Errors.INVALID_EMAIL, getDefaultErrorMessage()
+ )
+ );
+ }
+
+ @Override
+ public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext) {
+ UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
+ EventBuilder event = tokenContext.getEvent();
+
+ event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail());
+
+ AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
+
+ // verify user email as we know it is valid as this entry point would never have gotten here.
+ user.setEmailVerified(true);
+ user.removeRequiredAction(RequiredAction.VERIFY_EMAIL);
+ authSession.removeRequiredAction(RequiredAction.VERIFY_EMAIL);
+
+ event.success();
+
+ if (tokenContext.isAuthenticationSessionFresh()) {
+ AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
+ asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
+ return tokenContext.getSession().getProvider(LoginFormsProvider.class)
+ .setSuccess(Messages.EMAIL_VERIFIED)
+ .createInfoPage();
+ }
+
+ tokenContext.setEvent(event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN));
+
+ String nextAction = AuthenticationManager.nextRequiredAction(tokenContext.getSession(), authSession, tokenContext.getClientConnection(), tokenContext.getRequest(), tokenContext.getUriInfo(), event);
+ return AuthenticationManager.redirectToRequiredActions(tokenContext.getSession(), tokenContext.getRealm(), authSession, tokenContext.getUriInfo(), nextAction);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index a34e4ee..2427091 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -31,8 +31,8 @@ import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@@ -43,13 +43,18 @@ import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.ErrorPage;
+import org.keycloak.services.ErrorPageException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.util.CacheControlUtil;
+import org.keycloak.services.util.AuthenticationFlowURLHelper;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.CommonClientSessionModel;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
@@ -64,10 +69,17 @@ import java.util.Map;
*/
public class AuthenticationProcessor {
public static final String CURRENT_AUTHENTICATION_EXECUTION = "current.authentication.execution";
+ public static final String LAST_PROCESSED_EXECUTION = "last.processed.execution";
+ public static final String CURRENT_FLOW_PATH = "current.flow.path";
+ public static final String FORKED_FROM = "forked.from";
+
+ public static final String BROKER_SESSION_ID = "broker.session.id";
+ public static final String BROKER_USER_ID = "broker.user.id";
+
protected static final Logger logger = Logger.getLogger(AuthenticationProcessor.class);
protected RealmModel realm;
protected UserSessionModel userSession;
- protected ClientSessionModel clientSession;
+ protected AuthenticationSessionModel authenticationSession;
protected ClientConnection connection;
protected UriInfo uriInfo;
protected KeycloakSession session;
@@ -77,7 +89,7 @@ public class AuthenticationProcessor {
protected String flowPath;
protected boolean browserFlow;
protected BruteForceProtector protector;
- protected boolean oneActionWasSuccessful;
+ protected Runnable afterResetListener;
/**
* This could be an error message forwarded from another authenticator
*/
@@ -87,7 +99,6 @@ public class AuthenticationProcessor {
* This could be an success message forwarded from another authenticator
*/
protected FormMessage forwardedSuccessMessage;
- protected boolean userSessionCreated;
// Used for client authentication
protected ClientModel client;
@@ -128,8 +139,8 @@ public class AuthenticationProcessor {
return clientAuthAttributes;
}
- public ClientSessionModel getClientSession() {
- return clientSession;
+ public AuthenticationSessionModel getAuthenticationSession() {
+ return authenticationSession;
}
public ClientConnection getConnection() {
@@ -148,17 +159,13 @@ public class AuthenticationProcessor {
return userSession;
}
- public boolean isUserSessionCreated() {
- return userSessionCreated;
- }
-
public AuthenticationProcessor setRealm(RealmModel realm) {
this.realm = realm;
return this;
}
- public AuthenticationProcessor setClientSession(ClientSessionModel clientSession) {
- this.clientSession = clientSession;
+ public AuthenticationProcessor setAuthenticationSession(AuthenticationSessionModel authenticationSession) {
+ this.authenticationSession = authenticationSession;
return this;
}
@@ -213,8 +220,8 @@ public class AuthenticationProcessor {
}
public String generateCode() {
- ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getClientSession());
- clientSession.setTimestamp(Time.currentTime());
+ ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getAuthenticationSession());
+ authenticationSession.setTimestamp(Time.currentTime());
return accessCode.getCode();
}
@@ -232,15 +239,15 @@ public class AuthenticationProcessor {
}
public void setAutheticatedUser(UserModel user) {
- UserModel previousUser = clientSession.getAuthenticatedUser();
+ UserModel previousUser = getAuthenticationSession().getAuthenticatedUser();
if (previousUser != null && !user.getId().equals(previousUser.getId()))
throw new AuthenticationFlowException(AuthenticationFlowError.USER_CONFLICT);
validateUser(user);
- getClientSession().setAuthenticatedUser(user);
+ getAuthenticationSession().setAuthenticatedUser(user);
}
public void clearAuthenticatedUser() {
- getClientSession().setAuthenticatedUser(null);
+ getAuthenticationSession().setAuthenticatedUser(null);
}
public class Result implements AuthenticationFlowContext, ClientAuthenticationFlowContext {
@@ -363,7 +370,7 @@ public class AuthenticationProcessor {
@Override
public UserModel getUser() {
- return getClientSession().getAuthenticatedUser();
+ return getAuthenticationSession().getAuthenticatedUser();
}
@Override
@@ -397,8 +404,8 @@ public class AuthenticationProcessor {
}
@Override
- public ClientSessionModel getClientSession() {
- return AuthenticationProcessor.this.getClientSession();
+ public AuthenticationSessionModel getAuthenticationSession() {
+ return AuthenticationProcessor.this.getAuthenticationSession();
}
@Override
@@ -483,19 +490,30 @@ public class AuthenticationProcessor {
}
@Override
- public URI getActionUrl() {
- return getActionUrl(generateAccessCode());
+ public URI getActionTokenUrl(String tokenString) {
+ return LoginActionsService.actionTokenProcessor(getUriInfo())
+ .queryParam("key", tokenString)
+ .queryParam("execution", getExecution().getId())
+ .build(getRealm().getName());
+ }
+
+ @Override
+ public URI getRefreshExecutionUrl() {
+ return LoginActionsService.loginActionsBaseUrl(getUriInfo())
+ .path(AuthenticationProcessor.this.flowPath)
+ .queryParam("execution", getExecution().getId())
+ .build(getRealm().getName());
}
@Override
public void cancelLogin() {
getEvent().error(Errors.REJECTED_BY_USER);
- LoginProtocol protocol = getSession().getProvider(LoginProtocol.class, getClientSession().getAuthMethod());
+ LoginProtocol protocol = getSession().getProvider(LoginProtocol.class, getAuthenticationSession().getProtocol());
protocol.setRealm(getRealm())
.setHttpHeaders(getHttpRequest().getHttpHeaders())
.setUriInfo(getUriInfo())
.setEventBuilder(event);
- Response response = protocol.sendError(getClientSession(), Error.CANCELLED_BY_USER);
+ Response response = protocol.sendError(getAuthenticationSession(), Error.CANCELLED_BY_USER);
forceChallenge(response);
}
@@ -505,6 +523,12 @@ public class AuthenticationProcessor {
}
@Override
+ public void resetFlow(Runnable afterResetListener) {
+ this.status = FlowStatus.FLOW_RESET;
+ AuthenticationProcessor.this.afterResetListener = afterResetListener;
+ }
+
+ @Override
public void fork() {
this.status = FlowStatus.FORK;
}
@@ -539,7 +563,7 @@ public class AuthenticationProcessor {
public void logFailure() {
if (realm.isBruteForceProtected()) {
- String username = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
+ String username = authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
// todo need to handle non form failures
if (username == null) {
@@ -554,7 +578,7 @@ public class AuthenticationProcessor {
protected void logSuccess() {
if (realm.isBruteForceProtected()) {
- String username = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
+ String username = authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
// TODO: as above, need to handle non form success
if(username == null) {
@@ -569,9 +593,9 @@ public class AuthenticationProcessor {
}
public boolean isSuccessful(AuthenticationExecutionModel model) {
- ClientSessionModel.ExecutionStatus status = clientSession.getExecutionStatus().get(model.getId());
+ AuthenticationSessionModel.ExecutionStatus status = authenticationSession.getExecutionStatus().get(model.getId());
if (status == null) return false;
- return status == ClientSessionModel.ExecutionStatus.SUCCESS;
+ return status == AuthenticationSessionModel.ExecutionStatus.SUCCESS;
}
public Response handleBrowserException(Exception failure) {
@@ -602,10 +626,12 @@ public class AuthenticationProcessor {
} else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
ForkFlowException reset = (ForkFlowException)e;
- ClientSessionModel clone = clone(session, clientSession);
- clone.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
+ AuthenticationSessionModel clone = clone(session, authenticationSession);
+ clone.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
+ setAuthenticationSession(clone);
+
AuthenticationProcessor processor = new AuthenticationProcessor();
- processor.setClientSession(clone)
+ processor.setAuthenticationSession(clone)
.setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
.setFlowId(realm.getBrowserFlow().getId())
.setForwardedErrorMessage(reset.getErrorMessage())
@@ -698,47 +724,50 @@ public class AuthenticationProcessor {
public Response redirectToFlow() {
- String code = generateCode();
+ URI redirect = new AuthenticationFlowURLHelper(session, realm, uriInfo).getLastExecutionUrl(authenticationSession);
+
+ logger.debug("Redirecting to URL: " + redirect.toString());
- URI redirect = LoginActionsService.loginActionsBaseUrl(getUriInfo())
- .path(flowPath)
- .queryParam(OAuth2Constants.CODE, code).build(getRealm().getName());
return Response.status(302).location(redirect).build();
}
- public static Response redirectToRequiredActions(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession, UriInfo uriInfo) {
-
- // redirect to non-action url so browser refresh button works without reposting past data
- ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession);
- accessCode.setAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name());
- clientSession.setTimestamp(Time.currentTime());
-
- URI redirect = LoginActionsService.loginActionsBaseUrl(uriInfo)
- .path(LoginActionsService.REQUIRED_ACTION)
- .queryParam(OAuth2Constants.CODE, accessCode.getCode()).build(realm.getName());
- return Response.status(302).location(redirect).build();
+ public void resetFlow() {
+ resetFlow(authenticationSession, flowPath);
+ if (afterResetListener != null) {
+ afterResetListener.run();
+ }
}
- public static void resetFlow(ClientSessionModel clientSession) {
+ public static void resetFlow(AuthenticationSessionModel authSession, String flowPath) {
logger.debug("RESET FLOW");
- clientSession.setTimestamp(Time.currentTime());
- clientSession.setAuthenticatedUser(null);
- clientSession.clearExecutionStatus();
- clientSession.clearUserSessionNotes();
- clientSession.removeNote(CURRENT_AUTHENTICATION_EXECUTION);
+ authSession.setTimestamp(Time.currentTime());
+ authSession.setAuthenticatedUser(null);
+ authSession.clearExecutionStatus();
+ authSession.clearUserSessionNotes();
+ authSession.clearAuthNotes();
+
+ authSession.setAction(CommonClientSessionModel.Action.AUTHENTICATE.name());
+
+ authSession.setAuthNote(CURRENT_FLOW_PATH, flowPath);
}
- public static ClientSessionModel clone(KeycloakSession session, ClientSessionModel clientSession) {
- ClientSessionModel clone = session.sessions().createClientSession(clientSession.getRealm(), clientSession.getClient());
- for (Map.Entry<String, String> entry : clientSession.getNotes().entrySet()) {
- clone.setNote(entry.getKey(), entry.getValue());
+ public static AuthenticationSessionModel clone(KeycloakSession session, AuthenticationSessionModel authSession) {
+ AuthenticationSessionModel clone = new AuthenticationSessionManager(session).createAuthenticationSession(authSession.getRealm(), authSession.getClient(), true);
+
+ // Transfer just the client "notes", but not "authNotes"
+ for (Map.Entry<String, String> entry : authSession.getClientNotes().entrySet()) {
+ clone.setClientNote(entry.getKey(), entry.getValue());
}
- clone.setRedirectUri(clientSession.getRedirectUri());
- clone.setAuthMethod(clientSession.getAuthMethod());
+
+ clone.setRedirectUri(authSession.getRedirectUri());
+ clone.setProtocol(authSession.getProtocol());
clone.setTimestamp(Time.currentTime());
- clone.removeNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+
+ clone.setAuthNote(FORKED_FROM, authSession.getId());
+ logger.debugf("Forked authSession %s from authSession %s", clone.getId(), authSession.getId());
+
return clone;
}
@@ -746,27 +775,25 @@ public class AuthenticationProcessor {
public Response authenticationAction(String execution) {
logger.debug("authenticationAction");
- checkClientSession();
- String current = clientSession.getNote(CURRENT_AUTHENTICATION_EXECUTION);
- if (!execution.equals(current)) {
+ checkClientSession(true);
+ String current = authenticationSession.getAuthNote(CURRENT_AUTHENTICATION_EXECUTION);
+ if (execution == null || !execution.equals(current)) {
logger.debug("Current execution does not equal executed execution. Might be a page refresh");
- //logFailure();
- //resetFlow(clientSession);
- return authenticate();
+ return new AuthenticationFlowURLHelper(session, realm, uriInfo).showPageExpired(authenticationSession);
}
- UserModel authUser = clientSession.getAuthenticatedUser();
+ UserModel authUser = authenticationSession.getAuthenticatedUser();
validateUser(authUser);
AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(execution);
if (model == null) {
logger.debug("Cannot find execution, reseting flow");
logFailure();
- resetFlow(clientSession);
+ resetFlow();
return authenticate();
}
- event.client(clientSession.getClient().getClientId())
- .detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
- .detail(Details.AUTH_METHOD, clientSession.getAuthMethod());
- String authType = clientSession.getNote(Details.AUTH_TYPE);
+ event.client(authenticationSession.getClient().getClientId())
+ .detail(Details.REDIRECT_URI, authenticationSession.getRedirectUri())
+ .detail(Details.AUTH_METHOD, authenticationSession.getProtocol());
+ String authType = authenticationSession.getAuthNote(Details.AUTH_TYPE);
if (authType != null) {
event.detail(Details.AUTH_TYPE, authType);
}
@@ -774,95 +801,113 @@ public class AuthenticationProcessor {
AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, model);
Response challenge = authenticationFlow.processAction(execution);
if (challenge != null) return challenge;
- if (clientSession.getAuthenticatedUser() == null) {
+ if (authenticationSession.getAuthenticatedUser() == null) {
throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER);
}
return authenticationComplete();
}
- public void checkClientSession() {
- ClientSessionCode code = new ClientSessionCode(session, realm, clientSession);
- String action = ClientSessionModel.Action.AUTHENTICATE.name();
- if (!code.isValidAction(action)) {
- throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CLIENT_SESSION);
+ private void checkClientSession(boolean checkAction) {
+ ClientSessionCode code = new ClientSessionCode(session, realm, authenticationSession);
+
+ if (checkAction) {
+ String action = AuthenticationSessionModel.Action.AUTHENTICATE.name();
+ if (!code.isValidAction(action)) {
+ throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CLIENT_SESSION);
+ }
}
if (!code.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
throw new AuthenticationFlowException(AuthenticationFlowError.EXPIRED_CODE);
}
- clientSession.setTimestamp(Time.currentTime());
+ authenticationSession.setTimestamp(Time.currentTime());
}
public Response authenticateOnly() throws AuthenticationFlowException {
logger.debug("AUTHENTICATE ONLY");
- checkClientSession();
- event.client(clientSession.getClient().getClientId())
- .detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
- .detail(Details.AUTH_METHOD, clientSession.getAuthMethod());
- String authType = clientSession.getNote(Details.AUTH_TYPE);
+ checkClientSession(false);
+ event.client(authenticationSession.getClient().getClientId())
+ .detail(Details.REDIRECT_URI, authenticationSession.getRedirectUri())
+ .detail(Details.AUTH_METHOD, authenticationSession.getProtocol());
+ String authType = authenticationSession.getAuthNote(Details.AUTH_TYPE);
if (authType != null) {
event.detail(Details.AUTH_TYPE, authType);
}
- UserModel authUser = clientSession.getAuthenticatedUser();
+ UserModel authUser = authenticationSession.getAuthenticatedUser();
validateUser(authUser);
AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, null);
Response challenge = authenticationFlow.processFlow();
if (challenge != null) return challenge;
- if (clientSession.getAuthenticatedUser() == null) {
+ if (authenticationSession.getAuthenticatedUser() == null) {
throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER);
}
return challenge;
}
- /**
- * Marks that at least one action was successful
- *
- */
- public void setActionSuccessful() {
-// oneActionWasSuccessful = true;
- }
+ // May create userSession too
+ public AuthenticatedClientSessionModel attachSession() {
+ AuthenticatedClientSessionModel clientSession = attachSession(authenticationSession, userSession, session, realm, connection, event);
- public Response checkWasSuccessfulBrowserAction() {
- if (oneActionWasSuccessful && isBrowserFlow()) {
- // redirect to non-action url so browser refresh button works without reposting past data
- String code = generateCode();
-
- URI redirect = LoginActionsService.loginActionsBaseUrl(getUriInfo())
- .path(flowPath)
- .queryParam(OAuth2Constants.CODE, code).build(getRealm().getName());
- return Response.status(302).location(redirect).build();
- } else {
- return null;
+ if (userSession == null) {
+ userSession = clientSession.getUserSession();
}
+
+ return clientSession;
}
- public void attachSession() {
- String username = clientSession.getAuthenticatedUser().getUsername();
- String attemptedUsername = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
+ // May create new userSession too (if userSession argument is null)
+ public static AuthenticatedClientSessionModel attachSession(AuthenticationSessionModel authSession, UserSessionModel userSession, KeycloakSession session, RealmModel realm, ClientConnection connection, EventBuilder event) {
+ String username = authSession.getAuthenticatedUser().getUsername();
+ String attemptedUsername = authSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
if (attemptedUsername != null) username = attemptedUsername;
- String rememberMe = clientSession.getNote(Details.REMEMBER_ME);
+ String rememberMe = authSession.getAuthNote(Details.REMEMBER_ME);
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("true");
+ String brokerSessionId = authSession.getAuthNote(BROKER_SESSION_ID);
+ String brokerUserId = authSession.getAuthNote(BROKER_USER_ID);
+
if (userSession == null) { // if no authenticator attached a usersession
- userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), clientSession.getAuthMethod(), remember, null, null);
- userSession.setState(UserSessionModel.State.LOGGING_IN);
- userSessionCreated = true;
+
+ userSession = session.sessions().getUserSession(realm, authSession.getId());
+ if (userSession == null) {
+ userSession = session.sessions().createUserSession(authSession.getId(), realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
+ , remember, brokerSessionId, brokerUserId);
+ } else if (userSession.getUser() == null || !AuthenticationManager.isSessionValid(realm, userSession)) {
+ userSession.restartSession(realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
+ , remember, brokerSessionId, brokerUserId);
+ } else {
+ // We have existing userSession even if it wasn't attached to authenticator. Could happen if SSO authentication was ignored (eg. prompt=login) and in some other cases.
+ // We need to handle case when different user was used
+ logger.debugf("No SSO login, but found existing userSession with ID '%s' after finished authentication.", userSession.getId());
+ if (!authSession.getAuthenticatedUser().equals(userSession.getUser())) {
+ event.detail(Details.EXISTING_USER, userSession.getUser().getId());
+ event.error(Errors.DIFFERENT_USER_AUTHENTICATED);
+ throw new ErrorPageException(session, Messages.DIFFERENT_USER_AUTHENTICATED, userSession.getUser().getUsername());
+ }
+ }
+ userSession.setState(UserSessionModel.State.LOGGED_IN);
}
+
if (remember) {
event.detail(Details.REMEMBER_ME, "true");
}
- TokenManager.attachClientSession(userSession, clientSession);
+
+ AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(session, userSession, authSession);
+
event.user(userSession.getUser())
.detail(Details.USERNAME, username)
.session(userSession);
+
+ return clientSession;
}
public void evaluateRequiredActionTriggers() {
- AuthenticationManager.evaluateRequiredActionTriggers(session, userSession, clientSession, connection, request, uriInfo, event, realm, clientSession.getAuthenticatedUser());
+ AuthenticationManager.evaluateRequiredActionTriggers(session, authenticationSession, connection, request, uriInfo, event, realm, authenticationSession.getAuthenticatedUser());
}
public Response finishAuthentication(LoginProtocol protocol) {
event.success();
- RealmModel realm = clientSession.getRealm();
- return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, connection, event, protocol);
+ RealmModel realm = authenticationSession.getRealm();
+ AuthenticatedClientSessionModel clientSession = attachSession();
+ return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession,clientSession, request, uriInfo, connection, event, protocol);
}
@@ -877,19 +922,22 @@ public class AuthenticationProcessor {
}
protected Response authenticationComplete() {
- attachSession();
- if (isActionRequired()) {
- return redirectToRequiredActions(session, realm, clientSession, uriInfo);
+ // attachSession(); // Session will be attached after requiredActions + consents are finished.
+ AuthenticationManager.setRolesAndMappersInSession(authenticationSession);
+
+ String nextRequiredAction = nextRequiredAction();
+ if (nextRequiredAction != null) {
+ return AuthenticationManager.redirectToRequiredActions(session, realm, authenticationSession, uriInfo, nextRequiredAction);
} else {
- event.detail(Details.CODE_ID, clientSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set
+ event.detail(Details.CODE_ID, authenticationSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set
// the user has successfully logged in and we can clear his/her previous login failure attempts.
logSuccess();
- return AuthenticationManager.finishedRequiredActions(session, userSession, clientSession, connection, request, uriInfo, event);
+ return AuthenticationManager.finishedRequiredActions(session, authenticationSession, userSession, connection, request, uriInfo, event);
}
}
- public boolean isActionRequired() {
- return AuthenticationManager.isActionRequired(session, userSession, clientSession, connection, request, uriInfo, event);
+ public String nextRequiredAction() {
+ return AuthenticationManager.nextRequiredAction(session, authenticationSession, connection, request, uriInfo, event);
}
public AuthenticationProcessor.Result createAuthenticatorContext(AuthenticationExecutionModel model, Authenticator authenticator, List<AuthenticationExecutionModel> executions) {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java
index 87108da..8247566 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java
@@ -25,11 +25,11 @@ import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.events.Errors;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
@@ -47,25 +47,25 @@ public abstract class AbstractIdpAuthenticator implements Authenticator {
// The clientSession note flag to indicate that email provided by identityProvider was changed on updateProfile page
public static final String UPDATE_PROFILE_EMAIL_CHANGED = "UPDATE_PROFILE_EMAIL_CHANGED";
- // The clientSession note flag to indicate if re-authentication after first broker login happened in different browser window. This can happen for example during email verification
- public static final String IS_DIFFERENT_BROWSER = "IS_DIFFERENT_BROWSER";
-
// The clientSession note flag to indicate that updateProfile page will be always displayed even if "updateProfileOnFirstLogin" is off
public static final String ENFORCE_UPDATE_PROFILE = "ENFORCE_UPDATE_PROFILE";
// clientSession.note flag specifies if we imported new user to keycloak (true) or we just linked to an existing keycloak user (false)
public static final String BROKER_REGISTERED_NEW_USER = "BROKER_REGISTERED_NEW_USER";
+ // Set after firstBrokerLogin is successfully finished and contains the providerId of the provider, whose 'first-broker-login' flow was just finished
+ public static final String FIRST_BROKER_LOGIN_SUCCESS = "FIRST_BROKER_LOGIN_SUCCESS";
+
@Override
public void authenticate(AuthenticationFlowContext context) {
- ClientSessionModel clientSession = context.getClientSession();
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
- SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, BROKERED_CONTEXT_NOTE);
+ SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, BROKERED_CONTEXT_NOTE);
if (serializedCtx == null) {
throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
}
- BrokeredIdentityContext brokerContext = serializedCtx.deserialize(context.getSession(), clientSession);
+ BrokeredIdentityContext brokerContext = serializedCtx.deserialize(context.getSession(), authSession);
if (!brokerContext.getIdpConfig().isEnabled()) {
sendFailureChallenge(context, Errors.IDENTITY_PROVIDER_ERROR, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
@@ -76,9 +76,9 @@ public abstract class AbstractIdpAuthenticator implements Authenticator {
@Override
public void action(AuthenticationFlowContext context) {
- ClientSessionModel clientSession = context.getClientSession();
+ AuthenticationSessionModel clientSession = context.getAuthenticationSession();
- SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, BROKERED_CONTEXT_NOTE);
+ SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(clientSession, BROKERED_CONTEXT_NOTE);
if (serializedCtx == null) {
throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
}
@@ -112,8 +112,8 @@ public abstract class AbstractIdpAuthenticator implements Authenticator {
}
- public static UserModel getExistingUser(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession) {
- String existingUserId = clientSession.getNote(EXISTING_USER_INFO);
+ public static UserModel getExistingUser(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession) {
+ String existingUserId = authSession.getAuthNote(EXISTING_USER_INFO);
if (existingUserId == null) {
throw new AuthenticationFlowException("Unexpected state. There is no existing duplicated user identified in ClientSession",
AuthenticationFlowError.INTERNAL_ERROR);
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpConfirmLinkAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpConfirmLinkAuthenticator.java
index 8234700..3ed3dd7 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpConfirmLinkAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpConfirmLinkAuthenticator.java
@@ -20,16 +20,17 @@ package org.keycloak.authentication.authenticators.broker;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AuthenticationFlowException;
+import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.forms.login.LoginFormsProvider;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@@ -41,9 +42,9 @@ public class IdpConfirmLinkAuthenticator extends AbstractIdpAuthenticator {
@Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
- ClientSessionModel clientSession = context.getClientSession();
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
- String existingUserInfo = clientSession.getNote(EXISTING_USER_INFO);
+ String existingUserInfo = authSession.getAuthNote(EXISTING_USER_INFO);
if (existingUserInfo == null) {
ServicesLogger.LOGGER.noDuplicationDetected();
context.attempted();
@@ -65,9 +66,12 @@ public class IdpConfirmLinkAuthenticator extends AbstractIdpAuthenticator {
String action = formData.getFirst("submitAction");
if (action != null && action.equals("updateProfile")) {
- context.getClientSession().setNote(ENFORCE_UPDATE_PROFILE, "true");
- context.getClientSession().removeNote(EXISTING_USER_INFO);
- context.resetFlow();
+ context.resetFlow(() -> {
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+
+ serializedCtx.saveToAuthenticationSession(authSession, BROKERED_CONTEXT_NOTE);
+ authSession.setAuthNote(ENFORCE_UPDATE_PROFILE, "true");
+ });
} else if (action != null && action.equals("linkAccount")) {
context.success();
} else {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java
index 317cb64..aacd1e6 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java
@@ -53,7 +53,7 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
- if (context.getClientSession().getNote(EXISTING_USER_INFO) != null) {
+ if (context.getAuthenticationSession().getAuthNote(EXISTING_USER_INFO) != null) {
context.attempted();
return;
}
@@ -61,7 +61,7 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
String username = getUsername(context, serializedCtx, brokerContext);
if (username == null) {
ServicesLogger.LOGGER.resetFlow(realm.isRegistrationEmailAsUsername() ? "Email" : "Username");
- context.getClientSession().setNote(ENFORCE_UPDATE_PROFILE, "true");
+ context.getAuthenticationSession().setAuthNote(ENFORCE_UPDATE_PROFILE, "true");
context.resetFlow();
return;
}
@@ -91,14 +91,14 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator
userRegisteredSuccess(context, federatedUser, serializedCtx, brokerContext);
context.setUser(federatedUser);
- context.getClientSession().setNote(BROKER_REGISTERED_NEW_USER, "true");
+ context.getAuthenticationSession().setAuthNote(BROKER_REGISTERED_NEW_USER, "true");
context.success();
} else {
logger.debugf("Duplication detected. There is already existing user with %s '%s' .",
duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue());
// Set duplicated user, so next authenticators can deal with it
- context.getClientSession().setNote(EXISTING_USER_INFO, duplication.serialize());
+ context.getAuthenticationSession().setAuthNote(EXISTING_USER_INFO, duplication.serialize());
Response challengeResponse = context.form()
.setError(Messages.FEDERATED_IDENTITY_EXISTS, duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue())
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
index 420eb20..a2a9b3a 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
@@ -20,9 +20,10 @@ package org.keycloak.authentication.authenticators.broker;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionToken;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
-import org.keycloak.authentication.requiredactions.VerifyEmail;
import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.common.util.Time;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
@@ -30,19 +31,22 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.Urls;
+import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
-import org.keycloak.services.resources.LoginActionsService;
+import org.keycloak.sessions.AuthenticationSessionModel;
-import javax.ws.rs.core.MultivaluedMap;
+import java.net.URI;
+import java.util.Objects;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.util.concurrent.TimeUnit;
+import javax.ws.rs.core.*;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -51,45 +55,92 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
+ public static final String VERIFY_ACCOUNT_IDP_USERNAME = "VERIFY_ACCOUNT_IDP_USERNAME";
+
@Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
- ClientSessionModel clientSession = context.getClientSession();
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
- if (realm.getSmtpConfig().size() == 0) {
+ if (realm.getSmtpConfig().isEmpty()) {
ServicesLogger.LOGGER.smtpNotConfigured();
context.attempted();
return;
}
- // Create action cookie to detect if email verification happened in same browser
- LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getClientSession().getId());
+ if (Objects.equals(authSession.getAuthNote(VERIFY_ACCOUNT_IDP_USERNAME), brokerContext.getUsername())) {
+ UserModel existingUser = getExistingUser(session, realm, authSession);
+
+ logger.debugf("User '%s' confirmed that wants to link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(),
+ brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername());
+
+ context.setUser(existingUser);
+ context.success();
+ return;
+ }
+
+ UserModel existingUser = getExistingUser(session, realm, authSession);
+
+ // Do not allow resending e-mail by simple page refresh
+ if (! Objects.equals(authSession.getAuthNote(Constants.VERIFY_EMAIL_KEY), existingUser.getEmail())) {
+ authSession.setAuthNote(Constants.VERIFY_EMAIL_KEY, existingUser.getEmail());
+ sendVerifyEmail(session, context, existingUser, brokerContext);
+ } else {
+ showEmailSentPage(context, brokerContext);
+ }
+ }
+
+ @Override
+ protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
+ logger.debugf("Re-sending email requested for user, details follow");
- VerifyEmail.setupKey(clientSession);
+ // This will allow user to re-send email again
+ context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY);
+
+ authenticateImpl(context, serializedCtx, brokerContext);
+ }
+
+ @Override
+ public boolean requiresUser() {
+ return false;
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return false;
+ }
- UserModel existingUser = getExistingUser(session, realm, clientSession);
+ private void sendVerifyEmail(KeycloakSession session, AuthenticationFlowContext context, UserModel existingUser, BrokeredIdentityContext brokerContext) throws UriBuilderException, IllegalArgumentException {
+ RealmModel realm = session.getContext().getRealm();
+ UriInfo uriInfo = session.getContext().getUri();
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
- String link = UriBuilder.fromUri(context.getActionUrl())
- .queryParam(Constants.KEY, clientSession.getNote(Constants.VERIFY_EMAIL_KEY))
- .build().toString();
+ int validityInSecs = realm.getActionTokenGeneratedByAdminLifespan();
+ int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK)
.user(existingUser)
.detail(Details.USERNAME, existingUser.getUsername())
.detail(Details.EMAIL, existingUser.getEmail())
- .detail(Details.CODE_ID, clientSession.getId())
+ .detail(Details.CODE_ID, authSession.getId())
.removeDetail(Details.AUTH_METHOD)
.removeDetail(Details.AUTH_TYPE);
- long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
- try {
+ IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
+ existingUser.getId(), absoluteExpirationInSecs, authSession.getId(),
+ brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
+ );
+ UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+ String link = builder.queryParam("execution", context.getExecution().getId()).build(realm.getName()).toString();
+ long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
+ try {
context.getSession().getProvider(EmailTemplateProvider.class)
.setRealm(realm)
.setUser(existingUser)
.setAttribute(EmailTemplateProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
- .sendConfirmIdentityBrokerLink(link, expiration);
+ .sendConfirmIdentityBrokerLink(link, expirationInMinutes);
event.success();
} catch (EmailException e) {
@@ -103,62 +154,20 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
return;
}
+ showEmailSentPage(context, brokerContext);
+ }
+
+
+ protected void showEmailSentPage(AuthenticationFlowContext context, BrokeredIdentityContext brokerContext) {
+ String accessCode = context.generateAccessCode();
+ URI action = context.getActionUrl(accessCode);
+
Response challenge = context.form()
.setStatus(Response.Status.OK)
.setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
+ .setActionUri(action)
.createIdpLinkEmailPage();
context.forceChallenge(challenge);
}
- @Override
- protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
- MultivaluedMap<String, String> queryParams = context.getSession().getContext().getUri().getQueryParameters();
- String key = queryParams.getFirst(Constants.KEY);
- ClientSessionModel clientSession = context.getClientSession();
- RealmModel realm = context.getRealm();
- KeycloakSession session = context.getSession();
-
- if (key != null) {
- String keyFromSession = clientSession.getNote(Constants.VERIFY_EMAIL_KEY);
- clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
- if (key.equals(keyFromSession)) {
- UserModel existingUser = getExistingUser(session, realm, clientSession);
-
- logger.debugf("User '%s' confirmed that wants to link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(),
- brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername());
-
- String actionCookieValue = LoginActionsService.getActionCookie(session.getContext().getRequestHeaders(), realm, session.getContext().getUri(), context.getConnection());
- if (actionCookieValue == null || !actionCookieValue.equals(clientSession.getId())) {
- clientSession.setNote(IS_DIFFERENT_BROWSER, "true");
- }
-
- // User successfully confirmed linking by email verification. His email was defacto verified
- existingUser.setEmailVerified(true);
-
- context.setUser(existingUser);
- context.success();
- } else {
- ServicesLogger.LOGGER.keyParamDoesNotMatch();
- Response challengeResponse = context.form()
- .setError(Messages.INVALID_ACCESS_CODE)
- .createErrorPage();
- context.failureChallenge(AuthenticationFlowError.IDENTITY_PROVIDER_ERROR, challengeResponse);
- }
- } else {
- Response challengeResponse = context.form()
- .setError(Messages.MISSING_PARAMETER, Constants.KEY)
- .createErrorPage();
- context.failureChallenge(AuthenticationFlowError.IDENTITY_PROVIDER_ERROR, challengeResponse);
- }
- }
-
- @Override
- public boolean requiresUser() {
- return false;
- }
-
- @Override
- public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
- return false;
- }
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java
index c58e3e1..c430fef 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java
@@ -33,7 +33,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
-import org.keycloak.services.ServicesLogger;
import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation;
@@ -74,7 +73,7 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
}
protected boolean requiresUpdateProfilePage(AuthenticationFlowContext context, SerializedBrokeredIdentityContext userCtx, BrokeredIdentityContext brokerContext) {
- String enforceUpdateProfile = context.getClientSession().getNote(ENFORCE_UPDATE_PROFILE);
+ String enforceUpdateProfile = context.getAuthenticationSession().getAuthNote(ENFORCE_UPDATE_PROFILE);
if (Boolean.parseBoolean(enforceUpdateProfile)) {
return true;
}
@@ -123,12 +122,12 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
}
userCtx.setEmail(email);
- context.getClientSession().setNote(UPDATE_PROFILE_EMAIL_CHANGED, "true");
+ context.getAuthenticationSession().setAuthNote(UPDATE_PROFILE_EMAIL_CHANGED, "true");
}
AttributeFormDataProcessor.process(formData, realm, userCtx);
- userCtx.saveToClientSession(context.getClientSession(), BROKERED_CONTEXT_NOTE);
+ userCtx.saveToAuthenticationSession(context.getAuthenticationSession(), BROKERED_CONTEXT_NOTE);
logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername());
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java
index cd09c37..0ea8157 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java
@@ -39,7 +39,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm {
@Override
protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
- UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getClientSession());
+ UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getAuthenticationSession());
return setupForm(context, formData, existingUser)
.setStatus(Response.Status.OK)
@@ -48,7 +48,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm {
@Override
protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
- UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getClientSession());
+ UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getAuthenticationSession());
context.setUser(existingUser);
// Restore formData for the case of error
@@ -58,7 +58,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm {
}
protected LoginFormsProvider setupForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData, UserModel existingUser) {
- SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(context.getClientSession(), AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
+ SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(context.getAuthenticationSession(), AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
if (serializedCtx == null) {
throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java
index 1e40462..a9c6d1e 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java
@@ -24,13 +24,13 @@ import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderDataMarshaller;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.reflections.Reflections;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.services.resources.IdentityBrokerService;
+import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
@@ -246,7 +246,7 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
}
}
- public BrokeredIdentityContext deserialize(KeycloakSession session, ClientSessionModel clientSession) {
+ public BrokeredIdentityContext deserialize(KeycloakSession session, AuthenticationSessionModel authSession) {
BrokeredIdentityContext ctx = new BrokeredIdentityContext(getId());
ctx.setUsername(getBrokerUsername());
@@ -258,7 +258,7 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
ctx.setBrokerUserId(getBrokerUserId());
ctx.setToken(getToken());
- RealmModel realm = clientSession.getRealm();
+ RealmModel realm = authSession.getRealm();
IdentityProviderModel idpConfig = realm.getIdentityProviderByAlias(getIdentityProviderId());
if (idpConfig == null) {
throw new ModelException("Can't find identity provider with ID " + getIdentityProviderId() + " in realm " + realm.getName());
@@ -282,7 +282,7 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
}
}
- ctx.setClientSession(clientSession);
+ ctx.setAuthenticationSession(authSession);
return ctx;
}
@@ -299,7 +299,7 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
ctx.setToken(context.getToken());
ctx.setIdentityProviderId(context.getIdpConfig().getAlias());
- ctx.emailAsUsername = context.getClientSession().getRealm().isRegistrationEmailAsUsername();
+ ctx.emailAsUsername = context.getAuthenticationSession().getRealm().isRegistrationEmailAsUsername();
IdentityProviderDataMarshaller serializer = context.getIdp().getMarshaller();
@@ -313,24 +313,24 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
return ctx;
}
- // Save this context as note to clientSession
- public void saveToClientSession(ClientSessionModel clientSession, String noteKey) {
+ // Save this context as note to authSession
+ public void saveToAuthenticationSession(AuthenticationSessionModel authSession, String noteKey) {
try {
String asString = JsonSerialization.writeValueAsString(this);
- clientSession.setNote(noteKey, asString);
+ authSession.setAuthNote(noteKey, asString);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
- public static SerializedBrokeredIdentityContext readFromClientSession(ClientSessionModel clientSession, String noteKey) {
- String asString = clientSession.getNote(noteKey);
+ public static SerializedBrokeredIdentityContext readFromAuthenticationSession(AuthenticationSessionModel authSession, String noteKey) {
+ String asString = authSession.getAuthNote(noteKey);
if (asString == null) {
return null;
} else {
try {
SerializedBrokeredIdentityContext serializedCtx = JsonSerialization.readValue(asString, SerializedBrokeredIdentityContext.class);
- serializedCtx.emailAsUsername = clientSession.getRealm().isRegistrationEmailAsUsername();
+ serializedCtx.emailAsUsername = authSession.getRealm().isRegistrationEmailAsUsername();
return serializedCtx;
} catch (IOException ioe) {
throw new RuntimeException(ioe);
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java
index f837d3c..a0f13bc 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java
@@ -126,7 +126,7 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
username = username.trim();
context.getEvent().detail(Details.USERNAME, username);
- context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
+ context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
UserModel user = null;
try {
@@ -159,10 +159,10 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
String rememberMe = inputData.getFirst("rememberMe");
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on");
if (remember) {
- context.getClientSession().setNote(Details.REMEMBER_ME, "true");
+ context.getAuthenticationSession().setAuthNote(Details.REMEMBER_ME, "true");
context.getEvent().detail(Details.REMEMBER_ME, "true");
} else {
- context.getClientSession().removeNote(Details.REMEMBER_ME);
+ context.getAuthenticationSession().removeAuthNote(Details.REMEMBER_ME);
}
context.setUser(user);
return true;
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 b4552af..cf7e1a0 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
@@ -19,12 +19,12 @@ package org.keycloak.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.sessions.AuthenticationSessionModel;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -44,14 +44,14 @@ public class CookieAuthenticator implements Authenticator {
if (authResult == null) {
context.attempted();
} else {
- ClientSessionModel clientSession = context.getClientSession();
- LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, clientSession.getAuthMethod());
+ AuthenticationSessionModel clientSession = context.getAuthenticationSession();
+ LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, clientSession.getProtocol());
// Cookie re-authentication is skipped if re-authentication is required
if (protocol.requireReauthentication(authResult.getSession(), clientSession)) {
context.attempted();
} else {
- clientSession.setNote(AuthenticationManager.SSO_AUTH, "true");
+ clientSession.setClientNote(AuthenticationManager.SSO_AUTH, "true");
context.setUser(authResult.getUser());
context.attachUserSession(authResult.getSession());
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java
index f8408a4..cb31e8d 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java
@@ -63,7 +63,7 @@ public class IdentityProviderAuthenticator implements Authenticator {
List<IdentityProviderModel> identityProviders = context.getRealm().getIdentityProviders();
for (IdentityProviderModel identityProvider : identityProviders) {
if (identityProvider.isEnabled() && providerId.equals(identityProvider.getAlias())) {
- String accessCode = new ClientSessionCode(context.getSession(), context.getRealm(), context.getClientSession()).getCode();
+ String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getCode();
Response response = Response.seeOther(
Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode))
.build();
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
index 0b400f0..af63974 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
@@ -47,7 +47,7 @@ import java.util.Map;
* <li>{@code realm} the {@link RealmModel}</li>
* <li>{@code user} the current {@link UserModel}</li>
* <li>{@code session} the active {@link KeycloakSession}</li>
- * <li>{@code clientSession} the current {@link org.keycloak.models.ClientSessionModel}</li>
+ * <li>{@code clientSession} the current {@link org.keycloak.sessions.AuthenticationSessionModel}</li>
* <li>{@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}</li>
* <li>{@code LOG} a {@link org.jboss.logging.Logger} scoped to {@link ScriptBasedAuthenticator}/li>
* </ol>
@@ -160,7 +160,7 @@ public class ScriptBasedAuthenticator implements Authenticator {
bindings.put("user", context.getUser());
bindings.put("session", context.getSession());
bindings.put("httpRequest", context.getHttpRequest());
- bindings.put("clientSession", context.getClientSession());
+ bindings.put("clientSession", context.getAuthenticationSession());
bindings.put("LOG", LOGGER);
});
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java
index 8bfb995..6b72686 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java
@@ -30,7 +30,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
-import org.keycloak.services.ServicesLogger;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.HttpHeaders;
@@ -98,7 +97,7 @@ public class SpnegoAuthenticator extends AbstractUsernameFormAuthenticator imple
context.setUser(output.getAuthenticatedUser());
if (output.getState() != null && !output.getState().isEmpty()) {
for (Map.Entry<String, String> entry : output.getState().entrySet()) {
- context.getClientSession().setUserSessionNote(entry.getKey(), entry.getValue());
+ context.getAuthenticationSession().setUserSessionNote(entry.getKey(), entry.getValue());
}
}
context.success();
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
index 4f8e2d1..bd81263 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
@@ -59,7 +59,7 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
@Override
public void authenticate(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
- String loginHint = context.getClientSession().getNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);
+ String loginHint = context.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);
String rememberMeUsername = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders());
@@ -72,7 +72,6 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
}
}
Response challengeResponse = challenge(context, formData);
- context.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, context.getExecution().getId());
context.challenge(challengeResponse);
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java
index da7a67f..a1cbbe5 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java
@@ -55,7 +55,7 @@ public class ValidateUsername extends AbstractDirectGrantAuthenticator {
return;
}
context.getEvent().detail(Details.USERNAME, username);
- context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
+ context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
UserModel user = null;
try {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java
index 46097a0..4be9b9c 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java
@@ -17,12 +17,10 @@
package org.keycloak.authentication.authenticators.resetcred;
+import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
import org.jboss.logging.Logger;
import org.keycloak.Config;
-import org.keycloak.authentication.AuthenticationFlowContext;
-import org.keycloak.authentication.AuthenticationFlowError;
-import org.keycloak.authentication.Authenticator;
-import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.*;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.events.Details;
@@ -34,7 +32,6 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty;
-import org.keycloak.services.ServicesLogger;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MultivaluedMap;
@@ -53,9 +50,9 @@ public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFa
@Override
public void authenticate(AuthenticationFlowContext context) {
- String existingUserId = context.getClientSession().getNote(AbstractIdpAuthenticator.EXISTING_USER_INFO);
+ String existingUserId = context.getAuthenticationSession().getAuthNote(AbstractIdpAuthenticator.EXISTING_USER_INFO);
if (existingUserId != null) {
- UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getClientSession());
+ UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getAuthenticationSession());
logger.debugf("Forget-password triggered when reauthenticating user after first broker login. Skipping reset-credential-choose-user screen and using user '%s' ", existingUser.getUsername());
context.setUser(existingUser);
@@ -63,6 +60,18 @@ public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFa
return;
}
+ String actionTokenUserId = context.getAuthenticationSession().getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID);
+ if (actionTokenUserId != null) {
+ UserModel existingUser = context.getSession().users().getUserById(actionTokenUserId, context.getRealm());
+
+ // Action token logics handles checks for user ID validity and user being enabled
+
+ logger.debugf("Forget-password triggered when reauthenticating user after authentication via action token. Skipping reset-credential-choose-user screen and using user '%s' ", existingUser.getUsername());
+ context.setUser(existingUser);
+ context.success();
+ return;
+ }
+
Response challenge = context.form().createPasswordReset();
context.challenge(challenge);
}
@@ -89,7 +98,7 @@ public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFa
user = context.getSession().users().getUserByEmail(username, realm);
}
- context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
+ context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
// we don't want people guessing usernames, so if there is a problem, just continue, but don't set the user
// a null user will notify further executions, that this was a failure.
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
index 0d41b06..4ac9bff 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
@@ -17,43 +17,37 @@
package org.keycloak.authentication.authenticators.resetcred;
-import org.jboss.logging.Logger;
+import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
import org.keycloak.Config;
-import org.keycloak.authentication.AuthenticationFlowContext;
-import org.keycloak.authentication.AuthenticationFlowError;
-import org.keycloak.authentication.Authenticator;
-import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.*;
+import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionToken;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
+import org.keycloak.common.util.Time;
+import org.keycloak.credential.*;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
-import org.keycloak.models.AuthenticationExecutionModel;
-import org.keycloak.models.Constants;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
+import org.keycloak.models.*;
import org.keycloak.models.utils.FormMessage;
-import org.keycloak.models.utils.HmacOTP;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.messages.Messages;
-import org.keycloak.services.resources.LoginActionsService;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import java.util.*;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
-import java.util.List;
import java.util.concurrent.TimeUnit;
+import org.jboss.logging.Logger;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory {
- public static final String RESET_CREDENTIAL_SECRET = "RESET_CREDENTIAL_SECRET";
private static final Logger logger = Logger.getLogger(ResetCredentialEmail.class);
@@ -61,10 +55,9 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
@Override
public void authenticate(AuthenticationFlowContext context) {
- LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getClientSession().getId());
-
UserModel user = context.getUser();
- String username = context.getClientSession().getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
+ AuthenticationSessionModel authenticationSession = context.getAuthenticationSession();
+ String username = authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
// we don't want people guessing usernames, so if there was a problem obtaining the user, the user will be null.
// just reset login for with a success message
@@ -73,6 +66,13 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
return;
}
+ String actionTokenUserId = authenticationSession.getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID);
+ if (actionTokenUserId != null && Objects.equals(user.getId(), actionTokenUserId)) {
+ logger.debugf("Forget-password triggered when reauthenticating user after authentication via action token. Skipping " + PROVIDER_ID + " screen and using user '%s' ", user.getUsername());
+ context.success();
+ return;
+ }
+
EventBuilder event = context.getEvent();
// we don't want people guessing usernames, so if there is a problem, just continuously challenge
@@ -85,19 +85,23 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
return;
}
- // We send the secret in the email in a link as a query param. We don't need to sign it or anything because
- // it can only be guessed once, and it must match watch is stored in the client session.
- String secret = HmacOTP.generateSecret(10);
- context.getClientSession().setNote(RESET_CREDENTIAL_SECRET, secret);
- String link = UriBuilder.fromUri(context.getActionUrl()).queryParam(Constants.KEY, secret).build().toString();
- long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
+ int validityInSecs = context.getRealm().getActionTokenGeneratedByUserLifespan();
+ int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
+
+ // We send the secret in the email in a link as a query param.
+ ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authenticationSession.getId());
+ String link = UriBuilder
+ .fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
+ .build()
+ .toString();
+ long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
try {
+ context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expirationInMinutes);
- context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expiration);
event.clone().event(EventType.SEND_RESET_PASSWORD)
.user(user)
.detail(Details.USERNAME, username)
- .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, context.getClientSession().getId()).success();
+ .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, authenticationSession.getId()).success();
context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT));
} catch (EmailException e) {
event.clone().event(EventType.SEND_RESET_PASSWORD)
@@ -112,22 +116,16 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
}
}
+ public static Long getLastChangedTimestamp(KeycloakSession session, RealmModel realm, UserModel user) {
+ // TODO(hmlnarik): Make this more generic to support non-password credential types
+ PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) session.getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID);
+ CredentialModel password = passwordProvider.getPassword(realm, user);
+
+ return password == null ? null : password.getCreatedDate();
+ }
+
@Override
public void action(AuthenticationFlowContext context) {
- String secret = context.getClientSession().getNote(RESET_CREDENTIAL_SECRET);
- String key = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY);
-
- // Can only guess once! We remove the note so another guess can't happen
- context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET);
- if (secret == null || key == null || !secret.equals(key)) {
- context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
- Response challenge = context.form()
- .setError(Messages.INVALID_ACCESS_CODE)
- .createErrorPage();
- context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
- return;
- }
- // We now know email is valid, so set it to valid.
context.getUser().setEmailVerified(true);
context.success();
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java
index 40c703b..4c1fdad 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java
@@ -33,7 +33,7 @@ public class ResetOTP extends AbstractSetRequiredActionAuthenticator {
if (context.getExecution().isRequired() ||
(context.getExecution().isOptional() &&
configuredFor(context))) {
- context.getClientSession().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
+ context.getAuthenticationSession().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
}
context.success();
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java
index 64098fa..68b8bfc 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java
@@ -20,8 +20,6 @@ package org.keycloak.authentication.authenticators.resetcred;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
-import org.keycloak.services.managers.AuthenticationManager;
-import org.keycloak.services.resources.LoginActionsService;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -33,15 +31,10 @@ public class ResetPassword extends AbstractSetRequiredActionAuthenticator {
@Override
public void authenticate(AuthenticationFlowContext context) {
- String actionCookie = LoginActionsService.getActionCookie(context.getSession().getContext().getRequestHeaders(), context.getRealm(), context.getUriInfo(), context.getConnection());
- if (actionCookie == null || !actionCookie.equals(context.getClientSession().getId())) {
- context.getClientSession().setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
- }
-
if (context.getExecution().isRequired() ||
(context.getExecution().isOptional() &&
configuredFor(context))) {
- context.getClientSession().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+ context.getAuthenticationSession().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
}
context.success();
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java
index e0860fa..89048ac 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java
@@ -92,7 +92,7 @@ public class ValidateX509CertificateUsername extends AbstractX509ClientCertifica
UserModel user;
try {
context.getEvent().detail(Details.USERNAME, userIdentity.toString());
- context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString());
+ context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString());
user = getUserIdentityToModelMapper(config).find(context, userIdentity);
}
catch(ModelDuplicateException e) {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java
index 21e67ec..2aa5a63 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java
@@ -111,7 +111,7 @@ public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertif
UserModel user;
try {
context.getEvent().detail(Details.USERNAME, userIdentity.toString());
- context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString());
+ context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString());
user = getUserIdentityToModelMapper(config).find(context, userIdentity);
}
catch(ModelDuplicateException e) {
@@ -166,7 +166,6 @@ public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertif
// to call the method "challenge" results in a wrong/unexpected behavior.
// The question is whether calling "forceChallenge" here is ok from
// the design viewpoint?
- context.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, context.getExecution().getId());
context.forceChallenge(createSuccessResponse(context, certs[0].getSubjectDN().getName()));
// Do not set the flow status yet, we want to display a form to let users
// choose whether to accept the identity from certificate or to specify username/password explicitly
diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
index 40433cf..3a9c53c 100755
--- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
@@ -20,9 +20,9 @@ package org.keycloak.authentication;
import org.jboss.logging.Logger;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
import java.util.Iterator;
@@ -51,11 +51,11 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
protected boolean isProcessed(AuthenticationExecutionModel model) {
if (model.isDisabled()) return true;
- ClientSessionModel.ExecutionStatus status = processor.getClientSession().getExecutionStatus().get(model.getId());
+ AuthenticationSessionModel.ExecutionStatus status = processor.getAuthenticationSession().getExecutionStatus().get(model.getId());
if (status == null) return false;
- return status == ClientSessionModel.ExecutionStatus.SUCCESS || status == ClientSessionModel.ExecutionStatus.SKIPPED
- || status == ClientSessionModel.ExecutionStatus.ATTEMPTED
- || status == ClientSessionModel.ExecutionStatus.SETUP_REQUIRED;
+ return status == AuthenticationSessionModel.ExecutionStatus.SUCCESS || status == AuthenticationSessionModel.ExecutionStatus.SKIPPED
+ || status == AuthenticationSessionModel.ExecutionStatus.ATTEMPTED
+ || status == AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED;
}
@@ -75,7 +75,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
Response flowChallenge = authenticationFlow.processAction(actionExecution);
if (flowChallenge == null) {
- processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SUCCESS);
+ processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
if (model.isAlternative()) alternativeSuccessful = true;
return processFlow();
} else {
@@ -90,13 +90,9 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
logger.debugv("action: {0}", model.getAuthenticator());
authenticator.action(result);
- Response response = processResult(result);
+ Response response = processResult(result, true);
if (response == null) {
- processor.getClientSession().removeNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
- if (result.status == FlowStatus.SUCCESS) {
- // we do this so that flow can redirect to a non-action URL
- processor.setActionSuccessful();
- }
+ processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
return processFlow();
} else return response;
}
@@ -119,7 +115,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
}
if (model.isAlternative() && alternativeSuccessful) {
logger.debug("Skip alternative execution");
- processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
+ processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
continue;
}
if (model.isAuthenticatorFlow()) {
@@ -127,7 +123,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model);
Response flowChallenge = authenticationFlow.processFlow();
if (flowChallenge == null) {
- processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SUCCESS);
+ processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
if (model.isAlternative()) alternativeSuccessful = true;
continue;
} else {
@@ -135,13 +131,13 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
alternativeChallenge = flowChallenge;
challengedAlternativeExecution = model;
} else if (model.isRequired()) {
- processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
+ processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return flowChallenge;
} else if (model.isOptional()) {
- processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
+ processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
continue;
} else {
- processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
+ processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
continue;
}
return flowChallenge;
@@ -154,11 +150,11 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
}
Authenticator authenticator = factory.create(processor.getSession());
logger.debugv("authenticator: {0}", factory.getId());
- UserModel authUser = processor.getClientSession().getAuthenticatedUser();
+ UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser();
if (authenticator.requiresUser() && authUser == null) {
if (alternativeChallenge != null) {
- processor.getClientSession().setExecutionStatus(challengedAlternativeExecution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
+ processor.getAuthenticationSession().setExecutionStatus(challengedAlternativeExecution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return alternativeChallenge;
}
throw new AuthenticationFlowException("authenticator: " + factory.getId(), AuthenticationFlowError.UNKNOWN_USER);
@@ -170,88 +166,88 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
if (model.isRequired()) {
if (factory.isUserSetupAllowed()) {
logger.debugv("authenticator SETUP_REQUIRED: {0}", factory.getId());
- processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SETUP_REQUIRED);
- authenticator.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getClientSession().getAuthenticatedUser());
+ processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED);
+ authenticator.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getAuthenticationSession().getAuthenticatedUser());
continue;
} else {
throw new AuthenticationFlowException(AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
}
} else if (model.isOptional()) {
- processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
+ processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
continue;
}
}
}
// skip if action as successful already
- Response redirect = processor.checkWasSuccessfulBrowserAction();
- if (redirect != null) return redirect;
+// Response redirect = processor.checkWasSuccessfulBrowserAction();
+// if (redirect != null) return redirect;
AuthenticationProcessor.Result context = processor.createAuthenticatorContext(model, authenticator, executions);
logger.debug("invoke authenticator.authenticate");
authenticator.authenticate(context);
- Response response = processResult(context);
+ Response response = processResult(context, false);
if (response != null) return response;
}
return null;
}
- public Response processResult(AuthenticationProcessor.Result result) {
+ public Response processResult(AuthenticationProcessor.Result result, boolean isAction) {
AuthenticationExecutionModel execution = result.getExecution();
FlowStatus status = result.getStatus();
switch (status) {
case SUCCESS:
logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator());
- processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS);
+ processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
if (execution.isAlternative()) alternativeSuccessful = true;
return null;
case FAILED:
logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator());
processor.logFailure();
- processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED);
+ processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.FAILED);
if (result.getChallenge() != null) {
return sendChallenge(result, execution);
}
throw new AuthenticationFlowException(result.getError());
case FORK:
logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator());
- processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId());
+ processor.getAuthenticationSession().setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId());
throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage());
case FORCE_CHALLENGE:
- processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
+ processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
case CHALLENGE:
logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator());
if (execution.isRequired()) {
- processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
+ processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
}
- UserModel authenticatedUser = processor.getClientSession().getAuthenticatedUser();
+ UserModel authenticatedUser = processor.getAuthenticationSession().getAuthenticatedUser();
if (execution.isOptional() && authenticatedUser != null && result.getAuthenticator().configuredFor(processor.getSession(), processor.getRealm(), authenticatedUser)) {
- processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
+ processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
}
if (execution.isAlternative()) {
alternativeChallenge = result.getChallenge();
challengedAlternativeExecution = execution;
} else {
- processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
+ processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
}
return null;
case FAILURE_CHALLENGE:
logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator());
processor.logFailure();
- processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
+ processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
case ATTEMPTED:
logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator());
if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS);
}
- processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.ATTEMPTED);
+ processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED);
return null;
case FLOW_RESET:
- AuthenticationProcessor.resetFlow(processor.getClientSession());
+ processor.resetFlow();
return processor.authenticate();
default:
logger.debugv("authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator());
@@ -261,7 +257,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
}
public Response sendChallenge(AuthenticationProcessor.Result result, AuthenticationExecutionModel execution) {
- processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId());
+ processor.getAuthenticationSession().setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId());
return result.getChallenge();
}
diff --git a/services/src/main/java/org/keycloak/authentication/ExplainedVerificationException.java b/services/src/main/java/org/keycloak/authentication/ExplainedVerificationException.java
new file mode 100644
index 0000000..7102588
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/ExplainedVerificationException.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authentication;
+
+import org.keycloak.common.VerificationException;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ExplainedVerificationException extends VerificationException {
+ private final String errorEvent;
+
+ public ExplainedVerificationException(String errorEvent) {
+ super();
+ this.errorEvent = errorEvent;
+ }
+
+ public ExplainedVerificationException(String errorEvent, String message) {
+ super(message);
+ this.errorEvent = errorEvent;
+ }
+
+ public ExplainedVerificationException(String errorEvent, String message, Throwable cause) {
+ super(message);
+ this.errorEvent = errorEvent;
+ }
+
+ public ExplainedVerificationException(String errorEvent, Throwable cause) {
+ super(cause);
+ this.errorEvent = errorEvent;
+ }
+
+ public String getErrorEvent() {
+ return errorEvent;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
index 59c85fb..955879f 100755
--- a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
@@ -24,12 +24,12 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.resources.LoginActionsService;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@@ -93,7 +93,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
@Override
public UserModel getUser() {
- return getClientSession().getAuthenticatedUser();
+ return getAuthenticationSession().getAuthenticatedUser();
}
@Override
@@ -107,8 +107,8 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
}
@Override
- public ClientSessionModel getClientSession() {
- return processor.getClientSession();
+ public AuthenticationSessionModel getAuthenticationSession() {
+ return processor.getAuthenticationSession();
}
@Override
@@ -166,19 +166,19 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
if (!actionExecution.equals(formExecution.getId())) {
throw new AuthenticationFlowException("action is not current execution", AuthenticationFlowError.INTERNAL_ERROR);
}
- Map<String, ClientSessionModel.ExecutionStatus> executionStatus = new HashMap<>();
+ Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus = new HashMap<>();
List<FormAction> requiredActions = new LinkedList<>();
List<ValidationContextImpl> successes = new LinkedList<>();
List<ValidationContextImpl> errors = new LinkedList<>();
for (AuthenticationExecutionModel formActionExecution : formActionExecutions) {
if (!formActionExecution.isEnabled()) {
- executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
+ executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
continue;
}
FormActionFactory factory = (FormActionFactory)processor.getSession().getKeycloakSessionFactory().getProviderFactory(FormAction.class, formActionExecution.getAuthenticator());
FormAction action = factory.create(processor.getSession());
- UserModel authUser = processor.getClientSession().getAuthenticatedUser();
+ UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser();
if (action.requiresUser() && authUser == null) {
throw new AuthenticationFlowException("form action: " + formExecution.getAuthenticator() + " requires user", AuthenticationFlowError.UNKNOWN_USER);
}
@@ -189,14 +189,14 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
if (formActionExecution.isRequired()) {
if (factory.isUserSetupAllowed()) {
AuthenticationProcessor.logger.debugv("authenticator SETUP_REQUIRED: {0}", formExecution.getAuthenticator());
- executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SETUP_REQUIRED);
+ executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED);
requiredActions.add(action);
continue;
} else {
throw new AuthenticationFlowException(AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED);
}
} else if (formActionExecution.isOptional()) {
- executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
+ executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED);
continue;
}
}
@@ -205,10 +205,10 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
ValidationContextImpl result = new ValidationContextImpl(formActionExecution, action);
action.validate(result);
if (result.success) {
- executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS);
+ executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS);
successes.add(result);
} else {
- executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
+ executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED);
errors.add(result);
}
}
@@ -234,16 +234,15 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
context.action.success(context);
}
// set status and required actions only if form is fully successful
- for (Map.Entry<String, ClientSessionModel.ExecutionStatus> entry : executionStatus.entrySet()) {
- processor.getClientSession().setExecutionStatus(entry.getKey(), entry.getValue());
+ for (Map.Entry<String, AuthenticationSessionModel.ExecutionStatus> entry : executionStatus.entrySet()) {
+ processor.getAuthenticationSession().setExecutionStatus(entry.getKey(), entry.getValue());
}
for (FormAction action : requiredActions) {
- action.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getClientSession().getAuthenticatedUser());
+ action.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getAuthenticationSession().getAuthenticatedUser());
}
- processor.getClientSession().setExecutionStatus(actionExecution, ClientSessionModel.ExecutionStatus.SUCCESS);
- processor.getClientSession().removeNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
- processor.setActionSuccessful();
+ processor.getAuthenticationSession().setExecutionStatus(actionExecution, AuthenticationSessionModel.ExecutionStatus.SUCCESS);
+ processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
return null;
}
@@ -262,7 +261,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
public Response renderForm(MultivaluedMap<String, String> formData, List<FormMessage> errors) {
String executionId = formExecution.getId();
- processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, executionId);
+ processor.getAuthenticationSession().setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, executionId);
String code = processor.generateCode();
URI actionUrl = getActionUrl(executionId, code);
LoginFormsProvider form = processor.getSession().getProvider(LoginFormsProvider.class)
diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
index 90dee70..6567aef 100755
--- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
+++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
@@ -134,16 +134,16 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
user.setEnabled(true);
user.setEmail(email);
- context.getClientSession().setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
+ context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
AttributeFormDataProcessor.process(formData, context.getRealm(), user);
context.setUser(user);
context.getEvent().user(user);
context.getEvent().success();
context.newEvent().event(EventType.LOGIN);
- context.getEvent().client(context.getClientSession().getClient().getClientId())
- .detail(Details.REDIRECT_URI, context.getClientSession().getRedirectUri())
- .detail(Details.AUTH_METHOD, context.getClientSession().getAuthMethod());
- String authType = context.getClientSession().getNote(Details.AUTH_TYPE);
+ context.getEvent().client(context.getAuthenticationSession().getClient().getClientId())
+ .detail(Details.REDIRECT_URI, context.getAuthenticationSession().getRedirectUri())
+ .detail(Details.AUTH_METHOD, context.getAuthenticationSession().getProtocol());
+ String authType = context.getAuthenticationSession().getAuthNote(Details.AUTH_TYPE);
if (authType != null) {
context.getEvent().detail(Details.AUTH_TYPE, authType);
}
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
index 8f830d1..87b3403 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
@@ -23,13 +23,12 @@ import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.Time;
import org.keycloak.events.EventBuilder;
import org.keycloak.forms.login.LoginFormsProvider;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.resources.LoginActionsService;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
@@ -40,8 +39,7 @@ import java.net.URI;
* @version $Revision: 1 $
*/
public class RequiredActionContextResult implements RequiredActionContext {
- protected UserSessionModel userSession;
- protected ClientSessionModel clientSession;
+ protected AuthenticationSessionModel authenticationSession;
protected RealmModel realm;
protected EventBuilder eventBuilder;
protected KeycloakSession session;
@@ -51,12 +49,11 @@ public class RequiredActionContextResult implements RequiredActionContext {
protected UserModel user;
protected RequiredActionFactory factory;
- public RequiredActionContextResult(UserSessionModel userSession, ClientSessionModel clientSession,
+ public RequiredActionContextResult(AuthenticationSessionModel authSession,
RealmModel realm, EventBuilder eventBuilder, KeycloakSession session,
HttpRequest httpRequest,
UserModel user, RequiredActionFactory factory) {
- this.userSession = userSession;
- this.clientSession = clientSession;
+ this.authenticationSession = authSession;
this.realm = realm;
this.eventBuilder = eventBuilder;
this.session = session;
@@ -81,13 +78,8 @@ public class RequiredActionContextResult implements RequiredActionContext {
}
@Override
- public ClientSessionModel getClientSession() {
- return clientSession;
- }
-
- @Override
- public UserSessionModel getUserSession() {
- return userSession;
+ public AuthenticationSessionModel getAuthenticationSession() {
+ return authenticationSession;
}
@Override
@@ -142,14 +134,14 @@ public class RequiredActionContextResult implements RequiredActionContext {
public URI getActionUrl(String code) {
return LoginActionsService.requiredActionProcessor(getUriInfo())
.queryParam(OAuth2Constants.CODE, code)
- .queryParam("action", factory.getId())
+ .queryParam("execution", factory.getId())
.build(getRealm().getName());
}
@Override
public String generateCode() {
- ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getClientSession());
- clientSession.setTimestamp(Time.currentTime());
+ ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, getRealm(), getAuthenticationSession());
+ authenticationSession.setTimestamp(Time.currentTime());
return accessCode.getCode();
}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
index aa5bf25..6c7f745 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
@@ -88,8 +88,8 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
String passwordConfirm = formData.getFirst("password-confirm");
EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PASSWORD_ERROR)
- .client(context.getClientSession().getClient())
- .user(context.getClientSession().getUserSession().getUser());
+ .client(context.getAuthenticationSession().getClient())
+ .user(context.getAuthenticationSession().getAuthenticatedUser());
if (Validation.isBlank(passwordNew)) {
Response challenge = context.form()
@@ -157,4 +157,9 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
public String getId() {
return UserModel.RequiredAction.UPDATE_PASSWORD.name();
}
+
+ @Override
+ public boolean isOneTimeAction() {
+ return true;
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
index 829c705..de7a078 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
@@ -118,4 +118,9 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
public String getId() {
return UserModel.RequiredAction.CONFIGURE_TOTP.name();
}
+
+ @Override
+ public boolean isOneTimeAction() {
+ return true;
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
index 2d683d3..baa3c4e 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
@@ -22,20 +22,23 @@ import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
+import org.keycloak.common.util.Time;
+import org.keycloak.email.EmailException;
+import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.Constants;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.utils.HmacOTP;
-import org.keycloak.services.ServicesLogger;
-import org.keycloak.services.resources.LoginActionsService;
+import org.keycloak.models.*;
+import org.keycloak.services.Urls;
import org.keycloak.services.validation.Validation;
-import javax.ws.rs.core.Response;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import javax.ws.rs.core.*;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -52,32 +55,44 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
}
@Override
public void requiredActionChallenge(RequiredActionContext context) {
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+
if (context.getUser().isEmailVerified()) {
context.success();
+ authSession.removeAuthNote(Constants.VERIFY_EMAIL_KEY);
return;
}
- if (Validation.isBlank(context.getUser().getEmail())) {
+ String email = context.getUser().getEmail();
+ if (Validation.isBlank(email)) {
context.ignore();
return;
}
- context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail()).success();
- LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getUserSession().getId());
+ LoginFormsProvider loginFormsProvider = context.form();
+ Response challenge;
- setupKey(context.getClientSession());
+ // Do not allow resending e-mail by simple page refresh, i.e. when e-mail sent, it should be resent properly via email-verification endpoint
+ if (! Objects.equals(authSession.getAuthNote(Constants.VERIFY_EMAIL_KEY), email)) {
+ authSession.setAuthNote(Constants.VERIFY_EMAIL_KEY, email);
+ EventBuilder event = context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, email);
+ challenge = sendVerifyEmail(context.getSession(), loginFormsProvider, context.getUser(), context.getAuthenticationSession(), event);
+ } else {
+ challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
+ }
- LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
- .setClientSessionCode(context.generateCode())
- .setClientSession(context.getClientSession())
- .setUser(context.getUser());
- Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
context.challenge(challenge);
}
+
@Override
public void processAction(RequiredActionContext context) {
- context.failure();
+ logger.debugf("Re-sending email requested for user: %s", context.getUser().getUsername());
+
+ // This will allow user to re-send email again
+ context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY);
+
+ requiredActionChallenge(context);
}
@@ -112,8 +127,30 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
return UserModel.RequiredAction.VERIFY_EMAIL.name();
}
- public static void setupKey(ClientSessionModel clientSession) {
- String secret = HmacOTP.generateSecret(10);
- clientSession.setNote(Constants.VERIFY_EMAIL_KEY, secret);
+ private Response sendVerifyEmail(KeycloakSession session, LoginFormsProvider forms, UserModel user, AuthenticationSessionModel authSession, EventBuilder event) throws UriBuilderException, IllegalArgumentException {
+ RealmModel realm = session.getContext().getRealm();
+ UriInfo uriInfo = session.getContext().getUri();
+
+ int validityInSecs = realm.getActionTokenGeneratedByUserLifespan();
+ int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
+
+ VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSession.getId(), user.getEmail());
+ UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+ String link = builder.build(realm.getName()).toString();
+ long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
+
+ try {
+ session
+ .getProvider(EmailTemplateProvider.class)
+ .setRealm(realm)
+ .setUser(user)
+ .sendVerifyEmail(link, expirationInMinutes);
+ event.success();
+ } catch (EmailException e) {
+ logger.error("Failed to send verification email", e);
+ event.error(Errors.EMAIL_SEND_FAILED);
+ }
+
+ return forms.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
}
}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
index 2e82794..24e1a89 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
@@ -38,6 +38,7 @@ import javax.ws.rs.core.Response;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.util.Permissions;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.authorization.PolicyEvaluationRequest;
import org.keycloak.authorization.admin.representation.PolicyEvaluationResponseBuilder;
@@ -53,20 +54,24 @@ import org.keycloak.authorization.policy.evaluation.EvaluationContext;
import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
-import org.keycloak.authorization.util.Permissions;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.ProtocolMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.Urls;
+import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.resources.admin.RealmAuth;
+import org.keycloak.sessions.AuthenticationSessionModel;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -192,19 +197,13 @@ public class PolicyEvaluationService {
private static class CloseableKeycloakIdentity extends KeycloakIdentity {
private UserSessionModel userSession;
- private ClientSessionModel clientSession;
- public CloseableKeycloakIdentity(AccessToken accessToken, KeycloakSession keycloakSession, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public CloseableKeycloakIdentity(AccessToken accessToken, KeycloakSession keycloakSession, UserSessionModel userSession) {
super(accessToken, keycloakSession);
this.userSession = userSession;
- this.clientSession = clientSession;
}
public void close() {
- if (clientSession != null) {
- keycloakSession.sessions().removeClientSession(realm, clientSession);
- }
-
if (userSession != null) {
keycloakSession.sessions().removeUserSession(realm, userSession);
}
@@ -220,7 +219,7 @@ public class PolicyEvaluationService {
String subject = representation.getUserId();
- ClientSessionModel clientSession = null;
+ AuthenticatedClientSessionModel clientSession = null;
UserSessionModel userSession = null;
if (subject != null) {
UserModel userModel = keycloakSession.users().getUserById(subject, realm);
@@ -234,11 +233,15 @@ public class PolicyEvaluationService {
if (clientId != null) {
ClientModel clientModel = realm.getClientById(clientId);
- clientSession = keycloakSession.sessions().createClientSession(realm, clientModel);
- clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
- userSession = keycloakSession.sessions().createUserSession(realm, userModel, userModel.getUsername(), "127.0.0.1", "passwd", false, null, null);
+ String id = KeycloakModelUtils.generateId();
+
+ AuthenticationSessionModel authSession = keycloakSession.authenticationSessions().createAuthenticationSession(id, realm, clientModel);
+ authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ authSession.setAuthenticatedUser(userModel);
+ userSession = keycloakSession.sessions().createUserSession(id, realm, userModel, userModel.getUsername(), "127.0.0.1", "passwd", false, null, null);
- new TokenManager().attachClientSession(userSession, clientSession);
+ AuthenticationManager.setRolesAndMappersInSession(authSession);
+ clientSession = TokenManager.attachAuthenticationSession(keycloakSession, userSession, authSession);
Set<RoleModel> requestedRoles = new HashSet<>();
for (String roleId : clientSession.getRoles()) {
@@ -276,6 +279,6 @@ public class PolicyEvaluationService {
representation.getRoleIds().forEach(roleName -> realmAccess.addRole(roleName));
}
- return new CloseableKeycloakIdentity(accessToken, keycloakSession, userSession, clientSession);
+ return new CloseableKeycloakIdentity(accessToken, keycloakSession, userSession);
}
}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
index 6b5b027..9ea53e2 100644
--- a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
+++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
@@ -23,7 +23,6 @@ import org.keycloak.authorization.attribute.Attributes;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.util.Tokens;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@@ -118,8 +117,8 @@ public class KeycloakIdentity implements Identity {
@Override
public String getId() {
if (isResourceServer()) {
- ClientSessionModel clientSession = this.keycloakSession.sessions().getClientSession(this.accessToken.getClientSession());
- return clientSession.getClient().getId();
+ ClientModel client = getTargetClient();
+ return client==null ? null : client.getId();
}
return this.accessToken.getSubject();
@@ -137,20 +136,10 @@ public class KeycloakIdentity implements Identity {
private boolean isResourceServer() {
UserModel clientUser = null;
- if (this.accessToken.getClientSession() != null) {
- ClientSessionModel clientSession = this.keycloakSession.sessions().getClientSession(this.accessToken.getClientSession());
+ ClientModel clientModel = getTargetClient();
- if (clientSession != null) {
- clientUser = this.keycloakSession.users().getServiceAccount(clientSession.getClient());
- }
- }
-
- if (this.accessToken.getIssuedFor() != null) {
- ClientModel clientModel = this.keycloakSession.realms().getClientById(this.accessToken.getIssuedFor(), this.realm);
-
- if (clientModel != null) {
- clientUser = this.keycloakSession.users().getServiceAccount(clientModel);
- }
+ if (clientModel != null) {
+ clientUser = this.keycloakSession.users().getServiceAccount(clientModel);
}
if (clientUser == null) {
@@ -159,4 +148,17 @@ public class KeycloakIdentity implements Identity {
return this.accessToken.getSubject().equals(clientUser.getId());
}
+
+ private ClientModel getTargetClient() {
+ if (this.accessToken.getIssuedFor() != null) {
+ return realm.getClientByClientId(accessToken.getIssuedFor());
+ }
+
+ if (this.accessToken.getAudience() != null && this.accessToken.getAudience().length > 0) {
+ String audience = this.accessToken.getAudience()[0];
+ return realm.getClientByClientId(audience);
+ }
+
+ return null;
+ }
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
index 7f02e43..339747a 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -37,6 +37,7 @@ import org.keycloak.services.messages.Messages;
import javax.ws.rs.GET;
import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
@@ -240,6 +241,8 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
return callback.authenticated(federatedIdentity);
}
+ } catch (WebApplicationException e) {
+ return e.getResponse();
} catch (Exception e) {
logger.error("Failed to make identity provider oauth callback", e);
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index 4a8a71a..45183c3 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -32,7 +32,6 @@ import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.keys.loader.PublicKeyStorageManager;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
@@ -44,6 +43,7 @@ import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.GET;
@@ -350,14 +350,14 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
@Override
- public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) {
+ public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
AccessTokenResponse tokenResponse = (AccessTokenResponse)context.getContextData().get(FEDERATED_ACCESS_TOKEN_RESPONSE);
int currentTime = Time.currentTime();
long expiration = tokenResponse.getExpiresIn() > 0 ? tokenResponse.getExpiresIn() + currentTime : 0;
- userSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(expiration));
- userSession.setNote(FEDERATED_REFRESH_TOKEN, tokenResponse.getRefreshToken());
- userSession.setNote(FEDERATED_ACCESS_TOKEN, tokenResponse.getToken());
- userSession.setNote(FEDERATED_ID_TOKEN, tokenResponse.getIdToken());
+ authSession.setUserSessionNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(expiration));
+ authSession.setUserSessionNote(FEDERATED_REFRESH_TOKEN, tokenResponse.getRefreshToken());
+ authSession.setUserSessionNote(FEDERATED_ACCESS_TOKEN, tokenResponse.getToken());
+ authSession.setUserSessionNote(FEDERATED_ID_TOKEN, tokenResponse.getIdToken());
}
@Override
diff --git a/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java b/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java
index e46798c..1b91f56 100755
--- a/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java
+++ b/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java
@@ -87,14 +87,14 @@ public class HardcodedUserSessionAttributeMapper extends AbstractIdentityProvide
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(ATTRIBUTE);
String attributeValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE);
- context.getClientSession().setUserSessionNote(attribute, attributeValue);
+ context.getAuthenticationSession().setUserSessionNote(attribute, attributeValue);
}
@Override
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String attribute = mapperModel.getConfig().get(ATTRIBUTE);
String attributeValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE);
- context.getClientSession().setUserSessionNote(attribute, attributeValue);
+ context.getAuthenticationSession().setUserSessionNote(attribute, attributeValue);
}
@Override
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
index 5825f60..dc38d59 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -71,6 +71,7 @@ import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.PathParam;
+import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
@@ -436,7 +437,8 @@ public class SAMLEndpoint {
return callback.authenticated(identity);
-
+ } catch (WebApplicationException e) {
+ return e.getResponse();
} catch (Exception e) {
throw new IdentityBrokerException("Could not process response from SAML identity provider.", e);
}
diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
index c28cda8..51d6eb8 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
@@ -31,7 +31,6 @@ import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.events.EventBuilder;
import org.keycloak.keys.RsaKeyMetadata;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
@@ -56,6 +55,7 @@ import java.util.TreeSet;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import org.keycloak.keys.KeyMetadata;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
+import org.keycloak.sessions.AuthenticationSessionModel;
/**
* @author Pedro Igor
@@ -132,17 +132,17 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
}
@Override
- public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) {
+ public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
ResponseType responseType = (ResponseType)context.getContextData().get(SAMLEndpoint.SAML_LOGIN_RESPONSE);
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
SubjectType subject = assertion.getSubject();
SubjectType.STSubType subType = subject.getSubType();
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
- userSession.setNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT, subjectNameID.getValue());
- if (subjectNameID.getFormat() != null) userSession.setNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEFORMAT, subjectNameID.getFormat().toString());
+ authSession.setUserSessionNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT, subjectNameID.getValue());
+ if (subjectNameID.getFormat() != null) authSession.setUserSessionNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEFORMAT, subjectNameID.getFormat().toString());
AuthnStatementType authn = (AuthnStatementType)context.getContextData().get(SAMLEndpoint.SAML_AUTHN_STATEMENT);
if (authn != null && authn.getSessionIndex() != null) {
- userSession.setNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX, authn.getSessionIndex());
+ authSession.setUserSessionNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX, authn.getSessionIndex());
}
}
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/SessionsBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/SessionsBean.java
index a474f4f..f597d5c 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/SessionsBean.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/SessionsBean.java
@@ -19,7 +19,6 @@ package org.keycloak.forms.account.freemarker.model;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
@@ -79,8 +78,8 @@ public class SessionsBean {
public Set<String> getClients() {
Set<String> clients = new HashSet<String>();
- for (ClientSessionModel clientSession : session.getClientSessions()) {
- ClientModel client = clientSession.getClient();
+ for (String clientUUID : session.getAuthenticatedClientSessions().keySet()) {
+ ClientModel client = realm.getClientById(clientUUID);
clients.add(client.getClientId());
}
return clients;
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index 9f60404..625406c 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -23,8 +23,6 @@ import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.util.ObjectUtil;
-import org.keycloak.email.EmailException;
-import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.forms.login.LoginFormsPages;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.forms.login.freemarker.model.ClientBean;
@@ -38,18 +36,11 @@ import org.keycloak.forms.login.freemarker.model.RegisterBean;
import org.keycloak.forms.login.freemarker.model.RequiredActionUrlFormatterMethod;
import org.keycloak.forms.login.freemarker.model.TotpBean;
import org.keycloak.forms.login.freemarker.model.UrlBean;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.Constants;
-import org.keycloak.models.IdentityProviderModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserModel;
+import org.keycloak.models.*;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.theme.BrowserSecurityHeaderSetup;
import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.FreeMarkerUtil;
@@ -70,14 +61,7 @@ import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Properties;
-import java.util.concurrent.TimeUnit;
+import java.util.*;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -106,7 +90,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
private UserModel user;
- private ClientSessionModel clientSession;
private final Map<String, Object> attributes = new HashMap<String, Object>();
public FreeMarkerLoginFormsProvider(KeycloakSession session, FreeMarkerUtil freeMarker) {
@@ -123,7 +106,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
public Response createResponse(UserModel.RequiredAction action) {
RealmModel realm = session.getContext().getRealm();
- UriInfo uriInfo = session.getContext().getUri();
String actionMessage;
LoginFormsPages page;
@@ -145,20 +127,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
page = LoginFormsPages.LOGIN_UPDATE_PASSWORD;
break;
case VERIFY_EMAIL:
- try {
- UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
- builder.queryParam(OAuth2Constants.CODE, accessCode);
- builder.queryParam(Constants.KEY, clientSession.getNote(Constants.VERIFY_EMAIL_KEY));
-
- String link = builder.build(realm.getName()).toString();
- long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
-
- session.getProvider(EmailTemplateProvider.class).setRealm(realm).setUser(user).sendVerifyEmail(link, expiration);
- } catch (EmailException e) {
- logger.error("Failed to send verification email", e);
- return setError(Messages.EMAIL_SENT_ERROR).createErrorPage();
- }
-
actionMessage = Messages.VERIFY_EMAIL;
page = LoginFormsPages.LOGIN_VERIFY_EMAIL;
break;
@@ -182,6 +150,17 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
String requestURI = uriInfo.getBaseUri().getPath();
UriBuilder uriBuilder = UriBuilder.fromUri(requestURI);
+ if (page == LoginFormsPages.OAUTH_GRANT) {
+ // for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param
+ uriBuilder.replaceQuery(null);
+ }
+
+ URI baseUri = uriBuilder.build();
+
+ if (accessCode != null) {
+ uriBuilder.queryParam(OAuth2Constants.CODE, accessCode);
+ }
+ URI baseUriWithCode = uriBuilder.build();
for (String k : queryParameterMap.keySet()) {
@@ -190,10 +169,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
uriBuilder.replaceQueryParam(k, objects);
}
- if (accessCode != null) {
- uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode);
- }
-
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme;
try {
@@ -235,11 +210,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
attributes.put("messagesPerField", messagesPerField);
- if (page == LoginFormsPages.OAUTH_GRANT) {
- // for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param
- uriBuilder.replaceQuery(null);
- }
- URI baseUri = uriBuilder.build();
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
if (realm != null && user != null && session != null) {
attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session));
@@ -250,7 +220,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
- attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
+ attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUriWithCode));
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
@@ -298,7 +268,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("register", new RegisterBean(formData));
break;
case OAUTH_GRANT:
- attributes.put("oauth", new OAuthGrantBean(accessCode, clientSession, client, realmRolesRequested, resourceRolesRequested, protocolMappersRequested, this.accessRequestMessage));
+ attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested, protocolMappersRequested, this.accessRequestMessage));
attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
break;
case CODE:
@@ -342,10 +312,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
if (objects.length == 1 && objects[0] == null) continue; //
uriBuilder.replaceQueryParam(k, objects);
}
+
+ URI baseUri = uriBuilder.build();
+
if (accessCode != null) {
- uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode);
+ uriBuilder.queryParam(OAuth2Constants.CODE, accessCode);
}
- URI baseUri = uriBuilder.build();
+ URI baseUriWithCode = uriBuilder.build();
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme;
@@ -398,7 +371,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
- attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
+ attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUriWithCode));
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
@@ -467,6 +440,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
@Override
+ public Response createLoginExpiredPage() {
+ return createResponse(LoginFormsPages.LOGIN_PAGE_EXPIRED);
+ }
+
+ @Override
public Response createIdpLinkEmailPage() {
BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT);
String idpAlias = brokerContext.getIdpConfig().getAlias();
@@ -485,8 +463,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
@Override
- public Response createOAuthGrant(ClientSessionModel clientSession) {
- this.clientSession = clientSession;
+ public Response createOAuthGrant() {
return createResponse(LoginFormsPages.OAUTH_GRANT);
}
@@ -593,12 +570,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
@Override
- public LoginFormsProvider setClientSession(ClientSessionModel clientSession) {
- this.clientSession = clientSession;
- return this;
- }
-
- @Override
public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested, List<ProtocolMapperModel> protocolMappersRequested) {
this.realmRolesRequested = realmRolesRequested;
this.resourceRolesRequested = resourceRolesRequested;
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java
index 696e19b..986d0ef 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java
@@ -42,7 +42,7 @@ public class IdentityProviderBean {
private RealmModel realm;
private final KeycloakSession session;
- public IdentityProviderBean(RealmModel realm, KeycloakSession session, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) {
+ public IdentityProviderBean(RealmModel realm, KeycloakSession session, List<IdentityProviderModel> identityProviders, URI baseURI) {
this.realm = realm;
this.session = session;
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/OAuthGrantBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/OAuthGrantBean.java
index 556db25..bf424cb 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/OAuthGrantBean.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/OAuthGrantBean.java
@@ -18,7 +18,6 @@ package org.keycloak.forms.login.freemarker.model;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleModel;
@@ -38,7 +37,7 @@ public class OAuthGrantBean {
private ClientModel client;
private List<String> claimsRequested;
- public OAuthGrantBean(String code, ClientSessionModel clientSession, ClientModel client, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested,
+ public OAuthGrantBean(String code, ClientModel client, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested,
List<ProtocolMapperModel> protocolMappersRequested, String accessRequestMessage) {
this.code = code;
this.client = client;
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java
index a622c22..0c574c1 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java
@@ -50,6 +50,10 @@ public class UrlBean {
return Urls.realmLoginPage(baseURI, realm).toString();
}
+ public String getLoginRestartFlowUrl() {
+ return Urls.realmLoginRestartPage(baseURI, realm).toString();
+ }
+
public String getRegistrationAction() {
if (this.actionuri != null) {
return this.actionuri.toString();
@@ -81,10 +85,6 @@ public class UrlBean {
return Urls.loginUsernameReminder(baseURI, realm).toString();
}
- public String getLoginEmailVerificationUrl() {
- return Urls.loginActionEmailVerification(baseURI, realm).toString();
- }
-
public String getFirstBrokerLoginUrl() {
return Urls.firstBrokerLoginProcessor(baseURI, realm).toString();
}
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
index e28c627..f2a9d75 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
@@ -54,6 +54,8 @@ public class Templates {
return "login-update-profile.ftl";
case CODE:
return "code.ftl";
+ case LOGIN_PAGE_EXPIRED:
+ return "login-page-expired.ftl";
default:
throw new IllegalArgumentException();
}
diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
index 0c1462c..9c1e5a5 100755
--- a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
+++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
@@ -17,19 +17,25 @@
package org.keycloak.protocol;
+import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationFlowModel;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol.Error;
-import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.AuthenticationSessionManager;
+import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.resources.LoginActionsService;
+import org.keycloak.services.util.CacheControlUtil;
+import org.keycloak.services.util.AuthenticationFlowURLHelper;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
@@ -43,6 +49,10 @@ import javax.ws.rs.core.UriInfo;
*/
public abstract class AuthorizationEndpointBase {
+ private static final Logger logger = Logger.getLogger(AuthorizationEndpointBase.class);
+
+ public static final String APP_INITIATED_FLOW = "APP_INITIATED_FLOW";
+
protected RealmModel realm;
protected EventBuilder event;
protected AuthenticationManager authManager;
@@ -63,9 +73,9 @@ public abstract class AuthorizationEndpointBase {
this.event = event;
}
- protected AuthenticationProcessor createProcessor(ClientSessionModel clientSession, String flowId, String flowPath) {
+ protected AuthenticationProcessor createProcessor(AuthenticationSessionModel authSession, String flowId, String flowPath) {
AuthenticationProcessor processor = new AuthenticationProcessor();
- processor.setClientSession(clientSession)
+ processor.setAuthenticationSession(authSession)
.setFlowPath(flowPath)
.setFlowId(flowId)
.setBrowserFlow(true)
@@ -75,48 +85,54 @@ public abstract class AuthorizationEndpointBase {
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
+
+ authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, flowPath);
+
return processor;
}
/**
* Common method to handle browser authentication request in protocols unified way.
*
- * @param clientSession for current request
+ * @param authSession for current request
* @param protocol handler for protocol used to initiate login
* @param isPassive set to true if login should be passive (without login screen shown)
* @param redirectToAuthentication if true redirect to flow url. If initial call to protocol is a POST, you probably want to do this. This is so we can disable the back button on browser
* @return response to be returned to the browser
*/
- protected Response handleBrowserAuthenticationRequest(ClientSessionModel clientSession, LoginProtocol protocol, boolean isPassive, boolean redirectToAuthentication) {
+ protected Response handleBrowserAuthenticationRequest(AuthenticationSessionModel authSession, LoginProtocol protocol, boolean isPassive, boolean redirectToAuthentication) {
AuthenticationFlowModel flow = getAuthenticationFlow();
String flowId = flow.getId();
- AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
- event.detail(Details.CODE_ID, clientSession.getId());
+ AuthenticationProcessor processor = createProcessor(authSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
+ event.detail(Details.CODE_ID, authSession.getId());
if (isPassive) {
// OIDC prompt == NONE or SAML 2 IsPassive flag
// This means that client is just checking if the user is already completely logged in.
// We cancel login if any authentication action or required action is required
try {
if (processor.authenticateOnly() == null) {
- processor.attachSession();
+ // processor.attachSession();
} else {
- Response response = protocol.sendError(clientSession, Error.PASSIVE_LOGIN_REQUIRED);
- session.sessions().removeClientSession(realm, clientSession);
+ Response response = protocol.sendError(authSession, Error.PASSIVE_LOGIN_REQUIRED);
return response;
}
- if (processor.isActionRequired()) {
- Response response = protocol.sendError(clientSession, Error.PASSIVE_INTERACTION_REQUIRED);
- session.sessions().removeClientSession(realm, clientSession);
- return response;
+ AuthenticationManager.setRolesAndMappersInSession(authSession);
+
+ if (processor.nextRequiredAction() != null) {
+ Response response = protocol.sendError(authSession, Error.PASSIVE_INTERACTION_REQUIRED);
+ return response;
}
+
+ // Attach session once no requiredActions or other things are required
+ processor.attachSession();
} catch (Exception e) {
return processor.handleBrowserException(e);
}
return processor.finishAuthentication(protocol);
} else {
try {
- RestartLoginCookie.setRestartCookie(session, realm, clientConnection, uriInfo, clientSession);
+ RestartLoginCookie.setRestartCookie(session, realm, clientConnection, uriInfo, authSession);
if (redirectToAuthentication) {
return processor.redirectToFlow();
}
@@ -131,4 +147,111 @@ public abstract class AuthorizationEndpointBase {
return realm.getBrowserFlow();
}
+
+ protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) {
+ AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
+ String authSessionId = manager.getCurrentAuthenticationSessionId(realm);
+ AuthenticationSessionModel authSession = authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
+
+ if (authSession != null) {
+
+ ClientSessionCode<AuthenticationSessionModel> check = new ClientSessionCode<>(session, realm, authSession);
+ if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
+
+ logger.debugf("Authentication session '%s' exists, but is expired. Restart existing authentication session", authSession.getId());
+ authSession.restartSession(realm, client);
+ return new AuthorizationEndpointChecks(authSession);
+
+ } else if (isNewRequest(authSession, client, requestState)) {
+ // Check if we have lastProcessedExecution and restart the session just if yes. Otherwise update just client information from the AuthorizationEndpoint request.
+ // This difference is needed, because of logout from JS applications in multiple browser tabs.
+ if (hasProcessedExecution(authSession)) {
+ logger.debug("New request from application received, but authentication session already exists. Restart existing authentication session");
+ authSession.restartSession(realm, client);
+ } else {
+ logger.debug("New request from application received, but authentication session already exists. Update client information in existing authentication session");
+ authSession.clearClientNotes(); // update client data
+ authSession.updateClient(client);
+ }
+
+ return new AuthorizationEndpointChecks(authSession);
+
+ } else {
+ logger.debug("Re-sent some previous request to Authorization endpoint. Likely browser 'back' or 'refresh' button.");
+
+ // See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form
+ if (!shouldShowExpirePage(authSession)) {
+ return new AuthorizationEndpointChecks(authSession);
+ } else {
+ CacheControlUtil.noBackButtonCacheControlHeader();
+
+ Response response = new AuthenticationFlowURLHelper(session, realm, uriInfo)
+ .showPageExpired(authSession);
+ return new AuthorizationEndpointChecks(response);
+ }
+ }
+ }
+
+ UserSessionModel userSession = authSessionId==null ? null : session.sessions().getUserSession(realm, authSessionId);
+
+ if (userSession != null) {
+ logger.debugf("Sent request to authz endpoint. We don't have authentication session with ID '%s' but we have userSession. Will re-create authentication session with same ID", authSessionId);
+ authSession = session.authenticationSessions().createAuthenticationSession(authSessionId, realm, client);
+ } else {
+ authSession = manager.createAuthenticationSession(realm, client, true);
+ logger.debugf("Sent request to authz endpoint. Created new authentication session with ID '%s'", authSession.getId());
+ }
+
+ return new AuthorizationEndpointChecks(authSession);
+
+ }
+
+ private boolean hasProcessedExecution(AuthenticationSessionModel authSession) {
+ String lastProcessedExecution = authSession.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION);
+ return (lastProcessedExecution != null);
+ }
+
+ // See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form
+ private boolean shouldShowExpirePage(AuthenticationSessionModel authSession) {
+ if (hasProcessedExecution(authSession)) {
+ return true;
+ }
+
+ String initialFlow = authSession.getClientNote(APP_INITIATED_FLOW);
+ if (initialFlow == null) {
+ initialFlow = LoginActionsService.AUTHENTICATE_PATH;
+ }
+
+ String lastFlow = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
+ // Check if we transitted between flows (eg. clicking "register" on login screen and then clicking browser 'back', which showed this page)
+ if (!initialFlow.equals(lastFlow) && AuthenticationSessionModel.Action.AUTHENTICATE.toString().equals(authSession.getAction())) {
+ logger.debugf("Transition between flows! Current flow: %s, Previous flow: %s", initialFlow, lastFlow);
+
+ authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, initialFlow);
+ authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+ return false;
+ }
+
+ return false;
+ }
+
+ // Try to see if it is new request from the application, or refresh of some previous request
+ protected abstract boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String requestState);
+
+
+ protected static class AuthorizationEndpointChecks {
+ public final AuthenticationSessionModel authSession;
+ public final Response response;
+
+ private AuthorizationEndpointChecks(Response response) {
+ this.authSession = null;
+ this.response = response;
+ }
+
+ private AuthorizationEndpointChecks(AuthenticationSessionModel authSession) {
+ this.authSession = authSession;
+ this.response = null;
+ }
+ }
+
}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 1588321..26d012b 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -28,7 +28,6 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@@ -44,6 +43,7 @@ import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.util.CacheControlUtil;
+import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
import javax.ws.rs.GET;
@@ -63,12 +63,12 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
public static final String CODE_AUTH_TYPE = "code";
/**
- * Prefix used to store additional HTTP GET params from original client request into {@link ClientSessionModel} note to be available later in Authenticators, RequiredActions etc. Prefix is used to
+ * Prefix used to store additional HTTP GET params from original client request into {@link AuthenticationSessionModel} note to be available later in Authenticators, RequiredActions etc. Prefix is used to
* prevent collisions with internally used notes.
*
- * @see ClientSessionModel#getNote(String)
+ * @see AuthenticationSessionModel#getClientNote(String)
*/
- public static final String CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX = "client_request_param_";
+ public static final String LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX = "client_request_param_";
// https://tools.ietf.org/html/rfc7636#section-4.2
private static final Pattern VALID_CODE_CHALLENGE_PATTERN = Pattern.compile("^[0-9a-zA-Z\\-\\.~_]+$");
@@ -78,7 +78,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
}
private ClientModel client;
- private ClientSessionModel clientSession;
+ private AuthenticationSessionModel authenticationSession;
private Action action;
private OIDCResponseType parsedResponseType;
@@ -125,7 +125,14 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
return errorResponse;
}
- createClientSession();
+ AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, request.getState());
+ if (checks.response != null) {
+ return checks.response;
+ }
+
+ authenticationSession = checks.authSession;
+ updateAuthenticationSession();
+
// So back button doesn't work
CacheControlUtil.noBackButtonCacheControlHeader();
switch (action) {
@@ -162,6 +169,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
return this;
}
+
private void checkSsl() {
if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
event.error(Errors.SSL_REQUIRED);
@@ -356,44 +364,62 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
}
}
- private void createClientSession() {
- clientSession = session.sessions().createClientSession(realm, client);
- clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
- clientSession.setRedirectUri(redirectUri);
- clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
- clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, request.getResponseType());
- clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam());
- clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
-
- if (request.getState() != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, request.getState());
- if (request.getNonce() != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce());
- if (request.getMaxAge() != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge()));
- if (request.getScope() != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope());
- if (request.getLoginHint() != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint());
- if (request.getPrompt() != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
- if (request.getIdpHint() != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
- if (request.getResponseMode() != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
+
+ @Override
+ protected boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String stateFromRequest) {
+ if (stateFromRequest==null) {
+ return true;
+ }
+
+ // Check if it's different client
+ if (!clientFromRequest.equals(authSession.getClient())) {
+ return true;
+ }
+
+ // If state is same, we likely have the refresh of some previous request
+ String stateFromSession = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
+ return !stateFromRequest.equals(stateFromSession);
+ }
+
+
+ private void updateAuthenticationSession() {
+ authenticationSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ authenticationSession.setRedirectUri(redirectUri);
+ authenticationSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
+ authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, request.getResponseType());
+ authenticationSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam());
+ authenticationSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+
+ if (request.getState() != null) authenticationSession.setClientNote(OIDCLoginProtocol.STATE_PARAM, request.getState());
+ if (request.getNonce() != null) authenticationSession.setClientNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce());
+ if (request.getMaxAge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge()));
+ if (request.getScope() != null) authenticationSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope());
+ if (request.getLoginHint() != null) authenticationSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint());
+ if (request.getPrompt() != null) authenticationSession.setClientNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
+ if (request.getIdpHint() != null) authenticationSession.setClientNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
+ if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
// https://tools.ietf.org/html/rfc7636#section-4
- if (request.getCodeChallenge() != null) clientSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
+ if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
if (request.getCodeChallengeMethod() != null) {
- clientSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, request.getCodeChallengeMethod());
+ authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, request.getCodeChallengeMethod());
} else {
- clientSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, OIDCLoginProtocol.PKCE_METHOD_PLAIN);
+ authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, OIDCLoginProtocol.PKCE_METHOD_PLAIN);
}
if (request.getAdditionalReqParams() != null) {
for (String paramName : request.getAdditionalReqParams().keySet()) {
- clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName));
+ authenticationSession.setClientNote(LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName));
}
}
}
+
private Response buildAuthorizationCodeAuthorizationResponse() {
this.event.event(EventType.LOGIN);
- clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);
+ authenticationSession.setAuthNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);
- return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(request.getPrompt(), OIDCLoginProtocol.PROMPT_VALUE_NONE), false);
+ return handleBrowserAuthenticationRequest(authenticationSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(request.getPrompt(), OIDCLoginProtocol.PROMPT_VALUE_NONE), false);
}
private Response buildRegister() {
@@ -402,7 +428,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
AuthenticationFlowModel flow = realm.getRegistrationFlow();
String flowId = flow.getId();
- AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.REGISTRATION_PATH);
+ AuthenticationProcessor processor = createProcessor(authenticationSession, flowId, LoginActionsService.REGISTRATION_PATH);
+ authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.REGISTRATION_PATH);
return processor.authenticate();
}
@@ -413,7 +440,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
AuthenticationFlowModel flow = realm.getResetCredentialsFlow();
String flowId = flow.getId();
- AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.RESET_CREDENTIALS_PATH);
+ AuthenticationProcessor processor = createProcessor(authenticationSession, flowId, LoginActionsService.RESET_CREDENTIALS_PATH);
+ authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.REGISTRATION_PATH);
return processor.authenticate();
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 8fa4341..83570ef 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -32,13 +32,12 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
-import org.keycloak.models.UserSessionProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
@@ -48,10 +47,12 @@ import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.Cors;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST;
@@ -62,7 +63,6 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
-import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -175,6 +175,8 @@ public class TokenEndpoint {
if (client.isBearerOnly()) {
throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Bearer-only not allowed", Response.Status.BAD_REQUEST);
}
+
+
}
private void checkGrantType() {
@@ -208,29 +210,35 @@ public class TokenEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
}
- ClientSessionCode.ParseResult parseResult = ClientSessionCode.parseResult(code, session, realm);
- if (parseResult.isClientSessionNotFound() || parseResult.isIllegalHash()) {
- String[] parts = code.split("\\.");
- if (parts.length == 2) {
- event.detail(Details.CODE_ID, parts[1]);
- }
+ String[] parts = code.split("\\.");
+ if (parts.length == 4) {
+ event.detail(Details.CODE_ID, parts[2]);
+ }
+
+ ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, session, realm, AuthenticatedClientSessionModel.class);
+ if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) {
event.error(Errors.INVALID_CODE);
- if (parseResult.getClientSession() != null) {
- session.sessions().removeClientSession(realm, parseResult.getClientSession());
+
+ // Attempt to use same code twice should invalidate existing clientSession
+ AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
+ if (clientSession != null) {
+ clientSession.setUserSession(null);
}
+
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code not valid", Response.Status.BAD_REQUEST);
}
- ClientSessionModel clientSession = parseResult.getClientSession();
- event.detail(Details.CODE_ID, clientSession.getId());
+ AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
- if (!parseResult.getCode().isValid(ClientSessionModel.Action.CODE_TO_TOKEN.name(), ClientSessionCode.ActionType.CLIENT)) {
+ if (!parseResult.getCode().isValid(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), ClientSessionCode.ActionType.CLIENT)) {
event.error(Errors.INVALID_CODE);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code is expired", Response.Status.BAD_REQUEST);
}
+ // TODO: This shouldn't be needed to write into the AuthenticatedClientSessionModel itself
parseResult.getCode().setAction(null);
+ // TODO: Maybe rather create userSession even at this stage?
UserSessionModel userSession = clientSession.getUserSession();
if (userSession == null) {
@@ -355,7 +363,8 @@ public class TokenEndpoint {
if (!result.isOfflineToken()) {
UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState());
- updateClientSessions(userSession.getClientSessions());
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+ updateClientSession(clientSession);
updateUserSessionFromClientAuth(userSession);
}
@@ -369,7 +378,7 @@ public class TokenEndpoint {
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
- private void updateClientSession(ClientSessionModel clientSession) {
+ private void updateClientSession(AuthenticatedClientSessionModel clientSession) {
if(clientSession == null) {
ServicesLogger.LOGGER.clientSessionNull();
@@ -388,26 +397,6 @@ public class TokenEndpoint {
}
}
- private void updateClientSessions(List<ClientSessionModel> clientSessions) {
- if(clientSessions == null) {
- ServicesLogger.LOGGER.clientSessionNull();
- return;
- }
- for (ClientSessionModel clientSession : clientSessions) {
- if(clientSession == null) {
- ServicesLogger.LOGGER.clientSessionNull();
- continue;
- }
- if(clientSession.getClient() == null) {
- ServicesLogger.LOGGER.clientModelNull();
- continue;
- }
- if(client.getId().equals(clientSession.getClient().getId())) {
- updateClientSession(clientSession);
- }
- }
- }
-
private void updateUserSessionFromClientAuth(UserSessionModel userSession) {
for (Map.Entry<String, String> attr : clientAuthAttributes.entrySet()) {
userSession.setNote(attr.getKey(), attr.getValue());
@@ -428,17 +417,16 @@ public class TokenEndpoint {
}
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
- UserSessionProvider sessions = session.sessions();
- ClientSessionModel clientSession = sessions.createClientSession(realm, client);
- clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
- clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
- clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
- clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
+ AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false);
+ authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ authSession.setAction(AuthenticatedClientSessionModel.Action.AUTHENTICATE.name());
+ authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+ authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
AuthenticationFlowModel flow = realm.getDirectGrantFlow();
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
- processor.setClientSession(clientSession)
+ processor.setAuthenticationSession(authSession)
.setFlowId(flowId)
.setConnection(clientConnection)
.setEventBuilder(event)
@@ -449,13 +437,16 @@ public class TokenEndpoint {
Response challenge = processor.authenticateOnly();
if (challenge != null) return challenge;
processor.evaluateRequiredActionTriggers();
- UserModel user = clientSession.getAuthenticatedUser();
+ UserModel user = authSession.getAuthenticatedUser();
if (user.getRequiredActions() != null && user.getRequiredActions().size() > 0) {
event.error(Errors.RESOLVE_REQUIRED_ACTIONS);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Account is not fully set up", Response.Status.BAD_REQUEST);
}
- processor.attachSession();
+
+ AuthenticationManager.setRolesAndMappersInSession(authSession);
+
+ AuthenticatedClientSessionModel clientSession = processor.attachSession();
UserSessionModel userSession = processor.getUserSession();
updateUserSessionFromClientAuth(userSession);
@@ -505,17 +496,17 @@ public class TokenEndpoint {
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
- UserSessionProvider sessions = session.sessions();
-
- ClientSessionModel clientSession = sessions.createClientSession(realm, client);
- clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
- clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
- clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
+ AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false);
+ authSession.setAuthenticatedUser(clientUser);
+ authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+ authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
- UserSessionModel userSession = sessions.createUserSession(realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
+ UserSessionModel userSession = session.sessions().createUserSession(authSession.getId(), realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
event.session(userSession);
- TokenManager.attachClientSession(userSession, clientSession);
+ AuthenticationManager.setRolesAndMappersInSession(authSession);
+ AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(session, userSession, authSession);
// Notes about client details
userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
index 8984a4d..6ee2be3 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
@@ -29,6 +29,7 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
@@ -139,24 +140,7 @@ public class UserInfoEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Token invalid: " + e.getMessage(), Response.Status.UNAUTHORIZED);
}
- UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
- ClientSessionModel clientSession = session.sessions().getClientSession(token.getClientSession());
- if( userSession == null ) {
- userSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
- if( AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
- clientSession = session.sessions().getOfflineClientSession(realm, token.getClientSession());
- } else {
- userSession = null;
- clientSession = null;
- }
- }
-
- if (userSession == null) {
- event.error(Errors.USER_SESSION_NOT_FOUND);
- throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found", Response.Status.BAD_REQUEST);
- }
-
- event.session(userSession);
+ UserSessionModel userSession = findValidSession(token, event);
UserModel userModel = userSession.getUser();
if (userModel == null) {
@@ -168,11 +152,6 @@ public class UserInfoEndpoint {
.detail(Details.USERNAME, userModel.getUsername());
- if (clientSession == null || !AuthenticationManager.isSessionValid(realm, userSession)) {
- event.error(Errors.SESSION_EXPIRED);
- throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED);
- }
-
ClientModel clientModel = realm.getClientByClientId(token.getIssuedFor());
if (clientModel == null) {
event.error(Errors.CLIENT_NOT_FOUND);
@@ -186,6 +165,12 @@ public class UserInfoEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client disabled", Response.Status.BAD_REQUEST);
}
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientModel.getId());
+ if (clientSession == null) {
+ event.error(Errors.SESSION_EXPIRED);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED);
+ }
+
AccessToken userInfo = new AccessToken();
tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession);
@@ -224,4 +209,34 @@ public class UserInfoEndpoint {
return Cors.add(request, responseBuilder).auth().allowedOrigins(token).build();
}
+
+ private UserSessionModel findValidSession(AccessToken token, EventBuilder event) {
+ UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
+ UserSessionModel offlineUserSession = null;
+ if (AuthenticationManager.isSessionValid(realm, userSession)) {
+ event.session(userSession);
+ return userSession;
+ } else {
+ offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
+ if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) {
+ event.session(offlineUserSession);
+ return offlineUserSession;
+ }
+ }
+
+ if (userSession == null && offlineUserSession == null) {
+ event.error(Errors.USER_SESSION_NOT_FOUND);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found", Response.Status.BAD_REQUEST);
+ }
+
+ if (userSession != null) {
+ event.session(userSession);
+ } else {
+ event.session(offlineUserSession);
+ }
+
+ event.error(Errors.SESSION_EXPIRED);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED);
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java
index efe9434..d267f91 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java
@@ -18,7 +18,7 @@
package org.keycloak.protocol.oidc.mappers;
import org.keycloak.Config;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel;
@@ -61,7 +61,7 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper {
}
public AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession) {
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
if (!OIDCAttributeMapperHelper.includeInUserInfo(mappingModel)) {
return token;
@@ -72,7 +72,7 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper {
}
public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession) {
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
if (!OIDCAttributeMapperHelper.includeInAccessToken(mappingModel)){
return token;
@@ -83,7 +83,7 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper {
}
public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession) {
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
if (!OIDCAttributeMapperHelper.includeInIDToken(mappingModel)){
return token;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java
index 4666034..09b39c4 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java
@@ -1,7 +1,7 @@
package org.keycloak.protocol.oidc.mappers;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperContainerModel;
import org.keycloak.models.ProtocolMapperModel;
@@ -64,19 +64,19 @@ public abstract class AbstractPairwiseSubMapper extends AbstractOIDCProtocolMapp
}
@Override
- public final IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public final IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
return token;
}
@Override
- public final AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public final AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
return token;
}
@Override
- public final AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public final AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId()));
return token;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
index f4ef89d..d239951 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java
@@ -93,9 +93,9 @@ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper
// get a set of all realm roles assigned to the user or its group
Stream<RoleModel> clientUserRoles = getAllUserRolesStream(user).filter(restriction);
- boolean dontLimitScope = userSession.getClientSessions().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed());
+ boolean dontLimitScope = userSession.getAuthenticatedClientSessions().values().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed());
if (! dontLimitScope) {
- Set<RoleModel> clientRoles = userSession.getClientSessions().stream()
+ Set<RoleModel> clientRoles = userSession.getAuthenticatedClientSessions().values().stream()
.flatMap(cs -> cs.getClient().getScopeMappings().stream())
.collect(Collectors.toSet());
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java
index 1e4ad9d..1e9b3e2 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java
@@ -17,14 +17,11 @@
package org.keycloak.protocol.oidc.mappers;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
-import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import java.util.ArrayList;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java
index 41dbb47..b733f5c 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java
@@ -17,15 +17,12 @@
package org.keycloak.protocol.oidc.mappers;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.GroupModel;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
-import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import java.util.ArrayList;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedClaim.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedClaim.java
index 4062824..8d48ccf 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedClaim.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedClaim.java
@@ -17,13 +17,10 @@
package org.keycloak.protocol.oidc.mappers;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
-import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import java.util.ArrayList;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java
index 03ecb91..19ff925 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java
@@ -17,7 +17,7 @@
package org.keycloak.protocol.oidc.mappers;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
@@ -82,7 +82,7 @@ public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAcc
@Override
public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession) {
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
String role = mappingModel.getConfig().get(ROLE_CONFIG);
String[] scopedRole = KeycloakModelUtils.parseRole(role);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java
index 71dce26..e7e0b7b 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java
@@ -17,7 +17,7 @@
package org.keycloak.protocol.oidc.mappers;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
@@ -30,5 +30,5 @@ import org.keycloak.representations.AccessToken;
public interface OIDCAccessTokenMapper {
AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession);
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java
index dabc4a3..54f380b 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java
@@ -17,7 +17,7 @@
package org.keycloak.protocol.oidc.mappers;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
@@ -30,5 +30,5 @@ import org.keycloak.representations.IDToken;
public interface OIDCIDTokenMapper {
IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession);
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java
index fcdc373..d41063b 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java
@@ -17,7 +17,7 @@
package org.keycloak.protocol.oidc.mappers;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
@@ -25,7 +25,6 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.AccessToken;
-import org.keycloak.representations.IDToken;
import java.util.ArrayList;
import java.util.HashMap;
@@ -90,7 +89,7 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc
@Override
public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession) {
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
String role = mappingModel.getConfig().get(ROLE_CONFIG);
String newName = mappingModel.getConfig().get(NEW_ROLE_NAME);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java
index e6d0d20..9b2cf0f 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java
@@ -17,15 +17,12 @@
package org.keycloak.protocol.oidc.mappers;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.provider.ProviderConfigProperty;
-import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import java.util.ArrayList;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java
index 67ac1a2..e1fc17e 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java
@@ -17,7 +17,7 @@
package org.keycloak.protocol.oidc.mappers;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
@@ -29,5 +29,5 @@ import org.keycloak.representations.AccessToken;
public interface UserInfoTokenMapper {
AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession);
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserPropertyMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserPropertyMapper.java
index 6fd6491..2fc84ff 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserPropertyMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserPropertyMapper.java
@@ -17,14 +17,11 @@
package org.keycloak.protocol.oidc.mappers;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.provider.ProviderConfigProperty;
-import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import java.util.ArrayList;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserSessionNoteMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserSessionNoteMapper.java
index fd6bfe1..aadee6c 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserSessionNoteMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserSessionNoteMapper.java
@@ -17,14 +17,11 @@
package org.keycloak.protocol.oidc.mappers;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
-import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import java.util.ArrayList;
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 4c0691a..13d24a7 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -23,8 +23,8 @@ import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
@@ -36,8 +36,11 @@ 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.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager;
+import org.keycloak.sessions.CommonClientSessionModel;
+import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
import javax.ws.rs.core.HttpHeaders;
@@ -128,9 +131,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
}
- private void setupResponseTypeAndMode(ClientSessionModel clientSession) {
- String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
- String responseMode = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ private void setupResponseTypeAndMode(String responseType, String responseMode) {
this.responseType = OIDCResponseType.parse(responseType);
this.responseMode = OIDCResponseMode.parse(responseMode, this.responseType);
this.event.detail(Details.RESPONSE_TYPE, responseType);
@@ -169,9 +170,12 @@ public class OIDCLoginProtocol implements LoginProtocol {
@Override
- public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) {
- ClientSessionModel clientSession = accessCode.getClientSession();
- setupResponseTypeAndMode(clientSession);
+ public Response authenticated(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
+ ClientSessionCode<AuthenticatedClientSessionModel> accessCode = new ClientSessionCode<>(session, realm, clientSession);
+
+ String responseTypeParam = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ String responseModeParam = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ setupResponseTypeAndMode(responseTypeParam, responseModeParam);
String redirect = clientSession.getRedirectUri();
OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode);
@@ -182,7 +186,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
// Standard or hybrid flow
if (responseType.hasResponseType(OIDCResponseType.CODE)) {
- accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN.name());
+ accessCode.setAction(CommonClientSessionModel.Action.CODE_TO_TOKEN.name());
redirectUri.addParam(OAuth2Constants.CODE, accessCode.getCode());
}
@@ -227,16 +231,17 @@ public class OIDCLoginProtocol implements LoginProtocol {
@Override
- public Response sendError(ClientSessionModel clientSession, Error error) {
- setupResponseTypeAndMode(clientSession);
+ public Response sendError(AuthenticationSessionModel authSession, Error error) {
+ String responseTypeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ String responseModeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ setupResponseTypeAndMode(responseTypeParam, responseModeParam);
- String redirect = clientSession.getRedirectUri();
- String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM);
+ String redirect = authSession.getRedirectUri();
+ String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode).addParam(OAuth2Constants.ERROR, translateError(error));
if (state != null)
redirectUri.addParam(OAuth2Constants.STATE, state);
- session.sessions().removeClientSession(realm, clientSession);
- RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
+ new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
return redirectUri.build();
}
@@ -256,13 +261,13 @@ public class OIDCLoginProtocol implements LoginProtocol {
}
@Override
- public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
+ public void backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, client, clientSession);
}
@Override
- public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
+ public Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
// todo oidc redirect support
throw new RuntimeException("NOT IMPLEMENTED");
}
@@ -289,18 +294,18 @@ public class OIDCLoginProtocol implements LoginProtocol {
@Override
- public boolean requireReauthentication(UserSessionModel userSession, ClientSessionModel clientSession) {
- return isPromptLogin(clientSession) || isAuthTimeExpired(userSession, clientSession);
+ public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) {
+ return isPromptLogin(authSession) || isAuthTimeExpired(userSession, authSession);
}
- protected boolean isPromptLogin(ClientSessionModel clientSession) {
- String prompt = clientSession.getNote(OIDCLoginProtocol.PROMPT_PARAM);
+ protected boolean isPromptLogin(AuthenticationSessionModel authSession) {
+ String prompt = authSession.getClientNote(OIDCLoginProtocol.PROMPT_PARAM);
return TokenUtil.hasPrompt(prompt, OIDCLoginProtocol.PROMPT_VALUE_LOGIN);
}
- protected boolean isAuthTimeExpired(UserSessionModel userSession, ClientSessionModel clientSession) {
+ protected boolean isAuthTimeExpired(UserSessionModel userSession, AuthenticationSessionModel authSession) {
String authTime = userSession.getNote(AuthenticationManager.AUTH_TIME);
- String maxAge = clientSession.getNote(OIDCLoginProtocol.MAX_AGE_PARAM);
+ String maxAge = authSession.getClientNote(OIDCLoginProtocol.MAX_AGE_PARAM);
if (maxAge == null) {
return false;
}
@@ -310,7 +315,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
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);
+ authSession.getClient().getId(), maxAgeInt, authTimeInt);
return true;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 4cedd6b..9782b48 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -31,14 +31,13 @@ import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.HashProvider;
import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.models.ModelException;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@@ -58,8 +57,10 @@ import org.keycloak.representations.IDToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.UserSessionManager;
+import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
import org.keycloak.common.util.Time;
@@ -107,10 +108,10 @@ public class TokenManager {
public static class TokenValidation {
public final UserModel user;
public final UserSessionModel userSession;
- public final ClientSessionModel clientSession;
+ public final AuthenticatedClientSessionModel clientSession;
public final AccessToken newToken;
- public TokenValidation(UserModel user, UserSessionModel userSession, ClientSessionModel clientSession, AccessToken newToken) {
+ public TokenValidation(UserModel user, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession, AccessToken newToken) {
this.user = user;
this.userSession = userSession;
this.clientSession = clientSession;
@@ -129,29 +130,18 @@ public class TokenManager {
}
UserSessionModel userSession = null;
- ClientSessionModel clientSession = null;
if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
UserSessionManager sessionManager = new UserSessionManager(session);
- clientSession = sessionManager.findOfflineClientSession(realm, oldToken.getClientSession());
- if (clientSession != null) {
- userSession = clientSession.getUserSession();
-
- if (userSession == null) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not found", "Offline user session not found");
- }
-
- String userSessionId = oldToken.getSessionState();
- if (!userSessionId.equals(userSession.getId())) {
- throw new ModelException("User session don't match. Offline client session " + clientSession.getId() + ", It's user session " + userSession.getId() +
- " Wanted user session: " + userSessionId);
- }
+ userSession = sessionManager.findOfflineUserSession(realm, oldToken.getSessionState());
+ if (userSession != null) {
// Revoke timeouted offline userSession
if (userSession.getLastSessionRefresh() < Time.currentTime() - realm.getOfflineSessionIdleTimeout()) {
sessionManager.revokeOfflineUserSession(userSession);
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not active", "Offline user session session not active");
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline session not active", "Offline session not active");
}
+
}
} else {
// Find userSession regularly for online tokens
@@ -160,20 +150,14 @@ public class TokenManager {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true);
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
}
-
- for (ClientSessionModel clientSessionModel : userSession.getClientSessions()) {
- if (clientSessionModel.getId().equals(oldToken.getClientSession())) {
- clientSession = clientSessionModel;
- break;
- }
- }
}
- if (clientSession == null) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Client session not active", "Client session not active");
+ if (userSession == null) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not found", "Offline user session not found");
}
- ClientModel client = clientSession.getClient();
+ ClientModel client = session.getContext().getClient();
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
if (!client.getClientId().equals(oldToken.getIssuedFor())) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Unmatching clients", "Unmatching clients");
@@ -213,17 +197,23 @@ public class TokenManager {
return false;
}
+ ClientModel client = realm.getClientByClientId(token.getIssuedFor());
+ if (client == null || !client.isEnabled()) {
+ return false;
+ }
+
UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
if (AuthenticationManager.isSessionValid(realm, userSession)) {
- ClientSessionModel clientSession = session.sessions().getClientSession(realm, token.getClientSession());
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
if (clientSession != null) {
return true;
}
}
+
userSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
if (AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
- ClientSessionModel clientSession = session.sessions().getOfflineClientSession(realm, token.getClientSession());
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
if (clientSession != null) {
return true;
}
@@ -232,7 +222,8 @@ public class TokenManager {
return false;
}
- public RefreshResult refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient, String encodedRefreshToken, EventBuilder event, HttpHeaders headers) throws OAuthErrorException {
+ public RefreshResult refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient,
+ String encodedRefreshToken, EventBuilder event, HttpHeaders headers) throws OAuthErrorException {
RefreshToken refreshToken = verifyRefreshToken(session, realm, encodedRefreshToken);
event.user(refreshToken.getSubject()).session(refreshToken.getSessionState())
@@ -349,7 +340,8 @@ public class TokenManager {
}
}
- public AccessToken createClientAccessToken(KeycloakSession session, Set<RoleModel> requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public AccessToken createClientAccessToken(KeycloakSession session, Set<RoleModel> requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession,
+ AuthenticatedClientSessionModel clientSession) {
AccessToken token = initToken(realm, client, user, userSession, clientSession, session.getContext().getUri());
for (RoleModel role : requestedRoles) {
addComposites(token, role);
@@ -358,58 +350,49 @@ public class TokenManager {
return token;
}
- public static void attachClientSession(UserSessionModel session, ClientSessionModel clientSession) {
- if (clientSession.getUserSession() != null) {
- return;
- }
- UserModel user = session.getUser();
- clientSession.setUserSession(session);
- Set<String> requestedRoles = new HashSet<String>();
- // todo scope param protocol independent
- String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);
- ClientModel client = clientSession.getClient();
- for (RoleModel r : TokenManager.getAccess(scopeParam, true, client, user)) {
- requestedRoles.add(r.getId());
- }
- clientSession.setRoles(requestedRoles);
-
- Set<String> requestedProtocolMappers = new HashSet<String>();
- ClientTemplateModel clientTemplate = client.getClientTemplate();
- if (clientTemplate != null && client.useTemplateMappers()) {
- for (ProtocolMapperModel protocolMapper : clientTemplate.getProtocolMappers()) {
- if (protocolMapper.getProtocol().equals(clientSession.getAuthMethod())) {
- requestedProtocolMappers.add(protocolMapper.getId());
- }
- }
+ public static AuthenticatedClientSessionModel attachAuthenticationSession(KeycloakSession session, UserSessionModel userSession, AuthenticationSessionModel authSession) {
+ ClientModel client = authSession.getClient();
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+ if (clientSession == null) {
+ clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession);
}
- for (ProtocolMapperModel protocolMapper : client.getProtocolMappers()) {
- if (protocolMapper.getProtocol().equals(clientSession.getAuthMethod())) {
- requestedProtocolMappers.add(protocolMapper.getId());
- }
- }
- clientSession.setProtocolMappers(requestedProtocolMappers);
- Map<String, String> transferredNotes = clientSession.getUserSessionNotes();
+ clientSession.setRedirectUri(authSession.getRedirectUri());
+ clientSession.setProtocol(authSession.getProtocol());
+
+ clientSession.setRoles(authSession.getRoles());
+ clientSession.setProtocolMappers(authSession.getProtocolMappers());
+
+ Map<String, String> transferredNotes = authSession.getClientNotes();
for (Map.Entry<String, String> entry : transferredNotes.entrySet()) {
- session.setNote(entry.getKey(), entry.getValue());
+ clientSession.setNote(entry.getKey(), entry.getValue());
+ }
+
+ Map<String, String> transferredUserSessionNotes = authSession.getUserSessionNotes();
+ for (Map.Entry<String, String> entry : transferredUserSessionNotes.entrySet()) {
+ userSession.setNote(entry.getKey(), entry.getValue());
}
+ clientSession.setTimestamp(Time.currentTime());
+
+ // Remove authentication session now
+ new AuthenticationSessionManager(session).removeAuthenticationSession(userSession.getRealm(), authSession, true);
+
+ return clientSession;
}
- public static void dettachClientSession(UserSessionProvider sessions, RealmModel realm, ClientSessionModel clientSession) {
+ public static void dettachClientSession(UserSessionProvider sessions, RealmModel realm, AuthenticatedClientSessionModel clientSession) {
UserSessionModel userSession = clientSession.getUserSession();
if (userSession == null) {
return;
}
clientSession.setUserSession(null);
- clientSession.setRoles(null);
- clientSession.setProtocolMappers(null);
- if (userSession.getClientSessions().isEmpty()) {
+ if (userSession.getAuthenticatedClientSessions().isEmpty()) {
sessions.removeUserSession(realm, userSession);
}
}
@@ -543,8 +526,8 @@ public class TokenManager {
}
public AccessToken transformAccessToken(KeycloakSession session, AccessToken token, RealmModel realm, ClientModel client, UserModel user,
- UserSessionModel userSession, ClientSessionModel clientSession) {
- Set<ProtocolMapperModel> mappings = new ClientSessionCode(session, realm, clientSession).getRequestedProtocolMappers();
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
+ Set<ProtocolMapperModel> mappings = ClientSessionCode.getRequestedProtocolMappers(clientSession.getProtocolMappers(), client);
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
for (ProtocolMapperModel mapping : mappings) {
@@ -558,8 +541,8 @@ public class TokenManager {
}
public AccessToken transformUserInfoAccessToken(KeycloakSession session, AccessToken token, RealmModel realm, ClientModel client, UserModel user,
- UserSessionModel userSession, ClientSessionModel clientSession) {
- Set<ProtocolMapperModel> mappings = new ClientSessionCode(session, realm, clientSession).getRequestedProtocolMappers();
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
+ Set<ProtocolMapperModel> mappings = ClientSessionCode.getRequestedProtocolMappers(clientSession.getProtocolMappers(), client);
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
for (ProtocolMapperModel mapping : mappings) {
@@ -573,8 +556,8 @@ public class TokenManager {
}
public void transformIDToken(KeycloakSession session, IDToken token, RealmModel realm, ClientModel client, UserModel user,
- UserSessionModel userSession, ClientSessionModel clientSession) {
- Set<ProtocolMapperModel> mappings = new ClientSessionCode(session, realm, clientSession).getRequestedProtocolMappers();
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
+ Set<ProtocolMapperModel> mappings = ClientSessionCode.getRequestedProtocolMappers(clientSession.getProtocolMappers(), client);
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
for (ProtocolMapperModel mapping : mappings) {
@@ -585,9 +568,8 @@ public class TokenManager {
}
}
- protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user, UserSessionModel session, ClientSessionModel clientSession, UriInfo uriInfo) {
+ protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user, UserSessionModel session, AuthenticatedClientSessionModel clientSession, UriInfo uriInfo) {
AccessToken token = new AccessToken();
- if (clientSession != null) token.clientSession(clientSession.getId());
token.id(KeycloakModelUtils.generateId());
token.type(TokenUtil.TOKEN_TYPE_BEARER);
token.subject(user.getId());
@@ -607,9 +589,9 @@ public class TokenManager {
token.setAuthTime(Integer.parseInt(authTime));
}
- if (session != null) {
- token.setSessionState(session.getId());
- }
+
+ token.setSessionState(session.getId());
+
int tokenLifespan = getTokenLifespan(realm, clientSession);
if (tokenLifespan > 0) {
token.expiration(Time.currentTime() + tokenLifespan);
@@ -621,7 +603,7 @@ public class TokenManager {
return token;
}
- private int getTokenLifespan(RealmModel realm, ClientSessionModel clientSession) {
+ private int getTokenLifespan(RealmModel realm, AuthenticatedClientSessionModel clientSession) {
boolean implicitFlow = false;
String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
if (responseType != null) {
@@ -663,7 +645,7 @@ public class TokenManager {
return new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(token).sign(jwsAlgorithm, activeRsaKey.getPrivateKey());
}
- public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
return new AccessTokenResponseBuilder(realm, client, event, session, userSession, clientSession);
}
@@ -673,7 +655,7 @@ public class TokenManager {
EventBuilder event;
KeycloakSession session;
UserSessionModel userSession;
- ClientSessionModel clientSession;
+ AuthenticatedClientSessionModel clientSession;
AccessToken accessToken;
RefreshToken refreshToken;
@@ -682,7 +664,7 @@ public class TokenManager {
boolean generateAccessTokenHash = false;
String codeHash;
- public AccessTokenResponseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public AccessTokenResponseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
this.realm = realm;
this.client = client;
this.event = event;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
index 6e4498e..ffff19c 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java
@@ -53,6 +53,8 @@ public class AuthorizeClientUtil {
throw new ErrorResponseException("invalid_client", "Client authentication ended, but client is null", Response.Status.BAD_REQUEST);
}
+ session.getContext().setClient(client);
+
return new ClientAuthResult(client, processor.getClientAuthAttributes());
}
diff --git a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
index 51bdd81..0e488e1 100644
--- a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
+++ b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
@@ -23,24 +23,23 @@ import org.keycloak.common.ClientConnection;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.HMACProvider;
-import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.util.CookieHelper;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.crypto.SecretKey;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.UriInfo;
-import java.security.PublicKey;
import java.util.HashMap;
import java.util.Map;
/**
- * This is an an encoded token that is stored as a cookie so that if there is a client timeout, then the client session
+ * This is an an encoded token that is stored as a cookie so that if there is a client timeout, then the authentication session
* can be restarted.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -49,8 +48,6 @@ import java.util.Map;
public class RestartLoginCookie {
private static final Logger logger = Logger.getLogger(RestartLoginCookie.class);
public static final String KC_RESTART = "KC_RESTART";
- @JsonProperty("cs")
- protected String clientSession;
@JsonProperty("cid")
protected String clientId;
@@ -67,14 +64,6 @@ public class RestartLoginCookie {
@JsonProperty("notes")
protected Map<String, String> notes = new HashMap<>();
- public String getClientSession() {
- return clientSession;
- }
-
- public void setClientSession(String clientSession) {
- this.clientSession = clientSession;
- }
-
public Map<String, String> getNotes() {
return notes;
}
@@ -125,19 +114,18 @@ public class RestartLoginCookie {
public RestartLoginCookie() {
}
- public RestartLoginCookie(ClientSessionModel clientSession) {
+ public RestartLoginCookie(AuthenticationSessionModel clientSession) {
this.action = clientSession.getAction();
this.clientId = clientSession.getClient().getClientId();
- this.authMethod = clientSession.getAuthMethod();
+ this.authMethod = clientSession.getProtocol();
this.redirectUri = clientSession.getRedirectUri();
- this.clientSession = clientSession.getId();
- for (Map.Entry<String, String> entry : clientSession.getNotes().entrySet()) {
+ for (Map.Entry<String, String> entry : clientSession.getClientNotes().entrySet()) {
notes.put(entry.getKey(), entry.getValue());
}
}
- public static void setRestartCookie(KeycloakSession session, RealmModel realm, ClientConnection connection, UriInfo uriInfo, ClientSessionModel clientSession) {
- RestartLoginCookie restart = new RestartLoginCookie(clientSession);
+ public static void setRestartCookie(KeycloakSession session, RealmModel realm, ClientConnection connection, UriInfo uriInfo, AuthenticationSessionModel authSession) {
+ RestartLoginCookie restart = new RestartLoginCookie(authSession);
String encoded = restart.encode(session, realm);
String path = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
boolean secureOnly = realm.getSslRequired().isRequired(connection);
@@ -150,7 +138,8 @@ public class RestartLoginCookie {
CookieHelper.addCookie(KC_RESTART, "", path, null, null, 0, secureOnly, true);
}
- public static ClientSessionModel restartSession(KeycloakSession session, RealmModel realm, String code) throws Exception {
+
+ public static AuthenticationSessionModel restartSession(KeycloakSession session, RealmModel realm) throws Exception {
Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART);
if (cook == null) {
logger.debug("KC_RESTART cookie doesn't exist");
@@ -164,24 +153,18 @@ public class RestartLoginCookie {
return null;
}
RestartLoginCookie cookie = input.readJsonContent(RestartLoginCookie.class);
- String[] parts = code.split("\\.");
- String clientSessionId = parts[1];
- if (!clientSessionId.equals(cookie.getClientSession())) {
- logger.debug("RestartLoginCookie clientSession does not match code's clientSession");
- return null;
- }
ClientModel client = realm.getClientByClientId(cookie.getClientId());
if (client == null) return null;
- ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
- clientSession.setAuthMethod(cookie.getAuthMethod());
- clientSession.setRedirectUri(cookie.getRedirectUri());
- clientSession.setAction(cookie.getAction());
+ AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
+ authSession.setProtocol(cookie.getAuthMethod());
+ authSession.setRedirectUri(cookie.getRedirectUri());
+ authSession.setAction(cookie.getAction());
for (Map.Entry<String, String> entry : cookie.getNotes().entrySet()) {
- clientSession.setNote(entry.getKey(), entry.getValue());
+ authSession.setClientNote(entry.getKey(), entry.getValue());
}
- return clientSession;
+ return authSession;
}
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java
index 1a2db26..d193ee3 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java
@@ -19,7 +19,7 @@ package org.keycloak.protocol.saml.mappers;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
@@ -117,7 +117,7 @@ public class GroupMembershipMapper extends AbstractSAMLProtocolMapper implements
@Override
- public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
String single = mappingModel.getConfig().get(SINGLE_GROUP_ATTRIBUTE);
boolean singleAttribute = Boolean.parseBoolean(single);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedAttributeMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedAttributeMapper.java
index b8a6231..43c0241 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedAttributeMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedAttributeMapper.java
@@ -18,7 +18,7 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
@@ -76,7 +76,7 @@ public class HardcodedAttributeMapper extends AbstractSAMLProtocolMapper impleme
}
@Override
- public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
String attributeValue = mappingModel.getConfig().get(ATTRIBUTE_VALUE);
AttributeStatementHelper.addAttribute(attributeStatement, mappingModel, attributeValue);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java
index 5650333..3227350 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java
@@ -19,7 +19,7 @@ package org.keycloak.protocol.saml.mappers;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel;
@@ -111,14 +111,14 @@ public class RoleListMapper extends AbstractSAMLProtocolMapper implements SAMLRo
}
@Override
- public void mapRoles(AttributeStatementType roleAttributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public void mapRoles(AttributeStatementType roleAttributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
String single = mappingModel.getConfig().get(SINGLE_ROLE_ATTRIBUTE);
boolean singleAttribute = Boolean.parseBoolean(single);
List<SamlProtocol.ProtocolMapperProcessor<SAMLRoleNameMapper>> roleNameMappers = new LinkedList<>();
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
AttributeType singleAttributeType = null;
- Set<ProtocolMapperModel> requestedProtocolMappers = new ClientSessionCode(session, clientSession.getRealm(), clientSession).getRequestedProtocolMappers();
+ Set<ProtocolMapperModel> requestedProtocolMappers = ClientSessionCode.getRequestedProtocolMappers(clientSession.getProtocolMappers(), clientSession.getClient());
for (ProtocolMapperModel mapping : requestedProtocolMappers) {
ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLAttributeStatementMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLAttributeStatementMapper.java
index 48edfaa..a26b0e0 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLAttributeStatementMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLAttributeStatementMapper.java
@@ -18,7 +18,7 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
@@ -30,5 +30,5 @@ import org.keycloak.models.UserSessionModel;
public interface SAMLAttributeStatementMapper {
void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession);
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLLoginResponseMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLLoginResponseMapper.java
index cf5c9c8..1f962fe 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLLoginResponseMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLLoginResponseMapper.java
@@ -18,7 +18,7 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
@@ -30,5 +30,5 @@ import org.keycloak.models.UserSessionModel;
public interface SAMLLoginResponseMapper {
ResponseType transformLoginResponse(ResponseType response, ProtocolMapperModel mappingModel, KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession);
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLRoleListMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLRoleListMapper.java
index a822d8c..991c223 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLRoleListMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLRoleListMapper.java
@@ -18,7 +18,7 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
@@ -30,5 +30,5 @@ import org.keycloak.models.UserSessionModel;
public interface SAMLRoleListMapper {
void mapRoles(AttributeStatementType roleAttributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession);
+ UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
index f29d972..2579af1 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
@@ -18,7 +18,7 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
@@ -77,7 +77,7 @@ public class UserAttributeStatementMapper extends AbstractSAMLProtocolMapper imp
}
@Override
- public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
UserModel user = userSession.getUser();
String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
List<String> attributeValues = KeycloakModelUtils.resolveAttribute(user, attributeName);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java
index fd0de2a..1d7d038 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java
@@ -18,7 +18,7 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
@@ -76,7 +76,7 @@ public class UserPropertyAttributeStatementMapper extends AbstractSAMLProtocolMa
}
@Override
- public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
UserModel user = userSession.getUser();
String propertyName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
String propertyValue = ProtocolMapperUtils.getUserModelValue(user, propertyName);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserSessionNoteStatementMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserSessionNoteStatementMapper.java
index d6fd4d0..b4b24b5 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserSessionNoteStatementMapper.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserSessionNoteStatementMapper.java
@@ -18,7 +18,7 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
-import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
@@ -74,7 +74,7 @@ public class UserSessionNoteStatementMapper extends AbstractSAMLProtocolMapper i
}
@Override
- public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
String note = mappingModel.getConfig().get("note");
String value = userSession.getNote(note);
if (value == null) return;
diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
index ddaec72..f21eff3 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
@@ -104,7 +104,7 @@ public class HttpBasicAuthenticator implements AuthenticatorFactory {
boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password));
if (valid) {
- context.getClientSession().setAuthenticatedUser(user);
+ context.getAuthenticationSession().setAuthenticatedUser(user);
context.success();
} else {
context.getEvent().user(user);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java
index d2aaad6..b90a165 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java
@@ -20,8 +20,8 @@ package org.keycloak.protocol.saml.profile.ecp;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
@@ -35,6 +35,7 @@ import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ProcessingException;
+import org.keycloak.sessions.AuthenticationSessionModel;
import org.w3c.dom.Document;
import javax.ws.rs.core.Response;
@@ -85,15 +86,15 @@ public class SamlEcpProfileService extends SamlService {
}
@Override
- protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) {
- return super.newBrowserAuthentication(clientSession, isPassive, redirectToAuthentication, createEcpSamlProtocol());
+ protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) {
+ return super.newBrowserAuthentication(authSession, isPassive, redirectToAuthentication, createEcpSamlProtocol());
}
private SamlProtocol createEcpSamlProtocol() {
return new SamlProtocol() {
// method created to send a SOAP Binding response instead of a HTTP POST response
@Override
- protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException {
+ protected Response buildAuthenticatedResponse(AuthenticatedClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException {
Document document = bindingBuilder.postBinding(samlDocument).getDocument();
try {
@@ -113,7 +114,7 @@ public class SamlEcpProfileService extends SamlService {
}
}
- private void createRequestAuthenticatedHeader(ClientSessionModel clientSession, Soap.SoapMessageBuilder messageBuilder) {
+ private void createRequestAuthenticatedHeader(AuthenticatedClientSessionModel clientSession, Soap.SoapMessageBuilder messageBuilder) {
ClientModel client = clientSession.getClient();
if ("true".equals(client.getAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) {
@@ -133,7 +134,7 @@ public class SamlEcpProfileService extends SamlService {
}
@Override
- protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
+ protected Response buildErrorResponse(AuthenticationSessionModel authSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
return Soap.createMessage().addToBody(document).build();
}
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 20d86c0..a8218c1 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -30,8 +30,8 @@ import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.events.EventBuilder;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
@@ -57,10 +57,13 @@ import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.services.ErrorPage;
+import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.sessions.CommonClientSessionModel;
+import org.keycloak.sessions.AuthenticationSessionModel;
import org.w3c.dom.Document;
import javax.ws.rs.core.HttpHeaders;
@@ -156,9 +159,9 @@ public class SamlProtocol implements LoginProtocol {
}
@Override
- public Response sendError(ClientSessionModel clientSession, Error error) {
+ public Response sendError(AuthenticationSessionModel authSession, Error error) {
try {
- ClientModel client = clientSession.getClient();
+ ClientModel client = authSession.getClient();
if ("true".equals(client.getAttribute(SAML_IDP_INITIATED_LOGIN))) {
if (error == Error.CANCELLED_BY_USER) {
@@ -173,9 +176,9 @@ public class SamlProtocol implements LoginProtocol {
return ErrorPage.error(session, translateErrorToIdpInitiatedErrorMessage(error));
}
} else {
- SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(clientSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get());
+ SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(authSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get());
try {
- JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE));
+ JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(authSession.getClientNote(GeneralConstants.RELAY_STATE));
SamlClient samlClient = new SamlClient(client);
KeyManager keyManager = session.keys();
if (samlClient.requiresRealmSignature()) {
@@ -198,22 +201,21 @@ public class SamlProtocol implements LoginProtocol {
binding.encrypt(publicKey);
}
Document document = builder.buildDocument();
- return buildErrorResponse(clientSession, binding, document);
+ return buildErrorResponse(authSession, binding, document);
} catch (Exception e) {
return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
}
}
} finally {
- RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
- session.sessions().removeClientSession(realm, clientSession);
+ new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
}
}
- protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
- if (isPostBinding(clientSession)) {
- return binding.postBinding(document).response(clientSession.getRedirectUri());
+ protected Response buildErrorResponse(AuthenticationSessionModel authSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
+ if (isPostBinding(authSession)) {
+ return binding.postBinding(document).response(authSession.getRedirectUri());
} else {
- return binding.redirectBinding(document).response(clientSession.getRedirectUri());
+ return binding.redirectBinding(document).response(authSession.getRedirectUri());
}
}
@@ -248,7 +250,13 @@ public class SamlProtocol implements LoginProtocol {
return RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString();
}
- protected boolean isPostBinding(ClientSessionModel clientSession) {
+ protected boolean isPostBinding(AuthenticationSessionModel authSession) {
+ ClientModel client = authSession.getClient();
+ SamlClient samlClient = new SamlClient(client);
+ return SamlProtocol.SAML_POST_BINDING.equals(authSession.getClientNote(SamlProtocol.SAML_BINDING)) || samlClient.forcePostBinding();
+ }
+
+ protected boolean isPostBinding(AuthenticatedClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
SamlClient samlClient = new SamlClient(client);
return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || samlClient.forcePostBinding();
@@ -259,7 +267,7 @@ public class SamlProtocol implements LoginProtocol {
return SamlProtocol.SAML_POST_BINDING.equals(note);
}
- protected boolean isLogoutPostBindingForClient(ClientSessionModel clientSession) {
+ protected boolean isLogoutPostBindingForClient(AuthenticatedClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
SamlClient samlClient = new SamlClient(client);
String logoutPostUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
@@ -284,7 +292,7 @@ public class SamlProtocol implements LoginProtocol {
return (logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty());
}
- protected String getNameIdFormat(SamlClient samlClient, ClientSessionModel clientSession) {
+ protected String getNameIdFormat(SamlClient samlClient, AuthenticatedClientSessionModel clientSession) {
String nameIdFormat = clientSession.getNote(GeneralConstants.NAMEID_FORMAT);
boolean forceFormat = samlClient.forceNameIDFormat();
@@ -297,7 +305,7 @@ public class SamlProtocol implements LoginProtocol {
return nameIdFormat;
}
- protected String getNameId(String nameIdFormat, ClientSessionModel clientSession, UserSessionModel userSession) {
+ protected String getNameId(String nameIdFormat, CommonClientSessionModel clientSession, UserSessionModel userSession) {
if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
return userSession.getUser().getEmail();
} else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) {
@@ -327,7 +335,7 @@ public class SamlProtocol implements LoginProtocol {
*
* @return the user's persistent NameId
*/
- protected String getPersistentNameId(final ClientSessionModel clientSession, final UserSessionModel userSession) {
+ protected String getPersistentNameId(final CommonClientSessionModel clientSession, final UserSessionModel userSession) {
// attempt to retrieve the UserID for the client-specific attribute
final UserModel user = userSession.getUser();
final String clientNameId = String.format("%s.%s", SAML_PERSISTENT_NAME_ID_FOR,
@@ -351,8 +359,8 @@ public class SamlProtocol implements LoginProtocol {
}
@Override
- public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) {
- ClientSessionModel clientSession = accessCode.getClientSession();
+ public Response authenticated(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
+ ClientSessionCode<AuthenticatedClientSessionModel> accessCode = new ClientSessionCode<>(session, realm, clientSession);
ClientModel client = clientSession.getClient();
SamlClient samlClient = new SamlClient(client);
String requestID = clientSession.getNote(SAML_REQUEST_ID);
@@ -368,8 +376,12 @@ public class SamlProtocol implements LoginProtocol {
clientSession.setNote(SAML_NAME_ID_FORMAT, nameIdFormat);
SAML2LoginResponseBuilder builder = new SAML2LoginResponseBuilder();
- builder.requestID(requestID).destination(redirectUri).issuer(responseIssuer).assertionExpiration(realm.getAccessCodeLifespan()).subjectExpiration(realm.getAccessTokenLifespan()).sessionIndex(clientSession.getId())
+ builder.requestID(requestID).destination(redirectUri).issuer(responseIssuer).assertionExpiration(realm.getAccessCodeLifespan()).subjectExpiration(realm.getAccessTokenLifespan())
.requestIssuer(clientSession.getClient().getClientId()).nameIdentifier(nameIdFormat, nameId).authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get());
+
+ String sessionIndex = SamlSessionUtils.getSessionIndex(clientSession);
+ builder.sessionIndex(sessionIndex);
+
if (!samlClient.includeAuthnStatement()) {
builder.disableAuthnStatement(true);
}
@@ -460,7 +472,7 @@ public class SamlProtocol implements LoginProtocol {
}
}
- protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException {
+ protected Response buildAuthenticatedResponse(AuthenticatedClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException {
if (isPostBinding(clientSession)) {
return bindingBuilder.postBinding(samlDocument).response(redirectUri);
} else {
@@ -479,7 +491,7 @@ public class SamlProtocol implements LoginProtocol {
}
public AttributeStatementType populateAttributeStatements(List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers, KeycloakSession session, UserSessionModel userSession,
- ClientSessionModel clientSession) {
+ AuthenticatedClientSessionModel clientSession) {
AttributeStatementType attributeStatement = new AttributeStatementType();
for (ProtocolMapperProcessor<SAMLAttributeStatementMapper> processor : attributeStatementMappers) {
processor.mapper.transformAttributeStatement(attributeStatement, processor.model, session, userSession, clientSession);
@@ -488,14 +500,14 @@ public class SamlProtocol implements LoginProtocol {
return attributeStatement;
}
- public ResponseType transformLoginResponse(List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> mappers, ResponseType response, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ public ResponseType transformLoginResponse(List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> mappers, ResponseType response, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
for (ProtocolMapperProcessor<SAMLLoginResponseMapper> processor : mappers) {
response = processor.mapper.transformLoginResponse(response, processor.model, session, userSession, clientSession);
}
return response;
}
- public void populateRoles(ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession,
+ public void populateRoles(ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession,
final AttributeStatementType existingAttributeStatement) {
if (roleListMapper == null)
return;
@@ -509,8 +521,8 @@ public class SamlProtocol implements LoginProtocol {
} else {
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
}
- if (logoutServiceUrl == null && client instanceof ClientModel)
- logoutServiceUrl = ((ClientModel) client).getManagementUrl();
+ if (logoutServiceUrl == null)
+ logoutServiceUrl = client.getManagementUrl();
if (logoutServiceUrl == null || logoutServiceUrl.trim().equals(""))
return null;
return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), client.getRootUrl(), logoutServiceUrl);
@@ -518,11 +530,9 @@ public class SamlProtocol implements LoginProtocol {
}
@Override
- public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
+ public Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
SamlClient samlClient = new SamlClient(client);
- if (!(client instanceof ClientModel))
- return null;
try {
boolean postBinding = isLogoutPostBindingForClient(clientSession);
String bindingUri = getLogoutServiceUrl(uriInfo, client, postBinding ? SAML_POST_BINDING : SAML_REDIRECT_BINDING);
@@ -615,7 +625,7 @@ public class SamlProtocol implements LoginProtocol {
}
@Override
- public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
+ public void backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
SamlClient samlClient = new SamlClient(client);
String logoutUrl = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
@@ -674,15 +684,19 @@ public class SamlProtocol implements LoginProtocol {
}
- protected SAML2LogoutRequestBuilder createLogoutRequest(String logoutUrl, ClientSessionModel clientSession, ClientModel client) {
+ protected SAML2LogoutRequestBuilder createLogoutRequest(String logoutUrl, AuthenticatedClientSessionModel clientSession, ClientModel client) {
// build userPrincipal with subject used at login
- SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder().assertionExpiration(realm.getAccessCodeLifespan()).issuer(getResponseIssuer(realm)).sessionIndex(clientSession.getId())
+ SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder().assertionExpiration(realm.getAccessCodeLifespan()).issuer(getResponseIssuer(realm))
.userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT)).destination(logoutUrl);
+
+ String sessionIndex = SamlSessionUtils.getSessionIndex(clientSession);
+ logoutBuilder.sessionIndex(sessionIndex);
+
return logoutBuilder;
}
@Override
- public boolean requireReauthentication(UserSessionModel userSession, ClientSessionModel clientSession) {
+ public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) {
// Not yet supported
return false;
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
index d67faa2..9a6790b 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -37,8 +37,8 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.keys.RsaKeyMetadata;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@@ -86,9 +86,10 @@ import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.SPMetadataDescriptor;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
+import org.keycloak.sessions.AuthenticationSessionModel;
/**
- * Resource class for the oauth/openid connect token service
+ * Resource class for the saml connect token service
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@@ -97,9 +98,6 @@ public class SamlService extends AuthorizationEndpointBase {
protected static final Logger logger = Logger.getLogger(SamlService.class);
- @Context
- protected KeycloakSession session;
-
public SamlService(RealmModel realm, EventBuilder event) {
super(realm, event);
}
@@ -270,13 +268,19 @@ public class SamlService extends AuthorizationEndpointBase {
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
}
- ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
- clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL);
- clientSession.setRedirectUri(redirect);
- clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
- clientSession.setNote(SamlProtocol.SAML_BINDING, bindingType);
- clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
- clientSession.setNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID());
+ AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, relayState);
+ if (checks.response != null) {
+ return checks.response;
+ }
+
+ AuthenticationSessionModel authSession = checks.authSession;
+
+ authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
+ authSession.setRedirectUri(redirect);
+ authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
+ authSession.setClientNote(SamlProtocol.SAML_BINDING, bindingType);
+ authSession.setClientNote(GeneralConstants.RELAY_STATE, relayState);
+ authSession.setClientNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID());
// Handle NameIDPolicy from SP
NameIDPolicyType nameIdPolicy = requestAbstractType.getNameIDPolicy();
@@ -285,7 +289,7 @@ public class SamlService extends AuthorizationEndpointBase {
String nameIdFormat = nameIdFormatUri.toString();
// TODO: Handle AllowCreate too, relevant for persistent NameID.
if (isSupportedNameIdFormat(nameIdFormat)) {
- clientSession.setNote(GeneralConstants.NAMEID_FORMAT, nameIdFormat);
+ authSession.setClientNote(GeneralConstants.NAMEID_FORMAT, nameIdFormat);
} else {
event.detail(Details.REASON, "unsupported_nameid_format");
event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
@@ -301,13 +305,13 @@ public class SamlService extends AuthorizationEndpointBase {
BaseIDAbstractType baseID = subject.getSubType().getBaseID();
if (baseID != null && baseID instanceof NameIDType) {
NameIDType nameID = (NameIDType) baseID;
- clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue());
+ authSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue());
}
}
}
- return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive(), redirectToAuthentication);
+ return newBrowserAuthentication(authSession, requestAbstractType.isIsPassive(), redirectToAuthentication);
}
protected String getBindingType(AuthnRequestType requestAbstractType) {
@@ -368,31 +372,22 @@ public class SamlService extends AuthorizationEndpointBase {
userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, samlClient.getCanonicalizationMethod());
userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL);
// remove client from logout requests
- for (ClientSessionModel clientSession : userSession.getClientSessions()) {
- if (clientSession.getClient().getId().equals(client.getId())) {
- clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name());
- }
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+ if (clientSession != null) {
+ clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
}
logger.debug("browser Logout");
return authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection, headers);
} else if (logoutRequest.getSessionIndex() != null) {
for (String sessionIndex : logoutRequest.getSessionIndex()) {
- ClientSessionModel clientSession = session.sessions().getClientSession(realm, sessionIndex);
+
+ AuthenticatedClientSessionModel clientSession = SamlSessionUtils.getClientSession(session, realm, sessionIndex);
if (clientSession == null)
continue;
UserSessionModel userSession = clientSession.getUserSession();
if (clientSession.getClient().getClientId().equals(client.getClientId())) {
// remove requesting client from logout
- clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name());
-
- // Remove also other clientSessions of this client as there could be more in this UserSession
- if (userSession != null) {
- for (ClientSessionModel clientSession2 : userSession.getClientSessions()) {
- if (clientSession2.getClient().getId().equals(client.getId())) {
- clientSession2.setAction(ClientSessionModel.Action.LOGGED_OUT.name());
- }
- }
- }
+ clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
}
try {
@@ -442,6 +437,16 @@ public class SamlService extends AuthorizationEndpointBase {
return !realm.getSslRequired().isRequired(clientConnection);
}
}
+
+ public Response execute(String samlRequest, String samlResponse, String relayState) {
+ Response response = basicChecks(samlRequest, samlResponse);
+ if (response != null)
+ return response;
+ if (samlRequest != null)
+ return handleSamlRequest(samlRequest, relayState);
+ else
+ return handleSamlResponse(samlResponse, relayState);
+ }
}
protected class PostBindingProtocol extends BindingProtocol {
@@ -466,16 +471,6 @@ public class SamlService extends AuthorizationEndpointBase {
return SamlProtocol.SAML_POST_BINDING;
}
- public Response execute(String samlRequest, String samlResponse, String relayState) {
- Response response = basicChecks(samlRequest, samlResponse);
- if (response != null)
- return response;
- if (samlRequest != null)
- return handleSamlRequest(samlRequest, relayState);
- else
- return handleSamlResponse(samlResponse, relayState);
- }
-
}
protected class RedirectBindingProtocol extends BindingProtocol {
@@ -506,25 +501,15 @@ public class SamlService extends AuthorizationEndpointBase {
return SamlProtocol.SAML_REDIRECT_BINDING;
}
- public Response execute(String samlRequest, String samlResponse, String relayState) {
- Response response = basicChecks(samlRequest, samlResponse);
- if (response != null)
- return response;
- if (samlRequest != null)
- return handleSamlRequest(samlRequest, relayState);
- else
- return handleSamlResponse(samlResponse, relayState);
- }
-
}
- protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive, boolean redirectToAuthentication) {
+ protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication) {
SamlProtocol samlProtocol = new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo);
- return newBrowserAuthentication(clientSession, isPassive, redirectToAuthentication, samlProtocol);
+ return newBrowserAuthentication(authSession, isPassive, redirectToAuthentication, samlProtocol);
}
- protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) {
- return handleBrowserAuthenticationRequest(clientSession, samlProtocol, isPassive, redirectToAuthentication);
+ protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) {
+ return handleBrowserAuthenticationRequest(authSession, samlProtocol, isPassive, redirectToAuthentication);
}
/**
@@ -609,15 +594,19 @@ public class SamlService extends AuthorizationEndpointBase {
event.error(Errors.CLIENT_NOT_FOUND);
return ErrorPage.error(session, Messages.CLIENT_NOT_FOUND);
}
+ if (!client.isEnabled()) {
+ event.error(Errors.CLIENT_DISABLED);
+ return ErrorPage.error(session, Messages.CLIENT_DISABLED);
+ }
if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) == null) {
logger.error("SAML assertion consumer url not set up");
event.error(Errors.INVALID_REDIRECT_URI);
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
}
- ClientSessionModel clientSession = createClientSessionForIdpInitiatedSso(this.session, this.realm, client, relayState);
+ AuthenticationSessionModel authSession = getOrCreateLoginSessionForIdpInitiatedSso(this.session, this.realm, client, relayState);
- return newBrowserAuthentication(clientSession, false, false);
+ return newBrowserAuthentication(authSession, false, false);
}
/**
@@ -631,7 +620,7 @@ public class SamlService extends AuthorizationEndpointBase {
* @param relayState Optional relay state - free field as per SAML specification
* @return
*/
- public static ClientSessionModel createClientSessionForIdpInitiatedSso(KeycloakSession session, RealmModel realm, ClientModel client, String relayState) {
+ public AuthenticationSessionModel getOrCreateLoginSessionForIdpInitiatedSso(KeycloakSession session, RealmModel realm, ClientModel client, String relayState) {
String bindingType = SamlProtocol.SAML_POST_BINDING;
if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) != null) {
bindingType = SamlProtocol.SAML_REDIRECT_BINDING;
@@ -647,21 +636,47 @@ public class SamlService extends AuthorizationEndpointBase {
redirect = client.getManagementUrl();
}
- ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
- clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL);
- clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
- clientSession.setNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING);
- clientSession.setNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN, "true");
- clientSession.setRedirectUri(redirect);
+ AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, null);
+ if (checks.response != null) {
+ throw new IllegalStateException("Not expected to detect re-sent request for IDP initiated SSO");
+ }
+
+ AuthenticationSessionModel authSession = checks.authSession;
+ authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
+ authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
+ authSession.setClientNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING);
+ authSession.setClientNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN, "true");
+ authSession.setRedirectUri(redirect);
if (relayState == null) {
relayState = client.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_RELAY_STATE);
}
if (relayState != null && !relayState.trim().equals("")) {
- clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
+ authSession.setClientNote(GeneralConstants.RELAY_STATE, relayState);
+ }
+
+ return authSession;
+ }
+
+
+ @Override
+ protected boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String requestRelayState) {
+ // No support of browser "refresh" or "back" buttons for SAML IDP initiated SSO. So always treat as new request
+ String idpInitiated = authSession.getClientNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN);
+ if (Boolean.parseBoolean(idpInitiated)) {
+ return true;
+ }
+
+ if (requestRelayState == null) {
+ return true;
+ }
+
+ // Check if it's different client
+ if (!clientFromRequest.equals(authSession.getClient())) {
+ return true;
}
- return clientSession;
+ return !requestRelayState.equals(authSession.getClientNote(GeneralConstants.RELAY_STATE));
}
@POST
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java
new file mode 100644
index 0000000..0083fdc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.protocol.saml;
+
+import java.util.regex.Pattern;
+
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SamlSessionUtils {
+
+ private static final String DELIMITER = "::";
+
+ // Just perf optimization
+ private static final Pattern PATTERN = Pattern.compile(DELIMITER);
+
+
+ public static String getSessionIndex(AuthenticatedClientSessionModel clientSession) {
+ UserSessionModel userSession = clientSession.getUserSession();
+ ClientModel client = clientSession.getClient();
+
+ return userSession.getId() + DELIMITER + client.getId();
+ }
+
+
+ public static AuthenticatedClientSessionModel getClientSession(KeycloakSession session, RealmModel realm, String sessionIndex) {
+ if (sessionIndex == null) {
+ return null;
+ }
+
+ String[] parts = PATTERN.split(sessionIndex);
+ if (parts.length != 2) {
+ return null;
+ }
+
+ UserSessionModel userSession = session.sessions().getUserSession(realm, parts[0]);
+ if (userSession == null) {
+ return null;
+ }
+
+ return userSession.getAuthenticatedClientSessions().get(parts[1]);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
index 9d615a5..67cce1f 100644
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java
@@ -33,6 +33,7 @@ import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.UserCache;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
+import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.storage.UserStorageManager;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
@@ -54,10 +55,10 @@ public class DefaultKeycloakSession implements KeycloakSession {
private final DefaultKeycloakTransactionManager transactionManager;
private final Map<String, Object> attributes = new HashMap<>();
private RealmProvider model;
- private UserProvider userModel;
private UserStorageManager userStorageManager;
private UserCredentialStoreManager userCredentialStorageManager;
private UserSessionProvider sessionProvider;
+ private AuthenticationSessionProvider authenticationSessionProvider;
private UserFederatedStorageProvider userFederatedStorageProvider;
private KeycloakContext context;
private KeyManager keyManager;
@@ -237,6 +238,14 @@ public class DefaultKeycloakSession implements KeycloakSession {
}
@Override
+ public AuthenticationSessionProvider authenticationSessions() {
+ if (authenticationSessionProvider == null) {
+ authenticationSessionProvider = getProvider(AuthenticationSessionProvider.class);
+ }
+ return authenticationSessionProvider;
+ }
+
+ @Override
public KeyManager keys() {
if (keyManager == null) {
keyManager = new DefaultKeyManager(this);
diff --git a/services/src/main/java/org/keycloak/services/ErrorPageException.java b/services/src/main/java/org/keycloak/services/ErrorPageException.java
index 4bcbbc8..51ee9c8 100644
--- a/services/src/main/java/org/keycloak/services/ErrorPageException.java
+++ b/services/src/main/java/org/keycloak/services/ErrorPageException.java
@@ -37,6 +37,8 @@ public class ErrorPageException extends WebApplicationException {
this.parameters = parameters;
}
+
+
@Override
public Response getResponse() {
return ErrorPage.error(session, errorMessage, parameters);
diff --git a/services/src/main/java/org/keycloak/services/managers/Auth.java b/services/src/main/java/org/keycloak/services/managers/Auth.java
index 714a3a2..8b6086e 100755
--- a/services/src/main/java/org/keycloak/services/managers/Auth.java
+++ b/services/src/main/java/org/keycloak/services/managers/Auth.java
@@ -17,8 +17,8 @@
package org.keycloak.services.managers;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
@@ -35,7 +35,7 @@ public class Auth {
private final UserModel user;
private final ClientModel client;
private final UserSessionModel session;
- private ClientSessionModel clientSession;
+ private AuthenticatedClientSessionModel clientSession;
public Auth(RealmModel realm, AccessToken token, UserModel user, ClientModel client, UserSessionModel session, boolean cookie) {
this.cookie = cookie;
@@ -71,11 +71,11 @@ public class Auth {
return session;
}
- public ClientSessionModel getClientSession() {
+ public AuthenticatedClientSessionModel getClientSession() {
return clientSession;
}
- public void setClientSession(ClientSessionModel clientSession) {
+ public void setClientSession(AuthenticatedClientSessionModel clientSession) {
this.clientSession = clientSession;
}
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 cf7d73c..31217f1 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -19,11 +19,14 @@ package org.keycloak.services.managers;
import org.jboss.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.OAuth2Constants;
import org.keycloak.TokenVerifier;
+import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
@@ -35,17 +38,7 @@ import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.jose.jws.JWSBuilder;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.KeyManager;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.RequiredActionProviderModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserConsentModel;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
@@ -55,9 +48,12 @@ import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.IdentityBrokerService;
+import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.services.util.P3PHelper;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.CommonClientSessionModel;
import javax.crypto.SecretKey;
import javax.ws.rs.core.Cookie;
@@ -65,12 +61,11 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.security.PublicKey;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
/**
* Stateless object that manages authentication
@@ -81,6 +76,10 @@ import java.util.Set;
public class AuthenticationManager {
public static final String SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS= "SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS";
public static final String END_AFTER_REQUIRED_ACTIONS = "END_AFTER_REQUIRED_ACTIONS";
+ public static final String INVALIDATE_ACTION_TOKEN = "INVALIDATE_ACTION_TOKEN";
+
+ // Last authenticated client in userSession.
+ public static final String LAST_AUTHENTICATED_CLIENT = "LAST_AUTHENTICATED_CLIENT";
// userSession note with authTime (time when authentication flow including requiredActions was finished)
public static final String AUTH_TIME = "AUTH_TIME";
@@ -124,7 +123,10 @@ public class AuthenticationManager {
if (cookie == null) return;
String tokenString = cookie.getValue();
- TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(false).checkTokenType(false);
+ TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString, AccessToken.class)
+ .realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()))
+ .checkActive(false)
+ .checkTokenType(false);
String kid = verifier.getHeader().getKeyId();
SecretKey secretKey = session.keys().getHmacSecretKey(realm, kid);
@@ -159,7 +161,7 @@ public class AuthenticationManager {
logger.debugv("Logging out: {0} ({1})", user.getUsername(), userSession.getId());
expireUserSessionCookie(session, userSession, realm, uriInfo, headers, connection);
- for (ClientSessionModel clientSession : userSession.getClientSessions()) {
+ for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers);
}
if (logoutBroker) {
@@ -169,6 +171,7 @@ public class AuthenticationManager {
try {
identityProvider.backchannelLogout(session, userSession, uriInfo, realm);
} catch (Exception e) {
+ logger.warn("Exception at broker backchannel logout for broker " + brokerId, e);
}
}
}
@@ -176,17 +179,17 @@ public class AuthenticationManager {
session.sessions().removeUserSession(realm, userSession);
}
- public static void backchannelLogoutClientSession(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession, UserSessionModel userSession, UriInfo uriInfo, HttpHeaders headers) {
+ public static void backchannelLogoutClientSession(KeycloakSession session, RealmModel realm, AuthenticatedClientSessionModel clientSession, UserSessionModel userSession, UriInfo uriInfo, HttpHeaders headers) {
ClientModel client = clientSession.getClient();
- if (client instanceof ClientModel && !client.isFrontchannelLogout() && !ClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
- String authMethod = clientSession.getAuthMethod();
+ if (!client.isFrontchannelLogout() && !AuthenticatedClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
+ String authMethod = clientSession.getProtocol();
if (authMethod == null) return; // must be a keycloak service like account
LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
protocol.setRealm(realm)
.setHttpHeaders(headers)
.setUriInfo(uriInfo);
protocol.backchannelLogout(userSession, clientSession);
- clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name());
+ clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
}
}
@@ -197,8 +200,8 @@ public class AuthenticationManager {
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
for (UserSessionModel userSession : userSessions) {
- List<ClientSessionModel> clientSessions = userSession.getClientSessions();
- for (ClientSessionModel clientSession : clientSessions) {
+ Collection<AuthenticatedClientSessionModel> clientSessions = userSession.getAuthenticatedClientSessions().values();
+ for (AuthenticatedClientSessionModel clientSession : clientSessions) {
if (clientSession.getClient().getId().equals(clientId)) {
AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers);
TokenManager.dettachClientSession(session.sessions(), realm, clientSession);
@@ -215,16 +218,16 @@ public class AuthenticationManager {
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
userSession.setState(UserSessionModel.State.LOGGING_OUT);
}
- List<ClientSessionModel> redirectClients = new LinkedList<ClientSessionModel>();
- for (ClientSessionModel clientSession : userSession.getClientSessions()) {
+ List<AuthenticatedClientSessionModel> redirectClients = new LinkedList<>();
+ for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
ClientModel client = clientSession.getClient();
- if (ClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) continue;
+ if (AuthenticatedClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) continue;
if (client.isFrontchannelLogout()) {
- String authMethod = clientSession.getAuthMethod();
+ String authMethod = clientSession.getProtocol();
if (authMethod == null) continue; // must be a keycloak service like account
redirectClients.add(clientSession);
} else {
- String authMethod = clientSession.getAuthMethod();
+ String authMethod = clientSession.getProtocol();
if (authMethod == null) continue; // must be a keycloak service like account
LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
protocol.setRealm(realm)
@@ -233,21 +236,21 @@ public class AuthenticationManager {
try {
logger.debugv("backchannel logout to: {0}", client.getClientId());
protocol.backchannelLogout(userSession, clientSession);
- clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name());
+ clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
} catch (Exception e) {
ServicesLogger.LOGGER.failedToLogoutClient(e);
}
}
}
- for (ClientSessionModel nextRedirectClient : redirectClients) {
- String authMethod = nextRedirectClient.getAuthMethod();
+ for (AuthenticatedClientSessionModel nextRedirectClient : redirectClients) {
+ String authMethod = nextRedirectClient.getProtocol();
LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
protocol.setRealm(realm)
.setHttpHeaders(headers)
.setUriInfo(uriInfo);
// setting this to logged out cuz I"m not sure protocols can always verify that the client was logged out or not
- nextRedirectClient.setAction(ClientSessionModel.Action.LOGGED_OUT.name());
+ nextRedirectClient.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
try {
logger.debugv("frontchannel logout to: {0}", nextRedirectClient.getClient().getClientId());
Response response = protocol.frontchannelLogout(userSession, nextRedirectClient);
@@ -410,20 +413,20 @@ public class AuthenticationManager {
public static Response redirectAfterSuccessfulFlow(KeycloakSession session, RealmModel realm, UserSessionModel userSession,
- ClientSessionModel clientSession,
+ AuthenticatedClientSessionModel clientSession,
HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection,
- EventBuilder event) {
- LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
- protocol.setRealm(realm)
+ EventBuilder event, String protocol) {
+ LoginProtocol protocolImpl = session.getProvider(LoginProtocol.class, protocol);
+ protocolImpl.setRealm(realm)
.setHttpHeaders(request.getHttpHeaders())
.setUriInfo(uriInfo)
.setEventBuilder(event);
- return redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection, event, protocol);
+ return redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection, event, protocolImpl);
}
public static Response redirectAfterSuccessfulFlow(KeycloakSession session, RealmModel realm, UserSessionModel userSession,
- ClientSessionModel clientSession,
+ AuthenticatedClientSessionModel clientSession,
HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection,
EventBuilder event, LoginProtocol protocol) {
Cookie sessionCookie = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_SESSION_COOKIE);
@@ -460,32 +463,66 @@ public class AuthenticationManager {
userSession.setNote(AUTH_TIME, String.valueOf(authTime));
}
- return protocol.authenticated(userSession, new ClientSessionCode(session, realm, clientSession));
+ userSession.setNote(LAST_AUTHENTICATED_CLIENT, clientSession.getClient().getId());
+
+ return protocol.authenticated(userSession, clientSession);
}
- public static boolean isSSOAuthentication(ClientSessionModel clientSession) {
+ public static boolean isSSOAuthentication(AuthenticatedClientSessionModel clientSession) {
String ssoAuth = clientSession.getNote(SSO_AUTH);
return Boolean.parseBoolean(ssoAuth);
}
- public static Response nextActionAfterAuthentication(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession,
+ public static Response nextActionAfterAuthentication(KeycloakSession session, AuthenticationSessionModel authSession,
ClientConnection clientConnection,
HttpRequest request, UriInfo uriInfo, EventBuilder event) {
- Response requiredAction = actionRequired(session, userSession, clientSession, clientConnection, request, uriInfo, event);
+ Response requiredAction = actionRequired(session, authSession, clientConnection, request, uriInfo, event);
if (requiredAction != null) return requiredAction;
- return finishedRequiredActions(session, userSession, clientSession, clientConnection, request, uriInfo, event);
+ return finishedRequiredActions(session, authSession, null, clientConnection, request, uriInfo, event);
}
- public static Response finishedRequiredActions(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession, ClientConnection clientConnection, HttpRequest request, UriInfo uriInfo, EventBuilder event) {
- if (clientSession.getNote(END_AFTER_REQUIRED_ACTIONS) != null) {
+
+ public static Response redirectToRequiredActions(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession, UriInfo uriInfo, String requiredAction) {
+ // redirect to non-action url so browser refresh button works without reposting past data
+ ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession);
+ accessCode.setAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name());
+ authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, LoginActionsService.REQUIRED_ACTION);
+ authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, requiredAction);
+
+ UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
+ .path(LoginActionsService.REQUIRED_ACTION);
+
+ if (requiredAction != null) {
+ uriBuilder.queryParam("execution", requiredAction);
+ }
+
+ URI redirect = uriBuilder.build(realm.getName());
+ return Response.status(302).location(redirect).build();
+
+ }
+
+
+ public static Response finishedRequiredActions(KeycloakSession session, AuthenticationSessionModel authSession, UserSessionModel userSession,
+ ClientConnection clientConnection, HttpRequest request, UriInfo uriInfo, EventBuilder event) {
+ String actionTokenKeyToInvalidate = authSession.getAuthNote(INVALIDATE_ACTION_TOKEN);
+ if (actionTokenKeyToInvalidate != null) {
+ ActionTokenKeyModel actionTokenKey = DefaultActionTokenKey.from(actionTokenKeyToInvalidate);
+
+ if (actionTokenKey != null) {
+ ActionTokenStoreProvider actionTokenStore = session.getProvider(ActionTokenStoreProvider.class);
+ actionTokenStore.put(actionTokenKey, null); // Token is invalidated
+ }
+ }
+
+ if (authSession.getAuthNote(END_AFTER_REQUIRED_ACTIONS) != null) {
LoginFormsProvider infoPage = session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.ACCOUNT_UPDATED);
- if (clientSession.getNote(SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS) != null) {
- if (clientSession.getRedirectUri() != null) {
- infoPage.setAttribute("pageRedirectUri", clientSession.getRedirectUri());
+ if (authSession.getAuthNote(SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS) != null) {
+ if (authSession.getRedirectUri() != null) {
+ infoPage.setAttribute("pageRedirectUri", authSession.getRedirectUri());
}
} else {
@@ -493,44 +530,56 @@ public class AuthenticationManager {
}
Response response = infoPage
.createInfoPage();
- session.sessions().removeUserSession(session.getContext().getRealm(), userSession);
return response;
+ // Don't remove authentication session for now, to ensure that browser buttons (back/refresh) will still work fine.
+
}
+ RealmModel realm = authSession.getRealm();
+
+ AuthenticatedClientSessionModel clientSession = AuthenticationProcessor.attachSession(authSession, userSession, session, realm, clientConnection, event);
+
+ event.event(EventType.LOGIN);
+ event.session(clientSession.getUserSession());
event.success();
- RealmModel realm = clientSession.getRealm();
- return redirectAfterSuccessfulFlow(session, realm , userSession, clientSession, request, uriInfo, clientConnection, event);
+ return redirectAfterSuccessfulFlow(session, realm, clientSession.getUserSession(), clientSession, request, uriInfo, clientConnection, event, authSession.getProtocol());
}
- public static boolean isActionRequired(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession,
- final ClientConnection clientConnection,
- final HttpRequest request, final UriInfo uriInfo, final EventBuilder event) {
- final RealmModel realm = clientSession.getRealm();
- final UserModel user = userSession.getUser();
- final ClientModel client = clientSession.getClient();
+ // Return null if action is not required. Or the name of the requiredAction in case it is required.
+ public static String nextRequiredAction(final KeycloakSession session, final AuthenticationSessionModel authSession,
+ final ClientConnection clientConnection,
+ final HttpRequest request, final UriInfo uriInfo, final EventBuilder event) {
+ final RealmModel realm = authSession.getRealm();
+ final UserModel user = authSession.getAuthenticatedUser();
+ final ClientModel client = authSession.getClient();
- evaluateRequiredActionTriggers(session, userSession, clientSession, clientConnection, request, uriInfo, event, realm, user);
+ evaluateRequiredActionTriggers(session, authSession, clientConnection, request, uriInfo, event, realm, user);
- if (!user.getRequiredActions().isEmpty() || !clientSession.getRequiredActions().isEmpty()) return true;
+ if (!user.getRequiredActions().isEmpty()) {
+ return user.getRequiredActions().iterator().next();
+ }
+ if (!authSession.getRequiredActions().isEmpty()) {
+ return authSession.getRequiredActions().iterator().next();
+ }
if (client.isConsentRequired()) {
UserConsentModel grantedConsent = session.users().getConsentByClient(realm, user.getId(), client.getId());
- ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession);
+ ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession);
for (RoleModel r : accessCode.getRequestedRoles()) {
// Consent already granted by user
if (grantedConsent != null && grantedConsent.isRoleGranted(r)) {
continue;
}
- return true;
+ return CommonClientSessionModel.Action.OAUTH_GRANT.name();
}
for (ProtocolMapperModel protocolMapper : accessCode.getRequestedProtocolMappers()) {
if (protocolMapper.isConsentRequired() && protocolMapper.getConsentText() != null) {
if (grantedConsent == null || !grantedConsent.isProtocolMapperGranted(protocolMapper)) {
- return true;
+ return CommonClientSessionModel.Action.OAUTH_GRANT.name();
}
}
}
@@ -539,32 +588,32 @@ public class AuthenticationManager {
} else {
event.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED);
}
- return false;
+ return null;
}
- public static Response actionRequired(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession,
+ public static Response actionRequired(final KeycloakSession session, final AuthenticationSessionModel authSession,
final ClientConnection clientConnection,
final HttpRequest request, final UriInfo uriInfo, final EventBuilder event) {
- final RealmModel realm = clientSession.getRealm();
- final UserModel user = userSession.getUser();
- final ClientModel client = clientSession.getClient();
+ final RealmModel realm = authSession.getRealm();
+ final UserModel user = authSession.getAuthenticatedUser();
+ final ClientModel client = authSession.getClient();
- evaluateRequiredActionTriggers(session, userSession, clientSession, clientConnection, request, uriInfo, event, realm, user);
+ evaluateRequiredActionTriggers(session, authSession, clientConnection, request, uriInfo, event, realm, user);
logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired());
- event.detail(Details.CODE_ID, clientSession.getId());
+ event.detail(Details.CODE_ID, authSession.getId());
Set<String> requiredActions = user.getRequiredActions();
- Response action = executionActions(session, userSession, clientSession, request, event, realm, user, requiredActions);
+ Response action = executionActions(session, authSession, request, event, realm, user, requiredActions);
if (action != null) return action;
// executionActions() method should remove any duplicate actions that might be in the clientSession
- requiredActions = clientSession.getRequiredActions();
- action = executionActions(session, userSession, clientSession, request, event, realm, user, requiredActions);
+ requiredActions = authSession.getRequiredActions();
+ action = executionActions(session, authSession, request, event, realm, user, requiredActions);
if (action != null) return action;
if (client.isConsentRequired()) {
@@ -573,7 +622,7 @@ public class AuthenticationManager {
List<RoleModel> realmRoles = new LinkedList<>();
MultivaluedMap<String, RoleModel> resourceRoles = new MultivaluedMapImpl<>();
- ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession);
+ ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession);
for (RoleModel r : accessCode.getRequestedRoles()) {
// Consent already granted by user
@@ -599,13 +648,15 @@ public class AuthenticationManager {
// Skip grant screen if everything was already approved by this user
if (realmRoles.size() > 0 || resourceRoles.size() > 0 || protocolMappers.size() > 0) {
- accessCode.setAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name());
- clientSession.setNote(CURRENT_REQUIRED_ACTION, ClientSessionModel.Action.OAUTH_GRANT.name());
+ accessCode.
+
+ setAction(AuthenticatedClientSessionModel.Action.REQUIRED_ACTIONS.name());
+ authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name());
return session.getProvider(LoginFormsProvider.class)
.setClientSessionCode(accessCode.getCode())
.setAccessRequest(realmRoles, resourceRoles, protocolMappers)
- .createOAuthGrant(clientSession);
+ .createOAuthGrant();
} else {
String consentDetail = (grantedConsent != null) ? Details.CONSENT_VALUE_PERSISTED_CONSENT : Details.CONSENT_VALUE_NO_CONSENT_REQUIRED;
event.detail(Details.CONSENT, consentDetail);
@@ -617,7 +668,38 @@ public class AuthenticationManager {
}
- protected static Response executionActions(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession,
+
+ public static void setRolesAndMappersInSession(AuthenticationSessionModel authSession) {
+ ClientModel client = authSession.getClient();
+ UserModel user = authSession.getAuthenticatedUser();
+
+ Set<String> requestedRoles = new HashSet<String>();
+ // todo scope param protocol independent
+ String scopeParam = authSession.getClientNote(OAuth2Constants.SCOPE);
+ for (RoleModel r : TokenManager.getAccess(scopeParam, true, client, user)) {
+ requestedRoles.add(r.getId());
+ }
+ authSession.setRoles(requestedRoles);
+
+ Set<String> requestedProtocolMappers = new HashSet<String>();
+ ClientTemplateModel clientTemplate = client.getClientTemplate();
+ if (clientTemplate != null && client.useTemplateMappers()) {
+ for (ProtocolMapperModel protocolMapper : clientTemplate.getProtocolMappers()) {
+ if (protocolMapper.getProtocol().equals(authSession.getProtocol())) {
+ requestedProtocolMappers.add(protocolMapper.getId());
+ }
+ }
+
+ }
+ for (ProtocolMapperModel protocolMapper : client.getProtocolMappers()) {
+ if (protocolMapper.getProtocol().equals(authSession.getProtocol())) {
+ requestedProtocolMappers.add(protocolMapper.getId());
+ }
+ }
+ authSession.setProtocolMappers(requestedProtocolMappers);
+ }
+
+ protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession,
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user,
Set<String> requiredActions) {
for (String action : requiredActions) {
@@ -635,34 +717,34 @@ public class AuthenticationManager {
throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?");
}
RequiredActionProvider actionProvider = factory.create(session);
- RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory);
+ RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory);
actionProvider.requiredActionChallenge(context);
if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
- LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
+ LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getAuthenticationSession().getProtocol());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
.setUriInfo(context.getUriInfo())
.setEventBuilder(event);
- Response response = protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED);
+ Response response = protocol.sendError(context.getAuthenticationSession(), Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
return response;
}
else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
- clientSession.setNote(CURRENT_REQUIRED_ACTION, model.getProviderId());
+ authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, model.getProviderId());
return context.getChallenge();
}
else if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
event.clone().event(EventType.CUSTOM_REQUIRED_ACTION).detail(Details.CUSTOM_REQUIRED_ACTION, factory.getId()).success();
// don't have to perform the same action twice, so remove it from both the user and session required actions
- clientSession.getUserSession().getUser().removeRequiredAction(factory.getId());
- clientSession.removeRequiredAction(factory.getId());
+ authSession.getAuthenticatedUser().removeRequiredAction(factory.getId());
+ authSession.removeRequiredAction(factory.getId());
}
}
return null;
}
- public static void evaluateRequiredActionTriggers(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) {
+ public static void evaluateRequiredActionTriggers(final KeycloakSession session, final AuthenticationSessionModel authSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) {
// see if any required actions need triggering, i.e. an expired password
for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) {
@@ -672,7 +754,7 @@ public class AuthenticationManager {
throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?");
}
RequiredActionProvider provider = factory.create(session);
- RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory) {
+ RequiredActionContextResult result = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory) {
@Override
public void challenge(Response response) {
throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()");
@@ -702,7 +784,11 @@ public class AuthenticationManager {
public static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
boolean isCookie, String tokenString, HttpHeaders headers) {
try {
- TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
+ TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString, AccessToken.class)
+ .withDefaultChecks()
+ .realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()))
+ .checkActive(checkActive)
+ .checkTokenType(checkTokenType);
String kid = verifier.getHeader().getKeyId();
AlgorithmType algorithmType = verifier.getHeader().getAlgorithm().getType();
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java
new file mode 100644
index 0000000..1cba9dc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.services.managers;
+
+import javax.ws.rs.core.UriInfo;
+
+import org.jboss.logging.Logger;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.RestartLoginCookie;
+import org.keycloak.services.util.CookieHelper;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.StickySessionEncoderProvider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AuthenticationSessionManager {
+
+ public static final String AUTH_SESSION_ID = "AUTH_SESSION_ID";
+
+ private static final Logger log = Logger.getLogger(AuthenticationSessionManager.class);
+
+ private final KeycloakSession session;
+
+ public AuthenticationSessionManager(KeycloakSession session) {
+ this.session = session;
+ }
+
+
+ public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client, boolean browserCookie) {
+ AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client);
+
+ if (browserCookie) {
+ setAuthSessionCookie(authSession.getId(), realm);
+ }
+
+ return authSession;
+ }
+
+
+ public String getCurrentAuthenticationSessionId(RealmModel realm) {
+ return getAuthSessionCookieDecoded(realm);
+ }
+
+
+ public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm) {
+ String authSessionId = getAuthSessionCookieDecoded(realm);
+ return authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
+ }
+
+
+ public void setAuthSessionCookie(String authSessionId, RealmModel realm) {
+ UriInfo uriInfo = session.getContext().getUri();
+ String cookiePath = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
+
+ boolean sslRequired = realm.getSslRequired().isRequired(session.getContext().getConnection());
+
+ StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class);
+ String encodedAuthSessionId = encoder.encodeSessionId(authSessionId);
+
+ CookieHelper.addCookie(AUTH_SESSION_ID, encodedAuthSessionId, cookiePath, null, null, -1, sslRequired, true);
+
+ log.debugf("Set AUTH_SESSION_ID cookie with value %s", encodedAuthSessionId);
+ }
+
+
+ private String getAuthSessionCookieDecoded(RealmModel realm) {
+ String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID);
+
+ if (cookieVal != null) {
+ log.debugf("Found AUTH_SESSION_ID cookie with value %s", cookieVal);
+
+ StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class);
+ String decodedAuthSessionId = encoder.decodeSessionId(cookieVal);
+
+ // Check if owner of this authentication session changed due to re-hashing (usually node failover or addition of new node)
+ String reencoded = encoder.encodeSessionId(decodedAuthSessionId);
+ if (!reencoded.equals(cookieVal)) {
+ log.debugf("Route changed. Will update authentication session cookie");
+ setAuthSessionCookie(decodedAuthSessionId, realm);
+ }
+
+ return decodedAuthSessionId;
+ } else {
+ log.debugf("Not found AUTH_SESSION_ID cookie");
+ return null;
+ }
+ }
+
+
+ public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authSession, boolean expireRestartCookie) {
+ log.debugf("Removing authSession '%s'. Expire restart cookie: %b", authSession.getId(), expireRestartCookie);
+ session.authenticationSessions().removeAuthenticationSession(realm, authSession);
+
+ // expire restart cookie
+ if (expireRestartCookie) {
+ ClientConnection clientConnection = session.getContext().getConnection();
+ UriInfo uriInfo = session.getContext().getUri();
+ RestartLoginCookie.expireRestartCookie(realm, clientConnection, uriInfo);
+ }
+ }
+
+
+ // Check to see if we already have authenticationSession with same ID
+ public UserSessionModel getUserSession(AuthenticationSessionModel authSession) {
+ return session.sessions().getUserSession(authSession.getRealm(), authSession.getId());
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
index fec49c9..1bcfaf5 100644
--- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
@@ -39,6 +39,7 @@ import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.adapters.config.BaseRealmConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.sessions.AuthenticationSessionProvider;
import java.net.URI;
import java.util.Collections;
@@ -104,6 +105,11 @@ public class ClientManager {
sessionsPersister.onClientRemoved(realm, client);
}
+ AuthenticationSessionProvider authSessions = realmManager.getSession().authenticationSessions();
+ if (authSessions != null) {
+ authSessions.onClientRemoved(realm, client);
+ }
+
UserModel serviceAccountUser = realmManager.getSession().users().getServiceAccount(client);
if (serviceAccountUser != null) {
new UserManager(realmManager.getSession()).removeUser(realm, serviceAccountUser);
diff --git a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
new file mode 100644
index 0000000..a975aa5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.services.managers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.sessions.CommonClientSessionModel;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+/**
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+class CodeGenerateUtil {
+
+ private static final Logger logger = Logger.getLogger(CodeGenerateUtil.class);
+
+ private static final Map<Class<? extends CommonClientSessionModel>, ClientSessionParser> PARSERS = new HashMap<>();
+
+ static {
+ PARSERS.put(AuthenticationSessionModel.class, new AuthenticationSessionModelParser());
+ PARSERS.put(AuthenticatedClientSessionModel.class, new AuthenticatedClientSessionModelParser());
+ }
+
+
+
+ static <CS extends CommonClientSessionModel> ClientSessionParser<CS> getParser(Class<CS> clientSessionClass) {
+ for (Class<?> c : PARSERS.keySet()) {
+ if (c.isAssignableFrom(clientSessionClass)) {
+ return PARSERS.get(c);
+ }
+ }
+ return null;
+ }
+
+
+ interface ClientSessionParser<CS extends CommonClientSessionModel> {
+
+ CS parseSession(String code, KeycloakSession session, RealmModel realm);
+
+ String generateCode(CS clientSession, String actionId);
+
+ void removeExpiredSession(KeycloakSession session, CS clientSession);
+
+ String getNote(CS clientSession, String name);
+
+ void removeNote(CS clientSession, String name);
+
+ void setNote(CS clientSession, String name, String value);
+
+ }
+
+
+ // IMPLEMENTATIONS
+
+
+ private static class AuthenticationSessionModelParser implements ClientSessionParser<AuthenticationSessionModel> {
+
+ @Override
+ public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm) {
+ // Read authSessionID from cookie. Code is ignored for now
+ return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
+ }
+
+ @Override
+ public String generateCode(AuthenticationSessionModel clientSession, String actionId) {
+ return actionId;
+ }
+
+ @Override
+ public void removeExpiredSession(KeycloakSession session, AuthenticationSessionModel clientSession) {
+ new AuthenticationSessionManager(session).removeAuthenticationSession(clientSession.getRealm(), clientSession, true);
+ }
+
+ @Override
+ public String getNote(AuthenticationSessionModel clientSession, String name) {
+ return clientSession.getAuthNote(name);
+ }
+
+ @Override
+ public void removeNote(AuthenticationSessionModel clientSession, String name) {
+ clientSession.removeAuthNote(name);
+ }
+
+ @Override
+ public void setNote(AuthenticationSessionModel clientSession, String name, String value) {
+ clientSession.setAuthNote(name, value);
+ }
+ }
+
+
+ private static class AuthenticatedClientSessionModelParser implements ClientSessionParser<AuthenticatedClientSessionModel> {
+
+ @Override
+ public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm) {
+ try {
+ String[] parts = code.split("\\.");
+ String userSessionId = parts[2];
+ String clientUUID = parts[3];
+
+ UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId);
+ if (userSession == null) {
+ return null;
+ }
+
+ return userSession.getAuthenticatedClientSessions().get(clientUUID);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public String generateCode(AuthenticatedClientSessionModel clientSession, String actionId) {
+ String userSessionId = clientSession.getUserSession().getId();
+ String clientUUID = clientSession.getClient().getId();
+ StringBuilder sb = new StringBuilder();
+ sb.append("uss.");
+ sb.append(actionId);
+ sb.append('.');
+ sb.append(userSessionId);
+ sb.append('.');
+ sb.append(clientUUID);
+
+ return sb.toString();
+ }
+
+ @Override
+ public void removeExpiredSession(KeycloakSession session, AuthenticatedClientSessionModel clientSession) {
+ throw new IllegalStateException("Not yet implemented");
+ }
+
+ @Override
+ public String getNote(AuthenticatedClientSessionModel clientSession, String name) {
+ return clientSession.getNote(name);
+ }
+
+ @Override
+ public void removeNote(AuthenticatedClientSessionModel clientSession, String name) {
+ clientSession.removeNote(name);
+ }
+
+ @Override
+ public void setNote(AuthenticatedClientSessionModel clientSession, String name, String value) {
+ clientSession.setNote(name, value);
+ }
+ }
+
+
+}
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index 3306bcd..e94ff3c 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -47,6 +47,7 @@ import org.keycloak.representations.idm.OAuthClientRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.services.clientregistration.policy.DefaultClientRegistrationPolicies;
@@ -248,6 +249,11 @@ public class RealmManager {
sessionsPersister.onRealmRemoved(realm);
}
+ AuthenticationSessionProvider authSessions = session.authenticationSessions();
+ if (authSessions != null) {
+ authSessions.onRealmRemoved(realm);
+ }
+
// Refresh periodic sync tasks for configured storageProviders
List<UserStorageProviderModel> storageProviders = realm.getUserStorageProviders();
UserStorageSyncManager storageSync = new UserStorageSyncManager();
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 12e0449..9aa4b69 100755
--- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
@@ -24,8 +24,8 @@ import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.common.util.Time;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.constants.AdapterConstants;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@@ -113,7 +113,7 @@ public class ResourceAdminManager {
protected void logoutUserSessions(URI requestUri, RealmModel realm, List<UserSessionModel> userSessions) {
// Map from "app" to clientSessions for this app
- MultivaluedHashMap<ClientModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ClientModel, ClientSessionModel>();
+ MultivaluedHashMap<String, AuthenticatedClientSessionModel> clientSessions = new MultivaluedHashMap<>();
for (UserSessionModel userSession : userSessions) {
putClientSessions(clientSessions, userSession);
}
@@ -121,37 +121,40 @@ public class ResourceAdminManager {
logger.debugv("logging out {0} resources ", clientSessions.size());
//logger.infov("logging out resources: {0}", clientSessions);
- for (Map.Entry<ClientModel, List<ClientSessionModel>> entry : clientSessions.entrySet()) {
- logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue());
+ for (Map.Entry<String, List<AuthenticatedClientSessionModel>> entry : clientSessions.entrySet()) {
+ if (entry.getValue().size() == 0) {
+ continue;
+ }
+ logoutClientSessions(requestUri, realm, entry.getValue().get(0).getClient(), entry.getValue());
}
}
- private void putClientSessions(MultivaluedHashMap<ClientModel, ClientSessionModel> clientSessions, UserSessionModel userSession) {
- for (ClientSessionModel clientSession : userSession.getClientSessions()) {
- ClientModel client = clientSession.getClient();
- clientSessions.add(client, clientSession);
+ private void putClientSessions(MultivaluedHashMap<String, AuthenticatedClientSessionModel> clientSessions, UserSessionModel userSession) {
+ for (Map.Entry<String, AuthenticatedClientSessionModel> entry : userSession.getAuthenticatedClientSessions().entrySet()) {
+ clientSessions.add(entry.getKey(), entry.getValue());
}
}
public void logoutUserFromClient(URI requestUri, RealmModel realm, ClientModel resource, UserModel user) {
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
- List<ClientSessionModel> ourAppClientSessions = null;
+ List<AuthenticatedClientSessionModel> ourAppClientSessions = new LinkedList<>();
if (userSessions != null) {
- MultivaluedHashMap<ClientModel, ClientSessionModel> clientSessions = new MultivaluedHashMap<ClientModel, ClientSessionModel>();
for (UserSessionModel userSession : userSessions) {
- putClientSessions(clientSessions, userSession);
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(resource.getId());
+ if (clientSession != null) {
+ ourAppClientSessions.add(clientSession);
+ }
}
- ourAppClientSessions = clientSessions.get(resource);
}
logoutClientSessions(requestUri, realm, resource, ourAppClientSessions);
}
- public boolean logoutClientSession(URI requestUri, RealmModel realm, ClientModel resource, ClientSessionModel clientSession) {
+ public boolean logoutClientSession(URI requestUri, RealmModel realm, ClientModel resource, AuthenticatedClientSessionModel clientSession) {
return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession));
}
- protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ClientModel resource, List<ClientSessionModel> clientSessions) {
+ protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ClientModel resource, List<AuthenticatedClientSessionModel> clientSessions) {
String managementUrl = getManagementUrl(requestUri, resource);
if (managementUrl != null) {
@@ -160,7 +163,7 @@ public class ResourceAdminManager {
List<String> userSessions = new LinkedList<>();
if (clientSessions != null && clientSessions.size() > 0) {
adapterSessionIds = new MultivaluedHashMap<String, String>();
- for (ClientSessionModel clientSession : clientSessions) {
+ for (AuthenticatedClientSessionModel clientSession : clientSessions) {
String adapterSessionId = clientSession.getNote(AdapterConstants.CLIENT_SESSION_STATE);
if (adapterSessionId != null) {
String host = clientSession.getNote(AdapterConstants.CLIENT_SESSION_HOST);
diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java
index 4c8c2fe..f347d4c 100644
--- a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java
@@ -18,11 +18,10 @@ package org.keycloak.services.managers;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
@@ -31,7 +30,6 @@ import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.services.ServicesLogger;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -52,7 +50,7 @@ public class UserSessionManager {
this.persister = session.getProvider(UserSessionPersisterProvider.class);
}
- public void createOrUpdateOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
+ public void createOrUpdateOfflineSession(AuthenticatedClientSessionModel clientSession, UserSessionModel userSession) {
UserModel user = userSession.getUser();
// Create and persist offline userSession if we don't have one
@@ -65,50 +63,50 @@ public class UserSessionManager {
}
// Create and persist clientSession
- ClientSessionModel offlineClientSession = kcSession.sessions().getOfflineClientSession(clientSession.getRealm(), clientSession.getId());
+ AuthenticatedClientSessionModel offlineClientSession = offlineUserSession.getAuthenticatedClientSessions().get(clientSession.getClient().getId());
if (offlineClientSession == null) {
createOfflineClientSession(user, clientSession, offlineUserSession);
}
}
- // userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation
- public ClientSessionModel findOfflineClientSession(RealmModel realm, String clientSessionId) {
- return kcSession.sessions().getOfflineClientSession(realm, clientSessionId);
+
+ public UserSessionModel findOfflineUserSession(RealmModel realm, String userSessionId) {
+ return kcSession.sessions().getOfflineUserSession(realm, userSessionId);
}
public Set<ClientModel> findClientsWithOfflineToken(RealmModel realm, UserModel user) {
- List<ClientSessionModel> clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user);
+ List<UserSessionModel> userSessions = kcSession.sessions().getOfflineUserSessions(realm, user);
Set<ClientModel> clients = new HashSet<>();
- for (ClientSessionModel clientSession : clientSessions) {
- clients.add(clientSession.getClient());
+ for (UserSessionModel userSession : userSessions) {
+ Set<String> clientIds = userSession.getAuthenticatedClientSessions().keySet();
+ for (String clientUUID : clientIds) {
+ ClientModel client = realm.getClientById(clientUUID);
+ clients.add(client);
+ }
}
return clients;
}
- public List<UserSessionModel> findOfflineSessions(RealmModel realm, ClientModel client, UserModel user) {
- List<ClientSessionModel> clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user);
- List<UserSessionModel> userSessions = new LinkedList<>();
- for (ClientSessionModel clientSession : clientSessions) {
- userSessions.add(clientSession.getUserSession());
- }
- return userSessions;
+ public List<UserSessionModel> findOfflineSessions(RealmModel realm, UserModel user) {
+ return kcSession.sessions().getOfflineUserSessions(realm, user);
}
public boolean revokeOfflineToken(UserModel user, ClientModel client) {
RealmModel realm = client.getRealm();
- List<ClientSessionModel> clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user);
+ List<UserSessionModel> userSessions = kcSession.sessions().getOfflineUserSessions(realm, user);
boolean anyRemoved = false;
- for (ClientSessionModel clientSession : clientSessions) {
- if (clientSession.getClient().getId().equals(client.getId())) {
+ for (UserSessionModel userSession : userSessions) {
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+ if (clientSession != null) {
if (logger.isTraceEnabled()) {
- logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' .",
- user.getUsername(), client.getClientId(), clientSession.getId());
+ logger.tracef("Removing existing offline token for user '%s' and client '%s' .",
+ user.getUsername(), client.getClientId());
}
- kcSession.sessions().removeOfflineClientSession(realm, clientSession.getId());
- persister.removeClientSession(clientSession.getId(), true);
- checkOfflineUserSessionHasClientSessions(realm, user, clientSession.getUserSession(), clientSessions);
+ clientSession.setUserSession(null);
+ persister.removeClientSession(userSession.getId(), client.getId(), true);
+ checkOfflineUserSessionHasClientSessions(realm, user, userSession);
anyRemoved = true;
}
}
@@ -124,7 +122,7 @@ public class UserSessionManager {
persister.removeUserSession(userSession.getId(), true);
}
- public boolean isOfflineTokenAllowed(ClientSessionModel clientSession) {
+ public boolean isOfflineTokenAllowed(AuthenticatedClientSessionModel clientSession) {
RoleModel offlineAccessRole = clientSession.getRealm().getRole(Constants.OFFLINE_ACCESS_ROLE);
if (offlineAccessRole == null) {
ServicesLogger.LOGGER.roleNotInRealm(Constants.OFFLINE_ACCESS_ROLE);
@@ -144,30 +142,26 @@ public class UserSessionManager {
return offlineUserSession;
}
- private void createOfflineClientSession(UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) {
+ private void createOfflineClientSession(UserModel user, AuthenticatedClientSessionModel clientSession, UserSessionModel offlineUserSession) {
if (logger.isTraceEnabled()) {
logger.tracef("Creating new offline token client session. ClientSessionId: '%s', UserSessionID: '%s' , Username: '%s', Client: '%s'" ,
- clientSession.getId(), userSession.getId(), user.getUsername(), clientSession.getClient().getClientId());
+ clientSession.getId(), offlineUserSession.getId(), user.getUsername(), clientSession.getClient().getClientId());
}
- ClientSessionModel offlineClientSession = kcSession.sessions().createOfflineClientSession(clientSession);
- offlineClientSession.setUserSession(userSession);
+ kcSession.sessions().createOfflineClientSession(clientSession, offlineUserSession);
persister.createClientSession(clientSession, true);
}
// Check if userSession has any offline clientSessions attached to it. Remove userSession if not
- private void checkOfflineUserSessionHasClientSessions(RealmModel realm, UserModel user, UserSessionModel userSession, List<ClientSessionModel> clientSessions) {
- String userSessionId = userSession.getId();
- for (ClientSessionModel clientSession : clientSessions) {
- if (clientSession.getUserSession().getId().equals(userSessionId)) {
- return;
- }
+ private void checkOfflineUserSessionHasClientSessions(RealmModel realm, UserModel user, UserSessionModel userSession) {
+ if (userSession.getAuthenticatedClientSessions().size() > 0) {
+ return;
}
if (logger.isTraceEnabled()) {
- logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSessionId);
+ logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSession.getId());
}
kcSession.sessions().removeOfflineUserSession(realm, userSession);
- persister.removeUserSession(userSessionId, true);
+ persister.removeUserSession(userSession.getId(), true);
}
}
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index d9d3c4e..295f07b 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -33,6 +33,8 @@ public class Messages {
public static final String EXPIRED_CODE = "expiredCodeMessage";
+ public static final String EXPIRED_ACTION = "expiredActionMessage";
+
public static final String MISSING_FIRST_NAME = "missingFirstNameMessage";
public static final String MISSING_LAST_NAME = "missingLastNameMessage";
@@ -197,5 +199,10 @@ public class Messages {
public static final String FAILED_LOGOUT = "failedLogout";
public static final String CONSENT_DENIED="consentDenied";
+
public static final String ALREADY_LOGGED_IN="alreadyLoggedIn";
+
+ public static final String DIFFERENT_USER_AUTHENTICATED = "differentUserAuthenticated";
+
+ public static final String BROKER_LINKING_SESSION_EXPIRED = "brokerLinkingSessionExpired";
}
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index e0e5f8b..9dd9c4b 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -30,8 +30,8 @@ import org.keycloak.forms.account.AccountPages;
import org.keycloak.forms.account.AccountProvider;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.AccountRoles;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
@@ -44,7 +44,6 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.ModelToRepresentation;
-import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.ForbiddenException;
@@ -53,11 +52,11 @@ import org.keycloak.services.Urls;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager;
-import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation;
+import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.util.JsonSerialization;
@@ -67,13 +66,12 @@ import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
-import javax.ws.rs.core.Variant;
+
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -164,19 +162,11 @@ public class AccountService extends AbstractSecuredLocalService {
if (authResult != null) {
UserSessionModel userSession = authResult.getSession();
if (userSession != null) {
- boolean associated = false;
- for (ClientSessionModel c : userSession.getClientSessions()) {
- if (c.getClient().equals(client)) {
- auth.setClientSession(c);
- associated = true;
- break;
- }
- }
- if (!associated) {
- ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
- clientSession.setUserSession(userSession);
- auth.setClientSession(clientSession);
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
+ if (clientSession == null) {
+ clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession);
}
+ auth.setClientSession(clientSession);
}
account.setUser(auth.getUser());
@@ -216,14 +206,18 @@ public class AccountService extends AbstractSecuredLocalService {
setReferrerOnPage();
- String forwardedError = auth.getClientSession().getNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
- if (forwardedError != null) {
- try {
- FormMessage errorMessage = JsonSerialization.readValue(forwardedError, FormMessage.class);
- account.setError(errorMessage.getMessage(), errorMessage.getParameters());
- auth.getClientSession().removeNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
- } catch (IOException ioe) {
- throw new RuntimeException(ioe);
+ UserSessionModel userSession = auth.getClientSession().getUserSession();
+ AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, userSession.getId());
+ if (authSession != null) {
+ String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
+ if (forwardedError != null) {
+ try {
+ FormMessage errorMessage = JsonSerialization.readValue(forwardedError, FormMessage.class);
+ account.setError(errorMessage.getMessage(), errorMessage.getParameters());
+ authSession.removeAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
}
}
@@ -777,7 +771,7 @@ public class AccountService extends AbstractSecuredLocalService {
try {
String nonce = UUID.randomUUID().toString();
MessageDigest md = MessageDigest.getInstance("SHA-256");
- String input = nonce + auth.getSession().getId() + auth.getClientSession().getId() + providerId;
+ String input = nonce + auth.getSession().getId() + client.getClientId() + providerId;
byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
String hash = Base64Url.encode(check);
URI linkUrl = Urls.identityProviderLinkRequest(this.uriInfo.getBaseUri(), providerId, realm.getName());
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index 89b0a33..a92729e 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -25,8 +25,8 @@ import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
@@ -492,8 +492,11 @@ public class ClientResource {
UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession);
// Update lastSessionRefresh with the timestamp from clientSession
- for (ClientSessionModel clientSession : userSession.getClientSessions()) {
- if (client.getId().equals(clientSession.getClient().getId())) {
+ for (Map.Entry<String, AuthenticatedClientSessionModel> csEntry : userSession.getAuthenticatedClientSessions().entrySet()) {
+ String clientUuid = csEntry.getKey();
+ AuthenticatedClientSessionModel clientSession = csEntry.getValue();
+
+ if (client.getId().equals(clientUuid)) {
rep.setLastAccess(Time.toMillis(clientSession.getTimestamp()));
break;
}
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 3259982..815ee89 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
@@ -22,6 +22,7 @@ import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
@@ -33,24 +34,9 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.Constants;
-import org.keycloak.models.FederatedIdentityModel;
-import org.keycloak.models.GroupModel;
-import org.keycloak.models.IdentityProviderModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelDuplicateException;
-import org.keycloak.models.ModelException;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserConsentModel;
-import org.keycloak.models.UserCredentialModel;
-import org.keycloak.models.UserLoginFailureModel;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
-import org.keycloak.models.credential.PasswordUserCredentialModel;
+import org.keycloak.models.*;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
-import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.provider.ProviderFactory;
@@ -62,14 +48,12 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ErrorResponseException;
-import org.keycloak.services.ServicesLogger;
-import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.BruteForceProtector;
-import org.keycloak.services.managers.ClientSessionCode;
-import org.keycloak.models.UserManager;
-import org.keycloak.services.managers.UserSessionManager;
+import org.keycloak.services.*;
+import org.keycloak.services.managers.*;
import org.keycloak.services.resources.AccountService;
+import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.validation.Validation;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.utils.ProfileHelper;
@@ -83,26 +67,18 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
-import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
-import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
+import java.util.*;
import java.util.concurrent.TimeUnit;
+import javax.ws.rs.*;
+import javax.ws.rs.core.*;
/**
* Base resource for managing users
@@ -344,7 +320,8 @@ public class UsersResource {
}
EventBuilder event = new EventBuilder(realm, session, clientConnection);
- UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
+ String sessionId = KeycloakModelUtils.generateId();
+ UserSessionModel userSession = session.sessions().createUserSession(sessionId, realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, uriInfo, clientConnection);
URI redirect = AccountService.accountServiceApplicationPage(uriInfo).build(realm.getName());
Map<String, Object> result = new HashMap<>();
@@ -396,7 +373,7 @@ public class UsersResource {
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
- public List<UserSessionRepresentation> getSessions(final @PathParam("id") String id, final @PathParam("clientId") String clientId) {
+ public List<UserSessionRepresentation> getOfflineSessions(final @PathParam("id") String id, final @PathParam("clientId") String clientId) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm);
@@ -407,19 +384,21 @@ public class UsersResource {
if (client == null) {
throw new NotFoundException("Client not found");
}
- List<UserSessionModel> sessions = new UserSessionManager(session).findOfflineSessions(realm, client, user);
+ List<UserSessionModel> sessions = new UserSessionManager(session).findOfflineSessions(realm, user);
List<UserSessionRepresentation> reps = new ArrayList<UserSessionRepresentation>();
for (UserSessionModel session : sessions) {
UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session);
// Update lastSessionRefresh with the timestamp from clientSession
- for (ClientSessionModel clientSession : session.getClientSessions()) {
- if (clientId.equals(clientSession.getClient().getId())) {
- rep.setLastAccess(Time.toMillis(clientSession.getTimestamp()));
- break;
- }
+ AuthenticatedClientSessionModel clientSession = session.getAuthenticatedClientSessions().get(clientId);
+
+ // Skip if userSession is not for this client
+ if (clientSession == null) {
+ continue;
}
+ rep.setLastAccess(clientSession.getTimestamp());
+
reps.add(rep);
}
return reps;
@@ -837,7 +816,7 @@ public class UsersResource {
@QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) {
List<String> actions = new LinkedList<>();
actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name());
- return executeActionsEmail(id, redirectUri, clientId, actions);
+ return executeActionsEmail(id, redirectUri, clientId, null, actions);
}
@@ -852,6 +831,7 @@ public class UsersResource {
* @param id User is
* @param redirectUri Redirect uri
* @param clientId Client id
+ * @param lifespan Number of seconds after which the generated token expires
* @param actions required actions the user needs to complete
* @return
*/
@@ -861,6 +841,7 @@ public class UsersResource {
public Response executeActionsEmail(@PathParam("id") String id,
@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri,
@QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId,
+ @QueryParam("lifespan") Integer lifespan,
List<String> actions) {
auth.requireManage();
@@ -873,25 +854,51 @@ public class UsersResource {
return ErrorResponse.error("User email missing", Response.Status.BAD_REQUEST);
}
- ClientSessionModel clientSession = createClientSession(user, redirectUri, clientId);
- for (String action : actions) {
- clientSession.addRequiredAction(action);
+ if (!user.isEnabled()) {
+ throw new WebApplicationException(
+ ErrorResponse.error("User is disabled", Response.Status.BAD_REQUEST));
+ }
+
+ if (redirectUri != null && clientId == null) {
+ throw new WebApplicationException(
+ ErrorResponse.error("Client id missing", Response.Status.BAD_REQUEST));
+ }
+
+ if (clientId == null) {
+ clientId = Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
}
+
+ ClientModel client = realm.getClientByClientId(clientId);
+ if (client == null || !client.isEnabled()) {
+ throw new WebApplicationException(
+ ErrorResponse.error(clientId + " not enabled", Response.Status.BAD_REQUEST));
+ }
+
+ String redirect;
if (redirectUri != null) {
- clientSession.setNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true");
+ redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client);
+ if (redirect == null) {
+ throw new WebApplicationException(
+ ErrorResponse.error("Invalid redirect uri.", Response.Status.BAD_REQUEST));
+ }
+ }
+ if (lifespan == null) {
+ lifespan = realm.getActionTokenGeneratedByAdminLifespan();
}
- ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession);
- accessCode.setAction(ClientSessionModel.Action.EXECUTE_ACTIONS.name());
+ int expiration = Time.currentTime() + lifespan;
+ ExecuteActionsActionToken token = new ExecuteActionsActionToken(id, expiration, actions, redirectUri, clientId);
try {
- UriBuilder builder = Urls.executeActionsBuilder(uriInfo.getBaseUri());
- builder.queryParam("key", accessCode.getCode());
+ UriBuilder builder = LoginActionsService.actionTokenProcessor(uriInfo);
+ builder.queryParam("key", token.serialize(session, realm, uriInfo));
String link = builder.build(realm.getName()).toString();
- long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
- this.session.getProvider(EmailTemplateProvider.class).setRealm(realm).setUser(user).sendExecuteActions(link, expiration);
+ this.session.getProvider(EmailTemplateProvider.class)
+ .setRealm(realm)
+ .setUser(user)
+ .sendExecuteActions(link, TimeUnit.SECONDS.toMinutes(lifespan));
//audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getCodeId()).success();
@@ -922,49 +929,7 @@ public class UsersResource {
public Response sendVerifyEmail(@PathParam("id") String id, @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) {
List<String> actions = new LinkedList<>();
actions.add(UserModel.RequiredAction.VERIFY_EMAIL.name());
- return executeActionsEmail(id, redirectUri, clientId, actions);
- }
-
- private ClientSessionModel createClientSession(UserModel user, String redirectUri, String clientId) {
-
- if (!user.isEnabled()) {
- throw new WebApplicationException(
- ErrorResponse.error("User is disabled", Response.Status.BAD_REQUEST));
- }
-
- if (redirectUri != null && clientId == null) {
- throw new WebApplicationException(
- ErrorResponse.error("Client id missing", Response.Status.BAD_REQUEST));
- }
-
- if (clientId == null) {
- clientId = Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
- }
-
- ClientModel client = realm.getClientByClientId(clientId);
- if (client == null || !client.isEnabled()) {
- throw new WebApplicationException(
- ErrorResponse.error(clientId + " not enabled", Response.Status.BAD_REQUEST));
- }
-
- String redirect = null;
- if (redirectUri != null) {
- redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client);
- if (redirect == null) {
- throw new WebApplicationException(
- ErrorResponse.error("Invalid redirect uri.", Response.Status.BAD_REQUEST));
- }
- }
-
-
- UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "form", false, null, null);
- //audit.session(userSession);
- ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
- clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
- clientSession.setRedirectUri(redirect);
- clientSession.setUserSession(userSession);
-
- return clientSession;
+ return executeActionsEmail(id, redirectUri, clientId, null, actions);
}
@GET
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index f9efe19..333a1cf 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -20,8 +20,6 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
-
-import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants;
@@ -43,10 +41,10 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
-import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.AccountRoles;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderMapperModel;
@@ -72,26 +70,16 @@ import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
-import org.keycloak.services.managers.AuthenticationManager.AuthResult;
+import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
+import org.keycloak.services.util.BrowserHistoryHelper;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.validation.Validation;
+import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.JsonSerialization;
-import javax.ws.rs.GET;
-import javax.ws.rs.OPTIONS;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
@@ -106,10 +94,18 @@ import java.util.Optional;
import java.util.Set;
import java.util.UUID;
-import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
-import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS;
-import static org.keycloak.models.ClientSessionModel.Action.AUTHENTICATE;
-import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
/**
* <p></p>
@@ -118,6 +114,9 @@ import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
*/
public class IdentityBrokerService implements IdentityProvider.AuthenticationCallback {
+ // Authentication session note, which references identity provider that is currently linked
+ private static final String LINKING_IDENTITY_PROVIDER = "LINKING_IDENTITY_PROVIDER";
+
private static final Logger logger = Logger.getLogger(IdentityBrokerService.class);
private final RealmModel realmModel;
@@ -205,13 +204,14 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
this.event.event(EventType.CLIENT_INITIATED_ACCOUNT_LINKING);
checkRealm();
ClientModel client = checkClient(clientId);
- AuthenticationManager authenticationManager = new AuthenticationManager();
redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realmModel, client);
if (redirectUri == null) {
event.error(Errors.INVALID_REDIRECT_URI);
throw new ErrorPageException(session, Messages.INVALID_REQUEST);
}
+ event.detail(Details.REDIRECT_URI, redirectUri);
+
if (nonce == null || hash == null) {
event.error(Errors.INVALID_REDIRECT_URI);
throw new ErrorPageException(session, Messages.INVALID_REQUEST);
@@ -230,7 +230,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
}
- AuthResult cookieResult = authenticationManager.authenticateIdentityCookie(session, realmModel, true);
+ AuthenticationManager.AuthResult cookieResult = AuthenticationManager.authenticateIdentityCookie(session, realmModel, true);
String errorParam = "link_error";
if (cookieResult == null) {
event.error(Errors.NOT_LOGGED_IN);
@@ -241,10 +241,13 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return Response.status(302).location(builder.build()).build();
}
+ cookieResult.getSession();
+ event.session(cookieResult.getSession());
+ event.user(cookieResult.getUser());
+ event.detail(Details.USERNAME, cookieResult.getUser().getUsername());
-
- ClientSessionModel clientSession = null;
- for (ClientSessionModel cs : cookieResult.getSession().getClientSessions()) {
+ AuthenticatedClientSessionModel clientSession = null;
+ for (AuthenticatedClientSessionModel cs : cookieResult.getSession().getAuthenticatedClientSessions().values()) {
if (cs.getClient().getClientId().equals(clientId)) {
byte[] decoded = Base64Url.decode(hash);
MessageDigest md = null;
@@ -253,7 +256,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
} catch (NoSuchAlgorithmException e) {
throw new ErrorPageException(session, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST);
}
- String input = nonce + cookieResult.getSession().getId() + cs.getId() + providerId;
+ String input = nonce + cookieResult.getSession().getId() + clientId + providerId;
byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
if (MessageDigest.isEqual(decoded, check)) {
clientSession = cs;
@@ -266,14 +269,14 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
throw new ErrorPageException(session, Messages.INVALID_REQUEST);
}
+ event.detail(Details.IDENTITY_PROVIDER, providerId);
-
- ClientModel accountService = this.realmModel.getClientByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID);
+ ClientModel accountService = this.realmModel.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
if (!accountService.getId().equals(client.getId())) {
- RoleModel manageAccountRole = accountService.getRole(MANAGE_ACCOUNT);
+ RoleModel manageAccountRole = accountService.getRole(AccountRoles.MANAGE_ACCOUNT);
if (!clientSession.getRoles().contains(manageAccountRole.getId())) {
- RoleModel linkRole = accountService.getRole(MANAGE_ACCOUNT_LINKS);
+ RoleModel linkRole = accountService.getRole(AccountRoles.MANAGE_ACCOUNT_LINKS);
if (!clientSession.getRoles().contains(linkRole.getId())) {
event.error(Errors.NOT_ALLOWED);
UriBuilder builder = UriBuilder.fromUri(redirectUri)
@@ -296,16 +299,22 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
+ // Create AuthenticationSessionModel with same ID like userSession and refresh cookie
+ UserSessionModel userSession = cookieResult.getSession();
+ AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(userSession.getId(), realmModel, client);
+ new AuthenticationSessionManager(session).setAuthSessionCookie(userSession.getId(), realmModel);
- ClientSessionCode clientSessionCode = new ClientSessionCode(session, realmModel, clientSession);
- clientSessionCode.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
+ ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
+ clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
clientSessionCode.getCode();
- clientSession.setRedirectUri(redirectUri);
- clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, UUID.randomUUID().toString());
+ authSession.setProtocol(client.getProtocol());
+ authSession.setRedirectUri(redirectUri);
+ authSession.setClientNote(OIDCLoginProtocol.STATE_PARAM, UUID.randomUUID().toString());
+ authSession.setAuthNote(LINKING_IDENTITY_PROVIDER, cookieResult.getSession().getId() + clientId + providerId);
+ event.detail(Details.CODE_ID, userSession.getId());
event.success();
-
try {
IdentityProvider identityProvider = getIdentityProvider(session, realmModel, providerId);
Response response = identityProvider.performLogin(createAuthenticationRequest(providerId, clientSessionCode));
@@ -414,7 +423,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
try {
AppAuthManager authManager = new AppAuthManager();
- AuthResult authResult = authManager.authenticateBearerToken(this.session, this.realmModel, this.uriInfo, this.clientConnection, this.request.getHttpHeaders());
+ AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(this.session, this.realmModel, this.uriInfo, this.clientConnection, this.request.getHttpHeaders());
if (authResult != null) {
AccessToken token = authResult.getToken();
@@ -475,7 +484,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
if (parsedCode.response != null) {
return parsedCode.response;
}
- ClientSessionCode clientCode = parsedCode.clientSessionCode;
+ ClientSessionCode<AuthenticationSessionModel> clientCode = parsedCode.clientSessionCode;
String providerId = identityProviderConfig.getAlias();
if (!identityProviderConfig.isStoreToken()) {
@@ -485,10 +494,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
context.setToken(null);
}
- ClientSessionModel clientSession = clientCode.getClientSession();
- context.setClientSession(clientSession);
+ AuthenticationSessionModel authenticationSession = clientCode.getClientSession();
+ context.setAuthenticationSession(authenticationSession);
- session.getContext().setClient(clientSession.getClient());
+ session.getContext().setClient(authenticationSession.getClient());
context.getIdp().preprocessFederatedIdentity(session, realmModel, context);
Set<IdentityProviderMapperModel> mappers = realmModel.getIdentityProviderMappersByAlias(context.getIdpConfig().getAlias());
@@ -504,14 +513,16 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
context.getUsername(), context.getToken());
this.event.event(EventType.IDENTITY_PROVIDER_LOGIN)
- .detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
+ .detail(Details.REDIRECT_URI, authenticationSession.getRedirectUri())
+ .detail(Details.IDENTITY_PROVIDER, providerId)
.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername());
UserModel federatedUser = this.session.users().getUserByFederatedIdentity(federatedIdentityModel, this.realmModel);
// Check if federatedUser is already authenticated (this means linking social into existing federatedUser account)
- if (clientSession.getUserSession() != null) {
- return performAccountLinking(clientSession, context, federatedIdentityModel, federatedUser);
+ UserSessionModel userSession = new AuthenticationSessionManager(session).getUserSession(authenticationSession);
+ if (shouldPerformAccountLinking(authenticationSession, userSession, providerId)) {
+ return performAccountLinking(authenticationSession, userSession, context, federatedIdentityModel, federatedUser);
}
if (federatedUser == null) {
@@ -531,13 +542,13 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
username = username.trim();
context.setModelUsername(username);
- clientSession.setTimestamp(Time.currentTime());
+ // Redirect to firstBrokerLogin after successful login and ensure that previous authentication state removed
+ AuthenticationProcessor.resetFlow(authenticationSession, LoginActionsService.FIRST_BROKER_LOGIN_PATH);
SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context);
- ctx.saveToClientSession(clientSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
+ ctx.saveToAuthenticationSession(authenticationSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
URI redirect = LoginActionsService.firstBrokerLoginProcessor(uriInfo)
- .queryParam(OAuth2Constants.CODE, clientCode.getCode())
.build(realmModel.getName());
return Response.status(302).location(redirect).build();
@@ -548,12 +559,13 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
updateFederatedIdentity(context, federatedUser);
- clientSession.setAuthenticatedUser(federatedUser);
+ authenticationSession.setAuthenticatedUser(federatedUser);
- return finishOrRedirectToPostBrokerLogin(clientSession, context, false, parsedCode.clientSessionCode);
+ return finishOrRedirectToPostBrokerLogin(authenticationSession, context, false, parsedCode.clientSessionCode);
}
}
+
public Response validateUser(UserModel user, RealmModel realm) {
if (!user.isEnabled()) {
event.error(Errors.USER_DISABLED);
@@ -580,29 +592,35 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return afterFirstBrokerLogin(parsedCode.clientSessionCode);
}
- private Response afterFirstBrokerLogin(ClientSessionCode clientSessionCode) {
- ClientSessionModel clientSession = clientSessionCode.getClientSession();
+ private Response afterFirstBrokerLogin(ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
+ AuthenticationSessionModel authSession = clientSessionCode.getClientSession();
try {
- this.event.detail(Details.CODE_ID, clientSession.getId())
+ this.event.detail(Details.CODE_ID, authSession.getId())
.removeDetail("auth_method");
- SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
+ SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
if (serializedCtx == null) {
throw new IdentityBrokerException("Not found serialized context in clientSession");
}
- BrokeredIdentityContext context = serializedCtx.deserialize(session, clientSession);
+ BrokeredIdentityContext context = serializedCtx.deserialize(session, authSession);
String providerId = context.getIdpConfig().getAlias();
event.detail(Details.IDENTITY_PROVIDER, providerId);
event.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername());
+ // Ensure the first-broker-login flow was successfully finished
+ String authProvider = authSession.getAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS);
+ if (authProvider == null || !authProvider.equals(providerId)) {
+ throw new IdentityBrokerException("Invalid request. Not found the flag that first-broker-login flow was finished");
+ }
+
// firstBrokerLogin workflow finished. Removing note now
- clientSession.removeNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
+ authSession.removeAuthNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
- UserModel federatedUser = clientSession.getAuthenticatedUser();
+ UserModel federatedUser = authSession.getAuthenticatedUser();
if (federatedUser == null) {
- throw new IdentityBrokerException("Couldn't found authenticated federatedUser in clientSession");
+ throw new IdentityBrokerException("Couldn't found authenticated federatedUser in authentication session");
}
event.user(federatedUser);
@@ -623,7 +641,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
session.users().addFederatedIdentity(realmModel, federatedUser, federatedIdentityModel);
- String isRegisteredNewUser = clientSession.getNote(AbstractIdpAuthenticator.BROKER_REGISTERED_NEW_USER);
+ String isRegisteredNewUser = authSession.getAuthNote(AbstractIdpAuthenticator.BROKER_REGISTERED_NEW_USER);
if (Boolean.parseBoolean(isRegisteredNewUser)) {
logger.debugf("Registered new user '%s' after first login with identity provider '%s'. Identity provider username is '%s' . ", federatedUser.getUsername(), providerId, context.getUsername());
@@ -638,7 +656,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
}
- if (context.getIdpConfig().isTrustEmail() && !Validation.isBlank(federatedUser.getEmail()) && !Boolean.parseBoolean(clientSession.getNote(AbstractIdpAuthenticator.UPDATE_PROFILE_EMAIL_CHANGED))) {
+ if (context.getIdpConfig().isTrustEmail() && !Validation.isBlank(federatedUser.getEmail()) && !Boolean.parseBoolean(authSession.getAuthNote(AbstractIdpAuthenticator.UPDATE_PROFILE_EMAIL_CHANGED))) {
logger.debugf("Email verified automatically after registration of user '%s' through Identity provider '%s' ", federatedUser.getUsername(), context.getIdpConfig().getAlias());
federatedUser.setEmailVerified(true);
}
@@ -657,7 +675,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
updateFederatedIdentity(context, federatedUser);
}
- return finishOrRedirectToPostBrokerLogin(clientSession, context, true, clientSessionCode);
+ return finishOrRedirectToPostBrokerLogin(authSession, context, true, clientSessionCode);
} catch (Exception e) {
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e);
@@ -665,25 +683,24 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
- private Response finishOrRedirectToPostBrokerLogin(ClientSessionModel clientSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin, ClientSessionCode clientSessionCode) {
+ private Response finishOrRedirectToPostBrokerLogin(AuthenticationSessionModel authSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
String postBrokerLoginFlowId = context.getIdpConfig().getPostBrokerLoginFlowId();
if (postBrokerLoginFlowId == null) {
logger.debugf("Skip redirect to postBrokerLogin flow. PostBrokerLogin flow not set for identityProvider '%s'.", context.getIdpConfig().getAlias());
- return afterPostBrokerLoginFlowSuccess(clientSession, context, wasFirstBrokerLogin, clientSessionCode);
+ return afterPostBrokerLoginFlowSuccess(authSession, context, wasFirstBrokerLogin, clientSessionCode);
} else {
logger.debugf("Redirect to postBrokerLogin flow after authentication with identityProvider '%s'.", context.getIdpConfig().getAlias());
- clientSession.setTimestamp(Time.currentTime());
+ authSession.setTimestamp(Time.currentTime());
SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context);
- ctx.saveToClientSession(clientSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
+ ctx.saveToAuthenticationSession(authSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
- clientSession.setNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN, String.valueOf(wasFirstBrokerLogin));
+ authSession.setAuthNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN, String.valueOf(wasFirstBrokerLogin));
URI redirect = LoginActionsService.postBrokerLoginProcessor(uriInfo)
- .queryParam(OAuth2Constants.CODE, clientSessionCode.getCode())
.build(realmModel.getName());
return Response.status(302).location(redirect).build();
}
@@ -699,87 +716,89 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
if (parsedCode.response != null) {
return parsedCode.response;
}
- ClientSessionModel clientSession = parsedCode.clientSessionCode.getClientSession();
+ AuthenticationSessionModel authenticationSession = parsedCode.clientSessionCode.getClientSession();
try {
- SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
+ SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authenticationSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
if (serializedCtx == null) {
throw new IdentityBrokerException("Not found serialized context in clientSession. Note " + PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT + " was null");
}
- BrokeredIdentityContext context = serializedCtx.deserialize(session, clientSession);
+ BrokeredIdentityContext context = serializedCtx.deserialize(session, authenticationSession);
- String wasFirstBrokerLoginNote = clientSession.getNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN);
+ String wasFirstBrokerLoginNote = authenticationSession.getAuthNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN);
boolean wasFirstBrokerLogin = Boolean.parseBoolean(wasFirstBrokerLoginNote);
// Ensure the post-broker-login flow was successfully finished
String authStateNoteKey = PostBrokerLoginConstants.PBL_AUTH_STATE_PREFIX + context.getIdpConfig().getAlias();
- String authState = clientSession.getNote(authStateNoteKey);
+ String authState = authenticationSession.getAuthNote(authStateNoteKey);
if (!Boolean.parseBoolean(authState)) {
throw new IdentityBrokerException("Invalid request. Not found the flag that post-broker-login flow was finished");
}
// remove notes
- clientSession.removeNote(PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
- clientSession.removeNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN);
+ authenticationSession.removeAuthNote(PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
+ authenticationSession.removeAuthNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN);
- return afterPostBrokerLoginFlowSuccess(clientSession, context, wasFirstBrokerLogin, parsedCode.clientSessionCode);
+ return afterPostBrokerLoginFlowSuccess(authenticationSession, context, wasFirstBrokerLogin, parsedCode.clientSessionCode);
} catch (IdentityBrokerException e) {
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e);
}
}
- private Response afterPostBrokerLoginFlowSuccess(ClientSessionModel clientSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin, ClientSessionCode clientSessionCode) {
+ private Response afterPostBrokerLoginFlowSuccess(AuthenticationSessionModel authSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
String providerId = context.getIdpConfig().getAlias();
- UserModel federatedUser = clientSession.getAuthenticatedUser();
+ UserModel federatedUser = authSession.getAuthenticatedUser();
if (wasFirstBrokerLogin) {
-
- String isDifferentBrowser = clientSession.getNote(AbstractIdpAuthenticator.IS_DIFFERENT_BROWSER);
- if (Boolean.parseBoolean(isDifferentBrowser)) {
- session.sessions().removeClientSession(realmModel, clientSession);
- return session.getProvider(LoginFormsProvider.class)
- .setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, context.getIdpConfig().getAlias(), context.getUsername())
- .createInfoPage();
- } else {
- return finishBrokerAuthentication(context, federatedUser, clientSession, providerId);
- }
-
+ return finishBrokerAuthentication(context, federatedUser, authSession, providerId);
} else {
- boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null);
+ boolean firstBrokerLoginInProgress = (authSession.getAuthNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null);
if (firstBrokerLoginInProgress) {
logger.debugf("Reauthenticated with broker '%s' when linking user '%s' with other broker", context.getIdpConfig().getAlias(), federatedUser.getUsername());
- UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realmModel, clientSession);
+ UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realmModel, authSession);
if (!linkingUser.getId().equals(federatedUser.getId())) {
return redirectToErrorPage(Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, federatedUser.getUsername(), linkingUser.getUsername());
}
+ SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
+ authSession.setAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS, serializedCtx.getIdentityProviderId());
+
return afterFirstBrokerLogin(clientSessionCode);
} else {
- return finishBrokerAuthentication(context, federatedUser, clientSession, providerId);
+ return finishBrokerAuthentication(context, federatedUser, authSession, providerId);
}
}
}
- private Response finishBrokerAuthentication(BrokeredIdentityContext context, UserModel federatedUser, ClientSessionModel clientSession, String providerId) {
- UserSessionModel userSession = this.session.sessions()
- .createUserSession(this.realmModel, federatedUser, federatedUser.getUsername(), this.clientConnection.getRemoteAddr(), "broker", false, context.getBrokerSessionId(), context.getBrokerUserId());
+ private Response finishBrokerAuthentication(BrokeredIdentityContext context, UserModel federatedUser, AuthenticationSessionModel authSession, String providerId) {
+ authSession.setAuthNote(AuthenticationProcessor.BROKER_SESSION_ID, context.getBrokerSessionId());
+ authSession.setAuthNote(AuthenticationProcessor.BROKER_USER_ID, context.getBrokerUserId());
this.event.user(federatedUser);
- this.event.session(userSession);
- TokenManager.attachClientSession(userSession, clientSession);
- context.getIdp().attachUserSession(userSession, clientSession, context);
- userSession.setNote(Details.IDENTITY_PROVIDER, providerId);
- userSession.setNote(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername());
+ context.getIdp().authenticationFinished(authSession, context);
+ authSession.setUserSessionNote(Details.IDENTITY_PROVIDER, providerId);
+ authSession.setUserSessionNote(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername());
+
+ event.detail(Details.IDENTITY_PROVIDER, providerId)
+ .detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername());
if (isDebugEnabled()) {
logger.debugf("Performing local authentication for user [%s].", federatedUser);
}
- return AuthenticationProcessor.redirectToRequiredActions(session, realmModel, clientSession, uriInfo);
+ AuthenticationManager.setRolesAndMappersInSession(authSession);
+
+ String nextRequiredAction = AuthenticationManager.nextRequiredAction(session, authSession, clientConnection, request, uriInfo, event);
+ if (nextRequiredAction != null) {
+ return AuthenticationManager.redirectToRequiredActions(session, realmModel, authSession, uriInfo, nextRequiredAction);
+ } else {
+ event.detail(Details.CODE_ID, authSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set
+ return AuthenticationManager.finishedRequiredActions(session, authSession, null, clientConnection, request, uriInfo, event);
+ }
}
@@ -789,7 +808,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
if (parsedCode.response != null) {
return parsedCode.response;
}
- ClientSessionCode clientCode = parsedCode.clientSessionCode;
+ ClientSessionCode<AuthenticationSessionModel> clientCode = parsedCode.clientSessionCode;
Response accountManagementFailedLinking = checkAccountManagementFailedLinking(clientCode.getClientSession(), Messages.CONSENT_DENIED);
if (accountManagementFailedLinking != null) {
@@ -805,7 +824,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
if (parsedCode.response != null) {
return parsedCode.response;
}
- ClientSessionCode clientCode = parsedCode.clientSessionCode;
+ ClientSessionCode<AuthenticationSessionModel> clientCode = parsedCode.clientSessionCode;
Response accountManagementFailedLinking = checkAccountManagementFailedLinking(clientCode.getClientSession(), message);
if (accountManagementFailedLinking != null) {
@@ -815,23 +834,50 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return browserAuthentication(clientCode.getClientSession(), message);
}
- private Response performAccountLinking(ClientSessionModel clientSession, BrokeredIdentityContext context, FederatedIdentityModel newModel, UserModel federatedUser) {
+
+ private boolean shouldPerformAccountLinking(AuthenticationSessionModel authSession, UserSessionModel userSession, String providerId) {
+ String noteFromSession = authSession.getAuthNote(LINKING_IDENTITY_PROVIDER);
+ if (noteFromSession == null) {
+ return false;
+ }
+
+ boolean linkingValid;
+ if (userSession == null) {
+ linkingValid = false;
+ } else {
+ String expectedNote = userSession.getId() + authSession.getClient().getClientId() + providerId;
+ linkingValid = expectedNote.equals(noteFromSession);
+ }
+
+ if (linkingValid) {
+ authSession.removeAuthNote(LINKING_IDENTITY_PROVIDER);
+ return true;
+ } else {
+ throw new ErrorPageException(session, Messages.BROKER_LINKING_SESSION_EXPIRED);
+ }
+ }
+
+
+ private Response performAccountLinking(AuthenticationSessionModel authSession, UserSessionModel userSession, BrokeredIdentityContext context, FederatedIdentityModel newModel, UserModel federatedUser) {
+ logger.debugf("Will try to link identity provider [%s] to user [%s]", context.getIdpConfig().getAlias(), userSession.getUser().getUsername());
+
this.event.event(EventType.FEDERATED_IDENTITY_LINK);
- UserModel authenticatedUser = clientSession.getUserSession().getUser();
+ UserModel authenticatedUser = userSession.getUser();
+ authSession.setAuthenticatedUser(authenticatedUser);
if (federatedUser != null && !authenticatedUser.getId().equals(federatedUser.getId())) {
- return redirectToAccountErrorPage(clientSession, Messages.IDENTITY_PROVIDER_ALREADY_LINKED, context.getIdpConfig().getAlias());
+ return redirectToErrorWhenLinkingFailed(authSession, Messages.IDENTITY_PROVIDER_ALREADY_LINKED, context.getIdpConfig().getAlias());
}
- if (!authenticatedUser.hasRole(this.realmModel.getClientByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(MANAGE_ACCOUNT))) {
+ if (!authenticatedUser.hasRole(this.realmModel.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(AccountRoles.MANAGE_ACCOUNT))) {
return redirectToErrorPage(Messages.INSUFFICIENT_PERMISSION);
}
if (!authenticatedUser.isEnabled()) {
- return redirectToAccountErrorPage(clientSession, Messages.ACCOUNT_DISABLED);
+ return redirectToErrorWhenLinkingFailed(authSession, Messages.ACCOUNT_DISABLED);
}
@@ -849,8 +895,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
} else {
this.session.users().addFederatedIdentity(this.realmModel, authenticatedUser, newModel);
}
- context.getIdp().attachUserSession(clientSession.getUserSession(), clientSession, context);
+ context.getIdp().authenticationFinished(authSession, context);
+ AuthenticationManager.setRolesAndMappersInSession(authSession);
+ TokenManager.attachAuthenticationSession(session, userSession, authSession);
if (isDebugEnabled()) {
logger.debugf("Linking account [%s] from identity provider [%s] to user [%s].", newModel, context.getIdpConfig().getAlias(), authenticatedUser);
@@ -863,13 +911,26 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
.success();
// we do this to make sure that the parent IDP is logged out when this user session is complete.
+ // But for the case when userSession was previously authenticated with broker1 and now is linked to another broker2, we shouldn't override broker1 notes with the broker2 for sure.
+ // Maybe broker logout should be rather always skiped in case of broker-linking
+ if (userSession.getNote(Details.IDENTITY_PROVIDER) == null) {
+ userSession.setNote(Details.IDENTITY_PROVIDER, context.getIdpConfig().getAlias());
+ userSession.setNote(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername());
+ }
+
+ return Response.status(302).location(UriBuilder.fromUri(authSession.getRedirectUri()).build()).build();
+ }
- clientSession.getUserSession().setNote(Details.IDENTITY_PROVIDER, context.getIdpConfig().getAlias());
- clientSession.getUserSession().setNote(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername());
- return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build();
+ private Response redirectToErrorWhenLinkingFailed(AuthenticationSessionModel authSession, String message, Object... parameters) {
+ if (authSession.getClient() != null && authSession.getClient().getClientId().equals(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)) {
+ return redirectToAccountErrorPage(authSession, message, parameters);
+ } else {
+ return redirectToErrorPage(message, parameters); // Should rather redirect to app instead and display error here?
+ }
}
+
private void updateFederatedIdentity(BrokeredIdentityContext context, UserModel federatedUser) {
FederatedIdentityModel federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, context.getIdpConfig().getAlias(), this.realmModel);
@@ -900,45 +961,39 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
private ParsedCodeContext parseClientSessionCode(String code) {
- ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel);
-
- if (clientCode != null) {
- ClientSessionModel clientSession = clientCode.getClientSession();
-
- if (clientSession.getUserSession() != null) {
- this.event.session(clientSession.getUserSession());
- }
-
- ClientModel client = clientSession.getClient();
-
- if (client != null) {
-
- logger.debugf("Got authorization code from client [%s].", client.getClientId());
- this.event.client(client);
- this.session.getContext().setClient(client);
-
- if (!clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
- logger.debugf("Authorization code is not valid. Client session ID: %s, Client session's action: %s", clientSession.getId(), clientSession.getAction());
-
- // Check if error happened during login or during linking from account management
- Response accountManagementFailedLinking = checkAccountManagementFailedLinking(clientCode.getClientSession(), Messages.STALE_CODE_ACCOUNT);
- Response staleCodeError = (accountManagementFailedLinking != null) ? accountManagementFailedLinking : redirectToErrorPage(Messages.STALE_CODE);
+ if (code == null) {
+ logger.debugf("Invalid request. Authorization code was null");
+ Response staleCodeError = redirectToErrorPage(Messages.INVALID_REQUEST);
+ return ParsedCodeContext.response(staleCodeError);
+ }
+ SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, clientConnection, session, event, code, null, LoginActionsService.AUTHENTICATE_PATH);
+ checks.initialVerify();
+ if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
- return ParsedCodeContext.response(staleCodeError);
- }
+ AuthenticationSessionModel authSession = checks.getAuthenticationSession();
+ if (authSession != null) {
+ // Check if error happened during login or during linking from account management
+ Response accountManagementFailedLinking = checkAccountManagementFailedLinking(authSession, Messages.STALE_CODE_ACCOUNT);
+ if (accountManagementFailedLinking != null) {
+ return ParsedCodeContext.response(accountManagementFailedLinking);
+ } else {
+ Response errorResponse = checks.getResponse();
- if (isDebugEnabled()) {
- logger.debugf("Authorization code is valid.");
+ // Remove "code" from browser history
+ errorResponse = BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, errorResponse, true);
+ return ParsedCodeContext.response(errorResponse);
}
-
- return ParsedCodeContext.clientSessionCode(clientCode);
+ } else {
+ return ParsedCodeContext.response(checks.getResponse());
+ }
+ } else {
+ if (isDebugEnabled()) {
+ logger.debugf("Authorization code is valid.");
}
- }
- logger.debugf("Authorization code is not valid. Code: %s", code);
- Response staleCodeError = redirectToErrorPage(Messages.STALE_CODE);
- return ParsedCodeContext.response(staleCodeError);
+ return ParsedCodeContext.clientSessionCode(checks.getClientCode());
+ }
}
/**
@@ -962,56 +1017,38 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return ParsedCodeContext.response(redirectToErrorPage(Messages.CLIENT_NOT_FOUND));
}
- ClientSessionModel clientSession = SamlService.createClientSessionForIdpInitiatedSso(session, realmModel, oClient.get(), null);
-
- return ParsedCodeContext.clientSessionCode(new ClientSessionCode(session, this.realmModel, clientSession));
- }
+ SamlService samlService = new SamlService(realmModel, event);
+ ResteasyProviderFactory.getInstance().injectProperties(samlService);
+ AuthenticationSessionModel authSession = samlService.getOrCreateLoginSessionForIdpInitiatedSso(session, realmModel, oClient.get(), null);
- /**
- * Returns {@code true} if the client session is defined for the given code
- * in the current session and for the current realm.
- * Does <b>not</b> check the session validity. To obtain client session if
- * and only if it exists and is valid, use {@link ClientSessionCode#parse}.
- *
- * @param code
- * @return
- */
- protected boolean isClientSessionRegistered(String code) {
- if (code == null) {
- return false;
- }
-
- try {
- return ClientSessionCode.getClientSession(code, this.session, this.realmModel) != null;
- } catch (RuntimeException e) {
- return false;
- }
+ return ParsedCodeContext.clientSessionCode(new ClientSessionCode<>(session, this.realmModel, authSession));
}
- private Response checkAccountManagementFailedLinking(ClientSessionModel clientSession, String error, Object... parameters) {
- if (clientSession.getUserSession() != null && clientSession.getClient() != null && clientSession.getClient().getClientId().equals(ACCOUNT_MANAGEMENT_CLIENT_ID)) {
+ private Response checkAccountManagementFailedLinking(AuthenticationSessionModel authSession, String error, Object... parameters) {
+ UserSessionModel userSession = new AuthenticationSessionManager(session).getUserSession(authSession);
+ if (userSession != null && authSession.getClient() != null && authSession.getClient().getClientId().equals(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)) {
this.event.event(EventType.FEDERATED_IDENTITY_LINK);
- UserModel user = clientSession.getUserSession().getUser();
+ UserModel user = userSession.getUser();
this.event.user(user);
this.event.detail(Details.USERNAME, user.getUsername());
- return redirectToAccountErrorPage(clientSession, error, parameters);
+ return redirectToAccountErrorPage(authSession, error, parameters);
} else {
return null;
}
}
- private AuthenticationRequest createAuthenticationRequest(String providerId, ClientSessionCode clientSessionCode) {
- ClientSessionModel clientSession = null;
+ private AuthenticationRequest createAuthenticationRequest(String providerId, ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
+ AuthenticationSessionModel authSession = null;
String relayState = null;
if (clientSessionCode != null) {
- clientSession = clientSessionCode.getClientSession();
+ authSession = clientSessionCode.getClientSession();
relayState = clientSessionCode.getCode();
}
- return new AuthenticationRequest(this.session, this.realmModel, clientSession, this.request, this.uriInfo, relayState, getRedirectUri(providerId));
+ return new AuthenticationRequest(this.session, this.realmModel, authSession, this.request, this.uriInfo, relayState, getRedirectUri(providerId));
}
private String getRedirectUri(String providerId) {
@@ -1028,40 +1065,36 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
}
fireErrorEvent(message, throwable);
+
+ if (throwable != null && throwable instanceof WebApplicationException) {
+ WebApplicationException webEx = (WebApplicationException) throwable;
+ return webEx.getResponse();
+ }
+
return ErrorPage.error(this.session, message, parameters);
}
- private Response redirectToAccountErrorPage(ClientSessionModel clientSession, String message, Object ... parameters) {
+ private Response redirectToAccountErrorPage(AuthenticationSessionModel authSession, String message, Object ... parameters) {
fireErrorEvent(message);
FormMessage errorMessage = new FormMessage(message, parameters);
try {
String serializedError = JsonSerialization.writeValueAsString(errorMessage);
- clientSession.setNote(AccountService.ACCOUNT_MGMT_FORWARDED_ERROR_NOTE, serializedError);
+ authSession.setAuthNote(AccountService.ACCOUNT_MGMT_FORWARDED_ERROR_NOTE, serializedError);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
- return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build();
+ return Response.status(302).location(UriBuilder.fromUri(authSession.getRedirectUri()).build()).build();
}
- private Response redirectToLoginPage(Throwable t, ClientSessionCode clientCode) {
- String message = t.getMessage();
- if (message == null) {
- message = Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR;
- }
-
- fireErrorEvent(message);
- return browserAuthentication(clientCode.getClientSession(), message);
- }
-
- protected Response browserAuthentication(ClientSessionModel clientSession, String errorMessage) {
+ protected Response browserAuthentication(AuthenticationSessionModel authSession, String errorMessage) {
this.event.event(EventType.LOGIN);
AuthenticationFlowModel flow = realmModel.getBrowserFlow();
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
- processor.setClientSession(clientSession)
+ processor.setAuthenticationSession(authSession)
.setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
.setFlowId(flowId)
.setBrowserFlow(true)
@@ -1084,12 +1117,12 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
private Response badRequest(String message) {
fireErrorEvent(message);
- return ErrorResponse.error(message, Status.BAD_REQUEST);
+ return ErrorResponse.error(message, Response.Status.BAD_REQUEST);
}
private Response forbidden(String message) {
fireErrorEvent(message);
- return ErrorResponse.error(message, Status.FORBIDDEN);
+ return ErrorResponse.error(message, Response.Status.FORBIDDEN);
}
public static IdentityProvider getIdentityProvider(KeycloakSession session, RealmModel realm, String alias) {
@@ -1177,10 +1210,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
private static class ParsedCodeContext {
- private ClientSessionCode clientSessionCode;
+ private ClientSessionCode<AuthenticationSessionModel> clientSessionCode;
private Response response;
- public static ParsedCodeContext clientSessionCode(ClientSessionCode clientSessionCode) {
+ public static ParsedCodeContext clientSessionCode(ClientSessionCode<AuthenticationSessionModel> clientSessionCode) {
ParsedCodeContext ctx = new ParsedCodeContext();
ctx.clientSessionCode = clientSessionCode;
return ctx;
@@ -1192,4 +1225,5 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return ctx;
}
}
+
}
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 14df1dc..8f0d39e 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -16,6 +16,8 @@
*/
package org.keycloak.services.resources;
+import org.keycloak.authentication.actiontoken.DefaultActionToken;
+import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
@@ -24,20 +26,24 @@ import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.TokenVerifier;
+import org.keycloak.authentication.actiontoken.*;
+import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
-import org.keycloak.authentication.requiredactions.VerifyEmail;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.ClientConnection;
+import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
-import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.exceptions.TokenNotActiveException;
import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
@@ -47,12 +53,10 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
-import org.keycloak.models.UserModel.RequiredAction;
-import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
+import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
-import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
@@ -60,10 +64,13 @@ import org.keycloak.services.ErrorPage;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.util.CacheControlUtil;
-import org.keycloak.services.util.CookieHelper;
+import org.keycloak.services.util.AuthenticationFlowURLHelper;
+import org.keycloak.services.util.BrowserHistoryHelper;
+import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
@@ -72,7 +79,6 @@ import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
@@ -81,6 +87,10 @@ import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import java.net.URI;
+import java.util.Map;
+
+import javax.ws.rs.core.*;
+import static org.keycloak.authentication.actiontoken.DefaultActionToken.ACTION_TOKEN_BASIC_CHECKS;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -89,14 +99,16 @@ public class LoginActionsService {
private static final Logger logger = Logger.getLogger(LoginActionsService.class);
- public static final String ACTION_COOKIE = "KEYCLOAK_ACTION";
public static final String AUTHENTICATE_PATH = "authenticate";
public static final String REGISTRATION_PATH = "registration";
public static final String RESET_CREDENTIALS_PATH = "reset-credentials";
public static final String REQUIRED_ACTION = "required-action";
public static final String FIRST_BROKER_LOGIN_PATH = "first-broker-login";
public static final String POST_BROKER_LOGIN_PATH = "post-broker-login";
- public static final String LAST_PROCESSED_CODE = "last_processed_code";
+
+ public static final String RESTART_PATH = "restart";
+
+ public static final String FORWARDED_ERROR_MESSAGE_NOTE = "forwardedErrorMessage";
private RealmModel realm;
@@ -133,6 +145,10 @@ public class LoginActionsService {
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "requiredActionPOST");
}
+ public static UriBuilder actionTokenProcessor(UriInfo uriInfo) {
+ return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "executeActionToken");
+ }
+
public static UriBuilder registrationFormProcessor(UriInfo uriInfo) {
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "processRegister");
}
@@ -163,153 +179,45 @@ public class LoginActionsService {
}
}
+ private SessionCodeChecks checksForCode(String code, String execution, String flowPath) {
+ SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, code, execution, flowPath);
+ res.initialVerify();
+ return res;
+ }
- private class Checks {
- ClientSessionCode clientCode;
- Response response;
- ClientSessionCode.ParseResult result;
- boolean verifyCode(String code, String requiredAction, ClientSessionCode.ActionType actionType) {
- if (!verifyCode(code)) {
- return false;
- }
- if (!clientCode.isValidAction(requiredAction)) {
- ClientSessionModel clientSession = clientCode.getClientSession();
- if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(clientSession.getAction())) {
- response = redirectToRequiredActions(code);
- return false;
- } else if (clientSession.getUserSession() != null && clientSession.getUserSession().getState() == UserSessionModel.State.LOGGED_IN) {
- response = session.getProvider(LoginFormsProvider.class)
- .setSuccess(Messages.ALREADY_LOGGED_IN)
- .createInfoPage();
- return false;
- }
- }
- if (!isActionActive(actionType)) return false;
- return true;
- }
+ protected URI getLastExecutionUrl(String flowPath, String executionId) {
+ return new AuthenticationFlowURLHelper(session, realm, uriInfo)
+ .getLastExecutionUrl(flowPath, executionId);
+ }
- private boolean isValidAction(String requiredAction) {
- if (!clientCode.isValidAction(requiredAction)) {
- invalidAction();
- return false;
- }
- return true;
- }
- private void invalidAction() {
- event.client(clientCode.getClientSession().getClient());
- event.error(Errors.INVALID_CODE);
- response = ErrorPage.error(session, Messages.INVALID_CODE);
- }
+ /**
+ * protocol independent page for restart of the flow
+ *
+ * @return
+ */
+ @Path(RESTART_PATH)
+ @GET
+ public Response restartSession() {
+ event.event(EventType.RESTART_AUTHENTICATION);
+ SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, null, null, null);
- private boolean isActionActive(ClientSessionCode.ActionType actionType) {
- if (!clientCode.isActionActive(actionType)) {
- event.client(clientCode.getClientSession().getClient());
- event.clone().error(Errors.EXPIRED_CODE);
- if (clientCode.getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
- AuthenticationProcessor.resetFlow(clientCode.getClientSession());
- response = processAuthentication(null, clientCode.getClientSession(), Messages.LOGIN_TIMEOUT);
- return false;
- }
- response = ErrorPage.error(session, Messages.EXPIRED_CODE);
- return false;
- }
- return true;
+ AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
+ if (authSession == null) {
+ return checks.getResponse();
}
- public boolean verifyCode(String code) {
- if (!checkSsl()) {
- event.error(Errors.SSL_REQUIRED);
- response = ErrorPage.error(session, Messages.HTTPS_REQUIRED);
- return false;
- }
- if (!realm.isEnabled()) {
- event.error(Errors.REALM_DISABLED);
- response = ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
- return false;
- }
- result = ClientSessionCode.parseResult(code, session, realm);
- clientCode = result.getCode();
- if (clientCode == null) {
- if (result.isClientSessionNotFound()) { // timeout
- try {
- ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code);
- if (clientSession != null) {
- event.clone().detail(Details.RESTART_AFTER_TIMEOUT, "true").error(Errors.EXPIRED_CODE);
- response = processFlow(null, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT, new AuthenticationProcessor());
- return false;
- }
- } catch (Exception e) {
- ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
- }
- }
- event.error(Errors.INVALID_CODE);
- response = ErrorPage.error(session, Messages.INVALID_CODE);
- return false;
- }
- ClientSessionModel clientSession = clientCode.getClientSession();
- if (clientSession == null) {
- event.error(Errors.INVALID_CODE);
- response = ErrorPage.error(session, Messages.INVALID_CODE);
- return false;
- }
- event.detail(Details.CODE_ID, clientSession.getId());
- ClientModel client = clientSession.getClient();
- if (client == null) {
- event.error(Errors.CLIENT_NOT_FOUND);
- response = ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER);
- session.sessions().removeClientSession(realm, clientSession);
- return false;
- }
- if (!client.isEnabled()) {
- event.error(Errors.CLIENT_NOT_FOUND);
- response = ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED);
- session.sessions().removeClientSession(realm, clientSession);
- return false;
- }
- session.getContext().setClient(client);
- return true;
+ String flowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW);
+ if (flowPath == null) {
+ flowPath = AUTHENTICATE_PATH;
}
- public boolean verifyRequiredAction(String code, String executedAction) {
- if (!verifyCode(code)) {
- return false;
- }
- if (!isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) return false;
- if (!isActionActive(ClientSessionCode.ActionType.USER)) return false;
-
- final ClientSessionModel clientSession = clientCode.getClientSession();
+ AuthenticationProcessor.resetFlow(authSession, flowPath);
- final UserSessionModel userSession = clientSession.getUserSession();
- if (userSession == null) {
- ServicesLogger.LOGGER.userSessionNull();
- event.error(Errors.USER_SESSION_NOT_FOUND);
- throw new WebApplicationException(ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE));
- }
- if (!AuthenticationManager.isSessionValid(realm, userSession)) {
- AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
- event.error(Errors.INVALID_CODE);
- response = ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
- return false;
- }
-
- if (executedAction == null && userSession != null) { // do next required action only if user is already authenticated
- initEvent(clientSession);
- event.event(EventType.LOGIN);
- response = AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
- return false;
- }
-
- if (!executedAction.equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
- logger.debug("required action doesn't match current required action");
- clientSession.removeNote(AuthenticationManager.CURRENT_REQUIRED_ACTION);
- response = redirectToRequiredActions(code);
- return false;
- }
- return true;
-
- }
+ URI redirectUri = getLastExecutionUrl(flowPath, null);
+ logger.debugf("Flow restart requested. Redirecting to %s", redirectUri);
+ return Response.status(Response.Status.FOUND).location(redirectUri).build();
}
@@ -325,30 +233,23 @@ public class LoginActionsService {
@QueryParam("execution") String execution) {
event.event(EventType.LOGIN);
- ClientSessionModel clientSession = ClientSessionCode.getClientSession(code, session, realm);
- if (clientSession != null && code.equals(clientSession.getNote(LAST_PROCESSED_CODE))) {
- // Allow refresh of previous page
- } else {
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
- return checks.response;
- }
-
- ClientSessionCode clientSessionCode = checks.clientCode;
- clientSession = clientSessionCode.getClientSession();
+ SessionCodeChecks checks = checksForCode(code, execution, AUTHENTICATE_PATH);
+ if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
+ return checks.getResponse();
}
- event.detail(Details.CODE_ID, code);
- clientSession.setNote(LAST_PROCESSED_CODE, code);
- return processAuthentication(execution, clientSession, null);
+ AuthenticationSessionModel authSession = checks.getAuthenticationSession();
+ boolean actionRequest = checks.isActionRequest();
+
+ return processAuthentication(actionRequest, execution, authSession, null);
}
- protected Response processAuthentication(String execution, ClientSessionModel clientSession, String errorMessage) {
- return processFlow(execution, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage, new AuthenticationProcessor());
+ protected Response processAuthentication(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) {
+ return processFlow(action, execution, authSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage, new AuthenticationProcessor());
}
- protected Response processFlow(String execution, ClientSessionModel clientSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
- processor.setClientSession(clientSession)
+ protected Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
+ processor.setAuthenticationSession(authSession)
.setFlowPath(flowPath)
.setBrowserFlow(true)
.setFlowId(flow.getId())
@@ -358,17 +259,34 @@ public class LoginActionsService {
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
- if (errorMessage != null) processor.setForwardedErrorMessage(new FormMessage(null, errorMessage));
+ if (errorMessage != null) {
+ processor.setForwardedErrorMessage(new FormMessage(null, errorMessage));
+ }
+
+ // Check the forwarded error message, which was set by previous HTTP request
+ String forwardedErrorMessage = authSession.getAuthNote(FORWARDED_ERROR_MESSAGE_NOTE);
+ if (forwardedErrorMessage != null) {
+ authSession.removeAuthNote(FORWARDED_ERROR_MESSAGE_NOTE);
+ processor.setForwardedErrorMessage(new FormMessage(null, forwardedErrorMessage));
+ }
+
+ Response response;
try {
- if (execution != null) {
- return processor.authenticationAction(execution);
+ if (action) {
+ response = processor.authenticationAction(execution);
} else {
- return processor.authenticate();
+ response = processor.authenticate();
}
+ } catch (WebApplicationException e) {
+ response = e.getResponse();
+ authSession = processor.getAuthenticationSession();
} catch (Exception e) {
- return processor.handleBrowserException(e);
+ response = processor.handleBrowserException(e);
+ authSession = processor.getAuthenticationSession(); // Could be changed (eg. Forked flow)
}
+
+ return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, action);
}
/**
@@ -381,35 +299,25 @@ public class LoginActionsService {
@POST
public Response authenticateForm(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
- event.event(EventType.LOGIN);
-
- ClientSessionModel clientSession = ClientSessionCode.getClientSession(code, session, realm);
- if (clientSession != null && code.equals(clientSession.getNote(LAST_PROCESSED_CODE))) {
- // Post already processed (refresh) - ignore form post and return next form
- request.getFormParameters().clear();
- return authenticate(code, null);
- }
-
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
- return checks.response;
- }
- final ClientSessionCode clientCode = checks.clientCode;
- clientSession = clientCode.getClientSession();
- clientSession.setNote(LAST_PROCESSED_CODE, code);
-
- return processAuthentication(execution, clientSession, null);
+ return authenticate(code, execution);
}
@Path(RESET_CREDENTIALS_PATH)
@POST
public Response resetCredentialsPOST(@QueryParam("code") String code,
- @QueryParam("execution") String execution) {
+ @QueryParam("execution") String execution,
+ @QueryParam(Constants.KEY) String key) {
+ if (key != null) {
+ return handleActionToken(key, execution);
+ }
+
+ event.event(EventType.RESET_PASSWORD);
+
return resetCredentials(code, execution);
}
/**
- * Endpoint for executing reset credentials flow. If code is null, a client session is created with the account
+ * Endpoint for executing reset credentials flow. If token is null, a client session is created with the account
* service as the client. Successful reset sends you to the account page. Note, account service must be enabled.
*
* @param code
@@ -419,80 +327,237 @@ public class LoginActionsService {
@Path(RESET_CREDENTIALS_PATH)
@GET
public Response resetCredentialsGET(@QueryParam("code") String code,
- @QueryParam("execution") String execution) {
+ @QueryParam("execution") String execution) {
+ AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
+
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
- //
- if (code == null) {
+ if (authSession == null && code == null) {
if (!realm.isResetPasswordAllowed()) {
event.event(EventType.RESET_PASSWORD);
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
}
- // set up the account service as the endpoint to call.
- ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
- ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
- clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
- //clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
- clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
- String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
- clientSession.setRedirectUri(redirectUri);
- clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
- clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
- clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
- clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
- return processResetCredentials(null, clientSession, null);
+ authSession = createAuthenticationSessionForClient();
+ return processResetCredentials(false, null, authSession);
}
+
+ event.event(EventType.RESET_PASSWORD);
return resetCredentials(code, execution);
}
+ AuthenticationSessionModel createAuthenticationSessionForClient()
+ throws UriBuilderException, IllegalArgumentException {
+ AuthenticationSessionModel authSession;
+
+ // set up the account service as the endpoint to call.
+ ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
+ authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
+ authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
+ //authSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
+ authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
+ authSession.setRedirectUri(redirectUri);
+ authSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
+ authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
+ authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+
+ return authSession;
+ }
+
+ /**
+ * @param code
+ * @param execution
+ * @return
+ */
protected Response resetCredentials(String code, String execution) {
- event.event(EventType.RESET_PASSWORD);
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
- return checks.response;
+ SessionCodeChecks checks = checksForCode(code, execution, RESET_CREDENTIALS_PATH);
+ if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
+ return checks.getResponse();
}
- final ClientSessionCode clientCode = checks.clientCode;
- final ClientSessionModel clientSession = clientCode.getClientSession();
+ final AuthenticationSessionModel authSession = checks.getAuthenticationSession();
if (!realm.isResetPasswordAllowed()) {
- event.client(clientCode.getClientSession().getClient());
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
}
- return processResetCredentials(execution, clientSession, null);
+ return processResetCredentials(checks.isActionRequest(), execution, authSession);
}
- protected Response processResetCredentials(String execution, ClientSessionModel clientSession, String errorMessage) {
- AuthenticationProcessor authProcessor = new AuthenticationProcessor() {
+ /**
+ * Handles a given token using the given token handler. If there is any {@link VerificationException} thrown
+ * in the handler, it is handled automatically here to reduce boilerplate code.
+ *
+ * @param key
+ * @param execution
+ * @return
+ */
+ @Path("action-token")
+ @GET
+ public Response executeActionToken(@QueryParam("key") String key,
+ @QueryParam("execution") String execution) {
+ return handleActionToken(key, execution);
+ }
- @Override
- protected Response authenticationComplete() {
- boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null);
- if (firstBrokerLoginInProgress) {
+ protected <T extends DefaultActionToken> Response handleActionToken(String tokenString, String execution) {
+ T token;
+ ActionTokenHandler<T> handler;
+ ActionTokenContext<T> tokenContext;
+ String eventError = null;
+ String defaultErrorMessage = null;
+ AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
+
+ event.event(EventType.EXECUTE_ACTION_TOKEN);
+
+ // First resolve action token handler
+ try {
+ if (tokenString == null) {
+ throw new ExplainedTokenVerificationException(null, Errors.NOT_ALLOWED, Messages.INVALID_REQUEST);
+ }
- UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realm, clientSession);
- if (!linkingUser.getId().equals(clientSession.getAuthenticatedUser().getId())) {
- return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, clientSession.getAuthenticatedUser().getUsername(), linkingUser.getUsername());
- }
+ TokenVerifier<DefaultActionToken> tokenVerifier = TokenVerifier.create(tokenString, DefaultActionToken.class);
+ DefaultActionToken aToken = tokenVerifier.getToken();
- logger.debugf("Forget-password flow finished when authenticated user '%s' after first broker login.", linkingUser.getUsername());
+ event
+ .detail(Details.TOKEN_ID, aToken.getId())
+ .detail(Details.ACTION, aToken.getActionId())
+ .user(aToken.getUserId());
- return redirectToAfterBrokerLoginEndpoint(clientSession, true);
- } else {
- return super.authenticationComplete();
+ handler = resolveActionTokenHandler(aToken.getActionId());
+ eventError = handler.getDefaultEventError();
+ defaultErrorMessage = handler.getDefaultErrorMessage();
+
+ if (! realm.isEnabled()) {
+ throw new ExplainedTokenVerificationException(aToken, Errors.REALM_DISABLED, Messages.REALM_NOT_ENABLED);
+ }
+ if (! checkSsl()) {
+ throw new ExplainedTokenVerificationException(aToken, Errors.SSL_REQUIRED, Messages.HTTPS_REQUIRED);
+ }
+
+ tokenVerifier
+ .withChecks(
+ // Token introspection checks
+ TokenVerifier.IS_ACTIVE,
+ new TokenVerifier.RealmUrlCheck(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())),
+ ACTION_TOKEN_BASIC_CHECKS
+ )
+
+ .secretKey(session.keys().getActiveHmacKey(realm).getSecretKey())
+ .verify();
+
+ token = TokenVerifier.create(tokenString, handler.getTokenClass()).getToken();
+ } catch (TokenNotActiveException ex) {
+ if (authSession != null) {
+ event.clone().error(Errors.EXPIRED_CODE);
+ String flowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW);
+ if (flowPath == null) {
+ flowPath = AUTHENTICATE_PATH;
}
+ AuthenticationProcessor.resetFlow(authSession, flowPath);
+ return processAuthentication(false, null, authSession, Messages.LOGIN_TIMEOUT);
}
- };
- return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage, authProcessor);
+ return handleActionTokenVerificationException(null, ex, Errors.EXPIRED_CODE, defaultErrorMessage);
+ } catch (ExplainedTokenVerificationException ex) {
+ return handleActionTokenVerificationException(null, ex, ex.getErrorEvent(), ex.getMessage());
+ } catch (VerificationException ex) {
+ return handleActionTokenVerificationException(null, ex, eventError, defaultErrorMessage);
+ }
+
+ // Now proceed with the verification and handle the token
+ tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow);
+
+ try {
+ String tokenAuthSessionId = handler.getAuthenticationSessionIdFromToken(token);
+
+ if (tokenAuthSessionId != null) {
+ // This can happen if the token contains ID but user opens the link in a new browser
+ LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId);
+ }
+
+ if (authSession == null) {
+ authSession = handler.startFreshAuthenticationSession(token, tokenContext);
+ tokenContext.setAuthenticationSession(authSession, true);
+ } else if (tokenAuthSessionId == null ||
+ ! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId)) {
+ // There exists an authentication session but no auth session ID was received in the action token
+ logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
+ new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
+
+ authSession = handler.startFreshAuthenticationSession(token, tokenContext);
+ tokenContext.setAuthenticationSession(authSession, true);
+ }
+
+ initLoginEvent(authSession);
+ event.event(handler.eventType());
+
+ LoginActionsServiceChecks.checkIsUserValid(token, tokenContext);
+ LoginActionsServiceChecks.checkIsClientValid(token, tokenContext);
+
+ session.getContext().setClient(authSession.getClient());
+
+ TokenVerifier.create(token)
+ .withChecks(handler.getVerifiers(tokenContext))
+ .verify();
+
+ authSession = tokenContext.getAuthenticationSession();
+ event = tokenContext.getEvent();
+ event.event(handler.eventType());
+
+ if (! handler.canUseTokenRepeatedly(token, tokenContext)) {
+ LoginActionsServiceChecks.checkTokenWasNotUsedYet(token, tokenContext);
+ authSession.setAuthNote(AuthenticationManager.INVALIDATE_ACTION_TOKEN, token.serializeKey());
+ }
+
+ authSession.setAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID, token.getUserId());
+
+ return handler.handleToken(token, tokenContext);
+ } catch (ExplainedTokenVerificationException ex) {
+ return handleActionTokenVerificationException(tokenContext, ex, ex.getErrorEvent(), ex.getMessage());
+ } catch (LoginActionsServiceException ex) {
+ Response response = ex.getResponse();
+ return response == null
+ ? handleActionTokenVerificationException(tokenContext, ex, eventError, defaultErrorMessage)
+ : response;
+ } catch (VerificationException ex) {
+ return handleActionTokenVerificationException(tokenContext, ex, eventError, defaultErrorMessage);
+ }
+ }
+
+ private <T extends DefaultActionToken> ActionTokenHandler<T> resolveActionTokenHandler(String actionId) throws VerificationException {
+ if (actionId == null) {
+ throw new VerificationException("Action token operation not set");
+ }
+ ActionTokenHandler<T> handler = session.getProvider(ActionTokenHandler.class, actionId);
+
+ if (handler == null) {
+ throw new VerificationException("Invalid action token operation");
+ }
+ return handler;
+ }
+
+ private Response handleActionTokenVerificationException(ActionTokenContext<?> tokenContext, VerificationException ex, String eventError, String errorMessage) {
+ if (tokenContext != null && tokenContext.getAuthenticationSession() != null) {
+ new AuthenticationSessionManager(session).removeAuthenticationSession(realm, tokenContext.getAuthenticationSession(), true);
+ }
+
+ event
+ .detail(Details.REASON, ex == null ? "<unknown>" : ex.getMessage())
+ .error(eventError == null ? Errors.INVALID_CODE : eventError);
+ return ErrorPage.error(session, errorMessage == null ? Messages.INVALID_CODE : errorMessage);
+ }
+
+ protected Response processResetCredentials(boolean actionRequest, String execution, AuthenticationSessionModel authSession) {
+ AuthenticationProcessor authProcessor = new ResetCredentialsActionTokenHandler.ResetCredsAuthenticationProcessor();
+
+ return processFlow(actionRequest, execution, authSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), null, authProcessor);
}
- protected Response processRegistration(String execution, ClientSessionModel clientSession, String errorMessage) {
- return processFlow(execution, clientSession, REGISTRATION_PATH, realm.getRegistrationFlow(), errorMessage, new AuthenticationProcessor());
+ protected Response processRegistration(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) {
+ return processFlow(action, execution, authSession, REGISTRATION_PATH, realm.getRegistrationFlow(), errorMessage, new AuthenticationProcessor());
}
@@ -506,24 +571,7 @@ public class LoginActionsService {
@GET
public Response registerPage(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
- event.event(EventType.REGISTER);
- if (!realm.isRegistrationAllowed()) {
- event.error(Errors.REGISTRATION_DISABLED);
- return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
- }
-
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
- return checks.response;
- }
- event.detail(Details.CODE_ID, code);
- ClientSessionCode clientSessionCode = checks.clientCode;
- ClientSessionModel clientSession = clientSessionCode.getClientSession();
-
-
- AuthenticationManager.expireIdentityCookie(realm, uriInfo, clientConnection);
-
- return processRegistration(execution, clientSession, null);
+ return registerRequest(code, execution, false);
}
@@ -537,20 +585,27 @@ public class LoginActionsService {
@POST
public Response processRegister(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
+ return registerRequest(code, execution, true);
+ }
+
+
+ private Response registerRequest(String code, String execution, boolean isPostRequest) {
event.event(EventType.REGISTER);
if (!realm.isRegistrationAllowed()) {
event.error(Errors.REGISTRATION_DISABLED);
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
}
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
- return checks.response;
+
+ SessionCodeChecks checks = checksForCode(code, execution, REGISTRATION_PATH);
+ if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
+ return checks.getResponse();
}
- ClientSessionCode clientCode = checks.clientCode;
- ClientSessionModel clientSession = clientCode.getClientSession();
+ AuthenticationSessionModel authSession = checks.getAuthenticationSession();
+
+ AuthenticationManager.expireIdentityCookie(realm, uriInfo, clientConnection);
- return processRegistration(execution, clientSession, null);
+ return processRegistration(checks.isActionRequest(), execution, authSession, null);
}
@@ -558,50 +613,51 @@ public class LoginActionsService {
@GET
public Response firstBrokerLoginGet(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
- return brokerLoginFlow(code, execution, true);
+ return brokerLoginFlow(code, execution, FIRST_BROKER_LOGIN_PATH);
}
@Path(FIRST_BROKER_LOGIN_PATH)
@POST
public Response firstBrokerLoginPost(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
- return brokerLoginFlow(code, execution, true);
+ return brokerLoginFlow(code, execution, FIRST_BROKER_LOGIN_PATH);
}
@Path(POST_BROKER_LOGIN_PATH)
@GET
public Response postBrokerLoginGet(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
- return brokerLoginFlow(code, execution, false);
+ return brokerLoginFlow(code, execution, POST_BROKER_LOGIN_PATH);
}
@Path(POST_BROKER_LOGIN_PATH)
@POST
public Response postBrokerLoginPost(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
- return brokerLoginFlow(code, execution, false);
+ return brokerLoginFlow(code, execution, POST_BROKER_LOGIN_PATH);
}
- protected Response brokerLoginFlow(String code, String execution, final boolean firstBrokerLogin) {
+ protected Response brokerLoginFlow(String code, String execution, String flowPath) {
+ boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH);
+
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
event.event(eventType);
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
- return checks.response;
+ SessionCodeChecks checks = checksForCode(code, execution, flowPath);
+ if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
+ return checks.getResponse();
}
event.detail(Details.CODE_ID, code);
- ClientSessionCode clientSessionCode = checks.clientCode;
- final ClientSessionModel clientSessionn = clientSessionCode.getClientSession();
+ final AuthenticationSessionModel authSession = checks.getAuthenticationSession();
String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT;
- SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSessionn, noteKey);
+ SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, noteKey);
if (serializedCtx == null) {
ServicesLogger.LOGGER.notFoundSerializedCtxInClientSession(noteKey);
throw new WebApplicationException(ErrorPage.error(session, "Not found serialized context in clientSession."));
}
- BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, clientSessionn);
+ BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, authSession);
final String identityProviderAlias = brokerContext.getIdpConfig().getAlias();
String flowId = firstBrokerLogin ? brokerContext.getIdpConfig().getFirstBrokerLoginFlowId() : brokerContext.getIdpConfig().getPostBrokerLoginFlowId();
@@ -623,23 +679,28 @@ public class LoginActionsService {
@Override
protected Response authenticationComplete() {
- if (!firstBrokerLogin) {
+ if (firstBrokerLogin) {
+ authSession.setAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS, identityProviderAlias);
+ } else {
String authStateNoteKey = PostBrokerLoginConstants.PBL_AUTH_STATE_PREFIX + identityProviderAlias;
- clientSessionn.setNote(authStateNoteKey, "true");
+ authSession.setAuthNote(authStateNoteKey, "true");
}
- return redirectToAfterBrokerLoginEndpoint(clientSession, firstBrokerLogin);
+ return redirectToAfterBrokerLoginEndpoint(authSession, firstBrokerLogin);
}
};
- String flowPath = firstBrokerLogin ? FIRST_BROKER_LOGIN_PATH : POST_BROKER_LOGIN_PATH;
- return processFlow(execution, clientSessionn, flowPath, brokerLoginFlow, null, processor);
+ return processFlow(checks.isActionRequest(), execution, authSession, flowPath, brokerLoginFlow, null, processor);
+ }
+
+ private Response redirectToAfterBrokerLoginEndpoint(AuthenticationSessionModel authSession, boolean firstBrokerLogin) {
+ return redirectToAfterBrokerLoginEndpoint(session, realm, uriInfo, authSession, firstBrokerLogin);
}
- private Response redirectToAfterBrokerLoginEndpoint(ClientSessionModel clientSession, boolean firstBrokerLogin) {
- ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession);
- clientSession.setTimestamp(Time.currentTime());
+ public static Response redirectToAfterBrokerLoginEndpoint(KeycloakSession session, RealmModel realm, UriInfo uriInfo, AuthenticationSessionModel authSession, boolean firstBrokerLogin) {
+ ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession);
+ authSession.setTimestamp(Time.currentTime());
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) :
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) ;
@@ -648,6 +709,7 @@ public class LoginActionsService {
return Response.status(302).location(redirect).build();
}
+
/**
* OAuth grant page. You should not invoked this directly!
*
@@ -660,27 +722,26 @@ public class LoginActionsService {
public Response processConsent(final MultivaluedMap<String, String> formData) {
event.event(EventType.LOGIN);
String code = formData.getFirst("code");
- Checks checks = new Checks();
- if (!checks.verifyRequiredAction(code, ClientSessionModel.Action.OAUTH_GRANT.name())) {
- return checks.response;
+ SessionCodeChecks checks = checksForCode(code, null, REQUIRED_ACTION);
+ if (!checks.verifyRequiredAction(ClientSessionModel.Action.OAUTH_GRANT.name())) {
+ return checks.getResponse();
}
- ClientSessionCode accessCode = checks.clientCode;
- ClientSessionModel clientSession = accessCode.getClientSession();
- initEvent(clientSession);
+ AuthenticationSessionModel authSession = checks.getAuthenticationSession();
- UserSessionModel userSession = clientSession.getUserSession();
- UserModel user = userSession.getUser();
- ClientModel client = clientSession.getClient();
+ initLoginEvent(authSession);
+
+ UserModel user = authSession.getAuthenticatedUser();
+ ClientModel client = authSession.getClient();
if (formData.containsKey("cancel")) {
- LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
+ LoginProtocol protocol = session.getProvider(LoginProtocol.class, authSession.getProtocol());
protocol.setRealm(realm)
.setHttpHeaders(headers)
.setUriInfo(uriInfo)
.setEventBuilder(event);
- Response response = protocol.sendError(clientSession, Error.CONSENT_DENIED);
+ Response response = protocol.sendError(authSession, Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
return response;
}
@@ -690,10 +751,10 @@ public class LoginActionsService {
grantedConsent = new UserConsentModel(client);
session.users().addConsent(realm, user.getId(), grantedConsent);
}
- for (RoleModel role : accessCode.getRequestedRoles()) {
+ for (RoleModel role : ClientSessionCode.getRequestedRoles(authSession, realm)) {
grantedConsent.addGrantedRole(role);
}
- for (ProtocolMapperModel protocolMapper : accessCode.getRequestedProtocolMappers()) {
+ for (ProtocolMapperModel protocolMapper : ClientSessionCode.getRequestedProtocolMappers(authSession.getProtocolMappers(), client)) {
if (protocolMapper.isConsentRequired() && protocolMapper.getConsentText() != null) {
grantedConsent.addGrantedProtocolMapper(protocolMapper);
}
@@ -703,186 +764,82 @@ public class LoginActionsService {
event.detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED);
event.success();
- return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection, event);
+ AuthenticatedClientSessionModel clientSession = AuthenticationProcessor.attachSession(authSession, null, session, realm, clientConnection, event);
+ return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, clientSession.getUserSession(), clientSession, request, uriInfo, clientConnection, event, authSession.getProtocol());
}
- @Path("email-verification")
- @GET
- public Response emailVerification(@QueryParam("code") String code, @QueryParam("key") String key) {
- event.event(EventType.VERIFY_EMAIL);
- if (key != null) {
- ClientSessionModel clientSession = null;
- String keyFromSession = null;
- if (code != null) {
- clientSession = ClientSessionCode.getClientSession(code, session, realm);
- keyFromSession = clientSession != null ? clientSession.getNote(Constants.VERIFY_EMAIL_KEY) : null;
- }
-
- if (!key.equals(keyFromSession)) {
- ServicesLogger.LOGGER.invalidKeyForEmailVerification();
- event.error(Errors.INVALID_CODE);
- throw new WebApplicationException(ErrorPage.error(session, Messages.STALE_VERIFY_EMAIL_LINK));
- }
-
- clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
-
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
- if (checks.clientCode == null && checks.result.isClientSessionNotFound() || checks.result.isIllegalHash()) {
- return ErrorPage.error(session, Messages.STALE_VERIFY_EMAIL_LINK);
- }
- return checks.response;
- }
-
- ClientSessionCode accessCode = checks.clientCode;
- clientSession = accessCode.getClientSession();
- if (!ClientSessionModel.Action.VERIFY_EMAIL.name().equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
- ServicesLogger.LOGGER.reqdActionDoesNotMatch();
- event.error(Errors.INVALID_CODE);
- throw new WebApplicationException(ErrorPage.error(session, Messages.STALE_VERIFY_EMAIL_LINK));
- }
-
- UserSessionModel userSession = clientSession.getUserSession();
- UserModel user = userSession.getUser();
- initEvent(clientSession);
- event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail());
-
- user.setEmailVerified(true);
-
- user.removeRequiredAction(RequiredAction.VERIFY_EMAIL);
-
- event.success();
-
- String actionCookieValue = getActionCookie();
- if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) {
- session.sessions().removeClientSession(realm, clientSession);
- return session.getProvider(LoginFormsProvider.class)
- .setSuccess(Messages.EMAIL_VERIFIED)
- .createInfoPage();
- }
-
- event = event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN);
-
- return AuthenticationProcessor.redirectToRequiredActions(session, realm, clientSession, uriInfo);
- } else {
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
- return checks.response;
- }
- ClientSessionCode accessCode = checks.clientCode;
- ClientSessionModel clientSession = accessCode.getClientSession();
- UserSessionModel userSession = clientSession.getUserSession();
- initEvent(clientSession);
-
- createActionCookie(realm, uriInfo, clientConnection, userSession.getId());
+ private void initLoginEvent(AuthenticationSessionModel authSession) {
+ String responseType = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ if (responseType == null) {
+ responseType = "code";
+ }
+ String respMode = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ OIDCResponseMode responseMode = OIDCResponseMode.parse(respMode, OIDCResponseType.parse(responseType));
- VerifyEmail.setupKey(clientSession);
+ event.event(EventType.LOGIN).client(authSession.getClient())
+ .detail(Details.CODE_ID, authSession.getId())
+ .detail(Details.REDIRECT_URI, authSession.getRedirectUri())
+ .detail(Details.AUTH_METHOD, authSession.getProtocol())
+ .detail(Details.RESPONSE_TYPE, responseType)
+ .detail(Details.RESPONSE_MODE, responseMode.toString().toLowerCase());
- return session.getProvider(LoginFormsProvider.class)
- .setClientSessionCode(accessCode.getCode())
- .setClientSession(clientSession)
- .setUser(userSession.getUser())
- .createResponse(RequiredAction.VERIFY_EMAIL);
+ UserModel authenticatedUser = authSession.getAuthenticatedUser();
+ if (authenticatedUser != null) {
+ event.user(authenticatedUser)
+ .detail(Details.USERNAME, authenticatedUser.getUsername());
}
- }
- /**
- * Initiated by admin, not the user on login
- *
- * @param key
- * @return
- */
- @Path("execute-actions")
- @GET
- public Response executeActions(@QueryParam("key") String key) {
- event.event(EventType.EXECUTE_ACTIONS);
- if (key != null) {
- Checks checks = new Checks();
- if (!checks.verifyCode(key, ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
- return checks.response;
- }
- ClientSessionModel clientSession = checks.clientCode.getClientSession();
- // verify user email as we know it is valid as this entry point would never have gotten here.
- clientSession.getUserSession().getUser().setEmailVerified(true);
- clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
- clientSession.setNote(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), "true");
- return AuthenticationProcessor.redirectToRequiredActions(session, realm, clientSession, uriInfo);
- } else {
- event.error(Errors.INVALID_CODE);
- return ErrorPage.error(session, Messages.INVALID_CODE);
+ String attemptedUsername = authSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
+ if (attemptedUsername != null) {
+ event.detail(Details.USERNAME, attemptedUsername);
}
- }
-
- private String getActionCookie() {
- return getActionCookie(headers, realm, uriInfo, clientConnection);
- }
-
- public static String getActionCookie(HttpHeaders headers, RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection) {
- Cookie cookie = headers.getCookies().get(ACTION_COOKIE);
- AuthenticationManager.expireCookie(realm, ACTION_COOKIE, AuthenticationManager.getRealmCookiePath(realm, uriInfo), realm.getSslRequired().isRequired(clientConnection), clientConnection);
- return cookie != null ? cookie.getValue() : null;
- }
- public static void createActionCookie(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, String sessionId) {
- CookieHelper.addCookie(ACTION_COOKIE, sessionId, AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, null, -1, realm.getSslRequired().isRequired(clientConnection), true);
- }
-
- private void initEvent(ClientSessionModel clientSession) {
- UserSessionModel userSession = clientSession.getUserSession();
-
- String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
- if (responseType == null) {
- responseType = "code";
+ String rememberMe = authSession.getAuthNote(Details.REMEMBER_ME);
+ if (rememberMe==null || !rememberMe.equalsIgnoreCase("true")) {
+ rememberMe = "false";
}
- String respMode = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
- OIDCResponseMode responseMode = OIDCResponseMode.parse(respMode, OIDCResponseType.parse(responseType));
-
- event.event(EventType.LOGIN).client(clientSession.getClient())
- .user(userSession.getUser())
- .session(userSession.getId())
- .detail(Details.CODE_ID, clientSession.getId())
- .detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
- .detail(Details.USERNAME, clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME))
- .detail(Details.AUTH_METHOD, userSession.getAuthMethod())
- .detail(Details.USERNAME, userSession.getLoginUsername())
- .detail(Details.RESPONSE_TYPE, responseType)
- .detail(Details.RESPONSE_MODE, responseMode.toString().toLowerCase())
- .detail(Details.IDENTITY_PROVIDER, userSession.getNote(Details.IDENTITY_PROVIDER))
- .detail(Details.IDENTITY_PROVIDER_USERNAME, userSession.getNote(Details.IDENTITY_PROVIDER_USERNAME));
+ event.detail(Details.REMEMBER_ME, rememberMe);
- if (userSession.isRememberMe()) {
- event.detail(Details.REMEMBER_ME, "true");
+ Map<String, String> userSessionNotes = authSession.getUserSessionNotes();
+ String identityProvider = userSessionNotes.get(Details.IDENTITY_PROVIDER);
+ if (identityProvider != null) {
+ event.detail(Details.IDENTITY_PROVIDER, identityProvider)
+ .detail(Details.IDENTITY_PROVIDER_USERNAME, userSessionNotes.get(Details.IDENTITY_PROVIDER_USERNAME));
}
}
@Path(REQUIRED_ACTION)
@POST
public Response requiredActionPOST(@QueryParam("code") final String code,
- @QueryParam("action") String action) {
+ @QueryParam("execution") String action) {
return processRequireAction(code, action);
-
-
-
}
@Path(REQUIRED_ACTION)
@GET
public Response requiredActionGET(@QueryParam("code") final String code,
- @QueryParam("action") String action) {
+ @QueryParam("execution") String action) {
return processRequireAction(code, action);
}
- public Response processRequireAction(final String code, String action) {
+ private Response processRequireAction(final String code, String action) {
event.event(EventType.CUSTOM_REQUIRED_ACTION);
- event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
- Checks checks = new Checks();
- if (!checks.verifyRequiredAction(code, action)) {
- return checks.response;
+
+ SessionCodeChecks checks = checksForCode(code, action, REQUIRED_ACTION);
+ if (!checks.verifyRequiredAction(action)) {
+ return checks.getResponse();
}
- final ClientSessionCode clientCode = checks.clientCode;
- final ClientSessionModel clientSession = clientCode.getClientSession();
- final UserSessionModel userSession = clientSession.getUserSession();
+ AuthenticationSessionModel authSession = checks.getAuthenticationSession();
+ if (!checks.isActionRequest()) {
+ initLoginEvent(authSession);
+ event.event(EventType.CUSTOM_REQUIRED_ACTION);
+ return AuthenticationManager.nextActionAfterAuthentication(session, authSession, clientConnection, request, uriInfo, event);
+ }
+
+ initLoginEvent(authSession);
+ event.event(EventType.CUSTOM_REQUIRED_ACTION);
+ event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, action);
if (factory == null) {
@@ -892,58 +849,46 @@ public class LoginActionsService {
}
RequiredActionProvider provider = factory.create(session);
- initEvent(clientSession);
- event.event(EventType.CUSTOM_REQUIRED_ACTION);
-
-
- RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, userSession.getUser(), factory) {
+ RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, authSession.getAuthenticatedUser(), factory) {
@Override
public void ignore() {
throw new RuntimeException("Cannot call ignore within processAction()");
}
};
+
+ Response response;
provider.processAction(context);
+
+ if (action != null) {
+ authSession.setAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION, action);
+ }
+
if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
event.clone().success();
- initEvent(clientSession);
+ initLoginEvent(authSession);
event.event(EventType.LOGIN);
- clientSession.removeRequiredAction(factory.getId());
- userSession.getUser().removeRequiredAction(factory.getId());
- clientSession.removeNote(AuthenticationManager.CURRENT_REQUIRED_ACTION);
-
- if (AuthenticationManager.isActionRequired(session, userSession, clientSession, clientConnection, request, uriInfo, event)) {
- // redirect to a generic code URI so that browser refresh will work
- return redirectToRequiredActions(checks.clientCode.getCode());
- } else {
- return AuthenticationManager.finishedRequiredActions(session, userSession, clientSession, clientConnection, request, uriInfo, event);
-
- }
- }
- if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
- return context.getChallenge();
- }
- if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
- LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
+ authSession.removeRequiredAction(factory.getId());
+ authSession.getAuthenticatedUser().removeRequiredAction(factory.getId());
+ authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+
+ response = AuthenticationManager.nextActionAfterAuthentication(session, authSession, clientConnection, request, uriInfo, event);
+ } else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
+ response = context.getChallenge();
+ } else if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
+ LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, authSession.getProtocol());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
.setUriInfo(context.getUriInfo())
.setEventBuilder(event);
event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
- Response response = protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED);
+ response = protocol.sendError(authSession, Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
- return response;
-
+ } else {
+ throw new RuntimeException("Unreachable");
}
- throw new RuntimeException("Unreachable");
- }
-
- public Response redirectToRequiredActions(String code) {
- URI redirect = LoginActionsService.loginActionsBaseUrl(uriInfo)
- .path(LoginActionsService.REQUIRED_ACTION)
- .queryParam(OAuth2Constants.CODE, code).build(realm.getName());
- return Response.status(302).location(redirect).build();
+ return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, true);
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
new file mode 100644
index 0000000..87eaf20
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.services.resources;
+
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.authentication.actiontoken.DefaultActionToken;
+import org.keycloak.authentication.ExplainedVerificationException;
+import org.keycloak.authentication.actiontoken.ActionTokenContext;
+import org.keycloak.authentication.actiontoken.ExplainedTokenVerificationException;
+import org.keycloak.common.VerificationException;
+import org.keycloak.events.Errors;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.*;
+import org.keycloak.protocol.oidc.utils.RedirectUtils;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.AuthenticationSessionManager;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.CommonClientSessionModel.Action;
+import java.util.Objects;
+import java.util.function.Consumer;
+import org.jboss.logging.Logger;
+/**
+ *
+ * @author hmlnarik
+ */
+public class LoginActionsServiceChecks {
+
+ private static final Logger LOG = Logger.getLogger(LoginActionsServiceChecks.class.getName());
+
+ /**
+ * This check verifies that user ID (subject) from the token matches
+ * the one from the authentication session.
+ */
+ public static class AuthenticationSessionUserIdMatchesOneFromToken implements Predicate<JsonWebToken> {
+
+ private final ActionTokenContext<?> context;
+
+ public AuthenticationSessionUserIdMatchesOneFromToken(ActionTokenContext<?> context) {
+ this.context = context;
+ }
+
+ @Override
+ public boolean test(JsonWebToken t) throws VerificationException {
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+
+ if (authSession == null || authSession.getAuthenticatedUser() == null
+ || ! Objects.equals(t.getSubject(), authSession.getAuthenticatedUser().getId())) {
+ throw new ExplainedTokenVerificationException(t, Errors.INVALID_TOKEN, Messages.INVALID_USER);
+ }
+
+ return true;
+ }
+ }
+
+ /**
+ * Verifies that if authentication session exists and any action is required according to it, then it is
+ * the expected one.
+ *
+ * If there is an action required in the session, furthermore it is not the expected one, and the required
+ * action is redirection to "required actions", it throws with response performing the redirect to required
+ * actions.
+ * @param <T>
+ */
+ public static class IsActionRequired implements Predicate<JsonWebToken> {
+
+ private final ActionTokenContext<?> context;
+
+ private final ClientSessionModel.Action expectedAction;
+
+ public IsActionRequired(ActionTokenContext<?> context, Action expectedAction) {
+ this.context = context;
+ this.expectedAction = expectedAction;
+ }
+
+ @Override
+ public boolean test(JsonWebToken t) throws VerificationException {
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+
+ if (authSession != null && ! Objects.equals(authSession.getAction(), this.expectedAction.name())) {
+ if (Objects.equals(ClientSessionModel.Action.REQUIRED_ACTIONS.name(), authSession.getAction())) {
+ throw new LoginActionsServiceException(
+ AuthenticationManager.nextActionAfterAuthentication(context.getSession(), authSession,
+ context.getClientConnection(), context.getRequest(), context.getUriInfo(), context.getEvent()));
+ }
+ throw new ExplainedTokenVerificationException(t, Errors.INVALID_TOKEN, Messages.INVALID_CODE);
+ }
+
+ return true;
+ }
+ }
+
+ /**
+ * Verifies that the authentication session has not yet been converted to user session, in other words
+ * that the user has not yet completed authentication and logged in.
+ */
+ public static <T extends JsonWebToken> void checkNotLoggedInYet(ActionTokenContext<T> context, String authSessionId) throws VerificationException {
+ if (authSessionId == null) {
+ return;
+ }
+
+ UserSessionModel userSession = context.getSession().sessions().getUserSession(context.getRealm(), authSessionId);
+ if (userSession != null) {
+ LoginFormsProvider loginForm = context.getSession().getProvider(LoginFormsProvider.class)
+ .setSuccess(Messages.ALREADY_LOGGED_IN);
+
+ ClientModel client = null;
+ String lastClientUuid = userSession.getNote(AuthenticationManager.LAST_AUTHENTICATED_CLIENT);
+ if (lastClientUuid != null) {
+ client = context.getRealm().getClientById(lastClientUuid);
+ }
+
+ if (client != null) {
+ context.getSession().getContext().setClient(client);
+ } else {
+ loginForm.setAttribute("skipLink", true);
+ }
+
+ throw new LoginActionsServiceException(loginForm.createInfoPage());
+ }
+ }
+
+ /**
+ * Verifies whether the user given by ID both exists in the current realm. If yes,
+ * it optionally also injects the user using the given function (e.g. into session context).
+ */
+ public static void checkIsUserValid(KeycloakSession session, RealmModel realm, String userId, Consumer<UserModel> userSetter) throws VerificationException {
+ UserModel user = userId == null ? null : session.users().getUserById(userId, realm);
+
+ if (user == null) {
+ throw new ExplainedVerificationException(Errors.USER_NOT_FOUND, Messages.INVALID_USER);
+ }
+
+ if (! user.isEnabled()) {
+ throw new ExplainedVerificationException(Errors.USER_DISABLED, Messages.INVALID_USER);
+ }
+
+ if (userSetter != null) {
+ userSetter.accept(user);
+ }
+ }
+
+ /**
+ * Verifies whether the user given by ID both exists in the current realm. If yes,
+ * it optionally also injects the user using the given function (e.g. into session context).
+ */
+ public static <T extends DefaultActionToken> void checkIsUserValid(T token, ActionTokenContext<T> context) throws VerificationException {
+ try {
+ checkIsUserValid(context.getSession(), context.getRealm(), token.getUserId(), context.getAuthenticationSession()::setAuthenticatedUser);
+ } catch (ExplainedVerificationException ex) {
+ throw new ExplainedTokenVerificationException(token, ex);
+ }
+ }
+
+ /**
+ * Verifies whether the client denoted by client ID in token's {@code iss} ({@code issuedFor})
+ * field both exists and is enabled.
+ */
+ public static void checkIsClientValid(KeycloakSession session, ClientModel client) throws VerificationException {
+ if (client == null) {
+ throw new ExplainedVerificationException(Errors.CLIENT_NOT_FOUND, Messages.UNKNOWN_LOGIN_REQUESTER);
+ }
+
+ if (! client.isEnabled()) {
+ throw new ExplainedVerificationException(Errors.CLIENT_NOT_FOUND, Messages.LOGIN_REQUESTER_NOT_ENABLED);
+ }
+ }
+
+ /**
+ * Verifies whether the client denoted by client ID in token's {@code iss} ({@code issuedFor})
+ * field both exists and is enabled.
+ */
+ public static <T extends DefaultActionToken> void checkIsClientValid(T token, ActionTokenContext<T> context) throws VerificationException {
+ String clientId = token.getIssuedFor();
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+ ClientModel client = authSession == null ? null : authSession.getClient();
+
+ try {
+ checkIsClientValid(context.getSession(), client);
+
+ if (clientId != null && ! Objects.equals(client.getClientId(), clientId)) {
+ throw new ExplainedTokenVerificationException(token, Errors.CLIENT_NOT_FOUND, Messages.UNKNOWN_LOGIN_REQUESTER);
+ }
+ } catch (ExplainedVerificationException ex) {
+ throw new ExplainedTokenVerificationException(token, ex);
+ }
+ }
+
+ /**
+ * Verifies whether the given redirect URL, when set, is valid for the given client.
+ */
+ public static class IsRedirectValid implements Predicate<JsonWebToken> {
+
+ private final ActionTokenContext<?> context;
+
+ private final String redirectUri;
+
+ public IsRedirectValid(ActionTokenContext<?> context, String redirectUri) {
+ this.context = context;
+ this.redirectUri = redirectUri;
+ }
+
+ @Override
+ public boolean test(JsonWebToken t) throws VerificationException {
+ if (redirectUri == null) {
+ return true;
+ }
+
+ ClientModel client = context.getAuthenticationSession().getClient();
+
+ if (RedirectUtils.verifyRedirectUri(context.getUriInfo(), redirectUri, context.getRealm(), client) == null) {
+ throw new ExplainedTokenVerificationException(t, Errors.INVALID_REDIRECT_URI, Messages.INVALID_REDIRECT_URI);
+ }
+
+ return true;
+ }
+ }
+
+ /**
+ * This check verifies that current authentication session is consistent with the one specified in token.
+ * Examples:
+ * <ul>
+ * <li>1. Email from administrator with reset e-mail - token does not contain auth session ID</li>
+ * <li>2. Email from "verify e-mail" step within flow - token contains auth session ID.</li>
+ * <li>3. User clicked the link in an e-mail and gets to a new browser - authentication session cookie is not set</li>
+ * <li>4. User clicked the link in an e-mail while having authentication running - authentication session cookie
+ * is already set in the browser</li>
+ * </ul>
+ *
+ * <ul>
+ * <li>For combinations 1 and 3, 1 and 4, and 2 and 3: Requests next step</li>
+ * <li>For combination 2 and 4:
+ * <ul>
+ * <li>If the auth session IDs from token and cookie match, pass</li>
+ * <li>Else if the auth session from cookie was forked and its parent auth session ID
+ * matches that of token, replaces current auth session with that of parent and passes</li>
+ * <li>Else requests restart by throwing RestartFlow exception</li>
+ * </ul>
+ * </li>
+ * </ul>
+ *
+ * When the check passes, it also sets the authentication session in token context accordingly.
+ *
+ * @param <T>
+ */
+ public static <T extends JsonWebToken> boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext<T> context, String authSessionIdFromToken) throws VerificationException {
+ if (authSessionIdFromToken == null) {
+ return false;
+ }
+
+ AuthenticationSessionManager asm = new AuthenticationSessionManager(context.getSession());
+ String authSessionIdFromCookie = asm.getCurrentAuthenticationSessionId(context.getRealm());
+
+ if (authSessionIdFromCookie == null) {
+ return false;
+ }
+
+ AuthenticationSessionModel authSessionFromCookie = context.getSession()
+ .authenticationSessions().getAuthenticationSession(context.getRealm(), authSessionIdFromCookie);
+ if (authSessionFromCookie == null) { // Cookie contains ID of expired auth session
+ return false;
+ }
+
+ if (Objects.equals(authSessionIdFromCookie, authSessionIdFromToken)) {
+ context.setAuthenticationSession(authSessionFromCookie, false);
+ return true;
+ }
+
+ String parentSessionId = authSessionFromCookie.getAuthNote(AuthenticationProcessor.FORKED_FROM);
+ if (parentSessionId == null || ! Objects.equals(authSessionIdFromToken, parentSessionId)) {
+ return false;
+ }
+
+ AuthenticationSessionModel authSessionFromParent = context.getSession()
+ .authenticationSessions().getAuthenticationSession(context.getRealm(), parentSessionId);
+
+ // It's the correct browser. Let's remove forked session as we won't continue
+ // from the login form (browser flow) but from the token's flow
+ // Don't expire KC_RESTART cookie at this point
+ asm.removeAuthenticationSession(context.getRealm(), authSessionFromCookie, false);
+ LOG.debugf("Removed forked session: %s", authSessionFromCookie.getId());
+
+ // Refresh browser cookie
+ asm.setAuthSessionCookie(parentSessionId, context.getRealm());
+
+ context.setAuthenticationSession(authSessionFromParent, false);
+ context.setExecutionId(authSessionFromParent.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION));
+
+ return true;
+ }
+
+ public static <T extends DefaultActionToken> void checkTokenWasNotUsedYet(T token, ActionTokenContext<T> context) throws VerificationException {
+ ActionTokenStoreProvider actionTokenStore = context.getSession().getProvider(ActionTokenStoreProvider.class);
+ if (actionTokenStore.get(token) != null) {
+ throw new ExplainedTokenVerificationException(token, Errors.EXPIRED_CODE, Messages.EXPIRED_ACTION);
+ }
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceException.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceException.java
new file mode 100644
index 0000000..3e758df
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceException.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.services.resources;
+
+import org.keycloak.common.VerificationException;
+import javax.ws.rs.core.Response;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class LoginActionsServiceException extends VerificationException {
+
+ private final Response response;
+
+ public LoginActionsServiceException(Response response) {
+ this.response = response;
+ }
+
+ public LoginActionsServiceException(Response response, String message) {
+ super(message);
+ this.response = response;
+ }
+
+ public LoginActionsServiceException(Response response, String message, Throwable cause) {
+ super(message, cause);
+ this.response = response;
+ }
+
+ public LoginActionsServiceException(Response response, Throwable cause) {
+ super(cause);
+ this.response = response;
+ }
+
+ public Response getResponse() {
+ return response;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
new file mode 100644
index 0000000..978ad6f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.services.resources;
+
+import java.net.URI;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.jboss.logging.Logger;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.common.util.ObjectUtil;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.AuthorizationEndpointBase;
+import org.keycloak.protocol.RestartLoginCookie;
+import org.keycloak.services.ErrorPage;
+import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.AuthenticationSessionManager;
+import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.util.BrowserHistoryHelper;
+import org.keycloak.services.util.AuthenticationFlowURLHelper;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+
+public class SessionCodeChecks {
+
+ private static final Logger logger = Logger.getLogger(SessionCodeChecks.class);
+
+ private AuthenticationSessionModel authSession;
+ private ClientSessionCode<AuthenticationSessionModel> clientCode;
+ private Response response;
+ private boolean actionRequest;
+
+ private final RealmModel realm;
+ private final UriInfo uriInfo;
+ private final ClientConnection clientConnection;
+ private final KeycloakSession session;
+ private final EventBuilder event;
+
+ private final String code;
+ private final String execution;
+ private final String flowPath;
+
+
+ public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, String code, String execution, String flowPath) {
+ this.realm = realm;
+ this.uriInfo = uriInfo;
+ this.clientConnection = clientConnection;
+ this.session = session;
+ this.event = event;
+
+ this.code = code;
+ this.execution = execution;
+ this.flowPath = flowPath;
+ }
+
+
+ public AuthenticationSessionModel getAuthenticationSession() {
+ return authSession;
+ }
+
+
+ private boolean failed() {
+ return response != null;
+ }
+
+
+ public Response getResponse() {
+ return response;
+ }
+
+
+ public ClientSessionCode<AuthenticationSessionModel> getClientCode() {
+ return clientCode;
+ }
+
+ public boolean isActionRequest() {
+ return actionRequest;
+ }
+
+
+ private boolean checkSsl() {
+ if (uriInfo.getBaseUri().getScheme().equals("https")) {
+ return true;
+ } else {
+ return !realm.getSslRequired().isRequired(clientConnection);
+ }
+ }
+
+
+ public AuthenticationSessionModel initialVerifyAuthSession() {
+ // Basic realm checks
+ if (!checkSsl()) {
+ event.error(Errors.SSL_REQUIRED);
+ response = ErrorPage.error(session, Messages.HTTPS_REQUIRED);
+ return null;
+ }
+ if (!realm.isEnabled()) {
+ event.error(Errors.REALM_DISABLED);
+ response = ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
+ return null;
+ }
+
+ // object retrieve
+ AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, AuthenticationSessionModel.class);
+ if (authSession != null) {
+ return authSession;
+ }
+
+ // See if we are already authenticated and userSession with same ID exists.
+ String sessionId = new AuthenticationSessionManager(session).getCurrentAuthenticationSessionId(realm);
+ if (sessionId != null) {
+ UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
+ if (userSession != null) {
+
+ LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class)
+ .setSuccess(Messages.ALREADY_LOGGED_IN);
+
+ ClientModel client = null;
+ String lastClientUuid = userSession.getNote(AuthenticationManager.LAST_AUTHENTICATED_CLIENT);
+ if (lastClientUuid != null) {
+ client = realm.getClientById(lastClientUuid);
+ }
+
+ if (client != null) {
+ session.getContext().setClient(client);
+ } else {
+ loginForm.setAttribute("skipLink", true);
+ }
+
+ response = loginForm.createInfoPage();
+ return null;
+ }
+ }
+
+ // Otherwise just try to restart from the cookie
+ response = restartAuthenticationSessionFromCookie();
+ return null;
+ }
+
+
+ public boolean initialVerify() {
+ // Basic realm checks and authenticationSession retrieve
+ authSession = initialVerifyAuthSession();
+ if (authSession == null) {
+ return false;
+ }
+
+ // Check cached response from previous action request
+ response = BrowserHistoryHelper.getInstance().loadSavedResponse(session, authSession);
+ if (response != null) {
+ return false;
+ }
+
+ // Client checks
+ event.detail(Details.CODE_ID, authSession.getId());
+ ClientModel client = authSession.getClient();
+ if (client == null) {
+ event.error(Errors.CLIENT_NOT_FOUND);
+ response = ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER);
+ clientCode.removeExpiredClientSession();
+ return false;
+ }
+
+ event.client(client);
+ session.getContext().setClient(client);
+
+ if (!client.isEnabled()) {
+ event.error(Errors.CLIENT_DISABLED);
+ response = ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED);
+ clientCode.removeExpiredClientSession();
+ return false;
+ }
+
+
+ // Check if it's action or not
+ if (code == null) {
+ String lastExecFromSession = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+ String lastFlow = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
+
+ // Check if we transitted between flows (eg. clicking "register" on login screen)
+ if (execution==null && !flowPath.equals(lastFlow)) {
+ logger.debugf("Transition between flows! Current flow: %s, Previous flow: %s", flowPath, lastFlow);
+
+ // Don't allow moving to different flow if I am on requiredActions already
+ if (ClientSessionModel.Action.AUTHENTICATE.name().equals(authSession.getAction())) {
+ authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, flowPath);
+ authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+ lastExecFromSession = null;
+ }
+ }
+
+ if (ObjectUtil.isEqualOrBothNull(execution, lastExecFromSession)) {
+ // Allow refresh of previous page
+ clientCode = new ClientSessionCode<>(session, realm, authSession);
+ actionRequest = false;
+ return true;
+ } else {
+ response = showPageExpired(authSession);
+ return false;
+ }
+ } else {
+ ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, session, realm, AuthenticationSessionModel.class);
+ clientCode = result.getCode();
+ if (clientCode == null) {
+
+ // In case that is replayed action, but sent to the same FORM like actual FORM, we just re-render the page
+ if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION))) {
+ String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
+ URI redirectUri = getLastExecutionUrl(latestFlowPath, execution);
+
+ logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri);
+ authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION);
+ response = Response.status(Response.Status.FOUND).location(redirectUri).build();
+ } else {
+ response = showPageExpired(authSession);
+ }
+ return false;
+ }
+
+
+ actionRequest = true;
+ if (execution != null) {
+ authSession.setAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION, execution);
+ }
+ return true;
+ }
+ }
+
+
+ public boolean verifyActiveAndValidAction(String expectedAction, ClientSessionCode.ActionType actionType) {
+ if (failed()) {
+ return false;
+ }
+
+ if (!isActionActive(actionType)) {
+ return false;
+ }
+
+ if (!clientCode.isValidAction(expectedAction)) {
+ AuthenticationSessionModel authSession = getAuthenticationSession();
+ if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(authSession.getAction())) {
+ logger.debugf("Incorrect action '%s' . User authenticated already.", authSession.getAction());
+ response = showPageExpired(authSession);
+ return false;
+ } else {
+ logger.errorf("Bad action. Expected action '%s', current action '%s'", expectedAction, authSession.getAction());
+ response = ErrorPage.error(session, Messages.EXPIRED_CODE);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+
+ private boolean isActionActive(ClientSessionCode.ActionType actionType) {
+ if (!clientCode.isActionActive(actionType)) {
+ event.clone().error(Errors.EXPIRED_CODE);
+
+ AuthenticationProcessor.resetFlow(authSession, LoginActionsService.AUTHENTICATE_PATH);
+
+ authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.LOGIN_TIMEOUT);
+
+ URI redirectUri = getLastExecutionUrl(LoginActionsService.AUTHENTICATE_PATH, null);
+ logger.debugf("Flow restart after timeout. Redirecting to %s", redirectUri);
+ response = Response.status(Response.Status.FOUND).location(redirectUri).build();
+ return false;
+ }
+ return true;
+ }
+
+
+ public boolean verifyRequiredAction(String executedAction) {
+ if (failed()) {
+ return false;
+ }
+
+ if (!clientCode.isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) {
+ logger.debugf("Expected required action, but session action is '%s' . Showing expired page now.", authSession.getAction());
+ event.error(Errors.INVALID_CODE);
+
+ response = showPageExpired(authSession);
+
+ return false;
+ }
+
+ if (!isActionActive(ClientSessionCode.ActionType.USER)) {
+ return false;
+ }
+
+ if (actionRequest) {
+ String currentRequiredAction = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+ if (executedAction == null || !executedAction.equals(currentRequiredAction)) {
+ logger.debug("required action doesn't match current required action");
+ response = redirectToRequiredActions(currentRequiredAction);
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+ private Response restartAuthenticationSessionFromCookie() {
+ logger.debug("Authentication session not found. Trying to restart from cookie.");
+ AuthenticationSessionModel authSession = null;
+ try {
+ authSession = RestartLoginCookie.restartSession(session, realm);
+ } catch (Exception e) {
+ ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
+ }
+
+ if (authSession != null) {
+
+ event.clone();
+ event.detail(Details.RESTART_AFTER_TIMEOUT, "true");
+ event.error(Errors.EXPIRED_CODE);
+
+ String warningMessage = Messages.LOGIN_TIMEOUT;
+ authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, warningMessage);
+
+ String flowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW);
+ if (flowPath == null) {
+ flowPath = LoginActionsService.AUTHENTICATE_PATH;
+ }
+
+ URI redirectUri = getLastExecutionUrl(flowPath, null);
+ logger.debugf("Authentication session restart from cookie succeeded. Redirecting to %s", redirectUri);
+ return Response.status(Response.Status.FOUND).location(redirectUri).build();
+ } else {
+ // Finally need to show error as all the fallbacks failed
+ event.error(Errors.INVALID_CODE);
+ return ErrorPage.error(session, Messages.INVALID_CODE);
+ }
+ }
+
+
+ private Response redirectToRequiredActions(String action) {
+ UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
+ .path(LoginActionsService.REQUIRED_ACTION);
+
+ if (action != null) {
+ uriBuilder.queryParam("execution", action);
+ }
+ URI redirect = uriBuilder.build(realm.getName());
+ return Response.status(302).location(redirect).build();
+ }
+
+
+ private URI getLastExecutionUrl(String flowPath, String executionId) {
+ return new AuthenticationFlowURLHelper(session, realm, uriInfo)
+ .getLastExecutionUrl(flowPath, executionId);
+ }
+
+
+ private Response showPageExpired(AuthenticationSessionModel authSession) {
+ return new AuthenticationFlowURLHelper(session, realm, uriInfo)
+ .showPageExpired(authSession);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java
index 5935eb0..5315be4 100755
--- a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java
+++ b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java
@@ -32,6 +32,7 @@ public class ClearExpiredUserSessions implements ScheduledTask {
UserSessionProvider sessions = session.sessions();
for (RealmModel realm : session.realms().getRealms()) {
sessions.removeExpired(realm);
+ session.authenticationSessions().removeExpired(realm);
}
}
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index 21eb047..e92aa05 100755
--- a/services/src/main/java/org/keycloak/services/Urls.java
+++ b/services/src/main/java/org/keycloak/services/Urls.java
@@ -178,8 +178,9 @@ public class Urls {
return loginResetCredentialsBuilder(baseUri).build(realmName);
}
- public static UriBuilder executeActionsBuilder(URI baseUri) {
- return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActions");
+ public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString) {
+ return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
+ .queryParam("key", tokenString);
}
public static UriBuilder loginResetCredentialsBuilder(URI baseUri) {
@@ -206,6 +207,11 @@ public class Urls {
return loginActionsBase(baseUri).path(LoginActionsService.class, "authenticate").build(realmName);
}
+ public static URI realmLoginRestartPage(URI baseUri, String realmId) {
+ return loginActionsBase(baseUri).path(LoginActionsService.class, "restartSession")
+ .build(realmId);
+ }
+
private static UriBuilder realmLogout(URI baseUri) {
return tokenBase(baseUri).path(OIDCLoginProtocolService.class, "logout");
}
diff --git a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java
new file mode 100644
index 0000000..b97963e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.services.util;
+
+import java.net.URI;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.jboss.logging.Logger;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.AuthorizationEndpointBase;
+import org.keycloak.services.resources.LoginActionsService;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AuthenticationFlowURLHelper {
+
+ protected static final Logger logger = Logger.getLogger(AuthenticationFlowURLHelper.class);
+
+ private final KeycloakSession session;
+ private final RealmModel realm;
+ private final UriInfo uriInfo;
+
+ public AuthenticationFlowURLHelper(KeycloakSession session, RealmModel realm, UriInfo uriInfo) {
+ this.session = session;
+ this.realm = realm;
+ this.uriInfo = uriInfo;
+ }
+
+
+ public Response showPageExpired(AuthenticationSessionModel authSession) {
+ URI lastStepUrl = getLastExecutionUrl(authSession);
+
+ logger.debugf("Redirecting to 'page expired' now. Will use last step URL: %s", lastStepUrl);
+
+ return session.getProvider(LoginFormsProvider.class)
+ .setActionUri(lastStepUrl)
+ .createLoginExpiredPage();
+ }
+
+
+ public URI getLastExecutionUrl(String flowPath, String executionId) {
+ UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
+ .path(flowPath);
+
+ if (executionId != null) {
+ uriBuilder.queryParam("execution", executionId);
+ }
+ return uriBuilder.build(realm.getName());
+ }
+
+
+ public URI getLastExecutionUrl(AuthenticationSessionModel authSession) {
+ String executionId = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
+ String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
+
+ if (latestFlowPath == null) {
+ latestFlowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW);
+ }
+
+ if (latestFlowPath == null) {
+ latestFlowPath = LoginActionsService.AUTHENTICATE_PATH;
+ }
+
+ return getLastExecutionUrl(latestFlowPath, executionId);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java b/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java
new file mode 100644
index 0000000..ef34b16
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.services.util;
+
+import java.net.URI;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.ws.rs.core.Response;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.theme.BrowserSecurityHeaderSetup;
+import org.keycloak.utils.MediaType;
+
+/**
+ * The point of this is to improve experience of browser history (back/forward/refresh buttons), but ensure there is no more redirects then necessary.
+ *
+ * Ideally we want to:
+ * - Remove all POST requests from browser history, because browsers don't automatically re-send them when click "back" button. POSTS in history causes unfriendly dialogs and browser "Page is expired" pages.
+ *
+ * - Keep the browser URL to match the flow and execution from authentication session. This means that browser refresh works fine and show us the correct form.
+ *
+ * - Avoid redirects. This is possible with javascript based approach (JavascriptHistoryReplace). The RedirectAfterPostHelper requires one redirect after POST, but works even on browser without javascript and
+ * on old browsers where "history.replaceState" is unsupported.
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class BrowserHistoryHelper {
+
+ protected static final Logger logger = Logger.getLogger(BrowserHistoryHelper.class);
+
+ public abstract Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest);
+
+ public abstract Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession);
+
+
+ // Always rely on javascript for now
+ public static BrowserHistoryHelper getInstance() {
+ return new JavascriptHistoryReplace();
+ //return new RedirectAfterPostHelper();
+ //return new NoOpHelper();
+ }
+
+
+ // IMPL
+
+ private static class JavascriptHistoryReplace extends BrowserHistoryHelper {
+
+ private static final Pattern HEAD_END_PATTERN = Pattern.compile("</[hH][eE][aA][dD]>");
+
+ @Override
+ public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) {
+ if (!actionRequest) {
+ return response;
+ }
+
+ // For now, handle just status 200 with String body. See if more is needed...
+ Object entity = response.getEntity();
+ if (entity != null && entity instanceof String) {
+ String responseString = (String) entity;
+
+ URI lastExecutionURL = new AuthenticationFlowURLHelper(session, session.getContext().getRealm(), session.getContext().getUri()).getLastExecutionUrl(authSession);
+
+ // Inject javascript for history "replaceState"
+ String responseWithJavascript = responseWithJavascript(responseString, lastExecutionURL.toString());
+
+ return Response.fromResponse(response).entity(responseWithJavascript).build();
+ }
+
+
+ return response;
+ }
+
+ @Override
+ public Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession) {
+ return null;
+ }
+
+
+ private String responseWithJavascript(String origHtml, String lastExecutionUrl) {
+ Matcher m = HEAD_END_PATTERN.matcher(origHtml);
+
+ if (m.find()) {
+ int start = m.start();
+
+ String javascript = getJavascriptText(lastExecutionUrl);
+
+ return new StringBuilder(origHtml.substring(0, start))
+ .append(javascript )
+ .append(origHtml.substring(start))
+ .toString();
+ } else {
+ return origHtml;
+ }
+ }
+
+ private String getJavascriptText(String lastExecutionUrl) {
+ return new StringBuilder("<SCRIPT>")
+ .append(" if (typeof history.replaceState === 'function') {")
+ .append(" history.replaceState({}, \"some title\", \"" + lastExecutionUrl + "\");")
+ .append(" }")
+ .append("</SCRIPT>")
+ .toString();
+ }
+
+ }
+
+
+ // This impl is limited ATM. Saved request doesn't save response HTTP headers, so they may not be fully restored..
+ private static class RedirectAfterPostHelper extends BrowserHistoryHelper {
+
+ private static final String CACHED_RESPONSE = "cached.response";
+
+ @Override
+ public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) {
+ if (!actionRequest) {
+ return response;
+ }
+
+ // For now, handle just status 200 with String body. See if more is needed...
+ if (response.getStatus() == 200) {
+ Object entity = response.getEntity();
+ if (entity instanceof String) {
+ String responseString = (String) entity;
+ authSession.setAuthNote(CACHED_RESPONSE, responseString);
+
+ URI lastExecutionURL = new AuthenticationFlowURLHelper(session, session.getContext().getRealm(), session.getContext().getUri()).getLastExecutionUrl(authSession);
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Saved response challenge and redirect to %s", lastExecutionURL);
+ }
+
+ return Response.status(302).location(lastExecutionURL).build();
+ }
+ }
+
+ return response;
+ }
+
+
+ @Override
+ public Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession) {
+ String savedResponse = authSession.getAuthNote(CACHED_RESPONSE);
+ if (savedResponse != null) {
+ authSession.removeAuthNote(CACHED_RESPONSE);
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Restored previously saved request");
+ }
+
+ Response.ResponseBuilder builder = Response.status(200).type(MediaType.TEXT_HTML_UTF_8).entity(savedResponse);
+ BrowserSecurityHeaderSetup.headers(builder, session.getContext().getRealm()); // TODO rather all the headers from the saved response should be added here.
+ return builder.build();
+ }
+
+ return null;
+ }
+
+ }
+
+
+ private static class NoOpHelper extends BrowserHistoryHelper {
+
+ @Override
+ public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) {
+ return response;
+ }
+
+
+ @Override
+ public Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession) {
+ return null;
+ }
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/util/CookieHelper.java b/services/src/main/java/org/keycloak/services/util/CookieHelper.java
index b8d57f9..2005695 100755
--- a/services/src/main/java/org/keycloak/services/util/CookieHelper.java
+++ b/services/src/main/java/org/keycloak/services/util/CookieHelper.java
@@ -17,11 +17,19 @@
package org.keycloak.services.util;
+import java.net.URI;
+
import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.util.ServerCookie;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.managers.AuthenticationManager;
+import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -50,4 +58,10 @@ public class CookieHelper {
}
+ public static String getCookieValue(String name) {
+ HttpHeaders headers = ResteasyProviderFactory.getContextData(HttpHeaders.class);
+ Cookie cookie = headers.getCookies().get(name);
+ return cookie != null ? cookie.getValue() : null;
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
index c6b340f..00be27c 100755
--- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
@@ -26,14 +26,13 @@ import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
import twitter4j.Twitter;
import twitter4j.TwitterFactory;
import twitter4j.auth.AccessToken;
@@ -41,6 +40,7 @@ import twitter4j.auth.RequestToken;
import javax.ws.rs.GET;
import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
@@ -48,8 +48,6 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
-import static org.keycloak.models.ClientSessionModel.Action.AUTHENTICATE;
-
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -57,6 +55,10 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
SocialIdentityProvider<OAuth2IdentityProviderConfig> {
protected static final Logger logger = Logger.getLogger(TwitterIdentityProvider.class);
+
+ private static final String TWITTER_TOKEN = "twitter_token";
+ private static final String TWITTER_TOKENSECRET = "twitter_tokenSecret";
+
public TwitterIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(session, config);
}
@@ -75,10 +77,10 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
URI uri = new URI(request.getRedirectUri() + "?state=" + request.getState());
RequestToken requestToken = twitter.getOAuthRequestToken(uri.toString());
- ClientSessionModel clientSession = request.getClientSession();
+ AuthenticationSessionModel authSession = request.getAuthenticationSession();
- clientSession.setNote("twitter_token", requestToken.getToken());
- clientSession.setNote("twitter_tokenSecret", requestToken.getTokenSecret());
+ authSession.setAuthNote(TWITTER_TOKEN, requestToken.getToken());
+ authSession.setAuthNote(TWITTER_TOKENSECRET, requestToken.getTokenSecret());
URI authenticationUrl = URI.create(requestToken.getAuthenticationURL());
@@ -117,15 +119,17 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
return callback.cancelled(state);
}
+ Response errorResponse = null;
+
try {
Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(getConfig().getClientId(), getConfig().getClientSecret());
- ClientSessionModel clientSession = ClientSessionCode.getClientSession(state, session, realm);
+ AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(state, session, realm, AuthenticationSessionModel.class);
- String twitterToken = clientSession.getNote("twitter_token");
- String twitterSecret = clientSession.getNote("twitter_tokenSecret");
+ String twitterToken = authSession.getAuthNote(TWITTER_TOKEN);
+ String twitterSecret = authSession.getAuthNote(TWITTER_TOKENSECRET);
RequestToken requestToken = new RequestToken(twitterToken, twitterSecret);
@@ -152,37 +156,20 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
identity.setCode(state);
return callback.authenticated(identity);
+ } catch (WebApplicationException e) {
+ sendErrorEvent();
+ return e.getResponse();
} catch (Exception e) {
logger.error("Could get user profile from twitter.", e);
+ sendErrorEvent();
+ return ErrorPage.error(session, Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE);
}
+ }
+
+ private void sendErrorEvent() {
EventBuilder event = new EventBuilder(realm, session, clientConnection);
event.event(EventType.LOGIN);
event.error("twitter_login_failed");
- return ErrorPage.error(session, Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE);
- }
-
- private ClientSessionCode parseClientSessionCode(String code) {
- ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realm);
-
- if (clientCode != null && clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
- ClientSessionModel clientSession = clientCode.getClientSession();
-
- if (clientSession != null) {
- ClientModel client = clientSession.getClient();
-
- if (client == null) {
- throw new IdentityBrokerException("Invalid client");
- }
-
- logger.debugf("Got authorization code from client [%s].", client.getClientId());
- }
-
- logger.debugf("Authorization code is valid.");
-
- return clientCode;
- }
-
- throw new IdentityBrokerException("Invalid code, please login again through your application.");
}
}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
new file mode 100644
index 0000000..2a5b9ec
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
@@ -0,0 +1,4 @@
+org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler
+org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionTokenHandler
+org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionTokenHandler
+org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionTokenHandler
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index e234124..873ff8d 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -19,4 +19,4 @@ org.keycloak.exportimport.ClientDescriptionConverterSpi
org.keycloak.wellknown.WellKnownSpi
org.keycloak.services.clientregistration.ClientRegistrationSpi
org.keycloak.services.clientregistration.policy.ClientRegistrationPolicySpi
-
+org.keycloak.authentication.actiontoken.ActionTokenHandlerSpi
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
index 474417c..acf775c 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
@@ -33,12 +33,16 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.pages.IdpConfirmLinkPage;
import org.keycloak.testsuite.pages.IdpLinkEmailPage;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LoginExpiredPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import javax.mail.internet.MimeMessage;
@@ -48,6 +52,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -71,6 +77,9 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
@WebResource
protected LoginPasswordUpdatePage passwordUpdatePage;
+ @WebResource
+ protected LoginExpiredPage loginExpiredPage;
+
/**
@@ -291,6 +300,145 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
Assert.assertTrue(user.isEmailVerified());
}
+ /**
+ * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by email
+ */
+ @Test
+ public void testLinkAccountByEmailVerificationTwice() throws Exception {
+ setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF);
+
+ loginIDP("pedroigor");
+
+ this.idpConfirmLinkPage.assertCurrent();
+ Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
+ this.idpConfirmLinkPage.clickLinkAccount();
+
+ // Confirm linking account by email
+ this.idpLinkEmailPage.assertCurrent();
+ Assert.assertThat(
+ this.idpLinkEmailPage.getMessage(),
+ is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+ );
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+ String linkFromMail = getVerificationEmailLink(message);
+
+ driver.navigate().to(linkFromMail.trim());
+
+ // authenticated and redirected to app. User is linked with identity provider
+ assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
+
+ // Assert user's email is verified now
+ UserModel user = getFederatedUser();
+ Assert.assertTrue(user.isEmailVerified());
+
+ // Attempt to use the link for the second time
+ driver.navigate().to(linkFromMail.trim());
+
+ infoPage.assertCurrent();
+ Assert.assertThat(infoPage.getInfo(), is("You are already logged in."));
+
+ // Log out
+ driver.navigate().to("http://localhost:8081/test-app/logout");
+
+ // Go to the same link again
+ driver.navigate().to(linkFromMail.trim());
+
+ infoPage.assertCurrent();
+ Assert.assertThat(infoPage.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login."));
+ }
+
+ /**
+ * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by email
+ */
+ @Test
+ public void testLinkAccountByEmailVerificationDifferentBrowser() throws Exception, Throwable {
+ setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF);
+
+ loginIDP("pedroigor");
+
+ this.idpConfirmLinkPage.assertCurrent();
+ Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
+ this.idpConfirmLinkPage.clickLinkAccount();
+
+ // Confirm linking account by email
+ this.idpLinkEmailPage.assertCurrent();
+ Assert.assertThat(
+ this.idpLinkEmailPage.getMessage(),
+ is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+ );
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+ String linkFromMail = getVerificationEmailLink(message);
+
+ WebRule webRule2 = new WebRule(this);
+ try {
+ webRule2.initProperties();
+
+ WebDriver driver2 = webRule2.getDriver();
+ InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
+
+ driver2.navigate().to(linkFromMail.trim());
+
+ // authenticated, but not redirected to app. Just seeing info page.
+ infoPage2.assertCurrent();
+ Assert.assertThat(infoPage2.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login."));
+ } finally {
+ // Revert everything
+ webRule2.after();
+ }
+
+ this.idpLinkEmailPage.clickContinueFlowLink();
+
+ // authenticated and redirected to app. User is linked with identity provider
+ assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
+
+ // Assert user's email is verified now
+ UserModel user = getFederatedUser();
+ Assert.assertTrue(user.isEmailVerified());
+ }
+
+ @Test
+ public void testLinkAccountByEmailVerificationResendEmail() throws Exception, Throwable {
+ setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF);
+
+ loginIDP("pedroigor");
+
+ this.idpConfirmLinkPage.assertCurrent();
+ Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
+ this.idpConfirmLinkPage.clickLinkAccount();
+
+ // Confirm linking account by email
+ this.idpLinkEmailPage.assertCurrent();
+ Assert.assertThat(
+ this.idpLinkEmailPage.getMessage(),
+ is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+ );
+
+ this.idpLinkEmailPage.clickResendEmail();
+
+ this.idpLinkEmailPage.assertCurrent();
+ Assert.assertThat(
+ this.idpLinkEmailPage.getMessage(),
+ is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+ );
+
+ Assert.assertEquals(2, greenMail.getReceivedMessages().length);
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+ String linkFromMail = getVerificationEmailLink(message);
+
+ driver.navigate().to(linkFromMail.trim());
+
+ // authenticated and redirected to app. User is linked with identity provider
+ assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
+
+ // Assert user's email is verified now
+ UserModel user = getFederatedUser();
+ Assert.assertTrue(user.isEmailVerified());
+ }
+
/**
* Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by reauthentication (confirm password on login screen)
@@ -361,6 +509,101 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
/**
+ * Variation of previous test, which uses browser buttons (back, refresh etc)
+ */
+ @Test
+ public void testLinkAccountByReauthenticationWithPassword_browserButtons() throws Exception {
+ // Remove smtp config. The reauthentication by username+password screen will be automatically used then
+ final Map<String, String> smtpConfig = new HashMap<>();
+ brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) {
+ setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_OFF);
+ smtpConfig.putAll(realmWithBroker.getSmtpConfig());
+ realmWithBroker.setSmtpConfig(Collections.<String, String>emptyMap());
+ }
+
+ }, APP_REALM_ID);
+
+
+ // Use invalid username for the first time
+ loginIDP("foo");
+ assertTrue(driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
+ this.loginPage.login("pedroigor", "password");
+
+
+ this.idpConfirmLinkPage.assertCurrent();
+ Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
+
+ // Click browser 'back' and then 'forward' and then continue
+ driver.navigate().back();
+ Assert.assertTrue(driver.getPageSource().contains("You are already logged in."));
+ driver.navigate().forward();
+ this.loginExpiredPage.assertCurrent();
+ this.loginExpiredPage.clickLoginContinueLink();
+ this.idpConfirmLinkPage.assertCurrent();
+
+ // Click browser 'back' on review profile page
+ this.idpConfirmLinkPage.clickReviewProfile();
+ this.updateProfilePage.assertCurrent();
+ driver.navigate().back();
+ this.loginExpiredPage.assertCurrent();
+ this.loginExpiredPage.clickLoginContinueLink();
+ this.updateProfilePage.assertCurrent();
+ this.updateProfilePage.update("Pedro", "Igor", "psilva@redhat.com");
+
+ this.idpConfirmLinkPage.assertCurrent();
+ this.idpConfirmLinkPage.clickLinkAccount();
+
+ // Login screen shown. Username is prefilled and disabled. Registration link and social buttons are not shown
+ Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle());
+ Assert.assertEquals("pedroigor", this.loginPage.getUsername());
+ Assert.assertFalse(this.loginPage.isUsernameInputEnabled());
+
+ Assert.assertEquals("Authenticate as pedroigor to link your account with " + getProviderId(), this.loginPage.getInfoMessage());
+
+ try {
+ this.loginPage.findSocialButton(getProviderId());
+ Assert.fail("Not expected to see social button with " + getProviderId());
+ } catch (NoSuchElementException expected) {
+ }
+
+ try {
+ this.loginPage.clickRegister();
+ Assert.fail("Not expected to see register link");
+ } catch (NoSuchElementException expected) {
+ }
+
+ // Use bad password first
+ this.loginPage.login("password1");
+ Assert.assertEquals("Invalid username or password.", this.loginPage.getError());
+
+ // Click browser 'back' and then continue
+ this.driver.navigate().back();
+ this.loginExpiredPage.assertCurrent();
+ this.loginExpiredPage.clickLoginContinueLink();
+
+ // Use correct password now
+ this.loginPage.login("password");
+
+ // authenticated and redirected to app. User is linked with identity provider
+ assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
+
+
+ // Restore smtp config
+ brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) {
+ realmWithBroker.setSmtpConfig(smtpConfig);
+ }
+
+ }, APP_REALM_ID);
+ }
+
+
+ /**
* Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by reauthentication (confirm password on login screen)
* and additionally he goes through "forget password"
*/
@@ -418,6 +661,96 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
}
+ /**
+ * Same like above, but "forget password" link is opened in different browser
+ */
+ @Test
+ public void testLinkAccountByReauthentication_forgetPassword_differentBrowser() throws Throwable {
+ brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) {
+ setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW,
+ IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.DISABLED);
+
+ setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_OFF);
+ }
+
+ }, APP_REALM_ID);
+
+ loginIDP("pedroigor");
+
+ this.idpConfirmLinkPage.assertCurrent();
+ Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
+ this.idpConfirmLinkPage.clickLinkAccount();
+
+ // Click "Forget password" on login page. Email sent directly because username is known
+ Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle());
+ this.loginPage.resetPassword();
+
+ Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle());
+ Assert.assertEquals("You should receive an email shortly with further instructions.", this.loginPage.getSuccessMessage());
+
+ // Click on link from email
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+ String linkFromMail = getVerificationEmailLink(message);
+
+ // Simulate 2nd browser
+ WebRule webRule2 = new WebRule(this);
+ try {
+ webRule2.initProperties();
+
+ WebDriver driver2 = webRule2.getDriver();
+ LoginPasswordUpdatePage passwordUpdatePage2 = webRule2.getPage(LoginPasswordUpdatePage.class);
+ InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
+
+ driver2.navigate().to(linkFromMail.trim());
+
+ // Need to update password now
+ passwordUpdatePage2.assertCurrent();
+ passwordUpdatePage2.changePassword("password", "password");
+
+ // authenticated, but not redirected to app. Just seeing info page.
+ infoPage2.assertCurrent();
+ Assert.assertEquals("Your account has been updated.", infoPage2.getInfo());
+ } finally {
+ // Revert everything
+ webRule2.after();
+ }
+
+ // User is not yet linked with identity provider. He needs to authenticate again in 1st browser
+ RealmModel realmWithBroker = getRealm();
+ Set<FederatedIdentityModel> federatedIdentities = this.session.users().getFederatedIdentities(this.session.users().getUserByUsername("pedroigor", realmWithBroker), realmWithBroker);
+ assertEquals(0, federatedIdentities.size());
+
+ // Continue with 1st browser. Note that the user has already authenticated with brokered IdP in the beginning of this test
+ // so entering their credentials there is now skipped.
+ loginToIDPWhenAlreadyLoggedIntoProviderIdP("pedroigor");
+
+ this.idpConfirmLinkPage.assertCurrent();
+ Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
+ this.idpConfirmLinkPage.clickLinkAccount();
+
+ Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle());
+ this.loginPage.login("password");
+
+ // authenticated and redirected to app. User is linked with identity provider
+ assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
+
+ brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) {
+ setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW,
+ IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.ALTERNATIVE);
+
+ }
+
+ }, APP_REALM_ID);
+ }
+
+
protected void assertFederatedUser(String expectedUsername, String expectedEmail, String expectedFederatedUsername) {
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app"));
UserModel federatedUser = getFederatedUser();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
index 8efc8c0..297d00a 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
@@ -37,14 +37,7 @@ import org.keycloak.testsuite.MailUtil;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.broker.util.UserSessionStatusServlet;
import org.keycloak.testsuite.broker.util.UserSessionStatusServlet.UserSessionStatus;
-import org.keycloak.testsuite.pages.AccountFederatedIdentityPage;
-import org.keycloak.testsuite.pages.AccountPasswordPage;
-import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
-import org.keycloak.testsuite.pages.ErrorPage;
-import org.keycloak.testsuite.pages.LoginPage;
-import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
-import org.keycloak.testsuite.pages.OAuthGrantPage;
-import org.keycloak.testsuite.pages.VerifyEmailPage;
+import org.keycloak.testsuite.pages.*;
import org.keycloak.testsuite.rule.GreenMailRule;
import org.keycloak.testsuite.rule.LoggingRule;
import org.keycloak.testsuite.rule.WebResource;
@@ -61,9 +54,8 @@ import java.net.URI;
import java.util.List;
import java.util.Set;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.*;
/**
* @author pedroigor
@@ -115,6 +107,9 @@ public abstract class AbstractIdentityProviderTest {
@WebResource
protected ErrorPage errorPage;
+ @WebResource
+ protected InfoPage infoPage;
+
protected KeycloakSession session;
protected int logoutTimeOffset = 0;
@@ -210,18 +205,29 @@ public abstract class AbstractIdentityProviderTest {
protected void loginIDP(String username) {
driver.navigate().to("http://localhost:8081/test-app");
- assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
+ assertThat(this.driver.getCurrentUrl(), startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
// choose the identity provider
this.loginPage.clickSocial(getProviderId());
String currentUrl = this.driver.getCurrentUrl();
- assertTrue(currentUrl.startsWith("http://localhost:8082/auth/"));
+ assertThat(currentUrl, startsWith("http://localhost:8082/auth/"));
// log in to identity provider
this.loginPage.login(username, "password");
doAfterProviderAuthentication();
}
+ protected void loginToIDPWhenAlreadyLoggedIntoProviderIdP(String username) {
+ driver.navigate().to("http://localhost:8081/test-app");
+
+ assertThat(this.driver.getCurrentUrl(), startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
+
+ // choose the identity provider
+ this.loginPage.clickSocial(getProviderId());
+
+ doAfterProviderAuthentication();
+ }
+
protected UserModel getFederatedUser() {
UserSessionStatus userSessionStatus = retrieveSessionStatus();
IDToken idToken = userSessionStatus.getIdToken();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java
index b047595..781714a 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java
@@ -69,7 +69,7 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent
}
@Test
- public void testDisabledUser() {
+ public void testDisabledUser() throws Exception {
KeycloakSession session = brokerServerRule.startSession();
setUpdateProfileFirstLogin(session.realms().getRealmByName("realm-with-broker"), IdentityProviderRepresentation.UPFLM_OFF);
brokerServerRule.stopSession(session, true);
@@ -328,7 +328,7 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent
}
@Test
- public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() {
+ public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() throws Exception {
RealmModel realm = getRealm();
realm.setRegistrationEmailAsUsername(true);
setUpdateProfileFirstLogin(realm, IdentityProviderRepresentation.UPFLM_OFF);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
index 58c4ca1..c8e9d9b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java
@@ -116,7 +116,7 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
}
@Test
- public void testDisabledUser() {
+ public void testDisabledUser() throws Exception {
super.testDisabledUser();
}
@@ -156,7 +156,7 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
}
@Test
- public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() {
+ public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() throws Exception {
super.testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java
index 078666f..b2ecd41 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java
@@ -117,11 +117,9 @@ public class OIDCKeycloakServerBrokerWithConsentTest extends AbstractIdentityPro
grantPage.assertCurrent();
grantPage.cancel();
- // Assert error page with backToApplication link displayed
- errorPage.assertCurrent();
- errorPage.clickBackToApplication();
-
- assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
+ // Assert login page with "You took too long to login..." message
+ assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/login-actions/authenticate"));
+ Assert.assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
} finally {
Time.setOffset(0);
@@ -143,6 +141,7 @@ public class OIDCKeycloakServerBrokerWithConsentTest extends AbstractIdentityPro
this.session = brokerServerRule.startSession();
session.sessions().removeExpired(getRealm());
+ session.authenticationSessions().removeExpired(getRealm());
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();
@@ -151,14 +150,9 @@ public class OIDCKeycloakServerBrokerWithConsentTest extends AbstractIdentityPro
grantPage.assertCurrent();
grantPage.cancel();
- // Assert error page without backToApplication link (clientSession expired and was removed on the server)
- errorPage.assertCurrent();
- try {
- errorPage.clickBackToApplication();
- fail("Not expected to have link backToApplication available");
- } catch (NoSuchElementException nsee) {
- // Expected;
- }
+ // Assert login page with "You took too long to login..." message
+ assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/login-actions/authenticate"));
+ Assert.assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
} finally {
Time.setOffset(0);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java
index 200b0a7..234617b 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java
@@ -23,6 +23,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
+import org.junit.Test;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java
index 3b83f03..8afc49b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java
@@ -128,7 +128,7 @@ public class SAMLKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
}
@Test
- public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() {
+ public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() throws Exception {
super.testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java
index 0ebc095..8c38363 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java
@@ -27,6 +27,7 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher;
import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
import org.jboss.resteasy.spi.ResteasyDeployment;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
@@ -37,12 +38,15 @@ import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.testsuite.util.cli.TestsuiteCLI;
import org.keycloak.util.JsonSerialization;
+import org.mvel2.util.Make;
import javax.servlet.DispatcherType;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Properties;
/**
@@ -187,6 +191,8 @@ public class KeycloakServer {
config.setWorkerThreads(undertowWorkerThreads);
}
+ detectNodeName(config);
+
final KeycloakServer keycloak = new KeycloakServer(config);
keycloak.sysout = true;
keycloak.start();
@@ -369,4 +375,24 @@ public class KeycloakServer {
return new File(s.toString());
}
+
+ private static void detectNodeName(KeycloakServerConfig config) {
+ String nodeName = System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME);
+ if (nodeName == null) {
+ // Try to autodetect "jboss.node.name" from the port
+ Map<Integer, String> nodesCfg = new HashMap<>();
+ nodesCfg.put(8181, "node1");
+ nodesCfg.put(8182, "node2");
+
+ nodeName = nodesCfg.get(config.getPort());
+ if (nodeName != null) {
+ System.setProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME, nodeName);
+ }
+ }
+
+ if (nodeName != null) {
+ log.infof("Node name: %s", nodeName);
+ }
+ }
+
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java
new file mode 100644
index 0000000..db08e81
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.model;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserManager;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.managers.ClientManager;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.CommonClientSessionModel;
+import org.keycloak.testsuite.rule.KeycloakRule;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AuthenticationSessionProviderTest {
+
+ @ClassRule
+ public static KeycloakRule kc = new KeycloakRule();
+
+ private KeycloakSession session;
+ private RealmModel realm;
+
+ @Before
+ public void before() {
+ session = kc.startSession();
+ realm = session.realms().getRealm("test");
+ session.users().addUser(realm, "user1").setEmail("user1@localhost");
+ session.users().addUser(realm, "user2").setEmail("user2@localhost");
+ }
+
+ @After
+ public void after() {
+ resetSession();
+ UserModel user1 = session.users().getUserByUsername("user1", realm);
+ UserModel user2 = session.users().getUserByUsername("user2", realm);
+
+ UserManager um = new UserManager(session);
+ if (user1 != null) {
+ um.removeUser(realm, user1);
+ }
+ if (user2 != null) {
+ um.removeUser(realm, user2);
+ }
+ kc.stopSession(session, true);
+ }
+
+ private void resetSession() {
+ kc.stopSession(session, true);
+ session = kc.startSession();
+ realm = session.realms().getRealm("test");
+ }
+
+ @Test
+ public void testLoginSessionsCRUD() {
+ ClientModel client1 = realm.getClientByClientId("test-app");
+ UserModel user1 = session.users().getUserByUsername("user1", realm);
+
+ AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1);
+
+ authSession.setAction("foo");
+ authSession.setTimestamp(100);
+
+ resetSession();
+
+ // Ensure session is here
+ authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
+ testAuthenticationSession(authSession, client1.getId(), null, "foo");
+ Assert.assertEquals(100, authSession.getTimestamp());
+
+ // Update and commit
+ authSession.setAction("foo-updated");
+ authSession.setTimestamp(200);
+ authSession.setAuthenticatedUser(session.users().getUserByUsername("user1", realm));
+
+ resetSession();
+
+ // Ensure session was updated
+ authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
+ testAuthenticationSession(authSession, client1.getId(), user1.getId(), "foo-updated");
+ Assert.assertEquals(200, authSession.getTimestamp());
+
+ // Remove and commit
+ session.authenticationSessions().removeAuthenticationSession(realm, authSession);
+
+ resetSession();
+
+ // Ensure session was removed
+ Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSession.getId()));
+
+ }
+
+ @Test
+ public void testAuthenticationSessionRestart() {
+ ClientModel client1 = realm.getClientByClientId("test-app");
+ UserModel user1 = session.users().getUserByUsername("user1", realm);
+
+ AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1);
+
+ authSession.setAction("foo");
+ authSession.setTimestamp(100);
+
+ authSession.setAuthenticatedUser(user1);
+ authSession.setAuthNote("foo", "bar");
+ authSession.setClientNote("foo2", "bar2");
+ authSession.setExecutionStatus("123", CommonClientSessionModel.ExecutionStatus.SUCCESS);
+
+ resetSession();
+
+ client1 = realm.getClientByClientId("test-app");
+ authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
+ authSession.restartSession(realm, client1);
+
+ resetSession();
+
+ authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
+ testAuthenticationSession(authSession, client1.getId(), null, null);
+ Assert.assertTrue(authSession.getTimestamp() > 0);
+
+ Assert.assertTrue(authSession.getClientNotes().isEmpty());
+ Assert.assertNull(authSession.getAuthNote("foo2"));
+ Assert.assertTrue(authSession.getExecutionStatus().isEmpty());
+
+ }
+
+
+ @Test
+ public void testExpiredAuthSessions() {
+ try {
+ realm.setAccessCodeLifespan(10);
+ realm.setAccessCodeLifespanUserAction(10);
+ realm.setAccessCodeLifespanLogin(30);
+
+ // Login lifespan is largest
+ String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
+ resetSession();
+
+ Time.setOffset(25);
+ session.authenticationSessions().removeExpired(realm);
+ resetSession();
+
+ assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+
+ Time.setOffset(35);
+ session.authenticationSessions().removeExpired(realm);
+ resetSession();
+
+ assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+
+ // User action is largest
+ realm.setAccessCodeLifespanUserAction(40);
+
+ Time.setOffset(0);
+ authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
+ resetSession();
+
+ Time.setOffset(35);
+ session.authenticationSessions().removeExpired(realm);
+ resetSession();
+
+ assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+
+ Time.setOffset(45);
+ session.authenticationSessions().removeExpired(realm);
+ resetSession();
+
+ assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+
+ // Access code is largest
+ realm.setAccessCodeLifespan(50);
+
+ Time.setOffset(0);
+ authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
+ resetSession();
+
+ Time.setOffset(45);
+ session.authenticationSessions().removeExpired(realm);
+ resetSession();
+
+ assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+
+ Time.setOffset(55);
+ session.authenticationSessions().removeExpired(realm);
+ resetSession();
+
+ assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
+ } finally {
+ Time.setOffset(0);
+
+ realm.setAccessCodeLifespan(60);
+ realm.setAccessCodeLifespanUserAction(300);
+ realm.setAccessCodeLifespanLogin(1800);
+
+ }
+ }
+
+
+ @Test
+ public void testOnRealmRemoved() {
+ RealmModel fooRealm = session.realms().createRealm("foo-realm");
+ ClientModel fooClient = fooRealm.addClient("foo-client");
+
+ String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
+ String authSessionId2 = session.authenticationSessions().createAuthenticationSession(fooRealm, fooClient).getId();
+
+ resetSession();
+
+ new RealmManager(session).removeRealm(session.realms().getRealmByName("foo-realm"));
+
+ resetSession();
+
+ AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
+ testAuthenticationSession(authSession, realm.getClientByClientId("test-app").getId(), null, null);
+ Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId2));
+ }
+
+ @Test
+ public void testOnClientRemoved() {
+ String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
+ String authSessionId2 = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("third-party")).getId();
+
+ String testAppClientUUID = realm.getClientByClientId("test-app").getId();
+
+ resetSession();
+
+ new ClientManager(new RealmManager(session)).removeClient(realm, realm.getClientByClientId("third-party"));
+
+ resetSession();
+
+ AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
+ testAuthenticationSession(authSession, testAppClientUUID, null, null);
+ Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId2));
+
+ // Revert client
+ realm.addClient("third-party");
+ }
+
+
+ private void testAuthenticationSession(AuthenticationSessionModel authSession, String expectedClientId, String expectedUserId, String expectedAction) {
+ Assert.assertEquals(expectedClientId, authSession.getClient().getId());
+
+ if (expectedUserId == null) {
+ Assert.assertNull(authSession.getAuthenticatedUser());
+ } else {
+ Assert.assertEquals(expectedUserId, authSession.getAuthenticatedUser().getId());
+ }
+
+ if (expectedAction == null) {
+ Assert.assertNull(authSession.getAction());
+ } else {
+ Assert.assertEquals(expectedAction, authSession.getAction());
+ }
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java
index abbf912..68746c0 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java
@@ -103,7 +103,7 @@ public class CacheTest {
user.setFirstName("firstName");
user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
- UserSessionModel userSession = session.sessions().createUserSession(realm, user, "testAddUserNotAddedToCache", "127.0.0.1", "auth", false, null, null);
+ UserSessionModel userSession = session.sessions().createUserSession("123", realm, user, "testAddUserNotAddedToCache", "127.0.0.1", "auth", false, null, null);
UserModel user2 = userSession.getUser();
user.setLastName("lastName");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java
index 7b6de1b..66894f6 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java
@@ -75,7 +75,7 @@ public class ClusterSessionCleanerTest {
RealmModel realm1 = session1.realms().getRealmByName(REALM_NAME);
UserModel user1 = session1.users().getUserByUsername("test-user@localhost", realm1);
for (int i=0 ; i<15 ; i++) {
- session1.sessions().createUserSession(realm1, user1, user1.getUsername(), "127.0.0.1", "form", true, null, null);
+ session1.sessions().createUserSession("123", realm1, user1, user1.getUsername(), "127.0.0.1", "form", true, null, null);
}
session1 = commit(server1, session1);
@@ -87,7 +87,7 @@ public class ClusterSessionCleanerTest {
Assert.assertEquals(user2.getId(), user1.getId());
for (int i=0 ; i<15 ; i++) {
- session2.sessions().createUserSession(realm2, user2, user2.getUsername(), "127.0.0.1", "form", true, null, null);
+ session2.sessions().createUserSession("456", realm2, user2, user2.getUsername(), "127.0.0.1", "form", true, null, null);
}
session2 = commit(server2, session2);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
index 2db24ad..8c01e2c 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
@@ -25,14 +25,15 @@ import org.junit.Test;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionProviderFactory;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.models.UserManager;
import org.keycloak.services.managers.UserSessionManager;
@@ -47,6 +48,7 @@ import java.util.Set;
*/
public class UserSessionInitializerTest {
+
@ClassRule
public static KeycloakRule kc = new KeycloakRule();
@@ -88,7 +90,7 @@ public class UserSessionInitializerTest {
for (UserSessionModel origSession : origSessions) {
UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId());
- for (ClientSessionModel clientSession : userSession.getClientSessions()) {
+ for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
}
}
@@ -128,8 +130,8 @@ public class UserSessionInitializerTest {
UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, serverStartTime, "test-app");
}
- private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
- ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
+ private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
+ AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession);
if (userSession != null) clientSession.setUserSession(userSession);
clientSession.setRedirectUri(redirect);
if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
@@ -140,7 +142,7 @@ public class UserSessionInitializerTest {
private UserSessionModel[] createSessions() {
UserSessionModel[] sessions = new UserSessionModel[3];
- sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
+ sessions[0] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
Set<String> roles = new HashSet<String>();
roles.add("one");
@@ -153,10 +155,10 @@ public class UserSessionInitializerTest {
createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers);
createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
- sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+ sessions[1] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
- sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
+ sessions[2] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
resetSession();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
index 60d92d9..8c89046 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
@@ -23,13 +23,14 @@ import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.common.util.Time;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.session.UserSessionPersisterProvider;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
@@ -46,6 +47,7 @@ import java.util.Set;
*/
public class UserSessionPersisterProviderTest {
+
@ClassRule
public static KeycloakRule kc = new KeycloakRule();
@@ -151,7 +153,7 @@ public class UserSessionPersisterProviderTest {
int clientSessionsCount = 0;
for (UserSessionModel loadedSession : loadedSessions) {
Assert.assertEquals(expectedTime, loadedSession.getLastSessionRefresh());
- for (ClientSessionModel clientSession : loadedSession.getClientSessions()) {
+ for (AuthenticatedClientSessionModel clientSession : loadedSession.getAuthenticatedClientSessions().values()) {
Assert.assertEquals(expectedTime, clientSession.getTimestamp());
clientSessionsCount++;
}
@@ -183,11 +185,11 @@ public class UserSessionPersisterProviderTest {
try {
persistedSession.setLastSessionRefresh(Time.currentTime());
persistedSession.setNote("foo", "bar");
- persistedSession.setState(UserSessionModel.State.LOGGING_IN);
+ persistedSession.setState(UserSessionModel.State.LOGGED_IN);
persister.updateUserSession(persistedSession, true);
// create new clientSession
- ClientSessionModel clientSession = createClientSession(realm.getClientByClientId("third-party"), session.sessions().getUserSession(realm, persistedSession.getId()),
+ AuthenticatedClientSessionModel clientSession = createClientSession(realm.getClientByClientId("third-party"), session.sessions().getUserSession(realm, persistedSession.getId()),
"http://redirect", "state", new HashSet<String>(), new HashSet<String>());
persister.createClientSession(clientSession, true);
@@ -198,10 +200,10 @@ public class UserSessionPersisterProviderTest {
persistedSession = loadedSessions.get(0);
UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started+10, "test-app", "third-party");
Assert.assertEquals("bar", persistedSession.getNote("foo"));
- Assert.assertEquals(UserSessionModel.State.LOGGING_IN, persistedSession.getState());
+ Assert.assertEquals(UserSessionModel.State.LOGGED_IN, persistedSession.getState());
// Remove clientSession
- persister.removeClientSession(clientSession.getId(), true);
+ persister.removeClientSession(userSession.getId(), realm.getClientByClientId("third-party").getId(), true);
resetSession();
@@ -228,7 +230,7 @@ public class UserSessionPersisterProviderTest {
fooRealm.addClient("foo-app");
session.users().addUser(fooRealm, "user3");
- UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
+ UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
resetSession();
@@ -262,7 +264,7 @@ public class UserSessionPersisterProviderTest {
fooRealm.addClient("bar-app");
session.users().addUser(fooRealm, "user3");
- UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
+ UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
createClientSession(fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
@@ -358,8 +360,8 @@ public class UserSessionPersisterProviderTest {
}
- private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
- ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
+ private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
+ AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession);
if (userSession != null) clientSession.setUserSession(userSession);
clientSession.setRedirectUri(redirect);
if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
@@ -370,7 +372,7 @@ public class UserSessionPersisterProviderTest {
private UserSessionModel[] createSessions() {
UserSessionModel[] sessions = new UserSessionModel[3];
- sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
+ sessions[0] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
Set<String> roles = new HashSet<String>();
roles.add("one");
@@ -383,10 +385,10 @@ public class UserSessionPersisterProviderTest {
createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers);
createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
- sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+ sessions[1] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
- sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
+ sessions[2] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
return sessions;
@@ -394,7 +396,7 @@ public class UserSessionPersisterProviderTest {
private void persistUserSession(UserSessionModel userSession, boolean offline) {
persister.createUserSession(userSession, offline);
- for (ClientSessionModel clientSession : userSession.getClientSessions()) {
+ for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
persister.createClientSession(clientSession, offline);
}
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java
index fb4b3af..106f525 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java
@@ -24,13 +24,14 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.common.util.Time;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.session.UserSessionPersisterProvider;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
@@ -41,7 +42,6 @@ import org.keycloak.testsuite.rule.LoggingRule;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -94,29 +94,23 @@ public class UserSessionProviderOfflineTest {
resetSession();
- Map<String, String> offlineSessions = new HashMap<>();
+ // Key is userSession ID, values are client UUIDS
+ Map<String, Set<String>> offlineSessions = new HashMap<>();
// Persist 3 created userSessions and clientSessions as offline
ClientModel testApp = realm.getClientByClientId("test-app");
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, testApp);
for (UserSessionModel userSession : userSessions) {
- offlineSessions.putAll(createOfflineSessionIncludeClientSessions(userSession));
+ offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(userSession));
}
resetSession();
// Assert all previously saved offline sessions found
- for (Map.Entry<String, String> entry : offlineSessions.entrySet()) {
- Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey()) != null);
-
- UserSessionModel offlineSession = session.sessions().getUserSession(realm, entry.getValue());
- boolean found = false;
- for (ClientSessionModel clientSession : offlineSession.getClientSessions()) {
- if (clientSession.getId().equals(entry.getKey())) {
- found = true;
- }
- }
- Assert.assertTrue(found);
+ for (Map.Entry<String, Set<String>> entry : offlineSessions.entrySet()) {
+ UserSessionModel offlineSession = sessionManager.findOfflineUserSession(realm, entry.getKey());
+ Assert.assertNotNull(offlineSession);
+ Assert.assertEquals(offlineSession.getAuthenticatedClientSessions().keySet(), entry.getValue());
}
// Find clients with offline token
@@ -174,23 +168,23 @@ public class UserSessionProviderOfflineTest {
fooRealm.addClient("foo-app");
session.users().addUser(fooRealm, "user3");
- UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
- ClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+ UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
+ AuthenticatedClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
resetSession();
// Persist offline session
fooRealm = session.realms().getRealm("foo");
userSession = session.sessions().getUserSession(fooRealm, userSession.getId());
- clientSession = session.sessions().getClientSession(fooRealm, clientSession.getId());
- sessionManager.createOrUpdateOfflineSession(userSession.getClientSessions().get(0), userSession);
+ createOfflineSessionIncludeClientSessions(userSession);
resetSession();
- ClientSessionModel offlineClientSession = sessionManager.findOfflineClientSession(fooRealm, clientSession.getId());
+ UserSessionModel offlineUserSession = sessionManager.findOfflineUserSession(fooRealm, userSession.getId());
+ Assert.assertEquals(offlineUserSession.getAuthenticatedClientSessions().size(), 1);
+ AuthenticatedClientSessionModel offlineClientSession = offlineUserSession.getAuthenticatedClientSessions().values().iterator().next();
Assert.assertEquals("foo-app", offlineClientSession.getClient().getClientId());
Assert.assertEquals("user3", offlineClientSession.getUserSession().getUser().getUsername());
- Assert.assertEquals(offlineClientSession.getId(), offlineClientSession.getUserSession().getClientSessions().get(0).getId());
// Remove realm
RealmManager realmMgr = new RealmManager(session);
@@ -206,7 +200,7 @@ public class UserSessionProviderOfflineTest {
// Assert nothing loaded
fooRealm = session.realms().getRealm("foo");
- Assert.assertNull(sessionManager.findOfflineClientSession(fooRealm, clientSession.getId()));
+ Assert.assertNull(sessionManager.findOfflineUserSession(fooRealm, userSession.getId()));
Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(fooRealm, fooRealm.getClientByClientId("foo-app")));
// Cleanup
@@ -223,7 +217,7 @@ public class UserSessionProviderOfflineTest {
fooRealm.addClient("bar-app");
session.users().addUser(fooRealm, "user3");
- UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
+ UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
createClientSession(fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
@@ -256,8 +250,8 @@ public class UserSessionProviderOfflineTest {
// Assert just one bar-app clientSession persisted now
offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId());
- Assert.assertEquals(1, offlineSession.getClientSessions().size());
- Assert.assertEquals("bar-app", offlineSession.getClientSessions().get(0).getClient().getClientId());
+ Assert.assertEquals(1, offlineSession.getAuthenticatedClientSessions().size());
+ Assert.assertEquals("bar-app", offlineSession.getAuthenticatedClientSessions().values().iterator().next().getClient().getClientId());
// Remove bar-app client
client = fooRealm.getClientByClientId("bar-app");
@@ -266,8 +260,10 @@ public class UserSessionProviderOfflineTest {
resetSession();
// Assert nothing loaded - userSession was removed as well because it was last userSession
+ realmMgr = new RealmManager(session);
+ fooRealm = realmMgr.getRealm("foo");
offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId());
- Assert.assertEquals(0, offlineSession.getClientSessions().size());
+ Assert.assertEquals(0, offlineSession.getAuthenticatedClientSessions().size());
// Cleanup
realmMgr = new RealmManager(session);
@@ -282,8 +278,8 @@ public class UserSessionProviderOfflineTest {
fooRealm.addClient("foo-app");
session.users().addUser(fooRealm, "user3");
- UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
- ClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+ UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
+ AuthenticatedClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
resetSession();
@@ -309,7 +305,6 @@ public class UserSessionProviderOfflineTest {
// Assert userSession removed as well
Assert.assertNull(session.sessions().getOfflineUserSession(fooRealm, userSession.getId()));
- Assert.assertNull(session.sessions().getOfflineClientSession(fooRealm, clientSession.getId()));
// Cleanup
realmMgr = new RealmManager(session);
@@ -325,33 +320,26 @@ public class UserSessionProviderOfflineTest {
resetSession();
- Map<String, String> offlineSessions = new HashMap<>();
+ // Key is userSessionId, value is set of client UUIDS
+ Map<String, Set<String>> offlineSessions = new HashMap<>();
// Persist 3 created userSessions and clientSessions as offline
ClientModel testApp = realm.getClientByClientId("test-app");
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, testApp);
for (UserSessionModel userSession : userSessions) {
- offlineSessions.putAll(createOfflineSessionIncludeClientSessions(userSession));
+ offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(userSession));
}
resetSession();
// Assert all previously saved offline sessions found
- for (Map.Entry<String, String> entry : offlineSessions.entrySet()) {
- Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey()) != null);
+ for (Map.Entry<String, Set<String>> entry : offlineSessions.entrySet()) {
+ UserSessionModel foundSession = sessionManager.findOfflineUserSession(realm, entry.getKey());
+ Assert.assertEquals(foundSession.getAuthenticatedClientSessions().keySet(), entry.getValue());
}
UserSessionModel session0 = session.sessions().getOfflineUserSession(realm, origSessions[0].getId());
Assert.assertNotNull(session0);
- List<String> clientSessions = new LinkedList<>();
- for (ClientSessionModel clientSession : session0.getClientSessions()) {
- clientSessions.add(clientSession.getId());
- Assert.assertNotNull(session.sessions().getOfflineClientSession(realm, clientSession.getId()));
- }
-
- UserSessionModel session1 = session.sessions().getOfflineUserSession(realm, origSessions[1].getId());
- Assert.assertEquals(1, session1.getClientSessions().size());
- ClientSessionModel cls1 = session1.getClientSessions().get(0);
// sessions are in persister too
Assert.assertEquals(3, persister.getUserSessionsCount(true));
@@ -359,9 +347,6 @@ public class UserSessionProviderOfflineTest {
// Set lastSessionRefresh to session[0] to 0
session0.setLastSessionRefresh(0);
- // Set timestamp to cls1 to 0
- cls1.setTimestamp(0);
-
resetSession();
session.sessions().removeExpired(realm);
@@ -370,21 +355,8 @@ public class UserSessionProviderOfflineTest {
// assert session0 not found now
Assert.assertNull(session.sessions().getOfflineUserSession(realm, origSessions[0].getId()));
- for (String clientSession : clientSessions) {
- Assert.assertNull(session.sessions().getOfflineClientSession(realm, origSessions[0].getId()));
- offlineSessions.remove(clientSession);
- }
- // Assert cls1 not found too
- for (Map.Entry<String, String> entry : offlineSessions.entrySet()) {
- String userSessionId = entry.getValue();
- if (userSessionId.equals(session1.getId())) {
- Assert.assertFalse(sessionManager.findOfflineClientSession(realm, entry.getKey()) != null);
- } else {
- Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey()) != null);
- }
- }
- Assert.assertEquals(1, persister.getUserSessionsCount(true));
+ Assert.assertEquals(2, persister.getUserSessionsCount(true));
// Expire everything and assert nothing found
Time.setOffset(3000000);
@@ -393,8 +365,8 @@ public class UserSessionProviderOfflineTest {
resetSession();
- for (Map.Entry<String, String> entry : offlineSessions.entrySet()) {
- Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey()) == null);
+ for (String userSessionId : offlineSessions.keySet()) {
+ Assert.assertNull(sessionManager.findOfflineUserSession(realm, userSessionId));
}
Assert.assertEquals(0, persister.getUserSessionsCount(true));
@@ -403,18 +375,17 @@ public class UserSessionProviderOfflineTest {
}
}
- private Map<String, String> createOfflineSessionIncludeClientSessions(UserSessionModel userSession) {
- Map<String, String> offlineSessions = new HashMap<>();
+ private Set<String> createOfflineSessionIncludeClientSessions(UserSessionModel userSession) {
+ Set<String> offlineSessions = new HashSet<>();
- for (ClientSessionModel clientSession : userSession.getClientSessions()) {
+ for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
- offlineSessions.put(clientSession.getId(), userSession.getId());
+ offlineSessions.add(clientSession.getClient().getId());
}
return offlineSessions;
}
-
private void resetSession() {
kc.stopSession(session, true);
session = kc.startSession();
@@ -423,8 +394,8 @@ public class UserSessionProviderOfflineTest {
persister = session.getProvider(UserSessionPersisterProvider.class);
}
- private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
- ClientSessionModel clientSession = session.sessions().createClientSession(client.getRealm(), client);
+ private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
+ AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(client.getRealm(), client, userSession);
if (userSession != null) clientSession.setUserSession(userSession);
clientSession.setRedirectUri(redirect);
if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
@@ -435,7 +406,7 @@ public class UserSessionProviderOfflineTest {
private UserSessionModel[] createSessions() {
UserSessionModel[] sessions = new UserSessionModel[3];
- sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
+ sessions[0] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
Set<String> roles = new HashSet<String>();
roles.add("one");
@@ -448,10 +419,10 @@ public class UserSessionProviderOfflineTest {
createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers);
createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
- sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+ sessions[1] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
- sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
+ sessions[2] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
return sessions;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
index 824200d..7d6745e 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
@@ -23,21 +23,23 @@ import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.common.util.Time;
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.models.UserManager;
import org.keycloak.testsuite.rule.KeycloakRule;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertArrayEquals;
@@ -104,21 +106,35 @@ public class UserSessionProviderTest {
}
@Test
+ public void testRestartSession() {
+ int started = Time.currentTime();
+ UserSessionModel[] sessions = createSessions();
+
+ Time.setOffset(100);
+
+ UserSessionModel userSession = session.sessions().getUserSession(realm, sessions[0].getId());
+ assertSession(userSession, session.users().getUserByUsername("user1", realm), "127.0.0.1", started, started, "test-app", "third-party");
+
+ userSession.restartSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.6", "form", true, null, null);
+
+ resetSession();
+
+ userSession = session.sessions().getUserSession(realm, sessions[0].getId());
+ assertSession(userSession, session.users().getUserByUsername("user2", realm), "127.0.0.6", started + 100, started + 100);
+
+ Time.setOffset(0);
+ }
+
+ @Test
public void testCreateClientSession() {
UserSessionModel[] sessions = createSessions();
- List<ClientSessionModel> clientSessions = session.sessions().getUserSession(realm, sessions[0].getId()).getClientSessions();
+ Map<String, AuthenticatedClientSessionModel> clientSessions = session.sessions().getUserSession(realm, sessions[0].getId()).getAuthenticatedClientSessions();
assertEquals(2, clientSessions.size());
- String client1 = realm.getClientByClientId("test-app").getId();
+ String clientUUID = realm.getClientByClientId("test-app").getId();
- ClientSessionModel session1;
-
- if (clientSessions.get(0).getClient().getId().equals(client1)) {
- session1 = clientSessions.get(0);
- } else {
- session1 = clientSessions.get(1);
- }
+ AuthenticatedClientSessionModel session1 = clientSessions.get(clientUUID);
assertEquals(null, session1.getAction());
assertEquals(realm.getClientByClientId("test-app").getClientId(), session1.getClient().getClientId());
@@ -137,21 +153,22 @@ public class UserSessionProviderTest {
public void testUpdateClientSession() {
UserSessionModel[] sessions = createSessions();
- String id = sessions[0].getClientSessions().get(0).getId();
+ String userSessionId = sessions[0].getId();
+ String clientUUID = realm.getClientByClientId("test-app").getId();
- ClientSessionModel clientSession = session.sessions().getClientSession(realm, id);
+ AuthenticatedClientSessionModel clientSession = sessions[0].getAuthenticatedClientSessions().get(clientUUID);
int time = clientSession.getTimestamp();
assertEquals(null, clientSession.getAction());
- clientSession.setAction(ClientSessionModel.Action.CODE_TO_TOKEN.name());
+ clientSession.setAction(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name());
clientSession.setTimestamp(time + 10);
kc.stopSession(session, true);
session = kc.startSession();
- ClientSessionModel updated = session.sessions().getClientSession(realm, id);
- assertEquals(ClientSessionModel.Action.CODE_TO_TOKEN.name(), updated.getAction());
+ AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessions().get(clientUUID);
+ assertEquals(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), updated.getAction());
assertEquals(time + 10, updated.getTimestamp());
}
@@ -167,17 +184,12 @@ public class UserSessionProviderTest {
public void testRemoveUserSessionsByUser() {
UserSessionModel[] sessions = createSessions();
- List<String> clientSessionsRemoved = new LinkedList<String>();
- List<String> clientSessionsKept = new LinkedList<String>();
+ Map<String, Integer> clientSessionsKept = new HashMap<>();
for (UserSessionModel s : sessions) {
s = session.sessions().getUserSession(realm, s.getId());
- for (ClientSessionModel c : s.getClientSessions()) {
- if (c.getUserSession().getUser().getUsername().equals("user1")) {
- clientSessionsRemoved.add(c.getId());
- } else {
- clientSessionsKept.add(c.getId());
- }
+ if (!s.getUser().getUsername().equals("user1")) {
+ clientSessionsKept.put(s.getId(), s.getAuthenticatedClientSessions().keySet().size());
}
}
@@ -185,13 +197,12 @@ public class UserSessionProviderTest {
resetSession();
assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm)).isEmpty());
- assertFalse(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty());
+ List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm));
+ assertFalse(userSessions.isEmpty());
- for (String c : clientSessionsRemoved) {
- assertNull(session.sessions().getClientSession(realm, c));
- }
- for (String c : clientSessionsKept) {
- assertNotNull(session.sessions().getClientSession(realm, c));
+ Assert.assertEquals(userSessions.size(), clientSessionsKept.size());
+ for (UserSessionModel userSession : userSessions) {
+ Assert.assertEquals((int) clientSessionsKept.get(userSession.getId()), userSession.getAuthenticatedClientSessions().size());
}
}
@@ -199,76 +210,47 @@ public class UserSessionProviderTest {
public void testRemoveUserSession() {
UserSessionModel userSession = createSessions()[0];
- List<String> clientSessionsRemoved = new LinkedList<String>();
- for (ClientSessionModel c : userSession.getClientSessions()) {
- clientSessionsRemoved.add(c.getId());
- }
-
session.sessions().removeUserSession(realm, userSession);
resetSession();
assertNull(session.sessions().getUserSession(realm, userSession.getId()));
- for (String c : clientSessionsRemoved) {
- assertNull(session.sessions().getClientSession(realm, c));
- }
}
@Test
public void testRemoveUserSessionsByRealm() {
UserSessionModel[] sessions = createSessions();
- List<ClientSessionModel> clientSessions = new LinkedList<ClientSessionModel>();
- for (UserSessionModel s : sessions) {
- clientSessions.addAll(s.getClientSessions());
- }
-
session.sessions().removeUserSessions(realm);
resetSession();
assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm)).isEmpty());
assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty());
-
- for (ClientSessionModel c : clientSessions) {
- assertNull(session.sessions().getClientSession(realm, c.getId()));
- }
}
@Test
public void testOnClientRemoved() {
UserSessionModel[] sessions = createSessions();
- List<String> clientSessionsRemoved = new LinkedList<String>();
- List<String> clientSessionsKept = new LinkedList<String>();
+ String thirdPartyClientUUID = realm.getClientByClientId("third-party").getId();
+
+ Map<String, Set<String>> clientSessionsKept = new HashMap<>();
for (UserSessionModel s : sessions) {
- s = session.sessions().getUserSession(realm, s.getId());
- for (ClientSessionModel c : s.getClientSessions()) {
- if (c.getClient().getClientId().equals("third-party")) {
- clientSessionsRemoved.add(c.getId());
- } else {
- clientSessionsKept.add(c.getId());
- }
- }
+ Set<String> clientUUIDS = new HashSet<>(s.getAuthenticatedClientSessions().keySet());
+ clientUUIDS.remove(thirdPartyClientUUID); // This client will be later removed, hence his clientSessions too
+ clientSessionsKept.put(s.getId(), clientUUIDS);
}
- session.sessions().onClientRemoved(realm, realm.getClientByClientId("third-party"));
+ realm.removeClient(thirdPartyClientUUID);
resetSession();
- for (String c : clientSessionsRemoved) {
- assertNull(session.sessions().getClientSession(realm, c));
- }
- for (String c : clientSessionsKept) {
- assertNotNull(session.sessions().getClientSession(realm, c));
+ for (UserSessionModel s : sessions) {
+ s = session.sessions().getUserSession(realm, s.getId());
+ Set<String> clientUUIDS = s.getAuthenticatedClientSessions().keySet();
+ assertEquals(clientUUIDS, clientSessionsKept.get(s.getId()));
}
- session.sessions().onClientRemoved(realm, realm.getClientByClientId("test-app"));
- resetSession();
-
- for (String c : clientSessionsRemoved) {
- assertNull(session.sessions().getClientSession(realm, c));
- }
- for (String c : clientSessionsKept) {
- assertNull(session.sessions().getClientSession(realm, c));
- }
+ // Revert client
+ realm.addClient("third-party");
}
@Test
@@ -278,27 +260,25 @@ public class UserSessionProviderTest {
try {
Set<String> expired = new HashSet<String>();
- Set<String> expiredClientSessions = new HashSet<String>();
Time.setOffset(-(realm.getSsoSessionMaxLifespan() + 1));
- expired.add(session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null).getId());
- expiredClientSessions.add(session.sessions().createClientSession(realm, client).getId());
+ UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
+ expired.add(userSession.getId());
+ AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession);
+ Assert.assertEquals(userSession, clientSession.getUserSession());
Time.setOffset(0);
- UserSessionModel s = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.1", "form", true, null, null);
+ UserSessionModel s = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.1", "form", true, null, null);
//s.setLastSessionRefresh(Time.currentTime() - (realm.getSsoSessionIdleTimeout() + 1));
s.setLastSessionRefresh(0);
expired.add(s.getId());
- ClientSessionModel clSession = session.sessions().createClientSession(realm, client);
- clSession.setUserSession(s);
- expiredClientSessions.add(clSession.getId());
-
Set<String> valid = new HashSet<String>();
Set<String> validClientSessions = new HashSet<String>();
- valid.add(session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null).getId());
- validClientSessions.add(session.sessions().createClientSession(realm, client).getId());
+ userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
+ valid.add(userSession.getId());
+ validClientSessions.add(session.sessions().createClientSession(realm, client, userSession).getId());
resetSession();
@@ -308,91 +288,18 @@ public class UserSessionProviderTest {
for (String e : expired) {
assertNull(session.sessions().getUserSession(realm, e));
}
- for (String e : expiredClientSessions) {
- assertNull(session.sessions().getClientSession(realm, e));
- }
for (String v : valid) {
- assertNotNull(session.sessions().getUserSession(realm, v));
- }
- for (String e : validClientSessions) {
- assertNotNull(session.sessions().getClientSession(realm, e));
+ UserSessionModel userSessionLoaded = session.sessions().getUserSession(realm, v);
+ assertNotNull(userSessionLoaded);
+ Assert.assertEquals(1, userSessionLoaded.getAuthenticatedClientSessions().size());
+ Assert.assertNotNull(userSessionLoaded.getAuthenticatedClientSessions().get(client.getId()));
}
} finally {
Time.setOffset(0);
}
}
- @Test
- public void testExpireDetachedClientSessions() {
- try {
- realm.setAccessCodeLifespan(10);
- realm.setAccessCodeLifespanUserAction(10);
- realm.setAccessCodeLifespanLogin(30);
-
- // Login lifespan is largest
- String clientSessionId = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app")).getId();
- resetSession();
-
- Time.setOffset(25);
- session.sessions().removeExpired(realm);
- resetSession();
-
- assertNotNull(session.sessions().getClientSession(clientSessionId));
-
- Time.setOffset(35);
- session.sessions().removeExpired(realm);
- resetSession();
-
- assertNull(session.sessions().getClientSession(clientSessionId));
-
- // User action is largest
- realm.setAccessCodeLifespanUserAction(40);
-
- Time.setOffset(0);
- clientSessionId = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app")).getId();
- resetSession();
-
- Time.setOffset(35);
- session.sessions().removeExpired(realm);
- resetSession();
-
- assertNotNull(session.sessions().getClientSession(clientSessionId));
-
- Time.setOffset(45);
- session.sessions().removeExpired(realm);
- resetSession();
-
- assertNull(session.sessions().getClientSession(clientSessionId));
-
- // Access code is largest
- realm.setAccessCodeLifespan(50);
-
- Time.setOffset(0);
- clientSessionId = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app")).getId();
- resetSession();
-
- Time.setOffset(45);
- session.sessions().removeExpired(realm);
- resetSession();
-
- assertNotNull(session.sessions().getClientSession(clientSessionId));
-
- Time.setOffset(55);
- session.sessions().removeExpired(realm);
- resetSession();
-
- assertNull(session.sessions().getClientSession(clientSessionId));
- } finally {
- Time.setOffset(0);
-
- realm.setAccessCodeLifespan(60);
- realm.setAccessCodeLifespanUserAction(300);
- realm.setAccessCodeLifespanLogin(1800);
-
- }
- }
-
// KEYCLOAK-2508
@Test
public void testRemovingExpiredSession() {
@@ -425,13 +332,14 @@ public class UserSessionProviderTest {
try {
for (int i = 0; i < 25; i++) {
Time.setOffset(i);
- UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0." + i, "form", false, null, null);
- ClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app"));
+ UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0." + i, "form", false, null, null);
+ AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app"), userSession);
clientSession.setUserSession(userSession);
clientSession.setRedirectUri("http://redirect");
clientSession.setRoles(new HashSet<String>());
clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, "state");
clientSession.setTimestamp(userSession.getStarted());
+ userSession.setLastSessionRefresh(userSession.getStarted());
}
} finally {
Time.setOffset(0);
@@ -448,15 +356,111 @@ public class UserSessionProviderTest {
@Test
public void testCreateAndGetInSameTransaction() {
- UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
- ClientSessionModel clientSession = createClientSession(realm.getClientByClientId("test-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+ ClientModel client = realm.getClientByClientId("test-app");
+ UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+ AuthenticatedClientSessionModel clientSession = createClientSession(client, userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ UserSessionModel userSessionLoaded = session.sessions().getUserSession(realm, userSession.getId());
+ AuthenticatedClientSessionModel clientSessionLoaded = userSessionLoaded.getAuthenticatedClientSessions().get(client.getId());
+ Assert.assertNotNull(userSessionLoaded);
+ Assert.assertNotNull(clientSessionLoaded);
+
+ Assert.assertEquals(userSession.getId(), clientSessionLoaded.getUserSession().getId());
+ Assert.assertEquals(1, userSessionLoaded.getAuthenticatedClientSessions().size());
+ }
+
+ @Test
+ public void testAuthenticatedClientSessions() {
+ UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+
+ ClientModel client1 = realm.getClientByClientId("test-app");
+ ClientModel client2 = realm.getClientByClientId("third-party");
+
+ // Create client1 session
+ AuthenticatedClientSessionModel clientSession1 = session.sessions().createClientSession(realm, client1, userSession);
+ clientSession1.setAction("foo1");
+ clientSession1.setTimestamp(100);
+
+ // Create client2 session
+ AuthenticatedClientSessionModel clientSession2 = session.sessions().createClientSession(realm, client2, userSession);
+ clientSession2.setAction("foo2");
+ clientSession2.setTimestamp(200);
+
+ // commit
+ resetSession();
+
+ // Ensure sessions are here
+ userSession = session.sessions().getUserSession(realm, userSession.getId());
+ Map<String, AuthenticatedClientSessionModel> clientSessions = userSession.getAuthenticatedClientSessions();
+ Assert.assertEquals(2, clientSessions.size());
+ testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1", 100);
+ testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2", 200);
+
+ // Update session1
+ clientSessions.get(client1.getId()).setAction("foo1-updated");
+
+ // commit
+ resetSession();
+
+ // Ensure updated
+ userSession = session.sessions().getUserSession(realm, userSession.getId());
+ clientSessions = userSession.getAuthenticatedClientSessions();
+ testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", 100);
+
+ // Rewrite session2
+ clientSession2 = session.sessions().createClientSession(realm, client2, userSession);
+ clientSession2.setAction("foo2-rewrited");
+ clientSession2.setTimestamp(300);
+
+ // commit
+ resetSession();
+
+ // Ensure updated
+ userSession = session.sessions().getUserSession(realm, userSession.getId());
+ clientSessions = userSession.getAuthenticatedClientSessions();
+ Assert.assertEquals(2, clientSessions.size());
+ testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", 100);
+ testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2-rewrited", 300);
+
+ // remove session
+ clientSession1 = userSession.getAuthenticatedClientSessions().get(client1.getId());
+ clientSession1.setUserSession(null);
+
+ // Commit and ensure removed
+ resetSession();
+
+ userSession = session.sessions().getUserSession(realm, userSession.getId());
+ clientSessions = userSession.getAuthenticatedClientSessions();
+ Assert.assertEquals(1, clientSessions.size());
+ Assert.assertNull(clientSessions.get(client1.getId()));
+ }
+
+
+ @Test
+ public void testFailCreateExistingSession() {
+ UserSessionModel userSession = session.sessions().createUserSession("123", realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+
+ // commit
+ resetSession();
+
+
+ try {
+ session.sessions().createUserSession("123", realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+ kc.stopSession(session, true);
+ Assert.fail("Not expected to successfully create duplicated userSession");
+ } catch (IllegalStateException e) {
+ // Expected
+ session = kc.startSession();
+ }
+
+ }
- Assert.assertNotNull(session.sessions().getUserSession(realm, userSession.getId()));
- Assert.assertNotNull(session.sessions().getClientSession(realm, clientSession.getId()));
- Assert.assertEquals(userSession.getId(), clientSession.getUserSession().getId());
- Assert.assertEquals(1, userSession.getClientSessions().size());
- Assert.assertEquals(clientSession.getId(), userSession.getClientSessions().get(0).getId());
+ private void testAuthenticatedClientSession(AuthenticatedClientSessionModel clientSession, String expectedClientId, String expectedUserSessionId, String expectedAction, int expectedTimestamp) {
+ Assert.assertEquals(expectedClientId, clientSession.getClient().getClientId());
+ Assert.assertEquals(expectedUserSessionId, clientSession.getUserSession().getId());
+ Assert.assertEquals(expectedAction, clientSession.getAction());
+ Assert.assertEquals(expectedTimestamp, clientSession.getTimestamp());
}
private void assertPaginatedSession(RealmModel realm, ClientModel client, int start, int max, int expectedSize) {
@@ -545,9 +549,8 @@ public class UserSessionProviderTest {
assertNotNull(session.sessions().getUserLoginFailure(realm, "user2"));
}
- private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
- ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
- if (userSession != null) clientSession.setUserSession(userSession);
+ private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
+ AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession);
clientSession.setRedirectUri(redirect);
if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
if (roles != null) clientSession.setRoles(roles);
@@ -557,7 +560,7 @@ public class UserSessionProviderTest {
private UserSessionModel[] createSessions() {
UserSessionModel[] sessions = new UserSessionModel[3];
- sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
+ sessions[0] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
Set<String> roles = new HashSet<String>();
roles.add("one");
@@ -570,10 +573,10 @@ public class UserSessionProviderTest {
createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers);
createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
- sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+ sessions[1] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
- sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
+ sessions[2] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
resetSession();
@@ -613,9 +616,14 @@ public class UserSessionProviderTest {
assertTrue(session.getStarted() >= started - 1 && session.getStarted() <= started + 1);
assertTrue(session.getLastSessionRefresh() >= lastRefresh - 1 && session.getLastSessionRefresh() <= lastRefresh + 1);
- String[] actualClients = new String[session.getClientSessions().size()];
- for (int i = 0; i < actualClients.length; i++) {
- actualClients[i] = session.getClientSessions().get(i).getClient().getClientId();
+ String[] actualClients = new String[session.getAuthenticatedClientSessions().size()];
+ int i = 0;
+ for (Map.Entry<String, AuthenticatedClientSessionModel> entry : session.getAuthenticatedClientSessions().entrySet()) {
+ String clientUUID = entry.getKey();
+ AuthenticatedClientSessionModel clientSession = entry.getValue();
+ Assert.assertEquals(clientUUID, clientSession.getClient().getId());
+ actualClients[i] = clientSession.getClient().getClientId();
+ i++;
}
Arrays.sort(clients);
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 46d62b8..2da679e 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
@@ -35,6 +35,7 @@ import org.keycloak.common.util.PemUtils;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
@@ -69,7 +70,9 @@ public class OAuthClient {
private String redirectUri = "http://localhost:8081/app/auth";
- private String state = "mystate";
+ private StateParamProvider state = () -> {
+ return KeycloakModelUtils.generateId();
+ };
private String scope;
@@ -438,7 +441,7 @@ public class OAuthClient {
b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
}
if (state != null) {
- b.queryParam(OAuth2Constants.STATE, state);
+ b.queryParam(OAuth2Constants.STATE, state.getState());
}
if(uiLocales != null){
b.queryParam(OAuth2Constants.UI_LOCALES_PARAM, uiLocales);
@@ -509,8 +512,17 @@ public class OAuthClient {
return this;
}
- public OAuthClient state(String state) {
- this.state = state;
+ public OAuthClient stateParamHardcoded(String value) {
+ this.state = () -> {
+ return value;
+ };
+ return this;
+ }
+
+ public OAuthClient stateParamRandom() {
+ this.state = () -> {
+ return KeycloakModelUtils.generateId();
+ };
return this;
}
@@ -639,4 +651,10 @@ public class OAuthClient {
}
}
+ private interface StateParamProvider {
+
+ String getState();
+
+ }
+
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java
index 8ed8461..e6bb660 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java
@@ -28,11 +28,25 @@ public class IdpLinkEmailPage extends AbstractPage {
@FindBy(id = "instruction1")
private WebElement message;
+ @FindBy(linkText = "Click here")
+ private WebElement resendEmailLink;
+
+ @FindBy(linkText = "Click here") // Actually same link like "resendEmailLink"
+ private WebElement continueFlowLink;
+
@Override
public boolean isCurrent() {
return driver.getTitle().startsWith("Link ");
}
+ public void clickResendEmail() {
+ resendEmailLink.click();
+ }
+
+ public void clickContinueFlowLink() {
+ continueFlowLink.click();
+ }
+
@Override
public void open() throws Exception {
throw new UnsupportedOperationException();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginExpiredPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginExpiredPage.java
new file mode 100644
index 0000000..e3ff938
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginExpiredPage.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.pages;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LoginExpiredPage extends AbstractPage {
+
+ @FindBy(id = "loginRestartLink")
+ private WebElement loginRestartLink;
+
+ @FindBy(id = "loginContinueLink")
+ private WebElement loginContinueLink;
+
+
+ public void clickLoginRestartLink() {
+ loginRestartLink.click();
+ }
+
+ public void clickLoginContinueLink() {
+ loginContinueLink.click();
+ }
+
+
+ public boolean isCurrent() {
+ return driver.getTitle().equals("Page has expired");
+ }
+
+ public void open() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
index 050bcf3..2904ef8 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
@@ -21,7 +21,6 @@ import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
-import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.ApplicationServlet;
@@ -90,24 +89,6 @@ public class KeycloakRule extends AbstractKeycloakRule {
stopSession(session, true);
}
- public ClientSessionCode verifyCode(String code) {
- KeycloakSession session = startSession();
- try {
- RealmModel realm = session.realms().getRealm("test");
- try {
- ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm);
- if (accessCode == null) {
- Assert.fail("Invalid code");
- }
- return accessCode;
- } catch (Throwable t) {
- throw new AssertionError("Failed to parse code", t);
- }
- } finally {
- stopSession(session, false);
- }
- }
-
public abstract static class KeycloakSetup {
protected KeycloakSession session;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
index 2cea40a..0d93d19 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
@@ -40,10 +40,14 @@ public class WebRule extends ExternalResource {
this.test = test;
}
- @Override
- public void before() throws Throwable {
+ public void initProperties() {
driver = createWebDriver();
oauth = new OAuthClient(driver);
+ }
+
+ @Override
+ public void before() throws Throwable {
+ initProperties();
initWebResources(test);
}
@@ -58,6 +62,7 @@ public class WebRule extends ExternalResource {
HtmlUnitDriver d = new HtmlUnitDriver();
d.getWebClient().getOptions().setJavaScriptEnabled(true);
d.getWebClient().getOptions().setCssEnabled(false);
+ d.getWebClient().getOptions().setTimeout(1000000);
driver = d;
} else if (browser.equals("chrome")) {
driver = new ChromeDriver();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java
index f94cea0..7aea03e 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java
@@ -47,9 +47,9 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand {
}
protected String toString(UserSessionEntity userSession) {
- int clientSessionsSize = userSession.getClientSessions()==null ? 0 : userSession.getClientSessions().size();
+ int clientSessionsSize = userSession.getAuthenticatedClientSessions()==null ? 0 : userSession.getAuthenticatedClientSessions().size();
return "ID: " + userSession.getId() + ", realm: " + userSession.getRealm() + ", lastAccessTime: " + Time.toDate(userSession.getLastSessionRefresh()) +
- ", clientSessions: " + clientSessionsSize;
+ ", authenticatedClientSessions: " + clientSessionsSize;
}
protected abstract void doRunCacheCommand(KeycloakSession session, Cache<String, SessionEntity> cache);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/PersistSessionsCommand.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/PersistSessionsCommand.java
index b2ede3e..c8b6771 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/PersistSessionsCommand.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/PersistSessionsCommand.java
@@ -17,8 +17,11 @@
package org.keycloak.testsuite.util.cli;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.RealmModel;
@@ -27,8 +30,6 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
-import java.util.LinkedList;
-import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -65,9 +66,9 @@ public class PersistSessionsCommand extends AbstractCommand {
});
}
+
private void createSessionsBatch(final int countInThisBatch) {
final List<String> userSessionIds = new LinkedList<>();
- final List<String> clientSessionIds = new LinkedList<>();
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@@ -79,13 +80,11 @@ public class PersistSessionsCommand extends AbstractCommand {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
for (int i = 0; i < countInThisBatch; i++) {
- UserSessionModel userSession = session.sessions().createUserSession(realm, john, "john-doh@localhost", "127.0.0.2", "form", true, null, null);
- ClientSessionModel clientSession = session.sessions().createClientSession(realm, testApp);
- clientSession.setUserSession(userSession);
+ UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, john, "john-doh@localhost", "127.0.0.2", "form", true, null, null);
+ AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, testApp, userSession);
clientSession.setRedirectUri("http://redirect");
clientSession.setNote("foo", "bar-" + i);
userSessionIds.add(userSession.getId());
- clientSessionIds.add(clientSession.getId());
}
}
@@ -100,6 +99,7 @@ public class PersistSessionsCommand extends AbstractCommand {
@Override
public void run(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("master");
+ ClientModel testApp = realm.getClientByClientId("security-admin-console");
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
int counter = 0;
@@ -107,17 +107,12 @@ public class PersistSessionsCommand extends AbstractCommand {
counter++;
UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId);
persister.createUserSession(userSession, true);
- }
-
- log.infof("%d user sessions persisted. Continue", counter);
- counter = 0;
- for (String clientSessionId : clientSessionIds) {
- counter++;
- ClientSessionModel clientSession = session.sessions().getClientSession(realm, clientSessionId);
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(testApp.getId());
persister.createClientSession(clientSession, true);
}
- log.infof("%d client sessions persisted. Continue", counter);
+
+ log.infof("%d user sessions persisted. Continue", counter);
}
});
diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties
index cac26ae..dcff0ec 100755
--- a/testsuite/integration/src/test/resources/log4j.properties
+++ b/testsuite/integration/src/test/resources/log4j.properties
@@ -80,4 +80,8 @@ log4j.logger.org.apache.directory.server.ldap.LdapProtocolHandler=error
#log4j.logger.org.apache.http.impl.conn=debug
# Enable to view details from identity provider authenticator
-# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
\ No newline at end of file
+log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
+log4j.logger.org.keycloak.services.resources.IdentityBrokerService=trace
+log4j.logger.org.keycloak.broker=trace
+
+# log4j.logger.io.undertow=trace
diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
index c463347..fc695d4 100755
--- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
@@ -90,7 +90,7 @@
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}",
"l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}",
"remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
- "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
+ "remoteStoreHost": "${keycloak.connectionsjen neInfinispan.remoteStoreHost:localhost}",
"remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
}
},
testsuite/integration-arquillian/HOW-TO-RUN.md 106(+91 -15)
diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md
index 1c519b1..10becee 100644
--- a/testsuite/integration-arquillian/HOW-TO-RUN.md
+++ b/testsuite/integration-arquillian/HOW-TO-RUN.md
@@ -22,7 +22,7 @@ Adding this system property when running any test:
-Darquillian.debug=true
will add lots of info to the log. Especially about:
-* The test method names, which will be executed for each test class, will be written at the proper running order to the log at the beginning of each test (done by KcArquillian class).
+* The test method names, which will be executed for each test class, will be written at the proper running order to the log at the beginning of each test class(done by KcArquillian class).
* All the triggered arquillian lifecycle events and executed observers listening to those events will be written to the log
* The bootstrap of WebDriver will be unlimited. By default there is just 1 minute timeout and test is cancelled when WebDriver is not bootstrapped within it.
@@ -257,20 +257,20 @@ mvn -f testsuite/integration-arquillian/tests/other/console/pom.xml \
## Welcome Page tests
The Welcome Page tests need to be run on WildFly/EAP and with `-Dskip.add.user.json` switch. So that they are disabled by default and are meant to be run separately.
-```
-# Prepare servers
-mvn -f testsuite/integration-arquillian/servers/pom.xml \
- clean install \
- -Pauth-server-wildfly \
- -Papp-server-wildfly
-
-# Run tests
-mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
- clean test \
- -Dtest=WelcomePageTest \
- -Dskip.add.user.json \
- -Pauth-server-wildfly
-```
+
+ # Prepare servers
+ mvn -f testsuite/integration-arquillian/servers/pom.xml \
+ clean install \
+ -Pauth-server-wildfly \
+ -Papp-server-wildfly
+
+ # Run tests
+ mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
+ clean test \
+ -Dtest=WelcomePageTest \
+ -Dskip.add.user.json \
+ -Pauth-server-wildfly
+
## Social Login
The social login tests require setup of all social networks including an example social user. These details can't be
@@ -341,4 +341,80 @@ To run the X.509 client certificate authentication tests:
-Dauth.server.ssl.required \
-Dbrowser=phantomjs \
"-Dtest=*.x509.*"
+
+## Cluster tests
+
+Cluster tests use 2 backend servers (Keycloak on Wildfly/EAP) and 1 frontend loadbalancer server node. Invalidation tests don't use loadbalancer.
+The browser usually communicates directly with the backend node1 and after doing some change here (eg. updating user), it verifies that the change is visible on node2 and user is updated here as well.
+
+Failover tests use loadbalancer and they require the setup with the distributed infinispan caches switched to have 2 owners (default value is 1 owner). Otherwise failover won't reliably work.
+
+
+The setup includes:
+
+* a `mod_cluster` load balancer on Wildfly
+* two clustered nodes of Keycloak server on Wildfly/EAP
+
+Clustering tests require MULTICAST to be enabled on machine's `loopback` network interface.
+This can be done by running the following commands under root privileges:
+
+ route add -net 224.0.0.0 netmask 240.0.0.0 dev lo
+ ifconfig lo multicast
+
+Then after build the sources, distribution and setup of clean shared database (replace command according your DB), you can use this command to setup servers:
+
+ export DB_HOST=localhost
+ mvn -f testsuite/integration-arquillian/servers/pom.xml \
+ -Pauth-server-wildfly,auth-server-cluster,jpa \
+ -Dsession.cache.owners=2 \
+ -Djdbc.mvn.groupId=mysql \
+ -Djdbc.mvn.version=5.1.29 \
+ -Djdbc.mvn.artifactId=mysql-connector-java \
+ -Dkeycloak.connectionsJpa.url=jdbc:mysql://$DB_HOST/keycloak \
+ -Dkeycloak.connectionsJpa.user=keycloak \
+ -Dkeycloak.connectionsJpa.password=keycloak \
+ clean install
+
+And then this to run the cluster tests:
+
+ mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
+ -Pauth-server-wildfly,auth-server-cluster \
+ -Dsession.cache.owners=2 \
+ -Dbackends.console.output=true \
+ -Dauth.server.log.check=false \
+ -Dfrontend.console.output=true \
+ -Dtest=org.keycloak.testsuite.cluster.**.*Test clean install
+
+
+### Cluster tests with embedded undertow
+
+#### Run cluster tests from IDE
+
+The test uses Undertow loadbalancer on `http://localhost:8180` and two embedded backend Undertow servers with Keycloak on `http://localhost:8181` and `http://localhost:8182` .
+You can use any cluster test (eg. AuthenticationSessionFailoverClusterTest) and run from IDE with those system properties (replace with your DB settings):
+
+ -Dauth.server.undertow=false -Dauth.server.undertow.cluster=true -Dauth.server.cluster=true
+ -Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver
+ -Dkeycloak.connectionsJpa.user=keycloak -Dkeycloak.connectionsJpa.password=keycloak -Dkeycloak.connectionsInfinispan.clustered=true -Dresources
+ -Dkeycloak.connectionsInfinispan.sessionsOwners=2 -Dsession.cache.owners=2
+
+Invalidation tests (subclass of `AbstractInvalidationClusterTest`) don't need last two properties.
+
+
+#### Run cluster environment from IDE
+
+This mode is useful for develop/manual tests of clustering features. You will need to manually run keycloak backend nodes and loadbalancer.
+
+1) Run KeycloakServer server1 with:
+
+ -Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver
+ -Dkeycloak.connectionsJpa.user=keycloak -Dkeycloak.connectionsJpa.password=keycloak -Dkeycloak.connectionsInfinispan.clustered=true
+ -Dkeycloak.connectionsInfinispan.sessionsOwners=2 -Dresources
+
+and argument: `-p 8181`
+
+2) Run KeycloakServer server2 with same parameters but argument: `-p 8182`
+
+3) Run loadbalancer (class `SimpleUndertowLoadBalancer`) without arguments and system properties. Loadbalancer runs on port 8180, so you can access Keycloak on `http://localhost:8180/auth`
+
diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/ispn-cache-owners.xsl b/testsuite/integration-arquillian/servers/auth-server/jboss/common/ispn-cache-owners.xsl
index 65abfea..46c6f7c 100644
--- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/ispn-cache-owners.xsl
+++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/ispn-cache-owners.xsl
@@ -18,6 +18,11 @@
<xsl:value-of select="$sessionCacheOwners"/>
</xsl:attribute>
</xsl:template>
+ <xsl:template match="//i:cache-container/i:distributed-cache[@name='authenticationSessions']/@owners">
+ <xsl:attribute name="owners">
+ <xsl:value-of select="$sessionCacheOwners"/>
+ </xsl:attribute>
+ </xsl:template>
<xsl:template match="//i:cache-container/i:distributed-cache[@name='offlineSessions']/@owners">
<xsl:attribute name="owners">
<xsl:value-of select="$offlineSessionCacheOwners"/>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughRegistration.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughRegistration.java
index 27de40e..d03c0b0 100755
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughRegistration.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughRegistration.java
@@ -52,15 +52,15 @@ public class PassThroughRegistration implements Authenticator, AuthenticatorFact
user.setEnabled(true);
user.setEmail(email);
- context.getClientSession().setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
+ context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
context.setUser(user);
context.getEvent().user(user);
context.getEvent().success();
context.newEvent().event(EventType.LOGIN);
- context.getEvent().client(context.getClientSession().getClient().getClientId())
- .detail(Details.REDIRECT_URI, context.getClientSession().getRedirectUri())
- .detail(Details.AUTH_METHOD, context.getClientSession().getAuthMethod());
- String authType = context.getClientSession().getNote(Details.AUTH_TYPE);
+ context.getEvent().client(context.getAuthenticationSession().getClient().getClientId())
+ .detail(Details.REDIRECT_URI, context.getAuthenticationSession().getRedirectUri())
+ .detail(Details.AUTH_METHOD, context.getAuthenticationSession().getProtocol());
+ String authType = context.getAuthenticationSession().getAuthNote(Details.AUTH_TYPE);
if (authType != null) {
context.getEvent().detail(Details.AUTH_TYPE, authType);
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
index a4ae8c2..b868827 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java
@@ -163,6 +163,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
session.sessions().removeExpired(realm);
+ session.authenticationSessions().removeExpired(realm);
return Response.ok().build();
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java
index c894674..83bb19b 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java
@@ -41,8 +41,11 @@ import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.descriptor.api.Descriptor;
import org.jboss.shrinkwrap.undertow.api.UndertowWebArchive;
import org.keycloak.common.util.reflections.Reflections;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.services.filters.KeycloakSessionServletFilter;
+import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.resources.KeycloakApplication;
import javax.servlet.DispatcherType;
@@ -106,6 +109,11 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
@Override
public ProtocolMetaData deploy(Archive<?> archive) throws DeploymentException {
+ if (isRemoteMode()) {
+ log.infof("Skipped deployment of '%s' as we are in remote mode!", archive.getName());
+ return new ProtocolMetaData();
+ }
+
DeploymentInfo di = getDeplotymentInfoFromArchive(archive);
ClassLoader parentCl = Thread.currentThread().getContextClassLoader();
@@ -152,7 +160,7 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
return;
}
- log.info("Starting auth server on embedded Undertow.");
+ log.infof("Starting auth server on embedded Undertow on: http://%s:%d", configuration.getBindAddress(), configuration.getBindHttpPort());
long start = System.currentTimeMillis();
if (undertow == null) {
@@ -164,13 +172,37 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
.setIoThreads(configuration.getWorkerThreads() / 8)
);
- DeploymentInfo di = createAuthServerDeploymentInfo();
- undertow.deploy(di);
- ResteasyDeployment deployment = (ResteasyDeployment) di.getServletContextAttributes().get(ResteasyDeployment.class.getName());
- sessionFactory = ((KeycloakApplication) deployment.getApplication()).getSessionFactory();
+ if (configuration.getRoute() != null) {
+ log.info("Using route: " + configuration.getRoute());
+ }
+
+ SetSystemProperty setRouteProperty = new SetSystemProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME, configuration.getRoute());
+ try {
+ DeploymentInfo di = createAuthServerDeploymentInfo();
+ undertow.deploy(di);
+ ResteasyDeployment deployment = (ResteasyDeployment) di.getServletContextAttributes().get(ResteasyDeployment.class.getName());
+ sessionFactory = ((KeycloakApplication) deployment.getApplication()).getSessionFactory();
+
+ setupDevConfig();
+
+ log.info("Auth server started in " + (System.currentTimeMillis() - start) + " ms\n");
+ } finally {
+ setRouteProperty.revert();
+ }
+ }
- log.info("Auth server started in " + (System.currentTimeMillis() - start) + " ms\n");
+ protected void setupDevConfig() {
+ KeycloakSession session = sessionFactory.create();
+ try {
+ session.getTransactionManager().begin();
+ if (new ApplianceBootstrap(session).isNoMasterUser()) {
+ new ApplianceBootstrap(session).createMasterRealmUser("admin", "admin");
+ }
+ session.getTransactionManager().commit();
+ } finally {
+ session.close();
+ }
}
@Override
@@ -187,11 +219,16 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
private boolean isRemoteMode() {
//return true;
- return "true".equals(System.getProperty("remote.mode"));
+ return configuration.isRemoteMode();
}
@Override
public void undeploy(Archive<?> archive) throws DeploymentException {
+ if (isRemoteMode()) {
+ log.infof("Skipped undeployment of '%s' as we are in remote mode!", archive.getName());
+ return;
+ }
+
Field containerField = Reflections.findDeclaredField(UndertowJaxrsServer.class, "container");
Reflections.setAccessible(containerField);
ServletContainer container = (ServletContainer) Reflections.getFieldValue(containerField, undertow);
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowArquillianExtension.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowArquillianExtension.java
index 14aca1c..7966604 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowArquillianExtension.java
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowArquillianExtension.java
@@ -2,6 +2,7 @@ package org.keycloak.testsuite.arquillian.undertow;
import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
import org.jboss.arquillian.core.spi.LoadableExtension;
+import org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancerContainer;
/**
*
@@ -12,6 +13,7 @@ public class KeycloakOnUndertowArquillianExtension implements LoadableExtension
@Override
public void register(ExtensionBuilder builder) {
builder.service(DeployableContainer.class, KeycloakOnUndertow.class);
+ builder.service(DeployableContainer.class, SimpleUndertowLoadBalancerContainer.class);
}
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java
index 0a519ef..bdf0ff7 100644
--- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java
@@ -19,11 +19,18 @@ package org.keycloak.testsuite.arquillian.undertow;
import org.arquillian.undertow.UndertowContainerConfiguration;
import org.jboss.arquillian.container.spi.ConfigurationException;
+import org.jboss.logging.Logger;
public class KeycloakOnUndertowConfiguration extends UndertowContainerConfiguration {
+ protected static final Logger log = Logger.getLogger(KeycloakOnUndertowConfiguration.class);
+
private int workerThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2) * 8;
private String resourcesHome;
+ private boolean remoteMode;
+ private String route;
+
+ private int bindHttpPortOffset = 0;
public int getWorkerThreads() {
return workerThreads;
@@ -41,10 +48,39 @@ public class KeycloakOnUndertowConfiguration extends UndertowContainerConfigurat
this.resourcesHome = resourcesHome;
}
+ public int getBindHttpPortOffset() {
+ return bindHttpPortOffset;
+ }
+
+ public void setBindHttpPortOffset(int bindHttpPortOffset) {
+ this.bindHttpPortOffset = bindHttpPortOffset;
+ }
+
+ public String getRoute() {
+ return route;
+ }
+
+ public void setRoute(String route) {
+ this.route = route;
+ }
+
+ public boolean isRemoteMode() {
+ return remoteMode;
+ }
+
+ public void setRemoteMode(boolean remoteMode) {
+ this.remoteMode = remoteMode;
+ }
+
@Override
public void validate() throws ConfigurationException {
super.validate();
-
+
+ int basePort = getBindHttpPort();
+ int newPort = basePort + bindHttpPortOffset;
+ setBindHttpPort(newPort);
+ log.info("KeycloakOnUndertow will listen on port: " + newPort);
+
// TODO validate workerThreads
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java
new file mode 100644
index 0000000..3eda20c
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.arquillian.undertow.lb;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import io.undertow.Undertow;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.server.handlers.ResponseCodeHandler;
+import io.undertow.server.handlers.proxy.ExclusivityChecker;
+import io.undertow.server.handlers.proxy.LoadBalancingProxyClient;
+import io.undertow.server.handlers.proxy.ProxyCallback;
+import io.undertow.server.handlers.proxy.ProxyClient;
+import io.undertow.server.handlers.proxy.ProxyConnection;
+import io.undertow.server.handlers.proxy.ProxyHandler;
+import io.undertow.util.AttachmentKey;
+import io.undertow.util.Headers;
+import org.jboss.logging.Logger;
+import org.keycloak.services.managers.AuthenticationSessionManager;
+
+/**
+ * Loadbalancer on embedded undertow. Supports sticky session over "AUTH_SESSION_ID" cookie and failover to different node when sticky node not available.
+ * Status 503 is returned just if all backend nodes are unavailable.
+ *
+ * To configure backend nodes, you can use system property like : -Dkeycloak.nodes="node1=http://localhost:8181,node2=http://localhost:8182"
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SimpleUndertowLoadBalancer {
+
+ private static final Logger log = Logger.getLogger(SimpleUndertowLoadBalancer.class);
+
+ static final String DEFAULT_NODES = "node1=http://localhost:8181,node2=http://localhost:8182";
+
+ private final String host;
+ private final int port;
+ private final String nodesString;
+ private Undertow undertow;
+
+
+ public static void main(String[] args) throws Exception {
+ String nodes = System.getProperty("keycloak.nodes", DEFAULT_NODES);
+
+ SimpleUndertowLoadBalancer lb = new SimpleUndertowLoadBalancer("localhost", 8180, nodes);
+ lb.start();
+
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+
+ @Override
+ public void run() {
+ lb.stop();
+ }
+
+ });
+ }
+
+
+ public SimpleUndertowLoadBalancer(String host, int port, String nodesString) {
+ this.host = host;
+ this.port = port;
+ this.nodesString = nodesString;
+ log.infof("Keycloak nodes: %s", nodesString);
+ }
+
+
+ public void start() {
+ Map<String, String> nodes = parseNodes(nodesString);
+ try {
+ HttpHandler proxyHandler = createHandler(nodes);
+
+ undertow = Undertow.builder()
+ .addHttpListener(port, host)
+ .setHandler(proxyHandler)
+ .build();
+ undertow.start();
+
+ log.infof("Loadbalancer started and ready to serve requests on http://%s:%d", host, port);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ public void stop() {
+ undertow.stop();
+ }
+
+
+ static Map<String, String> parseNodes(String nodes) {
+ String[] nodesArray = nodes.split(",");
+ Map<String, String> result = new HashMap<>();
+
+ for (String nodeStr : nodesArray) {
+ String[] node = nodeStr.trim().split("=");
+ if (node.length != 2) {
+ throw new IllegalArgumentException("Illegal node format in the configuration: " + nodeStr);
+ }
+ result.put(node[0].trim(), node[1].trim());
+ }
+
+ return result;
+ }
+
+
+ private HttpHandler createHandler(Map<String, String> backendNodes) throws Exception {
+
+ // TODO: configurable options if needed
+ String sessionCookieNames = AuthenticationSessionManager.AUTH_SESSION_ID;
+ int connectionsPerThread = 20;
+ int problemServerRetry = 5; // In case of unavailable node, we will try to ping him every 5 seconds to check if it's back
+ int maxTime = 3600000; // 1 hour for proxy request timeout, so we can debug the backend keycloak servers
+ int requestQueueSize = 10;
+ int cachedConnectionsPerThread = 10;
+ int connectionIdleTimeout = 60;
+ int maxRetryAttempts = backendNodes.size() - 1;
+
+ final LoadBalancingProxyClient lb = new CustomLoadBalancingClient(new ExclusivityChecker() {
+
+ @Override
+ public boolean isExclusivityRequired(HttpServerExchange exchange) {
+ //we always create a new connection for upgrade requests
+ return exchange.getRequestHeaders().contains(Headers.UPGRADE);
+ }
+
+ }, maxRetryAttempts)
+ .setConnectionsPerThread(connectionsPerThread)
+ .setMaxQueueSize(requestQueueSize)
+ .setSoftMaxConnectionsPerThread(cachedConnectionsPerThread)
+ .setTtl(connectionIdleTimeout)
+ .setProblemServerRetry(problemServerRetry);
+ String[] sessionIds = sessionCookieNames.split(",");
+ for (String id : sessionIds) {
+ lb.addSessionCookieName(id);
+ }
+
+ for (Map.Entry<String, String> node : backendNodes.entrySet()) {
+ String route = node.getKey();
+ URI uri = new URI(node.getValue());
+
+ lb.addHost(uri, route);
+ log.infof("Added host: %s, route: %s", uri.toString(), route);
+ }
+
+ ProxyHandler handler = new ProxyHandler(lb, maxTime, ResponseCodeHandler.HANDLE_404);
+ return handler;
+ }
+
+
+ private class CustomLoadBalancingClient extends LoadBalancingProxyClient {
+
+ private final int maxRetryAttempts;
+
+ public CustomLoadBalancingClient(ExclusivityChecker checker, int maxRetryAttempts) {
+ super(checker);
+ this.maxRetryAttempts = maxRetryAttempts;
+ }
+
+
+ @Override
+ protected Host selectHost(HttpServerExchange exchange) {
+ Host host = super.selectHost(exchange);
+ log.debugf("Selected host: %s, host available: %b", host.getUri().toString(), host.isAvailable());
+ exchange.putAttachment(SELECTED_HOST, host);
+ return host;
+ }
+
+
+ @Override
+ protected Host findStickyHost(HttpServerExchange exchange) {
+ Host stickyHost = super.findStickyHost(exchange);
+
+ if (stickyHost != null) {
+
+ if (!stickyHost.isAvailable()) {
+ log.infof("Sticky host %s not available. Trying different hosts", stickyHost.getUri());
+ return null;
+ } else {
+ log.infof("Sticky host %s found and looks available", stickyHost.getUri());
+ }
+ }
+
+ return stickyHost;
+ }
+
+
+ @Override
+ public void getConnection(ProxyTarget target, HttpServerExchange exchange, ProxyCallback<ProxyConnection> callback, long timeout, TimeUnit timeUnit) {
+ long timeoutMs = timeUnit.toMillis(timeout);
+
+ ProxyCallbackDelegate callbackDelegate = new ProxyCallbackDelegate(this, callback, timeoutMs, maxRetryAttempts);
+ super.getConnection(target, exchange, callbackDelegate, timeout, timeUnit);
+ }
+
+ }
+
+
+ private static final AttachmentKey<LoadBalancingProxyClient.Host> SELECTED_HOST = AttachmentKey.create(LoadBalancingProxyClient.Host.class);
+ private static final AttachmentKey<Integer> REMAINING_RETRY_ATTEMPTS = AttachmentKey.create(Integer.class);
+
+
+ private class ProxyCallbackDelegate implements ProxyCallback<ProxyConnection> {
+
+ private final ProxyClient proxyClient;
+ private final ProxyCallback<ProxyConnection> delegate;
+ private final long timeoutMs;
+ private final int maxRetryAttempts;
+
+
+ public ProxyCallbackDelegate(ProxyClient proxyClient, ProxyCallback<ProxyConnection> delegate, long timeoutMs, int maxRetryAttempts) {
+ this.proxyClient = proxyClient;
+ this.delegate = delegate;
+ this.timeoutMs = timeoutMs;
+ this.maxRetryAttempts = maxRetryAttempts;
+ }
+
+
+ @Override
+ public void completed(HttpServerExchange exchange, ProxyConnection result) {
+ LoadBalancingProxyClient.Host host = exchange.getAttachment(SELECTED_HOST);
+ if (host == null) {
+ // shouldn't happen
+ log.error("Host is null!!!");
+ } else {
+ // Host was restored
+ if (!host.isAvailable()) {
+ log.infof("Host %s available again", host.getUri());
+ host.clearError();
+ }
+ }
+
+ delegate.completed(exchange, result);
+ }
+
+
+ @Override
+ public void failed(HttpServerExchange exchange) {
+ final long time = System.currentTimeMillis();
+
+ Integer remainingAttempts = exchange.getAttachment(REMAINING_RETRY_ATTEMPTS);
+ if (remainingAttempts == null) {
+ remainingAttempts = maxRetryAttempts;
+ } else {
+ remainingAttempts--;
+ }
+
+ exchange.putAttachment(REMAINING_RETRY_ATTEMPTS, remainingAttempts);
+
+ log.infof("Failed request to selected host. Remaining attempts: %d", remainingAttempts);
+ if (remainingAttempts > 0) {
+ if (timeoutMs > 0 && time > timeoutMs) {
+ delegate.failed(exchange);
+ } else {
+ ProxyClient.ProxyTarget target = proxyClient.findTarget(exchange);
+ if (target != null) {
+ final long remaining = timeoutMs > 0 ? timeoutMs - time : -1;
+ proxyClient.getConnection(target, exchange, this, remaining, TimeUnit.MILLISECONDS);
+ } else {
+ couldNotResolveBackend(exchange); // The context was registered when we started, so return 503
+ }
+ }
+ } else {
+ couldNotResolveBackend(exchange);
+ }
+ }
+
+
+ @Override
+ public void couldNotResolveBackend(HttpServerExchange exchange) {
+ delegate.couldNotResolveBackend(exchange);
+ }
+
+
+ @Override
+ public void queuedRequestFailed(HttpServerExchange exchange) {
+ delegate.queuedRequestFailed(exchange);
+ }
+
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java
new file mode 100644
index 0000000..3a0312c
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.arquillian.undertow.lb;
+
+import org.arquillian.undertow.UndertowContainerConfiguration;
+import org.jboss.arquillian.container.spi.ConfigurationException;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SimpleUndertowLoadBalancerConfiguration extends UndertowContainerConfiguration {
+
+ private String nodes = SimpleUndertowLoadBalancer.DEFAULT_NODES;
+
+ public String getNodes() {
+ return nodes;
+ }
+
+ public void setNodes(String nodes) {
+ this.nodes = nodes;
+ }
+
+ @Override
+ public void validate() throws ConfigurationException {
+ super.validate();
+
+ try {
+ SimpleUndertowLoadBalancer.parseNodes(nodes);
+ } catch (Exception e) {
+ throw new ConfigurationException(e);
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java
new file mode 100644
index 0000000..4b24c15
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.arquillian.undertow.lb;
+
+import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
+import org.jboss.arquillian.container.spi.client.container.DeploymentException;
+import org.jboss.arquillian.container.spi.client.container.LifecycleException;
+import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription;
+import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData;
+import org.jboss.logging.Logger;
+import org.jboss.shrinkwrap.api.Archive;
+import org.jboss.shrinkwrap.descriptor.api.Descriptor;
+
+/**
+ * Arquillian container over {@link SimpleUndertowLoadBalancer}
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SimpleUndertowLoadBalancerContainer implements DeployableContainer<SimpleUndertowLoadBalancerConfiguration> {
+
+ private static final Logger log = Logger.getLogger(SimpleUndertowLoadBalancerContainer.class);
+
+ private SimpleUndertowLoadBalancerConfiguration configuration;
+ private SimpleUndertowLoadBalancer container;
+
+ @Override
+ public Class<SimpleUndertowLoadBalancerConfiguration> getConfigurationClass() {
+ return SimpleUndertowLoadBalancerConfiguration.class;
+ }
+
+ @Override
+ public void setup(SimpleUndertowLoadBalancerConfiguration configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ public void start() throws LifecycleException {
+ this.container = new SimpleUndertowLoadBalancer(configuration.getBindAddress(), configuration.getBindHttpPort(), configuration.getNodes());
+ this.container.start();
+ }
+
+ @Override
+ public void stop() throws LifecycleException {
+ log.info("Going to stop loadbalancer");
+ this.container.stop();
+ }
+
+ @Override
+ public ProtocolDescription getDefaultProtocol() {
+ return new ProtocolDescription("Servlet 3.1");
+ }
+
+ @Override
+ public ProtocolMetaData deploy(Archive<?> archive) throws DeploymentException {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void undeploy(Archive<?> archive) throws DeploymentException {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void deploy(Descriptor descriptor) throws DeploymentException {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public void undeploy(Descriptor descriptor) throws DeploymentException {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java
new file mode 100644
index 0000000..86a7e2e
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.arquillian.undertow;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+class SetSystemProperty {
+
+ private String name;
+ private String oldValue;
+
+ public SetSystemProperty(String name, String value) {
+ this.name = name;
+ this.oldValue = System.getProperty(name);
+
+ if (value == null) {
+ if (oldValue != null) {
+ System.getProperties().remove(name);
+ }
+ } else {
+ System.setProperty(name, value);
+ }
+ }
+
+ public void revert() {
+ String value = System.getProperty(name);
+
+ if (oldValue == null) {
+ if (value != null) {
+ System.getProperties().remove(name);
+ }
+ } else {
+ System.setProperty(name, oldValue);
+ }
+ }
+
+}
diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java
index 45bbc60..f8849b0 100644
--- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java
+++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java
@@ -47,7 +47,7 @@ public class ClientInitiatedAccountLinkServlet extends HttpServlet {
String realm = request.getParameter("realm");
KeycloakSecurityContext session = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
AccessToken token = session.getToken();
- String clientSessionId = token.getClientSession();
+ String clientId = token.getAudience()[0];
String nonce = UUID.randomUUID().toString();
MessageDigest md = null;
try {
@@ -55,7 +55,7 @@ public class ClientInitiatedAccountLinkServlet extends HttpServlet {
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
- String input = nonce + token.getSessionState() + clientSessionId + provider;
+ String input = nonce + token.getSessionState() + clientId + provider;
byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
String hash = Base64Url.encode(check);
request.getSession().setAttribute("hash", hash);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
index e583608..7e7ee6f 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
@@ -71,6 +71,8 @@ public class AuthServerTestEnricher {
private static final String AUTH_SERVER_CLUSTER_PROPERTY = "auth.server.cluster";
public static final boolean AUTH_SERVER_CLUSTER = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CLUSTER_PROPERTY, "false"));
+ private static final boolean AUTH_SERVER_UNDERTOW_CLUSTER = Boolean.parseBoolean(System.getProperty("auth.server.undertow.cluster", "false"));
+
private static final Boolean START_MIGRATION_CONTAINER = "auto".equals(System.getProperty("migration.mode")) ||
"manual".equals(System.getProperty("migration.mode"));
@@ -112,9 +114,25 @@ public class AuthServerTestEnricher {
suiteContext = new SuiteContext(containers);
- String authServerFrontend = AUTH_SERVER_CLUSTER
- ? "auth-server-balancer-wildfly" // if cluster mode enabled, load-balancer is the frontend
- : AUTH_SERVER_CONTAINER; // single-node mode
+ String authServerFrontend = null;
+
+ if (AUTH_SERVER_CLUSTER) {
+ // if cluster mode enabled, load-balancer is the frontend
+ for (ContainerInfo c : containers) {
+ if (c.getQualifier().startsWith("auth-server-balancer")) {
+ authServerFrontend = c.getQualifier();
+ }
+ }
+
+ if (authServerFrontend != null) {
+ log.info("Using frontend container: " + authServerFrontend);
+ } else {
+ throw new IllegalStateException("Not found frontend container");
+ }
+ } else {
+ authServerFrontend = AUTH_SERVER_CONTAINER; // single-node mode
+ }
+
String authServerBackend = AUTH_SERVER_CONTAINER + "-backend";
int backends = 0;
for (ContainerInfo container : suiteContext.getContainers()) {
@@ -130,6 +148,11 @@ public class AuthServerTestEnricher {
}
}
+ // Setup with 2 undertow backend nodes and no loadbalancer.
+// if (AUTH_SERVER_UNDERTOW_CLUSTER && suiteContext.getAuthServerInfo() == null && !suiteContext.getAuthServerBackendsInfo().isEmpty()) {
+// suiteContext.setAuthServerInfo(suiteContext.getAuthServerBackendsInfo().get(0));
+// }
+
// validate auth server setup
if (suiteContext.getAuthServerInfo() == null) {
throw new RuntimeException(String.format("No auth server container matching '%s' found in arquillian.xml.", authServerFrontend));
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/LoginPasswordUpdatePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/LoginPasswordUpdatePage.java
index a7d8706..9f3f196 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/LoginPasswordUpdatePage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/LoginPasswordUpdatePage.java
@@ -18,6 +18,7 @@
package org.keycloak.testsuite.page;
import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.junit.Assert;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@@ -53,6 +54,12 @@ public class LoginPasswordUpdatePage {
return driver.getTitle().equals("Update password");
}
+ public void assertCurrent() {
+ String name = getClass().getSimpleName();
+ Assert.assertTrue("Expected " + name + " but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")",
+ isCurrent());
+ }
+
public void open() {
throw new UnsupportedOperationException();
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java
index df8f1d0..346a0db 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java
@@ -33,6 +33,9 @@ public class InfoPage extends AbstractPage {
@FindBy(className = "instruction")
private WebElement infoMessage;
+ @FindBy(linkText = "« Back to Application")
+ private WebElement backToApplicationLink;
+
public String getInfo() {
return infoMessage.getText();
}
@@ -46,4 +49,8 @@ public class InfoPage extends AbstractPage {
throw new UnsupportedOperationException();
}
+ public void clickBackToApplicationLink() {
+ backToApplicationLink.click();
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginExpiredPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginExpiredPage.java
new file mode 100644
index 0000000..e3ff938
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginExpiredPage.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.pages;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LoginExpiredPage extends AbstractPage {
+
+ @FindBy(id = "loginRestartLink")
+ private WebElement loginRestartLink;
+
+ @FindBy(id = "loginContinueLink")
+ private WebElement loginContinueLink;
+
+
+ public void clickLoginRestartLink() {
+ loginRestartLink.click();
+ }
+
+ public void clickLoginContinueLink() {
+ loginContinueLink.click();
+ }
+
+
+ public boolean isCurrent() {
+ return driver.getTitle().equals("Page has expired");
+ }
+
+ public void open() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java
index 810ba84..0fb07bf 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java
@@ -54,6 +54,9 @@ public class RegisterPage extends AbstractPage {
@FindBy(className = "instruction")
private WebElement loginInstructionMessage;
+ @FindBy(linkText = "« Back to Login")
+ private WebElement backToLoginLink;
+
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) {
firstNameInput.clear();
@@ -125,6 +128,10 @@ public class RegisterPage extends AbstractPage {
submitButton.click();
}
+ public void clickBackToLogin() {
+ backToLoginLink.click();
+ }
+
public String getError() {
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java
index c1869d7..a6f42c8 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java
@@ -33,6 +33,7 @@ import org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.models.Constants;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.arquillian.SuiteContext;
import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
@@ -41,7 +42,7 @@ import static org.keycloak.testsuite.util.IOUtil.PROJECT_BUILD_DIRECTORY;
public class AdminClientUtil {
- public static Keycloak createAdminClient(boolean ignoreUnknownProperties) throws Exception {
+ public static Keycloak createAdminClient(boolean ignoreUnknownProperties, String authServerContextRoot) throws Exception {
SSLContext ssl = null;
if ("true".equals(System.getProperty("auth.server.ssl.required"))) {
File trustore = new File(PROJECT_BUILD_DIRECTORY, "dependency/keystore/keycloak.truststore");
@@ -61,12 +62,12 @@ public class AdminClientUtil {
jacksonProvider.setMapper(objectMapper);
}
- return Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth",
+ return Keycloak.getInstance(authServerContextRoot + "/auth",
MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID, null, ssl, jacksonProvider);
}
public static Keycloak createAdminClient() throws Exception {
- return createAdminClient(false);
+ return createAdminClient(false, AuthServerTestEnricher.getAuthServerContextRoot());
}
private static SSLContext getSSLContextWithTrustore(File file, String password) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
index ae7487d..bc0b787 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
@@ -74,4 +74,38 @@ public class GreenMailRule extends ExternalResource {
return greenMail.getReceivedMessages();
}
+ /**
+ * Returns the very last received message. When no message is available, returns {@code null}.
+ * @return see description
+ */
+ public MimeMessage getLastReceivedMessage() {
+ MimeMessage[] receivedMessages = greenMail.getReceivedMessages();
+ return (receivedMessages == null || receivedMessages.length == 0)
+ ? null
+ : receivedMessages[receivedMessages.length - 1];
+ }
+
+ /**
+ * Use this method if you are sending email in a different thread from the one you're testing from.
+ * Block waits for an email to arrive in any mailbox for any user.
+ * Implementation Detail: No polling wait implementation
+ *
+ * @param timeout maximum time in ms to wait for emailCount of messages to arrive before giving up and returning false
+ * @param emailCount waits for these many emails to arrive before returning
+ * @return
+ * @throws InterruptedException
+ */
+ public boolean waitForIncomingEmail(long timeout, int emailCount) throws InterruptedException {
+ return greenMail.waitForIncomingEmail(timeout, emailCount);
+ }
+
+ /**
+ * Does the same thing as Object.wait(long, int) but with a timeout of 5000ms.
+ * @param emailCount waits for these many emails to arrive before returning
+ * @return
+ * @throws InterruptedException
+ */
+ public boolean waitForIncomingEmail(int emailCount) throws InterruptedException {
+ return greenMail.waitForIncomingEmail(emailCount);
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 682e745..4c89eaa 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -41,6 +41,7 @@ import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
@@ -73,7 +74,7 @@ import java.util.*;
*/
public class OAuthClient {
public static final String SERVER_ROOT = AuthServerTestEnricher.getAuthServerContextRoot();
- public static final String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth";
+ public static String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth";
public static final String APP_ROOT = AUTH_SERVER_ROOT + "/realms/master/app";
private static final boolean sslRequired = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required"));
@@ -89,7 +90,7 @@ public class OAuthClient {
private String redirectUri;
- private String state;
+ private StateParamProvider state;
private String scope;
@@ -162,7 +163,9 @@ public class OAuthClient {
realm = "test";
clientId = "test-app";
redirectUri = APP_ROOT + "/auth";
- state = "mystate";
+ state = () -> {
+ return KeycloakModelUtils.generateId();
+ };
scope = null;
uiLocales = null;
clientSessionState = null;
@@ -607,6 +610,7 @@ public class OAuthClient {
if (redirectUri != null) {
b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
}
+ String state = this.state.getState();
if (state != null) {
b.queryParam(OAuth2Constants.STATE, state);
}
@@ -692,8 +696,17 @@ public class OAuthClient {
return this;
}
- public OAuthClient state(String state) {
- this.state = state;
+ public OAuthClient stateParamHardcoded(String value) {
+ this.state = () -> {
+ return value;
+ };
+ return this;
+ }
+
+ public OAuthClient stateParamRandom() {
+ this.state = () -> {
+ return KeycloakModelUtils.generateId();
+ };
return this;
}
@@ -927,4 +940,12 @@ public class OAuthClient {
return publicKeys.get(realm);
}
+
+ private interface StateParamProvider {
+
+ String getState();
+
+ }
+
+
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
index ae1db35..1739370 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
@@ -18,26 +18,19 @@ package org.keycloak.testsuite;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
-import org.apache.http.ssl.SSLContexts;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.Time;
import org.keycloak.testsuite.arquillian.KcArquillian;
import org.keycloak.testsuite.arquillian.TestContext;
-import java.io.File;
-import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
-import java.security.KeyManagementException;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import javax.net.ssl.SSLContext;
+
import javax.ws.rs.NotFoundException;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.drone.api.annotation.Drone;
@@ -53,7 +46,6 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RealmsResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
-import org.keycloak.models.Constants;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
@@ -77,7 +69,6 @@ import org.openqa.selenium.WebDriver;
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
-import static org.keycloak.testsuite.util.IOUtil.PROJECT_BUILD_DIRECTORY;
/**
*
@@ -135,7 +126,8 @@ public abstract class AbstractKeycloakTest {
public void beforeAbstractKeycloakTest() throws Exception {
adminClient = testContext.getAdminClient();
if (adminClient == null) {
- adminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting());
+ String authServerContextRoot = suiteContext.getAuthServerInfo().getContextRoot().toString();
+ adminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), authServerContextRoot);
testContext.setAdminClient(adminClient);
}
@@ -147,10 +139,9 @@ public abstract class AbstractKeycloakTest {
TestEventsLogger.setDriver(driver);
- if (!suiteContext.isAdminPasswordUpdated()) {
- log.debug("updating admin password");
+ // The backend cluster nodes may not be yet started. Password will be updated later for cluster setup.
+ if (!AuthServerTestEnricher.AUTH_SERVER_CLUSTER) {
updateMasterAdminPassword();
- suiteContext.setAdminPasswordUpdated(true);
}
if (testContext.getTestRealmReps() == null) {
@@ -202,10 +193,16 @@ public abstract class AbstractKeycloakTest {
return false;
}
- private void updateMasterAdminPassword() {
- welcomePage.navigateTo();
- if (!welcomePage.isPasswordSet()) {
- welcomePage.setPassword("admin", "admin");
+ protected void updateMasterAdminPassword() {
+ if (!suiteContext.isAdminPasswordUpdated()) {
+ log.debug("updating admin password");
+
+ welcomePage.navigateTo();
+ if (!welcomePage.isPasswordSet()) {
+ welcomePage.setPassword("admin", "admin");
+ }
+
+ suiteContext.setAdminPasswordUpdated(true);
}
}
@@ -236,7 +233,8 @@ public abstract class AbstractKeycloakTest {
if (testingClient == null) {
testingClient = testContext.getTestingClient();
if (testingClient == null) {
- testingClient = KeycloakTestingClient.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth");
+ String authServerContextRoot = suiteContext.getAuthServerInfo().getContextRoot().toString();
+ testingClient = KeycloakTestingClient.getInstance(authServerContextRoot + "/auth");
testContext.setTestingClient(testingClient);
}
}
@@ -348,6 +346,10 @@ public abstract class AbstractKeycloakTest {
userResource.update(userRepresentation);
}
+ /**
+ * Sets time offset in seconds that will be added to Time.currentTime() and Time.currentTimeMillis() both for client and server.
+ * @param offset
+ */
public void setTimeOffset(int offset) {
String response = invokeTimeOffset(offset);
resetTimeOffset = offset != 0;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index dcc4246..eba81f4 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -158,7 +158,6 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
@Before
public void before() {
- oauth.state("mystate"); // keycloak enforces that a state param has been sent by client
userId = findUser("test-user@localhost").getId();
// Revert any password policy and user password changes
@@ -854,7 +853,6 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
try {
OAuthClient oauth2 = new OAuthClient();
oauth2.init(adminClient, driver2);
- oauth2.state("mystate");
oauth2.doLogin("view-sessions", "password");
EventRepresentation login2Event = events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java
index 59d277c..7fb9692 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java
@@ -18,17 +18,23 @@ package org.keycloak.testsuite.account;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
+import org.junit.Rule;
import org.junit.Test;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventType;
+import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.auth.page.account.AccountManagement;
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
import org.keycloak.testsuite.auth.page.login.VerifyEmail;
import org.keycloak.testsuite.util.MailServerConfiguration;
-import org.keycloak.testsuite.util.RealmRepUtil;
import org.keycloak.testsuite.util.SslMailServer;
import static org.junit.Assert.assertEquals;
@@ -54,6 +60,9 @@ public class TrustStoreEmailTest extends AbstractTestRealmKeycloakTest {
@Page
private VerifyEmail testRealmVerifyEmailPage;
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
log.info("enable verify email and configure smtp server to run with ssl in test realm");
@@ -86,6 +95,15 @@ public class TrustStoreEmailTest extends AbstractTestRealmKeycloakTest {
accountManagement.navigateTo();
testRealmLoginPage.form().login(user.getUsername(), "password");
+ EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL)
+ .user(user.getId())
+ .client("account")
+ .detail(Details.USERNAME, "test-user@localhost")
+ .detail(Details.EMAIL, "test-user@localhost")
+ .removeDetail(Details.REDIRECT_URI)
+ .assertEvent();
+ String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
+
assertEquals("You need to verify your email address to activate your account.",
testRealmVerifyEmailPage.getFeedbackText());
@@ -96,6 +114,23 @@ public class TrustStoreEmailTest extends AbstractTestRealmKeycloakTest {
driver.navigate().to(verifyEmailUrl);
+ events.expectRequiredAction(EventType.VERIFY_EMAIL)
+ .user(user.getId())
+ .client("account")
+ .detail(Details.USERNAME, "test-user@localhost")
+ .detail(Details.EMAIL, "test-user@localhost")
+ .detail(Details.CODE_ID, mailCodeId)
+ .removeDetail(Details.REDIRECT_URI)
+ .assertEvent();
+
+ events.expectLogin()
+ .client("account")
+ .user(user.getId())
+ .session(mailCodeId)
+ .detail(Details.USERNAME, "test-user@localhost")
+ .removeDetail(Details.REDIRECT_URI)
+ .assertEvent();
+
assertCurrentUrlStartsWith(accountManagement);
accountManagement.signOut();
testRealmLoginPage.form().login(user.getUsername(), "password");
@@ -103,15 +138,27 @@ public class TrustStoreEmailTest extends AbstractTestRealmKeycloakTest {
}
@Test
- public void verifyEmailWithSslWrongCertificate() {
+ public void verifyEmailWithSslWrongCertificate() throws Exception {
UserRepresentation user = ApiUtil.findUserByUsername(testRealm(), "test-user@localhost");
SslMailServer.startWithSsl(this.getClass().getClassLoader().getResource(SslMailServer.INVALID_KEY).getFile());
accountManagement.navigateTo();
loginPage.form().login(user.getUsername(), "password");
- assertEquals("Failed to send email, please try again later.\n" +
- "« Back to Application",
- testRealmVerifyEmailPage.getErrorMessage());
+ events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL_ERROR)
+ .error(Errors.EMAIL_SEND_FAILED)
+ .user(user.getId())
+ .client("account")
+ .detail(Details.USERNAME, "test-user@localhost")
+ .detail(Details.EMAIL, "test-user@localhost")
+ .removeDetail(Details.REDIRECT_URI)
+ .assertEvent();
+
+ // Email wasn't send
+ Assert.assertNull(SslMailServer.getLastReceivedMessage());
+
+ // Email wasn't send, but we won't notify end user about that. Admin is aware due to the error in the logs and the SEND_VERIFY_EMAIL_ERROR event.
+ assertEquals("You need to verify your email address to activate your account.",
+ testRealmVerifyEmailPage.getFeedbackText());
}
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index eb719d8..9fd5c7a 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.actions;
+import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Before;
@@ -25,12 +26,15 @@ import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
+import org.keycloak.models.Constants;
+import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
@@ -47,6 +51,7 @@ import javax.mail.Multipart;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
+import org.hamcrest.Matchers;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -79,6 +84,8 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
@Page
protected ErrorPage errorPage;
+ private String testUserId;
+
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.setVerifyEmail(Boolean.TRUE);
@@ -87,14 +94,11 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
@Before
public void before() {
- oauth.state("mystate"); // have to set this as keycloak validates that state is sent
-
-
ApiUtil.removeUserByUsername(testRealm(), "test-user@localhost");
UserRepresentation user = UserBuilder.create().enabled(true)
.username("test-user@localhost")
.email("test-user@localhost").build();
- ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
+ testUserId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
}
/**
@@ -106,11 +110,11 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
loginPage.open();
loginPage.login("test-user@localhost", "password");
- Assert.assertTrue(verifyEmailPage.isCurrent());
+ verifyEmailPage.assertCurrent();
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
- MimeMessage message = greenMail.getReceivedMessages()[0];
+ MimeMessage message = greenMail.getLastReceivedMessage();
// see testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json
Assert.assertEquals("<auto+bounces@keycloak.org>", message.getHeader("Return-Path")[0]);
@@ -124,7 +128,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
loginPage.open();
loginPage.login("test-user@localhost", "password");
- Assert.assertTrue(verifyEmailPage.isCurrent());
+ verifyEmailPage.assertCurrent();
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
@@ -134,19 +138,21 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost");
EventRepresentation sendEvent = emailEvent.assertEvent();
- String sessionId = sendEvent.getSessionId();
-
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
- Assert.assertEquals(mailCodeId, verificationUrl.split("code=")[1].split("\\&")[0].split("\\.")[1]);
-
driver.navigate().to(verificationUrl.trim());
- events.expectRequiredAction(EventType.VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
+ events.expectRequiredAction(EventType.VERIFY_EMAIL)
+ .user(testUserId)
+ .detail(Details.USERNAME, "test-user@localhost")
+ .detail(Details.EMAIL, "test-user@localhost")
+ .detail(Details.CODE_ID, mailCodeId)
+ .assertEvent();
+ appPage.assertCurrent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().session(sessionId).detail(Details.CODE_ID, mailCodeId).assertEvent();
+ events.expectLogin().user(testUserId).session(mailCodeId).detail(Details.USERNAME, "test-user@localhost").assertEvent();
}
@Test
@@ -157,15 +163,13 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
String userId = events.expectRegister("verifyEmail", "email@mail.com").assertEvent().getUserId();
- Assert.assertTrue(verifyEmailPage.isCurrent());
+ verifyEmailPage.assertCurrent();
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
- EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).user(userId).detail("username", "verifyemail").detail("email", "email@mail.com").assertEvent();
- String sessionId = sendEvent.getSessionId();
-
+ EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).user(userId).detail(Details.USERNAME, "verifyemail").detail("email", "email@mail.com").assertEvent();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
String verificationUrl = getPasswordResetEmailLink(message);
@@ -174,9 +178,14 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectRequiredAction(EventType.VERIFY_EMAIL).user(userId).session(sessionId).detail("username", "verifyemail").detail("email", "email@mail.com").detail(Details.CODE_ID, mailCodeId).assertEvent();
+ events.expectRequiredAction(EventType.VERIFY_EMAIL)
+ .user(userId)
+ .detail(Details.USERNAME, "verifyemail")
+ .detail(Details.EMAIL, "email@mail.com")
+ .detail(Details.CODE_ID, mailCodeId)
+ .assertEvent();
- events.expectLogin().user(userId).session(sessionId).detail("username", "verifyemail").detail(Details.CODE_ID, mailCodeId).assertEvent();
+ events.expectLogin().user(userId).session(mailCodeId).detail(Details.USERNAME, "verifyemail").assertEvent();
}
@Test
@@ -184,40 +193,95 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
loginPage.open();
loginPage.login("test-user@localhost", "password");
- Assert.assertTrue(verifyEmailPage.isCurrent());
+ verifyEmailPage.assertCurrent();
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
- EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost").assertEvent();
- String sessionId = sendEvent.getSessionId();
-
+ EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL)
+ .detail("email", "test-user@localhost")
+ .assertEvent();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
verifyEmailPage.clickResendEmail();
+ verifyEmailPage.assertCurrent();
+
+ events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL)
+ .detail(Details.CODE_ID, mailCodeId)
+ .detail("email", "test-user@localhost")
+ .assertEvent();
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
- MimeMessage message = greenMail.getReceivedMessages()[1];
+ MimeMessage message = greenMail.getLastReceivedMessage();
+ String verificationUrl = getPasswordResetEmailLink(message);
+
+ driver.navigate().to(verificationUrl.trim());
+
+ appPage.assertCurrent();
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ events.expectRequiredAction(EventType.VERIFY_EMAIL)
+ .user(testUserId)
+ .detail(Details.USERNAME, "test-user@localhost")
+ .detail(Details.EMAIL, "test-user@localhost")
+ .detail(Details.CODE_ID, mailCodeId)
+ .assertEvent();
+
+ events.expectLogin().user(testUserId).session(mailCodeId).detail(Details.USERNAME, "test-user@localhost").assertEvent();
+ }
+
+ @Test
+ public void verifyEmailResendWithRefreshes() throws IOException, MessagingException {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ verifyEmailPage.assertCurrent();
+ driver.navigate().refresh();
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+ EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL)
+ .detail("email", "test-user@localhost")
+ .assertEvent();
+ String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
+
+ verifyEmailPage.clickResendEmail();
+ verifyEmailPage.assertCurrent();
+ driver.navigate().refresh();
+
+ events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL)
+ .detail(Details.CODE_ID, mailCodeId)
+ .detail("email", "test-user@localhost")
+ .assertEvent();
- events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").assertEvent(sendEvent);
+ Assert.assertEquals(2, greenMail.getReceivedMessages().length);
+ MimeMessage message = greenMail.getLastReceivedMessage();
String verificationUrl = getPasswordResetEmailLink(message);
driver.navigate().to(verificationUrl.trim());
+ appPage.assertCurrent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectRequiredAction(EventType.VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
+ events.expectRequiredAction(EventType.VERIFY_EMAIL)
+ .user(testUserId)
+ .detail(Details.USERNAME, "test-user@localhost")
+ .detail(Details.EMAIL, "test-user@localhost")
+ .detail(Details.CODE_ID, mailCodeId)
+ .assertEvent();
- events.expectLogin().session(sessionId).assertEvent();
+ events.expectLogin().user(testUserId).session(mailCodeId).detail(Details.USERNAME, "test-user@localhost").assertEvent();
}
@Test
- public void verifyEmailResendFirstInvalidSecondStillValid() throws IOException, MessagingException {
+ public void verifyEmailResendFirstStillValidEvenWithSecond() throws IOException, MessagingException {
+ // Email verification can be performed any number of times
loginPage.open();
loginPage.login("test-user@localhost", "password");
verifyEmailPage.clickResendEmail();
+ verifyEmailPage.assertCurrent();
Assert.assertEquals(2, greenMail.getReceivedMessages().length);
@@ -227,8 +291,8 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
driver.navigate().to(verificationUrl1.trim());
- assertTrue(errorPage.isCurrent());
- assertEquals("The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?", errorPage.getError());
+ appPage.assertCurrent();
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
MimeMessage message2 = greenMail.getReceivedMessages()[1];
@@ -236,7 +300,38 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
driver.navigate().to(verificationUrl2.trim());
- Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ infoPage.assertCurrent();
+ Assert.assertEquals("You are already logged in.", infoPage.getInfo());
+ }
+
+ @Test
+ public void verifyEmailResendFirstAndSecondStillValid() throws IOException, MessagingException {
+ // Email verification can be performed any number of times
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ verifyEmailPage.clickResendEmail();
+ verifyEmailPage.assertCurrent();
+
+ Assert.assertEquals(2, greenMail.getReceivedMessages().length);
+
+ MimeMessage message1 = greenMail.getReceivedMessages()[0];
+
+ String verificationUrl1 = getPasswordResetEmailLink(message1);
+
+ driver.navigate().to(verificationUrl1.trim());
+
+ appPage.assertCurrent();
+ appPage.logout();
+
+ MimeMessage message2 = greenMail.getReceivedMessages()[1];
+
+ String verificationUrl2 = getPasswordResetEmailLink(message2);
+
+ driver.navigate().to(verificationUrl2.trim());
+
+ infoPage.assertCurrent();
+ assertEquals("Your email address has been verified.", infoPage.getInfo());
}
@Test
@@ -244,62 +339,62 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
loginPage.open();
loginPage.login("test-user@localhost", "password");
- Assert.assertTrue(verifyEmailPage.isCurrent());
+ verifyEmailPage.assertCurrent();
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
- MimeMessage message = greenMail.getReceivedMessages()[0];
+ MimeMessage message = greenMail.getLastReceivedMessage();
String verificationUrl = getPasswordResetEmailLink(message);
AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost");
EventRepresentation sendEvent = emailEvent.assertEvent();
- String sessionId = sendEvent.getSessionId();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
- Assert.assertEquals(mailCodeId, verificationUrl.split("code=")[1].split("\\&")[0].split("\\.")[1]);
-
driver.manage().deleteAllCookies();
driver.navigate().to(verificationUrl.trim());
- events.expectRequiredAction(EventType.VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent();
+ events.expectRequiredAction(EventType.VERIFY_EMAIL)
+ .user(testUserId)
+ .detail(Details.CODE_ID, Matchers.not(Matchers.is(mailCodeId)))
+ .client(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID) // as authentication sessions are browser-specific,
+ // the client and redirect_uri is unrelated to
+ // the "test-app" specified in loginPage.open()
+ .detail(Details.REDIRECT_URI, Matchers.any(String.class))
+ .assertEvent();
- assertTrue(infoPage.isCurrent());
+ infoPage.assertCurrent();
assertEquals("Your email address has been verified.", infoPage.getInfo());
loginPage.open();
-
- assertTrue(loginPage.isCurrent());
+ loginPage.assertCurrent();
}
-
@Test
- public void verifyInvalidKeyOrCode() throws IOException, MessagingException {
+ public void verifyEmailInvalidKeyInVerficationLink() throws IOException, MessagingException {
loginPage.open();
loginPage.login("test-user@localhost", "password");
- Assert.assertTrue(verifyEmailPage.isCurrent());
- String resendEmailLink = verifyEmailPage.getResendEmailLink();
- String keyInsteadCodeURL = resendEmailLink.replace("code=", "key=");
+ verifyEmailPage.assertCurrent();
- events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost").assertEvent();
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
- driver.navigate().to(keyInsteadCodeURL);
+ MimeMessage message = greenMail.getLastReceivedMessage();
- events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR)
- .error(Errors.INVALID_CODE)
- .client((String)null)
- .user((String)null)
- .session((String)null)
- .clearDetails()
- .assertEvent();
+ String verificationUrl = getPasswordResetEmailLink(message);
+
+ verificationUrl = KeycloakUriBuilder.fromUri(verificationUrl).replaceQueryParam(Constants.KEY, "foo").build().toString();
+
+ events.poll();
+
+ driver.navigate().to(verificationUrl.trim());
- String badKeyURL = KeycloakUriBuilder.fromUri(resendEmailLink).replaceQueryParam("key", "foo").build().toString();
- driver.navigate().to(badKeyURL);
+ errorPage.assertCurrent();
+ assertEquals("An error occurred, please login again through your application.", errorPage.getError());
- events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR)
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR)
.error(Errors.INVALID_CODE)
.client((String)null)
.user((String)null)
@@ -309,33 +404,77 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
}
@Test
- public void verifyEmailBadCode() throws IOException, MessagingException {
+ public void verifyEmailExpiredCode() throws IOException, MessagingException {
loginPage.open();
loginPage.login("test-user@localhost", "password");
- Assert.assertTrue(verifyEmailPage.isCurrent());
+ verifyEmailPage.assertCurrent();
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
- MimeMessage message = greenMail.getReceivedMessages()[0];
+ MimeMessage message = greenMail.getLastReceivedMessage();
String verificationUrl = getPasswordResetEmailLink(message);
- verificationUrl = KeycloakUriBuilder.fromUri(verificationUrl).replaceQueryParam("code", "foo").build().toString();
+ events.poll();
+
+ try {
+ setTimeOffset(3600);
+
+ driver.navigate().to(verificationUrl.trim());
+
+ loginPage.assertCurrent();
+ assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
+
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR)
+ .error(Errors.EXPIRED_CODE)
+ .client((String)null)
+ .user(testUserId)
+ .session((String)null)
+ .clearDetails()
+ .detail(Details.ACTION, VerifyEmailActionToken.TOKEN_TYPE)
+ .assertEvent();
+ } finally {
+ setTimeOffset(0);
+ }
+ }
+
+ @Test
+ public void verifyEmailExpiredCodeAndExpiredSession() throws IOException, MessagingException {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ verifyEmailPage.assertCurrent();
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getLastReceivedMessage();
+
+ String verificationUrl = getPasswordResetEmailLink(message);
events.poll();
- driver.navigate().to(verificationUrl.trim());
+ try {
+ setTimeOffset(3600);
- assertEquals("The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?", errorPage.getError());
+ driver.manage().deleteAllCookies();
- events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR)
- .error(Errors.INVALID_CODE)
- .client((String)null)
- .user((String)null)
- .session((String)null)
- .clearDetails()
- .assertEvent();
+ driver.navigate().to(verificationUrl.trim());
+
+ errorPage.assertCurrent();
+ assertEquals("The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?", errorPage.getError());
+
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR)
+ .error(Errors.EXPIRED_CODE)
+ .client((String)null)
+ .user(testUserId)
+ .session((String)null)
+ .clearDetails()
+ .detail(Details.ACTION, VerifyEmailActionToken.TOKEN_TYPE)
+ .assertEvent();
+ } finally {
+ setTimeOffset(0);
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
index 433b0b1..124620a 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java
@@ -63,46 +63,50 @@ public class RequiredActionMultipleActionsTest extends AbstractTestRealmKeycloak
loginPage.open();
loginPage.login("test-user@localhost", "password");
- String sessionId = null;
+ String codeId = null;
if (changePasswordPage.isCurrent()) {
- sessionId = updatePassword(sessionId);
+ codeId = updatePassword(codeId);
updateProfilePage.assertCurrent();
- updateProfile(sessionId);
+ updateProfile(codeId);
} else if (updateProfilePage.isCurrent()) {
- sessionId = updateProfile(sessionId);
+ codeId = updateProfile(codeId);
changePasswordPage.assertCurrent();
- updatePassword(sessionId);
+ updatePassword(codeId);
} else {
Assert.fail("Expected to update password and profile before login");
}
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().session(sessionId).assertEvent();
+ events.expectLogin().session(codeId).assertEvent();
}
- public String updatePassword(String sessionId) {
+ public String updatePassword(String codeId) {
changePasswordPage.changePassword("new-password", "new-password");
AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_PASSWORD);
- if (sessionId != null) {
- expectedEvent.session(sessionId);
+ if (codeId != null) {
+ expectedEvent.detail(Details.CODE_ID, codeId);
}
- return expectedEvent.assertEvent().getSessionId();
+ return expectedEvent.assertEvent().getDetails().get(Details.CODE_ID);
}
- public String updateProfile(String sessionId) {
+ public String updateProfile(String codeId) {
updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost");
- AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com");
- if (sessionId != null) {
- expectedEvent.session(sessionId);
+ AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_EMAIL)
+ .detail(Details.PREVIOUS_EMAIL, "test-user@localhost")
+ .detail(Details.UPDATED_EMAIL, "new@email.com");
+ if (codeId != null) {
+ expectedEvent.detail(Details.CODE_ID, codeId);
}
- sessionId = expectedEvent.assertEvent().getSessionId();
- events.expectRequiredAction(EventType.UPDATE_PROFILE).session(sessionId).assertEvent();
- return sessionId;
+ codeId = expectedEvent.assertEvent().getDetails().get(Details.CODE_ID);
+ events.expectRequiredAction(EventType.UPDATE_PROFILE)
+ .detail(Details.CODE_ID, codeId)
+ .assertEvent();
+ return codeId;
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
index d457b0b..59f88fe 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java
@@ -59,11 +59,6 @@ public class RequiredActionResetPasswordTest extends AbstractTestRealmKeycloakTe
@Page
protected LoginPasswordUpdatePage changePasswordPage;
- @Before
- public void before() {
- oauth.state("mystate"); // have to set this as keycloak validates that state is sent
- }
-
@Test
public void tempPassword() throws Exception {
@@ -73,11 +68,11 @@ public class RequiredActionResetPasswordTest extends AbstractTestRealmKeycloakTe
changePasswordPage.assertCurrent();
changePasswordPage.changePassword("new-password", "new-password");
- String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent().getSessionId();
+ events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
+ EventRepresentation loginEvent = events.expectLogin().assertEvent();
oauth.openLogout();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
index f613087..a06079c 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java
@@ -127,11 +127,12 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));
- String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent().getSessionId();
+ String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent()
+ .getDetails().get(Details.CODE_ID);
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setuptotp").assertEvent();
+ events.expectLogin().user(userId).session(authSessionId).detail(Details.USERNAME, "setuptotp").assertEvent();
}
@Test
@@ -145,15 +146,16 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
totpPage.configure(totp.generateTOTP(totpSecret));
- String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId();
+ String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
+ .getDetails().get(Details.CODE_ID);
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent();
+ EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent();
oauth.openLogout();
- events.expectLogout(loginEvent.getSessionId()).assertEvent();
+ events.expectLogout(authSessionId).assertEvent();
loginPage.open();
loginPage.login("test-user@localhost", "password");
@@ -229,7 +231,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
totpPage.assertCurrent();
totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret()));
- String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent().getSessionId();
+ String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent()
+ .getDetails().get(Details.CODE_ID);
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
@@ -260,7 +263,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
TimeBasedOTP timeBased = new TimeBasedOTP(HmacOTP.HMAC_SHA1, 8, 30, 1);
totpPage.configure(timeBased.generateTOTP(totpSecret));
- String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId();
+ String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
+ .getDetails().get(Details.CODE_ID);
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
@@ -311,7 +315,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest {
HmacOTP otpgen = new HmacOTP(6, HmacOTP.HMAC_SHA1, 1);
totpPage.configure(otpgen.generateHOTP(totpSecret, 0));
String uri = driver.getCurrentUrl();
- String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId();
+ String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent()
+ .getDetails().get(Details.CODE_ID);
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java
index 80ee0fe..9c18070 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.actions;
+import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Before;
@@ -89,12 +90,12 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe
updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost");
- String sessionId = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent().getSessionId();
- events.expectRequiredAction(EventType.UPDATE_PROFILE).session(sessionId).assertEvent();
+ events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent();
+ events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().session(sessionId).assertEvent();
+ events.expectLogin().assertEvent();
// assert user is really updated in persistent store
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
@@ -116,19 +117,17 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe
updateProfilePage.update("New first", "New last", "john-doh@localhost", "new");
- String sessionId = events
- .expectLogin()
+ events.expectLogin()
.event(EventType.UPDATE_PROFILE)
.detail(Details.USERNAME, "john-doh@localhost")
.user(userId)
- .session(AssertEvents.isUUID())
+ .session(Matchers.nullValue(String.class))
.removeDetail(Details.CONSENT)
- .assertEvent()
- .getSessionId();
+ .assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().detail(Details.USERNAME, "john-doh@localhost").user(userId).session(sessionId).assertEvent();
+ events.expectLogin().detail(Details.USERNAME, "john-doh@localhost").user(userId).assertEvent();
// assert user is really updated in persistent store
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "new");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java
index ab52294..86066e8 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.actions;
+import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Before;
@@ -86,11 +87,11 @@ public class TermsAndConditionsTest extends AbstractTestRealmKeycloakTest {
termsPage.acceptTerms();
- String sessionId = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI).detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent().getSessionId();
+ events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI).detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().session(sessionId).assertEvent();
+ events.expectLogin().assertEvent();
// assert user attribute is properly set
UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost");
@@ -123,6 +124,7 @@ public class TermsAndConditionsTest extends AbstractTestRealmKeycloakTest {
events.expectLogin().event(EventType.CUSTOM_REQUIRED_ACTION_ERROR).detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID)
.error(Errors.REJECTED_BY_USER)
.removeDetail(Details.CONSENT)
+ .session(Matchers.nullValue(String.class))
.assertEvent();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java
index 5c3f1f1..f6a8bb2 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java
@@ -28,6 +28,8 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
+import javax.ws.rs.core.Response;
+
import org.jboss.arquillian.container.test.api.Deployer;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.junit.BeforeClass;
@@ -199,7 +201,8 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda
assertFalse(policy.getUsers().isEmpty());
- getAuthorizationResource().policies().user().create(policy);
+ Response response = getAuthorizationResource().policies().user().create(policy);
+ response.close();
}
protected interface ExceptionRunnable {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java
index b37e9e6..49158fa 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java
@@ -23,6 +23,8 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
+import javax.ws.rs.core.Response;
+
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
@@ -289,7 +291,8 @@ public abstract class AbstractServletAuthzFunctionalAdapterTest extends Abstract
policy.addClient("admin-cli");
ClientPoliciesResource policyResource = getAuthorizationResource().policies().client();
- policyResource.create(policy);
+ Response response = policyResource.create(policy);
+ response.close();
policy = policyResource.findByName(policy.getName());
updatePermissionPolicies("Protected Resource Permission", policy.getName());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java
index 72e8c46..750df03 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java
@@ -38,10 +38,14 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
+import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.broker.BrokerTestTools;
import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
+import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
+import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.WaitUtils;
@@ -72,11 +76,17 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer
public static final String PARENT_USERNAME = "parent";
@Page
- protected UpdateAccountInformationPage profilePage;
+ protected LoginUpdateProfilePage loginUpdateProfilePage;
+
+ @Page
+ protected AccountUpdateProfilePage profilePage;
@Page
private LoginPage loginPage;
+ @Page
+ protected ErrorPage errorPage;
+
public static class ClientApp extends AbstractPageWithInjectedUrl {
public static final String DEPLOYMENT_NAME = "client-linking";
@@ -532,6 +542,92 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer
}
+
+ @Test
+ public void testAccountNotLinkedAutomatically() throws Exception {
+ RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+ List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ // Login to account mgmt first
+ profilePage.open(CHILD_IDP);
+ WaitUtils.waitForPageToLoad(driver);
+
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.login("child", "password");
+ profilePage.assertCurrent();
+
+ // Now in another tab, open login screen with "prompt=login" . Login screen will be displayed even if I have SSO cookie
+ UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
+ .path("nosuch");
+ String linkUrl = linkBuilder.clone()
+ .queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
+ .build().toString();
+
+ navigateTo(linkUrl);
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.clickSocial(PARENT_IDP);
+ Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
+ loginPage.login(PARENT_USERNAME, "password");
+
+ // Test I was not automatically linked.
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ loginUpdateProfilePage.assertCurrent();
+ loginUpdateProfilePage.update("Joe", "Doe", "joe@parent.com");
+
+ errorPage.assertCurrent();
+ Assert.assertEquals("You are already authenticated as different user 'child' in this session. Please logout first.", errorPage.getError());
+
+ logoutAll();
+
+ // Remove newly created user
+ String newUserId = ApiUtil.findUserByUsername(realm, "parent").getId();
+ getCleanup("child").addUserId(newUserId);
+ }
+
+
+ @Test
+ public void testAccountLinkingExpired() throws Exception {
+ RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+ List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ // Login to account mgmt first
+ profilePage.open(CHILD_IDP);
+ WaitUtils.waitForPageToLoad(driver);
+
+ Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+ loginPage.login("child", "password");
+ profilePage.assertCurrent();
+
+ // Now in another tab, request account linking
+ UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
+ .path("link");
+ String linkUrl = linkBuilder.clone()
+ .queryParam("realm", CHILD_IDP)
+ .queryParam("provider", PARENT_IDP).build().toString();
+ navigateTo(linkUrl);
+
+ Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
+
+ // Logout "child" userSession in the meantime (for example through admin request)
+ realm.logoutAll();
+
+ // Finish login on parent.
+ loginPage.login(PARENT_USERNAME, "password");
+
+ // Test I was not automatically linked
+ links = realm.users().get(childUserId).getFederatedIdentity();
+ Assert.assertTrue(links.isEmpty());
+
+ errorPage.assertCurrent();
+ Assert.assertEquals("Requested broker account linking, but current session is no longer valid.", errorPage.getError());
+
+ logoutAll();
+ }
+
private void navigateTo(String uri) {
driver.navigate().to(uri);
WaitUtils.waitForPageToLoad(driver);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java
index cc47020..56f21ab 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java
@@ -137,6 +137,13 @@ public class ApiUtil {
return realm.users().get(findUserByUsername(realm, username).getId());
}
+ /**
+ * Creates a user
+ * @param realm
+ * @param user
+ * @param password
+ * @return ID of the new user
+ */
public static String createUserWithAdminClient(RealmResource realm, UserRepresentation user) {
Response response = realm.users().create(user);
String createdId = getCreatedId(response);
@@ -144,6 +151,13 @@ public class ApiUtil {
return createdId;
}
+ /**
+ * Creates a user and sets the password
+ * @param realm
+ * @param user
+ * @param password
+ * @return ID of the new user
+ */
public static String createUserAndResetPasswordWithAdminClient(RealmResource realm, UserRepresentation user, String password) {
String id = createUserWithAdminClient(realm, user);
resetUserPassword(realm.users().get(id), password, false);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractPolicyManagementTest.java
index e0a4c53..41f3890 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractPolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractPolicyManagementTest.java
@@ -28,6 +28,8 @@ import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
+import javax.ws.rs.core.Response;
+
import org.junit.Before;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
@@ -39,7 +41,6 @@ import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
-import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
@@ -131,7 +132,10 @@ public abstract class AbstractPolicyManagementTest extends AbstractKeycloakTest
resources.add(new ResourceRepresentation("Resource B", scopes));
resources.add(new ResourceRepresentation("Resource C", scopes));
- resources.forEach(resource -> getClient().authorization().resources().create(resource));
+ resources.forEach(resource -> {
+ Response response = getClient().authorization().resources().create(resource);
+ response.close();
+ });
}
private void createPolicies(RealmResource realm, ClientResource client) throws IOException {
@@ -147,7 +151,8 @@ public abstract class AbstractPolicyManagementTest extends AbstractKeycloakTest
representation.setName(name);
representation.addUser(userId);
- client.authorization().policies().user().create(representation);
+ Response response = client.authorization().policies().user().create(representation);
+ response.close();
}
protected ClientResource getClient() {
@@ -161,7 +166,7 @@ public abstract class AbstractPolicyManagementTest extends AbstractKeycloakTest
protected RealmResource getRealm() {
try {
- return AdminClientUtil.createAdminClient().realm("authz-test");
+ return adminClient.realm("authz-test");
} catch (Exception cause) {
throw new RuntimeException("Failed to create admin client", cause);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClientPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClientPolicyManagementTest.java
index 87848d9..d3613da 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClientPolicyManagementTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClientPolicyManagementTest.java
@@ -112,6 +112,7 @@ public class ClientPolicyManagementTest extends AbstractPolicyManagementTest {
ClientPoliciesResource policies = authorization.policies().client();
Response response = policies.create(representation);
ClientPolicyRepresentation created = response.readEntity(ClientPolicyRepresentation.class);
+ response.close();
policies.findById(created.getId()).remove();
@@ -136,6 +137,7 @@ public class ClientPolicyManagementTest extends AbstractPolicyManagementTest {
ClientPoliciesResource policies = authorization.policies().client();
Response response = policies.create(representation);
ClientPolicyRepresentation created = response.readEntity(ClientPolicyRepresentation.class);
+ response.close();
PolicyResource policy = authorization.policies().policy(created.getId());
PolicyRepresentation genericConfig = policy.toRepresentation();
@@ -152,6 +154,7 @@ public class ClientPolicyManagementTest extends AbstractPolicyManagementTest {
ClientPoliciesResource permissions = authorization.policies().client();
Response response = permissions.create(representation);
ClientPolicyRepresentation created = response.readEntity(ClientPolicyRepresentation.class);
+ response.close();
ClientPolicyResource permission = permissions.findById(created.getId());
assertRepresentation(representation, permission);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
index d821179..531157d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
@@ -46,8 +46,6 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
-import org.keycloak.representations.idm.UserFederationMapperRepresentation;
-import org.keycloak.representations.idm.UserFederationProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
@@ -64,7 +62,6 @@ import org.keycloak.testsuite.util.FederatedIdentityBuilder;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.IdentityProviderBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
-import org.keycloak.testsuite.util.TestCleanup;
import org.keycloak.testsuite.util.UserBuilder;
import javax.ws.rs.ClientErrorException;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
index e5c1287..ba0b7c9 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java
@@ -255,6 +255,8 @@ public class RealmTest extends AbstractAdminTest {
rep.setSsoSessionIdleTimeout(123);
rep.setSsoSessionMaxLifespan(12);
rep.setAccessCodeLifespanLogin(1234);
+ rep.setActionTokenGeneratedByAdminLifespan(2345);
+ rep.setActionTokenGeneratedByUserLifespan(3456);
rep.setRegistrationAllowed(true);
rep.setRegistrationEmailAsUsername(true);
rep.setEditUsernameAllowed(true);
@@ -267,6 +269,8 @@ public class RealmTest extends AbstractAdminTest {
assertEquals(123, rep.getSsoSessionIdleTimeout().intValue());
assertEquals(12, rep.getSsoSessionMaxLifespan().intValue());
assertEquals(1234, rep.getAccessCodeLifespanLogin().intValue());
+ assertEquals(2345, rep.getActionTokenGeneratedByAdminLifespan().intValue());
+ assertEquals(3456, rep.getActionTokenGeneratedByUserLifespan().intValue());
assertEquals(Boolean.TRUE, rep.isRegistrationAllowed());
assertEquals(Boolean.TRUE, rep.isRegistrationEmailAsUsername());
assertEquals(Boolean.TRUE, rep.isEditUsernameAllowed());
@@ -443,6 +447,12 @@ public class RealmTest extends AbstractAdminTest {
if (realm.getAccessCodeLifespan() != null) assertEquals(realm.getAccessCodeLifespan(), storedRealm.getAccessCodeLifespan());
if (realm.getAccessCodeLifespanUserAction() != null)
assertEquals(realm.getAccessCodeLifespanUserAction(), storedRealm.getAccessCodeLifespanUserAction());
+ if (realm.getActionTokenGeneratedByAdminLifespan() != null)
+ assertEquals(realm.getActionTokenGeneratedByAdminLifespan(), storedRealm.getActionTokenGeneratedByAdminLifespan());
+ if (realm.getActionTokenGeneratedByUserLifespan() != null)
+ assertEquals(realm.getActionTokenGeneratedByUserLifespan(), storedRealm.getActionTokenGeneratedByUserLifespan());
+ else
+ assertEquals(realm.getAccessCodeLifespanUserAction(), storedRealm.getActionTokenGeneratedByUserLifespan());
if (realm.getNotBefore() != null) assertEquals(realm.getNotBefore(), storedRealm.getNotBefore());
if (realm.getAccessTokenLifespan() != null) assertEquals(realm.getAccessTokenLifespan(), storedRealm.getAccessTokenLifespan());
if (realm.getAccessTokenLifespanForImplicitFlow() != null) assertEquals(realm.getAccessTokenLifespanForImplicitFlow(), storedRealm.getAccessTokenLifespanForImplicitFlow());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 6a75b33..3d99124 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -21,9 +21,7 @@ import org.hamcrest.Matchers;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.test.api.ArquillianResource;
-import org.junit.After;
import org.junit.Assert;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.IdentityProviderResource;
@@ -47,6 +45,7 @@ import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.page.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.AdminEventPaths;
@@ -62,7 +61,6 @@ import org.openqa.selenium.WebDriver;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.ws.rs.ClientErrorException;
-import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
@@ -71,6 +69,7 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -99,6 +98,9 @@ public class UserTest extends AbstractAdminTest {
protected InfoPage infoPage;
@Page
+ protected ErrorPage errorPage;
+
+ @Page
protected LoginPage loginPage;
public String createUser() {
@@ -541,7 +543,110 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
- assertTrue(passwordUpdatePage.isCurrent());
+ passwordUpdatePage.assertCurrent();
+
+ passwordUpdatePage.changePassword("new-pass", "new-pass");
+
+ assertEquals("Your account has been updated.", driver.getTitle());
+
+ driver.navigate().to(link);
+
+ assertEquals("We're sorry...", driver.getTitle());
+ }
+
+ @Test
+ public void sendResetPasswordEmailSuccessTokenShortLifespan() throws IOException, MessagingException {
+ UserRepresentation userRep = new UserRepresentation();
+ userRep.setEnabled(true);
+ userRep.setUsername("user1");
+ userRep.setEmail("user1@test.com");
+
+ String id = createUser(userRep);
+
+ final AtomicInteger originalValue = new AtomicInteger();
+
+ RealmRepresentation realmRep = realm.toRepresentation();
+ originalValue.set(realmRep.getActionTokenGeneratedByAdminLifespan());
+ realmRep.setActionTokenGeneratedByAdminLifespan(60);
+ realm.update(realmRep);
+
+ try {
+ UserResource user = realm.users().get(id);
+ List<String> actions = new LinkedList<>();
+ actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name());
+ user.executeActionsEmail(actions);
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String link = MailUtils.getPasswordResetEmailLink(message);
+
+ setTimeOffset(70);
+
+ driver.navigate().to(link);
+
+ errorPage.assertCurrent();
+ assertEquals("An error occurred, please login again through your application.", errorPage.getError());
+ } finally {
+ setTimeOffset(0);
+
+ realmRep.setActionTokenGeneratedByAdminLifespan(originalValue.get());
+ realm.update(realmRep);
+ }
+ }
+
+ @Test
+ public void sendResetPasswordEmailSuccessWithRecycledAuthSession() throws IOException, MessagingException {
+ UserRepresentation userRep = new UserRepresentation();
+ userRep.setEnabled(true);
+ userRep.setUsername("user1");
+ userRep.setEmail("user1@test.com");
+
+ String id = createUser(userRep);
+
+ UserResource user = realm.users().get(id);
+ List<String> actions = new LinkedList<>();
+ actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name());
+
+ // The following block creates a client and requests updating password with redirect to this client.
+ // After clicking the link (starting a fresh auth session with client), the user goes away and sends the email
+ // with password reset again - now without the client - and attempts to complete the password reset.
+ {
+ ClientRepresentation client = new ClientRepresentation();
+ client.setClientId("myclient2");
+ client.setRedirectUris(new LinkedList<>());
+ client.getRedirectUris().add("http://myclient.com/*");
+ client.setName("myclient2");
+ client.setEnabled(true);
+ Response response = realm.clients().create(client);
+ String createdId = ApiUtil.getCreatedId(response);
+ assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.clientResourcePath(createdId), client, ResourceType.CLIENT);
+
+ user.executeActionsEmail("myclient2", "http://myclient.com/home.html", actions);
+ assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER);
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String link = MailUtils.getPasswordResetEmailLink(message);
+
+ driver.navigate().to(link);
+ }
+
+ user.executeActionsEmail(actions);
+ assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER);
+
+ Assert.assertEquals(2, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
+
+ String link = MailUtils.getPasswordResetEmailLink(message);
+
+ driver.navigate().to(link);
+
+ passwordUpdatePage.assertCurrent();
passwordUpdatePage.changePassword("new-pass", "new-pass");
@@ -601,7 +706,7 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
- assertTrue(passwordUpdatePage.isCurrent());
+ passwordUpdatePage.assertCurrent();
passwordUpdatePage.changePassword("new-pass", "new-pass");
@@ -673,6 +778,11 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
Assert.assertEquals("Your account has been updated.", infoPage.getInfo());
+
+ driver.navigate().to("about:blank");
+
+ driver.navigate().to(link); // It should be possible to use the same action token multiple times
+ Assert.assertEquals("Your account has been updated.", infoPage.getInfo());
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java
index 4445de9..098c05a 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java
@@ -20,6 +20,7 @@ package org.keycloak.testsuite;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Assert;
import org.junit.rules.TestRule;
@@ -39,6 +40,7 @@ import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import static org.hamcrest.Matchers.is;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -88,7 +90,7 @@ public class AssertEvents implements TestRule {
}
public ExpectedEvent expectRequiredAction(EventType event) {
- return expectLogin().event(event).removeDetail(Details.CONSENT).session(isUUID());
+ return expectLogin().event(event).removeDetail(Details.CONSENT).session(Matchers.isEmptyOrNullString());
}
public ExpectedEvent expectLogin() {
@@ -175,7 +177,7 @@ public class AssertEvents implements TestRule {
private Matcher<String> realmId;
private Matcher<String> userId;
private Matcher<String> sessionId;
- private HashMap<String, Matcher<String>> details;
+ private HashMap<String, Matcher<? super String>> details;
public ExpectedEvent realm(Matcher<String> realmId) {
this.realmId = realmId;
@@ -240,9 +242,9 @@ public class AssertEvents implements TestRule {
return detail(key, CoreMatchers.equalTo(value));
}
- public ExpectedEvent detail(String key, Matcher<String> matcher) {
+ public ExpectedEvent detail(String key, Matcher<? super String> matcher) {
if (details == null) {
- details = new HashMap<String, Matcher<String>>();
+ details = new HashMap<String, Matcher<? super String>>();
}
details.put(key, matcher);
return this;
@@ -270,28 +272,28 @@ public class AssertEvents implements TestRule {
}
public EventRepresentation assertEvent(EventRepresentation actual) {
- if (expected.getError() != null && !expected.getType().toString().endsWith("_ERROR")) {
+ if (expected.getError() != null && ! expected.getType().toString().endsWith("_ERROR")) {
expected.setType(expected.getType() + "_ERROR");
}
- Assert.assertEquals(expected.getType(), actual.getType());
- Assert.assertThat(actual.getRealmId(), realmId);
- Assert.assertEquals(expected.getClientId(), actual.getClientId());
- Assert.assertEquals(expected.getError(), actual.getError());
- Assert.assertEquals(expected.getIpAddress(), actual.getIpAddress());
- Assert.assertThat(actual.getUserId(), userId);
- Assert.assertThat(actual.getSessionId(), sessionId);
+ Assert.assertThat("type", actual.getType(), is(expected.getType()));
+ Assert.assertThat("realm ID", actual.getRealmId(), is(realmId));
+ Assert.assertThat("client ID", actual.getClientId(), is(expected.getClientId()));
+ Assert.assertThat("error", actual.getError(), is(expected.getError()));
+ Assert.assertThat("ip address", actual.getIpAddress(), is(expected.getIpAddress()));
+ Assert.assertThat("user ID", actual.getUserId(), is(userId));
+ Assert.assertThat("session ID", actual.getSessionId(), is(sessionId));
if (details == null || details.isEmpty()) {
// Assert.assertNull(actual.getDetails());
} else {
Assert.assertNotNull(actual.getDetails());
- for (Map.Entry<String, Matcher<String>> d : details.entrySet()) {
+ for (Map.Entry<String, Matcher<? super String>> d : details.entrySet()) {
String actualValue = actual.getDetails().get(d.getKey());
if (!actual.getDetails().containsKey(d.getKey())) {
Assert.fail(d.getKey() + " missing");
}
- Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, d.getValue());
+ Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, is(d.getValue()));
}
/*
for (String k : actual.getDetails().keySet()) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java
index 6db4891..45a937c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java
@@ -50,7 +50,6 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
-import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
@@ -140,7 +139,7 @@ public class ConflictingScopePermissionTest extends AbstractKeycloakTest {
}
private RealmResource getRealm() throws Exception {
- return AdminClientUtil.createAdminClient().realm("authz-test");
+ return adminClient.realm("authz-test");
}
private ClientResource getClient(RealmResource realm) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RequireUmaAuthorizationScopeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RequireUmaAuthorizationScopeTest.java
index e2eae34..cf54a66 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RequireUmaAuthorizationScopeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RequireUmaAuthorizationScopeTest.java
@@ -24,6 +24,8 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
+import javax.ws.rs.core.Response;
+
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.AuthorizationResource;
@@ -43,7 +45,6 @@ import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
-import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.RoleBuilder;
@@ -77,14 +78,16 @@ public class RequireUmaAuthorizationScopeTest extends AbstractKeycloakTest {
AuthorizationResource authorization = client.authorization();
ResourceRepresentation resource = new ResourceRepresentation("Resource A");
- authorization.resources().create(resource);
+ Response response = authorization.resources().create(resource);
+ response.close();
JSPolicyRepresentation policy = new JSPolicyRepresentation();
policy.setName("Default Policy");
policy.setCode("$evaluation.grant();");
- authorization.policies().js().create(policy);
+ response = authorization.policies().js().create(policy);
+ response.close();
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
@@ -92,7 +95,8 @@ public class RequireUmaAuthorizationScopeTest extends AbstractKeycloakTest {
permission.addResource(resource.getName());
permission.addPolicy(policy.getName());
- authorization.permissions().resource().create(permission);
+ response = authorization.permissions().resource().create(permission);
+ response.close();
}
@Test
@@ -140,7 +144,7 @@ public class RequireUmaAuthorizationScopeTest extends AbstractKeycloakTest {
}
private RealmResource getRealm() throws Exception {
- return AdminClientUtil.createAdminClient().realm("authz-test");
+ return adminClient.realm("authz-test");
}
private ClientResource getClient(RealmResource realm) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java
index 25abe65..d5c9ff2 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java
@@ -45,6 +45,12 @@ public abstract class AbstractClusterTest extends AbstractKeycloakTest {
logFailoverSetup();
}
+ // Assume that route like "node6" will have corresponding backend container like "auth-server-wildfly-backend6"
+ protected void setCurrentFailNodeForRoute(String route) {
+ String routeNumber = route.substring(route.length() - 1);
+ currentFailNodeIndex = Integer.parseInt(routeNumber) - 1;
+ }
+
protected ContainerInfo getCurrentFailNode() {
return backendNode(currentFailNodeIndex);
}
@@ -111,9 +117,13 @@ public abstract class AbstractClusterTest extends AbstractKeycloakTest {
}
protected Keycloak getAdminClientFor(ContainerInfo node) {
- return node.equals(suiteContext.getAuthServerInfo())
- ? adminClient // frontend client
- : backendAdminClients.get(node);
+ Keycloak adminClient = backendAdminClients.get(node);
+
+ if (adminClient == null && node.equals(suiteContext.getAuthServerInfo())) {
+ adminClient = this.adminClient;
+ }
+
+ return adminClient;
}
@Before
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractFailoverClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractFailoverClusterTest.java
new file mode 100644
index 0000000..aa65e79
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractFailoverClusterTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.cluster;
+
+
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.page.AbstractPage;
+import org.keycloak.testsuite.page.PageWithLogOutAction;
+import org.openqa.selenium.Cookie;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+import static org.keycloak.testsuite.util.WaitUtils.pause;
+
+public abstract class AbstractFailoverClusterTest extends AbstractClusterTest {
+
+ public static final String KEYCLOAK_SESSION_COOKIE = "KEYCLOAK_SESSION";
+
+ public static final Integer SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("session.cache.owners", "1"));
+ public static final Integer OFFLINE_SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("offline.session.cache.owners", "1"));
+ public static final Integer LOGIN_FAILURES_CACHE_OWNERS = Integer.parseInt(System.getProperty("login.failure.cache.owners", "1"));
+
+ public static final Integer REBALANCE_WAIT = Integer.parseInt(System.getProperty("rebalance.wait", "5000"));
+
+ @Override
+ public void addTestRealms(List<RealmRepresentation> testRealms) {
+ }
+
+
+ /**
+ * failure --> failback --> failure of next node
+ */
+ protected void switchFailedNode() {
+ assertFalse(controller.isStarted(getCurrentFailNode().getQualifier()));
+
+ failback();
+ pause(REBALANCE_WAIT);
+
+ iterateCurrentFailNode();
+
+ failure();
+ pause(REBALANCE_WAIT);
+
+ assertFalse(controller.isStarted(getCurrentFailNode().getQualifier()));
+ }
+
+ protected Cookie login(AbstractPage targetPage) {
+ targetPage.navigateTo();
+ assertCurrentUrlStartsWith(loginPage);
+ loginPage.form().login(ADMIN, ADMIN);
+ assertCurrentUrlStartsWith(targetPage);
+ Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
+ assertNotNull(sessionCookie);
+ return sessionCookie;
+ }
+
+ protected void logout(AbstractPage targetPage) {
+ if (!(targetPage instanceof PageWithLogOutAction)) {
+ throw new IllegalArgumentException(targetPage.getClass().getSimpleName() + " must implement PageWithLogOutAction interface");
+ }
+ targetPage.navigateTo();
+ assertCurrentUrlStartsWith(targetPage);
+ ((PageWithLogOutAction) targetPage).logOut();
+ }
+
+ protected Cookie verifyLoggedIn(AbstractPage targetPage, Cookie sessionCookieForVerification) {
+ // verify on realm path
+ masterRealmPage.navigateTo();
+ Cookie sessionCookieOnRealmPath = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
+ assertNotNull(sessionCookieOnRealmPath);
+ assertEquals(sessionCookieOnRealmPath.getValue(), sessionCookieForVerification.getValue());
+ // verify on target page
+ targetPage.navigateTo();
+ assertCurrentUrlStartsWith(targetPage);
+ Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
+ assertNotNull(sessionCookie);
+ assertEquals(sessionCookie.getValue(), sessionCookieForVerification.getValue());
+ return sessionCookie;
+ }
+
+ protected void verifyLoggedOut(AbstractPage targetPage) {
+ // verify on target page
+ targetPage.navigateTo();
+ driver.navigate().refresh();
+ assertCurrentUrlStartsWith(loginPage);
+ Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
+ assertNull(sessionCookie);
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionFailoverClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionFailoverClusterTest.java
new file mode 100644
index 0000000..c1811f9
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionFailoverClusterTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.cluster;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.mail.MessagingException;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.services.managers.AuthenticationSessionManager;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.openqa.selenium.Cookie;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+import static org.keycloak.testsuite.util.WaitUtils.pause;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AuthenticationSessionFailoverClusterTest extends AbstractFailoverClusterTest {
+
+ private String userId;
+
+ @Page
+ protected LoginPage loginPage;
+
+ @Page
+ protected LoginPasswordUpdatePage updatePasswordPage;
+
+
+ @Page
+ protected LoginUpdateProfilePage updateProfilePage;
+
+ @Page
+ protected AppPage appPage;
+
+
+ @Before
+ public void setup() {
+ try {
+ adminClient.realm("test").remove();
+ } catch (Exception ignore) {
+ }
+
+ RealmRepresentation testRealm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
+ adminClient.realms().create(testRealm);
+
+ UserRepresentation user = UserBuilder.create()
+ .username("login-test")
+ .email("login@test.com")
+ .enabled(true)
+ .requiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString())
+ .requiredAction(UserModel.RequiredAction.UPDATE_PROFILE.toString())
+ .build();
+
+ userId = ApiUtil.createUserAndResetPasswordWithAdminClient(adminClient.realm("test"), user, "password");
+ getCleanup().addUserId(userId);
+
+ oauth.clientId("test-app");
+ }
+
+ @After
+ public void after() {
+ adminClient.realm("test").remove();
+ }
+
+
+ @Test
+ public void failoverDuringAuthentication() throws Exception {
+
+ boolean expectSuccessfulFailover = SESSION_CACHE_OWNERS >= 2;
+
+ log.info("AUTHENTICATION FAILOVER TEST: cluster size = " + getClusterSize() + ", session-cache owners = " + SESSION_CACHE_OWNERS
+ + " --> Testsing for " + (expectSuccessfulFailover ? "" : "UN") + "SUCCESSFUL session failover.");
+
+ assertEquals(2, getClusterSize());
+
+ failoverTest(expectSuccessfulFailover);
+ }
+
+
+ protected void failoverTest(boolean expectSuccessfulFailover) throws IOException, MessagingException {
+ loginPage.open();
+
+ String cookieValue1 = getAuthSessionCookieValue();
+
+ // Login and assert on "updatePassword" page
+ loginPage.login("login-test", "password");
+ updatePasswordPage.assertCurrent();
+
+ // Route didn't change
+ Assert.assertEquals(cookieValue1, getAuthSessionCookieValue());
+
+ log.info("Authentication session cookie: " + cookieValue1);
+
+ setCurrentFailNodeForRoute(cookieValue1);
+
+ failure();
+ pause(REBALANCE_WAIT);
+ logFailoverSetup();
+
+ // Trigger the action now
+ updatePasswordPage.changePassword("password", "password");
+
+ if (expectSuccessfulFailover) {
+ //Action was successful
+ updateProfilePage.assertCurrent();
+
+ String cookieValue2 = getAuthSessionCookieValue();
+
+ log.info("Authentication session cookie after failover: " + cookieValue2);
+
+ // Cookie was moved to the second node
+ Assert.assertEquals(cookieValue1.substring(0, 36), cookieValue2.substring(0, 36));
+ Assert.assertNotEquals(cookieValue1, cookieValue2);
+
+ } else {
+ loginPage.assertCurrent();
+ String error = loginPage.getError();
+ log.info("Failover not successful as expected. Error on login page: " + error);
+ Assert.assertNotNull(error);
+
+ loginPage.login("login-test", "password");
+ updatePasswordPage.changePassword("password", "password");
+ }
+
+
+ updateProfilePage.assertCurrent();
+
+ // Successfully update profile and assert user logged
+ updateProfilePage.update("John", "Doe3", "john@doe3.com");
+ appPage.assertCurrent();
+ }
+
+ private String getAuthSessionCookieValue() {
+ Cookie authSessionCookie = driver.manage().getCookieNamed(AuthenticationSessionManager.AUTH_SESSION_ID);
+ Assert.assertNotNull(authSessionCookie);
+ return authSessionCookie.getValue();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/SessionFailoverClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/SessionFailoverClusterTest.java
index ca94179..74d9776 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/SessionFailoverClusterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/SessionFailoverClusterTest.java
@@ -2,38 +2,16 @@ package org.keycloak.testsuite.cluster;
import org.junit.Before;
import org.junit.Test;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.testsuite.page.AbstractPage;
-import org.keycloak.testsuite.page.PageWithLogOutAction;
import org.openqa.selenium.Cookie;
-import java.util.List;
-
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
-import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
import static org.keycloak.testsuite.util.WaitUtils.pause;
/**
*
* @author tkyjovsk
*/
-public class SessionFailoverClusterTest extends AbstractClusterTest {
-
- public static final String KEYCLOAK_SESSION_COOKIE = "KEYCLOAK_SESSION";
-
- public static final Integer SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("session.cache.owners", "1"));
- public static final Integer OFFLINE_SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("offline.session.cache.owners", "1"));
- public static final Integer LOGIN_FAILURES_CACHE_OWNERS = Integer.parseInt(System.getProperty("login.failure.cache.owners", "1"));
-
- public static final Integer REBALANCE_WAIT = Integer.parseInt(System.getProperty("rebalance.wait", "5000"));
-
- @Override
- public void addTestRealms(List<RealmRepresentation> testRealms) {
- }
+public class SessionFailoverClusterTest extends AbstractFailoverClusterTest {
@Before
public void beforeSessionFailover() {
@@ -45,7 +23,7 @@ public class SessionFailoverClusterTest extends AbstractClusterTest {
@Test
public void sessionFailover() {
- boolean expectSuccessfulFailover = SESSION_CACHE_OWNERS >= getClusterSize();
+ boolean expectSuccessfulFailover = SESSION_CACHE_OWNERS >= 2;
log.info("SESSION FAILOVER TEST: cluster size = " + getClusterSize() + ", session-cache owners = " + SESSION_CACHE_OWNERS
+ " --> Testsing for " + (expectSuccessfulFailover ? "" : "UN") + "SUCCESSFUL session failover.");
@@ -91,64 +69,4 @@ public class SessionFailoverClusterTest extends AbstractClusterTest {
}
- /**
- * failure --> failback --> failure of next node
- */
- protected void switchFailedNode() {
- assertFalse(controller.isStarted(getCurrentFailNode().getQualifier()));
-
- failback();
- pause(REBALANCE_WAIT);
-
- iterateCurrentFailNode();
-
- failure();
- pause(REBALANCE_WAIT);
-
- assertFalse(controller.isStarted(getCurrentFailNode().getQualifier()));
- }
-
- protected Cookie login(AbstractPage targetPage) {
- targetPage.navigateTo();
- assertCurrentUrlStartsWith(loginPage);
- loginPage.form().login(ADMIN, ADMIN);
- assertCurrentUrlStartsWith(targetPage);
- Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
- assertNotNull(sessionCookie);
- return sessionCookie;
- }
-
- protected void logout(AbstractPage targetPage) {
- if (!(targetPage instanceof PageWithLogOutAction)) {
- throw new IllegalArgumentException(targetPage.getClass().getSimpleName() + " must implement PageWithLogOutAction interface");
- }
- targetPage.navigateTo();
- assertCurrentUrlStartsWith(targetPage);
- ((PageWithLogOutAction) targetPage).logOut();
- }
-
- protected Cookie verifyLoggedIn(AbstractPage targetPage, Cookie sessionCookieForVerification) {
- // verify on realm path
- masterRealmPage.navigateTo();
- Cookie sessionCookieOnRealmPath = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
- assertNotNull(sessionCookieOnRealmPath);
- assertEquals(sessionCookieOnRealmPath.getValue(), sessionCookieForVerification.getValue());
- // verify on target page
- targetPage.navigateTo();
- assertCurrentUrlStartsWith(targetPage);
- Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
- assertNotNull(sessionCookie);
- assertEquals(sessionCookie.getValue(), sessionCookieForVerification.getValue());
- return sessionCookie;
- }
-
- protected void verifyLoggedOut(AbstractPage targetPage) {
- // verify on target page
- targetPage.navigateTo();
- driver.navigate().refresh();
- assertCurrentUrlStartsWith(loginPage);
- Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
- assertNull(sessionCookie);
- }
-
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java
index abda821..37b9d72 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java
@@ -40,6 +40,7 @@ import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.params.AuthPolicy;
import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.ietf.jgss.GSSCredential;
import org.jboss.arquillian.graphene.page.Page;
@@ -347,7 +348,10 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
cleanupApacheHttpClient();
}
- DefaultHttpClient httpClient = (DefaultHttpClient) new HttpClientBuilder().build();
+ DefaultHttpClient httpClient = (DefaultHttpClient) new HttpClientBuilder()
+ .disableCookieCache(false)
+ .build();
+
httpClient.getAuthSchemes().register(AuthPolicy.SPNEGO, spnegoSchemeFactory);
if (useSpnego) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java
index 65e5a3e..2d6d3af 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java
@@ -17,18 +17,23 @@
package org.keycloak.testsuite.federation.kerberos;
+import java.net.URI;
+import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.common.constants.KerberosConstants;
+import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.federation.kerberos.KerberosConfig;
@@ -148,15 +153,24 @@ public class KerberosStandaloneTest extends AbstractKerberosTest {
Response spnegoResponse = spnegoLogin("hnelson", "secret");
String context = spnegoResponse.readEntity(String.class);
spnegoResponse.close();
+
+ Assert.assertTrue(context.contains("Log in to test"));
+
Pattern pattern = Pattern.compile("action=\"([^\"]+)\"");
Matcher m = pattern.matcher(context);
Assert.assertTrue(m.find());
String url = m.group(1);
- driver.navigate().to(url);
- Assert.assertTrue(loginPage.isCurrent());
- loginPage.login("test-user@localhost", "password");
- String pageSource = driver.getPageSource();
- assertAuthenticationSuccess(driver.getCurrentUrl());
+
+
+ // Follow login with HttpClient. Improve if needed
+ MultivaluedMap<String, String> params = new javax.ws.rs.core.MultivaluedHashMap<>();
+ params.putSingle("username", "test-user@localhost");
+ params.putSingle("password", "password");
+ Response response = client.target(url).request()
+ .post(Entity.form(params));
+
+ URI redirectUri = response.getLocation();
+ assertAuthenticationSuccess(redirectUri.toString());
events.clear();
testRealmResource().components().add(kerberosProvider);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java
new file mode 100644
index 0000000..08d8265
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.forms;
+
+import java.io.IOException;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventType;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LoginExpiredPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginPasswordResetPage;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
+import org.keycloak.testsuite.pages.OAuthGrantPage;
+import org.keycloak.testsuite.pages.RegisterPage;
+import org.keycloak.testsuite.pages.VerifyEmailPage;
+import org.keycloak.testsuite.util.GreenMailRule;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.UserBuilder;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test for browser back/forward/refresh buttons
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest {
+
+ private String userId;
+
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+ }
+
+ @Before
+ public void setup() {
+ UserRepresentation user = UserBuilder.create()
+ .username("login-test")
+ .email("login@test.com")
+ .enabled(true)
+ .requiredAction(UserModel.RequiredAction.UPDATE_PROFILE.toString())
+ .requiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString())
+ .build();
+
+ userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
+ expectedMessagesCount = 0;
+ getCleanup().addUserId(userId);
+
+ oauth.clientId("test-app");
+ }
+
+ @Rule
+ public GreenMailRule greenMail = new GreenMailRule();
+
+ @Page
+ protected AppPage appPage;
+
+ @Page
+ protected LoginPage loginPage;
+
+ @Page
+ protected ErrorPage errorPage;
+
+ @Page
+ protected InfoPage infoPage;
+
+ @Page
+ protected VerifyEmailPage verifyEmailPage;
+
+ @Page
+ protected LoginPasswordResetPage resetPasswordPage;
+
+ @Page
+ protected LoginPasswordUpdatePage updatePasswordPage;
+
+ @Page
+ protected LoginUpdateProfilePage updateProfilePage;
+
+ @Page
+ protected LoginExpiredPage loginExpiredPage;
+
+ @Page
+ protected RegisterPage registerPage;
+
+ @Page
+ protected OAuthGrantPage grantPage;
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ private int expectedMessagesCount;
+
+
+ // KEYCLOAK-4670 - Flow 1
+ @Test
+ public void invalidLoginAndBackButton() throws IOException, MessagingException {
+ loginPage.open();
+
+ loginPage.login("login-test2", "invalid");
+ loginPage.assertCurrent();
+
+ loginPage.login("login-test3", "invalid");
+ loginPage.assertCurrent();
+
+ // Click browser back. Should be still on login page (TODO: Retest with real browsers like FF or Chrome. Maybe they need some additional actions to confirm re-sending POST request )
+ driver.navigate().back();
+ loginPage.assertCurrent();
+
+ // Click browser refresh. Should be still on login page
+ driver.navigate().refresh();
+ loginPage.assertCurrent();
+ }
+
+
+ // KEYCLOAK-4670 - Flow 2
+ @Test
+ public void requiredActionsBackForwardTest() throws IOException, MessagingException {
+ loginPage.open();
+
+ // Login and assert on "updatePassword" page
+ loginPage.login("login-test", "password");
+ updatePasswordPage.assertCurrent();
+
+ // Update password and assert on "updateProfile" page
+ updatePasswordPage.changePassword("password", "password");
+ updateProfilePage.assertCurrent();
+
+ // Click browser back. Assert on "Page expired" page
+ driver.navigate().back();
+ loginExpiredPage.assertCurrent();
+
+ // Click browser forward. Assert on "updateProfile" page again
+ driver.navigate().forward();
+ updateProfilePage.assertCurrent();
+
+
+ // Successfully update profile and assert user logged
+ updateProfilePage.update("John", "Doe3", "john@doe3.com");
+ appPage.assertCurrent();
+ }
+
+
+ // KEYCLOAK-4670 - Flow 3 extended
+ @Test
+ public void requiredActionsBackAndRefreshTest() throws IOException, MessagingException {
+ loginPage.open();
+
+ // Login and assert on "updatePassword" page
+ loginPage.login("login-test", "password");
+ updatePasswordPage.assertCurrent();
+
+ // Click browser refresh. Assert still on updatePassword page
+ driver.navigate().refresh();
+ updatePasswordPage.assertCurrent();
+
+ // Update password and assert on "updateProfile" page
+ updatePasswordPage.changePassword("password", "password");
+ updateProfilePage.assertCurrent();
+
+ // Click browser back. Assert on "Page expired" page
+ driver.navigate().back();
+ loginExpiredPage.assertCurrent();
+
+ // Click browser refresh. Assert still on "Page expired" page
+ driver.navigate().refresh();
+ loginExpiredPage.assertCurrent();
+
+ // Click "login restart" and assert on loginPage
+ loginExpiredPage.clickLoginRestartLink();
+ loginPage.assertCurrent();
+
+ // Login again and assert on "updateProfile" page
+ loginPage.login("login-test", "password");
+ updateProfilePage.assertCurrent();
+
+ // Click browser back. Assert on "Page expired" page
+ driver.navigate().back();
+ loginExpiredPage.assertCurrent();
+
+ // Click "login continue" and assert on updateProfile page
+ loginExpiredPage.clickLoginContinueLink();
+ updateProfilePage.assertCurrent();
+
+ // Successfully update profile and assert user logged
+ updateProfilePage.update("John", "Doe3", "john@doe3.com");
+ appPage.assertCurrent();
+ }
+
+
+ // KEYCLOAK-4670 - Flow 4
+ @Test
+ public void consentRefresh() {
+ oauth.clientId("third-party");
+
+ // Login and go through required actions
+ loginPage.open();
+ loginPage.login("login-test", "password");
+ updatePasswordPage.changePassword("password", "password");
+ updateProfilePage.update("John", "Doe3", "john@doe3.com");
+
+ // Assert on consent screen
+ grantPage.assertCurrent();
+
+ // Click browser back. Assert on "page expired"
+ driver.navigate().back();
+ loginExpiredPage.assertCurrent();
+
+ // Click continue login. Assert on consent screen again
+ loginExpiredPage.clickLoginContinueLink();
+ grantPage.assertCurrent();
+
+ // Click refresh. Assert still on consent screen
+ driver.navigate().refresh();
+ grantPage.assertCurrent();
+
+ // Confirm consent. Assert authenticated
+ grantPage.accept();
+ appPage.assertCurrent();
+ }
+
+
+ // KEYCLOAK-4670 - Flow 5
+ @Test
+ public void clickBackButtonAfterReturnFromRegister() throws Exception {
+ loginPage.open();
+ loginPage.clickRegister();
+ registerPage.assertCurrent();
+
+ // Click "Back to login" link on registerPage
+ registerPage.clickBackToLogin();
+ loginPage.assertCurrent();
+
+ // Click browser "back" button. Should be back on register page
+ driver.navigate().back();
+ registerPage.assertCurrent();
+ }
+
+ @Test
+ public void clickBackButtonFromRegisterPage() {
+ loginPage.open();
+ loginPage.clickRegister();
+ registerPage.assertCurrent();
+
+ // Click browser "back" button. Should be back on login page
+ driver.navigate().back();
+ loginPage.assertCurrent();
+ }
+
+
+ @Test
+ public void backButtonToAuthorizationEndpoint() {
+ loginPage.open();
+
+ // Login and assert on "updatePassword" page
+ loginPage.login("login-test", "password");
+ updatePasswordPage.assertCurrent();
+
+ // Click browser back. I should be on 'page expired' . URL corresponds to OIDC AuthorizationEndpoint
+ driver.navigate().back();
+ loginExpiredPage.assertCurrent();
+
+ // Click 'restart' link. I should be on login page
+ loginExpiredPage.clickLoginRestartLink();
+ loginPage.assertCurrent();
+ }
+
+
+ @Test
+ public void backButtonInResetPasswordFlow() throws Exception {
+ // Click on "forgot password" and type username
+ loginPage.open();
+ loginPage.resetPassword();
+
+ resetPasswordPage.assertCurrent();
+
+ resetPasswordPage.changePassword("login-test");
+
+ loginPage.assertCurrent();
+ assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+
+ // Receive email
+ MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
+
+ String changePasswordUrl = ResetPasswordTest.getPasswordResetEmailLink(message);
+
+ driver.navigate().to(changePasswordUrl.trim());
+
+ updatePasswordPage.assertCurrent();
+
+ // Click browser back. Should be on 'page expired'
+ driver.navigate().back();
+ loginExpiredPage.assertCurrent();
+
+ // Click 'continue' should be on updatePasswordPage
+ loginExpiredPage.clickLoginContinueLink();
+ updatePasswordPage.assertCurrent();
+
+ // Click browser back. Should be on 'page expired'
+ driver.navigate().back();
+ loginExpiredPage.assertCurrent();
+
+ // Click 'restart' . Should be on login page
+ loginExpiredPage.clickLoginRestartLink();
+ loginPage.assertCurrent();
+
+ }
+
+
+ @Test
+ public void appInitiatedRegistrationWithBackButton() throws Exception {
+ // Send request from the application directly to 'registrations'
+ String appInitiatedRegisterUrl = oauth.getLoginFormUrl();
+ appInitiatedRegisterUrl = appInitiatedRegisterUrl.replace("openid-connect/auth", "openid-connect/registrations"); // Should be done better way...
+ driver.navigate().to(appInitiatedRegisterUrl);
+ registerPage.assertCurrent();
+
+
+ // Click 'back to login'
+ registerPage.clickBackToLogin();
+ loginPage.assertCurrent();
+
+ // Login
+ loginPage.login("login-test", "password");
+ updatePasswordPage.assertCurrent();
+
+ // Click browser back. Should be on 'page expired'
+ driver.navigate().back();
+ loginExpiredPage.assertCurrent();
+
+ // Click 'continue' should be on updatePasswordPage
+ loginExpiredPage.clickLoginContinueLink();
+ updatePasswordPage.assertCurrent();
+
+ // Click browser back. Should be on 'page expired'
+ driver.navigate().back();
+ loginExpiredPage.assertCurrent();
+
+ // Click 'restart' . Check that I was put to the registration page as flow was initiated as registration flow
+ loginExpiredPage.clickLoginRestartLink();
+ registerPage.assertCurrent();
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
index 63ed003..0700677 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
@@ -23,21 +23,26 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.BrowserSecurityHeaders;
+import org.keycloak.models.Constants;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
+import org.openqa.selenium.NoSuchElementException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
@@ -47,6 +52,7 @@ import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
@@ -395,18 +401,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
}
- @Test
- public void loginTimeout() {
- loginPage.open();
-
- setTimeOffset(1850);
-
- loginPage.login("login-test", "password");
-
- setTimeOffset(0);
- events.expectLogin().clearDetails().detail(Details.CODE_ID, AssertEvents.isCodeId()).user((String) null).session((String) null).error("expired_code").assertEvent().getSessionId();
- }
@Test
public void loginLoginHint() {
@@ -555,11 +550,33 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
}
}
+
+ // Login timeout scenarios
+
// KEYCLOAK-1037
@Test
public void loginExpiredCode() {
loginPage.open();
setTimeOffset(5000);
+ // No explicitly call "removeExpired". Hence authSession will still exists, but will be expired
+ //testingClient.testing().removeExpired("test");
+
+ loginPage.login("login@test.com", "password");
+ loginPage.assertCurrent();
+
+ Assert.assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
+ setTimeOffset(0);
+
+ events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
+ .assertEvent();
+ }
+
+ // KEYCLOAK-1037
+ @Test
+ public void loginExpiredCodeWithExplicitRemoveExpired() {
+ loginPage.open();
+ setTimeOffset(5000);
+ // Explicitly call "removeExpired". Hence authSession won't exist, but will be restarted from the KC_RESTART
testingClient.testing().removeExpired("test");
loginPage.login("login@test.com", "password");
@@ -567,13 +584,68 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
//loginPage.assertCurrent();
loginPage.assertCurrent();
- //Assert.assertEquals("Login timeout. Please login again.", loginPage.getError());
+ Assert.assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
setTimeOffset(0);
- events.expectLogin().user((String) null).session((String) null).error("expired_code").clearDetails()
+ events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
.client((String) null)
.assertEvent();
}
+
+ @Test
+ public void loginExpiredCodeAndExpiredCookies() {
+ loginPage.open();
+
+ driver.manage().deleteAllCookies();
+
+ // Cookies are expired including KC_RESTART. No way to continue login. Error page must be shown
+ loginPage.login("login@test.com", "password");
+ errorPage.assertCurrent();
+ }
+
+
+
+ @Test
+ public void openLoginFormWithDifferentApplication() throws Exception {
+ // Login form shown after redirect from admin console
+ oauth.clientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
+ oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/admin/test/console");
+ oauth.openLoginForm();
+
+ // Login form shown after redirect from app
+ oauth.clientId("test-app");
+ oauth.redirectUri(OAuthClient.APP_ROOT + "/auth");
+ oauth.openLoginForm();
+
+ assertTrue(loginPage.isCurrent());
+ loginPage.login("test-user@localhost", "password");
+ appPage.assertCurrent();
+
+ events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
+ }
+
+ @Test
+ public void openLoginFormAfterExpiredCode() throws Exception {
+ oauth.openLoginForm();
+
+ setTimeOffset(5000);
+
+ oauth.openLoginForm();
+
+ loginPage.assertCurrent();
+ try {
+ String loginError = loginPage.getError();
+ Assert.fail("Not expected to have error on loginForm. Error is: " + loginError);
+ } catch (NoSuchElementException nsee) {
+ // Expected
+ }
+
+ loginPage.login("test-user@localhost", "password");
+ appPage.assertCurrent();
+
+ events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
index db66deb..7fc3f74 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java
@@ -126,7 +126,7 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
// Check session 1 not logged-in
oauth.openLoginForm();
- assertEquals(oauth.getLoginFormUrl(), driver.getCurrentUrl());
+ loginPage.assertCurrent();
// Login session 3
oauth.doLogin("test-user@localhost", "password");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java
new file mode 100644
index 0000000..24a70fd
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.forms;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.events.Details;
+import org.keycloak.models.Constants;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.InfoPage;
+import org.keycloak.testsuite.pages.LoginExpiredPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginPasswordResetPage;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
+import org.keycloak.testsuite.pages.OAuthGrantPage;
+import org.keycloak.testsuite.pages.RegisterPage;
+import org.keycloak.testsuite.pages.VerifyEmailPage;
+import org.keycloak.testsuite.util.GreenMailRule;
+import org.keycloak.testsuite.util.UserBuilder;
+
+/**
+ * Tries to simulate testing with multiple browser tabs
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
+
+ private String userId;
+
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+ }
+
+ @Before
+ public void setup() {
+ UserRepresentation user = UserBuilder.create()
+ .username("login-test")
+ .email("login@test.com")
+ .enabled(true)
+ .requiredAction(UserModel.RequiredAction.UPDATE_PROFILE.toString())
+ .requiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString())
+ .build();
+
+ userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
+ getCleanup().addUserId(userId);
+
+ oauth.clientId("test-app");
+ }
+
+ @Rule
+ public GreenMailRule greenMail = new GreenMailRule();
+
+ @Page
+ protected AppPage appPage;
+
+ @Page
+ protected LoginPage loginPage;
+
+ @Page
+ protected ErrorPage errorPage;
+
+ @Page
+ protected InfoPage infoPage;
+
+ @Page
+ protected VerifyEmailPage verifyEmailPage;
+
+ @Page
+ protected LoginPasswordResetPage resetPasswordPage;
+
+ @Page
+ protected LoginPasswordUpdatePage updatePasswordPage;
+
+ @Page
+ protected LoginUpdateProfilePage updateProfilePage;
+
+ @Page
+ protected LoginExpiredPage loginExpiredPage;
+
+ @Page
+ protected RegisterPage registerPage;
+
+ @Page
+ protected OAuthGrantPage grantPage;
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+
+ // Test for scenario when user is logged into JS application in 2 browser tabs. Then click "logout" in tab1 and he is logged-out from both tabs (tab2 is logged-out automatically due to session iframe few seconds later)
+ // Now both browser tabs show the 1st login screen and we need to make sure that actionURL (code with execution) is valid on both tabs, so user won't have error page when he tries to login from tab1
+ @Test
+ public void openMultipleTabs() {
+ oauth.openLoginForm();
+ loginPage.assertCurrent();
+ String actionUrl1 = getActionUrl(driver.getPageSource());
+
+ oauth.openLoginForm();
+ loginPage.assertCurrent();
+ String actionUrl2 = getActionUrl(driver.getPageSource());
+
+ Assert.assertEquals(actionUrl1, actionUrl2);
+
+ }
+
+
+ private String getActionUrl(String pageSource) {
+ return pageSource.split("action=\"")[1].split("\"")[0].replaceAll("&", "&");
+ }
+
+
+ @Test
+ public void multipleTabsParallelLoginTest() {
+ oauth.openLoginForm();
+ loginPage.assertCurrent();
+
+ loginPage.login("login-test", "password");
+ updatePasswordPage.assertCurrent();
+
+ String tab1Url = driver.getCurrentUrl();
+
+ // Simulate login in different browser tab tab2. I will be on loginPage again.
+ oauth.openLoginForm();
+ loginPage.assertCurrent();
+
+ // Login in tab2
+ loginPage.login("login-test", "password");
+ updatePasswordPage.assertCurrent();
+
+ updatePasswordPage.changePassword("password", "password");
+ updateProfilePage.update("John", "Doe3", "john@doe3.com");
+ appPage.assertCurrent();
+
+ // Try to go back to tab 1. We should have ALREADY_LOGGED_IN info page
+ driver.navigate().to(tab1Url);
+ infoPage.assertCurrent();
+ Assert.assertEquals("You are already logged in.", infoPage.getInfo());
+
+ infoPage.clickBackToApplicationLink();
+ appPage.assertCurrent();
+ }
+
+
+ @Test
+ public void expiredAuthenticationAction_currentCodeExpiredExecution() {
+ // Simulate to open login form in 2 tabs
+ oauth.openLoginForm();
+ loginPage.assertCurrent();
+ String actionUrl1 = getActionUrl(driver.getPageSource());
+
+ // Click "register" in tab2
+ loginPage.clickRegister();
+ registerPage.assertCurrent();
+
+ // Simulate going back to tab1 and confirm login form. Page "showExpired" should be shown (NOTE: WebDriver does it with GET, when real browser would do it with POST. Improve test if needed...)
+ driver.navigate().to(actionUrl1);
+ loginExpiredPage.assertCurrent();
+
+ // Click on continue and assert I am on "register" form
+ loginExpiredPage.clickLoginContinueLink();
+ registerPage.assertCurrent();
+
+ // Finally click "Back to login" and authenticate
+ registerPage.clickBackToLogin();
+ loginPage.assertCurrent();
+
+ // Login success now
+ loginPage.login("login-test", "password");
+ updatePasswordPage.changePassword("password", "password");
+ updateProfilePage.update("John", "Doe3", "john@doe3.com");
+ appPage.assertCurrent();
+ }
+
+
+ @Test
+ public void expiredAuthenticationAction_expiredCodeCurrentExecution() {
+ // Simulate to open login form in 2 tabs
+ oauth.openLoginForm();
+ loginPage.assertCurrent();
+ String actionUrl1 = getActionUrl(driver.getPageSource());
+
+ loginPage.login("invalid", "invalid");
+ loginPage.assertCurrent();
+ Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+ // Simulate going back to tab1 and confirm login form. Login page with "action expired" message should be shown (NOTE: WebDriver does it with GET, when real browser would do it with POST. Improve test if needed...)
+ driver.navigate().to(actionUrl1);
+ loginPage.assertCurrent();
+ Assert.assertEquals("Action expired. Please continue with login now.", loginPage.getError());
+
+ // Login success now
+ loginPage.login("login-test", "password");
+ updatePasswordPage.changePassword("password", "password");
+ updateProfilePage.update("John", "Doe3", "john@doe3.com");
+ appPage.assertCurrent();
+ }
+
+
+ @Test
+ public void expiredAuthenticationAction_expiredCodeExpiredExecution() {
+ // Open tab1
+ oauth.openLoginForm();
+ loginPage.assertCurrent();
+ String actionUrl1 = getActionUrl(driver.getPageSource());
+
+ // Authenticate in tab2
+ loginPage.login("login-test", "password");
+ updatePasswordPage.assertCurrent();
+
+ // Simulate going back to tab1 and confirm login form. Page "Page expired" should be shown (NOTE: WebDriver does it with GET, when real browser would do it with POST. Improve test if needed...)
+ driver.navigate().to(actionUrl1);
+ loginExpiredPage.assertCurrent();
+
+ // Finish login
+ loginExpiredPage.clickLoginContinueLink();
+ updatePasswordPage.assertCurrent();
+
+ updatePasswordPage.changePassword("password", "password");
+ updateProfilePage.update("John", "Doe3", "john@doe3.com");
+ appPage.assertCurrent();
+ }
+
+
+ @Test
+ public void loginActionWithoutExecution() throws Exception {
+ oauth.openLoginForm();
+
+ // Manually remove execution from the URL and try to simulate the request just with "code" parameter
+ String actionUrl = driver.getPageSource().split("action=\"")[1].split("\"")[0].replaceAll("&", "&");
+ actionUrl = actionUrl.replaceFirst("&execution=.*", "");
+
+ driver.navigate().to(actionUrl);
+
+ loginExpiredPage.assertCurrent();
+ }
+
+
+ // Same like "loginActionWithoutExecution", but AuthenticationSession is in REQUIRED_ACTIONS action
+ @Test
+ public void loginActionWithoutExecutionInRequiredActions() throws Exception {
+ oauth.openLoginForm();
+ loginPage.assertCurrent();
+
+ loginPage.login("login-test", "password");
+ updatePasswordPage.assertCurrent();
+
+ // Manually remove execution from the URL and try to simulate the request just with "code" parameter
+ String actionUrl = driver.getPageSource().split("action=\"")[1].split("\"")[0].replaceAll("&", "&");
+ actionUrl = actionUrl.replaceFirst("&execution=.*", "");
+
+ driver.navigate().to(actionUrl);
+
+ // Back on updatePasswordPage now
+ updatePasswordPage.assertCurrent();
+
+ updatePasswordPage.changePassword("password", "password");
+ updateProfilePage.update("John", "Doe3", "john@doe3.com");
+ appPage.assertCurrent();
+ }
+
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
index 1a68d09..346bbd7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
@@ -21,18 +21,17 @@ import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
-import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
-import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.*;
import org.keycloak.testsuite.pages.AppPage.RequestType;
-import org.keycloak.testsuite.pages.LoginPage;
-import org.keycloak.testsuite.pages.RegisterPage;
-import org.keycloak.testsuite.util.RealmBuilder;
-import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.testsuite.util.*;
+import javax.mail.internet.MimeMessage;
import static org.jgroups.util.Util.assertTrue;
import static org.junit.Assert.assertEquals;
@@ -55,8 +54,14 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
protected RegisterPage registerPage;
@Page
+ protected VerifyEmailPage verifyEmailPage;
+
+ @Page
protected AccountUpdateProfilePage accountPage;
+ @Rule
+ public GreenMailRule greenMail = new GreenMailRule();
+
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@@ -295,10 +300,15 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
registerPage.register("firstName", "lastName", "registerUserSuccess@email", "registerUserSuccess", "password", "password");
+ appPage.assertCurrent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
String userId = events.expectRegister("registerUserSuccess", "registerUserSuccess@email").assertEvent().getUserId();
- events.expectLogin().detail("username", "registerusersuccess").user(userId).assertEvent();
+ assertUserRegistered(userId, "registerusersuccess", "registerusersuccess@email");
+ }
+
+ private void assertUserRegistered(String userId, String username, String email) {
+ events.expectLogin().detail("username", username.toLowerCase()).user(userId).assertEvent();
UserRepresentation user = getUser(userId);
Assert.assertNotNull(user);
@@ -306,13 +316,122 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
// test that timestamp is current with 10s tollerance
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000);
// test user info is set from form
- assertEquals("registerusersuccess", user.getUsername());
- assertEquals("registerusersuccess@email", user.getEmail());
+ assertEquals(username.toLowerCase(), user.getUsername());
+ assertEquals(email.toLowerCase(), user.getEmail());
assertEquals("firstName", user.getFirstName());
assertEquals("lastName", user.getLastName());
}
@Test
+ public void registerUserSuccessWithEmailVerification() throws Exception {
+ RealmRepresentation realm = testRealm().toRepresentation();
+ boolean origVerifyEmail = realm.isVerifyEmail();
+
+ try {
+ realm.setVerifyEmail(true);
+ testRealm().update(realm);
+
+ loginPage.open();
+ loginPage.clickRegister();
+ registerPage.assertCurrent();
+
+ registerPage.register("firstName", "lastName", "registerUserSuccessWithEmailVerification@email", "registerUserSuccessWithEmailVerification", "password", "password");
+ verifyEmailPage.assertCurrent();
+
+ String userId = events.expectRegister("registerUserSuccessWithEmailVerification", "registerUserSuccessWithEmailVerification@email").assertEvent().getUserId();
+
+ {
+ assertTrue("Expecting verify email", greenMail.waitForIncomingEmail(1000, 1));
+
+ events.expect(EventType.SEND_VERIFY_EMAIL)
+ .detail(Details.EMAIL, "registerUserSuccessWithEmailVerification@email".toLowerCase())
+ .user(userId)
+ .assertEvent();
+
+ MimeMessage message = greenMail.getLastReceivedMessage();
+ String link = MailUtils.getPasswordResetEmailLink(message);
+
+ driver.navigate().to(link);
+ }
+
+ events.expectRequiredAction(EventType.VERIFY_EMAIL)
+ .detail(Details.EMAIL, "registerUserSuccessWithEmailVerification@email".toLowerCase())
+ .user(userId)
+ .assertEvent();
+
+ assertUserRegistered(userId, "registerUserSuccessWithEmailVerification", "registerUserSuccessWithEmailVerification@email");
+
+ appPage.assertCurrent();
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ // test that timestamp is current with 10s tollerance
+ // test user info is set from form
+ } finally {
+ realm.setVerifyEmail(origVerifyEmail);
+ testRealm().update(realm);
+ }
+ }
+
+ @Test
+ public void registerUserSuccessWithEmailVerificationWithResend() throws Exception {
+ RealmRepresentation realm = testRealm().toRepresentation();
+ boolean origVerifyEmail = realm.isVerifyEmail();
+ try {
+ realm.setVerifyEmail(true);
+ testRealm().update(realm);
+
+ loginPage.open();
+ loginPage.clickRegister();
+ registerPage.assertCurrent();
+
+ registerPage.register("firstName", "lastName", "registerUserSuccessWithEmailVerificationWithResend@email", "registerUserSuccessWithEmailVerificationWithResend", "password", "password");
+ verifyEmailPage.assertCurrent();
+
+ String userId = events.expectRegister("registerUserSuccessWithEmailVerificationWithResend", "registerUserSuccessWithEmailVerificationWithResend@email").assertEvent().getUserId();
+
+ {
+ assertTrue("Expecting verify email", greenMail.waitForIncomingEmail(1000, 1));
+
+ events.expect(EventType.SEND_VERIFY_EMAIL)
+ .detail(Details.EMAIL, "registerUserSuccessWithEmailVerificationWithResend@email".toLowerCase())
+ .user(userId)
+ .assertEvent();
+
+ verifyEmailPage.clickResendEmail();
+ verifyEmailPage.assertCurrent();
+
+ assertTrue("Expecting second verify email", greenMail.waitForIncomingEmail(1000, 1));
+
+ events.expect(EventType.SEND_VERIFY_EMAIL)
+ .detail(Details.EMAIL, "registerUserSuccessWithEmailVerificationWithResend@email".toLowerCase())
+ .user(userId)
+ .assertEvent();
+
+ MimeMessage message = greenMail.getLastReceivedMessage();
+ String link = MailUtils.getPasswordResetEmailLink(message);
+
+ driver.navigate().to(link);
+ }
+
+ events.expectRequiredAction(EventType.VERIFY_EMAIL)
+ .detail(Details.EMAIL, "registerUserSuccessWithEmailVerificationWithResend@email".toLowerCase())
+ .user(userId)
+ .assertEvent();
+
+ assertUserRegistered(userId, "registerUserSuccessWithEmailVerificationWithResend", "registerUserSuccessWithEmailVerificationWithResend@email");
+
+ appPage.assertCurrent();
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ // test that timestamp is current with 10s tollerance
+ // test user info is set from form
+ } finally {
+ realm.setVerifyEmail(origVerifyEmail);
+ testRealm().update(realm);
+ }
+ }
+
+ @Test
public void registerUserUmlats() {
loginPage.open();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index 3a12a76..04ee911 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -16,10 +16,8 @@
*/
package org.keycloak.testsuite.forms;
+import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionToken;
import org.jboss.arquillian.graphene.page.Page;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
@@ -50,8 +48,8 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import org.junit.*;
+import static org.junit.Assert.*;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -74,6 +72,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
.build();
userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
+ expectedMessagesCount = 0;
getCleanup().addUserId(userId);
}
@@ -104,6 +103,8 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
+ private int expectedMessagesCount;
+
@Test
public void resetPasswordLink() throws IOException, MessagingException {
String username = "login-test";
@@ -138,15 +139,15 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
updatePasswordPage.changePassword("resetPassword", "resetPassword");
- String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD)
- .detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/account/")
+ events.expectRequiredAction(EventType.UPDATE_PASSWORD)
+ .detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/account/")
.client("account")
- .user(userId).detail(Details.USERNAME, username).assertEvent().getSessionId();
+ .user(userId).detail(Details.USERNAME, username).assertEvent();
- events.expectLogin().user(userId).detail(Details.USERNAME, username)
+ String sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, username)
.detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/account/")
.client("account")
- .session(sessionId).assertEvent();
+ .assertEvent().getSessionId();
oauth.openLogout();
@@ -168,21 +169,47 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
}
@Test
- public void resetPasswordWithSpacesInUsername() throws IOException, MessagingException {
- resetPassword(" login-test ");
+ public void resetPasswordTwice() throws IOException, MessagingException {
+ String changePasswordUrl = resetPassword("login-test");
+ events.clear();
+
+ assertSecondPasswordResetFails(changePasswordUrl, null); // KC_RESTART doesn't exists, it was deleted after first successful reset-password flow was finished
}
@Test
- public void resetPasswordCancelChangeUser() throws IOException, MessagingException {
- loginPage.open();
- loginPage.resetPassword();
+ public void resetPasswordTwiceInNewBrowser() throws IOException, MessagingException {
+ String changePasswordUrl = resetPassword("login-test");
+ events.clear();
- resetPasswordPage.assertCurrent();
+ String resetUri = oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials";
+ driver.navigate().to(resetUri); // This is necessary to delete KC_RESTART cookie that is restricted to /auth/realms/test path
+ driver.manage().deleteAllCookies();
- resetPasswordPage.changePassword("test-user@localhost");
+ assertSecondPasswordResetFails(changePasswordUrl, null);
+ }
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ public void assertSecondPasswordResetFails(String changePasswordUrl, String clientId) {
+ driver.navigate().to(changePasswordUrl.trim());
+
+ errorPage.assertCurrent();
+ assertEquals("Action expired. Please continue with login now.", errorPage.getError());
+
+ events.expect(EventType.RESET_PASSWORD)
+ .client("account")
+ .session((String) null)
+ .user(userId)
+ .error(Errors.EXPIRED_CODE)
+ .assertEvent();
+ }
+
+ @Test
+ public void resetPasswordWithSpacesInUsername() throws IOException, MessagingException {
+ resetPassword(" login-test ");
+ }
+
+ @Test
+ public void resetPasswordCancelChangeUser() throws IOException, MessagingException {
+ initiateResetPasswordFromResetPasswordPage("test-user@localhost");
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).detail(Details.USERNAME, "test-user@localhost")
.session((String) null)
@@ -206,16 +233,12 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
resetPassword("login@test.com");
}
- private void resetPassword(String username) throws IOException, MessagingException {
- loginPage.open();
- loginPage.resetPassword();
+ private String resetPassword(String username) throws IOException, MessagingException {
+ return resetPassword(username, "resetPassword");
+ }
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword(username);
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ private String resetPassword(String username, String password) throws IOException, MessagingException {
+ initiateResetPasswordFromResetPasswordPage(username);
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.user(userId)
@@ -224,9 +247,9 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
.session((String)null)
.assertEvent();
- assertEquals(1, greenMail.getReceivedMessages().length);
+ assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length);
- MimeMessage message = greenMail.getReceivedMessages()[0];
+ MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String changePasswordUrl = getPasswordResetEmailLink(message);
@@ -234,13 +257,13 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
updatePasswordPage.assertCurrent();
- updatePasswordPage.changePassword("resetPassword", "resetPassword");
+ updatePasswordPage.changePassword(password, password);
- String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username.trim()).assertEvent().getSessionId();
+ events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username.trim()).assertEvent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().user(userId).detail(Details.USERNAME, username.trim()).session(sessionId).assertEvent();
+ String sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, username.trim()).assertEvent().getSessionId();
oauth.openLogout();
@@ -248,89 +271,60 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
loginPage.open();
- loginPage.login("login-test", "resetPassword");
+ loginPage.login("login-test", password);
- events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+ sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- }
-
- private void resetPassword(String username, String password) throws IOException, MessagingException {
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword(username);
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
-
- events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null)
- .detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
-
- MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
-
- String changePasswordUrl = getPasswordResetEmailLink(message);
-
- driver.navigate().to(changePasswordUrl.trim());
-
- updatePasswordPage.assertCurrent();
-
- updatePasswordPage.changePassword(password, password);
-
- String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId)
- .detail(Details.USERNAME, username).assertEvent().getSessionId();
-
- assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
-
- events.expectLogin().user(userId).detail(Details.USERNAME, username).assertEvent();
oauth.openLogout();
events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
+
+ return changePasswordUrl;
}
private void resetPasswordInvalidPassword(String username, String password, String error) throws IOException, MessagingException {
- loginPage.open();
- loginPage.resetPassword();
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword(username);
+ initiateResetPasswordFromResetPasswordPage(username);
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
-
- events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null)
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String) null)
.detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
+ assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length);
+
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String changePasswordUrl = getPasswordResetEmailLink(message);
driver.navigate().to(changePasswordUrl.trim());
+
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword(password, password);
- assertTrue(updatePasswordPage.isCurrent());
+ updatePasswordPage.assertCurrent();
assertEquals(error, updatePasswordPage.getError());
events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
}
- @Test
- public void resetPasswordWrongEmail() throws IOException, MessagingException, InterruptedException {
+ private void initiateResetPasswordFromResetPasswordPage(String username) {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("invalid");
+
+ resetPasswordPage.changePassword(username);
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ expectedMessagesCount++;
+ }
+
+ @Test
+ public void resetPasswordWrongEmail() throws IOException, MessagingException, InterruptedException {
+ initiateResetPasswordFromResetPasswordPage("invalid");
assertEquals(0, greenMail.getReceivedMessages().length);
@@ -358,27 +352,19 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
@Test
public void resetPasswordExpiredCode() throws IOException, MessagingException, InterruptedException {
- try {
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("login-test");
+ initiateResetPasswordFromResetPasswordPage("login-test");
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
-
- events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
- .session((String)null)
- .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
+ .session((String)null)
+ .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
- assertEquals(1, greenMail.getReceivedMessages().length);
+ assertEquals(1, greenMail.getReceivedMessages().length);
- MimeMessage message = greenMail.getReceivedMessages()[0];
+ MimeMessage message = greenMail.getReceivedMessages()[0];
- String changePasswordUrl = getPasswordResetEmailLink(message);
+ String changePasswordUrl = getPasswordResetEmailLink(message);
+ try {
setTimeOffset(1800 + 23);
driver.navigate().to(changePasswordUrl.trim());
@@ -387,7 +373,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
- events.expectRequiredAction(EventType.RESET_PASSWORD).error("expired_code").client("test-app").user((String) null).session((String) null).clearDetails().assertEvent();
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent();
} finally {
setTimeOffset(0);
}
@@ -398,20 +384,12 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
final AtomicInteger originalValue = new AtomicInteger();
RealmRepresentation realmRep = testRealm().toRepresentation();
- originalValue.set(realmRep.getAccessCodeLifespan());
- realmRep.setAccessCodeLifespanUserAction(60);
+ originalValue.set(realmRep.getActionTokenGeneratedByUserLifespan());
+ realmRep.setActionTokenGeneratedByUserLifespan(60);
testRealm().update(realmRep);
try {
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("login-test");
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ initiateResetPasswordFromResetPasswordPage("login-test");
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.session((String)null)
@@ -431,58 +409,53 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
- events.expectRequiredAction(EventType.RESET_PASSWORD).error("expired_code").client("test-app").user((String) null).session((String) null).clearDetails().assertEvent();
+ events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent();
} finally {
setTimeOffset(0);
+
+ realmRep.setActionTokenGeneratedByUserLifespan(originalValue.get());
+ testRealm().update(realmRep);
}
}
@Test
public void resetPasswordDisabledUser() throws IOException, MessagingException, InterruptedException {
UserRepresentation user = findUser("login-test");
- user.setEnabled(false);
- updateUser(user);
-
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("login-test");
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ try {
+ user.setEnabled(false);
+ updateUser(user);
- assertEquals(0, greenMail.getReceivedMessages().length);
+ initiateResetPasswordFromResetPasswordPage("login-test");
- events.expectRequiredAction(EventType.RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent();
+ assertEquals(0, greenMail.getReceivedMessages().length);
- user.setEnabled(true);
- updateUser(user);
+ events.expectRequiredAction(EventType.RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent();
+ } finally {
+ user.setEnabled(true);
+ updateUser(user);
+ }
}
@Test
public void resetPasswordNoEmail() throws IOException, MessagingException, InterruptedException {
- final String[] email = new String[1];
+ final String email;
UserRepresentation user = findUser("login-test");
- email[0] = user.getEmail();
- user.setEmail("");
- updateUser(user);
+ email = user.getEmail();
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
+ try {
+ user.setEmail("");
+ updateUser(user);
- resetPasswordPage.changePassword("login-test");
+ initiateResetPasswordFromResetPasswordPage("login-test");
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ assertEquals(0, greenMail.getReceivedMessages().length);
- assertEquals(0, greenMail.getReceivedMessages().length);
-
- events.expectRequiredAction(EventType.RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent();
+ events.expectRequiredAction(EventType.RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent();
+ } finally {
+ user.setEmail(email);
+ updateUser(user);
+ }
}
@Test
@@ -496,29 +469,31 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
RealmRepresentation realmRep = testRealm().toRepresentation();
Map<String, String> oldSmtp = realmRep.getSmtpServer();
- realmRep.setSmtpServer(smtpConfig);
- testRealm().update(realmRep);
-
- loginPage.open();
- loginPage.resetPassword();
+ try {
+ realmRep.setSmtpServer(smtpConfig);
+ testRealm().update(realmRep);
- resetPasswordPage.assertCurrent();
+ loginPage.open();
+ loginPage.resetPassword();
- resetPasswordPage.changePassword("login-test");
+ resetPasswordPage.assertCurrent();
- errorPage.assertCurrent();
+ resetPasswordPage.changePassword("login-test");
- assertEquals("Failed to send email, please try again later.", errorPage.getError());
+ errorPage.assertCurrent();
- assertEquals(0, greenMail.getReceivedMessages().length);
+ assertEquals("Failed to send email, please try again later.", errorPage.getError());
- events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId)
- .session((String)null)
- .detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent();
+ assertEquals(0, greenMail.getReceivedMessages().length);
- // Revert SMTP back
- realmRep.setSmtpServer(oldSmtp);
- testRealm().update(realmRep);
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId)
+ .session((String)null)
+ .detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent();
+ } finally {
+ // Revert SMTP back
+ realmRep.setSmtpServer(oldSmtp);
+ testRealm().update(realmRep);
+ }
}
private void setPasswordPolicy(String policy) {
@@ -531,15 +506,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
public void resetPasswordWithLengthPasswordPolicy() throws IOException, MessagingException {
setPasswordPolicy("length");
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("login-test");
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ initiateResetPasswordFromResetPasswordPage("login-test");
assertEquals(1, greenMail.getReceivedMessages().length);
@@ -561,11 +528,11 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy");
- String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
+ events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").session(sessionId).assertEvent();
+ String sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
oauth.openLogout();
@@ -581,7 +548,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
}
@Test
- public void resetPasswordWithPasswordHisoryPolicy() throws IOException, MessagingException {
+ public void resetPasswordWithPasswordHistoryPolicy() throws IOException, MessagingException {
//Block passwords that are equal to previous passwords. Default value is 3.
setPasswordPolicy("passwordHistory");
@@ -597,13 +564,14 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords.");
resetPasswordInvalidPassword("login-test", "password2", "Invalid password: must not be equal to any of last 3 passwords.");
- setTimeOffset(8000000);
+ setTimeOffset(6000000);
resetPassword("login-test", "password3");
resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords.");
resetPasswordInvalidPassword("login-test", "password2", "Invalid password: must not be equal to any of last 3 passwords.");
resetPasswordInvalidPassword("login-test", "password3", "Invalid password: must not be equal to any of last 3 passwords.");
+ setTimeOffset(8000000);
resetPassword("login-test", "password");
} finally {
setTimeOffset(0);
@@ -620,6 +588,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
resetPasswordPage.changePassword(username);
+ log.info("Should be at login page again.");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
@@ -638,16 +607,20 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
String changePasswordUrl = getPasswordResetEmailLink(message);
+ log.debug("Going to reset password URI.");
+ driver.navigate().to(resetUri); // This is necessary to delete KC_RESTART cookie that is restricted to /auth/realms/test path
+ log.debug("Removing cookies.");
driver.manage().deleteAllCookies();
+ log.debug("Going to URI from e-mail.");
driver.navigate().to(changePasswordUrl.trim());
- System.out.println(driver.getPageSource());
+// System.out.println(driver.getPageSource());
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("resetPassword", "resetPassword");
- assertTrue(infoPage.isCurrent());
+ infoPage.assertCurrent();
assertEquals("Your account has been updated.", infoPage.getInfo());
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
index 22f75da..90b8049 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
@@ -23,9 +23,12 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+import org.keycloak.models.UserModel;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.drone.Different;
@@ -33,6 +36,7 @@ import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.WebDriver;
@@ -59,6 +63,9 @@ public class SSOTest extends AbstractTestRealmKeycloakTest {
@Page
protected AccountUpdateProfilePage profilePage;
+ @Page
+ protected LoginPasswordUpdatePage updatePasswordPage;
+
@Rule
public AssertEvents events = new AssertEvents(this);
@@ -109,6 +116,7 @@ public class SSOTest extends AbstractTestRealmKeycloakTest {
events.clear();
}
+
@Test
public void multipleSessions() {
loginPage.open();
@@ -124,7 +132,6 @@ public class SSOTest extends AbstractTestRealmKeycloakTest {
OAuthClient oauth2 = new OAuthClient();
oauth2.init(adminClient, driver2);
- oauth2.state("mystate");
oauth2.doLogin("test-user@localhost", "password");
EventRepresentation login2 = events.expectLogin().assertEvent();
@@ -158,4 +165,38 @@ public class SSOTest extends AbstractTestRealmKeycloakTest {
}
}
+
+ @Test
+ public void loginWithRequiredActionAddedInTheMeantime() {
+ // SSO login
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+ EventRepresentation loginEvent = events.expectLogin().assertEvent();
+ String sessionId = loginEvent.getSessionId();
+
+ // Add update-profile required action to user now
+ UserRepresentation user = testRealm().users().get(loginEvent.getUserId()).toRepresentation();
+ user.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PASSWORD.toString());
+ testRealm().users().get(loginEvent.getUserId()).update(user);
+
+ // Attempt SSO login. update-password form is shown
+ oauth.openLoginForm();
+ updatePasswordPage.assertCurrent();
+
+ updatePasswordPage.changePassword("password", "password");
+ events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
+
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ loginEvent = events.expectLogin().removeDetail(Details.USERNAME).client("test-app").assertEvent();
+ String sessionId2 = loginEvent.getSessionId();
+ assertEquals(sessionId, sessionId2);
+
+
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 92e68cb..ecf540c 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -210,12 +210,7 @@ public class AccessTokenTest extends AbstractKeycloakTest {
oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/admin/test/console/nosuch.html");
oauth.openLoginForm();
- String actionUrl = driver.getPageSource().split("action=\"")[1].split("\"")[0].replaceAll("&", "&");
- actionUrl = actionUrl.replaceFirst("&execution=.*", "");
-
- String loginPageCode = actionUrl.split("code=")[1].split("&")[0];
-
- driver.navigate().to(actionUrl);
+ String loginPageCode = driver.getPageSource().split("code=")[1].split("&")[0].split("\"")[0];
oauth.fillLoginForm("test-user@localhost", "password");
@@ -452,7 +447,7 @@ public class AccessTokenTest extends AbstractKeycloakTest {
Assert.assertEquals(400, response.getStatusCode());
EventRepresentation event = events.poll();
- assertNotNull(event.getDetails().get(Details.CODE_ID));
+ assertNull(event.getDetails().get(Details.CODE_ID));
UserManager.realm(adminClient.realm("test")).user(user).removeRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE.toString());
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
index 729f0c5..7577990 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
@@ -16,8 +16,6 @@
*/
package org.keycloak.testsuite.oauth;
-import org.jboss.arquillian.graphene.page.Page;
-import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
@@ -31,7 +29,6 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
-import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.By;
@@ -61,11 +58,12 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
public void clientConfiguration() {
oauth.responseType(OAuth2Constants.CODE);
oauth.responseMode(null);
+ oauth.stateParamRandom();
}
@Test
public void authorizationRequest() throws IOException {
- oauth.state("OpenIdConnect.AuthenticationProperties=2302984sdlk");
+ oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk");
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
@@ -100,8 +98,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
public void authorizationValidRedirectUri() throws IOException {
ClientManager.realm(adminClient.realm("test")).clientId("test-app").addRedirectUris(oauth.getRedirectUri());
- oauth.state("mystate");
-
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertTrue(response.isRedirected());
@@ -113,7 +109,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
@Test
public void authorizationRequestNoState() throws IOException {
- oauth.state(null);
+ oauth.stateParamHardcoded(null);
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
@@ -143,7 +139,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
@Test
public void authorizationRequestFormPostResponseMode() throws IOException {
oauth.responseMode(OIDCResponseMode.FORM_POST.toString().toLowerCase());
- oauth.state("OpenIdConnect.AuthenticationProperties=2302984sdlk");
+ oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk");
oauth.doLoginGrant("test-user@localhost", "password");
String sources = driver.getPageSource();
@@ -159,7 +155,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
}
private void assertCode(String expectedCodeId, String actualCode) {
- assertEquals(expectedCodeId, actualCode.split("\\.")[1]);
+ assertEquals(expectedCodeId, actualCode.split("\\.")[2]);
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
index e244f9a..c5304ff 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
@@ -16,8 +16,8 @@
*/
package org.keycloak.testsuite.oauth;
+import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
-import org.junit.After;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
@@ -155,6 +155,7 @@ public class OAuthGrantTest extends AbstractKeycloakTest {
.client(THIRD_PARTY_APP)
.error("rejected_by_user")
.removeDetail(Details.CONSENT)
+ .session(Matchers.nullValue(String.class))
.assertEvent();
}
@@ -309,6 +310,7 @@ public class OAuthGrantTest extends AbstractKeycloakTest {
.client(THIRD_PARTY_APP)
.error("rejected_by_user")
.removeDetail(Details.CONSENT)
+ .session(Matchers.nullValue(String.class))
.assertEvent();
oauth.scope("foo-role third-party/bar-role");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
index de83d4e..efaf4bd 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
@@ -36,6 +36,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.ClientManager;
@@ -204,6 +205,8 @@ public class ResourceOwnerPasswordCredentialsGrantTest extends AbstractKeycloakT
.removeDetail(Details.CONSENT)
.assertEvent();
+ Assert.assertTrue(login.equals(accessToken.getPreferredUsername()) || login.equals(accessToken.getEmail()));
+
assertEquals(accessToken.getSessionState(), refreshToken.getSessionState());
OAuthClient.AccessTokenResponse refreshedResponse = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
index 57a2102..03522d6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java
@@ -304,6 +304,31 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
}
+
+ @Test
+ public void promptLoginDifferentUser() throws Exception {
+ String sss = oauth.getLoginFormUrl();
+ System.out.println(sss);
+
+ // Login user
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+ Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
+ IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent);
+
+ // Assert need to re-authenticate with prompt=login
+ driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=login");
+
+ // Authenticate as different user
+ loginPage.assertCurrent();
+ loginPage.login("john-doh@localhost", "password");
+
+ errorPage.assertCurrent();
+ Assert.assertTrue(errorPage.getError().startsWith("You are already authenticated as different user"));
+ }
+
// DISPLAY & OTHERS
@Test
@@ -324,6 +349,8 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
@Test
public void requestParamUnsigned() throws Exception {
+ oauth.stateParamHardcoded("mystate2");
+
String validRedirectUri = oauth.getRedirectUri();
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
@@ -344,12 +371,14 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
oauth.request(requestStr);
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode());
- Assert.assertEquals("mystate", response.getState());
+ Assert.assertEquals("mystate2", response.getState());
assertTrue(appPage.isCurrent());
}
@Test
public void requestUriParamUnsigned() throws Exception {
+ oauth.stateParamHardcoded("mystate1");
+
String validRedirectUri = oauth.getRedirectUri();
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
@@ -367,12 +396,14 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode());
- Assert.assertEquals("mystate", response.getState());
+ Assert.assertEquals("mystate1", response.getState());
assertTrue(appPage.isCurrent());
}
@Test
public void requestUriParamSigned() throws Exception {
+ oauth.stateParamHardcoded("mystate3");
+
String validRedirectUri = oauth.getRedirectUri();
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
@@ -412,7 +443,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
// Check signed request_uri will pass
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode());
- Assert.assertEquals("mystate", response.getState());
+ Assert.assertEquals("mystate3", response.getState());
assertTrue(appPage.isCurrent());
// Revert requiring signature for client
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/testrealm.json
index b20eb5d..ef6f105 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/testrealm.json
@@ -9,6 +9,8 @@
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
"defaultRoles": [ "user" ],
+ "actionTokenGeneratedByAdminLifespan": "147",
+ "actionTokenGeneratedByUserLifespan": "258",
"smtpServer": {
"from": "auto@keycloak.org",
"host": "localhost",
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
index e01a4d5..6bc040f 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml
@@ -53,6 +53,7 @@
<property name="bindAddress">localhost</property>
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
<property name="bindHttpPort">${auth.server.http.port}</property>
+ <property name="remoteMode">${undertow.remote}</property>
</configuration>
</container>
@@ -90,7 +91,7 @@
<property name="jbossHome">${auth.server.backend1.home}</property>
<property name="serverConfig">standalone-ha.xml</property>
<property name="jbossArguments">
- -Djboss.socket.binding.port-offset=${auth.server.backend1.port.offset}
+ -Djboss.socket.binding.port-offset=${auth.server.backend1.port.offset}
-Djboss.node.name=node1
${adapter.test.props}
${auth.server.profile}
@@ -127,6 +128,43 @@
</container>
</group>
+ <!-- Clustering with embedded undertow -->
+ <group qualifier="auth-server-undertow-cluster">
+ <container qualifier="auth-server-undertow-backend1" mode="manual" >
+ <configuration>
+ <property name="enabled">${auth.server.undertow.cluster}</property>
+ <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
+ <property name="bindAddress">localhost</property>
+ <property name="bindHttpPort">${auth.server.http.port}</property>
+ <property name="bindHttpPortOffset">1</property>
+ <property name="route">node1</property>
+ <property name="remoteMode">${undertow.remote}</property>
+ </configuration>
+ </container>
+ <container qualifier="auth-server-undertow-backend2" mode="manual" >
+ <configuration>
+ <property name="enabled">${auth.server.undertow.cluster}</property>
+ <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
+ <property name="bindAddress">localhost</property>
+ <property name="bindHttpPort">${auth.server.http.port}</property>
+ <property name="bindHttpPortOffset">2</property>
+ <property name="route">node2</property>
+ <property name="remoteMode">${undertow.remote}</property>
+ </configuration>
+ </container>
+
+ <container qualifier="auth-server-balancer-undertow" mode="suite" >
+ <configuration>
+ <property name="enabled">${auth.server.undertow.cluster}</property>
+ <property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancerContainer</property>
+ <property name="bindAddress">localhost</property>
+ <property name="bindHttpPort">${auth.server.http.port}</property>
+ <property name="nodes">node1=http://localhost:8181,node2=http://localhost:8182</property>
+ </configuration>
+ </container>
+ </group>
+
+
<container qualifier="auth-server-balancer-wildfly" mode="suite" >
<configuration>
<property name="enabled">${auth.server.cluster}</property>
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index c6da264..d038877 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -108,8 +108,9 @@
"connectionsInfinispan": {
"default": {
"clustered": "${keycloak.connectionsInfinispan.clustered:false}",
- "async": "${keycloak.connectionsInfinispan.async:true}",
- "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}",
+ "async": "${keycloak.connectionsInfinispan.async:false}",
+ "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}",
+ "l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}",
"remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
"remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
"remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/client-session-test.js b/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/client-session-test.js
index 07a07a1..0fd70d5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/client-session-test.js
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/client-session-test.js
@@ -12,7 +12,7 @@ function authenticate(context) {
return;
}
- if (clientSession.getAuthMethod() != "${authMethod}") {
+ if (clientSession.getProtocol() != "${authMethod}") {
context.failure(AuthenticationFlowError.INVALID_CLIENT_SESSION);
return;
}
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 8b776c6..f129e45 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -108,6 +108,10 @@ access-token-lifespan=Access Token Lifespan
access-token-lifespan.tooltip=Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout.
access-token-lifespan-for-implicit-flow=Access Token Lifespan For Implicit Flow
access-token-lifespan-for-implicit-flow.tooltip=Max time before an access token issued during OpenID Connect Implicit Flow is expired. This value is recommended to be shorter than SSO timeout. There is no possibility to refresh token during implicit flow, that's why there is separate timeout different to 'Access Token Lifespan'.
+action-token-generated-by-admin-lifespan=Default Admin Action Token Lifespan
+action-token-generated-by-admin-lifespan.tooltip=Max time before an action token generated via admin interface is expired. This value is recommended to be long to allow admins send e-mails for users that are currently offline. The default timeout can be overridden right before issuing the token.
+action-token-generated-by-user-lifespan=User Action Token Lifespan
+action-token-generated-by-user-lifespan.tooltip=Max time before an action token generated via user action (e.g. e-mail verification) is expired. This value is recommended to be short because it is expected that the user would react to self-created action token quickly.
client-login-timeout=Client login timeout
client-login-timeout.tooltip=Max time an client has to finish the access token protocol. This should normally be 1 minute.
login-timeout=Login timeout
@@ -1292,6 +1296,8 @@ credential-types=Credential Types
manage-user-password=Manage Password
disable-credentials=Disable Credentials
credential-reset-actions=Credential Reset
+credential-reset-actions-timeout=Token validity
+credential-reset-actions-timeout.tooltip=Max time before the action token allowing execution of given actions is expired.
ldap-mappers=LDAP Mappers
create-ldap-mapper=Create LDAP mapper
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index bcc655f..c658bb5 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -1044,6 +1044,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
$scope.realm.accessCodeLifespan = TimeUnit2.asUnit(realm.accessCodeLifespan);
$scope.realm.accessCodeLifespanLogin = TimeUnit2.asUnit(realm.accessCodeLifespanLogin);
$scope.realm.accessCodeLifespanUserAction = TimeUnit2.asUnit(realm.accessCodeLifespanUserAction);
+ $scope.realm.actionTokenGeneratedByAdminLifespan = TimeUnit2.asUnit(realm.actionTokenGeneratedByAdminLifespan);
+ $scope.realm.actionTokenGeneratedByUserLifespan = TimeUnit2.asUnit(realm.actionTokenGeneratedByUserLifespan);
var oldCopy = angular.copy($scope.realm);
$scope.changed = false;
@@ -1063,6 +1065,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
$scope.realm.accessCodeLifespan = $scope.realm.accessCodeLifespan.toSeconds();
$scope.realm.accessCodeLifespanUserAction = $scope.realm.accessCodeLifespanUserAction.toSeconds();
$scope.realm.accessCodeLifespanLogin = $scope.realm.accessCodeLifespanLogin.toSeconds();
+ $scope.realm.actionTokenGeneratedByAdminLifespan = $scope.realm.actionTokenGeneratedByAdminLifespan.toSeconds();
+ $scope.realm.actionTokenGeneratedByUserLifespan = $scope.realm.actionTokenGeneratedByUserLifespan.toSeconds();
Realm.update($scope.realm, function () {
$route.reload();
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 13d8343..2c3c34f 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -482,7 +482,7 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
}
});
-module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, RequiredActions, User, UserExecuteActionsEmail, UserCredentials, Notifications, Dialog) {
+module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, RequiredActions, User, UserExecuteActionsEmail, UserCredentials, Notifications, Dialog, TimeUnit2) {
console.log('UserCredentialsCtrl');
$scope.realm = realm;
@@ -548,6 +548,7 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, R
};
$scope.emailActions = [];
+ $scope.emailActionsTimeout = TimeUnit2.asUnit(realm.actionTokenGeneratedByAdminLifespan);
$scope.disableableCredentialTypes = [];
$scope.sendExecuteActionsEmail = function() {
@@ -556,7 +557,7 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, R
return;
}
Dialog.confirm('Send Email', 'Are you sure you want to send email to user?', function() {
- UserExecuteActionsEmail.update({ realm: realm.realm, userId: user.id }, $scope.emailActions, function() {
+ UserExecuteActionsEmail.update({ realm: realm.realm, userId: user.id, lifespan: $scope.emailActionsLifespan.toSeconds() }, $scope.emailActions, function() {
Notifications.success("Email sent to user");
$scope.emailActions = [];
}, function() {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index fe09ebb..e850b3b 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -496,7 +496,8 @@ module.factory('UserCredentials', function($resource) {
module.factory('UserExecuteActionsEmail', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/users/:userId/execute-actions-email', {
realm : '@realm',
- userId : '@userId'
+ userId : '@userId',
+ lifespan : '@lifespan',
}, {
update : {
method : 'PUT'
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
index f0f9e58..f508716 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
@@ -142,6 +142,40 @@
</div>
<div class="form-group">
+ <label class="col-md-2 control-label" for="actionTokenGeneratedByUserLifespan" class="two-lines">{{:: 'action-token-generated-by-user-lifespan' | translate}}</label>
+
+ <div class="col-md-6 time-selector">
+ <input class="form-control" type="number" required min="1" max="31536000" data-ng-model="realm.actionTokenGeneratedByUserLifespan.time"
+ id="actionTokenGeneratedByUserLifespan" name="actionTokenGeneratedByUserLifespan">
+ <select class="form-control" name="actionTokenGeneratedByUserLifespanUnit" data-ng-model="realm.actionTokenGeneratedByUserLifespan.unit">
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
+ </select>
+ </div>
+ <kc-tooltip>
+ {{:: 'action-token-generated-by-user-lifespan.tooltip' | translate}}
+ </kc-tooltip>
+ </div>
+
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="actionTokenGeneratedByAdminLifespan" class="two-lines">{{:: 'action-token-generated-by-admin-lifespan' | translate}}</label>
+
+ <div class="col-md-6 time-selector">
+ <input class="form-control" type="number" required min="1" max="31536000" data-ng-model="realm.actionTokenGeneratedByAdminLifespan.time"
+ id="actionTokenGeneratedByAdminLifespan" name="actionTokenGeneratedByAdminLifespan">
+ <select class="form-control" name="actionTokenGeneratedByAdminLifespanUnit" data-ng-model="realm.actionTokenGeneratedByAdminLifespan.unit">
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
+ </select>
+ </div>
+ <kc-tooltip>
+ {{:: 'action-token-generated-by-admin-lifespan.tooltip' | translate}}
+ </kc-tooltip>
+ </div>
+
+ <div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
index d213fdd..9f3512e 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
@@ -76,6 +76,20 @@
<kc-tooltip>{{:: 'credentials.reset-actions.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="reqActionsEmailTimeout">{{:: 'credential-reset-actions-timeout' | translate}}</label>
+
+ <div class="col-md-6 time-selector">
+ <input class="form-control" type="number" required min="1" max="31536000" data-ng-model="emailActionsTimeout.time"
+ id="reqActionsEmailTimeout" name="reqActionsEmailTimeout">
+ <select class="form-control" name="reqActionsEmailTimeoutUnit" data-ng-model="emailActionsTimeout.unit">
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
+ </select>
+ </div>
+ <kc-tooltip>{{:: 'credential-reset-actions-timeout.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
<label class="col-md-2 control-label" for="reqActionsEmail">{{:: 'reset-actions-email' | translate}}</label>
<div class="col-md-6">
diff --git a/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl b/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl
index 5dc29f1..1dbd43d 100644
--- a/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl
+++ b/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl
@@ -9,7 +9,10 @@
${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
</p>
<p id="instruction2" class="instruction">
- ${msg("emailLinkIdp2")} <a href="${url.firstBrokerLoginUrl}">${msg("doClickHere")}</a> ${msg("emailLinkIdp3")}
+ ${msg("emailLinkIdp2")} <a href="${url.loginAction}">${msg("doClickHere")}</a> ${msg("emailLinkIdp3")}
+ </p>
+ <p id="instruction3" class="instruction">
+ ${msg("emailLinkIdp4")} <a href="${url.loginAction}">${msg("doClickHere")}</a> ${msg("emailLinkIdp5")}
</p>
</#if>
</@layout.registrationLayout>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/login/login-page-expired.ftl b/themes/src/main/resources/theme/base/login/login-page-expired.ftl
new file mode 100644
index 0000000..f58c25d
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/login-page-expired.ftl
@@ -0,0 +1,13 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "title">
+ ${msg("pageExpiredTitle")}
+ <#elseif section = "header">
+ ${msg("pageExpiredTitle")}
+ <#elseif section = "form">
+ <p id="instruction1" class="instruction">
+ ${msg("pageExpiredMsg1")} <a id="loginRestartLink" href="${url.loginRestartFlowUrl}">${msg("doClickHere")}</a> .
+ ${msg("pageExpiredMsg2")} <a id="loginContinueLink" href="${url.loginAction}">${msg("doClickHere")}</a> .
+ </p>
+ </#if>
+</@layout.registrationLayout>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/login/login-verify-email.ftl b/themes/src/main/resources/theme/base/login/login-verify-email.ftl
index 1396351..53caaa3 100755
--- a/themes/src/main/resources/theme/base/login/login-verify-email.ftl
+++ b/themes/src/main/resources/theme/base/login/login-verify-email.ftl
@@ -9,7 +9,7 @@
${msg("emailVerifyInstruction1")}
</p>
<p class="instruction">
- ${msg("emailVerifyInstruction2")} <a href="${url.loginEmailVerificationUrl}">${msg("doClickHere")}</a> ${msg("emailVerifyInstruction3")}
+ ${msg("emailVerifyInstruction2")} <a href="${url.loginAction}">${msg("doClickHere")}</a> ${msg("emailVerifyInstruction3")}
</p>
</#if>
</@layout.registrationLayout>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 6c168ae..cf26236 100755
--- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -83,6 +83,8 @@ emailLinkIdpTitle=Link {0}
emailLinkIdp1=An email with instructions to link {0} account {1} with your {2} account has been sent to you.
emailLinkIdp2=Haven''t received a verification code in your email?
emailLinkIdp3=to re-send the email.
+emailLinkIdp4=If you already verified the email in different browser
+emailLinkIdp5=to continue.
backToLogin=« Back to Login
@@ -90,6 +92,10 @@ emailInstruction=Enter your username or email address and we will send you instr
copyCodeInstruction=Please copy this code and paste it into your application:
+pageExpiredTitle=Page has expired
+pageExpiredMsg1=To restart the login process
+pageExpiredMsg2=To continue the login process
+
personalInfo=Personal Info:
role_admin=Admin
role_realm-admin=Realm Admin
@@ -123,6 +129,7 @@ invalidEmailMessage=Invalid email address.
accountDisabledMessage=Account is disabled, contact admin.
accountTemporarilyDisabledMessage=Account is temporarily disabled, contact admin or try again later.
expiredCodeMessage=Login timeout. Please login again.
+expiredActionMessage=Action expired. Please continue with login now.
missingFirstNameMessage=Please specify first name.
missingLastNameMessage=Please specify last name.
@@ -203,12 +210,13 @@ sessionNotActiveMessage=Session not active.
invalidCodeMessage=An error occurred, please login again through your application.
identityProviderUnexpectedErrorMessage=Unexpected error when authenticating with identity provider
identityProviderNotFoundMessage=Could not find an identity provider with the identifier.
-identityProviderLinkSuccess=Your account was successfully linked with {0} account {1} .
+identityProviderLinkSuccess=You successfully verified your email. Please go back to your original browser and continue there with the login.
staleCodeMessage=This page is no longer valid, please go back to your application and login again
realmSupportsNoCredentialsMessage=Realm does not support any credential type.
identityProviderNotUniqueMessage=Realm supports multiple identity providers. Could not determine which identity provider should be used to authenticate with.
emailVerifiedMessage=Your email address has been verified.
staleEmailVerificationLink=The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?
+identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user.
locale_ca=Catal\u00E0
locale_de=Deutsch
@@ -229,5 +237,7 @@ clientNotFoundMessage=Client not found.
clientDisabledMessage=Client disabled.
invalidParameterMessage=Invalid parameter\: {0}
alreadyLoggedIn=You are already logged in.
+differentUserAuthenticated=You are already authenticated as different user ''{0}'' in this session. Please logout first.
+brokerLinkingSessionExpired=Requested broker account linking, but current session is no longer valid.
p3pPolicy=CP="This is not a P3P policy!"
diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java
index 96078ff..d83cd18 100755
--- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java
+++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java
@@ -41,12 +41,12 @@ import java.util.List;
public class KeycloakServerDeploymentProcessor implements DeploymentUnitProcessor {
private static final String[] CACHES = new String[] {
- "realms", "users","sessions","offlineSessions","loginFailures","work","authorization","keys"
+ "realms", "users","sessions","authenticationSessions","offlineSessions","loginFailures","work","authorization","keys","actionTokens"
};
// This param name is defined again in Keycloak Services class
// org.keycloak.services.resources.KeycloakApplication. We have this value in
- // two places to avoid dependency between Keycloak Subsystem and Keyclaok Services module.
+ // two places to avoid dependency between Keycloak Subsystem and Keycloak Services module.
public static final String KEYCLOAK_CONFIG_PARAM_NAME = "org.keycloak.server-subsystem.Config";
@Override
diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
index 205c1f5..0d3b4aa 100755
--- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
+++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
@@ -32,6 +32,7 @@
<eviction max-entries="10000" strategy="LRU"/>
</local-cache>
<local-cache name="sessions"/>
+ <local-cache name="authenticationSessions"/>
<local-cache name="offlineSessions"/>
<local-cache name="loginFailures"/>
<local-cache name="work"/>
@@ -42,6 +43,10 @@
<eviction max-entries="1000" strategy="LRU"/>
<expiration max-idle="3600000" />
</local-cache>
+ <local-cache name="actionTokens">
+ <eviction max-entries="-1" strategy="NONE"/>
+ <expiration max-idle="-1" interval="300000"/>
+ </local-cache>
</cache-container>
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
<local-cache name="default">
@@ -97,6 +102,7 @@
<eviction max-entries="10000" strategy="LRU"/>
</local-cache>
<distributed-cache name="sessions" mode="SYNC" owners="1"/>
+ <distributed-cache name="authenticationSessions" mode="SYNC" owners="1"/>
<distributed-cache name="offlineSessions" mode="SYNC" owners="1"/>
<distributed-cache name="loginFailures" mode="SYNC" owners="1"/>
<local-cache name="authorization">
@@ -107,6 +113,10 @@
<eviction max-entries="1000" strategy="LRU"/>
<expiration max-idle="3600000" />
</local-cache>
+ <local-cache name="actionTokens">
+ <eviction max-entries="-1" strategy="NONE"/>
+ <expiration max-idle="-1" interval="300000"/>
+ </local-cache>
</cache-container>
<cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
<transport lock-timeout="60000"/>
diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml
index 95bcffd..a76162b 100755
--- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml
+++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml
@@ -32,6 +32,7 @@
<eviction max-entries="10000" strategy="LRU"/>
</local-cache>
<local-cache name="sessions"/>
+ <local-cache name="authenticationSessions"/>
<local-cache name="offlineSessions"/>
<local-cache name="loginFailures"/>
<local-cache name="work"/>
@@ -42,6 +43,10 @@
<eviction max-entries="1000" strategy="LRU"/>
<expiration max-idle="3600000" />
</local-cache>
+ <local-cache name="actionTokens">
+ <eviction max-entries="-1" strategy="NONE"/>
+ <expiration max-idle="-1" interval="300000"/>
+ </local-cache>
</cache-container>
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
<local-cache name="default">
@@ -100,6 +105,7 @@
<eviction max-entries="10000" strategy="LRU"/>
</local-cache>
<distributed-cache name="sessions" mode="SYNC" owners="1"/>
+ <distributed-cache name="authenticationSessions" mode="SYNC" owners="1"/>
<distributed-cache name="offlineSessions" mode="SYNC" owners="1"/>
<distributed-cache name="loginFailures" mode="SYNC" owners="1"/>
<local-cache name="authorization">
@@ -110,6 +116,10 @@
<eviction max-entries="1000" strategy="LRU"/>
<expiration max-idle="3600000" />
</local-cache>
+ <local-cache name="actionTokens">
+ <eviction max-entries="-1" strategy="NONE"/>
+ <expiration max-idle="-1" interval="300000"/>
+ </local-cache>
</cache-container>
<cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
<transport lock-timeout="60000"/>