keycloak-uncached
Changes
.travis.yml 12(+3 -9)
client-registration/api/pom.xml 11(+3 -8)
client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistration.java 120(+109 -11)
client-registration/api/src/main/java/org/keycloak/client/registration/ClientRegistrationException.java 0(+0 -0)
client-registration/api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java 13(+13 -0)
client-registration/api/src/main/java/org/keycloak/client/registration/HttpErrorException.java 0(+0 -0)
client-registration/api/src/main/java/org/keycloak/client/registration/OIDCClientRepresentationMixIn.java 22(+22 -0)
client-registration/cli/pom.xml 34(+34 -0)
client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java 72(+72 -0)
client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCommand.java 64(+64 -0)
client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/ExitCommand.java 29(+29 -0)
client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/SetupCommand.java 48(+48 -0)
client-registration/pom.xml 19(+19 -0)
connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java 1(+1 -0)
connections/mongo-update/src/main/java/org/keycloak/connections/mongo/updater/impl/DefaultMongoUpdaterProvider.java 3(+2 -1)
connections/mongo-update/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update1_7_0.java 39(+39 -0)
core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessCreatePresentation.java 36(+36 -0)
distribution/adapters/pom.xml 2(+1 -1)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml 0(+0 -0)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-spi/main/module.xml 0(+0 -0)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-subsystem/main/module.xml 2(+1 -1)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-common/main/module.xml 0(+0 -0)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-core/main/module.xml 0(+0 -0)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml 0(+0 -0)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-servlet-oauth-client/main/module.xml 0(+0 -0)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-undertow-adapter/main/module.xml 0(+0 -0)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-wildfly-adapter/main/module.xml 0(+0 -0)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-wildfly-subsystem/main/module.xml 2(+1 -1)
distribution/demo-dist/assembly.xml 4(+2 -2)
distribution/demo-dist/pom.xml 12(+6 -6)
distribution/downloads/pom.xml 8(+4 -4)
distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-adapter-subsystem/main/module.xml 2(+1 -1)
distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-wf9-subsystem/main/module.xml 4(+2 -2)
distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/add-user.bat 73(+73 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/add-user.sh 72(+72 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/jboss/aesh/0.65/module.xml 37(+37 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/module.xml 2(+1 -1)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-wildfly-adduser/main/module.xml 15(+15 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-wildfly-server-subsystem/main/module.xml 4(+2 -2)
distribution/saml-adapters/pom.xml 2(+1 -1)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-spi/main/module.xml 0(+0 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-common/main/module.xml 0(+0 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-jboss-adapter-core/main/module.xml 0(+0 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-core/main/module.xml 0(+0 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-adapter-subsystem/main/module.xml 2(+1 -1)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-core/main/module.xml 0(+0 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-undertow-adapter/main/module.xml 0(+0 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-adapter/main/module.xml 0(+0 -0)
distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-subsystem/main/module.xml 2(+1 -1)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/module.xml 2(+1 -1)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml 0(+0 -0)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/server-war/WEB-INF/web.xml 0(+0 -0)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-server-subsystem/main/module.xml 2(+1 -1)
events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProviderFactory.java 6(+3 -3)
examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java 77(+48 -29)
export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java 8(+8 -0)
forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java 45(+31 -14)
forms/common-themes/src/main/resources/theme/base/account/messages/messages_ca.properties 155(+155 -0)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_ca.properties 466(+466 -0)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties 31(+26 -5)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_es.properties 74(+37 -37)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js 123(+64 -59)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js 66(+64 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js 73(+71 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html 30(+22 -8)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access.html 52(+52 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access-create.html 63(+63 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html 80(+80 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-attributes.html 4(+2 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-members.html 2(+1 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/role-selector.html 4(+2 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html 12(+5 -7)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html 5(+3 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html 17(+17 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-attributes.html 4(+2 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-group-list.html 11(+11 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html 1(+1 -0)
forms/common-themes/src/main/resources/theme/base/login/messages/messages_ca.properties 206(+206 -0)
forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_ca.properties 21(+21 -0)
forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java 109(+13 -96)
forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProviderFactory.java 11(+5 -6)
forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailProviderFactory 1(+0 -1)
forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailTemplateProviderFactory 1(+1 -0)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java 4(+2 -2)
integration/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java 4(+2 -2)
integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java 6(+3 -3)
integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java 24(+8 -16)
integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java 57(+17 -40)
integration/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java 46(+46 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientInitialAccessResource.java 31(+31 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupResource.java 80(+80 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java 39(+39 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java 29(+28 -1)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java 16(+16 -0)
integration/as7-eap6/pom.xml 1(+0 -1)
integration/jetty/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java 23(+23 -0)
integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java 9(+0 -9)
integration/js/src/main/resources/keycloak.js 261(+198 -63)
integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java 23(+23 -0)
integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java 5(+0 -5)
integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java 19(+17 -2)
integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcher.java 38(+38 -0)
integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java 57(+52 -5)
integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java 15(+15 -0)
integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponse.java 9(+9 -0)
integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcherTest.java 43(+43 -0)
integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPointTest.java 20(+18 -2)
integration/tomcat/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java 24(+24 -0)
integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java 6(+0 -6)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java 12(+6 -6)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java 40(+40 -0)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java 6(+5 -1)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPreAuthActionsHandler.java 4(+3 -1)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java 2(+1 -1)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java 6(+5 -1)
integration/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/ServletHttpFacade.java 46(+46 -0)
integration/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java 22(+22 -0)
integration/wildfly/pom.xml 4(+1 -3)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialAddHandler.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialDefinition.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialReadWriteAttributeHandler.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/CredentialRemoveHandler.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigDeploymentProcessor.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakAdapterConfigService.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakDependencyProcessor.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakDependencyProcessorWildFly.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakExtension.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemAdd.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemDefinition.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/KeycloakSubsystemParser.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmAddHandler.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmDefinition.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmRemoveHandler.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/RealmWriteAttributeHandler.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SecureDeploymentAddHandler.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SecureDeploymentDefinition.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SecureDeploymentRemoveHandler.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SecureDeploymentWriteAttributeHandler.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/extension/SharedAttributeDefinitons.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/logging/KeycloakLogger.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/logging/KeycloakMessages.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/resources/META-INF/services/org.jboss.as.controller.Extension 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/extension/LocalDescriptions.properties 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/main/resources/subsystem-templates/keycloak-adapter.xml 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/extension/RealmDefinitionTestCase.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/extension/SubsystemParsingTestCase.java 0(+0 -0)
integration/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/extension/keycloak-1.1.xml 0(+0 -0)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java 56(+41 -15)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java 76(+54 -22)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java 42(+42 -0)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java 2(+1 -1)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java 32(+22 -10)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java 15(+15 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java 49(+36 -13)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java 3(+3 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java 4(+4 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRoleEntity.java 12(+12 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientInitialAccessAdapter.java 69(+69 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientInitialAccessAdapter.java 55(+55 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientInitialAccessEntity.java 68(+68 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java 65(+51 -14)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java 8(+4 -4)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientInitialAccessEntity.java 48(+48 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 90(+69 -21)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientInitialAccessMapper.java 79(+79 -0)
pom.xml 35(+26 -9)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Configuration.java 60(+60 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Constants.java 120(+120 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/IdentityProviderAddHandler.java 41(+41 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/IdentityProviderDefinition.java 106(+106 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyAddHandler.java 41(+41 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakAdapterConfigDeploymentProcessor.java 80(+68 -12)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSamlExtension.java 19(+11 -8)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java 18(+10 -8)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemDefinition.java 13(+6 -7)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemParser.java 506(+504 -2)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyDefinition.java 122(+122 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyStoreCertificateDefinition.java 22(+13 -9)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyStoreDefinition.java 73(+73 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyStorePrivateKeyDefinition.java 52(+52 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/logging/KeycloakLogger.java 49(+49 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/logging/KeycloakMessages.java 34(+34 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SecureDeploymentAddHandler.java 42(+42 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SecureDeploymentDefinition.java 44(+44 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/ServiceProviderAddHandler.java 43(+43 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/ServiceProviderDefinition.java 125(+125 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SingleLogoutDefinition.java 84(+84 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SingleSignOnDefinition.java 68(+68 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java 534(+534 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/Spliterator.java 66(+66 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/resources/org/keycloak/subsystem/saml/as7/LocalDescriptions.properties 63(+63 -0)
saml/client-adapter/as7-eap6/subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_1.xsd 268(+268 -0)
saml/client-adapter/as7-eap6/subsystem/src/test/resources/org/keycloak/subsystem/saml/as7/keycloak-saml-1.1.xml 49(+49 -0)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java 7(+3 -4)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java 1(+1 -0)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java 42(+23 -19)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java 29(+17 -12)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java 49(+49 -0)
saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java 147(+116 -31)
saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java 9(+0 -9)
saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/JettySamlSessionStore.java 23(+23 -0)
saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java 23(+23 -0)
saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java 68(+38 -30)
saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java 4(+0 -4)
saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java 24(+24 -0)
saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java 39(+19 -20)
saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java 12(+11 -1)
saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java 40(+38 -2)
saml/client-adapter/wildfly/pom.xml 2(+1 -1)
saml/client-adapter/wildfly/wildfly9-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemParser.java 91(+0 -91)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Configuration.java 56(+56 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Constants.java 120(+120 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/IdentityProviderAddHandler.java 37(+37 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/IdentityProviderDefinition.java 106(+106 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyAddHandler.java 37(+37 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakAdapterConfigDeploymentProcessor.java 81(+78 -3)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakDependencyProcessor.java 1(+0 -1)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakDependencyProcessorWildFly.java 0(+0 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSamlExtension.java 12(+8 -4)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemAdd.java 1(+0 -1)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemDefinition.java 9(+5 -4)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemParser.java 569(+569 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyDefinition.java 122(+122 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStoreCertificateDefinition.java 36(+36 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStoreDefinition.java 73(+73 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStorePrivateKeyDefinition.java 52(+52 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/logging/KeycloakLogger.java 45(+45 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/logging/KeycloakMessages.java 34(+34 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SecureDeploymentAddHandler.java 38(+38 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SecureDeploymentDefinition.java 47(+47 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/ServiceProviderAddHandler.java 39(+39 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/ServiceProviderDefinition.java 125(+125 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SingleLogoutDefinition.java 84(+84 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SingleSignOnDefinition.java 68(+68 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/resources/META-INF/services/org.jboss.as.controller.Extension 0(+0 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/saml/extension/LocalDescriptions.properties 63(+63 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_1.xsd 268(+268 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/main/resources/subsystem-templates/keycloak-saml-adapter.xml 4(+2 -2)
saml/client-adapter/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/saml/extension/SubsystemParsingTestCase.java 56(+56 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.1.xml 50(+50 -0)
saml/client-adapter/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.1-err.xml 50(+50 -0)
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java 66(+11 -55)
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java 157(+157 -0)
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/SAMLGroupNameMapper.java 12(+12 -0)
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java 3(+2 -1)
saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper 1(+1 -0)
services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java 6(+3 -3)
services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java 17(+5 -12)
services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java 9(+0 -9)
services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java 9(+0 -9)
services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java 4(+2 -2)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java 166(+47 -119)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java 13(+9 -4)
services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java 12(+11 -1)
services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java 125(+125 -0)
services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java 5(+2 -3)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationAuth.java 116(+82 -34)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationProvider.java 3(+1 -2)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java 4(+2 -2)
services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java 100(+100 -0)
services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java 82(+15 -67)
services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java 18(+7 -11)
services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java 119(+52 -67)
services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProviderFactory.java 4(+3 -1)
services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientResponseRepresentation.java 77(+0 -77)
services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java 102(+102 -0)
services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java 16(+13 -3)
services/src/main/resources/META-INF/services/org.keycloak.email.EmailSenderProviderFactory 1(+1 -0)
testsuite/docker-cluster/pom.xml 4(+2 -2)
testsuite/integration/pom.xml 4(+4 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java 14(+12 -2)
testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientInitialAccessTest.java 58(+58 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java 14(+14 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java 5(+3 -2)
testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java 20(+13 -7)
testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java 133(+110 -23)
testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlKeycloakRule.java 1(+1 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java 12(+11 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java 12(+10 -2)
testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java 9(+5 -4)
testsuite/integration/src/test/java/org/keycloak/testsuite/offlineconfig/AdminRecoveryTest.java 133(+0 -133)
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java 12(+12 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java 19(+8 -11)
testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlKeycloakRule.java 1(+1 -0)
testsuite/integration/src/test/resources/keycloak-saml/simple-post-passive/WEB-INF/keycloak-saml.xml 25(+25 -0)
testsuite/integration-arquillian/tests/adapters/as7/src/test/java/org/keycloak/testsuite/adapter/servlet/AS7DemoServletsAdapterTest.java 2(+2 -0)
testsuite/integration-arquillian/tests/adapters/as7/src/test/java/org/keycloak/testsuite/adapter/servlet/AS7SessionServletAdapterTest.java 2(+2 -0)
testsuite/integration-arquillian/tests/adapters/eap6/src/test/java/org/keycloak/testsuite/adapter/servlet/EAP6DemoServletsAdapterTest.java 14(+14 -0)
testsuite/integration-arquillian/tests/adapters/eap6/src/test/java/org/keycloak/testsuite/adapter/servlet/EAP6SessionServletAdapterTest.java 14(+14 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Authentication.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateExecution.java 40(+40 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateExecutionForm.java 63(+63 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateFlow.java 44(+44 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateFlowForm.java 67(+67 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/Flows.java 90(+90 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/FlowsTable.java 93(+93 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/otppolicy/OTPPolicyForm.java 16(+9 -7)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/clients/CreateClientForm.java 42(+34 -8)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserAttributesForm.java 4(+2 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java 32(+23 -9)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java 3(+1 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java 16(+0 -16)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/InitialAccessTokenTest.java 103(+103 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java 85(+85 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java 19(+9 -10)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java 42(+42 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/FlowsTest.java 196(+196 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/OTPPolicyTest.java 19(+9 -10)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java 4(+3 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/ConfigTest.java 53(+53 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java 5(+4 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java 2(+2 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/URLAssert.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml 82(+82 -0)
testsuite/jetty/jetty81/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml 20(+20 -0)
testsuite/jetty/jetty81/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml 29(+29 -0)
testsuite/jetty/jetty91/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml 20(+20 -0)
testsuite/jetty/jetty91/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml 29(+28 -1)
testsuite/jetty/jetty92/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml 20(+20 -0)
testsuite/jetty/jetty92/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml 28(+28 -0)
wildfly/adduser/pom.xml 54(+54 -0)
wildfly/extensions/pom.xml 3(+1 -2)
wildfly/extensions/src/main/java/org/keycloak/provider/wildfly/ModuleProviderLoaderFactory.java 0(+0 -0)
wildfly/extensions/src/main/java/org/keycloak/provider/wildfly/ModuleThemeProviderFactory.java 0(+0 -0)
wildfly/extensions/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProviderFactory 0(+0 -0)
wildfly/extensions/src/main/resources/META-INF/services/org.keycloak.provider.ProviderLoaderFactory 0(+0 -0)
wildfly/pom.xml 22(+22 -0)
wildfly/server-eap6-subsystem/src/main/java/org/keycloak/subsystem/server/as7/KeycloakAdapterConfigService.java 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/java/org/keycloak/subsystem/server/as7/KeycloakExtension.java 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/java/org/keycloak/subsystem/server/as7/KeycloakServerDeploymentProcessor.java 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/java/org/keycloak/subsystem/server/as7/KeycloakSubsystemAdd.java 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/java/org/keycloak/subsystem/server/as7/KeycloakSubsystemDefinition.java 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/java/org/keycloak/subsystem/server/as7/KeycloakSubsystemParser.java 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/java/org/keycloak/subsystem/server/as7/KeycloakSubsystemRemoveHandler.java 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/java/org/keycloak/subsystem/server/as7/KeycloakSubsystemWriteAttributeHandler.java 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/java/org/keycloak/subsystem/server/as7/ServerUtil.java 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/java/org/keycloak/subsystem/server/logging/KeycloakLogger.java 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/java/org/keycloak/subsystem/server/logging/KeycloakMessages.java 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/resources/META-INF/services/org.jboss.as.controller.Extension 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/resources/org/keycloak/subsystem/server/as7/LocalDescriptions.properties 0(+0 -0)
wildfly/server-eap6-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml 0(+0 -0)
wildfly/server-eap6-subsystem/src/test/resources/org/keycloak/subsystem/server/extension/keycloak-server-1.1.xml 0(+0 -0)
wildfly/server-subsystem/pom.xml 12(+3 -9)
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakAdapterConfigService.java 0(+0 -0)
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakExtension.java 0(+0 -0)
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java 0(+0 -0)
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemAdd.java 0(+0 -0)
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemDefinition.java 0(+0 -0)
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemParser.java 0(+0 -0)
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemRemoveHandler.java 0(+0 -0)
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakSubsystemWriteAttributeHandler.java 0(+0 -0)
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ServerUtil.java 0(+0 -0)
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/logging/KeycloakLogger.java 0(+0 -0)
wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/logging/KeycloakMessages.java 0(+0 -0)
wildfly/server-subsystem/src/main/resources/META-INF/services/org.jboss.as.controller.Extension 0(+0 -0)
wildfly/server-subsystem/src/main/resources/org/keycloak/subsystem/server/extension/LocalDescriptions.properties 0(+0 -0)
Details
.travis.yml 12(+3 -9)
diff --git a/.travis.yml b/.travis.yml
index 8146d6e..b2b3627 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,17 +3,11 @@ language: java
jdk:
- oraclejdk8
-cache:
- directories:
- - $HOME/.m2
-
-before_cache:
- - rm -rf $HOME/.m2/repository/org/keycloak
-
-install: mvn install -Pdistribution -DskipTests=true -B -V
+install:
+ - travis_wait mvn install -Pdistribution -DskipTests=true -B -V -q
script:
- mvn test -B
- - mvn -file testsuite/integration-arquillian test -B
+ - mvn -file testsuite/integration-arquillian test -B -Pno-console
sudo: false
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
index eebf18d..3cf3242 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
@@ -5,6 +5,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessTokenResponse;
@@ -51,7 +52,13 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
@POST
@Path(AdapterConstants.K_LOGOUT)
public Response backchannelLogout(String input) {
- JWSInput token = new JWSInput(input);
+ JWSInput token = null;
+ try {
+ token = new JWSInput(input);
+ } catch (JWSInputException e) {
+ logger.warn("Failed to verify logout request");
+ return Response.status(400).build();
+ }
PublicKey key = getExternalIdpKey();
if (key != null) {
if (!verify(token, key)) {
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java
index 4178069..8547a03 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractClaimMapper.java
@@ -89,7 +89,7 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
} else if (value instanceof List) {
List list = (List)value;
for (Object val : list) {
- return valueEquals(desiredValue, val);
+ if (valueEquals(desiredValue, val)) return true;
}
}
return false;
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index 4d5b45e..6938c11 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -29,6 +29,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel;
@@ -282,40 +283,41 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
throw new IdentityBrokerException("No token from server.");
}
+ JsonWebToken token;
try {
JWSInput jws = new JWSInput(encodedToken);
if (!verify(jws, key)) {
throw new IdentityBrokerException("token signature validation failed");
}
- JsonWebToken token = jws.readJsonContent(JsonWebToken.class);
+ token = jws.readJsonContent(JsonWebToken.class);
+ } catch (JWSInputException e) {
+ throw new IdentityBrokerException("Invalid token", e);
+ }
- String iss = token.getIssuer();
+ String iss = token.getIssuer();
- if (!token.hasAudience(getConfig().getClientId())) {
- throw new IdentityBrokerException("Wrong audience from token.");
- }
+ if (!token.hasAudience(getConfig().getClientId())) {
+ throw new IdentityBrokerException("Wrong audience from token.");
+ }
- if (!token.isActive()) {
- throw new IdentityBrokerException("Token is no longer valid");
- }
+ if (!token.isActive()) {
+ throw new IdentityBrokerException("Token is no longer valid");
+ }
- String trustedIssuers = getConfig().getIssuer();
+ String trustedIssuers = getConfig().getIssuer();
- if (trustedIssuers != null) {
- String[] issuers = trustedIssuers.split(",");
+ if (trustedIssuers != null) {
+ String[] issuers = trustedIssuers.split(",");
- for (String trustedIssuer : issuers) {
- if (iss != null && iss.equals(trustedIssuer.trim())) {
- return token;
- }
+ for (String trustedIssuer : issuers) {
+ if (iss != null && iss.equals(trustedIssuer.trim())) {
+ return token;
}
-
- throw new IdentityBrokerException("Wrong issuer from token. Got: " + iss + " expected: " + getConfig().getIssuer());
}
- return token;
- } catch (IOException e) {
- throw new IdentityBrokerException("Could not decode token.", e);
+
+ throw new IdentityBrokerException("Wrong issuer from token. Got: " + iss + " expected: " + getConfig().getIssuer());
}
+ return token;
}
@Override
diff --git a/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java b/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java
new file mode 100644
index 0000000..ba382f6
--- /dev/null
+++ b/client-registration/api/src/main/java/org/keycloak/client/registration/ClientRepresentationMixIn.java
@@ -0,0 +1,13 @@
+package org.keycloak.client.registration;
+
+import org.codehaus.jackson.annotate.JsonIgnore;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+abstract class ClientRepresentationMixIn {
+
+ @JsonIgnore
+ String registrationAccessToken;
+
+}
diff --git a/client-registration/api/src/main/java/org/keycloak/client/registration/OIDCClientRepresentationMixIn.java b/client-registration/api/src/main/java/org/keycloak/client/registration/OIDCClientRepresentationMixIn.java
new file mode 100644
index 0000000..b0dfed6
--- /dev/null
+++ b/client-registration/api/src/main/java/org/keycloak/client/registration/OIDCClientRepresentationMixIn.java
@@ -0,0 +1,22 @@
+package org.keycloak.client.registration;
+
+import org.codehaus.jackson.annotate.JsonIgnore;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+abstract class OIDCClientRepresentationMixIn {
+
+ @JsonIgnore
+ private Integer client_id_issued_at;
+
+ @JsonIgnore
+ private Integer client_secret_expires_at;
+
+ @JsonIgnore
+ private String registration_client_uri;
+
+ @JsonIgnore
+ private String registration_access_token;
+
+}
client-registration/cli/pom.xml 34(+34 -0)
diff --git a/client-registration/cli/pom.xml b/client-registration/cli/pom.xml
new file mode 100755
index 0000000..13b8a8f
--- /dev/null
+++ b/client-registration/cli/pom.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-client-registration-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.7.0.Final-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-client-registration-cli</artifactId>
+ <name>Keycloak Client Registration CLI</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-client-registration-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.aesh</groupId>
+ <artifactId>aesh</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java
new file mode 100644
index 0000000..e76648b
--- /dev/null
+++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/ClientRegistrationCLI.java
@@ -0,0 +1,72 @@
+package org.keycloak.client.registration.cli;
+
+import org.jboss.aesh.cl.parser.CommandLineParserException;
+import org.jboss.aesh.console.AeshConsole;
+import org.jboss.aesh.console.AeshConsoleBuilder;
+import org.jboss.aesh.console.Prompt;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandNotFoundException;
+import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
+import org.jboss.aesh.console.settings.Settings;
+import org.jboss.aesh.console.settings.SettingsBuilder;
+import org.jboss.aesh.terminal.Color;
+import org.jboss.aesh.terminal.TerminalColor;
+import org.jboss.aesh.terminal.TerminalString;
+import org.keycloak.client.registration.Auth;
+import org.keycloak.client.registration.ClientRegistration;
+import org.keycloak.client.registration.cli.commands.CreateCommand;
+import org.keycloak.client.registration.cli.commands.ExitCommand;
+import org.keycloak.client.registration.cli.commands.SetupCommand;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientRegistrationCLI {
+
+ private static ClientRegistration reg;
+
+ public static void main(String[] args) throws CommandLineParserException, CommandNotFoundException {
+ reg = ClientRegistration.create().url("http://localhost:8080/auth/realms/master").build();
+ reg.auth(Auth.token("..."));
+
+ Context context = new Context();
+
+ List<Command> commands = new LinkedList<>();
+ commands.add(new SetupCommand(context));
+ commands.add(new CreateCommand(context));
+ commands.add(new ExitCommand(context));
+
+ SettingsBuilder builder = new SettingsBuilder().logging(true);
+ builder.enableMan(true).readInputrc(false);
+
+ Settings settings = builder.create();
+
+ AeshCommandRegistryBuilder commandRegistryBuilder = new AeshCommandRegistryBuilder();
+ for (Command c : commands) {
+ commandRegistryBuilder.command(c);
+ }
+
+ AeshConsole aeshConsole = new AeshConsoleBuilder()
+ .commandRegistry(commandRegistryBuilder.create())
+ .settings(settings)
+ .prompt(new Prompt(new TerminalString("[clientreg]$ ",
+ new TerminalColor(Color.GREEN, Color.DEFAULT, Color.Intensity.BRIGHT))))
+ .create();
+
+ aeshConsole.start();
+
+
+/*
+ if (args.length > 0) {
+ CommandContainer command = registry.getCommand(args[0], null);
+ ParserGenerator.parseAndPopulate(command, args[0], Arrays.copyOfRange(args, 1, args.length));
+ }*/
+
+ //commandInvocation.getCommandRegistry().getAllCommandNames()
+ }
+
+}
+
diff --git a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCommand.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCommand.java
new file mode 100644
index 0000000..280534b
--- /dev/null
+++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/CreateCommand.java
@@ -0,0 +1,64 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.Arguments;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.jboss.aesh.io.Resource;
+import org.keycloak.client.registration.ClientRegistrationException;
+import org.keycloak.client.registration.cli.Context;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@CommandDefinition(name="create", description = "[OPTIONS] FILE")
+public class CreateCommand implements Command {
+
+ @Option(shortName = 'h', hasValue = false, description = "display this help and exit")
+ private boolean help;
+
+ @Arguments(description = "files or directories thats listed")
+ private List<Resource> arguments;
+
+ private Context context;
+
+ public CreateCommand(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
+ System.out.println(help);
+
+
+ if(help) {
+ commandInvocation.getShell().out().println(commandInvocation.getHelpInfo("create"));
+ }
+ else {
+
+ if(arguments != null) {
+ for(Resource f : arguments) {
+ System.out.println(f.getAbsolutePath());
+ ClientRepresentation rep = JsonSerialization.readValue(f.read(), ClientRepresentation.class);
+ try {
+ context.getReg().create(rep);
+ } catch (ClientRegistrationException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+ }
+// reg.create();
+
+ return CommandResult.SUCCESS;
+ }
+
+}
diff --git a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/ExitCommand.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/ExitCommand.java
new file mode 100644
index 0000000..507881b
--- /dev/null
+++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/ExitCommand.java
@@ -0,0 +1,29 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.keycloak.client.registration.cli.Context;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@CommandDefinition(name="exit", description = "Exit the program")
+public class ExitCommand implements Command {
+
+ private Context context;
+
+ public ExitCommand(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
+ commandInvocation.stop();
+ return CommandResult.SUCCESS;
+ }
+
+}
diff --git a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/SetupCommand.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/SetupCommand.java
new file mode 100644
index 0000000..26579ab
--- /dev/null
+++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/commands/SetupCommand.java
@@ -0,0 +1,48 @@
+package org.keycloak.client.registration.cli.commands;
+
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.jboss.aesh.io.Resource;
+import org.keycloak.client.registration.ClientRegistrationException;
+import org.keycloak.client.registration.cli.Context;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@CommandDefinition(name="setup", description = "")
+public class SetupCommand implements Command {
+
+ @Option(shortName = 'h', hasValue = false, description = "display this help and exit")
+ private boolean help;
+
+ private Context context;
+
+ public SetupCommand(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
+ System.out.println(help);
+
+ if(help) {
+ commandInvocation.getShell().out().println(commandInvocation.getHelpInfo("create"));
+ }
+
+ return CommandResult.SUCCESS;
+ }
+
+
+ private String promptForUsername(CommandInvocation invocation) throws InterruptedException {
+ invocation.print("username: ");
+ return invocation.getInputLine();
+ }
+
+}
diff --git a/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/Context.java b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/Context.java
new file mode 100644
index 0000000..49d8fb9
--- /dev/null
+++ b/client-registration/cli/src/main/java/org/keycloak/client/registration/cli/Context.java
@@ -0,0 +1,37 @@
+package org.keycloak.client.registration.cli;
+
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.map.SerializationConfig;
+import org.codehaus.jackson.map.annotate.JsonSerialize;
+import org.keycloak.client.registration.ClientRegistration;
+import org.keycloak.util.SystemPropertiesJsonParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class Context {
+
+ private static final ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory());
+ static {
+ mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
+ mapper.enable(SerializationConfig.Feature.INDENT_OUTPUT);
+ }
+
+ private ClientRegistration reg;
+
+ public ClientRegistration getReg() {
+ return reg;
+ }
+
+ public void setReg(ClientRegistration reg) {
+ this.reg = reg;
+ }
+
+ public static <T> T readJson(InputStream bytes, Class<T> type) throws IOException {
+ return mapper.readValue(bytes, type);
+ }
+
+}
client-registration/pom.xml 19(+19 -0)
diff --git a/client-registration/pom.xml b/client-registration/pom.xml
new file mode 100755
index 0000000..9d1ea9f
--- /dev/null
+++ b/client-registration/pom.xml
@@ -0,0 +1,19 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.7.0.Final-SNAPSHOT</version>
+ </parent>
+
+ <name>Keycloak Client Registration Parent</name>
+ <description/>
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>keycloak-client-registration-parent</artifactId>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>api</module>
+ <!--<module>cli</module>-->
+ </modules>
+</project>
diff --git a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
index 328916e..35085c7 100755
--- a/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
+++ b/common/src/main/java/org/keycloak/common/util/KeycloakUriBuilder.java
@@ -342,6 +342,17 @@ public class KeycloakUriBuilder {
}
/**
+ * Set fragment, but not encode it. It assumes that given fragment was already properly encoded
+ *
+ * @param fragment
+ * @return
+ */
+ public KeycloakUriBuilder encodedFragment(String fragment) {
+ this.fragment = fragment;
+ return this;
+ }
+
+ /**
* Only replace path params in path of URI. This changes state of URIBuilder.
*
* @param name
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml
index aed99fc..eadbdcf 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.7.0.xml
@@ -37,12 +37,25 @@
<constraints nullable="false"/>
</column>
</createTable>
+ <createTable tableName="REALM_DEFAULT_GROUPS">
+ <column name="REALM_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="GROUP_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
<addColumn tableName="IDENTITY_PROVIDER">
<column name="FIRST_BROKER_LOGIN_FLOW_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</addColumn>
+
+ <addColumn tableName="REALM">
+ <column name="ACCESS_TOKEN_LIFE_IMPLICIT" type="INT" defaultValueNumeric="0"/>
+ </addColumn>
+
<dropColumn tableName="IDENTITY_PROVIDER" columnName="UPDATE_PROFILE_FIRST_LGN_MD"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_GROUP" tableName="KEYCLOAK_GROUP"/>
@@ -58,9 +71,40 @@
<addForeignKeyConstraint baseColumnNames="GROUP_ID" baseTableName="GROUP_ROLE_MAPPING" constraintName="FK_GROUP_ROLE_GROUP" referencedColumnNames="ID" referencedTableName="KEYCLOAK_GROUP"/>
<addForeignKeyConstraint baseColumnNames="ROLE_ID" baseTableName="GROUP_ROLE_MAPPING" constraintName="FK_GROUP_ROLE_ROLE" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
+ <addUniqueConstraint columnNames="GROUP_ID" constraintName="CON_GROUP_ID_DEF_GROUPS" tableName="REALM_DEFAULT_GROUPS"/>
+ <addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_DEFAULT_GROUPS" constraintName="FK_DEF_GROUPS_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
+ <addForeignKeyConstraint baseColumnNames="GROUP_ID" baseTableName="REALM_DEFAULT_GROUPS" constraintName="FK_DEF_GROUPS_GROUP" referencedColumnNames="ID" referencedTableName="KEYCLOAK_GROUP"/>
+
<addColumn tableName="CLIENT">
- <column name="REGISTRATION_SECRET" type="VARCHAR(255)"/>
+ <column name="REGISTRATION_TOKEN" type="VARCHAR(255)"/>
+ <column name="STANDARD_FLOW_ENABLED" type="BOOLEAN" defaultValueBoolean="true">
+ <constraints nullable="false"/>
+ </column>
+ <column name="IMPLICIT_FLOW_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
+ <constraints nullable="false"/>
+ </column>
+ <column name="DIRECT_ACCESS_GRANTS_ENABLED" type="BOOLEAN" defaultValueBoolean="true">
+ <constraints nullable="false"/>
+ </column>
</addColumn>
+ <update tableName="CLIENT">
+ <column name="STANDARD_FLOW_ENABLED" valueBoolean="false"/>
+ <where>DIRECT_GRANTS_ONLY = :value</where>
+ <whereParams>
+ <param valueBoolean="true" />
+ </whereParams>
+ </update>
+
+ <dropDefaultValue tableName="CLIENT" columnName="DIRECT_GRANTS_ONLY" />
+ <dropColumn tableName="CLIENT" columnName="DIRECT_GRANTS_ONLY"/>
+
+ <modifyDataType tableName="REALM" columnName="PASSWORD_POLICY" newDataType="VARCHAR(2550)"/>
+
+ <!-- Sybase specific hacks -->
+ <modifySql dbms="sybase">
+ <regExpReplace replace=".*(SET DEFAULT NULL)" with="SELECT 1" />
+ </modifySql>
+
</changeSet>
</databaseChangeLog>
\ No newline at end of file
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
index ae9701b..67cb127 100755
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
@@ -35,6 +35,7 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
"org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoUserEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity",
+ "org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoClientEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity",
diff --git a/connections/mongo-update/src/main/java/org/keycloak/connections/mongo/updater/impl/DefaultMongoUpdaterProvider.java b/connections/mongo-update/src/main/java/org/keycloak/connections/mongo/updater/impl/DefaultMongoUpdaterProvider.java
index fa75d6b..e66e16d 100644
--- a/connections/mongo-update/src/main/java/org/keycloak/connections/mongo/updater/impl/DefaultMongoUpdaterProvider.java
+++ b/connections/mongo-update/src/main/java/org/keycloak/connections/mongo/updater/impl/DefaultMongoUpdaterProvider.java
@@ -28,7 +28,8 @@ public class DefaultMongoUpdaterProvider implements MongoUpdaterProvider {
Update1_2_0_Beta1.class,
Update1_2_0_CR1.class,
Update1_3_0.class,
- Update1_4_0.class
+ Update1_4_0.class,
+ Update1_7_0.class
};
@Override
diff --git a/connections/mongo-update/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update1_7_0.java b/connections/mongo-update/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update1_7_0.java
new file mode 100644
index 0000000..666a3af
--- /dev/null
+++ b/connections/mongo-update/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update1_7_0.java
@@ -0,0 +1,39 @@
+package org.keycloak.connections.mongo.updater.impl.updates;
+
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBCollection;
+import com.mongodb.DBCursor;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class Update1_7_0 extends Update {
+
+ @Override
+ public String getId() {
+ return "1.7.0";
+ }
+
+ @Override
+ public void update(KeycloakSession session) throws ClassNotFoundException {
+ DBCollection clients = db.getCollection("clients");
+ DBCursor clientsCursor = clients.find();
+
+ try {
+ while (clientsCursor.hasNext()) {
+ BasicDBObject client = (BasicDBObject) clientsCursor.next();
+
+ boolean directGrantsOnly = client.getBoolean("directGrantsOnly", false);
+ client.append("standardFlowEnabled", !directGrantsOnly);
+ client.append("implicitFlowEnabled", false);
+ client.append("directAccessGrantsEnabled", true);
+ client.removeField("directGrantsOnly");
+
+ clients.save(client);
+ }
+ } finally {
+ clientsCursor.close();
+ }
+ }
+}
diff --git a/core/src/main/java/org/keycloak/jose/jws/crypto/RSAProvider.java b/core/src/main/java/org/keycloak/jose/jws/crypto/RSAProvider.java
index 59e25ee..dc0e4e3 100755
--- a/core/src/main/java/org/keycloak/jose/jws/crypto/RSAProvider.java
+++ b/core/src/main/java/org/keycloak/jose/jws/crypto/RSAProvider.java
@@ -64,7 +64,7 @@ public class RSAProvider implements SignatureProvider {
verifier.update(input.getEncodedSignatureInput().getBytes("UTF-8"));
return verifier.verify(input.getSignature());
} catch (Exception e) {
- throw new RuntimeException(e);
+ return false;
}
}
diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSInput.java b/core/src/main/java/org/keycloak/jose/jws/JWSInput.java
index b01355f..84f303d 100755
--- a/core/src/main/java/org/keycloak/jose/jws/JWSInput.java
+++ b/core/src/main/java/org/keycloak/jose/jws/JWSInput.java
@@ -21,14 +21,14 @@ public class JWSInput {
byte[] signature;
- public JWSInput(String wire) {
- this.wireString = wire;
- String[] parts = wire.split("\\.");
- if (parts.length < 2 || parts.length > 3) throw new IllegalArgumentException("Parsing error");
- encodedHeader = parts[0];
- encodedContent = parts[1];
- encodedSignatureInput = encodedHeader + '.' + encodedContent;
+ public JWSInput(String wire) throws JWSInputException {
try {
+ this.wireString = wire;
+ String[] parts = wire.split("\\.");
+ if (parts.length < 2 || parts.length > 3) throw new IllegalArgumentException("Parsing error");
+ encodedHeader = parts[0];
+ encodedContent = parts[1];
+ encodedSignatureInput = encodedHeader + '.' + encodedContent;
content = Base64Url.decode(encodedContent);
if (parts.length > 2) {
encodedSignature = parts[2];
@@ -37,8 +37,8 @@ public class JWSInput {
}
byte[] headerBytes = Base64Url.decode(encodedHeader);
header = JsonSerialization.readValue(headerBytes, JWSHeader.class);
- } catch (Exception e) {
- throw new RuntimeException(e);
+ } catch (Throwable t) {
+ throw new JWSInputException(t);
}
}
@@ -80,8 +80,12 @@ public class JWSInput {
return header.getAlgorithm().getProvider().verify(this, key);
}
- public <T> T readJsonContent(Class<T> type) throws IOException {
- return JsonSerialization.readValue(content, type);
+ public <T> T readJsonContent(Class<T> type) throws JWSInputException {
+ try {
+ return JsonSerialization.readValue(content, type);
+ } catch (IOException e) {
+ throw new JWSInputException(e);
+ }
}
public String readContentAsString() {
diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSInputException.java b/core/src/main/java/org/keycloak/jose/jws/JWSInputException.java
new file mode 100644
index 0000000..930a0b6
--- /dev/null
+++ b/core/src/main/java/org/keycloak/jose/jws/JWSInputException.java
@@ -0,0 +1,18 @@
+package org.keycloak.jose.jws;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class JWSInputException extends Exception {
+
+ public JWSInputException(String s) {
+ super(s);
+ }
+
+ public JWSInputException() {
+ }
+
+ public JWSInputException(Throwable throwable) {
+ super(throwable);
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessCreatePresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessCreatePresentation.java
new file mode 100644
index 0000000..4c18b3d
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessCreatePresentation.java
@@ -0,0 +1,36 @@
+package org.keycloak.representations.idm;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientInitialAccessCreatePresentation {
+
+ private Integer expiration;
+
+ private Integer count;
+
+ public ClientInitialAccessCreatePresentation() {
+ }
+
+ public ClientInitialAccessCreatePresentation(Integer expiration, Integer count) {
+ this.expiration = expiration;
+ this.count = count;
+ }
+
+ public Integer getExpiration() {
+ return expiration;
+ }
+
+ public void setExpiration(Integer expiration) {
+ this.expiration = expiration;
+ }
+
+ public Integer getCount() {
+ return count;
+ }
+
+ public void setCount(Integer count) {
+ this.count = count;
+ }
+
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessPresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessPresentation.java
new file mode 100644
index 0000000..d8021ad
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientInitialAccessPresentation.java
@@ -0,0 +1,67 @@
+package org.keycloak.representations.idm;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientInitialAccessPresentation {
+
+ private String id;
+
+ private String token;
+
+ private Integer timestamp;
+
+ private Integer expiration;
+
+ private Integer count;
+
+ private Integer remainingCount;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ public Integer getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(Integer timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public Integer getExpiration() {
+ return expiration;
+ }
+
+ public void setExpiration(Integer expiration) {
+ this.expiration = expiration;
+ }
+
+ public Integer getCount() {
+ return count;
+ }
+
+ public void setCount(Integer count) {
+ this.count = count;
+ }
+
+ public Integer getRemainingCount() {
+ return remainingCount;
+ }
+
+ public void setRemainingCount(Integer remainingCount) {
+ this.remainingCount = remainingCount;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
index 514c0fb..aef643c 100755
--- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
@@ -26,6 +26,9 @@ public class ClientRepresentation {
protected Integer notBefore;
protected Boolean bearerOnly;
protected Boolean consentRequired;
+ protected Boolean standardFlowEnabled;
+ protected Boolean implicitFlowEnabled;
+ protected Boolean directAccessGrantsEnabled;
protected Boolean serviceAccountsEnabled;
protected Boolean directGrantsOnly;
protected Boolean publicClient;
@@ -181,6 +184,30 @@ public class ClientRepresentation {
this.consentRequired = consentRequired;
}
+ public Boolean isStandardFlowEnabled() {
+ return standardFlowEnabled;
+ }
+
+ public void setStandardFlowEnabled(Boolean standardFlowEnabled) {
+ this.standardFlowEnabled = standardFlowEnabled;
+ }
+
+ public Boolean isImplicitFlowEnabled() {
+ return implicitFlowEnabled;
+ }
+
+ public void setImplicitFlowEnabled(Boolean implicitFlowEnabled) {
+ this.implicitFlowEnabled = implicitFlowEnabled;
+ }
+
+ public Boolean isDirectAccessGrantsEnabled() {
+ return directAccessGrantsEnabled;
+ }
+
+ public void setDirectAccessGrantsEnabled(Boolean directAccessGrantsEnabled) {
+ this.directAccessGrantsEnabled = directAccessGrantsEnabled;
+ }
+
public Boolean isServiceAccountsEnabled() {
return serviceAccountsEnabled;
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java
index 1e57dbc..6ad9715 100755
--- a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java
@@ -29,7 +29,7 @@ public class CredentialRepresentation {
private Integer period;
// only used when updating a credential. Might set required action
- protected boolean temporary;
+ protected Boolean temporary;
public String getType() {
return type;
@@ -79,11 +79,11 @@ public class CredentialRepresentation {
this.hashIterations = hashIterations;
}
- public boolean isTemporary() {
+ public Boolean isTemporary() {
return temporary;
}
- public void setTemporary(boolean temporary) {
+ public void setTemporary(Boolean temporary) {
this.temporary = temporary;
}
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 e1333e9..bc9ebf8 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -12,6 +12,7 @@ public class RealmRepresentation {
protected Integer notBefore;
protected Boolean revokeRefreshToken;
protected Integer accessTokenLifespan;
+ protected Integer accessTokenLifespanForImplicitFlow;
protected Integer ssoSessionIdleTimeout;
protected Integer ssoSessionMaxLifespan;
protected Integer offlineSessionIdleTimeout;
@@ -49,6 +50,7 @@ public class RealmRepresentation {
protected RolesRepresentation roles;
protected List<GroupRepresentation> groups;
protected List<String> defaultRoles;
+ protected List<String> defaultGroups;
@Deprecated
protected Set<String> requiredCredentials;
protected String passwordPolicy;
@@ -83,6 +85,7 @@ public class RealmRepresentation {
private List<IdentityProviderRepresentation> identityProviders;
private List<IdentityProviderMapperRepresentation> identityProviderMappers;
private List<ProtocolMapperRepresentation> protocolMappers;
+ @Deprecated
private Boolean identityFederationEnabled;
protected Boolean internationalizationEnabled;
protected Set<String> supportedLocales;
@@ -185,6 +188,14 @@ public class RealmRepresentation {
this.accessTokenLifespan = accessTokenLifespan;
}
+ public Integer getAccessTokenLifespanForImplicitFlow() {
+ return accessTokenLifespanForImplicitFlow;
+ }
+
+ public void setAccessTokenLifespanForImplicitFlow(Integer accessTokenLifespanForImplicitFlow) {
+ this.accessTokenLifespanForImplicitFlow = accessTokenLifespanForImplicitFlow;
+ }
+
public Integer getSsoSessionIdleTimeout() {
return ssoSessionIdleTimeout;
}
@@ -269,6 +280,14 @@ public class RealmRepresentation {
this.defaultRoles = defaultRoles;
}
+ public List<String> getDefaultGroups() {
+ return defaultGroups;
+ }
+
+ public void setDefaultGroups(List<String> defaultGroups) {
+ this.defaultGroups = defaultGroups;
+ }
+
public String getPrivateKey() {
return privateKey;
}
@@ -604,6 +623,7 @@ public class RealmRepresentation {
identityProviders.add(identityProviderRepresentation);
}
+ @Deprecated
public boolean isIdentityFederationEnabled() {
return identityProviders != null && !identityProviders.isEmpty();
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
index 07865db..0990a4e 100755
--- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
@@ -17,9 +17,9 @@ public class UserRepresentation {
protected String id;
protected Long createdTimestamp;
protected String username;
- protected boolean enabled;
- protected boolean totp;
- protected boolean emailVerified;
+ protected Boolean enabled;
+ protected Boolean totp;
+ protected Boolean emailVerified;
protected String firstName;
protected String lastName;
protected String email;
@@ -40,6 +40,8 @@ public class UserRepresentation {
@Deprecated
protected List<SocialLinkRepresentation> socialLinks;
+ protected List<String> groups;
+
public String getSelf() {
return self;
}
@@ -96,27 +98,27 @@ public class UserRepresentation {
this.username = username;
}
- public boolean isEnabled() {
+ public Boolean isEnabled() {
return enabled;
}
- public void setEnabled(boolean enabled) {
+ public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
- public boolean isTotp() {
+ public Boolean isTotp() {
return totp;
}
- public void setTotp(boolean totp) {
+ public void setTotp(Boolean totp) {
this.totp = totp;
}
- public boolean isEmailVerified() {
+ public Boolean isEmailVerified() {
return emailVerified;
}
- public void setEmailVerified(boolean emailVerified) {
+ public void setEmailVerified(Boolean emailVerified) {
this.emailVerified = emailVerified;
}
@@ -216,4 +218,12 @@ public class UserRepresentation {
public void setServiceAccountClientId(String serviceAccountClientId) {
this.serviceAccountClientId = serviceAccountClientId;
}
+
+ public List<String> getGroups() {
+ return groups;
+ }
+
+ public void setGroups(List<String> groups) {
+ this.groups = groups;
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/RefreshToken.java b/core/src/main/java/org/keycloak/representations/RefreshToken.java
index 39c7c46..0300673 100755
--- a/core/src/main/java/org/keycloak/representations/RefreshToken.java
+++ b/core/src/main/java/org/keycloak/representations/RefreshToken.java
@@ -28,6 +28,7 @@ public class RefreshToken extends AccessToken {
this.subject = token.subject;
this.issuedFor = token.issuedFor;
this.sessionState = token.sessionState;
+ this.nonce = token.nonce;
if (token.realmAccess != null) {
realmAccess = token.realmAccess.clone();
}
diff --git a/core/src/main/java/org/keycloak/RSATokenVerifier.java b/core/src/main/java/org/keycloak/RSATokenVerifier.java
index f177166..19babef 100755
--- a/core/src/main/java/org/keycloak/RSATokenVerifier.java
+++ b/core/src/main/java/org/keycloak/RSATokenVerifier.java
@@ -2,6 +2,7 @@ package org.keycloak;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.util.TokenUtil;
@@ -22,7 +23,7 @@ public class RSATokenVerifier {
JWSInput input = null;
try {
input = new JWSInput(tokenString);
- } catch (Exception e) {
+ } catch (JWSInputException e) {
throw new VerificationException("Couldn't parse token", e);
}
if (!isPublicKeyValid(input, realmKey)) throw new VerificationException("Invalid token signature.");
@@ -30,7 +31,7 @@ public class RSATokenVerifier {
AccessToken token;
try {
token = input.readJsonContent(AccessToken.class);
- } catch (IOException e) {
+ } catch (JWSInputException e) {
throw new VerificationException("Couldn't parse token signature", e);
}
String user = token.getSubject();
diff --git a/core/src/main/java/org/keycloak/util/JsonSerialization.java b/core/src/main/java/org/keycloak/util/JsonSerialization.java
index a1a93ba..19df33f 100755
--- a/core/src/main/java/org/keycloak/util/JsonSerialization.java
+++ b/core/src/main/java/org/keycloak/util/JsonSerialization.java
@@ -3,6 +3,7 @@ package org.keycloak.util;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.annotate.JsonSerialize;
+import org.codehaus.jackson.type.TypeReference;
import java.io.IOException;
import java.io.InputStream;
@@ -27,7 +28,10 @@ public class JsonSerialization {
public static void writeValueToStream(OutputStream os, Object obj) throws IOException {
mapper.writeValue(os, obj);
+ }
+ public static void writeValuePrettyToStream(OutputStream os, Object obj) throws IOException {
+ prettyMapper.writeValue(os, obj);
}
public static String writeValueAsPrettyString(Object obj) throws IOException {
@@ -53,6 +57,10 @@ public class JsonSerialization {
return readValue(bytes, type, false);
}
+ public static <T> T readValue(InputStream bytes, TypeReference<T> type) throws IOException {
+ return mapper.readValue(bytes, type);
+ }
+
public static <T> T readValue(InputStream bytes, Class<T> type, boolean replaceSystemProperties) throws IOException {
if (replaceSystemProperties) {
return sysPropertiesAwareMapper.readValue(bytes, type);
diff --git a/core/src/main/java/org/keycloak/util/TokenUtil.java b/core/src/main/java/org/keycloak/util/TokenUtil.java
index 0d103a6..94f6a73 100644
--- a/core/src/main/java/org/keycloak/util/TokenUtil.java
+++ b/core/src/main/java/org/keycloak/util/TokenUtil.java
@@ -4,6 +4,7 @@ import java.io.IOException;
import org.keycloak.OAuth2Constants;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.RefreshToken;
/**
@@ -19,6 +20,7 @@ public class TokenUtil {
public static final String TOKEN_TYPE_OFFLINE = "Offline";
+
public static boolean isOfflineTokenRequested(String scopeParam) {
if (scopeParam == null) {
return false;
@@ -40,11 +42,15 @@ public class TokenUtil {
* @param decodedToken
* @return
*/
- public static RefreshToken getRefreshToken(byte[] decodedToken) throws IOException {
- return JsonSerialization.readValue(decodedToken, RefreshToken.class);
+ public static RefreshToken getRefreshToken(byte[] decodedToken) throws JWSInputException {
+ try {
+ return JsonSerialization.readValue(decodedToken, RefreshToken.class);
+ } catch (IOException e) {
+ throw new JWSInputException(e);
+ }
}
- public static RefreshToken getRefreshToken(String refreshToken) throws IOException {
+ public static RefreshToken getRefreshToken(String refreshToken) throws JWSInputException {
byte[] encodedContent = new JWSInput(refreshToken).getContent();
return getRefreshToken(encodedContent);
}
@@ -55,13 +61,9 @@ public class TokenUtil {
* @param refreshToken
* @return
*/
- public static boolean isOfflineToken(String refreshToken) {
- try {
- RefreshToken token = getRefreshToken(refreshToken);
- return token.getType().equals(TOKEN_TYPE_OFFLINE);
- } catch (IOException ioe) {
- throw new RuntimeException(ioe);
- }
+ public static boolean isOfflineToken(String refreshToken) throws JWSInputException {
+ RefreshToken token = getRefreshToken(refreshToken);
+ return token.getType().equals(TOKEN_TYPE_OFFLINE);
}
}
distribution/adapters/pom.xml 2(+1 -1)
diff --git a/distribution/adapters/pom.xml b/distribution/adapters/pom.xml
index 10c8e0f..d13d992 100755
--- a/distribution/adapters/pom.xml
+++ b/distribution/adapters/pom.xml
@@ -25,6 +25,6 @@
<module>tomcat7-adapter-zip</module>
<module>tomcat8-adapter-zip</module>
<module>wf8-adapter</module>
- <module>wf9-adapter</module>
+ <module>wildfly-adapter</module>
</modules>
</project>
distribution/demo-dist/assembly.xml 4(+2 -2)
diff --git a/distribution/demo-dist/assembly.xml b/distribution/demo-dist/assembly.xml
index f1c39c2..bd169bf 100755
--- a/distribution/demo-dist/assembly.xml
+++ b/distribution/demo-dist/assembly.xml
@@ -33,14 +33,14 @@
</excludes>
</fileSet>
<fileSet>
- <directory>${project.build.directory}/unpacked/keycloak-wf9-adapter-${project.version}</directory>
+ <directory>${project.build.directory}/unpacked/keycloak-wildfly-adapter-${project.version}</directory>
<outputDirectory>keycloak</outputDirectory>
<excludes>
<exclude>standalone/configuration/standalone-keycloak.xml</exclude>
</excludes>
</fileSet>
<fileSet>
- <directory>${project.build.directory}/unpacked/keycloak-saml-wf9-adapter-${project.version}</directory>
+ <directory>${project.build.directory}/unpacked/keycloak-saml-wildfly-adapter-${project.version}</directory>
<outputDirectory>keycloak</outputDirectory>
<excludes>
<exclude>standalone/configuration/standalone-keycloak.xml</exclude>
distribution/demo-dist/pom.xml 12(+6 -6)
diff --git a/distribution/demo-dist/pom.xml b/distribution/demo-dist/pom.xml
index 9443478..2caeb2b 100755
--- a/distribution/demo-dist/pom.xml
+++ b/distribution/demo-dist/pom.xml
@@ -21,12 +21,12 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<type>zip</type>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-saml-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-saml-wildfly-adapter-dist</artifactId>
<type>zip</type>
</dependency>
<dependency>
@@ -99,9 +99,9 @@
<artifactItems>
<artifactItem>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<type>zip</type>
- <outputDirectory>${project.build.directory}/unpacked/keycloak-wf9-adapter-${project.version}</outputDirectory>
+ <outputDirectory>${project.build.directory}/unpacked/keycloak-wildfly-adapter-${project.version}</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
@@ -116,9 +116,9 @@
<artifactItems>
<artifactItem>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-saml-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-saml-wildfly-adapter-dist</artifactId>
<type>zip</type>
- <outputDirectory>${project.build.directory}/unpacked/keycloak-saml-wf9-adapter-${project.version}</outputDirectory>
+ <outputDirectory>${project.build.directory}/unpacked/keycloak-saml-wildfly-adapter-${project.version}</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl
index 106c08f..9f52547 100755
--- a/distribution/demo-dist/src/main/xslt/standalone.xsl
+++ b/distribution/demo-dist/src/main/xslt/standalone.xsl
@@ -44,7 +44,7 @@
<web-context>auth</web-context>
</subsystem>
<subsystem xmlns="urn:jboss:domain:keycloak:1.1"/>
- <subsystem xmlns="urn:jboss:domain:keycloak-saml:1.6"/>
+ <subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1"/>
</xsl:copy>
</xsl:template>
distribution/downloads/pom.xml 8(+4 -4)
diff --git a/distribution/downloads/pom.xml b/distribution/downloads/pom.xml
index b9daadf..b742897 100755
--- a/distribution/downloads/pom.xml
+++ b/distribution/downloads/pom.xml
@@ -239,12 +239,12 @@
<artifactItem>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<type>zip</type>
</artifactItem>
<artifactItem>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<type>tar.gz</type>
</artifactItem>
</artifactItems>
@@ -338,12 +338,12 @@
<artifactItem>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-saml-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-saml-wildfly-adapter-dist</artifactId>
<type>zip</type>
</artifactItem>
<artifactItem>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-saml-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-saml-wildfly-adapter-dist</artifactId>
<type>tar.gz</type>
</artifactItem>
</artifactItems>
diff --git a/distribution/feature-packs/adapter-feature-pack/pom.xml b/distribution/feature-packs/adapter-feature-pack/pom.xml
index 6d1a7a0..8fe0163 100755
--- a/distribution/feature-packs/adapter-feature-pack/pom.xml
+++ b/distribution/feature-packs/adapter-feature-pack/pom.xml
@@ -34,7 +34,7 @@
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-subsystem</artifactId>
+ <artifactId>keycloak-wildfly-subsystem</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-adapter-subsystem/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-adapter-subsystem/main/module.xml
index 66278bd..f759d30 100644
--- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-adapter-subsystem/main/module.xml
+++ b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-adapter-subsystem/main/module.xml
@@ -28,6 +28,6 @@
</resources>
<dependencies>
- <module name="org.keycloak.keycloak-wf9-subsystem" export="true" services="export"/>
+ <module name="org.keycloak.keycloak-wildfly-subsystem" export="true" services="export"/>
</dependencies>
</module>
diff --git a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-wf9-subsystem/main/module.xml b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-wf9-subsystem/main/module.xml
index 4b2efe5..732e674 100755
--- a/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-wf9-subsystem/main/module.xml
+++ b/distribution/feature-packs/adapter-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-wf9-subsystem/main/module.xml
@@ -22,10 +22,10 @@
~ 02110-1301 USA, or see the FSF site: http://www.fsf.org.
-->
-<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-wf9-subsystem">
+<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-wildfly-subsystem">
<resources>
- <artifact name="${org.keycloak:keycloak-wf9-subsystem}"/>
+ <artifact name="${org.keycloak:keycloak-wildfly-subsystem}"/>
</resources>
<dependencies>
diff --git a/distribution/feature-packs/server-feature-pack/pom.xml b/distribution/feature-packs/server-feature-pack/pom.xml
index 0f5dd37..6f05056 100644
--- a/distribution/feature-packs/server-feature-pack/pom.xml
+++ b/distribution/feature-packs/server-feature-pack/pom.xml
@@ -38,17 +38,25 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-wildfly-adduser</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-wildfly-extensions</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-server-subsystem</artifactId>
+ <artifactId>keycloak-wildfly-server-subsystem</artifactId>
</dependency>
<dependency>
<groupId>org.wildfly</groupId>
<artifactId>wildfly-feature-pack</artifactId>
<type>zip</type>
</dependency>
+ <dependency>
+ <groupId>org.jboss.aesh</groupId>
+ <artifactId>aesh</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/add-user.bat b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/add-user.bat
new file mode 100644
index 0000000..dca1136
--- /dev/null
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/add-user.bat
@@ -0,0 +1,73 @@
+@echo off
+rem -------------------------------------------------------------------------
+rem Add User script for Windows
+rem -------------------------------------------------------------------------
+rem
+rem A simple utility for adding new users to the properties file used
+rem for domain management authentication out of the box.
+
+rem $Id$
+
+@if not "%ECHO%" == "" echo %ECHO%
+@if "%OS%" == "Windows_NT" setlocal
+
+if "%OS%" == "Windows_NT" (
+ set "DIRNAME=%~dp0%"
+) else (
+ set DIRNAME=.\
+)
+
+pushd "%DIRNAME%.."
+set "RESOLVED_JBOSS_HOME=%CD%"
+popd
+
+if "x%JBOSS_HOME%" == "x" (
+ set "JBOSS_HOME=%RESOLVED_JBOSS_HOME%"
+)
+
+pushd "%JBOSS_HOME%"
+set "SANITIZED_JBOSS_HOME=%CD%"
+popd
+
+if /i "%RESOLVED_JBOSS_HOME%" NEQ "%SANITIZED_JBOSS_HOME%" (
+ echo.
+ echo WARNING: The JBOSS_HOME ^("%SANITIZED_JBOSS_HOME%"^) that this script uses points to a different installation than the one that this script resides in ^("%RESOLVED_JBOSS_HOME%"^). Unpredictable results may occur.
+ echo.
+ echo JBOSS_HOME: "%JBOSS_HOME%"
+ echo.
+)
+
+rem Setup JBoss specific properties
+if "x%JAVA_HOME%" == "x" (
+ set JAVA=java
+ echo JAVA_HOME is not set. Unexpected results may occur.
+ echo Set JAVA_HOME to the directory of your local JDK to avoid this message.
+) else (
+ set "JAVA=%JAVA_HOME%\bin\java"
+)
+
+rem Find jboss-modules.jar, or we can't continue
+if exist "%JBOSS_HOME%\jboss-modules.jar" (
+ set "RUNJAR=%JBOSS_HOME%\jboss-modules.jar"
+) else (
+ echo Could not locate "%JBOSS_HOME%\jboss-modules.jar".
+ echo Please check that you are in the bin directory when running this script.
+ goto END
+)
+
+rem Set default module root paths
+if "x%JBOSS_MODULEPATH%" == "x" (
+ set "JBOSS_MODULEPATH=%JBOSS_HOME%\modules"
+)
+
+rem Uncomment to override standalone and domain user location
+rem set "JAVA_OPTS=%JAVA_OPTS% -Djboss.server.config.user.dir=..\standalone\configuration -Djboss.domain.config.user.dir=..\domain\configuration"
+
+"%JAVA%" %JAVA_OPTS% ^
+ -jar "%JBOSS_HOME%\jboss-modules.jar" ^
+ -mp "%JBOSS_MODULEPATH%" ^
+ org.keycloak.keycloak-wildfly-adduser ^
+ %*
+
+:END
+if "x%NOPAUSE%" == "x" pause
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/add-user.sh b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/add-user.sh
new file mode 100755
index 0000000..1f6dfff
--- /dev/null
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/add-user.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+
+# Add User Utility
+#
+# A simple utility for adding new users to the properties file used
+# for domain management authentication out of the box.
+#
+
+DIRNAME=`dirname "$0"`
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false;
+if [ `uname|grep -i CYGWIN` ]; then
+ cygwin=true;
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JBOSS_HOME" ] &&
+ JBOSS_HOME=`cygpath --unix "$JBOSS_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$JAVAC_JAR" ] &&
+ JAVAC_JAR=`cygpath --unix "$JAVAC_JAR"`
+fi
+
+# Setup JBOSS_HOME
+RESOLVED_JBOSS_HOME=`cd "$DIRNAME/.."; pwd`
+if [ "x$JBOSS_HOME" = "x" ]; then
+ # get the full path (without any relative bits)
+ JBOSS_HOME=$RESOLVED_JBOSS_HOME
+else
+ SANITIZED_JBOSS_HOME=`cd "$JBOSS_HOME"; pwd`
+ if [ "$RESOLVED_JBOSS_HOME" != "$SANITIZED_JBOSS_HOME" ]; then
+ echo "WARNING: The JBOSS_HOME ($SANITIZED_JBOSS_HOME) that this script uses points to a different installation than the one that this script resides in ($RESOLVED_JBOSS_HOME). Unpredictable results may occur."
+ echo ""
+ fi
+fi
+export JBOSS_HOME
+
+# Setup the JVM
+if [ "x$JAVA" = "x" ]; then
+ if [ "x$JAVA_HOME" != "x" ]; then
+ JAVA="$JAVA_HOME/bin/java"
+ else
+ JAVA="java"
+ fi
+fi
+
+if [ "x$JBOSS_MODULEPATH" = "x" ]; then
+ JBOSS_MODULEPATH="$JBOSS_HOME/modules"
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ JBOSS_HOME=`cygpath --path --windows "$JBOSS_HOME"`
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ JBOSS_MODULEPATH=`cygpath --path --windows "$JBOSS_MODULEPATH"`
+fi
+
+# Sample JPDA settings for remote socket debugging
+#JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,address=8787,server=y,suspend=y"
+# Uncomment to override standalone and domain user location
+#JAVA_OPTS="$JAVA_OPTS -Djboss.server.config.user.dir=../standalone/configuration -Djboss.domain.config.user.dir=../domain/configuration"
+
+JAVA_OPTS="$JAVA_OPTS"
+
+eval \"$JAVA\" $JAVA_OPTS \
+ -jar \""$JBOSS_HOME"/jboss-modules.jar\" \
+ -mp \""${JBOSS_MODULEPATH}"\" \
+ org.keycloak.keycloak-wildfly-adduser \
+ '"$@"'
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/jboss/aesh/0.65/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/jboss/aesh/0.65/module.xml
new file mode 100644
index 0000000..4166dbd
--- /dev/null
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/jboss/aesh/0.65/module.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ ~ JBoss, Home of Professional Open Source.
+ ~ Copyright 2010, Red Hat, Inc., and individual contributors
+ ~ as indicated by the @author tags. See the copyright.txt file in the
+ ~ distribution for a full listing of individual contributors.
+ ~
+ ~ This is free software; you can redistribute it and/or modify it
+ ~ under the terms of the GNU Lesser General Public License as
+ ~ published by the Free Software Foundation; either version 2.1 of
+ ~ the License, or (at your option) any later version.
+ ~
+ ~ This software is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ~ Lesser General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU Lesser General Public
+ ~ License along with this software; if not, write to the Free
+ ~ Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ~ 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ -->
+
+<module xmlns="urn:jboss:module:1.3" name="org.jboss.aesh" slot="0.65">
+ <properties>
+ <property name="jboss.api" value="private"/>
+ </properties>
+
+ <resources>
+ <artifact name="${org.jboss.aesh:aesh}"/>
+ </resources>
+
+ <dependencies>
+ <module name="org.fusesource.jansi" />
+ </dependencies>
+</module>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/module.xml
index d6e7d81..8002aa2 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/module.xml
@@ -29,6 +29,6 @@
</resources>
<dependencies>
- <module name="org.keycloak.keycloak-wf9-server-subsystem" services="export" export="true"/>
+ <module name="org.keycloak.keycloak-wildfly-server-subsystem" services="export" export="true"/>
</dependencies>
</module>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-wildfly-adduser/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-wildfly-adduser/main/module.xml
new file mode 100755
index 0000000..d27499b
--- /dev/null
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-wildfly-adduser/main/module.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-wildfly-adduser">
+ <main-class name="org.keycloak.wildfly.adduser.AddUser"/>
+ <resources>
+ <artifact name="${org.keycloak:keycloak-wildfly-adduser}"/>
+ </resources>
+ <dependencies>
+ <module name="org.keycloak.keycloak-common"/>
+ <module name="org.keycloak.keycloak-core"/>
+ <module name="org.keycloak.keycloak-model-api"/>
+ <module name="org.jboss.aesh" slot="0.65"/>
+ <module name="org.jboss.as.domain-management"/>
+ <module name="org.codehaus.jackson.jackson-core-asl"/>
+ </dependencies>
+</module>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml b/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml
index 1acb6aa..78c4823 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/as7-adapter-zip/assembly.xml
@@ -29,7 +29,7 @@
</fileSets>
<files>
<file>
- <source>../../shared-cli/adapter-install.cli</source>
+ <source>../../shared-cli/adapter-install-saml.cli</source>
<outputDirectory>bin</outputDirectory>
</file>
</files>
diff --git a/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml b/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml
index f844a41..1ed0089 100755
--- a/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml
+++ b/distribution/saml-adapters/as7-eap6-adapter/eap6-adapter-zip/assembly.xml
@@ -29,7 +29,7 @@
</fileSets>
<files>
<file>
- <source>../../shared-cli/adapter-install.cli</source>
+ <source>../../shared-cli/adapter-install-saml.cli</source>
<outputDirectory>bin</outputDirectory>
</file>
</files>
distribution/saml-adapters/pom.xml 2(+1 -1)
diff --git a/distribution/saml-adapters/pom.xml b/distribution/saml-adapters/pom.xml
index 456f292..ea95663 100755
--- a/distribution/saml-adapters/pom.xml
+++ b/distribution/saml-adapters/pom.xml
@@ -15,7 +15,7 @@
<packaging>pom</packaging>
<modules>
- <module>wf9-adapter</module>
+ <module>wildfly-adapter</module>
<module>tomcat6-adapter-zip</module>
<module>tomcat7-adapter-zip</module>
<module>tomcat8-adapter-zip</module>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/build.xml b/distribution/server-overlay/eap6/eap6-server-modules/build.xml
index 9f60836..e8086dc 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/build.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/build.xml
@@ -274,8 +274,8 @@
<!-- subsystems -->
- <module-def name="org.keycloak.keycloak-as7-server-subsystem">
- <maven-resource group="org.keycloak" artifact="keycloak-as7-server-subsystem"/>
+ <module-def name="org.keycloak.keycloak-eap6-server-subsystem">
+ <maven-resource group="org.keycloak" artifact="keycloak-eap6-server-subsystem"/>
</module-def>
<module-def name="org.keycloak.keycloak-server-subsystem"/>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/pom.xml b/distribution/server-overlay/eap6/eap6-server-modules/pom.xml
index 91b2969..b3225fd 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/pom.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/pom.xml
@@ -32,7 +32,7 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-as7-server-subsystem</artifactId>
+ <artifactId>keycloak-eap6-server-subsystem</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-server-subsystem/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-server-subsystem/main/module.xml
index 90939b0..4829258 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-server-subsystem/main/module.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-server-subsystem/main/module.xml
@@ -30,6 +30,6 @@
</resources>
<dependencies>
- <module name="org.keycloak.keycloak-as7-server-subsystem" services="export" export="true"/>
+ <module name="org.keycloak.keycloak-eap6-server-subsystem" services="export" export="true"/>
</dependencies>
</module>
diff --git a/docbook/auth-server-docs/reference/en/en-US/master.xml b/docbook/auth-server-docs/reference/en/en-US/master.xml
index 2af744f..5078f10 100755
--- a/docbook/auth-server-docs/reference/en/en-US/master.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/master.xml
@@ -20,6 +20,7 @@
<!ENTITY SpringSecurityAdapter SYSTEM "modules/spring-security-adapter.xml">
<!ENTITY InstalledApplications SYSTEM "modules/installed-applications.xml">
<!ENTITY Logout SYSTEM "modules/logout.xml">
+ <!ENTITY ErrorHandling SYSTEM "modules/adapter_error_handling.xml">
<!ENTITY SAML SYSTEM "modules/saml.xml">
<!ENTITY JAAS SYSTEM "modules/jaas.xml">
<!ENTITY IdentityBroker SYSTEM "modules/identity-broker.xml">
@@ -27,6 +28,7 @@
<!ENTITY Migration SYSTEM "modules/MigrationFromOlderVersions.xml">
<!ENTITY Email SYSTEM "modules/email.xml">
<!ENTITY Roles SYSTEM "modules/roles.xml">
+ <!ENTITY Groups SYSTEM "modules/groups.xml">
<!ENTITY DirectAccess SYSTEM "modules/direct-access.xml">
<!ENTITY ServiceAccounts SYSTEM "modules/service-accounts.xml">
<!ENTITY CORS SYSTEM "modules/cors.xml">
@@ -36,7 +38,6 @@
<!ENTITY UserFederation SYSTEM "modules/user-federation.xml">
<!ENTITY Kerberos SYSTEM "modules/kerberos.xml">
<!ENTITY ExportImport SYSTEM "modules/export-import.xml">
- <!ENTITY AdminRecovery SYSTEM "modules/admin-recovery.xml">
<!ENTITY ServerCache SYSTEM "modules/cache.xml">
<!ENTITY SecurityVulnerabilities SYSTEM "modules/security-vulnerabilities.xml">
<!ENTITY Clustering SYSTEM "modules/clustering.xml">
@@ -48,6 +49,7 @@
<!ENTITY Recaptcha SYSTEM "modules/recaptcha.xml">
<!ENTITY AuthSPI SYSTEM "modules/auth-spi.xml">
<!ENTITY FilterAdapter SYSTEM "modules/servlet-filter-adapter.xml">
+ <!ENTITY ClientRegistration SYSTEM "modules/client-registration.xml">
]>
<book>
@@ -113,9 +115,11 @@ This one is short
&SpringSecurityAdapter;
&InstalledApplications;
&Logout;
+ &ErrorHandling;
&MultiTenancy;
&JAAS;
</chapter>
+ &ClientRegistration;
&IdentityBroker;
&Themes;
@@ -131,6 +135,7 @@ This one is short
</chapter>
&AccessTypes;
&Roles;
+ &Groups;
&DirectAccess;
&ServiceAccounts;
&CORS;
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/adapter_error_handling.xml b/docbook/auth-server-docs/reference/en/en-US/modules/adapter_error_handling.xml
new file mode 100755
index 0000000..38c0bb1
--- /dev/null
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/adapter_error_handling.xml
@@ -0,0 +1,58 @@
+<section id="adapter_error_handling">
+ <title>Error Handling</title>
+ <para>
+ Keycloak has some error handling facilities for servlet based client adapters. When an error is encountered in
+ authentication, keycloak will call <literal>HttpServletResponse.sendError()</literal>. You can set up an error-page
+ within your <literal>web.xml</literal> file to handle the error however you want. Keycloak may throw
+ 400, 401, 403, and 500 errors.
+ </para>
+ <para>
+<programlisting>
+<![CDATA[
+<error-page>
+ <error-code>404</error-code>
+ <location>/ErrorHandler</location>
+</error-page>]]>
+</programlisting>
+ </para>
+ <para>
+ Keycloak also sets an <literal>HttpServletRequest</literal> attribute that you can retrieve. The attribute name
+ is <literal>org.keycloak.adapters.spi.AuthenticationError</literal>. Typecast this object to:
+ <literal>org.keycloak.adapters.OIDCAuthenticationError</literal>. This class can tell you exactly what happened.
+ If this attribute is not set, then the adapter was not responsible for the error code.
+ </para>
+ <para>
+<programlisting>
+public class OIDCAuthenticationError implements AuthenticationError {
+ public static enum Reason {
+ NO_BEARER_TOKEN,
+ NO_REDIRECT_URI,
+ INVALID_STATE_COOKIE,
+ OAUTH_ERROR,
+ SSL_REQUIRED,
+ CODE_TO_TOKEN_FAILURE,
+ INVALID_TOKEN,
+ STALE_TOKEN,
+ NO_AUTHORIZATION_HEADER
+ }
+
+ private Reason reason;
+ private String description;
+
+ public OIDCAuthenticationError(Reason reason, String description) {
+ this.reason = reason;
+ this.description = description;
+ }
+
+ public Reason getReason() {
+ return reason;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
+
+</programlisting>
+ </para>
+</section>
\ No newline at end of file
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/admin-permissions.xml b/docbook/auth-server-docs/reference/en/en-US/modules/admin-permissions.xml
index 833d390..678c6d1 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/admin-permissions.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/admin-permissions.xml
@@ -64,6 +64,9 @@
<literal>manage-applications</literal> - Create, modify and delete applications in the realm
</listitem>
<listitem>
+ <literal>create-clients</literal> - Create clients in the realm
+ </listitem>
+ <listitem>
<literal>manage-clients</literal> - Create, modify and delete clients in the realm
</listitem>
<listitem>
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml b/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml
new file mode 100755
index 0000000..fab7119
--- /dev/null
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/client-registration.xml
@@ -0,0 +1,215 @@
+<chapter id="client-registration">
+ <title>Client Registration</title>
+
+ <para>
+ In order for an application or service to utilize Keycloak it has to register a client in Keycloak. An
+ admin can do this through the admin console (or admin REST endpoints), but clients can also register themselves
+ through Keycloak's client registration service.
+ </para>
+
+ <para>
+ The Client Registration Service provides built-in support for Keycloak Client Representations, OpenID Connect
+ Client Meta Data and SAML Entity Descriptors. It's also possible to plugin custom client registration providers
+ if required. The Client Registration Service endpoint is <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider></literal>.
+ </para>
+ <para>
+ The built-in supported <literal>providers</literal> are:
+ <itemizedlist>
+ <listitem><literal>default</literal> Keycloak Representations</listitem>
+ <listitem><literal>install</literal> Keycloak Adapter Configuration</listitem>
+ <listitem><literal>openid-connect</literal> OpenID Connect Dynamic Client Registration</listitem>
+ <listitem><literal>saml2-entity-descriptor</literal> SAML Entity Descriptors</listitem>
+ </itemizedlist>
+ The following sections will describe how to use the different providers.
+ </para>
+
+ <section>
+ <title>Authentication</title>
+ <para>
+ To invoke the Client Registration Services you need a token. The token can be a standard bearer token, a
+ initial access token or a registration access token.
+ </para>
+
+ <section>
+ <title>Bearer Token</title>
+ <para>
+ The bearertoken can be issued on behalf of a user or a Service Account. The following permissions are required
+ to invoke the endpoints (see <link linkend='admin-permissions'>Admin Permissions</link> for more details):
+ <itemizedlist>
+ <listitem>
+ <literal>create-client</literal> or <literal>manage-client</literal> - To create clients
+ </listitem>
+ <listitem>
+ <literal>view-client</literal> or <literal>manage-client</literal> - To view clients
+ </listitem>
+ <listitem>
+ <literal>manage-client</literal> - To update or delete clients
+ </listitem>
+ </itemizedlist>
+ If you are using a regular bearer token to create clients we recommend using a token from on behalf of a
+ Service Account with only the <literal>create-client</literal> role. See the
+ <link linkend="service-accounts">Service Account</link> section for more details.
+ </para>
+ </section>
+
+ <section>
+ <title>Initial Access Token</title>
+ <para>
+ The best approach to create new clients is by using initial access tokens. An initial access token can
+ only be used to create clients and has a configurable expiration as well as a configurable limit on
+ how many clients can be created.
+ </para>
+ <para>
+ An initial access token can be created through the admin console. To create a new initial access token
+ first select the realm in the admin console, then click on <literal>Realm Settings</literal> in the menu
+ on the left, followed by <literal>Initial Access Tokens</literal> in the tabs displayed in the page.
+ </para>
+ <para>
+ You will now be able to see any existing initial access tokens. If you have access you can delete tokens
+ that are no longer required. You can only retrieve the value of the token when you are creating it. To
+ create a new token click on <literal>Create</literal>. You can now optionally add how long the token
+ should be valid, also how many clients can be created using the token. After you click on <literal>Save</literal>
+ the token value is displayed. It is important that you copy/paste this token now as you won't be able
+ to retrieve it later. If you forget to copy/paste it, then delete the token and create another one.
+ The token value is used as a standard bearer token when invoking the Client Registration Services, by
+ adding it to the Authorization header in the request. For example:
+<programlisting><![CDATA[
+Authorization: bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJmMjJmNzQyYy04ZjNlLTQ2M....
+]]></programlisting>
+ </para>
+ </section>
+
+ <section>
+ <title>Registration Access Token</title>
+ <para>
+ When you create a client through the Client Registration Service the response will include a registration
+ access token. The registration access token provides access to retrieve the client configuration later, but
+ also to update or delete the client. The registration access token is included with the request in the
+ same way as a bearer token or initial access token. Registration access tokens are only valid once
+ when it's used the response will include a new token.
+ </para>
+ <para>
+ If a client was created outside of the Client Registration Service it won't have a registration access
+ token associated with it. You can create one through the admin console. This can also be useful if
+ you loose the token for a particular client. To create a new token find the client in the admin console
+ and click on <literal>Credentials</literal>. Then click on <literal>Generate registration access token</literal>.
+ </para>
+ </section>
+ </section>
+
+ <section>
+ <title>Keycloak Representations</title>
+ <para>
+ The <literal>default</literal> client registration provider can be used to create, retrieve, update and delete a client. It uses
+ Keycloaks Client Representation format which provides support for configuring clients exactly as they can
+ be configured through the admin console, including for example configuring protocol mappers.
+ </para>
+ <para>
+ To create a client create a Client Representation (JSON) then do a HTTP POST to:
+ <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider>/default</literal>. It will return a Client Representation
+ that also includes the registration access token. You should save the registration access token somewhere
+ if you want to retrieve the config, update or delete the client later.
+ </para>
+ <para>
+ To retrieve the Client Representation then do a HTTP GET to:
+ <literal><KEYCLOAK URL>/realms/<realm>clients/<provider>/default/<client id></literal>. It will also
+ return a new registration access token.
+ </para>
+ <para>
+ To update the Client Representation then do a HTTP PUT to with the updated Client Representation to:
+ <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider>/default/<client id></literal>. It will also
+ return a new registration access token.
+ </para>
+ <para>
+ To delete the Client Representation then do a HTTP DELETE to:
+ <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider>/default/<client id></literal>
+ </para>
+ </section>
+
+ <section>
+ <title>Keycloak Adapter Configuration</title>
+ <para>
+ The <literal>installation</literal> client registration provider can be used to retrieve the adapter configuration
+ for a client. In addition to token authentication you can also authenticate with client credentials using
+ HTTP basic authentication. To do this include the following header in the request:
+<programlisting><![CDATA[
+Authorization: basic BASE64(client-id + ':' + client-secret)
+]]></programlisting>
+ </para>
+ <para>
+ To retrieve the Adapter Configuration then do a HTTP GET to:
+ <literal><KEYCLOAK URL>//realms/<realm>clients/<provider>/installation/<client id></literal>
+ </para>
+ <para>
+ No authentication is required for public clients. This means that for the JavaScript adapter you can
+ load the client configuration directly from Keycloak using the above URL.
+ </para>
+ </section>
+
+ <section>
+ <title>OpenID Connect Dynamic Client Registration</title>
+ <para>
+ Keycloak implements <ulink url="https://openid.net/specs/openid-connect-registration-1_0.html">OpenID Connect Dynamic Client Registration</ulink>,
+ which extends <ulink url="https://tools.ietf.org/html/rfc7591">OAuth 2.0 Dynamic Client Registration Protocol</ulink> and
+ <ulink url="https://tools.ietf.org/html/rfc7592">OAuth 2.0 Dynamic Client Registration Management Protocol</ulink>.
+ </para>
+ <para>
+ The endpoint to use these specifications to register clients in Keycloak is:
+ <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider>/oidc[/<client id>]</literal>.
+ </para>
+ <para>
+ This endpoints can also be found in the OpenID Connect Discovery endpoint for the realm:
+ <literal><KEYCLOAK URL>/realms/<realm>/.well-known/openid-configuration</literal>.
+ </para>
+ </section>
+
+ <section>
+ <title>SAML Entity Descriptors</title>
+ <para>
+ The SAML Entity Descriptor endpoint only supports using SAML v2 Entity Descriptors to create clients. It
+ doesn't support retrieving, updating or deleting clients. For those operations the Keycloak representation
+ endpoints should be used. When creating a client a Keycloak Client Representation is returned with details
+ about the created client, including a registration access token.
+ </para>
+ <para>
+ To create a client do a HTTP POST with the SAML Entity Descriptor to:
+ <literal><KEYCLOAK URL>/realms/<realm>/clients/<provider>/saml2-entity-descriptor</literal>.
+ </para>
+ </section>
+
+ <section>
+ <title>Client Registration Java API</title>
+ <para>
+ The Client Registration Java API makes it easy to use the Client Registration Service using Java. To use
+ include the dependency <literal>org.keycloak:keycloak-client-registration-api:>VERSION<</literal> from
+ Maven.
+ </para>
+ <para>
+ For full instructions on using the Client Registration refer to the JavaDocs. Below is an example of creating
+ a client:
+<programlisting><![CDATA[
+String initialAccessToken = "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJmMjJmNzQyYy04ZjNlLTQ2M....";
+
+ClientRepresentation client = new ClientRepresentation();
+client.setClientId(CLIENT_ID);
+
+ClientRegistration reg = ClientRegistration.create().url("http://keycloak/auth/realms/myrealm").build();
+reg.auth(initialAccessToken);
+
+client = reg.create(client);
+
+String registrationAccessToken = client.getRegistrationAccessToken();
+]]></programlisting>
+ </para>
+ </section>
+
+ <!--
+ <section>
+ <title>Client Registration CLI</title>
+ <para>
+ TODO
+ </para>
+ </section>
+ -->
+
+</chapter>
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/groups.xml b/docbook/auth-server-docs/reference/en/en-US/modules/groups.xml
new file mode 100755
index 0000000..eb54e0e
--- /dev/null
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/groups.xml
@@ -0,0 +1,31 @@
+<chapter id="groups">
+ <title>Groups</title>
+ <para>
+ Groups in Keycloak allow you to manage a common set of attributes and role mappings for a large set of users.
+ Users can be members of zero or more groups. Users inherit the attributes and role mappings assign to each group.
+ As an admin this makes it easy for you to manage permissions for a user in one place.
+ </para>
+ <para>
+ Groups are hierarchical. A group can have many subgroups, but a group can only have one parent. Subgroups inherit
+ the attributes and role mappings from the parent. This applies to user as well. So, if you have a parent group and a child group
+ and a user that only belongs to the child group, the user inherits the attributes and role mappings of both the
+ parent and child.
+ </para>
+ <section>
+ <title>Groups vs. Roles</title>
+ <para>
+ In the IT world the concepts of Group and Role are often blurred and interchangeable. In Keycloak, Groups are just
+ a collection of users that you can apply roles and attributes to in one place. Roles are used to assign permissions
+ and access control.
+ </para>
+ <para>
+ Keycloak Roles have the concept of a Composite Role. A role can be associated with one or more additional roles.
+ This is called a Composite Role. If a user has a role mapping to the Composite Role, they inherit all the roles associated
+ with the composite. So what's the difference from a Keycloak Group and a Composite Role? Logically they could be
+ used for the same exact thing. The difference is conceptual. Composite roles should be used to compose the
+ permission model of your set of services and applications. So, roles become a set of permissions. Groups on the
+ other hand, would be a set of users that have a set of permissions. Use Groups to manage users, composite roles to
+ manage applications and services.
+ </para>
+ </section>
+</chapter>
\ No newline at end of file
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/jboss-adapter.xml b/docbook/auth-server-docs/reference/en/en-US/modules/jboss-adapter.xml
index 71e599d..7524f1d 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/jboss-adapter.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/jboss-adapter.xml
@@ -14,10 +14,10 @@
the Keycloak download site. They are also available as a maven artifact.
</para>
<para>
- Install on Wildfly 9:
+ Install on Wildfly 9 or 10:
<programlisting>
$ cd $WILDFLY_HOME
-$ unzip keycloak-wf9-adapter-dist.zip
+$ unzip keycloak-wildfly-adapter-dist.zip
</programlisting>
</para>
<para>
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml b/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml
index b0a443d..571cd6c 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml
@@ -84,6 +84,10 @@
<simplesect>
<title>Option 'Update Profile On First Login' moved from Identity provider to Review Profile authenticator</title>
<para>
+ form-error-page in web.xml will no longer work for client adapter authentication errors. You must define an error-page for
+ the the various HTTP error codes. See documentation for more details if you want to catch and handle adapter error conditions.
+ </para>
+ <para>
In this version, we added <literal>First Broker Login</literal>, which allows you to specify what exactly should be done
when new user is logged through Identity provider (or Social provider), but there is no existing Keycloak user
yet linked to the social account. As part of this work, we added option <literal>First Login Flow</literal> to identity providers where
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/roles.xml b/docbook/auth-server-docs/reference/en/en-US/modules/roles.xml
index 9cd6176..b7c034c 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/roles.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/roles.xml
@@ -1,7 +1,7 @@
<chapter id="roles">
<title>Roles</title>
<para>
- In Keycloak, roles (or permissions) can be defined globally at the realm level, or individually per application.
+ In Keycloak, roles can be defined globally at the realm level, or individually per application.
Each role has a name which must be unique at the level it is defined in, i.e. you can have only one "admin" role at
the realm level. You may have that a role named "admin" within an Application too, but "admin" must be unique
for that application.
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/saml.xml b/docbook/auth-server-docs/reference/en/en-US/modules/saml.xml
index de289b8..49416a0 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/saml.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/saml.xml
@@ -15,6 +15,15 @@
<para>
<variablelist>
<varlistentry>
+ <term>Client ID</term>
+ <listitem>
+ <para>
+ This value must match the issuer value sent with AuthNRequests. Keycloak will pull the issuer
+ from the Authn SAML request and match it to a client by this value.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
<term>Include AuthnStatement</term>
<listitem>
<para>
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml
index 78d9a4b..01ad7e6 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml
@@ -128,6 +128,25 @@ cd <WILDFLY_HOME>/bin
</itemizedlist>
</para>
<section>
+ <title>Admin User</title>
+ <para>
+ To access the admin console you need an account to login. Currently, there's a default account added
+ with the username <literal>admin</literal> and password <literal>admin</literal>. You will be required
+ to change the password on first login. We are planning on removing the built-in account soon and will
+ instead have an initial step to create the user.
+ </para>
+ <para>
+ You can also create a user with the <literal>add-user</literal> script found in <literal>bin</literal>.
+ This script will create a temporary file with the details of the user, which are imported at startup.
+ To add a user with this script run:
+<programlisting><![CDATA[
+bin/add-user.[sh|bat] -r master -u <username> -p <password>
+]]></programlisting>
+ Then restart the server.
+ </para>
+ </section>
+
+ <section>
<title>Relational Database Configuration</title>
<para>
You might want to use a better relational database for Keycloak like PostgreSQL or MySQL. You might also
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/timeouts.xml b/docbook/auth-server-docs/reference/en/en-US/modules/timeouts.xml
index 7f8800e..229ee8d 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/timeouts.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/timeouts.xml
@@ -43,6 +43,12 @@
application not knowing if the user's permissions have changed. This value is usually in minutes.
</para>
<para>
+ The <code class="literal">Access Token Lifespan For Implicit Flow</code> is how long an access token is valid for when using OpenID Connect implicit flow.
+ With implicit flow, there is no refresh token available, so that's why the lifespan is usually bigger than default Access Token Lifespan mentioned above.
+ See <ulink url="http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth">OpenID Connect specification</ulink> for details about implicit flow and
+ <link linkend="javascript-adapter">Javascript Adapter</link> for some additional details.
+ </para>
+ <para>
The <literal>Client login timeout</literal> is how long an access code is valid for. An access code is obtained
on the 1st leg of the OAuth 2.0 redirection protocol. This should be a short time limit. Usually seconds.
</para>
diff --git a/docbook/saml-adapter-docs/reference/en/en-US/master.xml b/docbook/saml-adapter-docs/reference/en/en-US/master.xml
index 5b798a1..7f14165 100755
--- a/docbook/saml-adapter-docs/reference/en/en-US/master.xml
+++ b/docbook/saml-adapter-docs/reference/en/en-US/master.xml
@@ -8,7 +8,9 @@
<!ENTITY Jetty9Adapter SYSTEM "modules/jetty9-adapter.xml">
<!ENTITY Jetty8Adapter SYSTEM "modules/jetty8-adapter.xml">
<!ENTITY FilterAdapter SYSTEM "modules/servlet-filter-adapter.xml">
+ <!ENTITY Assertions SYSTEM "modules/assertion-api.xml">
<!ENTITY Logout SYSTEM "modules/logout.xml">
+ <!ENTITY ErrorHandling SYSTEM "modules/adapter_error_handling.xml">
]>
<book>
@@ -49,6 +51,8 @@ This one is short
&Jetty8Adapter;
&FilterAdapter;
&Logout;
+ &Assertions;
+ &ErrorHandling;
diff --git a/docbook/saml-adapter-docs/reference/en/en-US/modules/adapter_error_handling.xml b/docbook/saml-adapter-docs/reference/en/en-US/modules/adapter_error_handling.xml
new file mode 100755
index 0000000..1d6d11f
--- /dev/null
+++ b/docbook/saml-adapter-docs/reference/en/en-US/modules/adapter_error_handling.xml
@@ -0,0 +1,42 @@
+<chapter id="adapter_error_handling">
+ <title>Error Handling</title>
+ <para>
+ Keycloak has some error handling facilities for servlet based client adapters. When an error is encountered in
+ authentication, keycloak will call <literal>HttpServletResponse.sendError()</literal>. You can set up an error-page
+ within your <literal>web.xml</literal> file to handle the error however you want. Keycloak may throw
+ 400, 401, 403, and 500 errors.
+ </para>
+ <para>
+<programlisting>
+<![CDATA[
+<error-page>
+ <error-code>404</error-code>
+ <location>/ErrorHandler</location>
+</error-page>]]>
+</programlisting>
+ </para>
+ <para>
+ Keycloak also sets an <literal>HttpServletRequest</literal> attribute that you can retrieve. The attribute name
+ is <literal>org.keycloak.adapters.spi.AuthenticationError</literal>. Typecast this object to:
+ <literal>org.keycloak.adapters.saml.SamlAuthenticationError</literal>. This class can tell you exactly what happened.
+ If this attribute is not set, then the adapter was not responsible for the error code.
+ </para>
+ <para>
+<programlisting>
+public class SamlAuthenticationError implements AuthenticationError {
+ public static enum Reason {
+ EXTRACTION_FAILURE,
+ INVALID_SIGNATURE,
+ ERROR_STATUS
+ }
+
+ public Reason getReason() {
+ return reason;
+ }
+ public StatusResponseType getStatus() {
+ return status;
+ }
+}
+</programlisting>
+ </para>
+</chapter>
\ No newline at end of file
diff --git a/docbook/saml-adapter-docs/reference/en/en-US/modules/adapter-config.xml b/docbook/saml-adapter-docs/reference/en/en-US/modules/adapter-config.xml
index cce3faf..5335362 100755
--- a/docbook/saml-adapter-docs/reference/en/en-US/modules/adapter-config.xml
+++ b/docbook/saml-adapter-docs/reference/en/en-US/modules/adapter-config.xml
@@ -11,7 +11,8 @@
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
- forceAuthentication="false">
+ forceAuthentication="false"
+ isPassive="false">
<Keys>
<Key signing="true" >
<KeyStore resource="/WEB-INF/keystore.jks" password="store123">
@@ -63,7 +64,8 @@
<SP entityID="sp"
sslPolicy="ssl"
nameIDPolicyFormat="format"
- forceAuthentication="true">
+ forceAuthentication="true"
+ isPassive="false">
...
</SP>]]></programlisting>
<para>
@@ -106,12 +108,23 @@
<listitem>
<para>
SAML clients can request that a user is re-authenticated even if
- they are already logged in at the IDP. Set this to true if you
+ they are already logged in at the IDP. Set this to <literal>true</literal> if you
want this.
<emphasis>OPTIONAL.</emphasis>. Set to <literal>false</literal> by default.
</para>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term>isPassive</term>
+ <listitem>
+ <para>
+ SAML clients can request that a user is never asked to authenticate even if
+ they are not logged in at the IDP. Set this to <literal>true</literal> if you want this.
+ Do not use together with <literal>forceAuthentication</literal> as they are opposite.
+ <emphasis>OPTIONAL.</emphasis>. Set to <literal>false</literal> by default.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</para>
</section>
diff --git a/docbook/saml-adapter-docs/reference/en/en-US/modules/assertion-api.xml b/docbook/saml-adapter-docs/reference/en/en-US/modules/assertion-api.xml
new file mode 100755
index 0000000..6106c07
--- /dev/null
+++ b/docbook/saml-adapter-docs/reference/en/en-US/modules/assertion-api.xml
@@ -0,0 +1,109 @@
+<chapter id="assertions">
+ <title>Obtaining Assertion Attributes</title>
+ <para>
+ After a successful SAML login, your application code may want to obtain attribute values passed with the SAML assertion.
+ <literal>HttpServletRequest.getUserPrincipal</literal> returns a Principal object that you can typecast into a
+ Keycloak specific class called <literal>org.keycloak.adapters.saml.SamlPrincipal</literal>. This object allows
+ you to look at the raw assertion and also has convenience functions to look up attribute values.
+ </para>
+ <para>
+<programlisting><![CDATA[
+package org.keycloak.adapters.saml;
+
+public class SamlPrincipal implements Serializable, Principal {
+ /**
+ * Get full saml assertion
+ *
+ * @return
+ */
+ public AssertionType getAssertion() {
+ ...
+ }
+
+ /**
+ * Get SAML subject sent in assertion
+ *
+ * @return
+ */
+ public String getSamlSubject() {
+ ...
+ }
+
+ /**
+ * Subject nameID format
+ *
+ * @return
+ */
+ public String getNameIDFormat() {
+ ...
+ }
+
+ @Override
+ public String getName() {
+ ...
+ }
+
+ /**
+ * Convenience function that gets Attribute value by attribute name
+ *
+ * @param name
+ * @return
+ */
+ public List<String> getAttributes(String name) {
+ ...
+
+ }
+
+ /**
+ * Convenience function that gets Attribute value by attribute friendly name
+ *
+ * @param friendlyName
+ * @return
+ */
+ public List<String> getFriendlyAttributes(String friendlyName) {
+ ...
+ }
+
+ /**
+ * Convenience function that gets first value of an attribute by attribute name
+ *
+ * @param name
+ * @return
+ */
+ public String getAttribute(String name) {
+ ...
+ }
+
+ /**
+ * Convenience function that gets first value of an attribute by attribute name
+ *
+ *
+ * @param friendlyName
+ * @return
+ */
+ public String getFriendlyAttribute(String friendlyName) {
+ ...
+ }
+
+ /**
+ * Get set of all assertion attribute names
+ *
+ * @return
+ */
+ public Set<String> getAttributeNames() {
+ ...
+ }
+
+ /**
+ * Get set of all assertion friendly attribute names
+ *
+ * @return
+ */
+ public Set<String> getFriendlyNames() {
+ ...
+ }
+}
+]]>
+</programlisting>
+ </para>
+</chapter>
\ No newline at end of file
diff --git a/docbook/saml-adapter-docs/reference/en/en-US/modules/jboss-adapter.xml b/docbook/saml-adapter-docs/reference/en/en-US/modules/jboss-adapter.xml
index fc29402..675cc0b 100755
--- a/docbook/saml-adapter-docs/reference/en/en-US/modules/jboss-adapter.xml
+++ b/docbook/saml-adapter-docs/reference/en/en-US/modules/jboss-adapter.xml
@@ -3,7 +3,7 @@
<para>
To be able to secure WAR apps deployed on JBoss EAP 6.x or Wildfly, you must install and
configure the Keycloak SAML Adapter Subsystem. You then provide a keycloak
- config, <literal>/WEB-INF/keycloak-saml</literal> file in your WAR and change the auth-method to KEYCLOAK-SAML within web.xml.
+ config, <literal>/WEB-INF/keycloak-saml.xml</literal> file in your WAR and change the auth-method to KEYCLOAK-SAML within web.xml.
Both methods are described in this section.
</para>
<section id="jboss-adapter-installation">
@@ -13,10 +13,10 @@
the Keycloak download site. They are also available as a maven artifact.
</para>
<para>
- Install on Wildfly 9:
+ Install on Wildfly 9 or 10:
<programlisting>
$ cd $WILDFLY_HOME
-$ unzip keycloak-saml-wf9-adapter-dist.zip
+$ unzip keycloak-saml-wildfly-adapter-dist.zip
</programlisting>
</para>
<para>
@@ -38,7 +38,7 @@ $ unzip keycloak-saml-eap6-adapter-dist.zip
from the server's bin directory:
<programlisting>
$ cd $JBOSS_HOME/bin
-$ jboss-cli.sh -c --file=adapter-install.cli
+$ jboss-cli.sh -c --file=adapter-install-saml.cli
</programlisting>
The script will add the extension, subsystem, and optional security-domain as described below.
</para>
@@ -52,7 +52,7 @@ $ jboss-cli.sh -c --file=adapter-install.cli
</extensions>
<profile>
- <subsystem xmlns="urn:jboss:domain:keycloak-saml:1.6"/>
+ <subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1"/>
...
</profile>
]]>
@@ -185,4 +185,92 @@ public class CustomerService {
</programlisting>
</para>
</section>
+ <section>
+ <title>Securing WARs via Keycloak SAML Subsystem</title>
+ <para>
+ You do not have to crack open a WAR to secure it with Keycloak. Alternatively, you can externally secure
+ it via the Keycloak SAML Adapter Subsystem. While you don't have to specify KEYCLOAK-SAML as an <literal>auth-method</literal>,
+ you still have to define the <literal>security-constraints</literal> in <literal>web.xml</literal>. You do
+ not, however, have to create a <literal>WEB-INF/keycloak-saml.xml</literal> file. This metadata is instead defined
+ within XML in your server's <literal>domain.xml</literal> or <literal>standalone.xml</literal> subsystem
+ configuration section.
+ </para>
+ <para>
+ <programlisting><![CDATA[
+<extensions>
+ <extension module="org.keycloak.keycloak-saml-adapter-subsystem"/>
+</extensions>
+
+<profile>
+ <subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1">
+ <secure-deployment name="WAR MODULE NAME.war">
+ <SP entityID="APPLICATION URL">
+ ...
+ </SP>
+ </secure-deployment>
+ </subsystem>
+</profile>
+]]>
+ </programlisting>
+ </para>
+ <para>
+ The <literal>secure-deployment</literal> <literal>name</literal> attribute identifies the WAR you want
+ to secure. Its value is the <literal>module-name</literal> defined in <literal>web.xml</literal> with
+ <literal>.war</literal> appended. The rest of the configuration uses the same XML syntax as
+ <literal>keycloak-saml.xml</literal> configuration defined in <link linkend='adapter-config'>general adapter configuration</link>.
+ </para>
+ <para>
+ An example configuration:
+ </para>
+ <para>
+ <programlisting><![CDATA[
+<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1">
+ <secure-deployment name="saml-post-encryption.war">
+ <SP entityID="http://localhost:8080/sales-post-enc/"
+ sslPolicy="EXTERNAL"
+ nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ logoutPage="/logout.jsp"
+ forceAuthentication="false">
+ <Keys>
+ <Key signing="true" encryption="true">
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
+ <PrivateKey alias="http://localhost:8080/sales-post-enc/" password="test123"/>
+ <Certificate alias="http://localhost:8080/sales-post-enc/"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ <PrincipalNameMapping policy="FROM_NAME_ID"/>
+ <RoleIdentifiers>
+ <Attribute name="Role"/>
+ </RoleIdentifiers>
+ <IDP entityID="idp">
+ <SingleSignOnService signRequest="true"
+ validateResponseSignature="true"
+ requestBinding="POST"
+ bindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"/>
+
+ <SingleLogoutService
+ validateRequestSignature="true"
+ validateResponseSignature="true"
+ signRequest="true"
+ signResponse="true"
+ requestBinding="POST"
+ responseBinding="POST"
+ postBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"
+ redirectBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"/>
+ <Keys>
+ <Key signing="true" >
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
+ <Certificate alias="saml-demo"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ </IDP>
+ </SP>
+ </secure-deployment>
+</subsystem>
+]]>
+ </programlisting>
+ </para>
+ </section>
</chapter>
\ No newline at end of file
diff --git a/docbook/saml-adapter-docs/reference/en/en-US/modules/tomcat-adapter.xml b/docbook/saml-adapter-docs/reference/en/en-US/modules/tomcat-adapter.xml
index f6db1b0..37ffbe1 100755
--- a/docbook/saml-adapter-docs/reference/en/en-US/modules/tomcat-adapter.xml
+++ b/docbook/saml-adapter-docs/reference/en/en-US/modules/tomcat-adapter.xml
@@ -1,5 +1,5 @@
<chapter id="tomcat-adapter">
- <title>Tomcat 6, 7 and 8 SAML dapters</title>
+ <title>Tomcat 6, 7 and 8 SAML adapters</title>
<para>
To be able to secure WAR apps deployed on Tomcat 6, 7 and 8 you must install the Keycloak Tomcat 6, 7 or 8 SAML adapter
into your Tomcat installation. You then have to provide some extra configuration in each WAR you deploy to
diff --git a/events/api/src/main/java/org/keycloak/events/Details.java b/events/api/src/main/java/org/keycloak/events/Details.java
index b9c5338..a995d7b 100755
--- a/events/api/src/main/java/org/keycloak/events/Details.java
+++ b/events/api/src/main/java/org/keycloak/events/Details.java
@@ -11,6 +11,7 @@ public interface Details {
String CODE_ID = "code_id";
String REDIRECT_URI = "redirect_uri";
String RESPONSE_TYPE = "response_type";
+ String RESPONSE_MODE = "response_mode";
String AUTH_TYPE = "auth_type";
String AUTH_METHOD = "auth_method";
String IDENTITY_PROVIDER = "identity_provider";
diff --git a/events/api/src/main/java/org/keycloak/events/Event.java b/events/api/src/main/java/org/keycloak/events/Event.java
index d4fd536..6382421 100644
--- a/events/api/src/main/java/org/keycloak/events/Event.java
+++ b/events/api/src/main/java/org/keycloak/events/Event.java
@@ -47,7 +47,7 @@ public class Event {
}
public void setRealmId(String realmId) {
- this.realmId = realmId;
+ this.realmId = maxLength(realmId, 255);
}
public String getClientId() {
@@ -55,7 +55,7 @@ public class Event {
}
public void setClientId(String clientId) {
- this.clientId = clientId;
+ this.clientId = maxLength(clientId, 255);
}
public String getUserId() {
@@ -63,7 +63,7 @@ public class Event {
}
public void setUserId(String userId) {
- this.userId = userId;
+ this.userId = maxLength(userId, 255);
}
public String getSessionId() {
@@ -112,4 +112,11 @@ public class Event {
return clone;
}
+ static String maxLength(String string, int length){
+ if (string != null && string.length() > length) {
+ return string.substring(0, length - 1);
+ }
+ return string;
+ }
+
}
diff --git a/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java b/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java
index 400ef04..9e1e127 100755
--- a/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java
+++ b/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java
@@ -2,7 +2,7 @@ package org.keycloak.events.email;
import org.jboss.logging.Logger;
import org.keycloak.email.EmailException;
-import org.keycloak.email.EmailProvider;
+import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider;
@@ -23,13 +23,13 @@ public class EmailEventListenerProvider implements EventListenerProvider {
private KeycloakSession session;
private RealmProvider model;
- private EmailProvider emailProvider;
+ private EmailTemplateProvider emailTemplateProvider;
private Set<EventType> includedEvents;
- public EmailEventListenerProvider(KeycloakSession session, EmailProvider emailProvider, Set<EventType> includedEvents) {
+ public EmailEventListenerProvider(KeycloakSession session, EmailTemplateProvider emailTemplateProvider, Set<EventType> includedEvents) {
this.session = session;
this.model = session.realms();
- this.emailProvider = emailProvider;
+ this.emailTemplateProvider = emailTemplateProvider;
this.includedEvents = includedEvents;
}
@@ -41,7 +41,7 @@ public class EmailEventListenerProvider implements EventListenerProvider {
UserModel user = session.users().getUserById(event.getUserId(), realm);
if (user != null && user.getEmail() != null && user.isEmailVerified()) {
try {
- emailProvider.setRealm(realm).setUser(user).sendEvent(event);
+ emailTemplateProvider.setRealm(realm).setUser(user).sendEvent(event);
} catch (EmailException e) {
log.error("Failed to send type mail", e);
}
diff --git a/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProviderFactory.java b/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProviderFactory.java
index 9f662dc..ae2c4d0 100755
--- a/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProviderFactory.java
+++ b/events/email/src/main/java/org/keycloak/events/email/EmailEventListenerProviderFactory.java
@@ -1,7 +1,7 @@
package org.keycloak.events.email;
import org.keycloak.Config;
-import org.keycloak.email.EmailProvider;
+import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventListenerProviderFactory;
import org.keycloak.events.EventType;
@@ -26,8 +26,8 @@ public class EmailEventListenerProviderFactory implements EventListenerProviderF
@Override
public EventListenerProvider create(KeycloakSession session) {
- EmailProvider emailProvider = session.getProvider(EmailProvider.class);
- return new EmailEventListenerProvider(session, emailProvider, includedEvents);
+ EmailTemplateProvider emailTemplateProvider = session.getProvider(EmailTemplateProvider.class);
+ return new EmailEventListenerProvider(session, emailTemplateProvider, includedEvents);
}
@Override
diff --git a/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java
old mode 100644
new mode 100755
index 8290b3b..2583ec4
--- a/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java
+++ b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java
@@ -17,10 +17,13 @@ import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterDeploymentContext;
+import org.keycloak.adapters.spi.AuthenticationError;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.ServerRequest;
+import org.keycloak.adapters.spi.LogoutError;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.RefreshToken;
import org.keycloak.util.JsonSerialization;
@@ -47,40 +50,44 @@ public class OfflineAccessPortalServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ try {
+ if (req.getRequestURI().endsWith("/login")) {
+ storeToken(req);
+ req.getRequestDispatcher("/WEB-INF/pages/loginCallback.jsp").forward(req, resp);
+ return;
+ }
- if (req.getRequestURI().endsWith("/login")) {
- storeToken(req);
- req.getRequestDispatcher("/WEB-INF/pages/loginCallback.jsp").forward(req, resp);
- return;
- }
+ String refreshToken = RefreshTokenDAO.loadToken();
+ String refreshTokenInfo;
+ boolean savedTokenAvailable;
+ if (refreshToken == null) {
+ refreshTokenInfo = "No token saved in database. Please login first";
+ savedTokenAvailable = false;
+ } else {
+ RefreshToken refreshTokenDecoded = null;
+ refreshTokenDecoded = TokenUtil.getRefreshToken(refreshToken);
+ String exp = (refreshTokenDecoded.getExpiration() == 0) ? "NEVER" : Time.toDate(refreshTokenDecoded.getExpiration()).toString();
+ refreshTokenInfo = String.format("<p>Type: %s</p><p>ID: %s</p><p>Expires: %s</p>", refreshTokenDecoded.getType(), refreshTokenDecoded.getId(), exp);
+ savedTokenAvailable = true;
+ }
+ req.setAttribute("tokenInfo", refreshTokenInfo);
+ req.setAttribute("savedTokenAvailable", savedTokenAvailable);
- String refreshToken = RefreshTokenDAO.loadToken();
- String refreshTokenInfo;
- boolean savedTokenAvailable;
- if (refreshToken == null) {
- refreshTokenInfo = "No token saved in database. Please login first";
- savedTokenAvailable = false;
- } else {
- RefreshToken refreshTokenDecoded = TokenUtil.getRefreshToken(refreshToken);
- String exp = (refreshTokenDecoded.getExpiration() == 0) ? "NEVER" : Time.toDate(refreshTokenDecoded.getExpiration()).toString();
- refreshTokenInfo = String.format("<p>Type: %s</p><p>ID: %s</p><p>Expires: %s</p>", refreshTokenDecoded.getType(), refreshTokenDecoded.getId(), exp);
- savedTokenAvailable = true;
- }
- req.setAttribute("tokenInfo", refreshTokenInfo);
- req.setAttribute("savedTokenAvailable", savedTokenAvailable);
-
- String customers;
- if (req.getRequestURI().endsWith("/loadCustomers")) {
- customers = loadCustomers(req, refreshToken);
- } else {
- customers = "";
- }
- req.setAttribute("customers", customers);
+ String customers;
+ if (req.getRequestURI().endsWith("/loadCustomers")) {
+ customers = loadCustomers(req, refreshToken);
+ } else {
+ customers = "";
+ }
+ req.setAttribute("customers", customers);
- req.getRequestDispatcher("/WEB-INF/pages/view.jsp").forward(req, resp);
+ req.getRequestDispatcher("/WEB-INF/pages/view.jsp").forward(req, resp);
+ } catch (JWSInputException e) {
+ throw new ServletException(e);
+ }
}
- private void storeToken(HttpServletRequest req) throws IOException {
+ private void storeToken(HttpServletRequest req) throws IOException, JWSInputException {
RefreshableKeycloakSecurityContext ctx = (RefreshableKeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
String refreshToken = ctx.getRefreshToken();
@@ -203,6 +210,18 @@ public class OfflineAccessPortalServlet extends HttpServlet {
public String getRemoteAddr() {
return servletRequest.getRemoteAddr();
}
+
+ @Override
+ public void setError(AuthenticationError error) {
+ servletRequest.setAttribute(AuthenticationError.class.getName(), error);
+
+ }
+
+ @Override
+ public void setError(LogoutError error) {
+ servletRequest.setAttribute(LogoutError.class.getName(), error);
+ }
+
};
}
diff --git a/examples/js-console/src/main/webapp/index.html b/examples/js-console/src/main/webapp/index.html
index 2cca0f8..3789a9f 100644
--- a/examples/js-console/src/main/webapp/index.html
+++ b/examples/js-console/src/main/webapp/index.html
@@ -67,8 +67,11 @@
var o = 'Token Expires:\t\t' + new Date((keycloak.tokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
o += 'Token Expires in:\t' + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds\n';
- o += 'Refresh Token Expires:\t' + new Date((keycloak.refreshTokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
- o += 'Refresh Expires in:\t' + Math.round(keycloak.refreshTokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds';
+ if (keycloak.refreshTokenParsed) {
+ o += 'Refresh Token Expires:\t' + new Date((keycloak.refreshTokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
+ o += 'Refresh Expires in:\t' + Math.round(keycloak.refreshTokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds';
+ }
+
output(o);
}
@@ -106,7 +109,17 @@
event('Auth Logout');
};
- keycloak.init().success(function(authenticated) {
+ keycloak.onTokenExpired = function () {
+ event('Access token expired.');
+ };
+
+ // Flow can be changed to 'implicit' or 'hybrid', but then client must enable implicit flow in admin console too
+ var initOptions = {
+ responseMode: 'fragment',
+ flow: 'standard'
+ };
+
+ keycloak.init(initOptions).success(function(authenticated) {
output('Init Success (' + (authenticated ? 'Authenticated' : 'Not Authenticated') + ')');
}).error(function() {
output('Init Error');
diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
index 30eb055..12b358e 100755
--- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
+++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
@@ -7,6 +7,7 @@ import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
@@ -15,6 +16,7 @@ import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
@@ -294,6 +296,12 @@ public class ExportUtils {
}
}
+ List<String> groups = new LinkedList<>();
+ for (GroupModel group : user.getGroups()) {
+ groups.add(ModelToRepresentation.buildGroupPath(group));
+ }
+ userRep.setGroups(groups);
+
return userRep;
}
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java
index 020d349..0a253d1 100644
--- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java
@@ -153,6 +153,10 @@ public class ExtendingThemeManager implements ThemeProvider {
private List<Theme> themes;
+ private Properties properties;
+
+ private ConcurrentHashMap<String, ConcurrentHashMap<Locale, Properties>> messages = new ConcurrentHashMap<>();
+
public ExtendingTheme(List<Theme> themes) {
this.themes = themes;
}
@@ -229,28 +233,41 @@ public class ExtendingThemeManager implements ThemeProvider {
@Override
public Properties getMessages(String baseBundlename, Locale locale) throws IOException {
- Properties messages = new Properties();
- ListIterator<Theme> itr = themes.listIterator(themes.size());
- while (itr.hasPrevious()) {
- Properties m = itr.previous().getMessages(baseBundlename, locale);
- if (m != null) {
- messages.putAll(m);
+ if (messages.get(baseBundlename) == null || messages.get(baseBundlename).get(locale) == null) {
+ Properties messages = new Properties();
+ ListIterator<Theme> itr = themes.listIterator(themes.size());
+ while (itr.hasPrevious()) {
+ Properties m = itr.previous().getMessages(baseBundlename, locale);
+ if (m != null) {
+ messages.putAll(m);
+ }
}
+
+ this.messages.putIfAbsent(baseBundlename, new ConcurrentHashMap<Locale, Properties>());
+ this.messages.get(baseBundlename).putIfAbsent(locale, messages);
+
+ return messages;
+ } else {
+ return messages.get(baseBundlename).get(locale);
}
- return messages;
}
@Override
public Properties getProperties() throws IOException {
- Properties properties = new Properties();
- ListIterator<Theme> itr = themes.listIterator(themes.size());
- while (itr.hasPrevious()) {
- Properties p = itr.previous().getProperties();
- if (p != null) {
- properties.putAll(p);
+ if (properties == null) {
+ Properties properties = new Properties();
+ ListIterator<Theme> itr = themes.listIterator(themes.size());
+ while (itr.hasPrevious()) {
+ Properties p = itr.previous().getProperties();
+ if (p != null) {
+ properties.putAll(p);
+ }
}
+ this.properties = properties;
+ return properties;
+ } else {
+ return properties;
}
- return properties;
}
}
diff --git a/forms/common-themes/src/main/resources/theme/base/account/account.ftl b/forms/common-themes/src/main/resources/theme/base/account/account.ftl
index 60a21b5..34c3e84 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/account.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/account/account.ftl
@@ -12,7 +12,7 @@
<form action="${url.accountUrl}" class="form-horizontal" method="post">
- <input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
+ <input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker?html}">
<div class="form-group ${messagesPerField.printIfExists('username','has-error')}">
<div class="col-sm-2 col-md-2">
diff --git a/forms/common-themes/src/main/resources/theme/base/account/applications.ftl b/forms/common-themes/src/main/resources/theme/base/account/applications.ftl
index b2bbdf2..bca5102 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/applications.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/account/applications.ftl
@@ -8,8 +8,8 @@
</div>
<form action="${url.revokeClientUrl}" method="post">
- <input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
- <input type="hidden" id="referrer" name="referrer" value="${stateChecker}">
+ <input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker?html}">
+ <input type="hidden" id="referrer" name="referrer" value="${stateChecker?html}">
<table class="table table-striped table-bordered">
<thead>
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_ca.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_ca.properties
new file mode 100644
index 0000000..2bb68b1
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_ca.properties
@@ -0,0 +1,155 @@
+doSave=Desa
+doCancel=Cancel\u00B7la
+doLogOutAllSessions=Desconnecta de totes les sessions
+doRemove=Elimina
+doAdd=Afegeix
+doSignOut=Desconnectar
+
+editAccountHtmlTtile=Edita compte
+federatedIdentitiesHtmlTitle=Identitats federades
+accountLogHtmlTitle=Registre del compte
+changePasswordHtmlTitle=Canvia contrasenya
+sessionsHtmlTitle=Sessions
+accountManagementTitle=Gesti\u00F3 de Compte Keycloak
+authenticatorTitle=Autenticador
+applicationsHtmlTitle=Aplicacions
+
+authenticatorCode=Codi d''un sol \u00FAs
+email=Email
+firstName=Nom
+givenName=Nom de pila
+fullName=Nom complet
+lastName=Cognoms
+familyName=Cognom
+password=Contrasenya
+passwordConfirm=Confirma la contrasenya
+passwordNew=Nova contrasenya
+username=Usuari
+address=Adre\u00E7a
+street=Carrer
+locality=Ciutat o Municipi
+region=Estat, Prov\u00EDncia, o Regi\u00F3
+postal_code=Postal code
+country=Pa\u00EDs
+emailVerified=Email verificat
+gssDelegationCredential=GSS Delegation Credential
+
+role_admin=Administrador
+role_realm-admin=Administrador del domini
+role_create-realm=Crear domini
+role_view-realm=Veure domini
+role_view-users=Veure usuaris
+role_view-applications=Veure aplicacions
+role_view-clients=Veure clients
+role_view-events=Veure events
+role_view-identity-providers=Veure prove\u00EFdors d''identitat
+role_manage-realm=Gestionar domini
+role_manage-users=Gestinar usuaris
+role_manage-applications=Gestionar aplicacions
+role_manage-identity-providers=Gestionar prove\u00EFdors d''identitat
+role_manage-clients=Gestionar clients
+role_manage-events=Gestionar events
+role_view-profile=Veure perfil
+role_manage-account=Gestionar compte
+role_read-token=Llegir token
+role_offline-access=Acc\u00E9s sense connexi\u00F3
+client_account=Compte
+client_security-admin-console=Consola d''Administraci\u00F3 de Seguretat
+client_realm-management=Gesti\u00F3 de domini
+client_broker=Broker
+
+
+requiredFields=Camps obligatoris
+allFieldsRequired=Tots els camps obligatoris
+
+backToApplication=« Torna a l''aplicaci\u00F3
+backTo=Torna a {0}
+
+date=Data
+event=Event
+ip=IP
+client=Client
+clients=Clients
+details=Detalls
+started=Iniciat
+lastAccess=\u00DAltim acc\u00E9s
+expires=Expira
+applications=Aplicacions
+
+account=Compte
+federatedIdentity=Identitat federada
+authenticator=Autenticador
+sessions=Sessions
+log=Registre
+
+application=Aplicaci\u00F3
+availablePermissions=Permisos disponibles
+grantedPermissions=Permisos concedits
+grantedPersonalInfo=Informaci\u00F3 personal concedida
+additionalGrants=Permisos addicionals
+action=Acci\u00F3
+inResource=a
+fullAccess=Acc\u00E9s total
+offlineToken=Codi d''autoritzaci\u00F3 offline
+revoke=Revocar perm\u00EDs
+
+configureAuthenticators=Autenticadors configurats
+mobile=M\u00F2bil
+totpStep1=Instal\u00B7la <a href=\"https://fedorahosted.org/freeotp/\" target=\"_blank\">FreeOTP</a> o Google Authenticator al teu tel\u00E8fon m\u00F2bil. Les dues aplicacions estan disponibles a <a href=\"https://play.google.com\">Google Play</a> i en l''App Store d''Apple.
+totpStep2=Obre l''aplicaci\u00F3 i escaneja el codi o introdueix la clau.
+totpStep3=Introdueix el codi \u00FAnic que et mostra l''aplicaci\u00F3 d''autenticaci\u00F3 i fes clic a Envia per finalitzar la configuraci\u00F3
+
+missingUsernameMessage=Si us plau indica el teu usuari.
+missingFirstNameMessage=Si us plau indica el nom.
+invalidEmailMessage=Email no v\u00E0lid
+missingLastNameMessage=Si us plau indica els teus cognoms.
+missingEmailMessage=Si us plau indica l''email.
+missingPasswordMessage=Si us plau indica la contrasenya.
+notMatchPasswordMessage=Les contrasenyes no coincideixen.
+
+missingTotpMessage=Si us plau indica el teu codi d''autenticaci\u00F3
+invalidPasswordExistingMessage=La contrasenya actual no \u00E9s correcta.
+invalidPasswordConfirmMessage=La confirmaci\u00F3 de contrasenya no coincideix.
+invalidTotpMessage=El c\u00F3digo de autenticaci\u00F3n no es v\u00E1lido.
+
+usernameExistsMessage=L''usuari ja existeix
+emailExistsMessage=L''email ja existeix
+
+readOnlyUserMessage=No pots actualitzar el teu usuari perqu\u00E8 el teu compte \u00E9s de nom\u00E9s lectura.
+readOnlyPasswordMessage=No pots actualitzar la contrasenya perqu\u00E8 el teu compte \u00E9s de nom\u00E9s lectura.
+
+successTotpMessage=Aplicaci\u00F3 d''autenticaci\u00F3 m\u00F2bil configurada.
+successTotpRemovedMessage=Aplicaci\u00F3 d''autenticaci\u00F3 m\u00F2bil eliminada.
+
+successGrantRevokedMessage=Perm\u00EDs revocat correctament
+
+accountUpdatedMessage=El teu compte s''ha actualitzat.
+accountPasswordUpdatedMessage=La contrasenya s''ha actualitzat.
+
+missingIdentityProviderMessage=Prove\u00EFdor d''identitat no indicat.
+invalidFederatedIdentityActionMessage=Acci\u00F3 no v\u00E0lida o no indicada.
+identityProviderNotFoundMessage=No s''ha trobat un prove\u00EFdor d''identitat.
+federatedIdentityLinkNotActiveMessage=Aquesta identitat ja no est\u00E0 activa
+federatedIdentityRemovingLastProviderMessage=No pots eliminar l''\u00FAltima identitat federada perqu\u00E8 no tens fixada una contrasenya.
+identityProviderRedirectErrorMessage=Error en la redirecci\u00F3 al prove\u00EFdor d''identitat
+identityProviderRemovedMessage=Prove\u00EFdor d''identitat esborrat correctament.
+
+accountDisabledMessage=El compte est\u00E0 desactivada, contacteu amb l''administrador.
+
+accountTemporarilyDisabledMessage=El compte est\u00E0 temporalment desactivat, contacta amb l''administrador o intenta-ho de nou m\u00E9s tard.
+invalidPasswordMinLengthMessage=Contrasenya incorrecta: longitud m\u00EDnima {0}.
+invalidPasswordMinLowerCaseCharsMessage=Contrasenya incorrecta: ha de contenir almenys {0} lletres min\u00FAscules.
+invalidPasswordMinDigitsMessage=Contrase\u00F1a incorrecta: debe contener al menos {0} caracteres num\u00E9ricos.
+invalidPasswordMinUpperCaseCharsMessage=Contrasenya incorrecta: ha de contenir almenys {0} lletres maj\u00FAscules.
+invalidPasswordMinSpecialCharsMessage=Contrasenya incorrecta: ha de contenir almenys {0} car\u00E0cters especials.
+invalidPasswordNotUsernameMessage=Contrasenya incorrecta: no pot ser igual al nom d''usuari.
+invalidPasswordRegexPatternMessage=Contrasenya incorrecta: no compleix l''expressi\u00F3 regular.
+invalidPasswordHistoryMessage=Contrasenya incorrecta: no pot ser igual a cap de les \u00FAltimes {0} contrasenyes.
+
+locale_de=Deutsch
+locale_en=English
+locale_it=Italian
+locale_pt-BR=Portugu\u00EAs (Brasil)
+locale_fr=Fran\u00E7ais
+locale_es=Espa\u00F1ol
+locale_ca=Catal\u00E0
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_es.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_es.properties
index 40743f8..ac59648 100644
--- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_es.properties
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_es.properties
@@ -96,7 +96,7 @@ revoke=Revocar permiso
configureAuthenticators=Autenticadores configurados
mobile=M\u00F3vil
totpStep1=Instala <a href=\"https://fedorahosted.org/freeotp/\" target=\"_blank\">FreeOTP</a> o Google Authenticator en tu tel\u00E9fono m\u00F3vil. Ambas aplicaciones est\u00E1n disponibles en <a href=\"https://play.google.com\">Google Play</a> y en la App Store de Apple.
-totpStep2=Abre la aplicacvi\u00F3n y escanea el c\u00F3digo o introduce la clave.
+totpStep2=Abre la aplicaci\u00F3n y escanea el c\u00F3digo o introduce la clave.
totpStep3=Introduce el c\u00F3digo \u00FAnico que te muestra la aplicaci\u00F3n de autenticaci\u00F3n y haz clic en Enviar para finalizar la configuraci\u00F3n
missingUsernameMessage=Por favor indica tu usuario.
@@ -152,3 +152,4 @@ locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil)
locale_fr=Fran\u00E7ais
locale_es=Espa\u00F1ol
+locale_ca=Catal\u00E0
diff --git a/forms/common-themes/src/main/resources/theme/base/account/password.ftl b/forms/common-themes/src/main/resources/theme/base/account/password.ftl
index abea25d..053fdbd 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/password.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/account/password.ftl
@@ -23,7 +23,7 @@
</div>
</#if>
- <input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
+ <input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker?html}">
<div class="form-group">
<div class="col-sm-2 col-md-2">
@@ -49,7 +49,6 @@
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
<div class="">
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
- <button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Cancel">${msg("doCancel")}</button>
</div>
</div>
</div>
diff --git a/forms/common-themes/src/main/resources/theme/base/account/totp.ftl b/forms/common-themes/src/main/resources/theme/base/account/totp.ftl
index 1cf9e67..1f261e7 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/totp.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/account/totp.ftl
@@ -41,7 +41,7 @@
<hr/>
<form action="${url.totpUrl}" class="form-horizontal" method="post">
- <input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
+ <input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker?html}">
<div class="form-group">
<div class="col-sm-2 col-md-2">
<label for="totp" class="control-label">${msg("authenticatorCode")}</label>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/index.ftl b/forms/common-themes/src/main/resources/theme/base/admin/index.ftl
index 0cdadfb..f6bbfa5 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/index.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/admin/index.ftl
@@ -53,10 +53,10 @@
</div>
</div>
-<div class="feedback-aligner" data-ng-show="notification">
+<div class="feedback-aligner" data-ng-show="notification.display">
<div class="alert alert-{{notification.type}} alert-dismissable">
- <button type="button" class="close">
- <span class="pficon pficon-close" data-ng-click="notification = null"/>
+ <button type="button" class="close" data-ng-click="notification.remove()" id="notification-close">
+ <span class="pficon pficon-close"/>
</button>
<span class="pficon pficon-ok" ng-show="notification.type == 'success'"></span>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_ca.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_ca.properties
new file mode 100644
index 0000000..31b9fe2
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_ca.properties
@@ -0,0 +1,466 @@
+# Common messages
+enabled=Habilitat
+name=Nom
+save=Desar
+cancel=Cancel\u00B7la
+onText=SI
+offText=NO
+client=Client
+clients=Clients
+clear=Neteja
+selectOne=Selecciona un...
+
+true=S\u00ED
+false=No
+
+
+# Realm settings
+realm-detail.enabled.tooltip=Els usuaris i clients nom\u00E9s poden accedir a un domini si est\u00E0 habilitat
+registrationAllowed=Registre d''usuari
+registrationAllowed.tooltip=Habilitar/deshabilitar la p\u00E0gina de registre. Un enlla\u00E7 per al registre es mostrar\u00E0 tamb\u00E9 a la p\u00E0gina d''inici de sessi\u00F3.
+registrationEmailAsUsername=Email com a nom d''usuari
+registrationEmailAsUsername.tooltip=Si est\u00E0 habilitat el nom d''usuari queda ocult del formulari de registre i l''email es fa servir com a nom d''usuari per als nous usuaris.
+editUsernameAllowed=Edita el nom d''usuari
+editUsernameAllowed.tooltip=Si est\u00E0 habilitat, el nom d''usuari \u00E9s editable, altrament \u00E9s de nom\u00E9s lectura.
+resetPasswordAllowed=Oblit contrasenya
+resetPasswordAllowed.tooltip=Mostra un enlla\u00E7 a la p\u00E0gina d''inici de sessi\u00F3 perqu\u00E8 l''usuari faci clic quan ha oblidat les seves credencials.
+rememberMe=Mantenir connectat
+rememberMe.tooltip=Mostra la casella de selecci\u00F3 en la p\u00E0gina d''inici de sessi\u00F3 per a permetre a l''usuari estar connectat entre reinicis del navegador fins que la sessi\u00F3 expiri.
+verifyEmail=Verificar email
+verifyEmail.tooltip=For\u00E7ar l''usuari a verificar la seva adre\u00E7a de correu electr\u00F2nic la primera vegada que inici\u00EF sessi\u00F3.
+sslRequired=Sol\u00B7licitar SSL
+sslRequired.option.all=totes les peticions
+sslRequired.option.external=peticions externes
+sslRequired.option.none=cap
+sslRequired.tooltip=\u00C9s HTTP obligatori? ''cap'' significa que HTTPS no \u00E9s obligatori per cap direcic\u00F3n IP de client, ''peticions externes'' indica que localhost i les adreces IP privades poden accedir sense HTTPS, ''totes les peticions'' vol dir que HTTPS \u00E9s obligatori per a totes les adreces IP.
+publicKey=Clau p\u00FAblica
+gen-new-keys=Generar noves claus
+certificate=Certificat
+host=Host
+smtp-host=Host SMTP
+port=Port
+smtp-port=Port SMTP (per defecte 25)
+from=Des de
+sender-email-addr=Email del emissor
+enable-ssl=Habilitar SSL
+enable-start-tls=Habilitar StartTLS
+enable-auth=Habilitar autenticaci\u00F3
+username=Usuari
+login-username=Usuari
+password=Contrasenya
+login-password=Contrasenya
+login-theme=Tema d''inici de sessi\u00F3
+select-one=Selecciona un...
+login-theme.tooltip=Selecciona el tema per a les p\u00E0gines d''inici de sessi\u00F3, TOTP, permisos, registre i recordatori de contrasenya.
+account-theme=Tema de compte
+account-theme.tooltip=Selecciona el tema per a les p\u00E0gines de gesti\u00F3 del compte d''usuari.
+admin-console-theme=Tema de consola d''administraci\u00F3
+select-theme-admin-console=Selecciona el tema per a la consola d''administraci\u00F3.
+email-theme=Tema d''email
+select-theme-email=Selecciona el tema per als correus electr\u00F2nics que s\u00F3n enviats pel servidor.
+i18n-enabled=Internacionalitzaci\u00F3 activa
+supported-locales=Idiomes suportats
+supported-locales.placeholder=Indica l''idioma i prem Intro
+default-locale=Idioma per defecte
+realm-cache-enabled=Cach\u00E9 de domini habilitada
+realm-cache-enabled.tooltip=Activar/desactivar la cach\u00E9 per al domini, client i dades de rols.
+user-cache-enabled=Cach\u00E9 d''usuari habilitada
+user-cache-enabled.tooltip=Habilitar/deshabilitar la cach\u00E9 d''usuaris i d''assignacions d''usuaris a rols.
+revoke-refresh-token=Revocar el token d''actualitzaci\u00F3
+revoke-refresh-token.tooltip=Si est\u00E0 activat els tokens d''actualitzaci\u00F3 nom\u00E9s poden usar-se una vegada. En un altre cas els tokens d''actualitzaci\u00F3 no es revoquen quan s''utilitzen i poden ser usat m\u00FAltiples vegades.
+sso-session-idle=Sessions SSO inactives
+seconds=Segons
+minutes=Minuts
+hours=Hores
+days=Dies
+sso-session-max=Temps m\u00E0xim sessi\u00F3 SSO
+sso-session-idle.tooltip=Temps m\u00E0xim que una sessi\u00F3 pot estar inactiva abans que expiri. Els tokens i sessions de navegador s\u00F3n invalidades quan la sessi\u00F3 expira.
+sso-session-max.tooltip=Temps m\u00E0xim abans que una sessi\u00F3 expiri. Els tokens i sessions de navegador s\u00F3n invalidats quan una sessi\u00F3 expira.
+offline-session-idle=Inactivitat de sessi\u00F3 sense connexi\u00F3
+offline-session-idle.tooltip=Temps m\u00E0xim inactiu d''una sessi\u00F3 sense connexi\u00F3 abans que expiri. Necessites fer servi un token sense connexi\u00F3 per refrescar almenys una vegada dins d'aquest per\u00EDode, en un altre cas la sessi\u00F3 sense connexi\u00F3 expirar\u00E0.
+access-token-lifespan=Durada del token d''acc\u00E9s
+access-token-lifespan.tooltip=Temps m\u00E0xim abans que un token d''acc\u00E9s expiri. Es recomana que aquest valor sigui curt en relaci\u00F3 al temps m\u00E0xim de SSO
+client-login-timeout=Temps m\u00E0xim d''autenticaci\u00F3
+client-login-timeout.tooltip=Temps m\u00E0xim que un client t\u00E9 per finalitzar el protocol d''obtenci\u00F3 del token d''acc\u00E9s. Hauria de ser normalment de l''ordre d''1 minut.
+login-timeout=Temps m\u00E0xim de desconnexi\u00F3
+login-timeout.tooltip=Temps m\u00E0xim que un usuari t\u00E9 per completar l''inici de sessi\u00F3. Es recomana que sigui relativament alt. 30 minuts o m\u00E9s.
+login-action-timeout=Temps m\u00E0xim d''acci\u00F3 en l''inici de sessi\u00F3
+login-action-timeout.tooltip=Temps m\u00E0xim que un usuari t\u00E9 per completar accions relacionades amb l''inici de sessi\u00F3, com l''actualitzaci\u00F3 de contrasenya o configuraci\u00F3 de TOTP. \u00C9s recomanat que sigui relativament alt. 5 minuts o m\u00E9s.
+headers=Cap\u00E7aleres
+brute-force-detection=Detecci\u00F3 d''atacs per for\u00E7a bruta
+x-frame-options=X-Frame-Options
+click-label-for-info=Fes clic a l''enlla\u00E7 de l''etiqueta per obtenir m\u00E9s informaci\u00F3. El valor per defecte evita que les p\u00E0gines siguin incloses des d'iframes externs.
+content-sec-policy=Content-Security-Policy
+max-login-failures=Nombre m\u00E0xim d''errors d''inici de sessi\u00F3
+max-login-failures.tooltip=Indica quants errors es permeten abans que es dispari una espera.
+wait-increment=Increment d''espera
+wait-increment.tooltip=Quan s''ha arribat al llindar d''error, quant de temps ha d''estar un usuari bloquejat?
+quick-login-check-millis=Temps en mil\u00B7lisegons entre inicis de sessi\u00F3 r\u00E0pids
+quick-login-check-millis.tooltip=Si ocorren errors de forma concurrent i molt r\u00E0pida, bloquejar a l''usuari.
+min-quick-login-wait=Temps m\u00EDnim entre errors de connexi\u00F3 r\u00E0pids
+min-quick-login-wait.tooltip=Quant de temps s''ha d''esperar despr\u00E9s d''un error en un intent r\u00E0pid d''identificaci\u00F3
+max-wait=Espera m\u00E0xima
+max-wait.tooltip=Temps m\u00E0xim que un usuari queda bloquejat.
+failure-reset-time=Reinici del comptador d''errors
+failure-reset-time.tooltip=Quan s''ha de reiniciar el comptador d''errors?
+realm-tab-login=Inici de sessi\u00F3
+realm-tab-keys=Claus
+realm-tab-email=Email
+realm-tab-themes=Temes
+realm-tab-cache=Cach\u00E9
+realm-tab-tokens=Tokens
+realm-tab-security-defenses=Defenses de seguretat
+realm-tab-general=General
+add-realm=Afegir domini
+
+#Session settings
+realm-sessions=Sessions de domini
+revocation=Revocaci\u00F3
+logout-all=Desconnectar tot
+active-sessions=Sessions actives
+sessions=Sessions
+not-before=No abans de
+not-before.tooltip=Revocar qualsevol token em\u00E8s abans d''aquesta data.
+set-to-now=Fixar a ara
+push=Push
+push.tooltip=Per a cada client que t\u00E9 un URL d''administraci\u00F3, notificar les noves pol\u00EDtiques de revocaci\u00F3.
+
+#Protocol Mapper
+usermodel.prop.label=Propietat
+usermodel.prop.tooltip=Nom del m\u00E8tode de propietat en la interf\u00EDcie UserModel. Per exemple, un valor de ''email'' faria refer\u00E8ncia al m\u00E8tode UserModel.getEmail().
+usermodel.attr.label=Atribut d''usuari
+usermodel.attr.tooltip=Nom de l''atribut d''usuari emmagatzemat que \u00E9s el nom de l''atribut dins el map UserModel.attribute.
+userSession.modelNote.label=Nota sessi\u00F3 usuari
+userSession.modelNote.tooltip=Nom de la nota emmagatzemada en la sessi\u00F3 d''usuari dins del mapa UserSessionModel.note
+multivalued.label=Valors m\u00FAltiples
+multivalued.tooltip=Indica si l''atribut suporta m\u00FAltiples valors. Si est\u00E0 habilitat, la llista de tots els valors d''aquest atribut es fixar\u00E0 com a reclamaci\u00F3. Si est\u00E0 deshabilitat, nom\u00E9s el primer valor ser\u00E0 fixat com a reclamaci\u00F3.
+selectRole.label=Selecciona rol
+selectRole.tooltip=Introdueix el rol a la caixa de text de l''esquerra, o fes clic a aquest bot\u00F3 per navegar i buscar el rol que vols.
+tokenClaimName.label=Nom de reclam del token
+tokenClaimName.tooltip=Nom del reclam a inserir en el testimoni. Pot ser un nom complet com ''address.street''. En aquest cas, es crear\u00E0 un objecte JSON niat.
+jsonType.label=Tipus JSON de reclamaci\u00F3
+jsonType.tooltip=El tipus de JSON que hauria de fer-se servir per omplir la petici\u00F3 de JSON en el token. long, int, boolean i String s\u00F3n valors v\u00E0lids
+includeInIdToken.label=Afegir al token d''ID
+includeInAccessToken.label=Afegir al token d''acc\u00E9s
+includeInAccessToken.tooltip=S''hauria d'afegir la identitat reclamada al token d''acc\u00E9s?
+
+
+# client details
+clients.tooltip=Els clients s\u00F3n aplicacions de navegador de confian\u00E7a i serveis web d''un domini. Aquests clients poden sol\u00B7licitar un inici de sessi\u00F3. Tamb\u00E9 pots definir rols espec\u00EDfics de client.
+search.placeholder=Cercar...
+create=Crea
+import=Importar
+client-id=ID Client
+base-url=URL Base
+actions=Accions
+not-defined=No definit
+edit=Edita
+delete=Esborra
+no-results=Sense resultats
+no-clients-available=No hi ha clients disponibles
+add-client=Afegir Client
+select-file=Selecciona arxiu
+view-details=Veure detalls
+clear-import=Neteja importaci\u00F3
+client-id.tooltip=Indica l''identificador (ID) referenciat en URIs i tokens. Per exemple ''my-client''
+client.name.tooltip=Indica el nom visible del client. Per exemple ''My Client''. Tamb\u00E9 suporta claus per valors localitzats. Per exemple: ${my_client}
+client.enabled.tooltip=Els clients deshabilitats no poden iniciar una identificaci\u00F3 o obtenir codis d''acc\u00E9s.
+consent-required=Consentiment necessari
+consent-required.tooltip=Si est\u00E0 habilitat, els usuaris han de consentir l''acc\u00E9s del client.
+direct-grants-only=Nom\u00E9s permisos directes
+direct-grants-only.tooltip=Quan est\u00E0 habilitat, el client nom\u00E9s pot obtenir permisos de l''API REST.
+client-protocol=Protocol del Client
+client-protocol.tooltip=''OpenID connect'' permet als clients verificar la identitat de l''usuari final basat en l''autenticaci\u00F3 realitzada per un servidor d''autoritzaci\u00F3. ''SAML'' habilita l''autenticaci\u00F3 i autoritzaci\u00F3 d''escenaris basats en web incloent cross-domain i single sign-on (SSO) i utilitza tokens de seguretat que contenen afirmacions per passar informaci\u00F3.
+access-type=Tipus d''acc\u00E9s
+access-type.tooltip=Els clients ''Confidential'' necessiten un secret per iniciar el protocol d''identificaci\u00F3. Els clients ''Public'' no requereixen un secret. Els clients 'Bearer-only' s\u00F3n serveis web que mai inicien un login.
+service-accounts-enabled=Comptes de servei habilitades
+service-accounts-enabled.tooltip=Permetre autenticar aquest client contra Keycloak i rebre un token d''acc\u00E9s dedicat per a aquest client.
+include-authnstatement=Incloure AuthnStatement
+include-authnstatement.tooltip=Hauria d''incloure''s una declaraci\u00F3 especificant el m\u00E8tode i la marca de temps en la resposta d''inici de sessi\u00F3?
+sign-documents=Signar documents
+sign-documents.tooltip=Hauria el domini de signar els documents SAML?
+sign-assertions=Signar assercions
+sign-assertions.tooltip=Haurien de signar-se les assercions en documents SAML? Aquest ajust no \u00E9s necessari si el document ja s''est\u00E0 signant.
+signature-algorithm=Algorisme de signatura
+signature-algorithm.tooltip=L''algorisme de signatura usat per signar els documents.
+canonicalization-method=M\u00E8tode de canonicalitzaci\u00F3
+canonicalization-method.tooltip=M\u00E8tode de canonicalitzaci\u00F3 per a les signatures XML
+encrypt-assertions=Xifrar afirmacions
+encrypt-assertions.tooltip=Haurien de xifrar-se les afirmacions SAML amb la clau p\u00FAblica del client fent servir AES?
+client-signature-required=Signatura de Client requerida
+client-signature-required.tooltip=Signar\u00E0 el client les seves peticions i respostes SAML? I haurien de ser validades?
+force-post-binding=For\u00E7ar enlla\u00E7os POST
+force-post-binding.tooltip=Fer servir sempre POST per a les respostes
+front-channel-logout=Desconnexi\u00F3 en primer pla (Front Channel)
+front-channel-logout.tooltip=Quan est\u00E0 activat, la desconnexi\u00F3 requereix una redirecci\u00F3 del navegador cap al client. Quan no est\u00E0 activat, el servidor realitza una invovaci\u00F3n de desconnexi\u00F3 en segon pla.
+force-name-id-format=For\u00E7ar format NameID
+force-name-id-format.tooltip=Ignorar la petici\u00F3 de subjecte NameID i fer servir la configurada a la consola d''administraci\u00F3.
+name-id-format=Format de NameID
+name-id-format.tooltip=El format de NameID que es far\u00E0 servir per al t\u00EDtol
+root-url=URL arrel
+root-url.tooltip=URL arrel afegida a les URL relatives
+valid-redirect-uris=URIs de redirecci\u00F3 v\u00E0lides
+valid-redirect-uris.tooltip=Patr\u00F3 d''URI v\u00E0lida per a la qual un navegador pot sol\u00B7licitar la redirecci\u00F3 despr\u00E9s d''un inici o tancament de sessi\u00F3 completat. Es permeten comodins simples p.ex. ''http://example.com/*''. Tamb\u00E9 es poden indicar rutes relatives p.ex. ''/my/relative/path/*''. Les rutes relatives generaran un URI de redirecci\u00F3 fent servir el host i port de la petici\u00F3. Per SAML, s''han de fixar patrons d''URI v\u00E0lids si vols confiar en l''URL del servei del consumidor indicada en la petici\u00F3 d''inici de sessi\u00F3.
+base-url.tooltip=URL per defecte per utilitzar quan el servidor d''autoritzaci\u00F3 necessita redirigir o enviar de tornada al client.
+admin-url=URL d''administraci\u00F3
+admin-url.tooltip=URL a la interf\u00EDcie d''administraci\u00F3 del client. Fixa aquest valor si el client suporta l''adaptador de REST. Aquesta API REST permet al servidor d''autenticaci\u00F3 enviar al client pol\u00EDtiques de revocaci\u00F3 i altres tasques administratives. Normalment es fixa a l''URL base del client.
+master-saml-processing-url=URL principal de processament SAML
+master-saml-processing-url.tooltip=Si est\u00E0 configurada, aquesta URL es fara servir per a cada enlla\u00E7 al prove\u00EFdor del servei del consumidor d''assercions i serveis de desconnexi\u00F3 \u00FAnics. Pot ser sobreescrit de forma individual per a cada enlla\u00E7 i servei en el punt final de configuraci\u00F3 fina de SAML.
+idp-sso-url-ref=Nom de la URL d''un SSO iniciat per l''IDP
+idp-sso-url-ref.tooltip=Nom del fragment de l''URL per referenciar al client quan vols un SSO iniciat per l''IDP. Deixant aix\u00F2 buit desactiva els SSO iniciats per l''IDP. L''URL referenciada des del navegador ser\u00E0: {server-root}/realms/{realm}/protocol/saml/clients/{client-url-name}
+idp-sso-relay-state=Estat de retransmissi\u00F3 d''un SSO iniciat per l''IDP
+idp-sso-relay-state.tooltip=Estat de retransmissi\u00F3 que vols enviar amb una petici\u00F3 SAML quan s''inicia un SSO iniciat per l''IDP
+web-origins=Or\u00EDgens web
+web-origins.tooltip=Or\u00EDgens CORS permesos. Per permetre tots els or\u00EDgens d''URIs de redirecci\u00F3 v\u00E0lides afegeix ''+''. Per permetre tots els or\u00EDgens afegeix ''*''.
+fine-saml-endpoint-conf=Fine Grain SAML Endpoint Configuration
+fine-saml-endpoint-conf.tooltip=Expandeix aquesta secci\u00F3 per configurar les URL exactes per Assertion Consumer i Single Logout Service.
+assertion-consumer-post-binding-url=Assertion Consumer Service POST Binding URL
+assertion-consumer-post-binding-url.tooltip=SAML POST Binding URL for the client''s assertion consumer service (login responses). You can leave this blank if you do not have a URL for this binding.
+assertion-consumer-redirect-binding-url=Assertion Consumer Service Redirect Binding URL
+assertion-consumer-redirect-binding-url.tooltip=Assertion Consumer Service Redirect Binding URL
+logout-service-binding-post-url=URL d''enlla\u00E7 SAML POST per a la desconnexi\u00F3
+logout-service-binding-post-url.tooltip=URL d''enlla\u00E7 SAML POST per a la desconnexi\u00F3 \u00FAnica del client. Pots deixar-ho en blanc si est\u00E0s fent servir un enlla\u00E7 diferent.
+logout-service-redir-binding-url=URL d''enlla\u00E7 SAML de redirecci\u00F3 per a la desconnexi\u00F3
+logout-service-redir-binding-url.tooltip=URL d''enlla\u00E7 SAML de redirecci\u00F3 per a la desconnexi\u00F3 \u00FAnica del client. Pots deixar-ho en blanc si est\u00E0s fent servir un enlla\u00E7 diferent.
+
+# client import
+import-client=Importar Client
+format-option=Format
+select-format=Selecciona un format
+import-file=Arxiu d''Importaci\u00F3
+
+# client tabs
+settings=Ajustos
+credentials=Credencials
+saml-keys=Claus SAML
+roles=Rols
+mappers=Assignadors
+mappers.tootip=Els assignadors de protocols realitzen transformacions en tokens i documents. Poden fer coses com assignar dades d''usuari en peticions de protocol, o simplement transformar qualsevol petici\u00F3 entre el client i el servidor d''autenticaci\u00F3.
+scope=\u00C0mbit
+scope.tooltip=Les assignacions d''\u00E0mbit et permeten restringir que assignacions de rols d''usuari s''inclouen en el testimoni d''acc\u00E9s sol\u00B7licitat pel client.
+sessions.tooltip=Veure sessions actives per a aquest client. Permet veure quins usuaris estan actius i quan es van identificar.
+offline-access=Acc\u00E9s sense connexi\u00F3
+offline-access.tooltip=Veure sessions sense connexi\u00F3 per aquest client. Et permet veure que usuaris han sol\u00B7licitat tokens sense connexi\u00F3 i quan els van sol\u00B7licitar. Per revocar tots els tokens del client, accedeix a la pestanya de Revocaci\u00F3 i fixa el valor \"No abans de\" a \"now\".
+clustering=Clustering
+installation=Instal\u00B7laci\u00F3
+installation.tooltip=Eina d''ajuda per generar la configuraci\u00F3 de diversos formats d''adaptadors de client que pots descarregar o copiar i enganxar per configurar teus clients.
+service-account-roles=Rols de compte de servei
+service-account-roles.tooltip=Permetre autenticar assignacions de rol per el compte de servei dedicat a aquest client.
+
+# client credentials
+client-authenticator=Client autenticador
+client-authenticator.tooltip=Client autenticador usat per autenticar aquest client contra el servidor Keycloak
+certificate.tooltip=Certificat de client per validar els JWT emesos per aquest client i signats amb la clau privada del client del teu magatzem de claus.
+no-client-certificate-configured=No s''ha configurat el certificat de client
+gen-new-keys-and-cert=Generar noves claus i certificat
+import-certificate=Importar Certificat
+gen-client-private-key=Generar clau privada de client
+generate-private-key=Generar clau privada
+archive-format=Format d''Arxiu
+archive-format.tooltip=Format d''arxiu Java keystore o PKCS12
+key-alias=\u00C0lies de clau
+key-alias.tooltip=\u00C0lies de l''arxiu de la teva clau privada i certificat.
+key-password=Contrasenya de la clau
+key-password.tooltip=Contrasenya per accedir a la clau privada continguda en l''arxiu
+store-password=Contrasenya del magatzem
+store-password.tooltip=Contrasenya per accedir a l''arxiu
+generate-and-download=Generar i descarregar
+client-certificate-import=Importaci\u00F3 de certificat de client
+import-client-certificate=Importar Certificat de Client
+jwt-import.key-alias.tooltip=\u00C0lies de l''arxiu del teu certificat.
+secret=Secret
+regenerate-secret=Regenerar secret
+add-role=Afegir rol
+role-name=Nom de rol
+composite=Compost
+description=Descripci\u00F3
+no-client-roles-available=No hi ha rols de client disponibles
+scope-param-required=Par\u00E0metre d''\u00E0mbit obligatori
+scope-param-required.tooltip=Aquest rol nom\u00E9s ser\u00E0 concedit si el par\u00E0metre d''\u00E0mbit amb el nom del rol \u00E9s usat durant la petici\u00F3 d''autoritzaci\u00F3/obtenci\u00F3 de token.
+composite-roles=Rols compostos
+composite-roles.tooltip=Quan aquest paper \u00E9s assignat/desassignat a un usuari qualsevol rol associat amb ell ser\u00E0 assignat/desassignat de forma impl\u00EDcita.
+realm-roles=Rols de domini
+available-roles=Rols Disponibles
+add-selected=Afegeix seleccionat
+associated-roles=Rols Associats
+composite.associated-realm-roles.tooltip=Rols a nivell de domini associats amb aquest rol compost.
+composite.available-realm-roles.tooltip=Rols a nivell de domini disponibles en aquest paper compost.
+remove-selected=Esborrar seleccionats
+client-roles=Rols de Client
+select-client-to-view-roles=Selecciona el client per veure els seus rols
+available-roles.tooltip=Rols d''aquest client que pots associar a aquest rol compost.
+client.associated-roles.tooltip=Rols de client associats amb aquest rol compost.
+add-builtin=Afegeix Builtin
+category=Categoria
+type=Tipus
+no-mappers-available=No hi ha assignadors disponibles
+add-builtin-protocol-mappers=Afegeix Builtin Protocol Mappers
+add-builtin-protocol-mapper=Afegeix Builtin Protocol Mapper
+scope-mappings=Assignacions d''\u00E0mbit
+full-scope-allowed=Permet tots els \u00E0mbits
+full-scope-allowed.tooltip=Permet deshabilitar totes les restriccions.
+scope.available-roles.tooltip=Rols de domini que poden ser assignats a l''\u00E0mbit
+assigned-roles=Rols Assignats
+assigned-roles.tooltip=Rols a nivell de domini assignats a aquest \u00E0mbit.
+effective-roles=Rols efectius
+realm.effective-roles.tooltip=Rols de domini assignats que poden haver estat heretats d''un rol compost.
+select-client-roles.tooltip=Selecciona el client per veure els seus rols
+assign.available-roles.tooltip=Rols de clients disponibles per ser assignats.
+client.assigned-roles.tooltip=Rols de client assignats
+client.effective-roles.tooltip=Rols de client assignats que poden haver estat heretats des d''un rol compost.
+basic-configuration=Configuraci\u00F3 b\u00E0sica
+node-reregistration-timeout=Temps d''espera de re-registre de node
+node-reregistration-timeout.tooltip=Indica el m\u00E0xim interval de temps perqu\u00E8 els nodes del cl\u00FAster registrats es tornin a registrar. Si el node del cl\u00FAster no envia una petici\u00F3 de re-registre a Keycloak dins d''aquest interval, ser\u00E0 desregistrat de Keycloak
+registered-cluster-nodes=Registrar nodes de cl\u00FAster
+register-node-manually=Registrar node manualment
+test-cluster-availability=Provar disponibilitat del cl\u00FAster
+last-registration=\u00DAltim registre
+node-host=Host del node
+no-registered-cluster-nodes=No hi ha nodes de cl\u00FAster registrats disponibles
+cluster-nodes=Nodes de cl\u00FAster
+add-node=Afegir Node
+active-sessions.tooltip=Nombre total de sessions actives per a aquest client.
+show-sessions=Mostrar sessions
+show-sessions.tooltip=Advert\u00E8ncia, aquesta \u00E9s una operaci\u00F3 potencialment costosa depenent del nombre de sessions actives.
+user=Usuari
+from-ip=Des de IP
+session-start=Inici de sessi\u00F3
+first-page=Primera p\u00E0gina
+previous-page=P\u00E0gina Anterior
+next-page=P\u00E0gina seg\u00FCent
+client-revoke.not-before.tooltip=Revocar tots els tokens emesos abans d''aquesta data per a aquest client.
+client-revoke.push.tooltip=Si l''URL d''administraci\u00F3 est\u00E0 configurada per a aquest client, envia aquesta pol\u00EDtica a aquest client.
+select-a-format=Selecciona un format
+download=Descarrega
+offline-tokens=Tokens sense connexi\u00F3
+offline-tokens.tooltip=Nombre total de tokens sense connexi\u00F3 d''aquest client.
+show-offline-tokens=Mostrar tokens sense connexi\u00F3
+show-offline-tokens.tooltip=Advert\u00E8ncia, aquesta \u00E9s una operaci\u00F3 potencialment costosa depenent del nombre de tokens sense connexi\u00F3.
+token-issued=Token expedit
+last-access=\u00DAltim Acc\u00E9s
+last-refresh=\u00DAltima actualitzaci\u00F3
+key-export=Exportar clau
+key-import=Importar clau
+export-saml-key=Exporta clau SAML
+import-saml-key=Importar clau SAML
+realm-certificate-alias=\u00C0lies del certificat del domini
+realm-certificate-alias.tooltip=El certificat del domini \u00E9s emmagatzemat en arxiu. Aquest \u00E9s l''\u00E0lies a aquest.
+signing-key=Clau de firma
+saml-signing-key=Clau de firma SAML.
+private-key=Clau Privada
+generate-new-keys=Generar noves claus
+export=Exporta
+encryption-key=Clau de xifrat
+saml-encryption-key.tooltip=Clau de xifrat de SAML
+service-accounts=Comptes de servei
+service-account.available-roles.tooltip=Rols de domini que poden ser assignats al compte del servei.
+service-account.assigned-roles.tooltip=Rols de domini assignats al compte del servei.
+service-account-is-not-enabled-for=El compte del servei no est\u00E0 habilitada per {{client}}
+create-protocol-mappers=Crea assignadors de protocol
+create-protocol-mapper=Crea assignador de protocol
+protocol=Protocol
+protocol.tooltip=Protocol.
+id=ID
+mapper.name.tooltip=Nom de l''assignador.
+mapper.consent-required.tooltip=Quan es concedeix acc\u00E9s temporal, \u00E9s necessari el consentiment de l''usuari per a proporcinar aquestes dades al client?
+consent-text=Text del consentiment
+consent-text.tooltip=Text per mostrar a la p\u00E0gina de consentiment.
+mapper-type=Tipus d''assignador
+
+# realm identity providers
+identity-providers=Prove\u00EFdors d''identitat
+table-of-identity-providers=Taula de prove\u00EFdors d''identitat
+add-provider.placeholder=Afegir prove\u00EFdor...
+provider=Prove\u00EFdor
+gui-order=Ordre en la interf\u00EDcie gr\u00E0fica (GUI)
+redirect-uri=URI de redirecci\u00F3
+redirect-uri.tooltip=L''URI de redirecci\u00F3 usada per configurar el prove\u00EFdor d''identitat.
+alias=\u00C0lies
+identity-provider.alias.tooltip=L''\u00E0lies que identifica de forma \u00FAnica un prove\u00EFdor d''identitat, es far servir tamb\u00E9 per construir la URI de redirecci\u00F3.
+identity-provider.enabled.tooltip=Habilita/deshabilita aquest prove\u00EFdor d''identitat.
+authenticate-by-default=Autenticar per defecte
+identity-provider.authenticate-by-default.tooltip=Indica si aquest prove\u00EFdor hauria de ser provat per defecte per autenticacaci\u00F3n fins i tot abans de mostrar la p\u00E0gina d''inici de sessi\u00F3.
+store-tokens=Emmagatzemar tokens
+identity-provider.store-tokens.tooltip=Habilitar/deshabilitar si els tokens han de ser emmagatzemats despr\u00E9s d''autenticar als usuaris.
+stored-tokens-readable=Tokens emmagatzemats llegibles
+identity-provider.stored-tokens-readable.tooltip=Habilitar/deshabilitar si els nous usuaris poden llegir els tokens emmagatzemats. Aix\u00F2 assigna el rol ''broker.read-token''.
+update-profile-on-first-login=Actualitzar perfil al primer inici de sessi\u00F3
+on=Activat
+on-missing-info=Si falta informaci\u00F3
+off=Desactivat
+update-profile-on-first-login.tooltip=Defineix condicions sota les quals un usuari ha de actualitzar el seu perfil durant el primer inici de sessi\u00F3.
+trust-email=Confiar en l''email
+trust-email.tooltip=Si est\u00E0 habilitat, l''email rebut d''aquest prove\u00EFdor no es verificar\u00E0 encara que la verificaci\u00F3 estigui habilitada per al domini.
+gui-order.tooltip=N\u00FAmero que defineix l''ordre del prove\u00EFdor en la interf\u00EDcie gr\u00E0fica (GUI) (ex. a la p\u00E0gina d''inici de sessi\u00F3)
+openid-connect-config=Configuraci\u00F3 d''OpenID Connect
+openid-connect-config.tooltip=Configuraci\u00F3 d''OIDC SP i IDP externs
+authorization-url=URL d''autoritzaci\u00F3
+authorization-url.tooltip=La URL d''autoritzaci\u00F3.
+token-url=Token URL
+token-url.tooltip=L''URL del token.
+logout-url=URL de desconnexi\u00F3
+identity-provider.logout-url.tooltip=Punt de tancament de sessi\u00F3 per utilitzar en la desconnexi\u00F3 d''usuaris des d''un prove\u00EFdor d''identitat (IDP) extern.
+backchannel-logout=Backchannel Logout
+backchannel-logout.tooltip=Does the external IDP support backchannel logout?
+user-info-url=URL d''informaci\u00F3 d''usuari
+user-info-url.tooltip=L''URL d''informaci\u00F3 d''usuari. Opcional.
+identity-provider.client-id.tooltip=El client o identificador de client registrat en el prove\u00EFdor d''identitat.
+client-secret=Secret de Client
+show-secret=Mostrar secret
+hide-secret=Amaga secret
+client-secret.tooltip=El client o el secret de client registrat en el prove\u00EFdor d''identitat.
+issuer=Emissor
+issuer.tooltip=L''identificador de l''emissor per a l''emissor de la resposta. Si no s''indica, no es realitzar\u00E0 cap validaci\u00F3.
+default-scopes=\u00C0mbits per defecte
+identity-provider.default-scopes.tooltip=Els \u00E0mbits que s''enviaran quan es sol\u00B7liciti autoritzaci\u00F3. Pot ser una llista d''\u00E0mbits separats per espais. El valor per defecte \u00E9s ''openid''.
+prompt=Prompt
+unspecified.option=no especificat
+none.option=cap
+consent.option=consentiment
+login.option=login
+select-account.option=select_account
+prompt.tooltip=Indica si el servidor d''autoritzaci\u00F3 sol\u00B7licita a l''usuari final per reautenticaci\u00F3n i consentiment.
+validate-signatures=Validar signatures
+identity-provider.validate-signatures.tooltip=Habilitar/deshabilitar la validaci\u00F3 de signatures de prove\u00EFdors d''identitat (IDP) externs
+validating-public-key=Validant clau p\u00FAblica
+identity-provider.validating-public-key.tooltip=La clau p\u00FAblica en format PEM que ha de fer-se servir per verificar les signatures de prove\u00EFdors d''identitat (IDP) externs.
+import-external-idp-config=Importar configuraci\u00F3 externa d''IDP
+import-external-idp-config.tooltip=Et permet carregar metadades d''un prove\u00EFdor d''identitat (IDP) extern d''un arxiu de coniguraci\u00F3n o descarregar des d''una URL.
+import-from-url=Importar des d''URL
+identity-provider.import-from-url.tooltip=Importa metadades des d''un descriptor d''un prove\u00EFdor d''identitat (IDP) remot.
+import-from-file=Importa des d''arxiu
+identity-provider.import-from-file.tooltip=Importa metadades des d''un descriptor d''un prove\u00EFdor d''identitat (IDP) descarregat.
+saml-config=Configuraci\u00F3 SAML
+identity-provider.saml-config.tooltip=Configuraci\u00F3 de prove\u00EFdor SAML i IDP extern
+single-signon-service-url=URL de servei de connexi\u00F3 \u00FAnic (SSO)
+saml.single-signon-service-url.tooltip=L''URL que s''ha de fer servir per enviar peticions d''autenticaci\u00F3 (SAML AuthnRequest).
+single-logout-service-url=URL de servei de desconnexi\u00F3 \u00FAnic
+saml.single-logout-service-url.tooltip=L''URL que ha de fer-se servir per enviar peticions de desconnexi\u00F3.
+nameid-policy-format=Format de pol\u00EDtica NameID
+nameid-policy-format.tooltip=Indica la refer\u00E8ncia a la URI corresponent a un format de NameID. El valor per defecte \u00E9s urn:oasis:names:tc:SAML:2.0:nameid-format:persistent.
+http-post-binding-response=HTTP-POST enlla\u00E7 de resposta
+http-post-binding-response.tooltip=Indica si es respon a les peticions fent servir HTTP-POST. Si no est\u00E0 activat, es far servir HTTP-REDIRECT.
+http-post-binding-for-authn-request=HTTP-POST per AuthnRequest
+http-post-binding-for-authn-request.tooltip=Indica si AuthnRequest ha de ser enviat usant HTTP-POST. Si no est\u00E0 activat es fa HTTP-REDIRECT.
+want-authn-requests-signed=Signar AuthnRequests
+want-authn-requests-signed.tooltip=Indica si el prove\u00EFdor d''identitat espera rebre signades les AuthnRequest.
+force-authentication=For\u00E7ar autenticaci\u00F3
+identity-provider.force-authentication.tooltip=Indica si el prove\u00EFdor d''identitat ha d'autenticar en presentar directament les credencials en lloc de dependre d''un context de seguretat previ.
+validate-signature=Validar signatura
+saml.validate-signature.tooltip=Habilitar/deshabilitar la validaci\u00F3 de signatura en respostes SAML.
+validating-x509-certificate=Validant certificat X509
+validating-x509-certificate.tooltip=El certificat en format PEM que ha de fer-se servir per comprovar les signatures.
+saml.import-from-url.tooltip=Importar metadades des d''un descriptor d'entitat remot d''un IDP de SAML
+social.client-id.tooltip=L''identificador del client registrat amb el prove\u00EFdor d''identitat.
+social.client-secret.tooltip=El secret del client registrat amb el prove\u00EFdor d''identitat.
+social.default-scopes.tooltip=\u00C0mbits que s''enviaran quan es sol\u00B7liciti autoritzaci\u00F3. Veure la documentaci\u00F3 per als possibles valors, separador i valor per defecte.
+key=Clau
+stackoverflow.key.tooltip=La clau obtinguda en el registre del client de Stack Overflow.
+
+realms=Dominis
+realm=Domini
+
+identity-provider-mappers=Assignadors de prove\u00EFdors d''identitat (IDP)
+create-identity-provider-mapper=Crea assignador de prove\u00EFdor d''identitat (IDP)
+add-identity-provider-mapper=Afegeix assignador de prove\u00EFdor d''identitat
+client.description.tooltip=Indica la descripci\u00F3 del client. Per exemple ''My Client for TimeSheets''. Tamb\u00E9 suporta claus per a valors localitzats. Per exemple: ${my_client_description}
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
old mode 100644
new mode 100755
index a9e74f7..b92f65b
--- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -80,6 +80,8 @@ offline-session-idle=Offline Session Idle
offline-session-idle.tooltip=Time an offline session is allowed to be idle before it expires. You need to use offline token to refresh at least once within this period, otherwise offline session will expire.
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'.
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
@@ -109,6 +111,7 @@ realm-tab-email=Email
realm-tab-themes=Themes
realm-tab-cache=Cache
realm-tab-tokens=Tokens
+realm-tab-client-initial-access=Initial Access Tokens
realm-tab-security-defenses=Security Defenses
realm-tab-general=General
add-realm=Add Realm
@@ -163,19 +166,23 @@ add-client=Add Client
select-file=Select file
view-details=View details
clear-import=Clear import
-client-id.tooltip=Specifies ID referenced in URI and tokens. For example 'my-client'
+client-id.tooltip=Specifies ID referenced in URI and tokens. For example 'my-client'. For SAML this is also the expected issuer value from authn requests
client.name.tooltip=Specifies display name of the client. For example 'My Client'. Supports keys for localized values as well. For example\: ${my_client}
client.enabled.tooltip=Disabled clients cannot initiate a login or have obtain access tokens.
consent-required=Consent Required
consent-required.tooltip=If enabled users have to consent to client access.
-direct-grants-only=Direct Grants Only
-direct-grants-only.tooltip=When enabled, client can only obtain grants from grant REST API.
client-protocol=Client Protocol
client-protocol.tooltip='OpenID connect' allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server.'SAML' enables web-based authentication and authorization scenarios including cross-domain single sign-on (SSO) and uses security tokens containing assertions to pass information.
access-type=Access Type
access-type.tooltip='Confidential' clients require a secret to initiate login protocol. 'Public' clients do not require a secret. 'Bearer-only' clients are web services that never initiate a login.
+standard-flow-enabled=Standard Flow Enabled
+standard-flow-enabled.tooltip=This enables standard OpenID Connect redirect based authentication with authorization code. In terms of OpenID Connect or OAuth2 specifications, this enables support of 'Authorization Code Flow' for this client.
+implicit-flow-enabled=Implicit Flow Enabled
+implicit-flow-enabled.tooltip=This enables support for OpenID Connect redirect based authentication without authorization code. In terms of OpenID Connect or OAuth2 specifications, this enables support of 'Implicit Flow' for this client.
+direct-access-grants-enabled=Direct Access Grants Enabled
+direct-access-grants-enabled.tooltip=This enables support for Direct Access Grants, which means that client has access to username/password of user and exchange it directly with Keycloak server for access token. In terms of OAuth2 specification, this enables support of 'Resource Owner Password Credentials Grant' for this client.
service-accounts-enabled=Service Accounts Enabled
-service-accounts-enabled.tooltip=Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client.
+service-accounts-enabled.tooltip=Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. In terms of OAuth2 specification, this enables support of 'Client Credentials Grant' for this client.
include-authnstatement=Include AuthnStatement
include-authnstatement.tooltip=Should a statement specifying the method and timestamp be included in login responses?
sign-documents=Sign Documents
@@ -270,7 +277,7 @@ client-certificate-import=Client Certificate Import
import-client-certificate=Import Client Certificate
jwt-import.key-alias.tooltip=Archive alias for your certificate.
secret=Secret
-regenerate-secret=Regenerate Secretsecret=Secret
+regenerate-secret=Regenerate Secret
registrationAccessToken=Registration access token
registrationAccessToken.regenerate=Regenerate registration access token
registrationAccessToken.tooltip=The registration access token provides access for clients to the client registration service.
@@ -470,3 +477,17 @@ identity-provider-mappers=Identity Provider Mappers
create-identity-provider-mapper=Create Identity Provider Mapper
add-identity-provider-mapper=Add Identity Provider Mapper
client.description.tooltip=Specifies description of the client. For example 'My Client for TimeSheets'. Supports keys for localized values as well. For example\: ${my_client_description}
+
+expires=Expires
+expiration=Expiration
+count=Count
+remainingCount=Remaining count
+created=Created
+back=Back
+initial-access-tokens=Initial Access Tokens
+add-initial-access-tokens=Add Initial Access Token
+initial-access-token=Initial Access Token
+initial-access.copyPaste.tooltip=Copy/paste the initial access token before navigating away from this page as it's not posible to retrieve later
+continue=Continue
+initial-access-token.confirm.title=Copy Initial Access Token
+initial-access-token.confirm.text=Please copy and paste the initial access token before confirming as it can't be retrieved later
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_es.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_es.properties
index 170720a..5a353ab 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_es.properties
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_es.properties
@@ -17,7 +17,7 @@ false=No
# Realm settings
realm-detail.enabled.tooltip=Los usuarios y clientes solo pueden acceder a un dominio si est\u00E1 habilitado
registrationAllowed=Registro de usuario
-registrationAllowed.tooltip=Habilitar/deshabilitar la p\u00E1gina de registro. Un enlace para el registro se motrar\u00E1 tambi\u00E9n en la p\u00E1gina de inicio de sesi\u00F3n.
+registrationAllowed.tooltip=Habilitar/deshabilitar la p\u00E1gina de registro. Un enlace para el registro se mostrar\u00E1 tambi\u00E9n en la p\u00E1gina de inicio de sesi\u00F3n.
registrationEmailAsUsername=Email como nombre de usuario
registrationEmailAsUsername.tooltip=Si est\u00E1 habilitado el nombre de usuario queda oculto del formulario de registro y el email se usa como nombre de usuario para los nuevos usuarios.
editUsernameAllowed=Editar nombre de usuario
@@ -32,7 +32,7 @@ sslRequired=Solicitar SSL
sslRequired.option.all=todas las peticiones
sslRequired.option.external=peticiones externas
sslRequired.option.none=ninguna
-sslRequired.tooltip=\u00BFEs HTTP obligatorio? 'ninguna' significa que HTTPS no es obligatorio para ninguna direcic\u00F3n IP de cliente, 'peticiones externas' indica que localhost y las direcciones IP privadas pueden acceder sin HTTPS, 'todas las peticiones' significa que HTTPS es obligatorio para todas las direcciones IP.
+sslRequired.tooltip=\u00BFEs HTTP obligatorio? ''ninguna'' significa que HTTPS no es obligatorio para ninguna direcic\u00F3n IP de cliente, ''peticiones externas'' indica que localhost y las direcciones IP privadas pueden acceder sin HTTPS, ''todas las peticiones'' significa que HTTPS es obligatorio para todas las direcciones IP.
publicKey=Clave p\u00FAblica
gen-new-keys=Generar nuevas claves
certificate=Certificado
@@ -51,7 +51,7 @@ password=Contrase\u00F1a
login-password=Contrase\u00F1a
login-theme=Tema de inicio de sesi\u00F3n
select-one=Selecciona uno...
-login-theme.tooltip=Selecciona el tema para las p\u00E1gina de inicio de sesi\u00F3n, TOTP, permisos, registro y recordatorio de contrase\u00F1a.
+login-theme.tooltip=Selecciona el tema para las p\u00E1ginas de inicio de sesi\u00F3n, TOTP, permisos, registro y recordatorio de contrase\u00F1a.
account-theme=Tema de cuenta
account-theme.tooltip=Selecciona el tema para las p\u00E1ginas de gesti\u00F3n de la cuenta de usuario.
admin-console-theme=Tema de consola de administraci\u00F3n
@@ -75,23 +75,23 @@ hours=Horas
days=D\u00EDas
sso-session-max=Tiempo m\u00E1ximo sesi\u00F3n SSO
sso-session-idle.tooltip=Tiempo m\u00E1ximo que una sesi\u00F3n puede estar inactiva antes de que expire. Los tokens y sesiones de navegador son invalidadas cuando la sesi\u00F3n expira.
-sso-session-max.tooltip=Tiempo m\u00E1ximo antes de que una sesi\u00F3n expire. Los tokesn y sesiones de navegador son invalidados cuando una sesi\u00F3n expira.
+sso-session-max.tooltip=Tiempo m\u00E1ximo antes de que una sesi\u00F3n expire. Los tokens y sesiones de navegador son invalidados cuando una sesi\u00F3n expira.
offline-session-idle=Inactividad de sesi\u00F3n sin conexi\u00F3n
offline-session-idle.tooltip=Tiempo m\u00E1ximo inactivo de una sesi\u00F3n sin conexi\u00F3n antes de que expire. Necesitas usar un token sin conexi\u00F3n para refrescar al menos una vez dentro de este periodo, en otro caso la sesi\u00F3n sin conexi\u00F3n expirar\u00E1.
access-token-lifespan=Duraci\u00F3n del token de acceso
-access-token-lifespan.tooltip=Tiempo m\u00E1ximo antes de que un token de acceso expire. Se recomiena que esta valor sea corto en relaci\u00F3n al tiempo m\u00E1ximo de SSO
+access-token-lifespan.tooltip=Tiempo m\u00E1ximo antes de que un token de acceso expire. Se recomienda que este valor sea corto en relaci\u00F3n al tiempo m\u00E1ximo de SSO
client-login-timeout=Tiempo m\u00E1ximo de autenticaci\u00F3n
-client-login-timeout.tooltip=Tiempo m\u00E1ximo que un cliente tien para finalizar el protocolo de obtenci\u00F3n del token de acceso. Deber\u00EDa ser normalmente del orden de 1 minuto.
+client-login-timeout.tooltip=Tiempo m\u00E1ximo que un cliente tiene para finalizar el protocolo de obtenci\u00F3n del token de acceso. Deber\u00EDa ser normalmente del orden de 1 minuto.
login-timeout=Tiempo m\u00E1ximo de desconexi\u00F3n
-login-timeout.tooltip=Tiempo m\u00E1xmo que un usuario tiene para completar el inicio de sesi\u00F3n. Se recomienda que sea relativamente alto. 30 minutos o m\u00E1s.
+login-timeout.tooltip=Tiempo m\u00E1ximo que un usuario tiene para completar el inicio de sesi\u00F3n. Se recomienda que sea relativamente alto. 30 minutos o m\u00E1s.
login-action-timeout=Tiempo m\u00E1ximo de acci\u00F3n en el inicio de sesi\u00F3n
login-action-timeout.tooltip=Tiempo m\u00E1ximo que un usuario tiene para completar acciones relacionadas con el inicio de sesi\u00F3n, como la actualizaci\u00F3n de contrase\u00F1a o configuraci\u00F3n de TOTP. Es recomendado que sea relativamente alto. 5 minutos o m\u00E1s.
headers=Cabeceras
brute-force-detection=Detecci\u00F3n de ataques por fuerza bruta
x-frame-options=X-Frame-Options
-click-label-for-info=Haz clic en el enlace de la etiqueta para obtener m\u00E1s informaci\u00F3n. El valor por defecto evita que las p\u00E1ginas sean incluidaos desde iframes externos.
+click-label-for-info=Haz clic en el enlace de la etiqueta para obtener m\u00E1s informaci\u00F3n. El valor por defecto evita que las p\u00E1ginas sean incluidas desde iframes externos.
content-sec-policy=Content-Security-Policy
-max-login-failures=N\u00FAmero m\u00E1ximo de fallos de inicios de sesi\u00F3n
+max-login-failures=N\u00FAmero m\u00E1ximo de fallos de inicio de sesi\u00F3n
max-login-failures.tooltip=Indica cuantos fallos se permiten antes de que se dispare una espera.
wait-increment=Incremento de espera
wait-increment.tooltip=Cuando se ha alcanzado el umbral de fallo, \u00BFcuanto tiempo debe estar un usuario bloqueado?
@@ -107,7 +107,7 @@ realm-tab-login=Inicio de sesi\u00F3n
realm-tab-keys=Claves
realm-tab-email=Email
realm-tab-themes=Temas
-realm-tab-cache=Cache
+realm-tab-cache=Cach\u00E9
realm-tab-tokens=Tokens
realm-tab-security-defenses=Defensas de seguridad
realm-tab-general=General
@@ -123,11 +123,11 @@ not-before=No antes de
not-before.tooltip=Revocar cualquier token emitido antes de esta fecha.
set-to-now=Fijar a ahora
push=Push
-push.tooltip=Para cada cliente que tiene una URL de administraci\u00F3n, notificarlos the las nuevas pol\u00EDticas de revocaci\u00F3n.
+push.tooltip=Para cada cliente que tiene una URL de administraci\u00F3n, notificarlos las nuevas pol\u00EDticas de revocaci\u00F3n.
#Protocol Mapper
usermodel.prop.label=Propiedad
-usermodel.prop.tooltip=Nombre del m\u00E9todo de propiedad in la interfaz UserModel. Por ejemplo, un valor de 'email' referenciar\u00EDa al m\u00E9todo UserModel.getEmail().
+usermodel.prop.tooltip=Nombre del m\u00E9todo de propiedad en la interfaz UserModel. Por ejemplo, un valor de ''email'' referenciar\u00EDa al m\u00E9todo UserModel.getEmail().
usermodel.attr.label=Atributo de usuario
usermodel.attr.tooltip=Nombre del atributo de usuario almacenado que es el nombre del atributo dentro del map UserModel.attribute.
userSession.modelNote.label=Nota sesi\u00F3n usuario
@@ -137,7 +137,7 @@ multivalued.tooltip=Indica si el atributo soporta m\u00FAltiples valores. Si est
selectRole.label=Selecciona rol
selectRole.tooltip=Introduce el rol en la caja de texto de la izquierda, o haz clic en este bot\u00F3n para navegar y buscar el rol que quieres.
tokenClaimName.label=Nombre de reclamo del token
-tokenClaimName.tooltip=Nombre del reclamo a insertar en el token. Puede ser un nombre completo como 'address.street'. En este caso, se crear\u00E1 un objeto JSON anidado.
+tokenClaimName.tooltip=Nombre del reclamo a insertar en el token. Puede ser un nombre completo como ''address.street''. En este caso, se crear\u00E1 un objeto JSON anidado.
jsonType.label=Tipo JSON de reclamaci\u00F3n
jsonType.tooltip=El tipo de JSON que deber\u00EDa ser usado para rellenar la petici\u00F3n de JSON en el token. long, int, boolean y String son valores v\u00E1lidos
includeInIdToken.label=A\u00F1adir al token de ID
@@ -162,21 +162,21 @@ add-client=A\u00F1adir Cliente
select-file=Selecciona archivo
view-details=Ver detalles
clear-import=Limpiar importaci\u00F3n
-client-id.tooltip=Indica el identificador (ID) referenciado en URIs y tokens. Por ejemplo 'my-client'
-client.name.tooltip=Indica el nombre visible del cliente. Por ejemplo 'My Client'. Tambi\u00E9n soporta claves para vallores localizados. Por ejemplo: ${my_client}
+client-id.tooltip=Indica el identificador (ID) referenciado en URIs y tokens. Por ejemplo ''my-client''
+client.name.tooltip=Indica el nombre visible del cliente. Por ejemplo ''My Client''. Tambi\u00E9n soporta claves para valores localizados. Por ejemplo: ${my_client}
client.enabled.tooltip=Los clientes deshabilitados no pueden iniciar una identificaci\u00F3n u obtener c\u00F3digos de acceso.
consent-required=Consentimiento necesario
consent-required.tooltip=Si est\u00E1 habilitado, los usuarios tienen que consentir el acceso del cliente.
direct-grants-only=Solo permisos directos
direct-grants-only.tooltip=Cuando est\u00E1 habilitado, el cliente solo puede obtener permisos de la API REST.
client-protocol=Protocolo del Cliente
-client-protocol.tooltip='OpenID connect' permite a los clientes verificar la identidad del usuario final basado en la autenticaci\u00F3n realizada por un servidor de autorizaci\u00F3n. 'SAML' habilita la autenticaci\u00F3n y autorizaci\u00F3n de escenarios basados en web incluyendo cross-domain y single sign-on (SSO) y utiliza tokdne de seguridad que contienen afirmaciones para pasar informaci\u00F3n.
+client-protocol.tooltip=''OpenID connect'' permite a los clientes verificar la identidad del usuario final basado en la autenticaci\u00F3n realizada por un servidor de autorizaci\u00F3n. ''SAML'' habilita la autenticaci\u00F3n y autorizaci\u00F3n de escenarios basados en web incluyendo cross-domain y single sign-on (SSO) y utiliza tokens de seguridad que contienen afirmaciones para pasar informaci\u00F3n.
access-type=Tipo de acceso
-access-type.tooltip=Los clientes 'Confidential' necesitan un secreto para iniciar el protocolo de identificaci\u00F3n. Los clientes 'Public' no requieren un secreto. Los clientes 'Bearer-only' son servicios web que nunca inician un login.
+access-type.tooltip=Los clientes ''Confidential'' necesitan un secreto para iniciar el protocolo de identificaci\u00F3n. Los clientes ''Public'' no requieren un secreto. Los clientes ''Bearer-only'' son servicios web que nunca inician un login.
service-accounts-enabled=Cuentas de servicio habilitadas
-service-accounts-enabled.tooltip=Permitir autenticar este cliente contra Keycloak y recivir un token de acceso dedicado para este cliente.
+service-accounts-enabled.tooltip=Permitir autenticar este cliente contra Keycloak y recibir un token de acceso dedicado para este cliente.
include-authnstatement=Incluir AuthnStatement
-include-authnstatement.tooltip=null
+include-authnstatement.tooltip=\u00BFDeber\u00EDa incluirse una declaraci\u00F3n especificando el m\u00E9todo y la marca de tiempo en la respuesta de inicio de sesi\u00F3n?
sign-documents=Firmar documentos
sign-documents.tooltip=\u00BFDeber\u00EDa el dominio firmar los documentos SAML?
sign-assertions=Firmar aserciones
@@ -200,22 +200,22 @@ name-id-format.tooltip=El formato de NameID que se usar\u00E1 para el t\u00EDtul
root-url=URL ra\u00EDz
root-url.tooltip=URL ra\u00EDz a\u00F1adida a las URLs relativas
valid-redirect-uris=URIs de redirecci\u00F3n v\u00E1lidas
-valid-redirect-uris.tooltip=Patr\u00F3n de URI v\u00E1lida para la cual un navegador puede solicitar la redirecci\u00F3n tras un inicio o cierre de sesi\u00F3n completado. Se permiten comodines simples p.ej. 'http://example.com/*'. Tambi\u00E9n se pueden indicar rutas relativas p.ej. '/my/relative/path/*'. Las rutas relativas generar\u00E1n una URI de redirecci\u00F3n usando el host y puerto de la petici\u00F3n. Para SAML, se deben fijar patrones de URI v\u00E1lidos si quieres confiar en la URL del servicio del consumidor indicada en la petici\u00F3n de inicio de sesi\u00F3n.
+valid-redirect-uris.tooltip=Patr\u00F3n de URI v\u00E1lida para la cual un navegador puede solicitar la redirecci\u00F3n tras un inicio o cierre de sesi\u00F3n completado. Se permiten comodines simples p.ej. ''http://example.com/*''. Tambi\u00E9n se pueden indicar rutas relativas p.ej. ''/my/relative/path/*''. Las rutas relativas generar\u00E1n una URI de redirecci\u00F3n usando el host y puerto de la petici\u00F3n. Para SAML, se deben fijar patrones de URI v\u00E1lidos si quieres confiar en la URL del servicio del consumidor indicada en la petici\u00F3n de inicio de sesi\u00F3n.
base-url.tooltip=URL por defecto para usar cuando el servidor de autorizaci\u00F3n necesita redirigir o enviar de vuelta al cliente.
admin-url=URL de administraci\u00F3n
admin-url.tooltip=URL a la interfaz de administraci\u00F3n del cliente. Fija este valor si el cliente soporta el adaptador de REST. Esta API REST permite al servidor de autenticaci\u00F3n enviar al cliente pol\u00EDticas de revocaci\u00F3n y otras tareas administrativas. Normalment se fija a la URL base del cliente.
master-saml-processing-url=URL principal de procesamiento SAML
master-saml-processing-url.tooltip=Si est\u00E1 configurada, esta URL se usar\u00E1 para cada enlace al proveedor del servicio del consumidor de aserciones y servicios de desconexi\u00F3n \u00FAnicos. Puede ser sobreescrito de forma individual para cada enlace y servicio en el punto final de configuraci\u00F3n fina de SAML.
idp-sso-url-ref=Nombre de la URL de un SSO iniciado por el IDP
-idp-sso-url-ref.tooltip=Nombre del fragmento de la URL para referenciar al cliente cuando quieres un SSO iniciado por el IDP. Dejanto esto vac\u00EDo deshabilita los SSO iniciados por el IDP. La URL referenciada desde el navegador ser\u00E1: {server-root}/realms/{realm}/protocol/saml/clients/{client-url-name}
+idp-sso-url-ref.tooltip=Nombre del fragmento de la URL para referenciar al cliente cuando quieres un SSO iniciado por el IDP. Dejando esto vac\u00EDo deshabilita los SSO iniciados por el IDP. La URL referenciada desde el navegador ser\u00E1: {server-root}/realms/{realm}/protocol/saml/clients/{client-url-name}
idp-sso-relay-state=Estado de retransmisi\u00F3n de un SSO iniciado por el IDP
-idp-sso-relay-state.tooltip=Estado de retransmisi\u00F3n que quiees enviar con una petici\u00F3n SAML cuando se inicia un SSO inidicado por el IDP
-web-origins=Origenes web
-web-origins.tooltip=Origenes CORS permitidos. Para permitir todos los or\u00EDgenes de URIs de redirecci\u00F3n v\u00E1lidas a\u00F1ade '+'. Para permitir todos los or\u00EDgenes a\u00F1ade '*'.
+idp-sso-relay-state.tooltip=Estado de retransmisi\u00F3n que quieres enviar con una petici\u00F3n SAML cuando se inicia un SSO iniciado por el IDP
+web-origins=Or\u00EDgenes web
+web-origins.tooltip=Or\u00EDgenes CORS permitidos. Para permitir todos los or\u00EDgenes de URIs de redirecci\u00F3n v\u00E1lidas a\u00F1ade ''+''. Para permitir todos los or\u00EDgenes a\u00F1ade ''*''.
fine-saml-endpoint-conf=Fine Grain SAML Endpoint Configuration
fine-saml-endpoint-conf.tooltip=Expande esta secci\u00F3n para configurar las URL exactas para Assertion Consumer y Single Logout Service.
assertion-consumer-post-binding-url=Assertion Consumer Service POST Binding URL
-assertion-consumer-post-binding-url.tooltip=SAML POST Binding URL for the client's assertion consumer service (login responses). You can leave this blank if you do not have a URL for this binding.
+assertion-consumer-post-binding-url.tooltip=SAML POST Binding URL for the client''s assertion consumer service (login responses). You can leave this blank if you do not have a URL for this binding.
assertion-consumer-redirect-binding-url=Assertion Consumer Service Redirect Binding URL
assertion-consumer-redirect-binding-url.tooltip=Assertion Consumer Service Redirect Binding URL
logout-service-binding-post-url=URL de enlace SAML POST para la desconexi\u00F3n
@@ -252,7 +252,7 @@ client-authenticator=Cliente autenticador
client-authenticator.tooltip=Cliente autenticador usado para autenticar este cliente contra el servidor Keycloak
certificate.tooltip=Certificado de clinete para validar los JWT emitidos por este cliente y firmados con la clave privada del cliente de tu almac\u00E9n de claves.
no-client-certificate-configured=No se ha configurado el certificado de cliente
-gen-new-keys-and-cert=Genearr nuevas claves y certificado
+gen-new-keys-and-cert=Generar nuevas claves y certificado
import-certificate=Importar Certificado
gen-client-private-key=Generar clave privada de cliente
generate-private-key=Generar clave privada
@@ -278,7 +278,7 @@ no-client-roles-available=No hay roles de cliente disponibles
scope-param-required=Par\u00E1metro de \u00E1mbito obligatorio
scope-param-required.tooltip=Este rol solo ser\u00E1 concedido si el par\u00E1metro de \u00E1mbito con el nombre del rol es usado durante la petici\u00F3n de autorizaci\u00F3n/obtenci\u00F3n de token.
composite-roles=Roles compuestos
-composite-roles.tooltip=Cuanto este rol es asignado/desasignado a un usuario cualquier rol asociado con \u00E9l ser\u00E1 asignado/desasignado de forma impl\u00EDcita.
+composite-roles.tooltip=Cuando este rol es asignado/desasignado a un usuario cualquier rol asociado con \u00E9l ser\u00E1 asignado/desasignado de forma impl\u00EDcita.
realm-roles=Roles de dominio
available-roles=Roles Disponibles
add-selected=A\u00F1adir seleccionado
@@ -317,7 +317,7 @@ test-cluster-availability=Probar disponibilidad del cluster
last-registration=\u00DAltimo registro
node-host=Host del nodo
no-registered-cluster-nodes=No hay nodos de cluster registrados disponibles
-cluster-nodes=Nodos de cluster
+cluster-nodes=Nodos de cl\u00FAster
add-node=A\u00F1adir Nodo
active-sessions.tooltip=N\u00FAmero total de sesiones activas para este cliente.
show-sessions=Mostrar sesiones
@@ -362,7 +362,7 @@ protocol=Protocolo
protocol.tooltip=Protocolo.
id=ID
mapper.name.tooltip=Nombre del asignador.
-mapper.consent-required.tooltip=Cuando se concede acceso temporal, \u00BFes necesario el consentimiento del usuario para proporcinar estos datos al cliente cliente?
+mapper.consent-required.tooltip=Cuando se concede acceso temporal, \u00BFes necesario el consentimiento del usuario para proporcinar estos datos al cliente?
consent-text=Texto del consentimiento
consent-text.tooltip=Texto para mostrar en la p\u00E1gina de consentimiento.
mapper-type=Tipo de asignador
@@ -381,14 +381,14 @@ identity-provider.enabled.tooltip=Habilita/deshabilita este proveedor de identid
authenticate-by-default=Autenticar por defecto
identity-provider.authenticate-by-default.tooltip=Indica si este proveedor deber\u00EDa ser probado por defecto para autenticacaci\u00F3n incluso antes de mostrar la p\u00E1gina de inicio de sesi\u00F3n.
store-tokens=Almacenar tokens
-identity-provider.store-tokens.tooltip=Hablitar/deshabilitar si los tokens deben ser almacenados despu\u00E9s de autenticar a los usuarios.
+identity-provider.store-tokens.tooltip=Habiltar/deshabilitar si los tokens deben ser almacenados despu\u00E9s de autenticar a los usuarios.
stored-tokens-readable=Tokens almacenados legibles
-identity-provider.stored-tokens-readable.tooltip=Habilitar/deshabilitar is los nuevos usuarios pueden leear los tokens almacenados. Esto asigna el rol 'broker.read-token'.
+identity-provider.stored-tokens-readable.tooltip=Habilitar/deshabilitar si los nuevos usuarios pueden leer los tokens almacenados. Esto asigna el rol ''broker.read-token''.
update-profile-on-first-login=Actualizar perfil en el primer inicio de sesi\u00F3n
on=Activado
on-missing-info=Si falta informaci\u00F3n
off=Desactivado
-update-profile-on-first-login.tooltip=Define condiciones bajos las cuales un usuario tiene que actualizar su perfil durante el primer inicio de sesi\u00F3n.
+update-profile-on-first-login.tooltip=Define condiciones bajo las cuales un usuario tiene que actualizar su perfil durante el primer inicio de sesi\u00F3n.
trust-email=Confiar en el email
trust-email.tooltip=Si est\u00E1 habilitado, el email recibido de este proveedor no se verificar\u00E1 aunque la verificaci\u00F3n est\u00E9 habilitada para el dominio.
gui-order.tooltip=N\u00FAmero que define el orden del proveedor en la interfaz gr\u00E1fica (GUI) (ej. en la p\u00E1gina de inicio de sesi\u00F3n)
@@ -403,7 +403,7 @@ identity-provider.logout-url.tooltip=Punto de cierre de sesi\u00F3n para usar en
backchannel-logout=Backchannel Logout
backchannel-logout.tooltip=Does the external IDP support backchannel logout?
user-info-url=URL de informaci\u00F3n de usuario
-user-info-url.tooltip=.La URL de informaci\u00F3n de usuario. Opcional
+user-info-url.tooltip=La URL de informaci\u00F3n de usuario. Opcional.
identity-provider.client-id.tooltip=El cliente o identificador de cliente registrado en el proveedor de identidad.
client-secret=Secreto de Cliente
show-secret=Mostrar secreto
@@ -412,7 +412,7 @@ client-secret.tooltip=El cliente o el secreto de cliente registrado en el provee
issuer=Emisor
issuer.tooltip=El identificador del emisor para el emisor de la respuesta. Si no se indica, no se realizar\u00E1 ninguna validaci\u00F3n.
default-scopes=\u00C1mbitos por defecto
-identity-provider.default-scopes.tooltip=Los \u00E1mbitos que se enviar\u00E1n cuando se solicite autorizaci\u00F3n. Puede ser una lista de \u00E1mbitos separados por espacios. El valor por defecto es 'openid'.
+identity-provider.default-scopes.tooltip=Los \u00E1mbitos que se enviar\u00E1n cuando se solicite autorizaci\u00F3n. Puede ser una lista de \u00E1mbitos separados por espacios. El valor por defecto es ''openid''.
prompt=Prompt
unspecified.option=no especificado
none.option=ninguno
@@ -431,7 +431,7 @@ identity-provider.import-from-url.tooltip=Importar metadatos desde un descriptor
import-from-file=Importar desde archivo
identity-provider.import-from-file.tooltip=Importar metadatos desde un descriptor de un proveedor de identidad (IDP) descargado.
saml-config=Configuraci\u00F3n SAML
-identity-provider.saml-config.tooltip=Configurci\u00F3n de proveedor SAML e IDP externo
+identity-provider.saml-config.tooltip=Configuraci\u00F3n de proveedor SAML e IDP externo
single-signon-service-url=URL de servicio de conexi\u00F3n \u00FAnico (SSO)
saml.single-signon-service-url.tooltip=La URL que debe ser usada para enviar peticiones de autenticaci\u00F3n (SAML AuthnRequest).
single-logout-service-url=URL de servicio de desconexi\u00F3n \u00FAnico
@@ -441,7 +441,7 @@ nameid-policy-format.tooltip=Indica la referencia a la URI correspondiente a un
http-post-binding-response=HTTP-POST enlace de respuesta
http-post-binding-response.tooltip=Indica si se reponde a las peticiones usando HTTP-POST. Si no est\u00E1 activado, se usa HTTP-REDIRECT.
http-post-binding-for-authn-request=HTTP-POST para AuthnRequest
-http-post-binding-for-authn-request.tooltip=Indica si AuthnRequest debe ser envianda usando HTTP-POST. Si no est\u00E1 activado se hace HTTP-REDIRECT.
+http-post-binding-for-authn-request.tooltip=Indica si AuthnRequest debe ser enviada usando HTTP-POST. Si no est\u00E1 activado se hace HTTP-REDIRECT.
want-authn-requests-signed=Firmar AuthnRequests
want-authn-requests-signed.tooltip=Indica si el proveedor de identidad espera recibir firmadas las AuthnRequest.
force-authentication=Forzar autenticaci\u00F3n
@@ -463,4 +463,4 @@ realm=Dominio
identity-provider-mappers=Asignadores de proveedores de identidad (IDP)
create-identity-provider-mapper=Crear asignador de proveedor de identidad (IDP)
add-identity-provider-mapper=A\u00F1adir asignador de proveedor de identidad
-client.description.tooltip=Indica la descripci\u00F3n del cliente. Por ejemplo 'My Client for TimeSheets'. Tambi\u00E9n soporta claves para valores localizzados. Por ejemplo: ${my_client_description}
+client.description.tooltip=Indica la descripci\u00F3n del cliente. Por ejemplo ''My Client for TimeSheets''. Tambi\u00E9n soporta claves para valores localizados. Por ejemplo: ${my_client_description}
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/messages_ca.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/messages_ca.properties
new file mode 100644
index 0000000..50aa0a0
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/messages_ca.properties
@@ -0,0 +1,8 @@
+invalidPasswordHistoryMessage=Contrasenya incorrecta: no pot ser igual a cap de les \u00FAltimes {0} contrasenyes.
+invalidPasswordMinDigitsMessage=Contrase\u00F1a incorrecta: debe contener al menos {0} caracteres num\u00E9ricos.
+invalidPasswordMinLengthMessage=Contrasenya incorrecta: longitud m\u00EDnima {0}.
+invalidPasswordMinLowerCaseCharsMessage=Contrasenya incorrecta: ha de contenir almenys {0} lletres min\u00FAscules.
+invalidPasswordMinSpecialCharsMessage=Contrasenya incorrecta: ha de contenir almenys {0} car\u00E0cters especials.
+invalidPasswordMinUpperCaseCharsMessage=Contrasenya incorrecta: ha de contenir almenys {0} lletres maj\u00FAscules.
+invalidPasswordNotUsernameMessage=Contrasenya incorrecta: no pot ser igual al nom d''usuari.
+invalidPasswordRegexPatternMessage=Contrasenya incorrecta: no compleix l''expressi\u00F3 regular.
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/messages_es.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/messages_es.properties
index b5b7ca8..70e3d73 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/messages/messages_es.properties
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/messages_es.properties
@@ -1,6 +1,6 @@
invalidPasswordMinLengthMessage=Contrase\u00F1a incorrecta: longitud m\u00EDnima {0}.
invalidPasswordMinLowerCaseCharsMessage=Contrase\u00F1a incorrecta: debe contener al menos {0} letras min\u00FAsculas.
-invalidPasswordMinDigitsMessage=Contrase\u00F1a incorrecta: debe contaner al menos {0} caracteres num\u00E9ricos.
+invalidPasswordMinDigitsMessage=Contrase\u00F1a incorrecta: debe contener al menos {0} caracteres num\u00E9ricos.
invalidPasswordMinUpperCaseCharsMessage=Contrase\u00F1a incorrecta: debe contener al menos {0} letras may\u00FAsculas.
invalidPasswordMinSpecialCharsMessage=Contrase\u00F1a incorrecta: debe contener al menos {0} caracteres especiales.
invalidPasswordNotUsernameMessage=Contrase\u00F1a incorrecta: no puede ser igual al nombre de usuario.
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index fe74ff2..bfbcee9 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -176,6 +176,27 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'RealmTokenDetailCtrl'
})
+ .when('/realms/:realm/client-initial-access', {
+ templateUrl : resourceUrl + '/partials/client-initial-access.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ },
+ clientInitialAccess : function(ClientInitialAccessLoader) {
+ return ClientInitialAccessLoader();
+ }
+ },
+ controller : 'ClientInitialAccessCtrl'
+ })
+ .when('/realms/:realm/client-initial-access/create', {
+ templateUrl : resourceUrl + '/partials/client-initial-access-create.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ }
+ },
+ controller : 'ClientInitialAccessCreateCtrl'
+ })
.when('/realms/:realm/keys-settings', {
templateUrl : resourceUrl + '/partials/realm-keys.html',
resolve : {
@@ -676,6 +697,18 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'GroupRoleMappingCtrl'
})
+ .when('/realms/:realm/default-groups', {
+ templateUrl : resourceUrl + '/partials/default-groups.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ },
+ groups : function(GroupListLoader) {
+ return GroupListLoader();
+ }
+ },
+ controller : 'DefaultGroupsCtrl'
+ })
.when('/create/role/:realm/clients/:client', {
@@ -1974,6 +2007,15 @@ module.directive('kcTabsGroup', function () {
}
});
+module.directive('kcTabsGroupList', function () {
+ return {
+ scope: true,
+ restrict: 'E',
+ replace: true,
+ templateUrl: resourceUrl + '/templates/kc-tabs-group-list.html'
+ }
+});
+
module.directive('kcTabsClient', function () {
return {
scope: true,
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 971b0c4..11bb11b 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -799,13 +799,75 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se
} else if ($scope.client.attributes['saml_name_id_format'] == 'persistent') {
$scope.nameIdFormat = $scope.nameIdFormats[3];
}
+ if ($scope.client.attributes["saml.server.signature"]) {
+ if ($scope.client.attributes["saml.server.signature"] == "true") {
+ $scope.samlServerSignature = true;
+ } else {
+ $scope.samlServerSignature = false;
+
+ }
+ }
+ if ($scope.client.attributes["saml.assertion.signature"]) {
+ if ($scope.client.attributes["saml.assertion.signature"] == "true") {
+ $scope.samlAssertionSignature = true;
+ } else {
+ $scope.samlAssertionSignature = false;
+ }
+ }
+ if ($scope.client.attributes["saml.client.signature"]) {
+ if ($scope.client.attributes["saml.client.signature"] == "true") {
+ $scope.samlClientSignature = true;
+ } else {
+ $scope.samlClientSignature = false;
+ }
+ }
+ if ($scope.client.attributes["saml.encrypt"]) {
+ if ($scope.client.attributes["saml.encrypt"] == "true") {
+ $scope.samlEncrypt = true;
+ } else {
+ $scope.samlEncrypt = false;
+ }
+ }
+ if ($scope.client.attributes["saml.authnstatement"]) {
+ if ($scope.client.attributes["saml.authnstatement"] == "true") {
+ $scope.samlAuthnStatement = true;
+ } else {
+ $scope.samlAuthnStatement = false;
+ }
+ }
+ if ($scope.client.attributes["saml_force_name_id_format"]) {
+ if ($scope.client.attributes["saml_force_name_id_format"] == "true") {
+ $scope.samlForceNameIdFormat = true;
+ } else {
+ $scope.samlForceNameIdFormat = false;
+ }
+ }
+ if ($scope.client.attributes["saml.multivalued.roles"]) {
+ if ($scope.client.attributes["saml.multivalued.roles"] == "true") {
+ $scope.samlMultiValuedRoles = true;
+ } else {
+ $scope.samlMultiValuedRoles = false;
+ }
+ }
+ if ($scope.client.attributes["saml.force.post.binding"]) {
+ if ($scope.client.attributes["saml.force.post.binding"] == "true") {
+ $scope.samlForcePostBinding = true;
+ } else {
+ $scope.samlForcePostBinding = false;
+ }
+ }
}
if (!$scope.create) {
$scope.client = angular.copy(client);
updateProperties();
} else {
- $scope.client = { enabled: true, attributes: {}};
+ $scope.client = {
+ enabled: true,
+ standardFlowEnabled: true,
+ directAccessGrantsEnabled: true,
+ attributes: {}
+ };
$scope.client.attributes['saml_signature_canonicalization_method'] = $scope.canonicalization[0].value;
$scope.client.redirectUris = [];
$scope.accessType = $scope.accessTypes[0];
@@ -816,63 +878,6 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se
$scope.samlForceNameIdFormat = false;
}
- if ($scope.client.attributes["saml.server.signature"]) {
- if ($scope.client.attributes["saml.server.signature"] == "true") {
- $scope.samlServerSignature = true;
- } else {
- $scope.samlServerSignature = false;
-
- }
- }
- if ($scope.client.attributes["saml.assertion.signature"]) {
- if ($scope.client.attributes["saml.assertion.signature"] == "true") {
- $scope.samlAssertionSignature = true;
- } else {
- $scope.samlAssertionSignature = false;
- }
- }
- if ($scope.client.attributes["saml.client.signature"]) {
- if ($scope.client.attributes["saml.client.signature"] == "true") {
- $scope.samlClientSignature = true;
- } else {
- $scope.samlClientSignature = false;
- }
- }
- if ($scope.client.attributes["saml.encrypt"]) {
- if ($scope.client.attributes["saml.encrypt"] == "true") {
- $scope.samlEncrypt = true;
- } else {
- $scope.samlEncrypt = false;
- }
- }
- if ($scope.client.attributes["saml.authnstatement"]) {
- if ($scope.client.attributes["saml.authnstatement"] == "true") {
- $scope.samlAuthnStatement = true;
- } else {
- $scope.samlAuthnStatement = false;
- }
- }
- if ($scope.client.attributes["saml_force_name_id_format"]) {
- if ($scope.client.attributes["saml_force_name_id_format"] == "true") {
- $scope.samlForceNameIdFormat = true;
- } else {
- $scope.samlForceNameIdFormat = false;
- }
- }
- if ($scope.client.attributes["saml.multivalued.roles"]) {
- if ($scope.client.attributes["saml.multivalued.roles"] == "true") {
- $scope.samlMultiValuedRoles = true;
- } else {
- $scope.samlMultiValuedRoles = false;
- }
- }
- if ($scope.client.attributes["saml.force.post.binding"]) {
- if ($scope.client.attributes["saml.force.post.binding"] == "true") {
- $scope.samlForcePostBinding = true;
- } else {
- $scope.samlForcePostBinding = false;
- }
- }
$scope.importFile = function(fileContent){
console.debug(fileContent);
@@ -1039,7 +1044,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se
$scope.client.attributes['saml.signature.algorithm'] = $scope.signatureAlgorithm;
$scope.client.attributes['saml_name_id_format'] = $scope.nameIdFormat;
- if ($scope.client.protocol != 'saml' && !$scope.client.bearerOnly && !$scope.client.directGrantsOnly && (!$scope.client.redirectUris || $scope.client.redirectUris.length == 0)) {
+ if ($scope.client.protocol != 'saml' && !$scope.client.bearerOnly && ($scope.client.standardFlowEnabled || $scope.client.implicitFlowEnabled) && (!$scope.client.redirectUris || $scope.client.redirectUris.length == 0)) {
Notifications.error("You must specify at least one redirect uri");
} else {
if ($scope.create) {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js
index 4e3b986..00c8e93 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js
@@ -8,6 +8,7 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
$scope.tree = [];
$scope.edit = function(selected) {
+ if (selected.id == 'realm') return;
$location.url("/realms/" + realm.realm + "/groups/" + selected.id);
}
@@ -23,6 +24,7 @@ module.controller('GroupListCtrl', function($scope, $route, realm, groups, Group
$scope.paste = function(selected) {
if (selected == null) return;
if ($scope.cutNode == null) return;
+ if (selected.id == $scope.cutNode.id) return;
if (selected.id == 'realm') {
Groups.save({realm: realm.realm}, {id:$scope.cutNode.id}, function() {
$route.reload();
@@ -95,7 +97,7 @@ module.controller('GroupCreateCtrl', function($scope, $route, realm, parentId, G
console.log('save!!!');
if (parentId == 'realm') {
console.log('realm')
- Groups.save({realm: realm.realm, groupId: parentId}, $scope.group, function(data, headers) {
+ Groups.save({realm: realm.realm}, $scope.group, function(data, headers) {
var l = headers().location;
@@ -323,7 +325,7 @@ module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group,
module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMembership) {
$scope.realm = realm;
$scope.page = 0;
-
+ $scope.group = group;
$scope.query = {
realm: realm.realm,
@@ -366,3 +368,63 @@ module.controller('GroupMembersCtrl', function($scope, realm, group, GroupMember
});
+module.controller('DefaultGroupsCtrl', function($scope, $route, realm, groups, DefaultGroups, Notifications, $location, Dialog) {
+ $scope.realm = realm;
+ $scope.groupList = groups;
+ $scope.selectedGroup = null;
+ $scope.tree = [];
+
+ DefaultGroups.query({realm: realm.realm}, function(data) {
+ $scope.defaultGroups = data;
+
+ });
+
+ $scope.addDefaultGroup = function() {
+ if (!$scope.tree.currentNode) {
+ Notifications.error('Please select a group to add');
+ return;
+ };
+
+ DefaultGroups.update({realm: realm.realm, groupId: $scope.tree.currentNode.id}, function() {
+ Notifications.success('Added default group');
+ $route.reload();
+ });
+
+ };
+
+ $scope.removeDefaultGroup = function() {
+ DefaultGroups.remove({realm: realm.realm, groupId: $scope.selectedGroup.id}, function() {
+ Notifications.success('Removed default group');
+ $route.reload();
+ });
+
+ };
+
+ var isLeaf = function(node) {
+ return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
+ };
+
+ $scope.getGroupClass = function(node) {
+ if (node.id == "realm") {
+ return 'pficon pficon-users';
+ }
+ if (isLeaf(node)) {
+ return 'normal';
+ }
+ if (node.subGroups.length && node.collapsed) return 'collapsed';
+ if (node.subGroups.length && !node.collapsed) return 'expanded';
+ return 'collapsed';
+
+ }
+
+ $scope.getSelectedClass = function(node) {
+ if (node.selected) {
+ return 'selected';
+ } else if ($scope.cutNode && $scope.cutNode.id == node.id) {
+ return 'cut';
+ }
+ return undefined;
+ }
+
+});
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 6cd5bea..a66e205 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -327,6 +327,8 @@ module.controller('RealmLoginSettingsCtrl', function($scope, Current, Realm, rea
});
module.controller('RealmOtpPolicyCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
+ $scope.optionsDigits = [ 6, 8 ];
+
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/otp-policy");
});
@@ -337,7 +339,7 @@ module.controller('RealmThemeCtrl', function($scope, Current, Realm, realm, serv
$scope.supportedLocalesOptions = {
'multiple' : true,
'simple_tags' : true,
- 'tags' : ['en', 'de', 'pt-BR', 'it', 'es']
+ 'tags' : ['en', 'de', 'pt-BR', 'it', 'es', 'ca']
};
$scope.$watch('realm.supportedLocales', function(oldVal, newVal) {
@@ -897,6 +899,12 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
$scope.realm.accessTokenLifespan = TimeUnit.convert($scope.realm.accessTokenLifespan, from, to);
});
+ $scope.realm.accessTokenLifespanForImplicitFlowUnit = TimeUnit.autoUnit(realm.accessTokenLifespanForImplicitFlow);
+ $scope.realm.accessTokenLifespanForImplicitFlow = TimeUnit.toUnit(realm.accessTokenLifespanForImplicitFlow, $scope.realm.accessTokenLifespanForImplicitFlowUnit);
+ $scope.$watch('realm.accessTokenLifespanForImplicitFlowUnit', function(to, from) {
+ $scope.realm.accessTokenLifespanForImplicitFlow = TimeUnit.convert($scope.realm.accessTokenLifespanForImplicitFlow, from, to);
+ });
+
$scope.realm.ssoSessionIdleTimeoutUnit = TimeUnit.autoUnit(realm.ssoSessionIdleTimeout);
$scope.realm.ssoSessionIdleTimeout = TimeUnit.toUnit(realm.ssoSessionIdleTimeout, $scope.realm.ssoSessionIdleTimeoutUnit);
$scope.$watch('realm.ssoSessionIdleTimeoutUnit', function(to, from) {
@@ -945,6 +953,7 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
$scope.save = function() {
var realmCopy = angular.copy($scope.realm);
delete realmCopy["accessTokenLifespanUnit"];
+ delete realmCopy["accessTokenLifespanForImplicitFlowUnit"];
delete realmCopy["ssoSessionMaxLifespanUnit"];
delete realmCopy["offlineSessionIdleTimeoutUnit"];
delete realmCopy["accessCodeLifespanUnit"];
@@ -953,6 +962,7 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
delete realmCopy["accessCodeLifespanLoginUnit"];
realmCopy.accessTokenLifespan = TimeUnit.toSeconds($scope.realm.accessTokenLifespan, $scope.realm.accessTokenLifespanUnit)
+ realmCopy.accessTokenLifespanForImplicitFlow = TimeUnit.toSeconds($scope.realm.accessTokenLifespanForImplicitFlow, $scope.realm.accessTokenLifespanForImplicitFlowUnit)
realmCopy.ssoSessionIdleTimeout = TimeUnit.toSeconds($scope.realm.ssoSessionIdleTimeout, $scope.realm.ssoSessionIdleTimeoutUnit)
realmCopy.ssoSessionMaxLifespan = TimeUnit.toSeconds($scope.realm.ssoSessionMaxLifespan, $scope.realm.ssoSessionMaxLifespanUnit)
realmCopy.offlineSessionIdleTimeout = TimeUnit.toSeconds($scope.realm.offlineSessionIdleTimeout, $scope.realm.offlineSessionIdleTimeoutUnit)
@@ -1193,7 +1203,7 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real
}
}
- obj['port'] = obj['port'].toString();
+ obj['port'] = obj['port'] && obj['port'].toString();
return obj;
}
@@ -1986,6 +1996,65 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow
});
+module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, ClientInitialAccess, Dialog, Notifications, $route) {
+ $scope.realm = realm;
+ $scope.clientInitialAccess = clientInitialAccess;
+
+ $scope.remove = function(id) {
+ Dialog.confirmDelete(id, 'initial access token', function() {
+ ClientInitialAccess.remove({ realm: realm.realm, id: id }, function() {
+ Notifications.success("The initial access token was deleted.");
+ $route.reload();
+ });
+ });
+ }
+});
+
+module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, ClientInitialAccess, TimeUnit, Dialog, $location, $translate) {
+ $scope.expirationUnit = 'Days';
+ $scope.expiration = TimeUnit.toUnit(0, $scope.expirationUnit);
+ $scope.count = 1;
+ $scope.realm = realm;
+
+ $scope.$watch('expirationUnit', function(to, from) {
+ $scope.expiration = TimeUnit.convert($scope.expiration, from, to);
+ });
+
+ $scope.save = function() {
+ var expiration = TimeUnit.toSeconds($scope.expiration, $scope.expirationUnit);
+ ClientInitialAccess.save({
+ realm: realm.realm
+ }, { expiration: expiration, count: $scope.count}, function (data) {
+ console.debug(data);
+ $scope.id = data.id;
+ $scope.token = data.token;
+ });
+ };
+
+ $scope.cancel = function() {
+ $location.url('/realms/' + realm.realm + '/client-initial-access');
+ };
+
+ $scope.done = function() {
+ var btns = {
+ ok: {
+ label: $translate.instant('continue'),
+ cssClass: 'btn btn-primary'
+ },
+ cancel: {
+ label: $translate.instant('cancel'),
+ cssClass: 'btn btn-default'
+ }
+ }
+
+ var title = $translate.instant('initial-access-token.confirm.title');
+ var message = $translate.instant('initial-access-token.confirm.text');
+ Dialog.open(title, message, btns, function() {
+ $location.url('/realms/' + realm.realm + '/client-initial-access');
+ });
+ };
+});
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
index bcd98b1..61b608f 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
@@ -475,6 +475,13 @@ module.factory('GroupLoader', function(Loader, Group, $route, $q) {
});
});
+module.factory('ClientInitialAccessLoader', function(Loader, ClientInitialAccess, $route) {
+ return Loader.query(ClientInitialAccess, function() {
+ return {
+ realm: $route.current.params.realm
+ }
+ });
+});
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index 34fdc9d..4365291 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -102,6 +102,10 @@ module.service('Dialog', function($modal) {
openDialog(title, message, btns, '/templates/kc-modal-message.html').then(success, cancel);
}
+ dialog.open = function(title, message, btns, success, cancel) {
+ openDialog(title, message, btns, '/templates/kc-modal.html').then(success, cancel);
+ }
+
return dialog
});
@@ -138,31 +142,32 @@ module.factory('Notifications', function($rootScope, $timeout) {
var delay = 5000;
var notifications = {};
+ notifications.current = { display: false };
+ notifications.current.remove = function() {
+ if (notifications.scheduled) {
+ $timeout.cancel(notifications.scheduled);
+ delete notifications.scheduled;
+ }
+ delete notifications.current.type;
+ delete notifications.current.header;
+ delete notifications.current.message;
+ notifications.current.display = false;
+ console.debug("Remove message");
+ }
- var scheduled = null;
- var schedulePop = function() {
- if (scheduled) {
- $timeout.cancel(scheduled);
- }
-
- scheduled = $timeout(function() {
- $rootScope.notification = null;
- scheduled = null;
- }, delay);
- };
-
- if (!$rootScope.notifications) {
- $rootScope.notifications = [];
- }
+ $rootScope.notification = notifications.current;
notifications.message = function(type, header, message) {
- $rootScope.notification = {
- type : type,
- header: header,
- message : message
- };
+ notifications.current.type = type;
+ notifications.current.header = header;
+ notifications.current.message = message;
+ notifications.current.display = true;
- schedulePop();
+ notifications.scheduled = $timeout(function() {
+ notifications.current.remove();
+ }, delay);
+
+ console.debug("Added message");
}
notifications.info = function(message) {
@@ -284,6 +289,13 @@ module.service('ServerInfo', function($resource, $q, $http) {
}
});
+module.factory('ClientInitialAccess', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/clients-initial-access/:id', {
+ realm : '@realm',
+ id : '@id'
+ });
+});
+
module.factory('ClientProtocolMapper', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/clients/:client/protocol-mappers/models/:id', {
@@ -1550,9 +1562,13 @@ module.factory('UserGroupMapping', function($resource) {
});
});
-
-
-
-
-
-
+module.factory('DefaultGroups', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/default-groups/:groupId', {
+ realm : '@realm',
+ groupId : '@groupId'
+ }, {
+ update : {
+ method : 'PUT'
+ }
+ });
+});
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index 443cbe9..eb54741 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -59,13 +59,6 @@
</div>
<kc-tooltip>{{:: 'consent-required.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group clearfix block">
- <label class="col-md-2 control-label" for="directGrantsOnly">{{:: 'direct-grants-only' | translate}}</label>
- <div class="col-sm-6">
- <input ng-model="client.directGrantsOnly" name="directGrantsOnly" id="directGrantsOnly" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
- </div>
- <kc-tooltip>{{:: 'direct-grants-only.tooltip' | translate}}</kc-tooltip>
- </div>
<div class="form-group">
<label class="col-md-2 control-label" for="protocol">{{:: 'client-protocol' | translate}}</label>
<div class="col-sm-6">
@@ -92,6 +85,27 @@
</div>
<kc-tooltip>{{:: 'access-type.tooltip' | translate}}</kc-tooltip>
</div>
+ <div class="form-group" data-ng-show="protocol == 'openid-connect' && !client.bearerOnly">
+ <label class="col-md-2 control-label" for="standardFlowEnabled">{{:: 'standard-flow-enabled' | translate}}</label>
+ <kc-tooltip>{{:: 'standard-flow-enabled.tooltip' | translate}}</kc-tooltip>
+ <div class="col-md-6">
+ <input ng-model="client.standardFlowEnabled" name="standardFlowEnabled" id="standardFlowEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+ </div>
+ </div>
+ <div class="form-group" data-ng-show="protocol == 'openid-connect' && client.publicClient && !client.bearerOnly">
+ <label class="col-md-2 control-label" for="implicitFlowEnabled">{{:: 'implicit-flow-enabled' | translate}}</label>
+ <kc-tooltip>{{:: 'implicit-flow-enabled.tooltip' | translate}}</kc-tooltip>
+ <div class="col-md-6">
+ <input ng-model="client.implicitFlowEnabled" name="implicitFlowEnabled" id="implicitFlowEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+ </div>
+ </div>
+ <div class="form-group" data-ng-show="protocol == 'openid-connect' && !client.bearerOnly">
+ <label class="col-md-2 control-label" for="directAccessGrantsEnabled">{{:: 'direct-access-grants-enabled' | translate}}</label>
+ <kc-tooltip>{{:: 'direct-access-grants-enabled.tooltip' | translate}}</kc-tooltip>
+ <div class="col-md-6">
+ <input ng-model="client.directAccessGrantsEnabled" name="directAccessGrantsEnabled" id="directAccessGrantsEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+ </div>
+ </div>
<div class="form-group" data-ng-show="protocol == 'openid-connect' && !client.publicClient && !client.bearerOnly">
<label class="col-md-2 control-label" for="serviceAccountsEnabled">{{:: 'service-accounts-enabled' | translate}}</label>
<kc-tooltip>{{:: 'service-accounts-enabled.tooltip' | translate}}</kc-tooltip>
@@ -202,7 +216,7 @@
<kc-tooltip>{{:: 'root-url.tooltip' | translate}}</kc-tooltip>
</div>
- <div class="form-group clearfix block" data-ng-hide="client.bearerOnly || client.directGrantsOnly">
+ <div class="form-group clearfix block" data-ng-hide="client.bearerOnly || (!client.standardFlowEnabled && !client.implicitFlowEnabled)">
<label class="col-md-2 control-label" for="newRedirectUri"><span class="required" data-ng-show="protocol != 'saml'">*</span> {{:: 'valid-redirect-uris' | translate}}</label>
<div class="col-sm-6">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access.html
new file mode 100755
index 0000000..7b7c90e
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access.html
@@ -0,0 +1,52 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <kc-tabs-realm></kc-tabs-realm>
+
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="6">
+ <div class="form-inline">
+ <div class="form-group">
+ <div class="input-group">
+ <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.id" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ <div class="input-group-addon">
+ <i class="fa fa-search" type="submit"></i>
+ </div>
+ </div>
+ </div>
+
+ <div class="pull-right" data-ng-show="access.manageRealm">
+ <a id="createClient" class="btn btn-default" href="#/realms/{{realm.realm}}/client-initial-access/create">{{:: 'create' | translate}}</a>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-hide="clients.length == 0">
+ <th>{{:: 'id' | translate}}</th>
+ <th>{{:: 'created' | translate}}</th>
+ <th>{{:: 'expires' | translate}}</th>
+ <th>{{:: 'count' | translate}}</th>
+ <th>{{:: 'remainingCount' | translate}}</th>
+ <th>{{:: 'actions' | translate}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="ia in clientInitialAccess | filter:search | orderBy:'timestamp'">
+ <td>{{ia.id}}</td>
+ <td>{{(ia.timestamp * 1000)|date:'shortDate'}} {{(ia.timestamp * 1000)|date:'mediumTime'}}</td>
+ <td><span data-ng-show="ia.expiration > 0">{{((ia.timestamp + ia.expiration) * 1000)|date:'shortDate'}} {{((ia.timestamp + ia.expiration) * 1000)|date:'mediumTime'}}</span></td>
+ <td>{{ia.count}}</td>
+ <td>{{ia.remainingCount}}</td>
+ <td class="kc-action-cell">
+ <button class="btn btn-default btn-block btn-sm" data-ng-click="remove(ia.id)">{{:: 'delete' | translate}}</button>
+ </td>
+ </tr>
+ <tr data-ng-show="(clients | filter:search).length == 0">
+ <td class="text-muted" colspan="3" data-ng-show="search.clientId">{{:: 'no-results' | translate}}</td>
+ <td class="text-muted" colspan="3" data-ng-hide="search.clientId">{{:: 'no-clients-available' | translate}}</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access-create.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access-create.html
new file mode 100755
index 0000000..ef54939
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-initial-access-create.html
@@ -0,0 +1,63 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/client-initial-access">{{:: 'initial-access-tokens' | translate}}</a></li>
+ <li>{{:: 'add-initial-access-tokens' | translate}}</li>
+ </ol>
+
+ <h1 data-ng-show="create">{{:: 'add-client' | translate}}</h1>
+
+ <form class="form-horizontal" name="createForm" novalidate kc-read-only="!access.manageRealm" data-ng-hide="token">
+
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="expiration">{{:: 'expiration' | translate}}</label>
+
+ <div class="col-md-6 time-selector">
+ <input class="form-control" type="number" required min="0" max="31536000" data-ng-model="expiration" id="expiration"
+ name="expiration"/>
+ <select class="form-control" name="expirationUnit" data-ng-model="expirationUnit">
+ <option data-ng-selected="!expirationUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
+ </select>
+ </div>
+ <kc-tooltip>{{:: 'expiration.tooltip' | translate}}</kc-tooltip>
+ </div>
+
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="count">{{:: 'count' | translate}} </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="count" name="count" required min="1" max="100" data-ng-model="count">
+ </div>
+ <kc-tooltip>{{:: 'count.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>{{:: 'save' | translate}}</button>
+ <button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
+ </div>
+ </div>
+ </form>
+
+ <form name="displayForm" data-ng-show="token">
+ <div class="form-group">
+ <label for="initialAccessToken">{{:: 'initial-access-token' | translate}}</label>
+
+ <div>
+ <textarea type="text" id="initialAccessToken" name="initialAccessToken" class="form-control" rows="6" kc-select-action="click">{{token}}</textarea>
+ </div>
+
+ <kc-tooltip>{{:: 'initial-access.copyPaste.tooltip' | translate}}</kc-tooltip>
+ </div>
+
+ <div class="form-group">
+ <div>
+ <button class="btn btn-default" data-ng-click="done()">{{:: 'Back' | translate}}</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html
new file mode 100755
index 0000000..bcfd547
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/default-groups.html
@@ -0,0 +1,80 @@
+ <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <kc-tabs-group-list></kc-tabs-group-list>
+
+ <form class="form-horizontal" name="realmForm" novalidate>
+ <div class="form-group" kc-read-only="!access.manageRealm">
+ <label class="col-md-1 control-label" class="control-label"></label>
+
+ <div class="col-md-8" >
+ <div class="row">
+ <div class="col-md-5">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="5">
+ <div class="form-inline">
+ <label class="control-label">Default Groups</label>
+ <kc-tooltip>Newly created or registered users will automatically be added to these groups</kc-tooltip>
+
+ <div class="pull-right" data-ng-show="access.manageRealm">
+ <button id="removeDefaultGroup" class="btn btn-default" ng-click="removeDefaultGroup()">Remove</button>
+ </div>
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <select id="defaultGroups" class="form-control" size=5
+ ng-model="selectedGroup"
+ ng-options="r.path for r in defaultGroups">
+ <option style="display:none" value="">select a type</option>
+ </select>
+
+
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="col-md-5">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="5">
+
+ <div class="form-inline">
+ <label class="control-label">Available Groups</label>
+ <kc-tooltip>Select a group you want to add as a default.</kc-tooltip>
+
+ <div class="pull-right" data-ng-show="access.manageRealm">
+ <button id="addDefaultGroup" class="btn btn-default" ng-click="addDefaultGroup()">Add</button>
+ </div>
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td> <div
+ tree-id="tree"
+ angular-treeview="true"
+ tree-model="groupList"
+ node-id="id"
+ node-label="name"
+ node-children="subGroups" >
+ </div>
+
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+ </div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-attributes.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-attributes.html
index 9fcfc8b..3885f8c 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-attributes.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-attributes.html
@@ -20,14 +20,14 @@
<td>{{key}}</td>
<td><input ng-model="group.attributes[key]" class="form-control" type="text" name="{{key}}" id="attribute-{{key}}" /></td>
<td class="kc-action-cell">
- <button class="btn btn-default btn-block btn-sm" data-ng-click="removeAttribute(key)">Delete</button>
+ <button type="button" class="btn btn-default btn-block btn-sm" data-ng-click="removeAttribute(key)">Delete</button>
</td>
</tr>
<tr>
<td><input ng-model="newAttribute.key" class="form-control" type="text" id="newAttributeKey" /></td>
<td><input ng-model="newAttribute.value" class="form-control" type="text" id="newAttributeValue" /></td>
<td class="kc-action-cell">
- <button class="btn btn-default btn-block btn-sm" data-ng-click="addAttribute()">Add</button>
+ <button class="btn btn-default btn-block btn-sm" data-ng-click="addAttribute()" data-ng-disabled="!newAttribute.key.length || !newAttribute.value.length">Add</button>
</td>
</tr>
</tbody>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-list.html
index 14acbae..ac8267c 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-list.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-list.html
@@ -1,8 +1,5 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
- <h1>
- <span>User Groups</span>
- <kc-tooltip>User groups</kc-tooltip>
- </h1>
+ <kc-tabs-group-list></kc-tabs-group-list>
<table class="table table-striped table-bordered">
<thead>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-members.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-members.html
index 6c20930..50bc11b 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-members.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-members.html
@@ -14,7 +14,7 @@
<th>Last Name</th>
<th>First Name</th>
<th>Email</th>
- <th>Actions</th>
+ <th></th>
</tr>
</tr>
</thead>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/role-selector.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/role-selector.html
index 43b165b..531c3b2 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/role-selector.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/role-selector.html
@@ -15,7 +15,7 @@
ng-options="r.name for r in realmRoles | orderBy:'toString()'">
<option style="display:none" value="">Select a role</option>
</select>
- <button class="btn btn-default" type="submit" ng-click="selectRealmRole()" tooltip-trigger="mouseover mouseout" tooltip="Select realm role" tooltip-placement="right">
+ <button class="btn btn-default" type="submit" data-ng-disabled="!selectedRealmRole.role" ng-click="selectRealmRole()" tooltip-trigger="mouseover mouseout" tooltip="Select realm role" tooltip-placement="right">
Select Realm Role</i>
</button>
</div>
@@ -32,7 +32,7 @@
ng-options="r.name for r in clientRoles | orderBy:'toString()'">
<option style="display:none" value="">Select a role</option>
</select>
- <button class="btn btn-default" type="submit" ng-click="selectClientRole()" tooltip-trigger="mouseover mouseout" tooltip="Select client role" tooltip-placement="right">
+ <button class="btn btn-default" type="submit" data-ng-disabled="!selectedClientRole.role" ng-click="selectClientRole()" tooltip-trigger="mouseover mouseout" tooltip="Select client role" tooltip-placement="right">
Select Client Role
</button>
</div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html
index 1f8890e..4abe5a2 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/otp-policy.html
@@ -34,9 +34,7 @@
<label class="col-md-2 control-label" for="digits">Number of Digits</label>
<div class="col-md-2">
<div>
- <select id="digits" ng-model="realm.otpPolicyDigits" class="form-control">
- <option value="6">6</option>
- <option value="8">8</option>
+ <select id="digits" ng-model="realm.otpPolicyDigits" class="form-control" ng-options="item as item for item in optionsDigits">
</select>
</div>
</div>
@@ -51,18 +49,18 @@
<kc-tooltip>How far ahead should the server look just in case the token generator and server are out of time sync or counter sync?</kc-tooltip>
</div>
- <div class="form-group" data-ng-show="realm.otpPolicyType == 'hotp'">
+ <div class="form-group" data-ng-if="realm.otpPolicyType == 'hotp'">
<label class="col-md-2 control-label" for="counter">Initial Counter</label>
<div class="col-md-6">
- <input class="form-control" type="number" required min="1" max="120" id="counter" name="counter" data-ng-model="realm.otpPolicyInitialCounter" autofocus>
+ <input class="form-control" type="number" data-ng-required="realm.otpPolicyType == 'hotp'" min="1" max="120" id="counter" name="counter" data-ng-model="realm.otpPolicyInitialCounter" autofocus>
</div>
<kc-tooltip>What should the initial counter value be?</kc-tooltip>
</div>
- <div class="form-group" data-ng-show="realm.otpPolicyType == 'totp'">
+ <div class="form-group" data-ng-if="realm.otpPolicyType == 'totp'">
<label class="col-md-2 control-label" for="counter">OTP Token Period</label>
<div class="col-md-6">
- <input class="form-control" type="number" required min="1" max="120" id="period" name="period" data-ng-model="realm.otpPolicyPeriod">
+ <input class="form-control" type="number" data-ng-required="realm.otpPolicyType == 'totp'" min="1" max="120" id="period" name="period" data-ng-model="realm.otpPolicyPeriod">
</div>
<kc-tooltip>How many seconds should an OTP token be valid? Defaults to 30 seconds.</kc-tooltip>
</div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html
index 4e4224a..afc9a5f 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/protocol-mapper-detail.html
@@ -31,7 +31,7 @@
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="name">{{:: 'name' | translate}}</label>
<div class="col-md-6">
- <input class="form-control" id="name" type="text" ng-model="mapper.name" data-ng-readonly="!create">
+ <input class="form-control" id="name" type="text" ng-model="mapper.name" data-ng-readonly="!create" required>
</div>
<kc-tooltip>{{:: 'mapper.name.tooltip' | translate}}</kc-tooltip>
</div>
@@ -56,7 +56,8 @@
<div>
<select class="form-control" id="mapperTypeCreate"
ng-model="mapperType"
- ng-options="mapperType.name for mapperType in mapperTypes">
+ ng-options="mapperType.name for mapperType in mapperTypes"
+ required>
</select>
</div>
</div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
index dee13f6..bdfd390 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
@@ -83,6 +83,23 @@
</div>
<div class="form-group">
+ <label class="col-md-2 control-label" for="accessTokenLifespanForImplicitFlow">{{:: 'access-token-lifespan-for-implicit-flow' | translate}}</label>
+
+ <div class="col-md-6 time-selector">
+ <input class="form-control" type="number" required min="1"
+ max="31536000" data-ng-model="realm.accessTokenLifespanForImplicitFlow"
+ id="accessTokenLifespanForImplicitFlow" name="accessTokenLifespanForImplicitFlow"/>
+ <select class="form-control" name="accessTokenLifespanForImplicitFlowUnit" data-ng-model="realm.accessTokenLifespanForImplicitFlowUnit">
+ <option data-ng-selected="!realm.accessTokenLifespanForImplicitFlowUnit" value="Seconds">{{:: 'seconds' | translate}}</option>
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
+ </select>
+ </div>
+ <kc-tooltip>{{:: 'access-token-lifespan-for-implicit-flow.tooltip' | translate}}</kc-tooltip>
+ </div>
+
+ <div class="form-group">
<label class="col-md-2 control-label" for="accessCodeLifespan">{{:: 'client-login-timeout' | translate}}</label>
<div class="col-md-6 time-selector">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-attributes.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-attributes.html
index 9713a80..df969fb 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-attributes.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-attributes.html
@@ -20,14 +20,14 @@
<td>{{key}}</td>
<td><input ng-model="user.attributes[key]" class="form-control" type="text" name="{{key}}" id="attribute-{{key}}" /></td>
<td class="kc-action-cell">
- <button class="btn btn-default btn-block btn-sm" data-ng-click="removeAttribute(key)">Delete</button>
+ <button type="button" class="btn btn-default btn-block btn-sm" data-ng-click="removeAttribute(key)">Delete</button>
</td>
</tr>
<tr>
<td><input ng-model="newAttribute.key" class="form-control" type="text" id="newAttributeKey" /></td>
<td><input ng-model="newAttribute.value" class="form-control" type="text" id="newAttributeValue" /></td>
<td class="kc-action-cell">
- <button class="btn btn-default btn-block btn-sm" data-ng-click="addAttribute()">Add</button>
+ <button class="btn btn-default btn-block btn-sm" data-ng-click="addAttribute()" data-ng-disabled="!newAttribute.key.length || !newAttribute.value.length">Add</button>
</td>
</tr>
</tbody>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-group-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-group-list.html
new file mode 100755
index 0000000..390dce3
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-group-list.html
@@ -0,0 +1,11 @@
+<div data-ng-controller="GroupTabCtrl">
+ <h1>
+ <span>User Groups</span>
+ </h1>
+
+ <ul class="nav nav-tabs">
+ <li ng-class="{active: path[2] == 'groups'}"><a href="#/realms/{{realm.realm}}/groups">Groups</a></li>
+ <li ng-class="{active: path[2] == 'default-groups'}"><a href="#/realms/{{realm.realm}}/default-groups">Default Groups</a><kc-tooltip>Set of groups that new users will automatically join.</kc-tooltip>
+ </li>
+ </ul>
+</div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html
index 76f01ea..5d14503 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html
@@ -13,6 +13,7 @@
<li ng-class="{active: path[2] == 'theme-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/theme-settings">{{:: 'realm-tab-themes' | translate}}</a></li>
<li ng-class="{active: path[2] == 'cache-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/cache-settings">{{:: 'realm-tab-cache' | translate}}</a></li>
<li ng-class="{active: path[2] == 'token-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/token-settings">{{:: 'realm-tab-tokens' | translate}}</a></li>
+ <li ng-class="{active: path[2] == 'client-initial-access'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/client-initial-access">{{:: 'realm-tab-client-initial-access' | translate}}</a></li>
<li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'realm-tab-security-defenses' | translate}}</a></li>
</ul>
</div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_ca.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_ca.properties
new file mode 100644
index 0000000..55ccb7b
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_ca.properties
@@ -0,0 +1,206 @@
+doLogIn=Inicia sessi\u00F3
+doRegister=Registra''t
+doCancel=Cancel\u00B7lar
+doSubmit=Envia
+doYes=S\u00ED
+doNo=No
+doContinue=Continua
+doAccept=Accepta
+doDecline=Rebutja
+doForgotPassword=Has oblidat la teva contrasenya?
+doClickHere=Fes clic aqu\u00ED
+doImpersonate=Personifica
+kerberosNotConfigured=Kerberos no configurat
+kerberosNotConfiguredTitle=Kerberos no configurat
+bypassKerberosDetail=O b\u00E9 no est\u00E0s identificat mitjan\u00E7ant Kerberos o el teu navegador no est\u00E0 configurat per identificar-se mitjan\u00E7ant Kerberos. Si us plau fes clic per identificar-te per un altre mitj\u00E0.
+kerberosNotSetUp=Kerberos no est\u00E0 configurat. No pots identificar-te.
+registerWithTitle=Registra''t amb {0}
+registerWithTitleHtml=Registra''t amb <strong>{0}</strong>
+loginTitle=Inicia sessi\u00F3 a {0}
+loginTitleHtml=Inicia sessi\u00F3 a <strong>{0}</strong>
+impersonateTitle={0}\u00A0Personifica Usuari
+impersonateTitleHtml=<strong>{0}</strong> Personifica Usuari</strong>
+realmChoice=Domini
+unknownUser=Usuari desconegut
+loginTotpTitle=Configura la teva aplicaci\u00F3 d''identificaci\u00F3 m\u00F2bil
+loginProfileTitle=Actualitza la informaci\u00F3 del teu compte
+loginTimeout=Has trigat massa a identificar-te. Inicia de nou la identificaci\u00F3.
+oauthGrantTitle=Concessi\u00F3 OAuth
+oauthGrantTitleHtml=Acc\u00E9s temporal per <strong>{0}</strong> sol\u00B7licitat per
+errorTitle=Ho sentim...
+errorTitleHtml=Ho <strong>sentim</strong>...
+emailVerifyTitle=Verificaci\u00F3 de l''email
+emailForgotTitle=Has oblidat la teva contrasenya?
+updatePasswordTitle=Modificaci\u00F3 de contrasenya
+codeSuccessTitle=Codi d''\u00E8xit
+codeErrorTitle=Codi d''error: {0}
+
+termsTitle=Termes i Condicions
+termsTitleHtml=Termes i Condicions
+termsText=<p>Termes i condicions a definir</p>
+
+recaptchaFailed=Reconeixement de text inv\u00E0lid
+recaptchaNotConfigured=El reconeixement de text \u00E9s obligatori per\u00F2 no est\u00E0 configurat
+consentDenied=Consentiment rebutjat.
+
+noAccount=Usuari nou?
+username=Usuari
+usernameOrEmail=Usuari o email
+firstName=Nom
+givenName=Nom de pila
+fullName=Nom complet
+lastName=Cognoms
+familyName=Cognoms
+email=Email
+password=Contrasenya
+passwordConfirm=Confirma la contrasenya
+passwordNew=Nova contrasenya
+passwordNewConfirm=Confirma la nova contrasenya
+rememberMe=Seguir connectat
+authenticatorCode=Codi d''identificaci\u00F3
+address=Adre\u00E7a
+street=Carrer
+locality=Ciutat o Municipi
+region=Estat, Prov\u00EDncia, o Regi\u00F3
+postal_code=Codi Postal
+country=Pa\u00EDs
+emailVerified=Email verificat
+gssDelegationCredential=GSS Delegation Credential
+
+loginTotpStep1=Instal\u00B7la <a href=\"https://fedorahosted.org/freeotp/\" target=\"_blank\">FreeOTP</a> o Google Authenticator al teu tel\u00E8fon m\u00F2bil. Les dues aplicacions estan disponibles a <a href=\"https://play.google.com\">Google Play</a> i en l''App Store d''Apple.
+loginTotpStep2=Obre l''aplicaci\u00F3 i escaneja el codi o introdueix la clau.
+loginTotpStep3=Introdueix el codi \u00FAnic que et mostra l''aplicaci\u00F3 d''autenticaci\u00F3 i fes clic a Envia per finalitzar la configuraci\u00F3
+loginTotpOneTime=Codi d''un sol \u00FAs
+
+oauthGrantRequest=Vols permetre aquests privilegis d''acc\u00E9s?
+inResource=a
+
+emailVerifyInstruction1=T''hem enviat un email amb instruccions per verificar el teu email.
+emailVerifyInstruction2=No has rebut un codi de verificaci\u00F3 al teu email?
+emailVerifyInstruction3=per reenviar l''email.
+
+backToLogin=« Torna a la identificaci\u00F3
+
+emailInstruction=Indica el teu usuari o email i t''enviarem instruccions indicant com generar una nova contrasenya.
+
+copyCodeInstruction=Si us plau, copia i enganxa aquest codi a la teva aplicaci\u00F3:
+
+personalInfo=Informaci\u00F3 personal:
+role_admin=Admin
+role_realm-admin=Administrador del domini
+role_create-realm=Crear domini
+role_create-client=Crear client
+role_view-realm=Veure domini
+role_view-users=Veure usuaris
+role_view-applications=Veure aplicacions
+role_view-clients=Veure clients
+role_view-events=Veure events
+role_view-identity-providers=Veure prove\u00EFdors d''identitat
+role_manage-realm=Gestionar domini
+role_manage-users=Gestionar usuaris
+role_manage-applications=Gestionar aplicacions
+role_manage-identity-providers=Gestionar prove\u00EFdors d''identitat
+role_manage-clients=Gestionar clients
+role_manage-events=Gestionar events
+role_view-profile=Veure perfil
+role_manage-account=Gestionar compte
+role_read-token=Llegir token
+role_offline-access=Acc\u00E9s sense connexi\u00F3
+client_account=Compte
+client_security-admin-console=Consola d''Administraci\u00F3 de Seguretat
+client_realm-management=Gesti\u00F3 del domini
+client_broker=Broker
+
+invalidUserMessage=Usuari o contrasenya incorrectes.
+invalidEmailMessage=Email no v\u00E0lid
+accountDisabledMessage=El compte est\u00E0 desactivat, contacta amb l''administrador.
+accountTemporarilyDisabledMessage=El compte est\u00E0 temporalment desactivat, contacta amb l''administrador o intenta-ho de nou m\u00E9s tard.
+expiredCodeMessage=S''ha esgotat el temps m\u00E0xim per a la identificaci\u00F3. Si us plau identifica''t de nou.
+
+missingFirstNameMessage=Si us plau indica el teu nom.
+missingLastNameMessage=Si us plau indica els teus cognoms.
+missingEmailMessage=Si us plau indica el teu email.
+missingUsernameMessage=Si us plau indica el teu usuari.
+missingPasswordMessage=Si us plau indica la teva contrasenya.
+missingTotpMessage=Si us plau indica el teu codi d''autenticaci\u00F3
+notMatchPasswordMessage=Les contrasenyes no coincideixen.
+
+invalidPasswordExistingMessage=La contrasenya actual no \u00E9s correcta.
+invalidPasswordConfirmMessage=La confirmaci\u00F3 de contrasenya no coincideix.
+invalidTotpMessage=El codi d''autenticaci\u00F3 no \u00E9s v\u00E0lid.
+
+usernameExistsMessage=El nom d''usuari ja existeix
+emailExistsMessage=L''email ja existeix
+
+federatedIdentityEmailExistsMessage=Ja existeix un usuari amb aquest email. Si us plau accedeix a la gesti\u00F3 del teu compte per enlla\u00E7ar-lo.
+federatedIdentityUsernameExistsMessage=Ja existeix un usuari amb aquest nom d''usuari. Si us plau accedeix a la gesti\u00F3 del teu compte per enlla\u00E7ar-lo.
+
+configureTotpMessage=Has de configurar l''aplicaci\u00F3 m\u00F2bil 'd'identificaci\u00F3 per activar el teu compte.
+updateProfileMessage=Has d''actualitzar el teu perfil d''usuari per activar el teu compte.
+updatePasswordMessage=Has de canviar la contrasenya per activar el teu compte.
+verifyEmailMessage=Has de verificar el teu email per activar el teu compte.
+
+emailSentMessage=En breu hauries de rebre un missatge amb m\u00E9s instruccions
+emailSendErrorMessage=Ha fallat l''enviament de l''email, si us plau intenta-ho de nou m\u00E9s tard.
+
+accountUpdatedMessage=El teu compte s''ha actualitzat.
+accountPasswordUpdatedMessage=La contrasenya s''ha actualitzat.
+
+noAccessMessage=Sense acc\u00E9s
+
+invalidPasswordMinLengthMessage=Contrasenya incorrecta: longitud m\u00EDnima {0}.
+invalidPasswordMinDigitsMessage=Contrasenya incorrecta: ha de contenir almenys {0} car\u00E0cters num\u00E8rics.
+invalidPasswordMinLowerCaseCharsMessage=Contrasenya incorrecta: ha de contenir almenys {0} lletres min\u00FAscules.
+invalidPasswordMinUpperCaseCharsMessage=Contrasenya incorrecta: ha de contenir almenys {0} lletres maj\u00FAscules.
+invalidPasswordMinSpecialCharsMessage=Contrasenya incorrecta: ha de contenir almenys {0} car\u00E0cters especials.
+invalidPasswordNotUsernameMessage=Contrasenya incorrecta: no pot ser igual al nom d''usuari.
+invalidPasswordRegexPatternMessage=Contrasenya incorrecta: no compleix l''expressi\u00F3 regular.
+invalidPasswordHistoryMessage=Contrasenya incorrecta: no pot ser igual a cap de les \u00FAltimes {0} contrasenyes.
+
+failedToProcessResponseMessage=Fallada en processar la resposta
+httpsRequiredMessage=HTTPS obligatori
+realmNotEnabledMessage=El domini no est\u00E0 activat
+invalidRequestMessage=Petici\u00F3 incorrecta
+failedLogout=Ha fallat la desconnexi\u00F3.
+unknownLoginRequesterMessage=Sol\u00B7licitant d''identificaci\u00F3 desconegut
+loginRequesterNotEnabledMessage=El sol\u00B7licitant d''inici de sessi\u00F3 est\u00E0 desactivat
+bearerOnlyMessage=Les aplicacions Bearer-only no poden iniciar sessi\u00F3 des del navegador.
+directGrantsOnlyMessage=Els clients de tipus Direct-grants-only no poden iniciar sessi\u00F3 des del navegador.
+invalidRedirectUriMessage=L''URI de redirecci\u00F3 no \u00E9s correcta
+unsupportedNameIdFormatMessage=NameIDFormat no suportat
+invlidRequesterMessage=Sol\u00B7licitant no v\u00E0lid
+registrationNotAllowedMessage=El registre no est\u00E0 perm\u00E8s
+resetCredentialNotAllowedMessage=El reinici de les credencials no est\u00E0 perm\u00E8s
+
+permissionNotApprovedMessage=Perm\u00EDs no aprovat.
+noRelayStateInResponseMessage=Sense estat de retransmissi\u00F3 en la resposta del prove\u00EFdor d''identitat.
+identityProviderAlreadyLinkedMessage=La identitat retornada pel prove\u00EFdor d''identitat ja est\u00E0 associada a un altre usuari.
+insufficientPermissionMessage=Permisos insuficients per enlla\u00E7ar identitats.
+couldNotProceedWithAuthenticationRequestMessage=No s''ha pogut continuar amb la petici\u00F3 d''autenticaci\u00F3 al prove\u00EFdor d''identitat.
+couldNotObtainTokenMessage=No s''ha pogut obtenir el codi del prove\u00EFdor d''identitat.
+unexpectedErrorRetrievingTokenMessage=Error inesperat obtenint el token del prove\u00EFdor d''identitat
+unexpectedErrorHandlingResponseMessage=Error inesperat processant la resposta del prove\u00EFdor d''identitat.
+identityProviderAuthenticationFailedMessage=Ha fallat l''autenticaci\u00F3. No ha estat possible autenticar-se en el prove\u00EFdor d''identitat.
+couldNotSendAuthenticationRequestMessage=No s''ha pogut enviar la petici\u00F3 d''identificaci\u00F3 al prove\u00EFdor d''identitat.
+unexpectedErrorHandlingRequestMessage=Error inesperat durant la petici\u00F3 d''identificaci\u00F3 al prove\u00EFdor d''identitat.
+invalidAccessCodeMessage=Codi d''acc\u00E9s no v\u00E0lid.
+sessionNotActiveMessage=La sessi\u00F3 no est\u00E0 activa
+invalidCodeMessage=Hi ha hagut un error, si us plau identifica''t de nou des de la teva aplicaci\u00F3.
+identityProviderUnexpectedErrorMessage=Error no esperat intentant autenticar en el prove\u00EFdor d''identitat.
+identityProviderNotFoundMessage=No s''ha trobat cap prove\u00EFdor d''identitat.
+realmSupportsNoCredentialsMessage=El domini no suporta cap tipus de credencials.
+identityProviderNotUniqueMessage=El domini suporta m\u00FAltiples prove\u00EFdors d''identitat. No s''ha pogut determinar el prove\u00EFdor d''identitat que hauria de ser utilitzat per identificar-se.
+emailVerifiedMessage=El teu email ha estat verificat.
+
+locale_de=German
+locale_en=English
+locale_it=Italian
+locale_pt-BR=Portugu\u00EAs (Brasil)
+locale_fr=Français
+locale_es=Espa\u00F1ol
+locale_ca=Catal\u00E0
+
+backToApplication=« Torna a l''aplicaci\u00F3
+missingParameterMessage=Par\u00E0metres que falten: {0}
+clientNotFoundMessage=Client no trobat
+invalidParameterMessage=Par\u00E0metre no v\u00E0lid: {0}
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 6885308..ffe8328 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -176,7 +176,8 @@ failedLogout=Logout failed
unknownLoginRequesterMessage=Unknown login requester
loginRequesterNotEnabledMessage=Login requester not enabled
bearerOnlyMessage=Bearer-only applications are not allowed to initiate browser login
-directGrantsOnlyMessage=Direct-grants-only clients are not allowed to initiate browser login
+standardFlowDisabledMessage=Client is not allowed to initiate browser login with given response_type. Standard flow is disabled for the client.
+implicitFlowDisabledMessage=Client is not allowed to initiate browser login with given response_type. Implicit flow is disabled for the client.
invalidRedirectUriMessage=Invalid redirect uri
unsupportedNameIdFormatMessage=Unsupported NameIDFormat
invlidRequesterMessage=Invalid requester
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_es.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_es.properties
index ff43eed..f497d07 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_es.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_es.properties
@@ -198,6 +198,7 @@ locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil)
locale_fr=Fran\u00C3\u00A7ais
locale_es=Espa\u00F1ol
+locale_ca=Catal\u00E0
backToApplication=« Volver a la aplicaci\u00F3n
missingParameterMessage=Par\u00E1metros que faltan: {0}
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_ca.properties b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_ca.properties
new file mode 100644
index 0000000..7f8504a
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_ca.properties
@@ -0,0 +1,21 @@
+emailVerificationSubject=Verificaci\u00F3 d''email
+emailVerificationBody=Alg\u00FA ha creat un compte de {2} amb aquesta adre\u00E7a de correu electr\u00F2nic. Si has estat tu, fes clic a l''enlla\u00E7 seg\u00FCent per verificar la teva adre\u00E7a de correu electr\u00F2nic.\n\n{0}\n\nAquest enlla\u00E7 expirar\u00E0 en {1} minuts.\n\nSi tu no has creat aquest compte, simplement ignora aquest missatge.
+emailVerificationBodyHtml=<p>Alg\u00FA ha creat un compte de {2} amb aquesta adre\u00E7a de correu electr\u00F2nic. Si has estat tu, fes clic a l''enlla\u00E7 seg\u00FCent per verificar la teva adre\u00E7a de correu electr\u00F2nic.</p><p><a href=\"{0}\">{0}</a></p><p> Aquest enlla\u00E7 expirar\u00E0 en {1} minuts.</p><p> Si tu no has creat aquest compte, simplement ignora aquest missatge.</p>
+passwordResetSubject=Reinicia contrasenya
+passwordResetBody=Alg\u00FA ha demanat de canviar les credencials del teu compte de {2}. Si has estat tu, fes clic a l''enlla\u00E7 seg\u00FCent per a reiniciar-les.\n\n{0}\n\nAquest enlla\u00E7 expirar\u00E0 en {1} minuts.\n\nSi no vols reiniciar les teves credencials, simplement ignora aquest missatge i no es realitzar\u00E0 cap canvi.
+passwordResetBodyHtml=<p>Alg\u00FA ha demanat de canviar les credencials del teu compte de {2}. Si has estat tu, fes clic a l''enlla\u00E7 seg\u00FCent per a reiniciar-les.</p><p><a href=\"{0}\">{0}</a></p><p>Aquest enlla\u00E7 expirar\u00E0 en {1} minuts.</p><p>Si no vols reiniciar les teves credencials, simplement ignora aquest missatge i no es realitzar\u00E0 cap canvi.</p>
+executeActionsSubject=Actualitza el teu compte
+executeActionsBody=L''administrador ha sol\u00B7licitat que actualitzis el teu compte de {2}. Fes clic a l''enlla\u00E7 inferior per iniciar aquest proc\u00E9s.\n\n{0}\n\nAquest enlla\u00E7 expirar\u00E0 en {1} minutes.\n\nSi no est\u00E0s al tant que l''administrador hagi sol\u00B7licitat aix\u00F2, simplement ignora aquest missatge i no es realitzar\u00E0 cap canvi.
+executeActionsBodyHtml=<p>L''administrador ha sol\u00B7licitat que actualitzis el teu compte de {2}. Fes clic a l''enlla\u00E7 inferior per iniciar aquest proc\u00E9s.</p><p><a href=\"{0}\">{0}</a></p><p>Aquest enlla\u00E7 expirar\u00E0 en {1} minutes.</p><p>Si no est\u00E0s al tant que l''administrador hagi sol\u00B7licitat aix\u00F2, simplement ignora aquest missatge i no es realitzar\u00E0 cap canvi.</p>
+eventLoginErrorSubject=Fallada en l''inici de sessi\u00F3
+eventLoginErrorBody=S''ha detectat un intent d''acc\u00E9s fallit al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l''administrador.
+eventLoginErrorBodyHtml=<p>S''ha detectat un intent d''acc\u00E9s fallit al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l''administrador.</p>
+eventRemoveTotpSubject=Esborrat TOTP
+eventRemoveTotpBody=TOTP s''ha eliminat del teu compte el {0} des de {1}. Si no has estat tu, per favor contacta amb l''administrador.
+eventRemoveTotpBodyHtml=<p>TOTP s''ha eliminat del teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l''administrador. </ P>
+eventUpdatePasswordSubject=Actualitzaci\u00F3 de contrasenya
+eventUpdatePasswordBody=La teva contrasenya s''ha actualitzat el {0} des de {1}. Si no has estat tu, si us plau contacta amb l''administrador.
+eventUpdatePasswordBodyHtml=<p>La teva contrasenya s''ha actualitzat el {0} des de {1}. Si no has estat tu, si us plau contacta amb l''administrador.</p>
+eventUpdateTotpSubject=Actualitzaci\u00F3 de TOTP
+eventUpdateTotpBody=TOTP s''ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l''administrador.
+eventUpdateTotpBodyHtml=<p>TOTP s''ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l''administrador.</p>
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css b/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css
index 55939b6..8f09a97 100644
--- a/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css
+++ b/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css
@@ -5,6 +5,7 @@
.kc-dropdown{
position: relative;
+ z-index: 9999;
}
.kc-dropdown > a{
position: absolute;
diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailSenderProvider.java b/forms/email-api/src/main/java/org/keycloak/email/EmailSenderProvider.java
new file mode 100755
index 0000000..95ccabe
--- /dev/null
+++ b/forms/email-api/src/main/java/org/keycloak/email/EmailSenderProvider.java
@@ -0,0 +1,14 @@
+package org.keycloak.email;
+
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface EmailSenderProvider extends Provider {
+
+ void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException;
+
+}
diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailTemplateProviderFactory.java b/forms/email-api/src/main/java/org/keycloak/email/EmailTemplateProviderFactory.java
new file mode 100644
index 0000000..bc934cb
--- /dev/null
+++ b/forms/email-api/src/main/java/org/keycloak/email/EmailTemplateProviderFactory.java
@@ -0,0 +1,9 @@
+package org.keycloak.email;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface EmailTemplateProviderFactory extends ProviderFactory<EmailTemplateProvider> {
+}
diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailTemplateSpi.java b/forms/email-api/src/main/java/org/keycloak/email/EmailTemplateSpi.java
new file mode 100644
index 0000000..c84c87c
--- /dev/null
+++ b/forms/email-api/src/main/java/org/keycloak/email/EmailTemplateSpi.java
@@ -0,0 +1,31 @@
+package org.keycloak.email;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class EmailTemplateSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "emailTemplate";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return EmailTemplateProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return org.keycloak.email.EmailTemplateProviderFactory.class;
+ }
+}
diff --git a/forms/email-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/forms/email-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 4110dba..80967a2 100644
--- a/forms/email-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/forms/email-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -1 +1,2 @@
-org.keycloak.email.EmailSpi
\ No newline at end of file
+org.keycloak.email.EmailSenderSpi
+org.keycloak.email.EmailTemplateSpi
\ No newline at end of file
diff --git a/forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailTemplateProviderFactory b/forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailTemplateProviderFactory
new file mode 100644
index 0000000..aa87e53
--- /dev/null
+++ b/forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailTemplateProviderFactory
@@ -0,0 +1 @@
+org.keycloak.email.freemarker.FreeMarkerEmailTemplateProviderFactory
\ No newline at end of file
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
index d1b4df9..1cbd328 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -24,7 +24,7 @@ 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.EmailProvider;
+import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
@@ -153,7 +153,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
- session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendVerifyEmail(link, expiration);
+ 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();
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java
index 5e243de..8083654 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java
@@ -57,7 +57,7 @@ public class AuthenticatedActionsHandler {
protected boolean abortTokenResponse() {
if (facade.getSecurityContext() == null) {
log.debugv("Not logged in, sending back 401: {0}",facade.getRequest().getURI());
- facade.getResponse().setStatus(401);
+ facade.getResponse().sendError(401);
facade.getResponse().end();
return true;
}
@@ -94,7 +94,7 @@ public class AuthenticatedActionsHandler {
log.debugv("allowedOrigins did not contain origin");
}
- facade.getResponse().setStatus(403);
+ facade.getResponse().sendError(403);
facade.getResponse().end();
return true;
}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java
old mode 100644
new mode 100755
index 6b90105..7f260ab
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java
@@ -33,7 +33,7 @@ public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticat
public AuthOutcome authenticate(HttpFacade exchange) {
List<String> authHeaders = exchange.getRequest().getHeaders("Authorization");
if (authHeaders == null || authHeaders.size() == 0) {
- challenge = challengeResponse(exchange, null, null);
+ challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.NO_AUTHORIZATION_HEADER, null, null);
return AuthOutcome.NOT_ATTEMPTED;
}
@@ -46,7 +46,7 @@ public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticat
}
if (tokenString == null) {
- challenge = challengeResponse(exchange, null, null);
+ challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, null, null);
return AuthOutcome.NOT_ATTEMPTED;
}
@@ -59,7 +59,7 @@ public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticat
tokenString = atr.getToken();
} catch (Exception e) {
log.debug("Failed to obtain token", e);
- challenge = challengeResponse(exchange, "no_token", e.getMessage());
+ challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "no_token", e.getMessage());
return AuthOutcome.FAILED;
}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java
index 92b03da..686b802 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java
@@ -45,7 +45,7 @@ public class BearerTokenRequestAuthenticator {
public AuthOutcome authenticate(HttpFacade exchange) {
List<String> authHeaders = exchange.getRequest().getHeaders("Authorization");
if (authHeaders == null || authHeaders.size() == 0) {
- challenge = challengeResponse(exchange, null, null);
+ challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.NO_BEARER_TOKEN, null, null);
return AuthOutcome.NOT_ATTEMPTED;
}
@@ -58,7 +58,7 @@ public class BearerTokenRequestAuthenticator {
}
if (tokenString == null) {
- challenge = challengeResponse(exchange, null, null);
+ challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.NO_BEARER_TOKEN, null, null);
return AuthOutcome.NOT_ATTEMPTED;
}
@@ -70,12 +70,12 @@ public class BearerTokenRequestAuthenticator {
token = RSATokenVerifier.verifyToken(tokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl());
} catch (VerificationException e) {
log.error("Failed to verify token", e);
- challenge = challengeResponse(exchange, "invalid_token", e.getMessage());
+ challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "invalid_token", e.getMessage());
return AuthOutcome.FAILED;
}
if (token.getIssuedAt() < deployment.getNotBefore()) {
log.error("Stale token");
- challenge = challengeResponse(exchange, "invalid_token", "Stale token");
+ challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.STALE_TOKEN, "invalid_token", "Stale token");
return AuthOutcome.FAILED;
}
boolean verifyCaller = false;
@@ -113,11 +113,6 @@ public class BearerTokenRequestAuthenticator {
protected AuthChallenge clientCertChallenge() {
return new AuthChallenge() {
@Override
- public boolean errorPage() {
- return false;
- }
-
- @Override
public int getResponseCode() {
return 0;
}
@@ -131,7 +126,7 @@ public class BearerTokenRequestAuthenticator {
}
- protected AuthChallenge challengeResponse(HttpFacade facade, String error, String description) {
+ protected AuthChallenge challengeResponse(HttpFacade facade, final OIDCAuthenticationError.Reason reason, final String error, final String description) {
StringBuilder header = new StringBuilder("Bearer realm=\"");
header.append(deployment.getRealm()).append("\"");
if (error != null) {
@@ -143,19 +138,16 @@ public class BearerTokenRequestAuthenticator {
final String challenge = header.toString();
return new AuthChallenge() {
@Override
- public boolean errorPage() {
- return true;
- }
-
- @Override
public int getResponseCode() {
return 401;
}
@Override
public boolean challenge(HttpFacade facade) {
- facade.getResponse().setStatus(401);
+ OIDCAuthenticationError error = new OIDCAuthenticationError(reason, description);
+ facade.getRequest().setError(error);
facade.getResponse().addHeader("WWW-Authenticate", challenge);
+ facade.getResponse().sendError(401);
return true;
}
};
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java
index b1bd124..b385592 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java
@@ -9,6 +9,7 @@ import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.common.VerificationException;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.common.util.KeycloakUriBuilder;
@@ -58,10 +59,10 @@ public class CookieTokenStore {
AccessToken accessToken = RSATokenVerifier.verifyToken(accessTokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl(), false, true);
IDToken idToken;
if (idTokenString != null && idTokenString.length() > 0) {
- JWSInput input = new JWSInput(idTokenString);
try {
+ JWSInput input = new JWSInput(idTokenString);
idToken = input.readJsonContent(IDToken.class);
- } catch (IOException e) {
+ } catch (JWSInputException e) {
throw new VerificationException(e);
}
} else {
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
index 908d239..cc6b188 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java
@@ -11,6 +11,7 @@ import org.keycloak.common.VerificationException;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.enums.TokenStore;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
@@ -174,32 +175,11 @@ public class OAuthRequestAuthenticator {
final String state = getStateCode();
final String redirect = getRedirectUri(state);
if (redirect == null) {
- return new AuthChallenge() {
- @Override
- public boolean challenge(HttpFacade exchange) {
- exchange.getResponse().setStatus(403);
- return true;
- }
-
- @Override
- public boolean errorPage() {
- return true;
- }
-
- @Override
- public int getResponseCode() {
- return 403;
- }
- };
+ return challenge(403, OIDCAuthenticationError.Reason.NO_REDIRECT_URI, null);
}
return new AuthChallenge() {
@Override
- public boolean errorPage() {
- return false;
- }
-
- @Override
public int getResponseCode() {
return 0;
}
@@ -221,7 +201,7 @@ public class OAuthRequestAuthenticator {
if (stateCookie == null) {
log.warn("No state cookie");
- return challenge(400);
+ return challenge(400, OIDCAuthenticationError.Reason.INVALID_STATE_COOKIE, null);
}
// reset the cookie
log.debug("** reseting application state cookie");
@@ -231,13 +211,13 @@ public class OAuthRequestAuthenticator {
String state = getQueryParamValue(OAuth2Constants.STATE);
if (state == null) {
log.warn("state parameter was null");
- return challenge(400);
+ return challenge(400, OIDCAuthenticationError.Reason.INVALID_STATE_COOKIE, null);
}
if (!state.equals(stateCookieValue)) {
log.warn("state parameter invalid");
log.warn("cookie: " + stateCookieValue);
log.warn("queryParam: " + state);
- return challenge(400);
+ return challenge(400, OIDCAuthenticationError.Reason.INVALID_STATE_COOKIE, null);
}
return null;
@@ -251,7 +231,7 @@ public class OAuthRequestAuthenticator {
if (error != null) {
// todo how do we send a response?
log.warn("There was an error: " + error);
- challenge = challenge(400);
+ challenge = challenge(400, OIDCAuthenticationError.Reason.OAUTH_ERROR, error);
return AuthOutcome.FAILED;
} else {
log.debug("redirecting to auth server");
@@ -269,21 +249,18 @@ public class OAuthRequestAuthenticator {
}
- protected AuthChallenge challenge(final int code) {
+ protected AuthChallenge challenge(final int code, final OIDCAuthenticationError.Reason reason, final String description) {
return new AuthChallenge() {
@Override
- public boolean errorPage() {
- return true;
- }
-
- @Override
public int getResponseCode() {
return code;
}
@Override
public boolean challenge(HttpFacade exchange) {
- exchange.getResponse().setStatus(code);
+ OIDCAuthenticationError error = new OIDCAuthenticationError(reason, description);
+ exchange.getRequest().setError(error);
+ exchange.getResponse().sendError(code);
return true;
}
};
@@ -305,7 +282,7 @@ public class OAuthRequestAuthenticator {
// abort if not HTTPS
if (!isRequestSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
log.error("Adapter requires SSL. Request: " + facade.getRequest().getURI());
- return challenge(403);
+ return challenge(403, OIDCAuthenticationError.Reason.SSL_REQUIRED, null);
}
log.debug("checking state cookie for after code");
@@ -324,11 +301,11 @@ public class OAuthRequestAuthenticator {
if (failure.getStatus() == 400 && failure.getError() != null) {
log.error(" " + failure.getError());
}
- return challenge(403);
+ return challenge(403, OIDCAuthenticationError.Reason.CODE_TO_TOKEN_FAILURE, null);
} catch (IOException e) {
log.error("failed to turn code into token", e);
- return challenge(403);
+ return challenge(403, OIDCAuthenticationError.Reason.CODE_TO_TOKEN_FAILURE, null);
}
tokenString = tokenResponse.getToken();
@@ -337,24 +314,24 @@ public class OAuthRequestAuthenticator {
try {
token = RSATokenVerifier.verifyToken(tokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl());
if (idTokenString != null) {
- JWSInput input = new JWSInput(idTokenString);
try {
+ JWSInput input = new JWSInput(idTokenString);
idToken = input.readJsonContent(IDToken.class);
- } catch (IOException e) {
+ } catch (JWSInputException e) {
throw new VerificationException();
}
}
log.debug("Token Verification succeeded!");
} catch (VerificationException e) {
log.error("failed verification of token: " + e.getMessage());
- return challenge(403);
+ return challenge(403, OIDCAuthenticationError.Reason.INVALID_TOKEN, null);
}
if (tokenResponse.getNotBeforePolicy() > deployment.getNotBefore()) {
deployment.setNotBefore(tokenResponse.getNotBeforePolicy());
}
if (token.getIssuedAt() < deployment.getNotBefore()) {
log.error("Stale token");
- return challenge(403);
+ return challenge(403, OIDCAuthenticationError.Reason.STALE_TOKEN, null);
}
log.debug("successful authenticated");
return null;
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java
new file mode 100755
index 0000000..089b689
--- /dev/null
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/OIDCAuthenticationError.java
@@ -0,0 +1,46 @@
+package org.keycloak.adapters;
+
+import org.keycloak.adapters.spi.AuthenticationError;
+
+/**
+ * Object that describes the OIDC error that happened.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class OIDCAuthenticationError implements AuthenticationError {
+ public static enum Reason {
+ NO_BEARER_TOKEN,
+ NO_REDIRECT_URI,
+ INVALID_STATE_COOKIE,
+ OAUTH_ERROR,
+ SSL_REQUIRED,
+ CODE_TO_TOKEN_FAILURE,
+ INVALID_TOKEN,
+ STALE_TOKEN,
+ NO_AUTHORIZATION_HEADER
+ }
+
+ private Reason reason;
+ private String description;
+
+ public OIDCAuthenticationError(Reason reason, String description) {
+ this.reason = reason;
+ this.description = description;
+ }
+
+ public Reason getReason() {
+ return reason;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public String toString() {
+ return "OIDCAuthenticationError [reason=" + reason + ", description=" + description + "]";
+ }
+
+
+}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
index 2588c40..a332f83 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
@@ -3,6 +3,7 @@ package org.keycloak.adapters;
import org.jboss.logging.Logger;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.UserSessionManagement;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.VersionRepresentation;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jws.JWSInput;
@@ -178,18 +179,17 @@ public class PreAuthActionsHandler {
return null;
}
- JWSInput input = new JWSInput(token);
- boolean verified = false;
try {
- verified = RSAProvider.verify(input, deployment.getRealmKey());
- } catch (Exception ignore) {
- }
- if (!verified) {
- log.warn("admin request failed, unable to verify token");
- facade.getResponse().sendError(403, "no token");
- return null;
+ JWSInput input = new JWSInput(token);
+ if (RSAProvider.verify(input, deployment.getRealmKey())) {
+ return input;
+ }
+ } catch (JWSInputException ignore) {
}
- return input;
+
+ log.warn("admin request failed, unable to verify token");
+ facade.getResponse().sendError(403, "no token");
+ return null;
}
diff --git a/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthChallenge.java b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthChallenge.java
index ff0960f..8bb2f71 100755
--- a/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthChallenge.java
+++ b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthChallenge.java
@@ -13,15 +13,7 @@ public interface AuthChallenge {
boolean challenge(HttpFacade exchange);
/**
- * Whether or not an error page should be displayed if possible along with the challenge
- *
- * @return
- */
- boolean errorPage();
-
- /**
- * If errorPage is true, this is the response code the challenge will send. This is used by platforms
- * that call HttpServletResponse.sendError() to forward to error page.
+ * Some platforms need the error code that will be sent (i.e. Undertow)
*
* @return
*/
diff --git a/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthenticationError.java b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthenticationError.java
new file mode 100755
index 0000000..238a7d8
--- /dev/null
+++ b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthenticationError.java
@@ -0,0 +1,13 @@
+package org.keycloak.adapters.spi;
+
+/**
+ * Common marker interface used by keycloak client adapters when there is an error. For servlets, you'll be able
+ * to extract this error from the HttpServletRequest.getAttribute(AuthenticationError.class.getName()). Each protocol
+ * will have their own subclass of this interface.
+ *
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface AuthenticationError {
+}
diff --git a/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthOutcome.java b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthOutcome.java
index 60a34b2..2479884 100755
--- a/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthOutcome.java
+++ b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/AuthOutcome.java
@@ -5,8 +5,5 @@ package org.keycloak.adapters.spi;
* @version $Revision: 1 $
*/
public enum AuthOutcome {
- NOT_ATTEMPTED,
- FAILED,
- AUTHENTICATED,
- LOGGED_OUT
+ NOT_ATTEMPTED, FAILED, AUTHENTICATED, NOT_AUTHENTICATED, LOGGED_OUT
}
diff --git a/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/HttpFacade.java b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/HttpFacade.java
index fb3804e..ead316a 100755
--- a/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/HttpFacade.java
+++ b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/HttpFacade.java
@@ -47,6 +47,8 @@ public interface HttpFacade {
InputStream getInputStream();
String getRemoteAddr();
+ void setError(AuthenticationError error);
+ void setError(LogoutError error);
}
interface Response {
@@ -56,6 +58,7 @@ public interface HttpFacade {
void resetCookie(String name, String path);
void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly);
OutputStream getOutputStream();
+ void sendError(int code);
void sendError(int code, String message);
/**
diff --git a/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/LogoutError.java b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/LogoutError.java
new file mode 100755
index 0000000..2d846ec
--- /dev/null
+++ b/integration/adapter-spi/src/main/java/org/keycloak/adapters/spi/LogoutError.java
@@ -0,0 +1,12 @@
+package org.keycloak.adapters.spi;
+
+/**
+ * Common marker interface used by keycloak client adapters when there is an error. For servlets, you'll be able
+ * to extract this error from the HttpServletRequest.getAttribute(LogoutError.class.getName()). Each protocol
+ * will have their own subclass of this interface.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface LogoutError {
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientInitialAccessResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientInitialAccessResource.java
new file mode 100644
index 0000000..8875a4c
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientInitialAccessResource.java
@@ -0,0 +1,31 @@
+package org.keycloak.admin.client.resource;
+
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface ClientInitialAccessResource {
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ ClientInitialAccessPresentation create(ClientInitialAccessCreatePresentation rep);
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ List<ClientInitialAccessPresentation> list();
+
+ @DELETE
+ @Path("{id}")
+ void delete(final @PathParam("id") String id);
+
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupResource.java
new file mode 100755
index 0000000..84af76e
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupResource.java
@@ -0,0 +1,80 @@
+package org.keycloak.admin.client.resource;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface GroupResource {
+
+ /**
+ * Does not expand hierarchy. Subgroups will not be set.
+ *
+ * @return
+ */
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public GroupRepresentation toRepresentation();
+
+ /**
+ * Update group
+ *
+ * @param rep
+ */
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ public void update(GroupRepresentation rep);
+
+ @DELETE
+ public void remove();
+
+
+ /**
+ * Set or create child. This will just set the parent if it exists. Create it and set the parent
+ * if the group doesn't exist.
+ *
+ * @param rep
+ */
+ @POST
+ @Path("children")
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response subGroup(GroupRepresentation rep);
+
+
+ @Path("role-mappings")
+ public RoleMappingResource roles();
+
+ /**
+ * Get users
+ * <p/>
+ * Returns a list of users, filtered according to query parameters
+ *
+ * @param firstResult Pagination offset
+ * @param maxResults Pagination size
+ * @return
+ */
+ @GET
+ @NoCache
+ @Path("/members")
+ @Produces(MediaType.APPLICATION_JSON)
+ public List<UserRepresentation> members(@QueryParam("first") Integer firstResult,
+ @QueryParam("max") Integer maxResults);
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java
new file mode 100755
index 0000000..f917d49
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java
@@ -0,0 +1,39 @@
+package org.keycloak.admin.client.resource;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.GroupRepresentation;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface GroupsResource {
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public List<GroupRepresentation> groups();
+
+ /**
+ * create or add a top level realm groupSet or create child. This will update the group and set the parent if it exists. Create it and set the parent
+ * if the group doesn't exist.
+ *
+ * @param rep
+ */
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response add(GroupRepresentation rep);
+
+ @Path("{id}")
+ public GroupResource group(@PathParam("id") String id);
+
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
old mode 100644
new mode 100755
index 87513ec..6dee1cb
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
@@ -1,6 +1,8 @@
package org.keycloak.admin.client.resource;
+import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import javax.ws.rs.*;
@@ -36,6 +38,28 @@ public interface RealmResource {
@Path("roles")
RolesResource roles();
+ @Path("groups")
+ GroupsResource groups();
+
+ @GET
+ @Path("group-by-path/{path: .*}")
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public GroupRepresentation getGroupByPath(@PathParam("path") String path);
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("default-groups")
+ public List<GroupRepresentation> getDefaultGroups();
+
+ @PUT
+ @Path("default-groups/{groupId}")
+ public void addDefaultGroup(@PathParam("groupId") String groupId);
+
+ @DELETE
+ @Path("default-groups/{groupId}")
+ public void removeDefaultGroup(@PathParam("groupId") String groupId);
+
@Path("identity-provider")
IdentityProvidersResource identityProviders();
@@ -45,5 +69,8 @@ public interface RealmResource {
@Path("client-session-stats")
@GET
List<Map<String, String>> getClientSessionStats();
-
+
+ @Path("clients-initial-access")
+ ClientInitialAccessResource clientInitialAccess();
+
}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java
index a2490b6..cc70e24 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java
@@ -2,6 +2,7 @@ package org.keycloak.admin.client.resource;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
+import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
@@ -34,6 +35,21 @@ public interface UserResource {
@DELETE
public void remove();
+ @Path("groups")
+ @GET
+ List<GroupRepresentation> groups();
+
+ @Path("groups/{groupId}")
+ @PUT
+ void joinGroup(@PathParam("groupId") String groupId);
+
+ @Path("groups/{groupId}")
+ @DELETE
+ void leaveGroup(@PathParam("groupId") String groupId);
+
+
+
+
@POST
@Path("logout")
public void logout();
integration/as7-eap6/pom.xml 1(+0 -1)
diff --git a/integration/as7-eap6/pom.xml b/integration/as7-eap6/pom.xml
index e1bca6a..52a1053 100755
--- a/integration/as7-eap6/pom.xml
+++ b/integration/as7-eap6/pom.xml
@@ -17,6 +17,5 @@
<module>as7-adapter-spi</module>
<module>as7-adapter</module>
<module>as7-subsystem</module>
- <module>as7-server-subsystem</module>
</modules>
</project>
\ No newline at end of file
diff --git a/integration/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/integration/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
index c468f8d..652860b 100644
--- a/integration/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
+++ b/integration/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
@@ -8,6 +8,7 @@ import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.ServerRequest;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
@@ -195,10 +196,10 @@ public class KeycloakInstalled {
token = RSATokenVerifier.verifyToken(tokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl());
if (idTokenString != null) {
- JWSInput input = new JWSInput(idTokenString);
try {
+ JWSInput input = new JWSInput(idTokenString);
idToken = input.readJsonContent(IDToken.class);
- } catch (IOException e) {
+ } catch (JWSInputException e) {
throw new VerificationException();
}
}
diff --git a/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java
index cce85d5..4af3b90 100755
--- a/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java
+++ b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java
@@ -12,6 +12,8 @@ import javax.ws.rs.core.SecurityContext;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.OIDCHttpFacade;
+import org.keycloak.adapters.spi.AuthenticationError;
+import org.keycloak.adapters.spi.LogoutError;
import org.keycloak.common.util.HostUtils;
/**
@@ -93,6 +95,17 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
// TODO: implement properly
return HostUtils.getIpAddress();
}
+
+ @Override
+ public void setError(AuthenticationError error) {
+ requestContext.setProperty(AuthenticationError.class.getName(), error);
+ }
+
+ @Override
+ public void setError(LogoutError error) {
+ requestContext.setProperty(LogoutError.class.getName(), error);
+
+ }
}
protected class ResponseFacade implements OIDCHttpFacade.Response {
@@ -133,6 +146,13 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
}
@Override
+ public void sendError(int code) {
+ javax.ws.rs.core.Response response = responseBuilder.status(code).build();
+ requestContext.abortWith(response);
+ responseFinished = true;
+ }
+
+ @Override
public void sendError(int code, String message) {
javax.ws.rs.core.Response response = responseBuilder.status(code).entity(message).build();
requestContext.abortWith(response);
diff --git a/integration/jetty/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java b/integration/jetty/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java
index ea2b3af..42ff201 100755
--- a/integration/jetty/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java
+++ b/integration/jetty/jetty-adapter-spi/src/main/java/org/keycloak/adapters/jetty/spi/JettyHttpFacade.java
@@ -1,6 +1,8 @@
package org.keycloak.adapters.jetty.spi;
+import org.keycloak.adapters.spi.AuthenticationError;
import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.spi.LogoutError;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.UriUtils;
@@ -125,6 +127,18 @@ public class JettyHttpFacade implements HttpFacade {
public String getRemoteAddr() {
return request.getRemoteAddr();
}
+
+ @Override
+ public void setError(AuthenticationError error) {
+ request.setAttribute(AuthenticationError.class.getName(), error);
+
+ }
+
+ @Override
+ public void setError(LogoutError error) {
+ request.setAttribute(LogoutError.class.getName(), error);
+ }
+
}
protected class ResponseFacade implements Response {
@@ -171,6 +185,15 @@ public class JettyHttpFacade implements HttpFacade {
}
@Override
+ public void sendError(int code) {
+ try {
+ response.sendError(code);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
public void sendError(int code, String message) {
try {
response.sendError(code, message);
diff --git a/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java b/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java
index d3790af..dad7570 100755
--- a/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java
+++ b/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java
@@ -265,15 +265,6 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
challenge.challenge(facade);
- if (challenge.errorPage() && errorPage != null) {
- Response response = (Response)res;
- try {
- response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), errorPage)));
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
-
- }
}
return Authentication.SEND_CONTINUE;
}
integration/js/src/main/resources/keycloak.js 261(+198 -63)
diff --git a/integration/js/src/main/resources/keycloak.js b/integration/js/src/main/resources/keycloak.js
index 09e0d86..a74e1cc 100755
--- a/integration/js/src/main/resources/keycloak.js
+++ b/integration/js/src/main/resources/keycloak.js
@@ -36,6 +36,39 @@
if (initOptions.onLoad === 'login-required') {
kc.loginRequired = true;
}
+
+ if (initOptions.responseMode) {
+ if (initOptions.responseMode === 'query' || initOptions.responseMode === 'fragment') {
+ kc.responseMode = initOptions.responseMode;
+ } else {
+ throw 'Invalid value for responseMode';
+ }
+ }
+
+ if (initOptions.flow) {
+ switch (initOptions.flow) {
+ case 'standard':
+ kc.responseType = 'code';
+ break;
+ case 'implicit':
+ kc.responseType = 'id_token token';
+ break;
+ case 'hybrid':
+ kc.responseType = 'code id_token token';
+ break;
+ default:
+ throw 'Invalid value for flow';
+ }
+ kc.flow = initOptions.flow;
+ }
+ }
+
+ if (!kc.responseMode) {
+ kc.responseMode = 'fragment';
+ }
+ if (!kc.responseType) {
+ kc.responseType = 'code';
+ kc.flow = 'standard';
}
var promise = createPromise();
@@ -95,7 +128,7 @@
return;
} else if (initOptions) {
if (initOptions.token || initOptions.refreshToken) {
- setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken);
+ setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken, false);
if (loginIframe.enable) {
setupCheckLoginIframe().success(function() {
@@ -132,13 +165,14 @@
kc.createLoginUrl = function(options) {
var state = createUUID();
+ var nonce = createUUID();
var redirectUri = adapter.redirectUri(options);
if (options && options.prompt) {
redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'prompt=' + options.prompt;
}
- sessionStorage.oauthState = JSON.stringify({ state: state, redirectUri: encodeURIComponent(redirectUri) });
+ sessionStorage.oauthState = JSON.stringify({ state: state, nonce: nonce, redirectUri: encodeURIComponent(redirectUri) });
var action = 'auth';
if (options && options.action == 'register') {
@@ -150,7 +184,9 @@
+ '?client_id=' + encodeURIComponent(kc.clientId)
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
+ '&state=' + encodeURIComponent(state)
- + '&response_type=code';
+ + '&nonce=' + encodeURIComponent(nonce)
+ + '&response_mode=' + encodeURIComponent(kc.responseMode)
+ + '&response_type=' + encodeURIComponent(kc.responseType);
if (options && options.prompt) {
url += '&prompt=' + encodeURIComponent(options.prompt);
@@ -277,7 +313,7 @@
}
kc.isTokenExpired = function(minValidity) {
- if (!kc.tokenParsed || !kc.refreshToken) {
+ if (!kc.tokenParsed || (!kc.refreshToken && kc.flow != 'implicit' )) {
throw 'Not authenticated';
}
@@ -327,7 +363,7 @@
timeLocal = (timeLocal + new Date().getTime()) / 2;
var tokenResponse = JSON.parse(req.responseText);
- setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token']);
+ setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], true);
kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;
@@ -365,7 +401,7 @@
kc.clearToken = function() {
if (kc.token) {
- setToken(null, null, null);
+ setToken(null, null, null, true);
kc.onAuthLogout && kc.onAuthLogout();
if (kc.loginRequired) {
kc.login();
@@ -394,7 +430,21 @@
var error = oauth.error;
var prompt = oauth.prompt;
- if (code) {
+ var timeLocal = new Date().getTime();
+
+ if (error) {
+ if (prompt != 'none') {
+ kc.onAuthError && kc.onAuthError();
+ promise && promise.setError();
+ } else {
+ promise && promise.setSuccess();
+ }
+ return;
+ } else if ((kc.flow != 'standard') && (oauth.access_token || oauth.id_token)) {
+ authSuccess(oauth.access_token, null, oauth.id_token, true);
+ }
+
+ if ((kc.flow != 'implicit') && code) {
var params = 'code=' + code + '&grant_type=authorization_code';
var url = getRealmUrl() + '/protocol/openid-connect/token';
@@ -412,20 +462,12 @@
req.withCredentials = true;
- var timeLocal = new Date().getTime();
-
req.onreadystatechange = function() {
if (req.readyState == 4) {
if (req.status == 200) {
- timeLocal = (timeLocal + new Date().getTime()) / 2;
var tokenResponse = JSON.parse(req.responseText);
- setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token']);
-
- kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;
-
- kc.onAuthSuccess && kc.onAuthSuccess();
- promise && promise.setSuccess();
+ authSuccess(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], kc.flow === 'standard');
} else {
kc.onAuthError && kc.onAuthError();
promise && promise.setError();
@@ -434,14 +476,30 @@
};
req.send(params);
- } else if (error) {
- if (prompt != 'none') {
- kc.onAuthError && kc.onAuthError();
+ }
+
+ function authSuccess(accessToken, refreshToken, idToken, fulfillPromise) {
+ timeLocal = (timeLocal + new Date().getTime()) / 2;
+
+ setToken(accessToken, refreshToken, idToken, true);
+
+ if ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) ||
+ (kc.refreshTokenParsed && kc.refreshTokenParsed.nonce != oauth.storedNonce) ||
+ (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce)) {
+
+ console.log('invalid nonce!');
+ kc.clearToken();
promise && promise.setError();
} else {
- promise && promise.setSuccess();
+ kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat;
+
+ if (fulfillPromise) {
+ kc.onAuthSuccess && kc.onAuthSuccess();
+ promise && promise.setSuccess();
+ }
}
}
+
}
function loadConfig(url) {
@@ -507,7 +565,12 @@
return promise.promise;
}
- function setToken(token, refreshToken, idToken) {
+ function setToken(token, refreshToken, idToken, useTokenTime) {
+ if (kc.tokenTimeoutHandle) {
+ clearTimeout(kc.tokenTimeoutHandle);
+ kc.tokenTimeoutHandle = null;
+ }
+
if (token) {
kc.token = token;
kc.tokenParsed = decodeToken(token);
@@ -520,6 +583,13 @@
kc.subject = kc.tokenParsed.sub;
kc.realmAccess = kc.tokenParsed.realm_access;
kc.resourceAccess = kc.tokenParsed.resource_access;
+
+ if (kc.onTokenExpired) {
+ var start = useTokenTime ? kc.tokenParsed.iat : (new Date().getTime() / 1000);
+ var expiresIn = kc.tokenParsed.exp - start;
+ kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn * 1000);
+ }
+
} else {
delete kc.token;
delete kc.tokenParsed;
@@ -597,53 +667,21 @@
}
function parseCallback(url) {
- if (url.indexOf('?') != -1) {
- var oauth = {};
+ var oauth = new CallbackParser(url, kc.responseMode).parseUri();
- oauth.newUrl = url.split('?')[0];
- var paramString = url.split('?')[1];
- var fragIndex = paramString.indexOf('#');
- if (fragIndex != -1) {
- paramString = paramString.substring(0, fragIndex);
- }
- var params = paramString.split('&');
- for (var i = 0; i < params.length; i++) {
- var p = params[i].split('=');
- switch (decodeURIComponent(p[0])) {
- case 'code':
- oauth.code = p[1];
- break;
- case 'error':
- oauth.error = p[1];
- break;
- case 'state':
- oauth.state = decodeURIComponent(p[1]);
- break;
- case 'redirect_fragment':
- oauth.fragment = decodeURIComponent(p[1]);
- break;
- case 'prompt':
- oauth.prompt = p[1];
- break;
- default:
- oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + p[0] + '=' + p[1];
- break;
- }
- }
-
- var sessionState = sessionStorage.oauthState && JSON.parse(sessionStorage.oauthState);
+ var sessionState = sessionStorage.oauthState && JSON.parse(sessionStorage.oauthState);
- if (sessionState && (oauth.code || oauth.error) && oauth.state && oauth.state == sessionState.state) {
- delete sessionStorage.oauthState;
+ if (sessionState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token) && oauth.state && oauth.state == sessionState.state) {
+ delete sessionStorage.oauthState;
- oauth.redirectUri = sessionState.redirectUri;
+ oauth.redirectUri = sessionState.redirectUri;
+ oauth.storedNonce = sessionState.nonce;
- if (oauth.fragment) {
- oauth.newUrl += '#' + oauth.fragment;
- }
-
- return oauth;
+ if (oauth.fragment) {
+ oauth.newUrl += '#' + oauth.fragment;
}
+
+ return oauth;
}
}
@@ -907,6 +945,103 @@
throw 'invalid adapter type: ' + type;
}
+
+
+ var CallbackParser = function(uriToParse, responseMode) {
+ if (!(this instanceof CallbackParser)) {
+ return new CallbackParser(uriToParse, responseMode);
+ }
+ var parser = this;
+
+ var initialParse = function() {
+ var baseUri = null;
+ var queryString = null;
+ var fragmentString = null;
+
+ var questionMarkIndex = uriToParse.indexOf("?");
+ var fragmentIndex = uriToParse.indexOf("#", questionMarkIndex + 1);
+ if (questionMarkIndex == -1 && fragmentIndex == -1) {
+ baseUri = uriToParse;
+ } else if (questionMarkIndex != -1) {
+ baseUri = uriToParse.substring(0, questionMarkIndex);
+ queryString = uriToParse.substring(questionMarkIndex + 1);
+ if (fragmentIndex != -1) {
+ fragmentIndex = queryString.indexOf("#");
+ fragmentString = queryString.substring(fragmentIndex + 1);
+ queryString = queryString.substring(0, fragmentIndex);
+ }
+ } else {
+ baseUri = uriToParse.substring(0, fragmentIndex);
+ fragmentString = uriToParse.substring(fragmentIndex + 1);
+ }
+
+ return { baseUri: baseUri, queryString: queryString, fragmentString: fragmentString };
+ }
+
+ var parseParams = function(paramString) {
+ var result = {};
+ var params = paramString.split('&');
+ for (var i = 0; i < params.length; i++) {
+ var p = params[i].split('=');
+ var paramName = decodeURIComponent(p[0]);
+ var paramValue = decodeURIComponent(p[1]);
+ result[paramName] = paramValue;
+ }
+ return result;
+ }
+
+ var handleQueryParam = function(paramName, paramValue, oauth) {
+ var supportedOAuthParams = [ 'code', 'error', 'state' ];
+
+ for (var i = 0 ; i< supportedOAuthParams.length ; i++) {
+ if (paramName === supportedOAuthParams[i]) {
+ oauth[paramName] = paramValue;
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ parser.parseUri = function() {
+ var parsedUri = initialParse();
+
+ var queryParams = {};
+ if (parsedUri.queryString) {
+ queryParams = parseParams(parsedUri.queryString);
+ }
+
+ var oauth = { newUrl: parsedUri.baseUri };
+ for (var param in queryParams) {
+ switch (param) {
+ case 'redirect_fragment':
+ oauth.fragment = queryParams[param];
+ break;
+ case 'prompt':
+ oauth.prompt = queryParams[param];
+ break;
+ default:
+ if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) {
+ oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + queryParams[param];
+ }
+ break;
+ }
+ }
+
+ if (responseMode === 'fragment') {
+ var fragmentParams = {};
+ if (parsedUri.fragmentString) {
+ fragmentParams = parseParams(parsedUri.fragmentString);
+ }
+ for (var param in fragmentParams) {
+ oauth[param] = fragmentParams[param];
+ }
+ }
+
+ return oauth;
+ }
+ }
+
}
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
diff --git a/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java b/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java
index 1550eaa..6d32bda 100755
--- a/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java
+++ b/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java
@@ -1,6 +1,8 @@
package org.keycloak.adapters.servlet;
+import org.keycloak.adapters.spi.AuthenticationError;
import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.spi.LogoutError;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.ServerCookie;
import org.keycloak.common.util.UriUtils;
@@ -111,6 +113,18 @@ public class ServletHttpFacade implements HttpFacade {
public String getRemoteAddr() {
return request.getRemoteAddr();
}
+
+
+ @Override
+ public void setError(AuthenticationError error) {
+ request.setAttribute(AuthenticationError.class.getName(), error);
+
+ }
+
+ @Override
+ public void setError(LogoutError error) {
+ request.setAttribute(LogoutError.class.getName(), error);
+ }
}
public boolean isEnded() {
return responseFacade.isEnded();
@@ -157,6 +171,15 @@ public class ServletHttpFacade implements HttpFacade {
}
@Override
+ public void sendError(int code) {
+ try {
+ response.sendError(code);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
public void sendError(int code, String message) {
try {
response.sendError(code, message);
diff --git a/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java b/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java
index 57bd93d..3cfc871 100755
--- a/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java
+++ b/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java
@@ -142,11 +142,6 @@ public class KeycloakOIDCFilter implements Filter {
if (challenge != null) {
log.fine("challenge");
challenge.challenge(facade);
- if (challenge.errorPage()) {
- response.sendError(challenge.getResponseCode());
- return;
- }
- log.fine("sending challenge");
return;
}
response.sendError(403);
diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
index c3f1dfd..be92dea 100755
--- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
+++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
@@ -6,7 +6,10 @@ import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.ServerRequest;
+import org.keycloak.adapters.spi.AuthenticationError;
+import org.keycloak.adapters.spi.LogoutError;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
import org.keycloak.common.util.KeycloakUriBuilder;
@@ -151,10 +154,10 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
public static IDToken extractIdToken(String idToken) {
if (idToken == null) return null;
- JWSInput input = new JWSInput(idToken);
try {
+ JWSInput input = new JWSInput(idToken);
return input.readJsonContent(IDToken.class);
- } catch (IOException e) {
+ } catch (JWSInputException e) {
throw new RuntimeException(e);
}
}
@@ -237,6 +240,18 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
public String getRemoteAddr() {
return servletRequest.getRemoteAddr();
}
+
+ @Override
+ public void setError(AuthenticationError error) {
+ servletRequest.setAttribute(AuthenticationError.class.getName(), error);
+
+ }
+
+ @Override
+ public void setError(LogoutError error) {
+ servletRequest.setAttribute(LogoutError.class.getName(), error);
+ }
+
};
}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcher.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcher.java
new file mode 100644
index 0000000..fd2b927
--- /dev/null
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcher.java
@@ -0,0 +1,38 @@
+package org.keycloak.adapters.springsecurity.authentication;
+
+import org.apache.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * {@link RequestMatcher} that determines if a given request is an API request or an
+ * interactive login request.
+ *
+ * @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
+ * @see RequestMatcher
+ */
+public class HttpHeaderInspectingApiRequestMatcher implements RequestMatcher {
+
+ protected static final String X_REQUESTED_WITH_HEADER = "X-Requested-With";
+ protected static final String X_REQUESTED_WITH_HEADER_AJAX_VALUE = "XMLHttpRequest";
+
+ /**
+ * Returns true if the given request is an API request or false if it's an interactive
+ * login request.
+ *
+ * @param request the <code>HttpServletRequest</code>
+ * @return <code>true</code> if the given <code>request</code> is an API request;
+ * <code>false</code> otherwise
+ */
+ @Override
+ public boolean matches(HttpServletRequest request) {
+ boolean ajax = X_REQUESTED_WITH_HEADER_AJAX_VALUE.equals(request.getHeader(X_REQUESTED_WITH_HEADER));
+ boolean html = request.getHeader(HttpHeaders.ACCEPT) != null && request.getHeader(HttpHeaders.ACCEPT).contains(
+ MediaType.TEXT_HTML_VALUE);
+
+ return ajax || !html;
+ }
+
+}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java
index 3357806..f498fe6 100644
--- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPoint.java
@@ -1,9 +1,12 @@
package org.keycloak.adapters.springsecurity.authentication;
+import org.apache.http.HttpHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import javax.servlet.ServletException;
@@ -12,10 +15,15 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
- * Provides a Keycloak {@link AuthenticationEntryPoint authentication entry point}.
+ * Provides a Keycloak {@link AuthenticationEntryPoint authentication entry point}. Uses a
+ * {@link RequestMatcher} to determine if the request is an interactive login request or a
+ * API request, which should not be redirected to an interactive login page. By default,
+ * this entry point uses a {@link HttpHeaderInspectingApiRequestMatcher} but can be overridden using in the
+ * constructor.
*
* @author <a href="mailto:srossillo@smartling.com">Scott Rossillo</a>
- * @version $Revision: 1 $
+ *
+ * @see HttpHeaderInspectingApiRequestMatcher
*/
public class KeycloakAuthenticationEntryPoint implements AuthenticationEntryPoint {
@@ -23,23 +31,62 @@ public class KeycloakAuthenticationEntryPoint implements AuthenticationEntryPoin
* Default Keycloak authentication login URI
*/
public static final String DEFAULT_LOGIN_URI = "/sso/login";
+ private static final String DEFAULT_REALM = "Unknown";
+ private static final RequestMatcher DEFAULT_API_REQUEST_MATCHER = new HttpHeaderInspectingApiRequestMatcher();
private final static Logger log = LoggerFactory.getLogger(KeycloakAuthenticationEntryPoint.class);
+ private final RequestMatcher apiRequestMatcher;
private String loginUri = DEFAULT_LOGIN_URI;
+ private String realm = DEFAULT_REALM;
+
+ /**
+ * Creates a new Keycloak authentication entry point.
+ */
+ public KeycloakAuthenticationEntryPoint() {
+ this(DEFAULT_API_REQUEST_MATCHER);
+ }
+
+ /**
+ * Creates a new Keycloak authentication entry point using the given request
+ * matcher to determine if the current request is an API request or a browser request.
+ *
+ * @param apiRequestMatcher the <code>RequestMatcher</code> to use to determine
+ * if the current request is an API request or a browser request (required)
+ */
+ public KeycloakAuthenticationEntryPoint(RequestMatcher apiRequestMatcher) {
+ Assert.notNull(apiRequestMatcher, "apiRequestMatcher required");
+ this.apiRequestMatcher = apiRequestMatcher;
+ }
@Override
- public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
- throws IOException, ServletException {
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException
+ {
+ if (apiRequestMatcher.matches(request)) {
+ commenceUnauthorizedResponse(request, response);
+ } else {
+ commenceLoginRedirect(request, response);
+ }
+ }
+ protected void commenceLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
String contextAwareLoginUri = request.getContextPath() + loginUri;
-
log.debug("Redirecting to login URI {}", contextAwareLoginUri);
response.sendRedirect(contextAwareLoginUri);
}
+ protected void commenceUnauthorizedResponse(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ response.addHeader(HttpHeaders.WWW_AUTHENTICATE, String.format("Bearer realm=\"%s\"", realm));
+ response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
+ }
+
public void setLoginUri(String loginUri) {
Assert.notNull(loginUri, "loginUri cannot be null");
this.loginUri = loginUri;
}
+
+ public void setRealm(String realm) {
+ Assert.notNull(realm, "realm cannot be null");
+ this.realm = realm;
+ }
}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java
index 79074da..ba9fa1a 100755
--- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletRequest.java
@@ -1,7 +1,9 @@
package org.keycloak.adapters.springsecurity.facade;
+import org.keycloak.adapters.spi.AuthenticationError;
import org.keycloak.adapters.spi.HttpFacade.Cookie;
import org.keycloak.adapters.spi.HttpFacade.Request;
+import org.keycloak.adapters.spi.LogoutError;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
@@ -109,4 +111,17 @@ class WrappedHttpServletRequest implements Request {
public String getRemoteAddr() {
return request.getRemoteAddr();
}
+
+ @Override
+ public void setError(AuthenticationError error) {
+ request.setAttribute(AuthenticationError.class.getName(), error);
+
+ }
+
+ @Override
+ public void setError(LogoutError error) {
+ request.setAttribute(LogoutError.class.getName(), error);
+ }
+
+
}
diff --git a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponse.java b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponse.java
index c6b352f..c356ebd 100644
--- a/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponse.java
+++ b/integration/spring-security/src/main/java/org/keycloak/adapters/springsecurity/facade/WrappedHttpServletResponse.java
@@ -96,6 +96,15 @@ class WrappedHttpServletResponse implements Response {
}
@Override
+ public void sendError(int code) {
+ try {
+ response.sendError(code);
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to set HTTP status", e);
+ }
+ }
+
+ @Override
public void sendError(int code, String message) {
try {
response.sendError(code, message);
diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcherTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcherTest.java
new file mode 100644
index 0000000..faa800d
--- /dev/null
+++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/HttpHeaderInspectingApiRequestMatcherTest.java
@@ -0,0 +1,43 @@
+package org.keycloak.adapters.springsecurity.authentication;
+
+import org.apache.http.HttpHeaders;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+import static org.junit.Assert.*;
+
+/**
+ * HTTP header inspecting API request matcher tests.
+ */
+public class HttpHeaderInspectingApiRequestMatcherTest {
+
+ private RequestMatcher apiRequestMatcher = new HttpHeaderInspectingApiRequestMatcher();
+ private MockHttpServletRequest request;
+
+ @Before
+ public void setUp() throws Exception {
+ request = new MockHttpServletRequest();
+ }
+
+ @Test
+ public void testMatches() throws Exception {
+ assertTrue(apiRequestMatcher.matches(request));
+ }
+
+ @Test
+ public void testMatchesBrowserRequest() throws Exception {
+ request.addHeader(HttpHeaders.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
+ assertFalse(apiRequestMatcher.matches(request));
+ }
+
+ @Test
+ public void testMatchesRequestedWith() throws Exception {
+ request.addHeader(
+ HttpHeaderInspectingApiRequestMatcher.X_REQUESTED_WITH_HEADER,
+ HttpHeaderInspectingApiRequestMatcher.X_REQUESTED_WITH_HEADER_AJAX_VALUE);
+ assertTrue(apiRequestMatcher.matches(request));
+ }
+
+}
diff --git a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPointTest.java b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPointTest.java
index b7204c3..f026015 100644
--- a/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPointTest.java
+++ b/integration/spring-security/src/test/java/org/keycloak/adapters/springsecurity/authentication/KeycloakAuthenticationEntryPointTest.java
@@ -1,5 +1,6 @@
package org.keycloak.adapters.springsecurity.authentication;
+import org.apache.http.HttpHeaders;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpStatus;
@@ -25,14 +26,16 @@ public class KeycloakAuthenticationEntryPointTest {
}
@Test
- public void testCommence() throws Exception {
+ public void testCommenceWithRedirect() throws Exception {
+ configureBrowserRequest();
authenticationEntryPoint.commence(request, response, null);
assertEquals(HttpStatus.FOUND.value(), response.getStatus());
assertEquals(KeycloakAuthenticationEntryPoint.DEFAULT_LOGIN_URI, response.getHeader("Location"));
}
@Test
- public void testCommenceNotRootContext() throws Exception {
+ public void testCommenceWithRedirectNotRootContext() throws Exception {
+ configureBrowserRequest();
String contextPath = "/foo";
request.setContextPath(contextPath);
authenticationEntryPoint.commence(request, response, null);
@@ -41,11 +44,24 @@ public class KeycloakAuthenticationEntryPointTest {
}
@Test
+ public void testCommenceWithUnauthorizedWithAccept() throws Exception {
+ request.addHeader(HttpHeaders.ACCEPT, "application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
+ authenticationEntryPoint.commence(request, response, null);
+ assertEquals(HttpStatus.UNAUTHORIZED.value(), response.getStatus());
+ assertNotNull(response.getHeader(HttpHeaders.WWW_AUTHENTICATE));
+ }
+
+ @Test
public void testSetLoginUri() throws Exception {
+ configureBrowserRequest();
final String logoutUri = "/foo";
authenticationEntryPoint.setLoginUri(logoutUri);
authenticationEntryPoint.commence(request, response, null);
assertEquals(HttpStatus.FOUND.value(), response.getStatus());
assertEquals(logoutUri, response.getHeader("Location"));
}
+
+ private void configureBrowserRequest() {
+ request.addHeader(HttpHeaders.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
+ }
}
diff --git a/integration/tomcat/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java b/integration/tomcat/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java
index cdac621..3638759 100755
--- a/integration/tomcat/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java
+++ b/integration/tomcat/tomcat-adapter-spi/src/main/java/org/keycloak/adapters/tomcat/CatalinaHttpFacade.java
@@ -1,6 +1,8 @@
package org.keycloak.adapters.tomcat;
+import org.keycloak.adapters.spi.AuthenticationError;
import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.spi.LogoutError;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.ServerCookie;
import org.keycloak.common.util.UriUtils;
@@ -125,6 +127,18 @@ public class CatalinaHttpFacade implements HttpFacade {
public String getRemoteAddr() {
return request.getRemoteAddr();
}
+
+ @Override
+ public void setError(AuthenticationError error) {
+ request.setAttribute(AuthenticationError.class.getName(), error);
+
+ }
+
+ @Override
+ public void setError(LogoutError error) {
+ request.setAttribute(LogoutError.class.getName(), error);
+ }
+
}
protected class ResponseFacade implements Response {
@@ -168,6 +182,15 @@ public class CatalinaHttpFacade implements HttpFacade {
}
@Override
+ public void sendError(int code) {
+ try {
+ response.sendError(code);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
public void sendError(int code, String message) {
try {
response.sendError(code, message);
@@ -176,6 +199,7 @@ public class CatalinaHttpFacade implements HttpFacade {
}
}
+
@Override
public void end() {
ended = true;
diff --git a/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java b/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java
index 89bee43..20c87bd 100755
--- a/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java
+++ b/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java
@@ -195,12 +195,6 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat
}
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
- if (loginConfig == null) {
- loginConfig = request.getContext().getLoginConfig();
- }
- if (challenge.errorPage()) {
- if (forwardToErrorPageInternal(request, response, loginConfig))return false;
- }
challenge.challenge(facade);
}
return false;
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java
index aded0ef..36e8aa4 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java
@@ -56,11 +56,7 @@ public abstract class AbstractUndertowKeycloakAuthMech implements Authentication
public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
AuthChallenge challenge = exchange.getAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY);
if (challenge != null) {
- if (challenge.errorPage() && errorPage != null) {
- Integer code = servePage(exchange, errorPage);
- return new ChallengeResult(true, code);
- }
- UndertowHttpFacade facade = new UndertowHttpFacade(exchange);
+ UndertowHttpFacade facade = createFacade(exchange);
if (challenge.challenge(facade)) {
return new ChallengeResult(true, exchange.getResponseCode());
}
@@ -68,6 +64,10 @@ public abstract class AbstractUndertowKeycloakAuthMech implements Authentication
return new ChallengeResult(false);
}
+ public UndertowHttpFacade createFacade(HttpServerExchange exchange) {
+ return new OIDCUndertowHttpFacade(exchange);
+ }
+
protected Integer servePage(final HttpServerExchange exchange, final String location) {
sendRedirect(exchange, location);
return StatusCodes.TEMPORARY_REDIRECT;
@@ -89,7 +89,7 @@ public abstract class AbstractUndertowKeycloakAuthMech implements Authentication
if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return;
HttpServerExchange exchange = notification.getExchange();
- UndertowHttpFacade facade = new OIDCUndertowHttpFacade(exchange);
+ UndertowHttpFacade facade = createFacade(exchange);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
KeycloakSecurityContext ksc = exchange.getAttachment(OIDCUndertowHttpFacade.KEYCLOAK_SECURITY_CONTEXT_KEY);
if (ksc != null && ksc instanceof RefreshableKeycloakSecurityContext) {
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java
new file mode 100755
index 0000000..ddf0f41
--- /dev/null
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.adapters.undertow;
+
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.AttachmentKey;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.OIDCHttpFacade;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class OIDCServletUndertowHttpFacade extends ServletHttpFacade implements OIDCHttpFacade {
+ public static final AttachmentKey<KeycloakSecurityContext> KEYCLOAK_SECURITY_CONTEXT_KEY = AttachmentKey.create(KeycloakSecurityContext.class);
+
+ public OIDCServletUndertowHttpFacade(HttpServerExchange exchange) {
+ super(exchange);
+ }
+
+ @Override
+ public KeycloakSecurityContext getSecurityContext() {
+ return exchange.getAttachment(KEYCLOAK_SECURITY_CONTEXT_KEY);
+ }
+
+}
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java
index b5984a4..b546e76 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java
@@ -79,7 +79,7 @@ public class ServletKeycloakAuthMech extends AbstractUndertowKeycloakAuthMech {
@Override
public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) {
- UndertowHttpFacade facade = new OIDCUndertowHttpFacade(exchange);
+ UndertowHttpFacade facade = createFacade(exchange);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
if (!deployment.isConfigured()) {
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
@@ -119,4 +119,8 @@ public class ServletKeycloakAuthMech extends AbstractUndertowKeycloakAuthMech {
}
}
+ @Override
+ public UndertowHttpFacade createFacade(HttpServerExchange exchange) {
+ return new OIDCServletUndertowHttpFacade(exchange);
+ }
}
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPreAuthActionsHandler.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPreAuthActionsHandler.java
index 362cdb0..05672db 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPreAuthActionsHandler.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletPreAuthActionsHandler.java
@@ -61,11 +61,13 @@ public class ServletPreAuthActionsHandler implements HttpHandler {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
- UndertowHttpFacade facade = new OIDCUndertowHttpFacade(exchange);
+ UndertowHttpFacade facade = new OIDCServletUndertowHttpFacade(exchange);
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
SessionManagementBridge bridge = new SessionManagementBridge(userSessionManagement, servletRequestContext.getDeployment().getSessionManager());
PreAuthActionsHandler handler = new PreAuthActionsHandler(bridge, deploymentContext, facade);
if (handler.handleRequest()) return;
next.handleRequest(exchange);
}
+
+
}
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java
index 85b7581..88ba705 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java
@@ -25,7 +25,7 @@ public class UndertowAuthenticationMechanism extends AbstractUndertowKeycloakAut
@Override
public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) {
- UndertowHttpFacade facade = new OIDCUndertowHttpFacade(exchange);
+ UndertowHttpFacade facade = createFacade(exchange);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
if (!deployment.isConfigured()) {
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java
index 77194e1..8ad97ef 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java
@@ -47,10 +47,14 @@ public class UndertowPreAuthActionsHandler implements HttpHandler {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
- UndertowHttpFacade facade = new OIDCUndertowHttpFacade(exchange);
+ UndertowHttpFacade facade = createFacade(exchange);
SessionManagementBridge bridge = new SessionManagementBridge(userSessionManagement, sessionManager);
PreAuthActionsHandler handler = new PreAuthActionsHandler(bridge, deploymentContext, facade);
if (handler.handleRequest()) return;
next.handleRequest(exchange);
}
+
+ public UndertowHttpFacade createFacade(HttpServerExchange exchange) {
+ return new OIDCUndertowHttpFacade(exchange);
+ }
}
diff --git a/integration/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/ServletHttpFacade.java b/integration/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/ServletHttpFacade.java
index 0b10188..815b1ce 100755
--- a/integration/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/ServletHttpFacade.java
+++ b/integration/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/ServletHttpFacade.java
@@ -2,9 +2,13 @@ package org.keycloak.adapters.undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.handlers.ServletRequestContext;
+import org.keycloak.adapters.spi.AuthenticationError;
+import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.spi.LogoutError;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -18,6 +22,7 @@ public class ServletHttpFacade extends UndertowHttpFacade {
super(exchange);
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
request = (HttpServletRequest)servletRequestContext.getServletRequest();
+ response = (HttpServletResponse)servletRequestContext.getServletResponse();
}
protected class RequestFacade extends UndertowHttpFacade.RequestFacade {
@@ -26,6 +31,47 @@ public class ServletHttpFacade extends UndertowHttpFacade {
return request.getParameter(param);
}
+ @Override
+ public void setError(AuthenticationError error) {
+ request.setAttribute(AuthenticationError.class.getName(), error);
+
+ }
+
+ @Override
+ public void setError(LogoutError error) {
+ request.setAttribute(LogoutError.class.getName(), error);
+ }
+
+
+ }
+
+ protected class ResponseFacade extends UndertowHttpFacade.ResponseFacade {
+ // can't call sendError from a challenge. Undertow ends up calling send error.
+ /*
+ @Override
+ public void sendError(int code) {
+ try {
+ response.sendError(code);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void sendError(int code, String message) {
+ try {
+ response.sendError(code, message);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ */
+
+ }
+
+ @Override
+ public Response getResponse() {
+ return new ResponseFacade();
}
@Override
diff --git a/integration/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java b/integration/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java
index 3d37877..d38fe55 100755
--- a/integration/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java
+++ b/integration/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java
@@ -2,9 +2,12 @@ package org.keycloak.adapters.undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.CookieImpl;
+import io.undertow.util.AttachmentKey;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
+import org.keycloak.adapters.spi.AuthenticationError;
import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.spi.LogoutError;
import org.keycloak.common.util.KeycloakUriBuilder;
import javax.security.cert.X509Certificate;
@@ -22,6 +25,9 @@ import java.util.Map;
* @version $Revision: 1 $
*/
public class UndertowHttpFacade implements HttpFacade {
+ public static final AttachmentKey<AuthenticationError> AUTH_ERROR_ATTACHMENT_KEY = AttachmentKey.create(AuthenticationError.class);
+ public static final AttachmentKey<LogoutError> LOGOUT_ERROR_ATTACHMENT_KEY = AttachmentKey.create(LogoutError.class);
+
protected HttpServerExchange exchange;
protected RequestFacade requestFacade = new RequestFacade();
protected ResponseFacade responseFacade = new ResponseFacade();
@@ -127,6 +133,17 @@ public class UndertowHttpFacade implements HttpFacade {
}
return address.getHostAddress();
}
+
+ @Override
+ public void setError(AuthenticationError error) {
+ exchange.putAttachment(AUTH_ERROR_ATTACHMENT_KEY, error);
+ }
+
+ @Override
+ public void setError(LogoutError error) {
+ exchange.putAttachment(LOGOUT_ERROR_ATTACHMENT_KEY, error);
+
+ }
}
protected class ResponseFacade implements Response {
@@ -171,6 +188,11 @@ public class UndertowHttpFacade implements HttpFacade {
}
@Override
+ public void sendError(int code) {
+ exchange.setResponseCode(code);
+ }
+
+ @Override
public void sendError(int code, String message) {
exchange.setResponseCode(code);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html");
integration/wildfly/pom.xml 4(+1 -3)
diff --git a/integration/wildfly/pom.xml b/integration/wildfly/pom.xml
index 141cd87..74ee90a 100644
--- a/integration/wildfly/pom.xml
+++ b/integration/wildfly/pom.xml
@@ -15,9 +15,7 @@
<modules>
<module>wildfly-adapter</module>
- <module>wildfly-extensions</module>
<module>wf8-subsystem</module>
- <module>wf9-subsystem</module>
- <module>wf9-server-subsystem</module>
+ <module>wildfly-subsystem</module>
</modules>
</project>
\ No newline at end of file
diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java
index e5c9484..06df073 100755
--- a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java
+++ b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java
@@ -5,6 +5,7 @@ import org.keycloak.migration.migrators.MigrateTo1_3_0;
import org.keycloak.migration.migrators.MigrateTo1_4_0;
import org.keycloak.migration.migrators.MigrateTo1_5_0;
import org.keycloak.migration.migrators.MigrateTo1_6_0;
+import org.keycloak.migration.migrators.MigrateTo1_7_0;
import org.keycloak.migration.migrators.MigrationTo1_2_0_CR1;
import org.keycloak.models.KeycloakSession;
@@ -54,6 +55,12 @@ public class MigrationModelManager {
}
new MigrateTo1_6_0().migrate(session);
}
+ if (stored == null || stored.lessThan(MigrateTo1_7_0.VERSION)) {
+ if (stored != null) {
+ logger.debug("Migrating older model to 1.7.0 updates");
+ }
+ new MigrateTo1_7_0().migrate(session);
+ }
model.setStoredVersion(MigrationModel.LATEST_VERSION);
}
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_7_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_7_0.java
new file mode 100644
index 0000000..e4ead4d
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_7_0.java
@@ -0,0 +1,23 @@
+package org.keycloak.migration.migrators;
+
+import java.util.List;
+
+import org.keycloak.migration.ModelVersion;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MigrateTo1_7_0 {
+
+ public static final ModelVersion VERSION = new ModelVersion("1.7.0");
+
+ public void migrate(KeycloakSession session) {
+ List<RealmModel> realms = session.realms().getRealms();
+ for (RealmModel realm : realms) {
+ realm.setAccessTokenLifespanForImplicitFlow(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT);
+ }
+ }
+}
diff --git a/model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java b/model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
new file mode 100755
index 0000000..7447319
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/ClientInitialAccessModel.java
@@ -0,0 +1,22 @@
+package org.keycloak.models;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface ClientInitialAccessModel {
+
+ String getId();
+
+ RealmModel getRealm();
+
+ int getTimestamp();
+
+ int getExpiration();
+
+ int getCount();
+
+ int getRemainingCount();
+
+ void decreaseRemainingCount();
+
+}
diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java
index c421aea..492c2e1 100755
--- a/model/api/src/main/java/org/keycloak/models/ClientModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java
@@ -90,8 +90,8 @@ public interface ClientModel extends RoleContainerModel {
String getSecret();
public void setSecret(String secret);
- String getRegistrationSecret();
- void setRegistrationSecret(String registrationSecret);
+ String getRegistrationToken();
+ void setRegistrationToken(String registrationToken);
boolean isFullScopeAllowed();
void setFullScopeAllowed(boolean value);
@@ -111,12 +111,18 @@ public interface ClientModel extends RoleContainerModel {
boolean isPublicClient();
void setPublicClient(boolean flag);
- boolean isDirectGrantsOnly();
- void setDirectGrantsOnly(boolean flag);
-
boolean isConsentRequired();
void setConsentRequired(boolean consentRequired);
+ boolean isStandardFlowEnabled();
+ void setStandardFlowEnabled(boolean standardFlowEnabled);
+
+ boolean isImplicitFlowEnabled();
+ void setImplicitFlowEnabled(boolean implicitFlowEnabled);
+
+ boolean isDirectAccessGrantsEnabled();
+ void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled);
+
boolean isServiceAccountsEnabled();
void setServiceAccountsEnabled(boolean serviceAccountsEnabled);
diff --git a/model/api/src/main/java/org/keycloak/models/Constants.java b/model/api/src/main/java/org/keycloak/models/Constants.java
index 3ecc51c..c50e4cd 100755
--- a/model/api/src/main/java/org/keycloak/models/Constants.java
+++ b/model/api/src/main/java/org/keycloak/models/Constants.java
@@ -20,6 +20,8 @@ public interface Constants {
String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE};
String OFFLINE_ACCESS_ROLE = OAuth2Constants.OFFLINE_ACCESS;
+ // 15 minutes
+ int DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT = 900;
// 30 days
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
index f15614f..b5edbaf 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
@@ -17,7 +17,7 @@ public class ClientEntity extends AbstractIdentifiableEntity {
private boolean enabled;
private String clientAuthenticatorType;
private String secret;
- private String registrationSecret;
+ private String registrationToken;
private String protocol;
private int notBefore;
private boolean publicClient;
@@ -30,6 +30,9 @@ public class ClientEntity extends AbstractIdentifiableEntity {
private String baseUrl;
private boolean bearerOnly;
private boolean consentRequired;
+ private boolean standardFlowEnabled;
+ private boolean implicitFlowEnabled;
+ private boolean directAccessGrantsEnabled;
private boolean serviceAccountsEnabled;
private boolean directGrantsOnly;
private int nodeReRegistrationTimeout;
@@ -91,12 +94,12 @@ public class ClientEntity extends AbstractIdentifiableEntity {
this.secret = secret;
}
- public String getRegistrationSecret() {
- return registrationSecret;
+ public String getRegistrationToken() {
+ return registrationToken;
}
- public void setRegistrationSecret(String registrationSecret) {
- this.registrationSecret = registrationSecret;
+ public void setRegistrationToken(String registrationToken) {
+ this.registrationToken = registrationToken;
}
public int getNotBefore() {
@@ -243,6 +246,30 @@ public class ClientEntity extends AbstractIdentifiableEntity {
this.consentRequired = consentRequired;
}
+ public boolean isStandardFlowEnabled() {
+ return standardFlowEnabled;
+ }
+
+ public void setStandardFlowEnabled(boolean standardFlowEnabled) {
+ this.standardFlowEnabled = standardFlowEnabled;
+ }
+
+ public boolean isImplicitFlowEnabled() {
+ return implicitFlowEnabled;
+ }
+
+ public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
+ this.implicitFlowEnabled = implicitFlowEnabled;
+ }
+
+ public boolean isDirectAccessGrantsEnabled() {
+ return directAccessGrantsEnabled;
+ }
+
+ public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
+ this.directAccessGrantsEnabled = directAccessGrantsEnabled;
+ }
+
public boolean isServiceAccountsEnabled() {
return serviceAccountsEnabled;
}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
index 389ec0a..7a08bca 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
@@ -44,6 +44,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
private int ssoSessionMaxLifespan;
private int offlineSessionIdleTimeout;
private int accessTokenLifespan;
+ private int accessTokenLifespanForImplicitFlow;
private int accessCodeLifespan;
private int accessCodeLifespanUserAction;
private int accessCodeLifespanLogin;
@@ -61,6 +62,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
// We are using names of defaultRoles (not ids)
private List<String> defaultRoles = new ArrayList<String>();
+ private List<String> defaultGroups = new ArrayList<String>();
private List<RequiredCredentialEntity> requiredCredentials = new ArrayList<RequiredCredentialEntity>();
private List<UserFederationProviderEntity> userFederationProviders = new ArrayList<UserFederationProviderEntity>();
@@ -271,6 +273,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
this.accessTokenLifespan = accessTokenLifespan;
}
+ public int getAccessTokenLifespanForImplicitFlow() {
+ return accessTokenLifespanForImplicitFlow;
+ }
+
+ public void setAccessTokenLifespanForImplicitFlow(int accessTokenLifespanForImplicitFlow) {
+ this.accessTokenLifespanForImplicitFlow = accessTokenLifespanForImplicitFlow;
+ }
+
public int getAccessCodeLifespan() {
return accessCodeLifespan;
}
@@ -629,6 +639,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
public void setClientAuthenticationFlow(String clientAuthenticationFlow) {
this.clientAuthenticationFlow = clientAuthenticationFlow;
}
+
+ public List<String> getDefaultGroups() {
+ return defaultGroups;
+ }
+
+ public void setDefaultGroups(List<String> defaultGroups) {
+ this.defaultGroups = defaultGroups;
+ }
}
diff --git a/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
index aff7d37..3674334 100755
--- a/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
+++ b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java
@@ -76,6 +76,8 @@ public class PasswordPolicy implements Serializable {
list.add(new PasswordHistory(arg));
} else if (name.equals(ForceExpiredPasswordChange.NAME)) {
list.add(new ForceExpiredPasswordChange(arg));
+ } else {
+ throw new IllegalArgumentException("Unsupported policy");
}
}
return list;
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index c3bb5c2..6c2c260 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -107,6 +107,9 @@ public interface RealmModel extends RoleContainerModel {
void setAccessTokenLifespan(int seconds);
+ int getAccessTokenLifespanForImplicitFlow();
+ void setAccessTokenLifespanForImplicitFlow(int seconds);
+
int getAccessCodeLifespan();
void setAccessCodeLifespan(int seconds);
@@ -165,6 +168,12 @@ public interface RealmModel extends RoleContainerModel {
void updateDefaultRoles(String[] defaultRoles);
+ List<GroupModel> getDefaultGroups();
+
+ void addDefaultGroup(GroupModel group);
+
+ void removeDefaultGroup(GroupModel group);
+
// Key is clientId
Map<String, ClientModel> getClientNameMap();
@@ -330,6 +339,7 @@ public interface RealmModel extends RoleContainerModel {
void setDefaultLocale(String locale);
GroupModel createGroup(String name);
+ GroupModel createGroup(String id, String name);
/**
* Move Group to top realm level. Basically just sets group parent to null. You need to call this though
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
index d75ed95..1a1709b 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -462,6 +462,9 @@ public class UserFederationManager implements UserProvider {
}
+
+
+
@Override
public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) {
return validCredentials(realm, user, Arrays.asList(input));
diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
index 1a59f4f..0c1df9c 100755
--- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -63,6 +63,11 @@ public interface UserSessionProvider extends Provider {
UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline);
ClientSessionModel importClientSession(ClientSessionModel persistentClientSession, boolean offline);
+ ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count);
+ ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id);
+ void removeClientInitialAccessModel(RealmModel realm, String id);
+ List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
+
void close();
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java b/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java
index 7ce15d8..d3e4d4a 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java
@@ -1,6 +1,7 @@
package org.keycloak.models.utils;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.OTPPolicy;
import org.keycloak.models.PasswordPolicy;
@@ -51,6 +52,10 @@ public class CredentialValidation {
}
public static boolean validateHashedCredential(RealmModel realm, UserModel user, String unhashedCredValue, UserCredentialValueModel credential) {
+ if(unhashedCredValue == null){
+ return false;
+ }
+
boolean validated = new Pbkdf2PasswordEncoder(credential.getSalt()).verify(unhashedCredValue, credential.getValue(), credential.getHashIterations());
if (validated) {
int iterations = hashIterations(realm);
@@ -69,11 +74,11 @@ public class CredentialValidation {
}
public static boolean validPasswordToken(RealmModel realm, UserModel user, String encodedPasswordToken) {
- JWSInput jws = new JWSInput(encodedPasswordToken);
- if (!RSAProvider.verify(jws, realm.getPublicKey())) {
- return false;
- }
try {
+ JWSInput jws = new JWSInput(encodedPasswordToken);
+ if (!RSAProvider.verify(jws, realm.getPublicKey())) {
+ return false;
+ }
PasswordToken passwordToken = jws.readJsonContent(PasswordToken.class);
if (!passwordToken.getRealm().equals(realm.getName())) {
return false;
@@ -85,7 +90,7 @@ public class CredentialValidation {
return false;
}
return true;
- } catch (IOException e) {
+ } catch (JWSInputException e) {
return false;
}
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/HmacOTP.java b/model/api/src/main/java/org/keycloak/models/utils/HmacOTP.java
index 210f82b..1f42f57 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/HmacOTP.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/HmacOTP.java
@@ -3,7 +3,7 @@ package org.keycloak.models.utils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
-import java.util.Random;
+import java.security.SecureRandom;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -29,7 +29,7 @@ public class HmacOTP {
public static String generateSecret(int length) {
String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW1234567890";
- Random r = new Random();
+ SecureRandom r = new SecureRandom();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
char c = chars.charAt(r.nextInt(chars.length()));
diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index c35c58e..8c6af88 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -1,6 +1,7 @@
package org.keycloak.models.utils;
import org.bouncycastle.openssl.PEMWriter;
+import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Base64Url;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
@@ -27,8 +28,15 @@ import org.keycloak.common.util.PemUtils;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.StringWriter;
-import java.security.*;
+import java.security.Key;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
import java.security.cert.X509Certificate;
+import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -43,8 +51,6 @@ import java.util.UUID;
*/
public final class KeycloakModelUtils {
- private static final int RANDOM_PASSWORD_BYTES = 32;
-
private KeycloakModelUtils() {
}
@@ -52,6 +58,16 @@ public final class KeycloakModelUtils {
return UUID.randomUUID().toString();
}
+ public static String generateSecret() {
+ return generateSecret(32);
+ }
+
+ public static String generateSecret(int bytes) {
+ byte[] buf = new byte[bytes];
+ new SecureRandom().nextBytes(buf);
+ return Base64Url.encode(buf);
+ }
+
public static PublicKey getPublicKey(String publicKeyPem) {
if (publicKeyPem != null) {
try {
@@ -182,16 +198,6 @@ public final class KeycloakModelUtils {
return secret;
}
- public static void generateRegistrationAccessToken(ClientModel client) {
- client.setRegistrationSecret(generatePassword());
- }
-
- public static String generatePassword() {
- byte[] buf = new byte[RANDOM_PASSWORD_BYTES];
- new SecureRandom().nextBytes(buf);
- return Base64Url.encode(buf);
- }
-
public static String getDefaultClientAuthenticatorType() {
return "client-secret";
}
@@ -418,4 +424,101 @@ public final class KeycloakModelUtils {
}
}
}
+
+ public static String resolveFirstAttribute(GroupModel group, String name) {
+ String value = group.getFirstAttribute(name);
+ if (value != null) return value;
+ if (group.getParentId() == null) return null;
+ return resolveFirstAttribute(group.getParent(), name);
+
+ }
+
+ /**
+ *
+ *
+ * @param user
+ * @param name
+ * @return
+ */
+ public static String resolveFirstAttribute(UserModel user, String name) {
+ String value = user.getFirstAttribute(name);
+ if (value != null) return value;
+ for (GroupModel group : user.getGroups()) {
+ value = resolveFirstAttribute(group, name);
+ if (value != null) return value;
+ }
+ return null;
+
+ }
+
+ public static List<String> resolveAttribute(GroupModel group, String name) {
+ List<String> values = group.getAttribute(name);
+ if (values != null && !values.isEmpty()) return values;
+ if (group.getParentId() == null) return null;
+ return resolveAttribute(group.getParent(), name);
+
+ }
+
+
+ public static List<String> resolveAttribute(UserModel user, String name) {
+ List<String> values = user.getAttribute(name);
+ if (!values.isEmpty()) return values;
+ for (GroupModel group : user.getGroups()) {
+ values = resolveAttribute(group, name);
+ if (values != null) return values;
+ }
+ return Collections.emptyList();
+ }
+
+
+ private static GroupModel findSubGroup(String[] path, int index, GroupModel parent) {
+ for (GroupModel group : parent.getSubGroups()) {
+ if (group.getName().equals(path[index])) {
+ if (path.length == index + 1) {
+ return group;
+ }
+ else {
+ if (index + 1 < path.length) {
+ GroupModel found = findSubGroup(path, index + 1, group);
+ if (found != null) return found;
+ } else {
+ return null;
+ }
+ }
+
+ }
+ }
+ return null;
+ }
+
+ public static GroupModel findGroupByPath(RealmModel realm, String path) {
+ if (path == null) {
+ return null;
+ }
+ if (path.startsWith("/")) {
+ path = path.substring(1);
+ }
+ if (path.endsWith("/")) {
+ path = path.substring(0, path.length() - 1);
+ }
+ String[] split = path.split("/");
+ if (split.length == 0) return null;
+ GroupModel found = null;
+ for (GroupModel group : realm.getTopLevelGroups()) {
+ if (group.getName().equals(split[0])) {
+ if (split.length == 1) {
+ found = group;
+ break;
+ }
+ else {
+ if (split.length > 1) {
+ found = findSubGroup(split, 1, group);
+ if (found != null) break;
+ }
+ }
+
+ }
+ }
+ return found;
+ }
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index a69bb2b..e60d915 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -205,6 +205,7 @@ public class ModelToRepresentation {
rep.setEditUsernameAllowed(realm.isEditUsernameAllowed());
rep.setRevokeRefreshToken(realm.isRevokeRefreshToken());
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
+ rep.setAccessTokenLifespanForImplicitFlow(realm.getAccessTokenLifespanForImplicitFlow());
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
rep.setOfflineSessionIdleTimeout(realm.getOfflineSessionIdleTimeout());
@@ -239,6 +240,14 @@ public class ModelToRepresentation {
roleStrings.addAll(defaultRoles);
rep.setDefaultRoles(roleStrings);
}
+ List<GroupModel> defaultGroups = realm.getDefaultGroups();
+ if (!defaultGroups.isEmpty()) {
+ List<String> groupPaths = new LinkedList<>();
+ for (GroupModel group : defaultGroups) {
+ groupPaths.add(ModelToRepresentation.buildGroupPath(group));
+ }
+ rep.setDefaultGroups(groupPaths);
+ }
List<RequiredCredentialModel> requiredCredentialModels = realm.getRequiredCredentials();
if (requiredCredentialModels.size() > 0) {
@@ -279,10 +288,16 @@ public class ModelToRepresentation {
if (internal) {
exportAuthenticationFlows(realm, rep);
exportRequiredActions(realm, rep);
+ exportGroups(realm, rep);
}
return rep;
}
+ public static void exportGroups(RealmModel realm, RealmRepresentation rep) {
+ List<GroupRepresentation> groups = toGroupHierarchy(realm, true);
+ rep.setGroups(groups);
+ }
+
public static void exportAuthenticationFlows(RealmModel realm, RealmRepresentation rep) {
rep.setAuthenticationFlows(new LinkedList<AuthenticationFlowRepresentation>());
rep.setAuthenticatorConfig(new LinkedList<AuthenticatorConfigRepresentation>());
@@ -404,15 +419,16 @@ public class ModelToRepresentation {
rep.setFullScopeAllowed(clientModel.isFullScopeAllowed());
rep.setBearerOnly(clientModel.isBearerOnly());
rep.setConsentRequired(clientModel.isConsentRequired());
+ rep.setStandardFlowEnabled(clientModel.isStandardFlowEnabled());
+ rep.setImplicitFlowEnabled(clientModel.isImplicitFlowEnabled());
+ rep.setDirectAccessGrantsEnabled(clientModel.isDirectAccessGrantsEnabled());
rep.setServiceAccountsEnabled(clientModel.isServiceAccountsEnabled());
- rep.setDirectGrantsOnly(clientModel.isDirectGrantsOnly());
rep.setSurrogateAuthRequired(clientModel.isSurrogateAuthRequired());
rep.setRootUrl(clientModel.getRootUrl());
rep.setBaseUrl(clientModel.getBaseUrl());
rep.setNotBefore(clientModel.getNotBefore());
rep.setNodeReRegistrationTimeout(clientModel.getNodeReRegistrationTimeout());
rep.setClientAuthenticatorType(clientModel.getClientAuthenticatorType());
- rep.setRegistrationAccessToken(clientModel.getRegistrationSecret());
Set<String> redirectUris = clientModel.getRedirectUris();
if (redirectUris != null) {
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index a46ee57..8360e48 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -12,6 +12,7 @@ import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.ClaimMask;
import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel;
+import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
@@ -36,6 +37,7 @@ import org.keycloak.representations.idm.ClaimRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
+import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OAuthClientRepresentation;
@@ -67,12 +69,12 @@ public class RepresentationToModel {
private static Logger logger = Logger.getLogger(RepresentationToModel.class);
public static OTPPolicy toPolicy(RealmRepresentation rep) {
OTPPolicy policy = new OTPPolicy();
- policy.setType(rep.getOtpPolicyType());
- policy.setLookAheadWindow(rep.getOtpPolicyLookAheadWindow());
- policy.setInitialCounter(rep.getOtpPolicyInitialCounter());
- policy.setAlgorithm(rep.getOtpPolicyAlgorithm());
- policy.setDigits(rep.getOtpPolicyDigits());
- policy.setPeriod(rep.getOtpPolicyPeriod());
+ if (rep.getOtpPolicyType() != null) policy.setType(rep.getOtpPolicyType());
+ if (rep.getOtpPolicyLookAheadWindow() != null) policy.setLookAheadWindow(rep.getOtpPolicyLookAheadWindow());
+ if (rep.getOtpPolicyInitialCounter() != null) policy.setInitialCounter(rep.getOtpPolicyInitialCounter());
+ if (rep.getOtpPolicyAlgorithm() != null) policy.setAlgorithm(rep.getOtpPolicyAlgorithm());
+ if (rep.getOtpPolicyDigits() != null) policy.setDigits(rep.getOtpPolicyDigits());
+ if (rep.getOtpPolicyPeriod() != null) policy.setPeriod(rep.getOtpPolicyPeriod());
return policy;
}
@@ -103,6 +105,9 @@ public class RepresentationToModel {
if (rep.getAccessTokenLifespan() != null) newRealm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
else newRealm.setAccessTokenLifespan(300);
+ if (rep.getAccessTokenLifespanForImplicitFlow() != null) newRealm.setAccessTokenLifespanForImplicitFlow(rep.getAccessTokenLifespanForImplicitFlow());
+ else newRealm.setAccessTokenLifespanForImplicitFlow(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT);
+
if (rep.getSsoSessionIdleTimeout() != null) newRealm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
else newRealm.setSsoSessionIdleTimeout(1800);
if (rep.getSsoSessionMaxLifespan() != null) newRealm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
@@ -311,6 +316,18 @@ public class RepresentationToModel {
}
}
+ if (rep.getGroups() != null) {
+ importGroups(newRealm, rep);
+ if (rep.getDefaultGroups() != null) {
+ for (String path : rep.getDefaultGroups()) {
+ GroupModel found = KeycloakModelUtils.findGroupByPath(newRealm, path);
+ if (found == null) throw new RuntimeException("default group in realm rep doesn't exist: " + path);
+ newRealm.addDefaultGroup(found);
+ }
+ }
+ }
+
+
// create users and their role mappings and social mappings
if (rep.getUsers() != null) {
@@ -330,6 +347,59 @@ public class RepresentationToModel {
}
}
+ public static void importGroups(RealmModel realm, RealmRepresentation rep) {
+ List<GroupRepresentation> groups = rep.getGroups();
+ if (groups == null) return;
+
+ Map<String, ClientModel> clientMap = realm.getClientNameMap();
+ GroupModel parent = null;
+ for (GroupRepresentation group : groups) {
+ importGroup(realm, clientMap, parent, group);
+ }
+ }
+
+ public static void importGroup(RealmModel realm, Map<String, ClientModel> clientMap, GroupModel parent, GroupRepresentation group) {
+ GroupModel newGroup = realm.createGroup(group.getId(), group.getName());
+ if (group.getAttributes() != null) {
+ for (Map.Entry<String, List<String>> attr : group.getAttributes().entrySet()) {
+ newGroup.setAttribute(attr.getKey(), attr.getValue());
+ }
+ }
+ realm.moveGroup(newGroup, parent);
+
+ if (group.getRealmRoles() != null) {
+ for (String roleString : group.getRealmRoles()) {
+ RoleModel role = realm.getRole(roleString.trim());
+ if (role == null) {
+ role = realm.addRole(roleString.trim());
+ }
+ newGroup.grantRole(role);
+ }
+ }
+ if (group.getClientRoles() != null) {
+ for (Map.Entry<String, List<String>> entry : group.getClientRoles().entrySet()) {
+ ClientModel client = clientMap.get(entry.getKey());
+ if (client == null) {
+ throw new RuntimeException("Unable to find client role mappings for client: " + entry.getKey());
+ }
+ List<String> roleNames = entry.getValue();
+ for (String roleName : roleNames) {
+ RoleModel role = client.getRole(roleName.trim());
+ if (role == null) {
+ role = client.addRole(roleName.trim());
+ }
+ newGroup.grantRole(role);
+
+ }
+ }
+ }
+ if (group.getSubGroups() != null) {
+ for (GroupRepresentation subGroup : group.getSubGroups()) {
+ importGroup(realm, clientMap, newGroup, subGroup);
+ }
+ }
+ }
+
public static void importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) {
if (rep.getAuthenticationFlows() == null) {
// assume this is an old version being imported
@@ -536,6 +606,7 @@ public class RepresentationToModel {
if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore());
if (rep.getRevokeRefreshToken() != null) realm.setRevokeRefreshToken(rep.getRevokeRefreshToken());
if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
+ if (rep.getAccessTokenLifespanForImplicitFlow() != null) realm.setAccessTokenLifespanForImplicitFlow(rep.getAccessTokenLifespanForImplicitFlow());
if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
if (rep.getOfflineSessionIdleTimeout() != null) realm.setOfflineSessionIdleTimeout(rep.getOfflineSessionIdleTimeout());
@@ -705,8 +776,17 @@ public class RepresentationToModel {
if (resourceRep.getBaseUrl() != null) client.setBaseUrl(resourceRep.getBaseUrl());
if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly());
if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired());
+ if (resourceRep.isStandardFlowEnabled() != null) client.setStandardFlowEnabled(resourceRep.isStandardFlowEnabled());
+ if (resourceRep.isImplicitFlowEnabled() != null) client.setImplicitFlowEnabled(resourceRep.isImplicitFlowEnabled());
+ if (resourceRep.isDirectAccessGrantsEnabled() != null) client.setDirectAccessGrantsEnabled(resourceRep.isDirectAccessGrantsEnabled());
if (resourceRep.isServiceAccountsEnabled() != null) client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled());
- if (resourceRep.isDirectGrantsOnly() != null) client.setDirectGrantsOnly(resourceRep.isDirectGrantsOnly());
+
+ // Backwards compatibility only
+ if (resourceRep.isDirectGrantsOnly() != null) {
+ logger.warn("Using deprecated 'directGrantsOnly' configuration in JSON representation. It will be removed in future versions");
+ client.setStandardFlowEnabled(!resourceRep.isDirectGrantsOnly());
+ }
+
if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient());
if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout());
if (resourceRep.getProtocol() != null) client.setProtocol(resourceRep.getProtocol());
@@ -737,8 +817,6 @@ public class RepresentationToModel {
KeycloakModelUtils.generateSecret(client);
}
- client.setRegistrationSecret(resourceRep.getRegistrationAccessToken());
-
if (resourceRep.getAttributes() != null) {
for (Map.Entry<String, String> entry : resourceRep.getAttributes().entrySet()) {
client.setAttribute(entry.getKey(), entry.getValue());
@@ -804,8 +882,10 @@ public class RepresentationToModel {
if (rep.isEnabled() != null) resource.setEnabled(rep.isEnabled());
if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly());
if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired());
+ if (rep.isStandardFlowEnabled() != null) resource.setStandardFlowEnabled(rep.isStandardFlowEnabled());
+ if (rep.isImplicitFlowEnabled() != null) resource.setImplicitFlowEnabled(rep.isImplicitFlowEnabled());
+ if (rep.isDirectAccessGrantsEnabled() != null) resource.setDirectAccessGrantsEnabled(rep.isDirectAccessGrantsEnabled());
if (rep.isServiceAccountsEnabled() != null) resource.setServiceAccountsEnabled(rep.isServiceAccountsEnabled());
- if (rep.isDirectGrantsOnly() != null) resource.setDirectGrantsOnly(rep.isDirectGrantsOnly());
if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient());
if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed());
if (rep.isFrontchannelLogout() != null) resource.setFrontchannelLogout(rep.isFrontchannelLogout());
@@ -815,7 +895,6 @@ public class RepresentationToModel {
if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired());
if (rep.getNodeReRegistrationTimeout() != null) resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout());
if (rep.getClientAuthenticatorType() != null) resource.setClientAuthenticatorType(rep.getClientAuthenticatorType());
- if (rep.getRegistrationAccessToken() != null) resource.setRegistrationSecret(rep.getRegistrationAccessToken());
resource.updateClient();
if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol());
@@ -932,14 +1011,14 @@ public class RepresentationToModel {
// Import users just to user storage. Don't federate
UserModel user = session.userStorage().addUser(newRealm, userRep.getId(), userRep.getUsername(), false, false);
- user.setEnabled(userRep.isEnabled());
+ user.setEnabled(userRep.isEnabled() != null && userRep.isEnabled());
user.setCreatedTimestamp(userRep.getCreatedTimestamp());
user.setEmail(userRep.getEmail());
- user.setEmailVerified(userRep.isEmailVerified());
+ if (userRep.isEmailVerified() != null) user.setEmailVerified(userRep.isEmailVerified());
user.setFirstName(userRep.getFirstName());
user.setLastName(userRep.getLastName());
user.setFederationLink(userRep.getFederationLink());
- user.setOtpEnabled(userRep.isTotp());
+ if (userRep.isTotp() != null) user.setOtpEnabled(userRep.isTotp());
if (userRep.getAttributes() != null) {
for (Map.Entry<String, Object> entry : userRep.getAttributes().entrySet()) {
Object value = entry.getValue();
@@ -1002,6 +1081,16 @@ public class RepresentationToModel {
}
user.setServiceAccountClientLink(client.getId());;
}
+ if (userRep.getGroups() != null) {
+ for (String path : userRep.getGroups()) {
+ GroupModel group = KeycloakModelUtils.findGroupByPath(newRealm, path);
+ if (group == null) {
+ throw new RuntimeException("Unable to find group specified by path: " + path);
+
+ }
+ user.joinGroup(group);
+ }
+ }
return user;
}
diff --git a/model/api/src/test/java/org/keycloak/models/PasswordPolicyTest.java b/model/api/src/test/java/org/keycloak/models/PasswordPolicyTest.java
index df76588..8c662fb 100755
--- a/model/api/src/test/java/org/keycloak/models/PasswordPolicyTest.java
+++ b/model/api/src/test/java/org/keycloak/models/PasswordPolicyTest.java
@@ -83,6 +83,15 @@ public class PasswordPolicyTest {
Assert.assertEquals("invalidPasswordNotUsernameMessage", policy.validate("jdoe", "jdoe").getMessage());
Assert.assertNull(policy.validate("jdoe", "ab&d1234"));
}
+
+ @Test
+ public void testInvalidPolicyName() {
+ try {
+ PasswordPolicy policy = new PasswordPolicy("noSuchPolicy");
+ Assert.fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ }
+ }
@Test
public void testRegexPatterns() {
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
index 7a87373..20c8725 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
@@ -120,14 +120,14 @@ public class ClientAdapter implements ClientModel {
getDelegateForUpdate();
updated.setSecret(secret);
}
- public String getRegistrationSecret() {
- if (updated != null) return updated.getRegistrationSecret();
- return cached.getRegistrationSecret();
+ public String getRegistrationToken() {
+ if (updated != null) return updated.getRegistrationToken();
+ return cached.getRegistrationToken();
}
- public void setRegistrationSecret(String registrationsecret) {
+ public void setRegistrationToken(String registrationToken) {
getDelegateForUpdate();
- updated.setRegistrationSecret(registrationsecret);
+ updated.setRegistrationToken(registrationToken);
}
public boolean isPublicClient() {
@@ -163,16 +163,6 @@ public class ClientAdapter implements ClientModel {
}
- public boolean isDirectGrantsOnly() {
- if (updated != null) return updated.isDirectGrantsOnly();
- return cached.isDirectGrantsOnly();
- }
-
- public void setDirectGrantsOnly(boolean flag) {
- getDelegateForUpdate();
- updated.setDirectGrantsOnly(flag);
- }
-
public Set<RoleModel> getScopeMappings() {
if (updated != null) return updated.getScopeMappings();
Set<RoleModel> roles = new HashSet<RoleModel>();
@@ -452,6 +442,42 @@ public class ClientAdapter implements ClientModel {
}
@Override
+ public boolean isStandardFlowEnabled() {
+ if (updated != null) return updated.isStandardFlowEnabled();
+ return cached.isStandardFlowEnabled();
+ }
+
+ @Override
+ public void setStandardFlowEnabled(boolean standardFlowEnabled) {
+ getDelegateForUpdate();
+ updated.setStandardFlowEnabled(standardFlowEnabled);
+ }
+
+ @Override
+ public boolean isImplicitFlowEnabled() {
+ if (updated != null) return updated.isImplicitFlowEnabled();
+ return cached.isImplicitFlowEnabled();
+ }
+
+ @Override
+ public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
+ getDelegateForUpdate();
+ updated.setImplicitFlowEnabled(implicitFlowEnabled);
+ }
+
+ @Override
+ public boolean isDirectAccessGrantsEnabled() {
+ if (updated != null) return updated.isDirectAccessGrantsEnabled();
+ return cached.isDirectAccessGrantsEnabled();
+ }
+
+ @Override
+ public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
+ getDelegateForUpdate();
+ updated.setDirectAccessGrantsEnabled(directAccessGrantsEnabled);
+ }
+
+ @Override
public boolean isServiceAccountsEnabled() {
if (updated != null) return updated.isServiceAccountsEnabled();
return cached.isServiceAccountsEnabled();
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java
index b046efc..60c089b 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java
@@ -1,11 +1,14 @@
package org.keycloak.models.cache.infinispan;
import org.infinispan.Cache;
+import org.infinispan.commands.write.RemoveCommand;
+import org.infinispan.container.entries.CacheEntry;
+import org.infinispan.context.InvocationContext;
+import org.infinispan.interceptors.base.CommandInterceptor;
import org.infinispan.notifications.Listener;
-import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
-import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
-import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
-import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
+import org.infinispan.notifications.cachelistener.annotation.*;
+import org.infinispan.notifications.cachelistener.event.*;
+import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
@@ -21,6 +24,8 @@ import java.util.concurrent.ConcurrentHashMap;
*/
public class InfinispanCacheUserProviderFactory implements CacheUserProviderFactory {
+ private static final Logger log = Logger.getLogger(InfinispanCacheUserProviderFactory.class);
+
protected InfinispanUserCache userCache;
protected final RealmLookup usernameLookup = new RealmLookup();
@@ -82,36 +87,63 @@ public class InfinispanCacheUserProviderFactory implements CacheUserProviderFact
@CacheEntryCreated
public void userCreated(CacheEntryCreatedEvent<String, CachedUser> event) {
if (!event.isPre()) {
-
- CachedUser cachedUser;
+ CachedUser user;
// Try optimized version if available
if (isNewInfinispan) {
- cachedUser = event.getValue();
+ user = event.getValue();
} else {
String userId = event.getKey();
- cachedUser = event.getCache().get(userId);
+ user = event.getCache().get(userId);
}
- if (cachedUser != null) {
- String realm = cachedUser.getRealm();
- usernameLookup.put(realm, cachedUser.getUsername(), cachedUser.getId());
- if (cachedUser.getEmail() != null) {
- emailLookup.put(realm, cachedUser.getEmail(), cachedUser.getId());
+ if (user != null) {
+ String realm = user.getRealm();
+
+ usernameLookup.put(realm, user.getUsername(), user.getId());
+ if (user.getEmail() != null) {
+ emailLookup.put(realm, user.getEmail(), user.getId());
}
+
+ log.tracev("User added realm={0}, id={1}, username={2}", realm, event.getValue().getId(), event.getValue().getUsername());
}
}
}
@CacheEntryRemoved
public void userRemoved(CacheEntryRemovedEvent<String, CachedUser> event) {
- if (event.isPre() && event.getValue() != null) {
- CachedUser cachedUser = event.getValue();
- String realm = cachedUser.getRealm();
- usernameLookup.remove(realm, cachedUser.getUsername());
- if (cachedUser.getEmail() != null) {
- emailLookup.remove(realm, cachedUser.getEmail());
- }
+ CachedUser user = event.getOldValue();
+ if (event.isPre() && user != null) {
+ removeUser(user);
+
+ log.tracev("User invalidated realm={0}, id={1}, username={2}", user.getRealm(), user.getId(), user.getUsername());
+ }
+ }
+
+ @CacheEntryInvalidated
+ public void userInvalidated(CacheEntryInvalidatedEvent<String, CachedUser> event) {
+ CachedUser user = event.getValue();
+ if (event.isPre() && user != null) {
+ removeUser(user);
+
+ log.tracev("User invalidated realm={0}, id={1}, username={2}", user.getRealm(), user.getId(), user.getUsername());
+ }
+ }
+
+ @CacheEntriesEvicted
+ public void userEvicted(CacheEntriesEvictedEvent<String, CachedUser> event) {
+ for (CachedUser user : event.getEntries().values()) {
+ removeUser(user);
+
+ log.tracev("User evicted realm={0}, id={1}, username={2}", user.getRealm(), user.getId(), user.getUsername());
+ }
+ }
+
+ private void removeUser(CachedUser cachedUser) {
+ String realm = cachedUser.getRealm();
+ usernameLookup.remove(realm, cachedUser.getUsername());
+ if (cachedUser.getEmail() != null) {
+ emailLookup.remove(realm, cachedUser.getEmail());
}
}
@@ -119,12 +151,12 @@ public class InfinispanCacheUserProviderFactory implements CacheUserProviderFact
static class RealmLookup {
- protected final ConcurrentHashMap<String, ConcurrentHashMap<String, String>> lookup = new ConcurrentHashMap<String, ConcurrentHashMap<String, String>>();
+ protected final ConcurrentHashMap<String, ConcurrentHashMap<String, String>> lookup = new ConcurrentHashMap<>();
public void put(String realm, String key, String value) {
ConcurrentHashMap<String, String> map = lookup.get(realm);
if(map == null) {
- map = new ConcurrentHashMap<String, String>();
+ map = new ConcurrentHashMap<>();
ConcurrentHashMap<String, String> p = lookup.putIfAbsent(realm, map);
if (p != null) {
map = p;
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index be9fe7c..c24f151 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -301,6 +301,18 @@ public class RealmAdapter implements RealmModel {
}
@Override
+ public int getAccessTokenLifespanForImplicitFlow() {
+ if (updated != null) return updated.getAccessTokenLifespanForImplicitFlow();
+ return cached.getAccessTokenLifespanForImplicitFlow();
+ }
+
+ @Override
+ public void setAccessTokenLifespanForImplicitFlow(int seconds) {
+ getDelegateForUpdate();
+ updated.setAccessTokenLifespanForImplicitFlow(seconds);
+ }
+
+ @Override
public int getAccessCodeLifespan() {
if (updated != null) return updated.getAccessCodeLifespan();
return cached.getAccessCodeLifespan();
@@ -481,6 +493,30 @@ public class RealmAdapter implements RealmModel {
}
@Override
+ public List<GroupModel> getDefaultGroups() {
+ List<GroupModel> defaultGroups = new LinkedList<>();
+ for (String id : cached.getDefaultGroups()) {
+ defaultGroups.add(cacheSession.getGroupById(id, this));
+ }
+ return defaultGroups;
+
+ }
+
+ @Override
+ public void addDefaultGroup(GroupModel group) {
+ getDelegateForUpdate();
+ updated.addDefaultGroup(group);
+
+ }
+
+ @Override
+ public void removeDefaultGroup(GroupModel group) {
+ getDelegateForUpdate();
+ updated.removeDefaultGroup(group);
+
+ }
+
+ @Override
public List<String> getDefaultRoles() {
if (updated != null) return updated.getDefaultRoles();
return cached.getDefaultRoles();
@@ -1308,6 +1344,12 @@ public class RealmAdapter implements RealmModel {
}
@Override
+ public GroupModel createGroup(String id, String name) {
+ getDelegateForUpdate();
+ return updated.createGroup(id, name);
+ }
+
+ @Override
public void addTopLevelGroup(GroupModel subGroup) {
getDelegateForUpdate();
updated.addTopLevelGroup(subGroup);
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
index 7113fde..8240005 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
@@ -321,7 +321,7 @@ public class UserAdapter implements UserModel {
public Set<GroupModel> getGroups() {
if (updated != null) return updated.getGroups();
Set<GroupModel> groups = new HashSet<GroupModel>();
- for (String id : cached.getRoleMappings()) {
+ for (String id : cached.getGroups()) {
GroupModel groupModel = keycloakSession.realms().getGroupById(id, realm);
if (groupModel == null) {
// chance that role was removed, so just delete to persistence and get user invalidated
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
index 1c04b9d..a683956 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
@@ -31,12 +31,11 @@ public class CachedClient implements Serializable {
private boolean enabled;
private String clientAuthenticatorType;
private String secret;
- private String registrationSecret;
+ private String registrationToken;
private String protocol;
private Map<String, String> attributes = new HashMap<String, String>();
private boolean publicClient;
private boolean fullScopeAllowed;
- private boolean directGrantsOnly;
private boolean frontchannelLogout;
private int notBefore;
private Set<String> scope = new HashSet<String>();
@@ -49,6 +48,9 @@ public class CachedClient implements Serializable {
private List<String> defaultRoles = new LinkedList<String>();
private boolean bearerOnly;
private boolean consentRequired;
+ private boolean standardFlowEnabled;
+ private boolean implicitFlowEnabled;
+ private boolean directAccessGrantsEnabled;
private boolean serviceAccountsEnabled;
private Map<String, String> roles = new HashMap<String, String>();
private int nodeReRegistrationTimeout;
@@ -58,7 +60,7 @@ public class CachedClient implements Serializable {
id = model.getId();
clientAuthenticatorType = model.getClientAuthenticatorType();
secret = model.getSecret();
- registrationSecret = model.getRegistrationSecret();
+ registrationToken = model.getRegistrationToken();
clientId = model.getClientId();
name = model.getName();
description = model.getDescription();
@@ -67,7 +69,6 @@ public class CachedClient implements Serializable {
protocol = model.getProtocol();
attributes.putAll(model.getAttributes());
notBefore = model.getNotBefore();
- directGrantsOnly = model.isDirectGrantsOnly();
frontchannelLogout = model.isFrontchannelLogout();
publicClient = model.isPublicClient();
fullScopeAllowed = model.isFullScopeAllowed();
@@ -86,6 +87,9 @@ public class CachedClient implements Serializable {
defaultRoles.addAll(model.getDefaultRoles());
bearerOnly = model.isBearerOnly();
consentRequired = model.isConsentRequired();
+ standardFlowEnabled = model.isStandardFlowEnabled();
+ implicitFlowEnabled = model.isImplicitFlowEnabled();
+ directAccessGrantsEnabled = model.isDirectAccessGrantsEnabled();
serviceAccountsEnabled = model.isServiceAccountsEnabled();
for (RoleModel role : model.getRoles()) {
roles.put(role.getName(), role.getId());
@@ -131,18 +135,14 @@ public class CachedClient implements Serializable {
return secret;
}
- public String getRegistrationSecret() {
- return registrationSecret;
+ public String getRegistrationToken() {
+ return registrationToken;
}
public boolean isPublicClient() {
return publicClient;
}
- public boolean isDirectGrantsOnly() {
- return directGrantsOnly;
- }
-
public int getNotBefore() {
return notBefore;
}
@@ -203,6 +203,18 @@ public class CachedClient implements Serializable {
return consentRequired;
}
+ public boolean isStandardFlowEnabled() {
+ return standardFlowEnabled;
+ }
+
+ public boolean isImplicitFlowEnabled() {
+ return implicitFlowEnabled;
+ }
+
+ public boolean isDirectAccessGrantsEnabled() {
+ return directAccessGrantsEnabled;
+ }
+
public boolean isServiceAccountsEnabled() {
return serviceAccountsEnabled;
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
index 604d7fa..e423562 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
@@ -61,6 +61,7 @@ public class CachedRealm implements Serializable {
private int ssoSessionMaxLifespan;
private int offlineSessionIdleTimeout;
private int accessTokenLifespan;
+ private int accessTokenLifespanForImplicitFlow;
private int accessCodeLifespan;
private int accessCodeLifespanUserAction;
private int accessCodeLifespanLogin;
@@ -107,6 +108,7 @@ public class CachedRealm implements Serializable {
protected Set<String> adminEnabledEventOperations = new HashSet<String>();
protected boolean adminEventsDetailsEnabled;
private List<String> defaultRoles = new LinkedList<String>();
+ private List<String> defaultGroups = new LinkedList<String>();
private Set<String> groups = new HashSet<String>();
private Map<String, String> realmRoles = new HashMap<String, String>();
private Map<String, String> clients = new HashMap<String, String>();
@@ -145,6 +147,7 @@ public class CachedRealm implements Serializable {
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
offlineSessionIdleTimeout = model.getOfflineSessionIdleTimeout();
accessTokenLifespan = model.getAccessTokenLifespan();
+ accessTokenLifespanForImplicitFlow = model.getAccessTokenLifespanForImplicitFlow();
accessCodeLifespan = model.getAccessCodeLifespan();
accessCodeLifespanUserAction = model.getAccessCodeLifespanUserAction();
accessCodeLifespanLogin = model.getAccessCodeLifespanLogin();
@@ -229,6 +232,10 @@ public class CachedRealm implements Serializable {
requiredActionProvidersByAlias.put(action.getAlias(), action);
}
+ for (GroupModel group : model.getDefaultGroups()) {
+ defaultGroups.add(group.getId());
+ }
+
browserFlow = model.getBrowserFlow();
registrationFlow = model.getRegistrationFlow();
directGrantFlow = model.getDirectGrantFlow();
@@ -342,6 +349,10 @@ public class CachedRealm implements Serializable {
return accessTokenLifespan;
}
+ public int getAccessTokenLifespanForImplicitFlow() {
+ return accessTokenLifespanForImplicitFlow;
+ }
+
public int getAccessCodeLifespan() {
return accessCodeLifespan;
}
@@ -516,4 +527,8 @@ public class CachedRealm implements Serializable {
public Set<String> getGroups() {
return groups;
}
+
+ public List<String> getDefaultGroups() {
+ return defaultGroups;
+ }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
index 5ea0b11..f677859 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
@@ -3,6 +3,7 @@ package org.keycloak.models.jpa;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
@@ -178,13 +179,13 @@ public class ClientAdapter implements ClientModel {
}
@Override
- public String getRegistrationSecret() {
- return entity.getRegistrationSecret();
+ public String getRegistrationToken() {
+ return entity.getRegistrationToken();
}
@Override
- public void setRegistrationSecret(String registrationSecret) {
- entity.setRegistrationSecret(registrationSecret);
+ public void setRegistrationToken(String registrationToken) {
+ entity.setRegistrationToken(registrationToken);
}
@Override
@@ -328,7 +329,7 @@ public class ClientAdapter implements ClientModel {
@Override
public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
if (getProtocolMapperByName(model.getProtocol(), model.getName()) != null) {
- throw new RuntimeException("protocol mapper name must be unique per protocol");
+ throw new ModelDuplicateException("Protocol mapper name must be unique per protocol");
}
String id = model.getId() != null ? model.getId() : KeycloakModelUtils.generateId();
ProtocolMapperEntity entity = new ProtocolMapperEntity();
@@ -498,23 +499,43 @@ public class ClientAdapter implements ClientModel {
}
@Override
- public boolean isServiceAccountsEnabled() {
- return entity.isServiceAccountsEnabled();
+ public boolean isStandardFlowEnabled() {
+ return entity.isStandardFlowEnabled();
}
@Override
- public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
- entity.setServiceAccountsEnabled(serviceAccountsEnabled);
+ public void setStandardFlowEnabled(boolean standardFlowEnabled) {
+ entity.setStandardFlowEnabled(standardFlowEnabled);
+ }
+
+ @Override
+ public boolean isImplicitFlowEnabled() {
+ return entity.isImplicitFlowEnabled();
+ }
+
+ @Override
+ public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
+ entity.setImplicitFlowEnabled(implicitFlowEnabled);
}
@Override
- public boolean isDirectGrantsOnly() {
- return entity.isDirectGrantsOnly();
+ public boolean isDirectAccessGrantsEnabled() {
+ return entity.isDirectAccessGrantsEnabled();
}
@Override
- public void setDirectGrantsOnly(boolean flag) {
- entity.setDirectGrantsOnly(flag);
+ public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
+ entity.setDirectAccessGrantsEnabled(directAccessGrantsEnabled);
+ }
+
+ @Override
+ public boolean isServiceAccountsEnabled() {
+ return entity.isServiceAccountsEnabled();
+ }
+
+ @Override
+ public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
+ entity.setServiceAccountsEnabled(serviceAccountsEnabled);
}
@Override
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
index 881b129..1569875 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
@@ -42,8 +42,8 @@ public class ClientEntity {
private boolean enabled;
@Column(name="SECRET")
private String secret;
- @Column(name="REGISTRATION_SECRET")
- private String registrationSecret;
+ @Column(name="REGISTRATION_TOKEN")
+ private String registrationToken;
@Column(name="CLIENT_AUTHENTICATOR_TYPE")
private String clientAuthenticatorType;
@Column(name="NOT_BEFORE")
@@ -95,15 +95,21 @@ public class ClientEntity {
@Column(name="MANAGEMENT_URL")
private String managementUrl;
- @Column(name="DIRECT_GRANTS_ONLY")
- protected boolean directGrantsOnly;
-
@Column(name="BEARER_ONLY")
private boolean bearerOnly;
@Column(name="CONSENT_REQUIRED")
private boolean consentRequired;
+ @Column(name="STANDARD_FLOW_ENABLED")
+ private boolean standardFlowEnabled;
+
+ @Column(name="IMPLICIT_FLOW_ENABLED")
+ private boolean implicitFlowEnabled;
+
+ @Column(name="DIRECT_ACCESS_GRANTS_ENABLED")
+ private boolean directAccessGrantsEnabled;
+
@Column(name="SERVICE_ACCOUNTS_ENABLED")
private boolean serviceAccountsEnabled;
@@ -203,12 +209,12 @@ public class ClientEntity {
this.secret = secret;
}
- public String getRegistrationSecret() {
- return registrationSecret;
+ public String getRegistrationToken() {
+ return registrationToken;
}
- public void setRegistrationSecret(String registrationSecret) {
- this.registrationSecret = registrationSecret;
+ public void setRegistrationToken(String registrationToken) {
+ this.registrationToken = registrationToken;
}
public int getNotBefore() {
@@ -339,20 +345,36 @@ public class ClientEntity {
this.consentRequired = consentRequired;
}
- public boolean isServiceAccountsEnabled() {
- return serviceAccountsEnabled;
+ public boolean isStandardFlowEnabled() {
+ return standardFlowEnabled;
}
- public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
- this.serviceAccountsEnabled = serviceAccountsEnabled;
+ public void setStandardFlowEnabled(boolean standardFlowEnabled) {
+ this.standardFlowEnabled = standardFlowEnabled;
+ }
+
+ public boolean isImplicitFlowEnabled() {
+ return implicitFlowEnabled;
}
- public boolean isDirectGrantsOnly() {
- return directGrantsOnly;
+ public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
+ this.implicitFlowEnabled = implicitFlowEnabled;
}
- public void setDirectGrantsOnly(boolean directGrantsOnly) {
- this.directGrantsOnly = directGrantsOnly;
+ public boolean isDirectAccessGrantsEnabled() {
+ return directAccessGrantsEnabled;
+ }
+
+ public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
+ this.directAccessGrantsEnabled = directAccessGrantsEnabled;
+ }
+
+ public boolean isServiceAccountsEnabled() {
+ return serviceAccountsEnabled;
+ }
+
+ public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
+ this.serviceAccountsEnabled = serviceAccountsEnabled;
}
public int getNodeReRegistrationTimeout() {
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java
index d5851fd..2326cad 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java
@@ -92,7 +92,7 @@ public class GroupEntity {
@Override
public boolean equals(Object o) {
if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
+ if (o == null) return false;
GroupEntity that = (GroupEntity) o;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index 78de054..05af313 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -86,6 +86,8 @@ public class RealmEntity {
private int offlineSessionIdleTimeout;
@Column(name="ACCESS_TOKEN_LIFESPAN")
protected int accessTokenLifespan;
+ @Column(name="ACCESS_TOKEN_LIFE_IMPLICIT")
+ protected int accessTokenLifespanForImplicitFlow;
@Column(name="ACCESS_CODE_LIFESPAN")
protected int accessCodeLifespan;
@Column(name="USER_ACTION_LIFESPAN")
@@ -133,9 +135,6 @@ public class RealmEntity {
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
Collection<RoleEntity> roles = new ArrayList<RoleEntity>();
- @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
- Collection<GroupEntity> groups = new ArrayList<GroupEntity>();
-
@ElementCollection
@MapKeyColumn(name="NAME")
@Column(name="VALUE")
@@ -146,6 +145,10 @@ public class RealmEntity {
@JoinTable(name="REALM_DEFAULT_ROLES", joinColumns = { @JoinColumn(name="REALM_ID")}, inverseJoinColumns = { @JoinColumn(name="ROLE_ID")})
protected Collection<RoleEntity> defaultRoles = new ArrayList<RoleEntity>();
+ @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true)
+ @JoinTable(name="REALM_DEFAULT_GROUPS", joinColumns = { @JoinColumn(name="REALM_ID")}, inverseJoinColumns = { @JoinColumn(name="GROUP_ID")})
+ protected Collection<GroupEntity> defaultGroups = new ArrayList<>();
+
@Column(name="EVENTS_ENABLED")
protected boolean eventsEnabled;
@Column(name="EVENTS_EXPIRATION")
@@ -335,6 +338,14 @@ public class RealmEntity {
this.accessTokenLifespan = accessTokenLifespan;
}
+ public int getAccessTokenLifespanForImplicitFlow() {
+ return accessTokenLifespanForImplicitFlow;
+ }
+
+ public void setAccessTokenLifespanForImplicitFlow(int accessTokenLifespanForImplicitFlow) {
+ this.accessTokenLifespanForImplicitFlow = accessTokenLifespanForImplicitFlow;
+ }
+
public int getAccessCodeLifespan() {
return accessCodeLifespan;
}
@@ -429,6 +440,14 @@ public class RealmEntity {
this.defaultRoles = defaultRoles;
}
+ public Collection<GroupEntity> getDefaultGroups() {
+ return defaultGroups;
+ }
+
+ public void setDefaultGroups(Collection<GroupEntity> defaultGroups) {
+ this.defaultGroups = defaultGroups;
+ }
+
public String getPasswordPolicy() {
return passwordPolicy;
}
@@ -722,20 +741,5 @@ public class RealmEntity {
this.clientAuthenticationFlow = clientAuthenticationFlow;
}
- public Collection<GroupEntity> getGroups() {
- return groups;
- }
-
- public void setGroups(Collection<GroupEntity> groups) {
- this.groups = groups;
- }
-
- public void addGroup(GroupEntity group) {
- if (groups == null) {
- groups = new ArrayList<GroupEntity>();
- }
- groups.add(group);
- }
-
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java
index 209d2ae..8ef4dd2 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java
@@ -104,6 +104,9 @@ public class GroupAdapter implements GroupModel {
@Override
public void setParent(GroupModel parent) {
if (parent == null) group.setParent(null);
+ else if (parent.getId().equals(getId())) {
+ return;
+ }
else {
GroupEntity parentEntity = toEntity(parent, em);
group.setParent(parentEntity);
@@ -112,11 +115,17 @@ public class GroupAdapter implements GroupModel {
@Override
public void addChild(GroupModel subGroup) {
+ if (subGroup.getId().equals(getId())) {
+ return;
+ }
subGroup.setParent(this);
}
@Override
public void removeChild(GroupModel subGroup) {
+ if (subGroup.getId().equals(getId())) {
+ return;
+ }
subGroup.setParent(null);
}
@@ -312,9 +321,9 @@ public class GroupAdapter implements GroupModel {
@Override
public boolean equals(Object o) {
if (this == o) return true;
- if (o == null || !(o instanceof UserModel)) return false;
+ if (o == null || !(o instanceof GroupModel)) return false;
- UserModel that = (UserModel) o;
+ GroupModel that = (GroupModel) o;
return that.getId().equals(getId());
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
index 4ec5d66..09b4fa2 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
@@ -99,19 +99,20 @@ public class JpaRealmProvider implements RealmProvider {
RealmAdapter adapter = new RealmAdapter(session, em, realm);
session.users().preRemove(adapter);
- for (ClientEntity a : new LinkedList<>(realm.getClients())) {
- adapter.removeClient(a.getId());
- }
-
int num = em.createNamedQuery("deleteGroupRoleMappingsByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupAttributesByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupsByRealm")
.setParameter("realm", realm).executeUpdate();
-
+ for (ClientEntity a : new LinkedList<>(realm.getClients())) {
+ adapter.removeClient(a.getId());
+ }
em.remove(realm);
+
+ em.flush();
+ em.clear();
return true;
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 5f8ce7f..64650c9 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -71,6 +71,10 @@ public class JpaUserProvider implements UserProvider {
userModel.grantRole(application.getRole(r));
}
}
+
+ for (GroupModel g : realm.getDefaultGroups()) {
+ userModel.joinGroup(g);
+ }
}
for (RequiredActionProviderModel r : realm.getRequiredActionProviders()) {
if (r.isEnabled() && r.isDefaultAction()) {
@@ -97,6 +101,7 @@ public class JpaUserProvider implements UserProvider {
private void removeUser(UserEntity user) {
String id = user.getId();
em.createNamedQuery("deleteUserRoleMappingsByUser").setParameter("user", user).executeUpdate();
+ em.createNamedQuery("deleteUserGroupMembershipsByUser").setParameter("user", user).executeUpdate();
em.createNamedQuery("deleteFederatedIdentityByUser").setParameter("user", user).executeUpdate();
em.createNamedQuery("deleteUserConsentRolesByUser").setParameter("user", user).executeUpdate();
em.createNamedQuery("deleteUserConsentProtMappersByUser").setParameter("user", user).executeUpdate();
@@ -174,10 +179,10 @@ public class JpaUserProvider implements UserProvider {
.setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUserAttributesByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
- num = em.createNamedQuery("deleteUsersByRealm")
- .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUserGroupMembershipByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
+ num = em.createNamedQuery("deleteUsersByRealm")
+ .setParameter("realmId", realm.getId()).executeUpdate();
}
@Override
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 7b8e3a6..cb69f14 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
@@ -361,6 +361,16 @@ public class RealmAdapter implements RealmModel {
}
@Override
+ public int getAccessTokenLifespanForImplicitFlow() {
+ return realm.getAccessTokenLifespanForImplicitFlow();
+ }
+
+ @Override
+ public void setAccessTokenLifespanForImplicitFlow(int seconds) {
+ realm.setAccessTokenLifespanForImplicitFlow(seconds);
+ }
+
+ @Override
public int getSsoSessionIdleTimeout() {
return realm.getSsoSessionIdleTimeout();
}
@@ -648,6 +658,44 @@ public class RealmAdapter implements RealmModel {
}
@Override
+ public List<GroupModel> getDefaultGroups() {
+ Collection<GroupEntity> entities = realm.getDefaultGroups();
+ List<GroupModel> defaultGroups = new LinkedList<>();
+ for (GroupEntity entity : entities) {
+ defaultGroups.add(session.realms().getGroupById(entity.getId(), this));
+ }
+ return defaultGroups;
+ }
+
+ @Override
+ public void addDefaultGroup(GroupModel group) {
+ Collection<GroupEntity> entities = realm.getDefaultGroups();
+ for (GroupEntity entity : entities) {
+ if (entity.getId().equals(group.getId())) return;
+ }
+ GroupEntity groupEntity = GroupAdapter.toEntity(group, em);
+ realm.getDefaultGroups().add(groupEntity);
+ em.flush();
+
+ }
+
+ @Override
+ public void removeDefaultGroup(GroupModel group) {
+ GroupEntity found = null;
+ for (GroupEntity defaultGroup : realm.getDefaultGroups()) {
+ if (defaultGroup.getId().equals(group.getId())) {
+ found = defaultGroup;
+ break;
+ }
+ }
+ if (found != null) {
+ realm.getDefaultGroups().remove(found);
+ em.flush();
+ }
+
+ }
+
+ @Override
public Map<String, ClientModel> getClientNameMap() {
Map<String, ClientModel> map = new HashMap<String, ClientModel>();
for (ClientModel app : getClients()) {
@@ -677,6 +725,8 @@ public class RealmAdapter implements RealmModel {
entity.setId(id);
entity.setClientId(clientId);
entity.setEnabled(true);
+ entity.setStandardFlowEnabled(true);
+ entity.setDirectAccessGrantsEnabled(true);
entity.setRealm(realm);
realm.getClients().add(entity);
em.persist(entity);
@@ -1000,6 +1050,7 @@ public class RealmAdapter implements RealmModel {
String compositeRoleTable = JpaUtils.getTableNameForNativeQuery("COMPOSITE_ROLE", em);
em.createNativeQuery("delete from " + compositeRoleTable + " where CHILD_ROLE = :role").setParameter("role", roleEntity).executeUpdate();
em.createNamedQuery("deleteScopeMappingByRole").setParameter("role", roleEntity).executeUpdate();
+ em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate();
em.remove(roleEntity);
@@ -1955,6 +2006,9 @@ public class RealmAdapter implements RealmModel {
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
+ if (toParent != null && group.getId().equals(toParent.getId())) {
+ return;
+ }
if (group.getParentId() != null) {
group.getParent().removeChild(group);
}
@@ -1971,7 +2025,7 @@ public class RealmAdapter implements RealmModel {
@Override
public List<GroupModel> getGroups() {
List<GroupModel> list = new LinkedList<>();
- Collection<GroupEntity> groups = realm.getGroups();
+ Collection<GroupEntity> groups = em.createNamedQuery("getAllGroupsByRealm").setParameter("realm", realm).getResultList();
if (groups == null) return list;
for (GroupEntity entity : groups) {
list.add(new GroupAdapter(this, em, entity));
@@ -2001,6 +2055,7 @@ public class RealmAdapter implements RealmModel {
if (!groupEntity.getRealm().getId().equals(getId())) {
return false;
}
+ realm.getDefaultGroups().remove(groupEntity);
for (GroupModel subGroup : group.getSubGroups()) {
removeGroup(subGroup);
}
@@ -2008,7 +2063,6 @@ public class RealmAdapter implements RealmModel {
session.users().preRemove(this, group);
moveGroup(group, null);
- realm.getGroups().remove(groupEntity);
em.createNamedQuery("deleteGroupAttributesByGroup").setParameter("group", groupEntity).executeUpdate();
em.createNamedQuery("deleteGroupRoleMappingsByGroup").setParameter("group", groupEntity).executeUpdate();
em.remove(groupEntity);
@@ -2019,8 +2073,15 @@ public class RealmAdapter implements RealmModel {
@Override
public GroupModel createGroup(String name) {
+ String id = KeycloakModelUtils.generateId();
+ return createGroup(id, name);
+ }
+
+ @Override
+ public GroupModel createGroup(String id, String name) {
+ if (id == null) id = KeycloakModelUtils.generateId();
GroupEntity groupEntity = new GroupEntity();
- groupEntity.setId(KeycloakModelUtils.generateId());
+ groupEntity.setId(id);
groupEntity.setName(name);
groupEntity.setRealm(realm);
em.persist(groupEntity);
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
index cbacd09..eb6bcd9 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
@@ -5,6 +5,7 @@ import com.mongodb.QueryBuilder;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@@ -178,13 +179,13 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
}
@Override
- public String getRegistrationSecret() {
- return getMongoEntity().getRegistrationSecret();
+ public String getRegistrationToken() {
+ return getMongoEntity().getRegistrationToken();
}
@Override
- public void setRegistrationSecret(String registrationSecretsecret) {
- getMongoEntity().setRegistrationSecret(registrationSecretsecret);
+ public void setRegistrationToken(String registrationToken) {
+ getMongoEntity().setRegistrationToken(registrationToken);
updateMongoEntity();
}
@@ -342,7 +343,7 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
@Override
public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
if (getProtocolMapperByName(model.getProtocol(), model.getName()) != null) {
- throw new RuntimeException("protocol mapper name must be unique per protocol");
+ throw new ModelDuplicateException("Protocol mapper name must be unique per protocol");
}
ProtocolMapperEntity entity = new ProtocolMapperEntity();
String id = model.getId() != null ? model.getId() : KeycloakModelUtils.generateId();
@@ -504,24 +505,46 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
}
@Override
- public boolean isServiceAccountsEnabled() {
- return getMongoEntity().isServiceAccountsEnabled();
+ public boolean isStandardFlowEnabled() {
+ return getMongoEntity().isStandardFlowEnabled();
}
@Override
- public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
- getMongoEntity().setServiceAccountsEnabled(serviceAccountsEnabled);
+ public void setStandardFlowEnabled(boolean standardFlowEnabled) {
+ getMongoEntity().setStandardFlowEnabled(standardFlowEnabled);
+ updateMongoEntity();
+ }
+
+ @Override
+ public boolean isImplicitFlowEnabled() {
+ return getMongoEntity().isImplicitFlowEnabled();
+ }
+
+ @Override
+ public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
+ getMongoEntity().setImplicitFlowEnabled(implicitFlowEnabled);
updateMongoEntity();
}
@Override
- public boolean isDirectGrantsOnly() {
- return getMongoEntity().isDirectGrantsOnly();
+ public boolean isDirectAccessGrantsEnabled() {
+ return getMongoEntity().isDirectAccessGrantsEnabled();
}
@Override
- public void setDirectGrantsOnly(boolean flag) {
- getMongoEntity().setDirectGrantsOnly(flag);
+ public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
+ getMongoEntity().setDirectAccessGrantsEnabled(directAccessGrantsEnabled);
+ updateMongoEntity();
+ }
+
+ @Override
+ public boolean isServiceAccountsEnabled() {
+ return getMongoEntity().isServiceAccountsEnabled();
+ }
+
+ @Override
+ public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
+ getMongoEntity().setServiceAccountsEnabled(serviceAccountsEnabled);
updateMongoEntity();
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java
index d261c6d..32be0be 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java
@@ -6,6 +6,7 @@ import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
@@ -20,6 +21,7 @@ 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.Set;
@@ -146,7 +148,11 @@ public class GroupAdapter extends AbstractMongoAdapter<MongoGroupEntity> impleme
if (group.getRoleIds() == null || group.getRoleIds().isEmpty()) return Collections.EMPTY_SET;
Set<RoleModel> roles = new HashSet<>();
for (String id : group.getRoleIds()) {
- roles.add(realm.getRoleById(id));
+ RoleModel roleById = realm.getRoleById(id);
+ if (roleById == null) {
+ throw new ModelException("role does not exist in group role mappings");
+ }
+ roles.add(roleById);
}
return roles;
}
@@ -198,24 +204,40 @@ public class GroupAdapter extends AbstractMongoAdapter<MongoGroupEntity> impleme
@Override
public Set<GroupModel> getSubGroups() {
+ DBObject query = new QueryBuilder()
+ .and("realmId").is(realm.getId())
+ .and("parentId").is(getId())
+ .get();
+ List<MongoGroupEntity> groups = getMongoStore().loadEntities(MongoGroupEntity.class, query, invocationContext);
+
Set<GroupModel> subGroups = new HashSet<>();
- for (GroupModel groupModel : realm.getGroups()) {
- if (groupModel.getParent().equals(this)) {
- subGroups.add(groupModel);
- }
+
+ if (groups == null) return subGroups;
+ for (MongoGroupEntity group : groups) {
+ subGroups.add(realm.getGroupById(group.getId()));
}
+
return subGroups;
}
@Override
- public void setParent(GroupModel group) {
- this.group.setParentId(group.getId());
+ public void setParent(GroupModel parent) {
+ if (parent == null) group.setParentId(null);
+ else if (parent.getId().equals(getId())) {
+ return;
+ }
+ else {
+ group.setParentId(parent.getId());
+ }
updateGroup();
}
@Override
public void addChild(GroupModel subGroup) {
+ if (subGroup.getId().equals(getId())) {
+ return;
+ }
subGroup.setParent(this);
updateGroup();
@@ -223,6 +245,9 @@ public class GroupAdapter extends AbstractMongoAdapter<MongoGroupEntity> impleme
@Override
public void removeChild(GroupModel subGroup) {
+ if (subGroup.getId().equals(getId())) {
+ return;
+ }
subGroup.setParent(null);
updateGroup();
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index 9b4f1f8..44758b7 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -300,6 +300,9 @@ public class MongoUserProvider implements UserProvider {
userModel.grantRole(application.getRole(r));
}
}
+ for (GroupModel g : realm.getDefaultGroups()) {
+ userModel.joinGroup(g);
+ }
}
if (addDefaultRequiredActions) {
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 30266af..c8fc1f0 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -369,6 +369,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
@Override
+ public int getAccessTokenLifespanForImplicitFlow() {
+ return realm.getAccessTokenLifespanForImplicitFlow();
+ }
+
+ @Override
+ public void setAccessTokenLifespanForImplicitFlow(int seconds) {
+ realm.setAccessTokenLifespanForImplicitFlow(seconds);
+ updateRealm();
+ }
+
+ @Override
public int getAccessCodeLifespan() {
return realm.getAccessCodeLifespan();
}
@@ -611,8 +622,15 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override
public GroupModel createGroup(String name) {
+ String id = KeycloakModelUtils.generateId();
+ return createGroup(id, name);
+ }
+
+ @Override
+ public GroupModel createGroup(String id, String name) {
+ if (id == null) id = KeycloakModelUtils.generateId();
MongoGroupEntity group = new MongoGroupEntity();
- group.setId(KeycloakModelUtils.generateId());
+ group.setId(id);
group.setName(name);
group.setRealmId(getId());
@@ -629,6 +647,9 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
+ if (toParent != null && group.getId().equals(toParent.getId())) {
+ return;
+ }
if (group.getParentId() != null) {
group.getParent().removeChild(group);
}
@@ -653,7 +674,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
if (groups == null) return result;
for (MongoGroupEntity group : groups) {
- result.add(new GroupAdapter(session, this, group, invocationContext));
+ result.add(model.getGroupById(group.getId(), this));
}
return result;
@@ -665,7 +686,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
Iterator<GroupModel> it = all.iterator();
while (it.hasNext()) {
GroupModel group = it.next();
- if (group.getParent() != null) {
+ if (group.getParentId() != null) {
it.remove();
}
}
@@ -674,6 +695,9 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override
public boolean removeGroup(GroupModel group) {
+ if (realm.getDefaultGroups() != null) {
+ getMongoStore().pullItemFromList(realm, "defaultGroups", group.getId(), invocationContext);
+ }
for (GroupModel subGroup : group.getSubGroups()) {
removeGroup(subGroup);
}
@@ -682,6 +706,8 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
return getMongoStore().removeEntity(MongoGroupEntity.class, group.getId(), invocationContext);
}
+
+
@Override
public List<String> getDefaultRoles() {
return realm.getDefaultRoles();
@@ -714,6 +740,27 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
@Override
+ public List<GroupModel> getDefaultGroups() {
+ List<GroupModel> defaultGroups = new LinkedList<>();
+ for (String id : realm.getDefaultGroups()) {
+ defaultGroups.add(session.realms().getGroupById(id, this));
+ }
+ return defaultGroups;
+ }
+
+ @Override
+ public void addDefaultGroup(GroupModel group) {
+ getMongoStore().pushItemToList(realm, "defaultGroups", group.getId(), true, invocationContext);
+
+ }
+
+ @Override
+ public void removeDefaultGroup(GroupModel group) {
+ getMongoStore().pullItemFromList(realm, "defaultGroups", group.getId(), invocationContext);
+
+ }
+
+ @Override
public ClientModel getClientById(String id) {
return model.getClientById(id, this);
}
@@ -763,6 +810,8 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
clientEntity.setClientId(clientId);
clientEntity.setRealmId(getId());
clientEntity.setEnabled(true);
+ clientEntity.setStandardFlowEnabled(true);
+ clientEntity.setDirectAccessGrantsEnabled(true);
getMongoStore().insertEntity(clientEntity, invocationContext);
final ClientModel model = new ClientAdapter(session, this, clientEntity, invocationContext);
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java
index 8269360..78f1076 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java
@@ -20,6 +20,10 @@ public class MongoRealmEntity extends RealmEntity implements MongoIdentifiableEn
.get();
// Remove all roles of this realm
+ context.getMongoStore().removeEntities(MongoGroupEntity.class, query, true, context);
+
+
+ // Remove all roles of this realm
context.getMongoStore().removeEntities(MongoRoleEntity.class, query, true, context);
// Remove all clients of this realm
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRoleEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRoleEntity.java
index 29491f8..520f678 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRoleEntity.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRoleEntity.java
@@ -41,6 +41,18 @@ public class MongoRoleEntity extends RoleEntity implements MongoIdentifiableEnti
public void afterRemove(MongoStoreInvocationContext invContext) {
MongoStore mongoStore = invContext.getMongoStore();
+ {
+ DBObject query = new QueryBuilder()
+ .and("roleIds").is(getId())
+ .get();
+
+ List<MongoGroupEntity> groups = mongoStore.loadEntities(MongoGroupEntity.class, query, invContext);
+ for (MongoGroupEntity group : groups) {
+ mongoStore.pullItemFromList(group, "roleIds", getId(), invContext);
+ }
+
+ }
+
// Remove this scope from all clients, which has it
DBObject query = new QueryBuilder()
.and("scopeIds").is(getId())
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientInitialAccessAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientInitialAccessAdapter.java
new file mode 100644
index 0000000..7d75335
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientInitialAccessAdapter.java
@@ -0,0 +1,69 @@
+package org.keycloak.models.sessions.infinispan;
+
+import org.infinispan.Cache;
+import org.keycloak.models.ClientInitialAccessModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientInitialAccessAdapter implements ClientInitialAccessModel {
+
+ private final KeycloakSession session;
+ private final InfinispanUserSessionProvider provider;
+ private final Cache<String, SessionEntity> cache;
+ private final RealmModel realm;
+ private final ClientInitialAccessEntity entity;
+
+ public ClientInitialAccessAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm, ClientInitialAccessEntity entity) {
+ this.session = session;
+ this.provider = provider;
+ this.cache = cache;
+ this.realm = realm;
+ this.entity = entity;
+ }
+
+ @Override
+ public String getId() {
+ return entity.getId();
+ }
+
+ @Override
+ public RealmModel getRealm() {
+ return realm;
+ }
+
+ @Override
+ public int getTimestamp() {
+ return entity.getTimestamp();
+ }
+
+ @Override
+ public int getExpiration() {
+ return entity.getExpiration();
+ }
+
+ @Override
+ public int getCount() {
+ return entity.getCount();
+ }
+
+ @Override
+ public int getRemainingCount() {
+ return entity.getRemainingCount();
+ }
+
+ @Override
+ public void decreaseRemainingCount() {
+ entity.setRemainingCount(entity.getRemainingCount() - 1);
+ update();
+ }
+
+ void update() {
+ provider.getTx().replace(cache, entity.getId(), entity);
+ }
+
+}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientInitialAccessAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientInitialAccessAdapter.java
new file mode 100644
index 0000000..3398eff
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/ClientInitialAccessAdapter.java
@@ -0,0 +1,55 @@
+package org.keycloak.models.sessions.infinispan.compat;
+
+import org.keycloak.models.ClientInitialAccessModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.sessions.infinispan.compat.entities.ClientInitialAccessEntity;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientInitialAccessAdapter implements ClientInitialAccessModel {
+
+ private final RealmModel realm;
+ private final ClientInitialAccessEntity entity;
+
+ public ClientInitialAccessAdapter(RealmModel realm, ClientInitialAccessEntity entity) {
+ this.realm = realm;
+ this.entity = entity;
+ }
+
+ @Override
+ public String getId() {
+ return entity.getId();
+ }
+
+ @Override
+ public RealmModel getRealm() {
+ return realm;
+ }
+
+ @Override
+ public int getTimestamp() {
+ return entity.getTimestamp();
+ }
+
+ @Override
+ public int getExpiration() {
+ return entity.getExpires();
+ }
+
+ @Override
+ public int getCount() {
+ return entity.getCount();
+ }
+
+ @Override
+ public int getRemainingCount() {
+ return entity.getRemainingCount();
+ }
+
+ @Override
+ public void decreaseRemainingCount() {
+ entity.setRemainingCount(entity.getRemainingCount() - 1);
+ }
+
+}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientInitialAccessEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientInitialAccessEntity.java
new file mode 100644
index 0000000..ed0aeac
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/entities/ClientInitialAccessEntity.java
@@ -0,0 +1,68 @@
+package org.keycloak.models.sessions.infinispan.compat.entities;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientInitialAccessEntity {
+
+ private String id;
+
+ private String realmId;
+
+ private int timestamp;
+
+ private int expires;
+
+ private int count;
+
+ private int remainingCount;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ public int getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(int timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public int getExpires() {
+ return expires;
+ }
+
+ public void setExpiration(int expires) {
+ this.expires = expires;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ public void setCount(int count) {
+ this.count = count;
+ }
+
+ public int getRemainingCount() {
+ return remainingCount;
+ }
+
+ public void setRemainingCount(int remainingCount) {
+ this.remainingCount = remainingCount;
+ }
+
+}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java
index f45edf1..db20ef8 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java
@@ -1,19 +1,8 @@
package org.keycloak.models.sessions.infinispan.compat;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelDuplicateException;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
-import org.keycloak.models.UserSessionProvider;
-import org.keycloak.models.UsernameLoginFailureModel;
+import org.keycloak.models.*;
import org.keycloak.models.session.UserSessionPersisterProvider;
-import org.keycloak.models.sessions.infinispan.compat.entities.ClientSessionEntity;
-import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity;
-import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity;
-import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureKey;
+import org.keycloak.models.sessions.infinispan.compat.entities.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.common.util.Time;
@@ -41,11 +30,12 @@ public class MemUserSessionProvider implements UserSessionProvider {
private final ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions;
private final ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions;
+ private ConcurrentHashMap<String, ClientInitialAccessEntity> clientInitialAccess;
public MemUserSessionProvider(KeycloakSession session, ConcurrentHashMap<String, UserSessionEntity> userSessions, ConcurrentHashMap<String, String> userSessionsByBrokerSessionId,
ConcurrentHashMap<String, Set<String>> userSessionsByBrokerUserId, ConcurrentHashMap<String, ClientSessionEntity> clientSessions,
ConcurrentHashMap<UsernameLoginFailureKey, UsernameLoginFailureEntity> loginFailures,
- ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions, ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions) {
+ ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions, ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions, ConcurrentHashMap<String, ClientInitialAccessEntity> clientInitialAccess) {
this.session = session;
this.userSessions = userSessions;
this.clientSessions = clientSessions;
@@ -54,6 +44,7 @@ public class MemUserSessionProvider implements UserSessionProvider {
this.userSessionsByBrokerUserId = userSessionsByBrokerUserId;
this.offlineUserSessions = offlineUserSessions;
this.offlineClientSessions = offlineClientSessions;
+ this.clientInitialAccess = clientInitialAccess;
}
@Override
@@ -341,6 +332,15 @@ public class MemUserSessionProvider implements UserSessionProvider {
persister.removeClientSession(s.getId(), true);
}
}
+
+ // Remove expired initial access
+ Iterator<ClientInitialAccessEntity> iaitr = clientInitialAccess.values().iterator();
+ while (iaitr.hasNext()) {
+ ClientInitialAccessEntity e = iaitr.next();
+ if (e.getRealmId().equals(realm.getId()) && (e.getExpires() < Time.currentTime())) {
+ iaitr.remove();
+ }
+ }
}
@Override
@@ -575,6 +575,43 @@ public class MemUserSessionProvider implements UserSessionProvider {
}
@Override
+ public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
+ String id = KeycloakModelUtils.generateId();
+
+ ClientInitialAccessEntity entity = new ClientInitialAccessEntity();
+ entity.setId(id);
+ entity.setRealmId(realm.getId());
+ entity.setTimestamp(Time.currentTime());
+ entity.setExpiration(expiration);
+ entity.setCount(count);
+ entity.setRemainingCount(count);
+
+ clientInitialAccess.put(id, entity);
+
+ return new ClientInitialAccessAdapter(realm, entity);
+ }
+
+ @Override
+ public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
+ ClientInitialAccessEntity entity = clientInitialAccess.get(id);
+ return entity != null ? new ClientInitialAccessAdapter(realm, entity) : null;
+ }
+
+ @Override
+ public void removeClientInitialAccessModel(RealmModel realm, String id) {
+ clientInitialAccess.remove(id);
+ }
+
+ @Override
+ public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
+ List<ClientInitialAccessModel> models = new LinkedList<>();
+ for (ClientInitialAccessEntity e : clientInitialAccess.values()) {
+ models.add(new ClientInitialAccessAdapter(realm, e));
+ }
+ return models;
+ }
+
+ @Override
public void close() {
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java
index 187a33f..451fcb0 100644
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java
@@ -1,14 +1,12 @@
package org.keycloak.models.sessions.infinispan.compat;
-import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserSessionProvider;
-import org.keycloak.models.UserSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.compat.entities.ClientSessionEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureEntity;
import org.keycloak.models.sessions.infinispan.compat.entities.UsernameLoginFailureKey;
+import org.keycloak.models.sessions.infinispan.compat.entities.ClientInitialAccessEntity;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -29,9 +27,11 @@ public class MemUserSessionProviderFactory {
private ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions = new ConcurrentHashMap<String, UserSessionEntity>();
private ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions = new ConcurrentHashMap<String, ClientSessionEntity>();
+ private ConcurrentHashMap<String, ClientInitialAccessEntity> clientInitialAccess = new ConcurrentHashMap<>();
+
public UserSessionProvider create(KeycloakSession session) {
return new MemUserSessionProvider(session, userSessions, userSessionsByBrokerSessionId, userSessionsByBrokerUserId, clientSessions, loginFailures,
- offlineUserSessions, offlineClientSessions);
+ offlineUserSessions, offlineClientSessions, clientInitialAccess);
}
public void close() {
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientInitialAccessEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientInitialAccessEntity.java
new file mode 100644
index 0000000..05daf1e
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientInitialAccessEntity.java
@@ -0,0 +1,48 @@
+package org.keycloak.models.sessions.infinispan.entities;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientInitialAccessEntity extends SessionEntity {
+
+ private int timestamp;
+
+ private int expires;
+
+ private int count;
+
+ private int remainingCount;
+
+ public int getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(int timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public int getExpiration() {
+ return expires;
+ }
+
+ public void setExpiration(int expires) {
+ this.expires = expires;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ public void setCount(int count) {
+ this.count = count;
+ }
+
+ public int getRemainingCount() {
+ return remainingCount;
+ }
+
+ public void setRemainingCount(int remainingCount) {
+ this.remainingCount = remainingCount;
+ }
+
+}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 9ba1587..d89f47b 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -3,28 +3,10 @@ package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
import org.infinispan.distexec.mapreduce.MapReduceTask;
import org.jboss.logging.Logger;
-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.UserModel;
-import org.keycloak.models.UserSessionModel;
-import org.keycloak.models.UserSessionProvider;
-import org.keycloak.models.UsernameLoginFailureModel;
+import org.keycloak.models.*;
import org.keycloak.models.session.UserSessionPersisterProvider;
-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 org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionMapper;
-import org.keycloak.models.sessions.infinispan.mapreduce.FirstResultReducer;
-import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer;
-import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper;
-import org.keycloak.models.sessions.infinispan.mapreduce.UserLoginFailureMapper;
-import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionMapper;
-import org.keycloak.models.sessions.infinispan.mapreduce.UserSessionNoteMapper;
+import org.keycloak.models.sessions.infinispan.entities.*;
+import org.keycloak.models.sessions.infinispan.mapreduce.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.common.util.Time;
@@ -355,6 +337,15 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
persister.removeClientSession(clientSessionId, true);
}
+ // Remove expired client initial access
+ map = new MapReduceTask(sessionCache)
+ .mappedWith(ClientInitialAccessMapper.create(realm.getId()).expired(Time.currentTime()).emitKey())
+ .reducedWith(new FirstResultReducer())
+ .execute();
+
+ for (String id : map.keySet()) {
+ tx.remove(sessionCache, id);
+ }
}
@Override
@@ -538,11 +529,24 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return models;
}
+ List<ClientInitialAccessModel> wrapClientInitialAccess(RealmModel realm, Collection<ClientInitialAccessEntity> entities) {
+ List<ClientInitialAccessModel> models = new LinkedList<>();
+ for (ClientInitialAccessEntity e : entities) {
+ models.add(wrap(realm, e));
+ }
+ 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;
+ }
+
UsernameLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
return entity != null ? new UsernameLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
@@ -680,6 +684,50 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return wrap(clientSession.getRealm(), entity, offline);
}
+ @Override
+ public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {
+ String id = KeycloakModelUtils.generateId();
+
+ ClientInitialAccessEntity entity = new ClientInitialAccessEntity();
+ entity.setId(id);
+ entity.setRealm(realm.getId());
+ entity.setTimestamp(Time.currentTime());
+ entity.setExpiration(expiration);
+ entity.setCount(count);
+ entity.setRemainingCount(count);
+
+ tx.put(sessionCache, id, entity);
+
+ return wrap(realm, entity);
+ }
+
+ @Override
+ public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
+ Cache<String, SessionEntity> cache = getCache(false);
+ ClientInitialAccessEntity entity = (ClientInitialAccessEntity) cache.get(id);
+
+ // If created in this transaction
+ if (entity == null) {
+ entity = (ClientInitialAccessEntity) tx.get(cache, id);
+ }
+
+ return wrap(realm, entity);
+ }
+
+ @Override
+ public void removeClientInitialAccessModel(RealmModel realm, String id) {
+ tx.remove(getCache(false), id);
+ }
+
+ @Override
+ public List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm) {
+ Map<String, ClientInitialAccessEntity> entities = new MapReduceTask(sessionCache)
+ .mappedWith(ClientInitialAccessMapper.create(realm.getId()))
+ .reducedWith(new FirstResultReducer())
+ .execute();
+ return wrapClientInitialAccess(realm, entities.values());
+ }
+
class InfinispanKeycloakTransaction implements KeycloakTransaction {
private boolean active;
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientInitialAccessMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientInitialAccessMapper.java
new file mode 100644
index 0000000..98dab2f
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientInitialAccessMapper.java
@@ -0,0 +1,79 @@
+package org.keycloak.models.sessions.infinispan.mapreduce;
+
+import org.infinispan.distexec.mapreduce.Collector;
+import org.infinispan.distexec.mapreduce.Mapper;
+import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+
+import java.io.Serializable;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientInitialAccessMapper implements Mapper<String, SessionEntity, String, Object>, Serializable {
+
+ public ClientInitialAccessMapper(String realm) {
+ this.realm = realm;
+ }
+
+ private enum EmitValue {
+ KEY, ENTITY
+ }
+
+ private String realm;
+
+ private EmitValue emit = EmitValue.ENTITY;
+
+ private Integer expired;
+
+ public static ClientInitialAccessMapper create(String realm) {
+ return new ClientInitialAccessMapper(realm);
+ }
+
+ public ClientInitialAccessMapper emitKey() {
+ emit = EmitValue.KEY;
+ return this;
+ }
+
+ public ClientInitialAccessMapper expired(int time) {
+ this.expired = time;
+ return this;
+ }
+
+ @Override
+ public void map(String key, SessionEntity e, Collector collector) {
+ if (!realm.equals(e.getRealm())) {
+ return;
+ }
+
+ if (!(e instanceof ClientInitialAccessEntity)) {
+ return;
+ }
+
+ ClientInitialAccessEntity entity = (ClientInitialAccessEntity) e;
+
+ boolean include = false;
+
+ if (expired != null) {
+ if (entity.getRemainingCount() <= 0) {
+ include = true;
+ } else if (entity.getExpiration() > 0 && (entity.getTimestamp() + entity.getExpiration()) < expired) {
+ include = true;
+ }
+ } else {
+ include = true;
+ }
+
+ if (include) {
+ switch (emit) {
+ case KEY:
+ collector.emit(key, key);
+ break;
+ case ENTITY:
+ collector.emit(key, entity);
+ break;
+ }
+ }
+ }
+
+}
pom.xml 35(+26 -9)
diff --git a/pom.xml b/pom.xml
index d545efd..7e20bc3 100755
--- a/pom.xml
+++ b/pom.xml
@@ -76,6 +76,7 @@
<log4j.version>1.2.17</log4j.version>
<greenmail.version>1.3.1b</greenmail.version>
<xmlsec.version>1.5.1</xmlsec.version>
+ <aesh.version>0.65.1</aesh.version>
<enforcer.plugin.version>1.4</enforcer.plugin.version>
<jboss.as.plugin.version>7.5.Final</jboss.as.plugin.version>
@@ -135,7 +136,7 @@
<modules>
<module>common</module>
<module>core</module>
- <module>client-api</module>
+ <module>client-registration</module>
<module>connections</module>
<module>dependencies</module>
<module>events</module>
@@ -153,6 +154,7 @@
<module>timer</module>
<module>export-import</module>
<module>util</module>
+ <module>wildfly</module>
</modules>
<dependencyManagement>
@@ -580,6 +582,11 @@
<artifactId>pax-web-runtime</artifactId>
<version>${pax.web.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.jboss.aesh</groupId>
+ <artifactId>aesh</artifactId>
+ <version>${aesh.version}</version>
+ </dependency>
<!-- keycloak -->
<dependency>
@@ -624,6 +631,11 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-client-registration-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-mongo-update</artifactId>
<version>${project.version}</version>
</dependency>
@@ -816,7 +828,7 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-as7-server-subsystem</artifactId>
+ <artifactId>keycloak-eap6-server-subsystem</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
@@ -826,12 +838,12 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-subsystem</artifactId>
+ <artifactId>keycloak-wildfly-subsystem</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-server-subsystem</artifactId>
+ <artifactId>keycloak-wildfly-server-subsystem</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
@@ -936,6 +948,11 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-wildfly-adduser</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-wildfly-extensions</artifactId>
<version>${project.version}</version>
</dependency>
@@ -1031,7 +1048,7 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-saml-wf9-subsystem</artifactId>
+ <artifactId>keycloak-saml-wildfly-subsystem</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
@@ -1120,25 +1137,25 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-modules</artifactId>
+ <artifactId>keycloak-wildfly-modules</artifactId>
<version>${project.version}</version>
<type>zip</type>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-saml-wf9-modules</artifactId>
+ <artifactId>keycloak-saml-wildfly-modules</artifactId>
<version>${project.version}</version>
<type>zip</type>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-saml-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-saml-wildfly-adapter-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
</dependency>
diff --git a/saml/client-adapter/as7-eap6/adapter/pom.xml b/saml/client-adapter/as7-eap6/adapter/pom.xml
index 6d1ace8..da632c7 100755
--- a/saml/client-adapter/as7-eap6/adapter/pom.xml
+++ b/saml/client-adapter/as7-eap6/adapter/pom.xml
@@ -31,10 +31,6 @@
<artifactId>keycloak-saml-adapter-core</artifactId>
</dependency>
<dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-saml-adapter-core</artifactId>
- </dependency>
- <dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
diff --git a/saml/client-adapter/as7-eap6/subsystem/pom.xml b/saml/client-adapter/as7-eap6/subsystem/pom.xml
index fa87dcf..872e5d1 100755
--- a/saml/client-adapter/as7-eap6/subsystem/pom.xml
+++ b/saml/client-adapter/as7-eap6/subsystem/pom.xml
@@ -101,9 +101,9 @@ projects that depend on this project.-->
</dependency>
<dependency>
- <groupId>org.jboss.msc</groupId>
- <artifactId>jboss-msc</artifactId>
- <version>1.0.2.GA</version>
+ <groupId>org.jboss.as</groupId>
+ <artifactId>jboss-as-controller</artifactId>
+ <version>${jboss.version}</version>
</dependency>
<dependency>
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Configuration.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Configuration.java
new file mode 100644
index 0000000..966cd67
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Configuration.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.dmr.ModelNode;
+import org.jboss.dmr.Property;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class Configuration {
+
+ static final Configuration INSTANCE = new Configuration();
+
+ private ModelNode config = new ModelNode();
+
+ private Configuration() {
+ }
+
+ void updateModel(ModelNode operation, ModelNode model) {
+ ModelNode node = config;
+ ModelNode addr = operation.get("address");
+ for (Property item : addr.asPropertyList()) {
+ node = getNodeForAddressElement(node, item);
+ }
+ node.set(model);
+ }
+
+ private ModelNode getNodeForAddressElement(ModelNode node, Property item) {
+ String key = item.getValue().asString();
+ ModelNode keymodel = node.get(item.getName());
+ return keymodel.get(key);
+ }
+
+ public ModelNode getSecureDeployment(String name) {
+ ModelNode secureDeployment = config.get("subsystem").get("keycloak-saml").get(Constants.Model.SECURE_DEPLOYMENT);
+ if (secureDeployment.hasDefined(name)) {
+ return secureDeployment.get(name);
+ }
+ return null;
+ }
+
+ public boolean isSecureDeployment(String name) {
+ return getSecureDeployment(name) != null;
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Constants.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Constants.java
new file mode 100644
index 0000000..07af4f7
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Constants.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class Constants {
+
+ static class Model {
+ static final String SECURE_DEPLOYMENT = "secure-deployment";
+ static final String SERVICE_PROVIDER = "service-provider";
+
+ static final String SSL_POLICY = "ssl-policy";
+ static final String NAME_ID_POLICY_FORMAT = "name-id-policy-format";
+ static final String LOGOUT_PAGE = "logout-page";
+ static final String FORCE_AUTHENTICATION = "force-authentication";
+ static final String ROLE_ATTRIBUTES = "role-attributes";
+ static final String SIGNING = "signing";
+ static final String ENCRYPTION = "encryption";
+ static final String KEY = "key";
+ static final String RESOURCE = "resource";
+ static final String PASSWORD = "password";
+
+ static final String PRIVATE_KEY_ALIAS = "private-key-alias";
+ static final String PRIVATE_KEY_PASSWORD = "private-key-password";
+ static final String CERTIFICATE_ALIAS = "certificate-alias";
+ static final String KEY_STORE = "key-store";
+ static final String SIGN_REQUEST = "sign-request";
+ static final String VALIDATE_RESPONSE_SIGNATURE = "validate-response-signature";
+ static final String REQUEST_BINDING = "request-binding";
+ static final String BINDING_URL = "binding-url";
+ static final String VALIDATE_REQUEST_SIGNATURE = "validate-request-signature";
+ static final String SIGN_RESPONSE = "sign-response";
+ static final String RESPONSE_BINDING = "response-binding";
+ static final String POST_BINDING_URL = "post-binding-url";
+ static final String REDIRECT_BINDING_URL = "redirect-binding-url";
+ static final String SINGLE_SIGN_ON = "single-sign-on";
+ static final String SINGLE_LOGOUT = "single-logout";
+ static final String IDENTITY_PROVIDER = "identity-provider";
+ static final String PRINCIPAL_NAME_MAPPING_POLICY = "principal-name-mapping-policy";
+ static final String PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME = "principal-name-mapping-attribute-name";
+ static final String SIGNATURE_ALGORITHM = "signature-algorithm";
+ static final String SIGNATURE_CANONICALIZATION_METHOD = "signature-canonicalization-method";
+ static final String PRIVATE_KEY_PEM = "private-key-pem";
+ static final String PUBLIC_KEY_PEM = "public-key-pem";
+ static final String CERTIFICATE_PEM = "certificate-pem";
+ static final String TYPE = "type";
+ static final String ALIAS = "alias";
+ static final String FILE = "file";
+ static final String SIGNATURES_REQUIRED = "signatures-required";
+ }
+
+
+ static class XML {
+ static final String SECURE_DEPLOYMENT = "secure-deployment";
+ static final String SERVICE_PROVIDER = "SP";
+
+ static final String NAME = "name";
+ static final String ENTITY_ID = "entityID";
+ static final String SSL_POLICY = "sslPolicy";
+ static final String NAME_ID_POLICY_FORMAT = "nameIDPolicyFormat";
+ static final String LOGOUT_PAGE = "logoutPage";
+ static final String FORCE_AUTHENTICATION = "forceAuthentication";
+ static final String ROLE_IDENTIFIERS = "RoleIdentifiers";
+ static final String SIGNING = "signing";
+ static final String ENCRYPTION = "encryption";
+ static final String KEYS = "Keys";
+ static final String KEY = "Key";
+ static final String RESOURCE = "resource";
+ static final String PASSWORD = "password";
+ static final String KEY_STORE = "KeyStore";
+ static final String PRIVATE_KEY = "PrivateKey";
+ static final String CERTIFICATE = "Certificate";
+
+ static final String PRIVATE_KEY_ALIAS = "alias";
+ static final String PRIVATE_KEY_PASSWORD = "password";
+ static final String CERTIFICATE_ALIAS = "alias";
+ static final String SIGN_REQUEST = "signRequest";
+ static final String VALIDATE_RESPONSE_SIGNATURE = "validateResponseSignature";
+ static final String REQUEST_BINDING = "requestBinding";
+ static final String BINDING_URL = "bindingUrl";
+ static final String VALIDATE_REQUEST_SIGNATURE = "validateRequestSignature";
+ static final String SIGN_RESPONSE = "signResponse";
+ static final String RESPONSE_BINDING = "responseBinding";
+ static final String POST_BINDING_URL = "postBindingUrl";
+ static final String REDIRECT_BINDING_URL = "redirectBindingUrl";
+ static final String SINGLE_SIGN_ON = "SingleSignOnService";
+ static final String SINGLE_LOGOUT = "SingleLogoutService";
+ static final String IDENTITY_PROVIDER = "IDP";
+ static final String PRINCIPAL_NAME_MAPPING = "PrincipalNameMapping";
+ static final String PRINCIPAL_NAME_MAPPING_POLICY = "policy";
+ static final String PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME = "attribute";
+ static final String ATTRIBUTE = "Attribute";
+ static final String SIGNATURE_ALGORITHM = "signatureAlgorithm";
+ static final String SIGNATURE_CANONICALIZATION_METHOD = "signatureCanonicalizationMethod";
+ static final String PRIVATE_KEY_PEM = "PrivateKeyPem";
+ static final String PUBLIC_KEY_PEM = "PublicKeyPem";
+ static final String CERTIFICATE_PEM = "CertificatePem";
+ static final String TYPE = "type";
+ static final String ALIAS = "alias";
+ static final String FILE = "file";
+ static final String SIGNATURES_REQUIRED = "signaturesRequired";
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/IdentityProviderAddHandler.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/IdentityProviderAddHandler.java
new file mode 100644
index 0000000..679658b
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/IdentityProviderAddHandler.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.AbstractAddStepHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.as.controller.ServiceVerificationHandler;
+import org.jboss.dmr.ModelNode;
+import org.jboss.msc.service.ServiceController;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+class IdentityProviderAddHandler extends AbstractAddStepHandler {
+
+ IdentityProviderAddHandler() {
+ super(IdentityProviderDefinition.ALL_ATTRIBUTES);
+ }
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List<ServiceController<?>> newControllers) throws OperationFailedException {
+ Configuration.INSTANCE.updateModel(operation, model);
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/IdentityProviderDefinition.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/IdentityProviderDefinition.java
new file mode 100644
index 0000000..09262f9
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/IdentityProviderDefinition.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.AttributeDefinition;
+import org.jboss.as.controller.ObjectTypeAttributeDefinition;
+import org.jboss.as.controller.OperationStepHandler;
+import org.jboss.as.controller.PathElement;
+import org.jboss.as.controller.ReloadRequiredRemoveStepHandler;
+import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler;
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.as.controller.SimpleResourceDefinition;
+import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
+import org.jboss.as.controller.registry.ManagementResourceRegistration;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class IdentityProviderDefinition extends SimpleResourceDefinition {
+
+ static final SimpleAttributeDefinition SIGNATURES_REQUIRED =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGNATURES_REQUIRED, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.SIGNATURES_REQUIRED)
+ .build();
+
+ static final SimpleAttributeDefinition SIGNATURE_ALGORITHM =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGNATURE_ALGORITHM, ModelType.STRING, true)
+ .setXmlName(Constants.XML.SIGNATURE_ALGORITHM)
+ .build();
+
+ static final SimpleAttributeDefinition SIGNATURE_CANONICALIZATION_METHOD =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGNATURE_CANONICALIZATION_METHOD, ModelType.STRING, true)
+ .setXmlName(Constants.XML.SIGNATURE_CANONICALIZATION_METHOD)
+ .build();
+
+ static final ObjectTypeAttributeDefinition SINGLE_SIGN_ON =
+ ObjectTypeAttributeDefinition.Builder.of(Constants.Model.SINGLE_SIGN_ON,
+ SingleSignOnDefinition.ATTRIBUTES)
+ .setAllowNull(false)
+ .build();
+
+ static final ObjectTypeAttributeDefinition SINGLE_LOGOUT =
+ ObjectTypeAttributeDefinition.Builder.of(Constants.Model.SINGLE_LOGOUT,
+ SingleLogoutDefinition.ATTRIBUTES)
+ .setAllowNull(false)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {SIGNATURES_REQUIRED, SIGNATURE_ALGORITHM, SIGNATURE_CANONICALIZATION_METHOD};
+
+ static final SimpleAttributeDefinition[] ALL_ATTRIBUTES = {SIGNATURES_REQUIRED, SIGNATURE_ALGORITHM, SIGNATURE_CANONICALIZATION_METHOD, SINGLE_SIGN_ON, SINGLE_LOGOUT};
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ALL_ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static final IdentityProviderDefinition INSTANCE = new IdentityProviderDefinition();
+
+ private IdentityProviderDefinition() {
+ super(PathElement.pathElement(Constants.Model.IDENTITY_PROVIDER),
+ KeycloakSamlExtension.getResourceDescriptionResolver(Constants.Model.IDENTITY_PROVIDER),
+ new IdentityProviderAddHandler(),
+ ReloadRequiredRemoveStepHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerOperations(ManagementResourceRegistration resourceRegistration) {
+ super.registerOperations(resourceRegistration);
+ resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
+ super.registerAttributes(resourceRegistration);
+
+ final OperationStepHandler writeHandler = new ReloadRequiredWriteAttributeHandler(ALL_ATTRIBUTES);
+ for (AttributeDefinition attribute : ALL_ATTRIBUTES) {
+ resourceRegistration.registerReadWriteAttribute(attribute, null, writeHandler);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
\ No newline at end of file
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyAddHandler.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyAddHandler.java
new file mode 100644
index 0000000..b362d4f
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyAddHandler.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.AbstractAddStepHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.as.controller.ServiceVerificationHandler;
+import org.jboss.dmr.ModelNode;
+import org.jboss.msc.service.ServiceController;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+class KeyAddHandler extends AbstractAddStepHandler {
+
+ KeyAddHandler() {
+ super(KeyDefinition.ALL_ATTRIBUTES);
+ }
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List<ServiceController<?>> newControllers) throws OperationFailedException {
+ Configuration.INSTANCE.updateModel(operation, model);
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakAdapterConfigDeploymentProcessor.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakAdapterConfigDeploymentProcessor.java
index 31008d4..68e4679 100755
--- a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakAdapterConfigDeploymentProcessor.java
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakAdapterConfigDeploymentProcessor.java
@@ -21,13 +21,22 @@ import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.as.server.deployment.DeploymentUnitProcessingException;
import org.jboss.as.server.deployment.DeploymentUnitProcessor;
import org.jboss.as.web.deployment.WarMetaData;
+import org.jboss.dmr.ModelNode;
import org.jboss.logging.Logger;
import org.jboss.metadata.javaee.spec.ParamValueMetaData;
import org.jboss.metadata.web.jboss.JBossWebMetaData;
import org.jboss.metadata.web.jboss.ValveMetaData;
import org.jboss.metadata.web.spec.LoginConfigMetaData;
+import org.jboss.staxmapper.XMLExtendedStreamWriter;
+import org.keycloak.adapters.saml.AdapterConstants;
import org.keycloak.adapters.saml.jbossweb.SamlAuthenticatorValve;
+import org.keycloak.subsystem.saml.as7.logging.KeycloakLogger;
+import org.keycloak.subsystem.saml.as7.xml.FormattingXMLStreamWriter;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
@@ -39,22 +48,16 @@ import java.util.List;
public class KeycloakAdapterConfigDeploymentProcessor implements DeploymentUnitProcessor {
protected Logger log = Logger.getLogger(KeycloakAdapterConfigDeploymentProcessor.class);
- // This param name is defined again in Keycloak Undertow Integration class
- // org.keycloak.adapters.undertow.KeycloakServletExtension. We have this value in
- // two places to avoid dependency between Keycloak Subsystem and Keyclaok Undertow Integration.
- public static final String AUTH_DATA_PARAM_NAME = "org.keycloak.saml.adapterConfig";
-
-
@Override
public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException {
DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit();
String deploymentName = deploymentUnit.getName();
- // if it's not a web-app there's nothing to secure
WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
if (warMetaData == null) {
return;
}
+
JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
if (webMetaData == null) {
webMetaData = new JBossWebMetaData();
@@ -64,12 +67,66 @@ public class KeycloakAdapterConfigDeploymentProcessor implements DeploymentUnitP
// otherwise
LoginConfigMetaData loginConfig = webMetaData.getLoginConfig();
- boolean webRequiresKC = loginConfig != null && "KEYCLOAK-SAML".equalsIgnoreCase(loginConfig.getAuthMethod());
+ try {
+ boolean webRequiresKC = loginConfig != null && "KEYCLOAK-SAML".equalsIgnoreCase(loginConfig.getAuthMethod());
+ boolean hasSubsystemConfig = Configuration.INSTANCE.isSecureDeployment(deploymentName);
+ if (hasSubsystemConfig || webRequiresKC) {
+ log.debug("Setting up KEYCLOAK-SAML auth method for WAR: " + deploymentName);
+
+ // if secure-deployment configuration exists for web app, we force KEYCLOAK-SAML auth method on it
+ if (hasSubsystemConfig) {
+ addXMLData(getXML(deploymentName), warMetaData);
+ if (loginConfig != null) {
+ loginConfig.setAuthMethod("KEYCLOAK-SAML");
+ //loginConfig.setRealmName(service.getRealmName(deploymentName));
+ } else {
+ log.warn("Failed to set up KEYCLOAK-SAML auth method for WAR: " + deploymentName + " (loginConfig == null)");
+ }
+ }
+ addValve(webMetaData);
+ KeycloakLogger.ROOT_LOGGER.deploymentSecured(deploymentName);
+ }
+ } catch (Exception e) {
+ throw new DeploymentUnitProcessingException("Failed to configure KeycloakSamlExtension from subsystem model", e);
+ }
+ }
+
+ private String getXML(String deploymentName) throws XMLStreamException {
+ ModelNode node = Configuration.INSTANCE.getSecureDeployment(deploymentName);
+ if (node != null) {
+ KeycloakSubsystemParser writer = new KeycloakSubsystemParser();
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ XMLExtendedStreamWriter streamWriter = new FormattingXMLStreamWriter(XMLOutputFactory.newInstance().createXMLStreamWriter(output));
+ try {
+ streamWriter.writeStartElement("keycloak-saml-adapter");
+ writer.writeSps(streamWriter, node);
+ streamWriter.writeEndElement();
+ } finally {
+ streamWriter.close();
+ }
+ return new String(output.toByteArray(), Charset.forName("utf-8"));
+ }
+ return null;
+ }
+
+ private void addXMLData(String xml, WarMetaData warMetaData) {
+ JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
+ if (webMetaData == null) {
+ webMetaData = new JBossWebMetaData();
+ warMetaData.setMergedJBossWebMetaData(webMetaData);
+ }
- if (webRequiresKC) {
- log.debug("Setting up KEYCLOAK-SAML auth method for WAR: " + deploymentName);
- addValve(webMetaData);
+ List<ParamValueMetaData> contextParams = webMetaData.getContextParams();
+ if (contextParams == null) {
+ contextParams = new ArrayList<>();
}
+
+ ParamValueMetaData param = new ParamValueMetaData();
+ param.setParamName(AdapterConstants.AUTH_DATA_PARAM_NAME);
+ param.setParamValue(xml);
+ contextParams.add(param);
+
+ webMetaData.setContextParams(contextParams);
}
private void addValve(JBossWebMetaData webMetaData) {
@@ -89,5 +146,4 @@ public class KeycloakAdapterConfigDeploymentProcessor implements DeploymentUnitP
public void undeploy(DeploymentUnit du) {
}
-
}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSamlExtension.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSamlExtension.java
index c52f2b5..d936982 100755
--- a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSamlExtension.java
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSamlExtension.java
@@ -18,6 +18,7 @@ package org.keycloak.subsystem.saml.as7;
import org.jboss.as.controller.Extension;
import org.jboss.as.controller.ExtensionContext;
+import org.jboss.as.controller.ModelVersion;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.ResourceDefinition;
import org.jboss.as.controller.SubsystemRegistration;
@@ -36,15 +37,12 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUB
public class KeycloakSamlExtension implements Extension {
public static final String SUBSYSTEM_NAME = "keycloak-saml";
- public static final String NAMESPACE = "urn:jboss:domain:keycloak-saml:1.6";
+ public static final String NAMESPACE = "urn:jboss:domain:keycloak-saml:1.1";
private static final KeycloakSubsystemParser PARSER = new KeycloakSubsystemParser();
static final PathElement PATH_SUBSYSTEM = PathElement.pathElement(SUBSYSTEM, SUBSYSTEM_NAME);
private static final String RESOURCE_NAME = KeycloakSamlExtension.class.getPackage().getName() + ".LocalDescriptions";
- private static final int MGMT_API_VERSION_MAJOR = 1;
- private static final int MGMT_API_VERSION_MINOR = 1;
-
+ private static final ModelVersion MGMT_API_VERSION = ModelVersion.create(1, 1, 0);
static final PathElement SUBSYSTEM_PATH = PathElement.pathElement(SUBSYSTEM, SUBSYSTEM_NAME);
- private static final ResourceDefinition KEYCLOAK_SUBSYSTEM_RESOURCE = new KeycloakSubsystemDefinition();
public static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) {
StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME);
@@ -67,10 +65,15 @@ public class KeycloakSamlExtension implements Extension {
*/
@Override
public void initialize(final ExtensionContext context) {
- final SubsystemRegistration subsystem = context.registerSubsystem(SUBSYSTEM_NAME, MGMT_API_VERSION_MAJOR, MGMT_API_VERSION_MINOR);
-
- ManagementResourceRegistration registration = subsystem.registerSubsystemModel(KEYCLOAK_SUBSYSTEM_RESOURCE);
+ final SubsystemRegistration subsystem = context.registerSubsystem(SUBSYSTEM_NAME,
+ MGMT_API_VERSION.getMajor(), MGMT_API_VERSION.getMinor(), MGMT_API_VERSION.getMicro());
+ ManagementResourceRegistration registration = subsystem.registerSubsystemModel(KeycloakSubsystemDefinition.INSTANCE);
+ ManagementResourceRegistration secureDeploymentRegistration = registration.registerSubModel(SecureDeploymentDefinition.INSTANCE);
+ ManagementResourceRegistration serviceProviderRegistration = secureDeploymentRegistration.registerSubModel(ServiceProviderDefinition.INSTANCE);
+ serviceProviderRegistration.registerSubModel(KeyDefinition.INSTANCE);
+ ManagementResourceRegistration idpRegistration = serviceProviderRegistration.registerSubModel(IdentityProviderDefinition.INSTANCE);
+ idpRegistration.registerSubModel(KeyDefinition.INSTANCE);
subsystem.registerXMLElementWriter(PARSER);
}
}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java
index 2a7fd55..eda678f 100755
--- a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemAdd.java
@@ -16,13 +16,12 @@
*/
package org.keycloak.subsystem.saml.as7;
-
import org.jboss.as.controller.AbstractBoottimeAddStepHandler;
import org.jboss.as.controller.OperationContext;
-import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.ServiceVerificationHandler;
import org.jboss.as.server.AbstractDeploymentChainStep;
import org.jboss.as.server.DeploymentProcessorTarget;
+import org.jboss.as.server.deployment.DeploymentUnitProcessor;
import org.jboss.as.server.deployment.Phase;
import org.jboss.dmr.ModelNode;
import org.jboss.msc.service.ServiceController;
@@ -43,17 +42,20 @@ class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler {
context.addStep(new AbstractDeploymentChainStep() {
@Override
protected void execute(DeploymentProcessorTarget processorTarget) {
- processorTarget.addDeploymentProcessor(Phase.DEPENDENCIES, 0, new KeycloakDependencyProcessorAS7());
- processorTarget.addDeploymentProcessor(
+ processorTarget.addDeploymentProcessor(KeycloakSamlExtension.SUBSYSTEM_NAME, Phase.DEPENDENCIES, 0, chooseDependencyProcessor());
+ processorTarget.addDeploymentProcessor(KeycloakSamlExtension.SUBSYSTEM_NAME,
Phase.POST_MODULE, // PHASE
Phase.POST_MODULE_VALIDATOR_FACTORY - 1, // PRIORITY
- new KeycloakAdapterConfigDeploymentProcessor());
+ chooseConfigDeploymentProcessor());
}
}, OperationContext.Stage.RUNTIME);
}
- @Override
- protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException {
- model.setEmptyObject();
+ private DeploymentUnitProcessor chooseDependencyProcessor() {
+ return new KeycloakDependencyProcessorAS7();
+ }
+
+ private DeploymentUnitProcessor chooseConfigDeploymentProcessor() {
+ return new KeycloakAdapterConfigDeploymentProcessor();
}
}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemDefinition.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemDefinition.java
index 400822e..4b7ef3f 100755
--- a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemDefinition.java
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemDefinition.java
@@ -14,23 +14,23 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
-
package org.keycloak.subsystem.saml.as7;
import org.jboss.as.controller.ReloadRequiredRemoveStepHandler;
import org.jboss.as.controller.SimpleResourceDefinition;
-import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
import org.jboss.as.controller.registry.ManagementResourceRegistration;
-import org.jboss.as.controller.registry.OperationEntry;
/**
- * Definition of subsystem=keycloak.
+ * Definition of subsystem=keycloak-saml.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc.
*/
public class KeycloakSubsystemDefinition extends SimpleResourceDefinition {
- protected KeycloakSubsystemDefinition() {
+
+ static final KeycloakSubsystemDefinition INSTANCE = new KeycloakSubsystemDefinition();
+
+ private KeycloakSubsystemDefinition() {
super(KeycloakSamlExtension.SUBSYSTEM_PATH,
KeycloakSamlExtension.getResourceDescriptionResolver("subsystem"),
KeycloakSubsystemAdd.INSTANCE,
@@ -41,7 +41,6 @@ public class KeycloakSubsystemDefinition extends SimpleResourceDefinition {
@Override
public void registerOperations(ManagementResourceRegistration resourceRegistration) {
super.registerOperations(resourceRegistration);
- resourceRegistration.registerOperationHandler(ModelDescriptionConstants.DESCRIBE, GenericSubsystemDescribeHandler.INSTANCE, GenericSubsystemDescribeHandler.INSTANCE, false, OperationEntry.EntryType.PRIVATE);
+ resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
}
-
}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemParser.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemParser.java
index 14899e1..0b2cef9 100755
--- a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemParser.java
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemParser.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
@@ -17,9 +17,14 @@
package org.keycloak.subsystem.saml.as7;
import org.jboss.as.controller.PathAddress;
+import org.jboss.as.controller.PathElement;
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
+import org.jboss.as.controller.operations.common.Util;
import org.jboss.as.controller.parsing.ParseUtils;
import org.jboss.as.controller.persistence.SubsystemMarshallingContext;
import org.jboss.dmr.ModelNode;
+import org.jboss.dmr.Property;
import org.jboss.staxmapper.XMLElementReader;
import org.jboss.staxmapper.XMLElementWriter;
import org.jboss.staxmapper.XMLExtendedStreamReader;
@@ -27,6 +32,10 @@ import org.jboss.staxmapper.XMLExtendedStreamWriter;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
/**
@@ -41,10 +50,15 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
public void readElement(final XMLExtendedStreamReader reader, final List<ModelNode> list) throws XMLStreamException {
// Require no attributes
ParseUtils.requireNoAttributes(reader);
- ModelNode addKeycloakSub = org.jboss.as.controller.operations.common.Util.createAddOperation(PathAddress.pathAddress(KeycloakSamlExtension.PATH_SUBSYSTEM));
+ ModelNode addKeycloakSub = Util.createAddOperation(PathAddress.pathAddress(KeycloakSamlExtension.PATH_SUBSYSTEM));
list.add(addKeycloakSub);
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ if (reader.getLocalName().equals(Constants.XML.SECURE_DEPLOYMENT)) {
+ readSecureDeployment(reader, list);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
}
}
@@ -53,6 +67,319 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
return reader.nextTag();
}
+ void readSecureDeployment(XMLExtendedStreamReader reader, List<ModelNode> list) throws XMLStreamException {
+ String name = readRequiredAttribute(reader, Constants.XML.NAME);
+
+ PathAddress addr = PathAddress.pathAddress(
+ PathElement.pathElement(ModelDescriptionConstants.SUBSYSTEM, KeycloakSamlExtension.SUBSYSTEM_NAME),
+ PathElement.pathElement(Constants.Model.SECURE_DEPLOYMENT, name));
+ ModelNode addSecureDeployment = Util.createAddOperation(addr);
+ list.add(addSecureDeployment);
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+ if (tagName.equals(Constants.XML.SERVICE_PROVIDER)) {
+ readServiceProvider(reader, list, addr);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ }
+ }
+
+ void readServiceProvider(XMLExtendedStreamReader reader, List<ModelNode> list, PathAddress parentAddr) throws XMLStreamException {
+ String entityId = readRequiredAttribute(reader, Constants.XML.ENTITY_ID);
+
+ PathAddress addr = PathAddress.pathAddress(parentAddr,
+ PathElement.pathElement(Constants.Model.SERVICE_PROVIDER, entityId));
+ ModelNode addServiceProvider = Util.createAddOperation(addr);
+ list.add(addServiceProvider);
+
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ if (Constants.XML.ENTITY_ID.equals(name)) {
+ continue;
+ }
+
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = ServiceProviderDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addServiceProvider, reader);
+ }
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+
+ if (Constants.XML.KEYS.equals(tagName)) {
+ readKeys(list, reader, addr);
+ } else if (Constants.XML.PRINCIPAL_NAME_MAPPING.equals(tagName)) {
+ readPrincipalNameMapping(addServiceProvider, reader);
+ } else if (Constants.XML.ROLE_IDENTIFIERS.equals(tagName)) {
+ readRoleIdentifiers(addServiceProvider, reader);
+ } else if (Constants.XML.IDENTITY_PROVIDER.equals(tagName)) {
+ readIdentityProvider(list, reader, addr);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ }
+ }
+
+ void readIdentityProvider(List<ModelNode> list, XMLExtendedStreamReader reader, PathAddress parentAddr) throws XMLStreamException {
+ String entityId = readRequiredAttribute(reader, Constants.XML.ENTITY_ID);
+
+ PathAddress addr = PathAddress.pathAddress(parentAddr,
+ PathElement.pathElement(Constants.Model.IDENTITY_PROVIDER, entityId));
+ ModelNode addIdentityProvider = Util.createAddOperation(addr);
+ list.add(addIdentityProvider);
+
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ if (Constants.XML.ENTITY_ID.equals(name)
+ // don't break if encountering this noop attr from client-adapter/core keycloak_saml_adapter_1_6.xsd
+ || "encryption".equals(name)) {
+ continue;
+ }
+ SimpleAttributeDefinition attr = IdentityProviderDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addIdentityProvider, reader);
+ }
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+
+ if (Constants.XML.SINGLE_SIGN_ON.equals(tagName)) {
+ readSingleSignOn(addIdentityProvider, reader);
+ } else if (Constants.XML.SINGLE_LOGOUT.equals(tagName)) {
+ readSingleLogout(addIdentityProvider, reader);
+ } else if (Constants.XML.KEYS.equals(tagName)) {
+ readKeys(list, reader, addr);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ }
+ }
+
+ void readSingleSignOn(ModelNode addIdentityProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
+ ModelNode sso = addIdentityProvider.get(Constants.Model.SINGLE_SIGN_ON);
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = SingleSignOnDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, sso, reader);
+ }
+ ParseUtils.requireNoContent(reader);
+ }
+
+ void readSingleLogout(ModelNode addIdentityProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
+ ModelNode slo = addIdentityProvider.get(Constants.Model.SINGLE_LOGOUT);
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = SingleLogoutDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, slo, reader);
+ }
+ ParseUtils.requireNoContent(reader);
+ }
+
+ void readKeys(List<ModelNode> list, XMLExtendedStreamReader reader, PathAddress parentAddr) throws XMLStreamException {
+ ParseUtils.requireNoAttributes(reader);
+ List<ModelNode> keyList = new LinkedList<>();
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+ if (!Constants.XML.KEY.equals(tagName)) {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ readKey(keyList, reader, parentAddr);
+ }
+ list.addAll(keyList);
+ }
+
+ void readKey(List<ModelNode> list, XMLExtendedStreamReader reader, PathAddress parentAddr) throws XMLStreamException {
+ PathAddress addr = PathAddress.pathAddress(parentAddr,
+ PathElement.pathElement(Constants.Model.KEY, "key-" + list.size()));
+ ModelNode addKey = Util.createAddOperation(addr);
+ list.add(addKey);
+
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = KeyDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addKey, reader);
+ }
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+
+ if (Constants.XML.KEY_STORE.equals(tagName)) {
+ readKeyStore(addKey, reader);
+ } else if (Constants.XML.PRIVATE_KEY_PEM.equals(tagName)
+ || Constants.XML.PUBLIC_KEY_PEM.equals(tagName)
+ || Constants.XML.CERTIFICATE_PEM.equals(tagName)) {
+
+ readNoAttrElementContent(KeyDefinition.lookupElement(tagName), addKey, reader);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ }
+ }
+
+ void readNoAttrElementContent(SimpleAttributeDefinition attr, ModelNode model, XMLExtendedStreamReader reader) throws XMLStreamException {
+ ParseUtils.requireNoAttributes(reader);
+ String value = reader.getElementText();
+ attr.parseAndSetParameter(value, model, reader);
+ }
+
+ void readKeyStore(ModelNode addKey, XMLExtendedStreamReader reader) throws XMLStreamException {
+ ModelNode addKeyStore = addKey.get(Constants.Model.KEY_STORE);
+
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = KeyStoreDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addKeyStore, reader);
+ }
+
+ if (!addKeyStore.hasDefined(Constants.Model.FILE) && !addKeyStore.hasDefined(Constants.Model.RESOURCE)) {
+ throw new XMLStreamException("KeyStore element must have 'file' or 'resource' attribute set", reader.getLocation());
+ }
+ if (!addKeyStore.hasDefined(Constants.Model.PASSWORD)) {
+ throw ParseUtils.missingRequired(reader, Constants.XML.PASSWORD);
+ }
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+ if (Constants.XML.PRIVATE_KEY.equals(tagName)) {
+ readPrivateKey(reader, addKeyStore);
+ } else if (Constants.XML.CERTIFICATE.equals(tagName)) {
+ readCertificate(reader, addKeyStore);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ }
+ }
+
+
+ void readPrivateKey(XMLExtendedStreamReader reader, ModelNode addKeyStore) throws XMLStreamException {
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = KeyStorePrivateKeyDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addKeyStore, reader);
+ }
+
+ if (!addKeyStore.hasDefined(Constants.Model.PRIVATE_KEY_ALIAS)) {
+ throw ParseUtils.missingRequired(reader, Constants.XML.PRIVATE_KEY_ALIAS);
+ }
+ if (!addKeyStore.hasDefined(Constants.Model.PRIVATE_KEY_PASSWORD)) {
+ throw ParseUtils.missingRequired(reader, Constants.XML.PRIVATE_KEY_PASSWORD);
+ }
+
+ ParseUtils.requireNoContent(reader);
+ }
+
+ void readCertificate(XMLExtendedStreamReader reader, ModelNode addKeyStore) throws XMLStreamException {
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = KeyStoreCertificateDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addKeyStore, reader);
+ }
+
+ if (!addKeyStore.hasDefined(Constants.Model.CERTIFICATE_ALIAS)) {
+ throw ParseUtils.missingRequired(reader, Constants.XML.CERTIFICATE_ALIAS);
+ }
+
+ ParseUtils.requireNoContent(reader);
+ }
+
+ void readRoleIdentifiers(ModelNode addServiceProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
+ ParseUtils.requireNoAttributes(reader);
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+
+ if (!Constants.XML.ATTRIBUTE.equals(tagName)) {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+
+ ParseUtils.requireSingleAttribute(reader, Constants.XML.NAME);
+ String name = ParseUtils.readStringAttributeElement(reader, Constants.XML.NAME);
+
+ ServiceProviderDefinition.ROLE_ATTRIBUTES.parseAndAddParameterElement(name, addServiceProvider, reader);
+ }
+ }
+
+ void readPrincipalNameMapping(ModelNode addServiceProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
+
+ boolean policySet = false;
+
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ if (Constants.XML.PRINCIPAL_NAME_MAPPING_POLICY.equals(name)) {
+ policySet = true;
+ ServiceProviderDefinition.PRINCIPAL_NAME_MAPPING_POLICY.parseAndSetParameter(value, addServiceProvider, reader);
+ } else if (Constants.XML.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME.equals(name)) {
+ ServiceProviderDefinition.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME.parseAndSetParameter(value, addServiceProvider, reader);
+ } else {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ }
+
+ if (!policySet) {
+ throw ParseUtils.missingRequired(reader, Constants.XML.PRINCIPAL_NAME_MAPPING_POLICY);
+ }
+ ParseUtils.requireNoContent(reader);
+ }
+
+ /**
+ * Read an attribute, and throw exception if attribute is not present
+ */
+ String readRequiredAttribute(XMLExtendedStreamReader reader, String attrName) throws XMLStreamException {
+ String value = null;
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String attr = reader.getAttributeLocalName(i);
+ if (attr.equals(attrName)) {
+ value = reader.getAttributeValue(i);
+ break;
+ }
+ }
+ if (value == null) {
+ throw ParseUtils.missingRequired(reader, Collections.singleton(attrName));
+ }
+ return value;
+ }
/**
* {@inheritDoc}
@@ -60,8 +387,183 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
@Override
public void writeContent(final XMLExtendedStreamWriter writer, final SubsystemMarshallingContext context) throws XMLStreamException {
context.startSubsystemElement(KeycloakSamlExtension.NAMESPACE, false);
+ writeSecureDeployment(writer, context.getModelNode());
+ writer.writeEndElement();
+ }
+
+ public void writeSecureDeployment(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.get(Constants.Model.SECURE_DEPLOYMENT).isDefined()) {
+ return;
+ }
+
+ for (Property sp : model.get(Constants.Model.SECURE_DEPLOYMENT).asPropertyList()) {
+ writer.writeStartElement(Constants.XML.SECURE_DEPLOYMENT);
+ writer.writeAttribute(Constants.XML.NAME, sp.getName());
+
+ writeSps(writer, sp.getValue());
+ writer.writeEndElement();
+ }
+ }
+
+ void writeSps(final XMLExtendedStreamWriter writer, final ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+ for (Property sp : model.get(Constants.Model.SERVICE_PROVIDER).asPropertyList()) {
+ writer.writeStartElement(Constants.XML.SERVICE_PROVIDER);
+ writer.writeAttribute(Constants.XML.ENTITY_ID, sp.getName());
+ ModelNode spAttributes = sp.getValue();
+ for (SimpleAttributeDefinition attr : ServiceProviderDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, spAttributes, false, writer);
+ }
+ writeKeys(writer, spAttributes.get(Constants.Model.KEY));
+ writePrincipalNameMapping(writer, spAttributes);
+ writeRoleIdentifiers(writer, spAttributes);
+ writeIdentityProvider(writer, spAttributes.get(Constants.Model.IDENTITY_PROVIDER));
+
+ writer.writeEndElement();
+ }
+ }
+
+ void writeIdentityProvider(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+
+ for (Property idp : model.asPropertyList()) {
+ writer.writeStartElement(Constants.XML.IDENTITY_PROVIDER);
+ writer.writeAttribute(Constants.XML.ENTITY_ID, idp.getName());
+
+ ModelNode idpAttributes = idp.getValue();
+ for (SimpleAttributeDefinition attr : IdentityProviderDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, idpAttributes, false, writer);
+ }
+
+ writeSingleSignOn(writer, idpAttributes.get(Constants.Model.SINGLE_SIGN_ON));
+ writeSingleLogout(writer, idpAttributes.get(Constants.Model.SINGLE_LOGOUT));
+ writeKeys(writer, idpAttributes.get(Constants.Model.KEY));
+ }
+ writer.writeEndElement();
+ }
+
+ void writeSingleSignOn(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+ writer.writeStartElement(Constants.XML.SINGLE_SIGN_ON);
+ for (SimpleAttributeDefinition attr : SingleSignOnDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, model, false, writer);
+ }
writer.writeEndElement();
}
+ void writeSingleLogout(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+ writer.writeStartElement(Constants.XML.SINGLE_LOGOUT);
+ for (SimpleAttributeDefinition attr : SingleLogoutDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, model, false, writer);
+ }
+ writer.writeEndElement();
+ }
+
+ void writeKeys(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+ boolean contains = false;
+ for (Property key : model.asPropertyList()) {
+ if (!contains) {
+ writer.writeStartElement(Constants.XML.KEYS);
+ contains = true;
+ }
+ writer.writeStartElement(Constants.XML.KEY);
+ ModelNode keyAttributes = key.getValue();
+ for (SimpleAttributeDefinition attr : KeyDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, keyAttributes, false, writer);
+ }
+ for (SimpleAttributeDefinition attr : KeyDefinition.ELEMENTS) {
+ attr.getAttributeMarshaller().marshallAsElement(attr, keyAttributes, false, writer);
+ }
+ writeKeyStore(writer, keyAttributes.get(Constants.Model.KEY_STORE));
+
+ writer.writeEndElement();
+ }
+ if (contains) {
+ writer.writeEndElement();
+ }
+ }
+
+ void writeKeyStore(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+ writer.writeStartElement(Constants.XML.KEY_STORE);
+ for (SimpleAttributeDefinition attr : KeyStoreDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, model, false, writer);
+ }
+ writePrivateKey(writer, model);
+ writeCertificate(writer, model);
+ writer.writeEndElement();
+ }
+
+ void writeCertificate(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ ModelNode value = model.get(Constants.Model.CERTIFICATE_ALIAS);
+ if (!value.isDefined()) {
+ return;
+ }
+ writer.writeStartElement(Constants.XML.CERTIFICATE);
+ SimpleAttributeDefinition attr = KeyStoreCertificateDefinition.CERTIFICATE_ALIAS;
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, model, false, writer);
+ writer.writeEndElement();
+ }
+
+ void writePrivateKey(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ ModelNode pk_alias = model.get(Constants.Model.PRIVATE_KEY_ALIAS);
+ ModelNode pk_password = model.get(Constants.Model.PRIVATE_KEY_PASSWORD);
+
+ if (!pk_alias.isDefined() && !pk_password.isDefined()) {
+ return;
+ }
+ writer.writeStartElement(Constants.XML.PRIVATE_KEY);
+ for (SimpleAttributeDefinition attr : KeyStorePrivateKeyDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, model, false, writer);
+ }
+ writer.writeEndElement();
+ }
+
+ void writeRoleIdentifiers(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ ModelNode value = model.get(Constants.Model.ROLE_ATTRIBUTES);
+ if (!value.isDefined()) {
+ return;
+ }
+
+ List<ModelNode> items = value.asList();
+ if (items.size() == 0) {
+ return;
+ }
+
+ writer.writeStartElement(Constants.XML.ROLE_IDENTIFIERS);
+ for (ModelNode item : items) {
+ writer.writeStartElement(Constants.XML.ATTRIBUTE);
+ writer.writeAttribute("name", item.asString());
+ writer.writeEndElement();
+ }
+ writer.writeEndElement();
+ }
+
+ void writePrincipalNameMapping(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ writer.writeStartElement(Constants.XML.PRINCIPAL_NAME_MAPPING);
+ ModelNode value = model.get(Constants.Model.PRINCIPAL_NAME_MAPPING_POLICY);
+ if (value.isDefined()) {
+ writer.writeAttribute(Constants.XML.PRINCIPAL_NAME_MAPPING_POLICY, value.asString());
+ }
+ value = model.get(Constants.Model.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME);
+ if (value.isDefined()) {
+ writer.writeAttribute(Constants.XML.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, value.asString());
+ }
+ writer.writeEndElement();
+ }
}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyDefinition.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyDefinition.java
new file mode 100644
index 0000000..0f76399
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyDefinition.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.AttributeDefinition;
+import org.jboss.as.controller.ObjectTypeAttributeDefinition;
+import org.jboss.as.controller.OperationStepHandler;
+import org.jboss.as.controller.PathElement;
+import org.jboss.as.controller.ReloadRequiredRemoveStepHandler;
+import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler;
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.as.controller.SimpleResourceDefinition;
+import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
+import org.jboss.as.controller.registry.ManagementResourceRegistration;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KeyDefinition extends SimpleResourceDefinition {
+
+ static final SimpleAttributeDefinition SIGNING =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGNING, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.SIGNING)
+ .build();
+
+ static final SimpleAttributeDefinition ENCRYPTION =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.ENCRYPTION, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.ENCRYPTION)
+ .build();
+
+ static final SimpleAttributeDefinition PRIVATE_KEY_PEM =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PRIVATE_KEY_PEM, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PRIVATE_KEY_PEM)
+ .build();
+
+ static final SimpleAttributeDefinition PUBLIC_KEY_PEM =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PUBLIC_KEY_PEM, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PUBLIC_KEY_PEM)
+ .build();
+
+ static final SimpleAttributeDefinition CERTIFICATE_PEM =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.CERTIFICATE_PEM, ModelType.STRING, true)
+ .setXmlName(Constants.XML.CERTIFICATE_PEM)
+ .build();
+
+ static final ObjectTypeAttributeDefinition KEY_STORE =
+ ObjectTypeAttributeDefinition.Builder.of(Constants.Model.KEY_STORE,
+ KeyStoreDefinition.ALL_ATTRIBUTES)
+ .setAllowNull(false)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {SIGNING, ENCRYPTION};
+ static final SimpleAttributeDefinition[] ELEMENTS = {PRIVATE_KEY_PEM, PUBLIC_KEY_PEM, CERTIFICATE_PEM};
+ static final AttributeDefinition[] ALL_ATTRIBUTES = {SIGNING, ENCRYPTION, PRIVATE_KEY_PEM, PUBLIC_KEY_PEM, CERTIFICATE_PEM, KEY_STORE};
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static final HashMap<String, SimpleAttributeDefinition> ELEMENT_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ELEMENTS) {
+ ELEMENT_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static final KeyDefinition INSTANCE = new KeyDefinition();
+
+ private KeyDefinition() {
+ super(PathElement.pathElement(Constants.Model.KEY),
+ KeycloakSamlExtension.getResourceDescriptionResolver(Constants.Model.KEY),
+ new KeyAddHandler(),
+ ReloadRequiredRemoveStepHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerOperations(ManagementResourceRegistration resourceRegistration) {
+ super.registerOperations(resourceRegistration);
+ resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
+ super.registerAttributes(resourceRegistration);
+
+ final OperationStepHandler writeHandler = new ReloadRequiredWriteAttributeHandler(ALL_ATTRIBUTES);
+ for (AttributeDefinition attribute : ALL_ATTRIBUTES) {
+ resourceRegistration.registerReadWriteAttribute(attribute, null, writeHandler);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+
+ static SimpleAttributeDefinition lookupElement(String xmlName) {
+ return ELEMENT_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyStoreDefinition.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyStoreDefinition.java
new file mode 100644
index 0000000..2fb14f5
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyStoreDefinition.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+abstract class KeyStoreDefinition {
+
+ static final SimpleAttributeDefinition RESOURCE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.RESOURCE, ModelType.STRING, true)
+ .setXmlName(Constants.XML.RESOURCE)
+ .build();
+
+ static final SimpleAttributeDefinition PASSWORD =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PASSWORD, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PASSWORD)
+ .build();
+
+ static final SimpleAttributeDefinition FILE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.FILE, ModelType.STRING, true)
+ .setXmlName(Constants.XML.FILE)
+ .build();
+
+ static final SimpleAttributeDefinition TYPE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.TYPE, ModelType.STRING, true)
+ .setXmlName(Constants.XML.TYPE)
+ .build();
+
+ static final SimpleAttributeDefinition ALIAS =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.ALIAS, ModelType.STRING, true)
+ .setXmlName(Constants.XML.ALIAS)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {RESOURCE, PASSWORD, FILE, TYPE, ALIAS};
+ static final SimpleAttributeDefinition[] ALL_ATTRIBUTES = {RESOURCE, PASSWORD, FILE, TYPE, ALIAS,
+ KeyStorePrivateKeyDefinition.PRIVATE_KEY_ALIAS,
+ KeyStorePrivateKeyDefinition.PRIVATE_KEY_PASSWORD,
+ KeyStoreCertificateDefinition.CERTIFICATE_ALIAS
+ };
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyStorePrivateKeyDefinition.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyStorePrivateKeyDefinition.java
new file mode 100644
index 0000000..1947ca9
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeyStorePrivateKeyDefinition.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KeyStorePrivateKeyDefinition {
+ static final SimpleAttributeDefinition PRIVATE_KEY_ALIAS =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PRIVATE_KEY_ALIAS, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PRIVATE_KEY_ALIAS)
+ .build();
+
+ static final SimpleAttributeDefinition PRIVATE_KEY_PASSWORD =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PRIVATE_KEY_PASSWORD, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PRIVATE_KEY_PASSWORD)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {PRIVATE_KEY_ALIAS, PRIVATE_KEY_PASSWORD};
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/logging/KeycloakLogger.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/logging/KeycloakLogger.java
new file mode 100755
index 0000000..8bd7ac1
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/logging/KeycloakLogger.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7.logging;
+
+import org.jboss.logging.BasicLogger;
+import org.jboss.logging.LogMessage;
+import org.jboss.logging.Logger;
+import org.jboss.logging.Message;
+import org.jboss.logging.MessageLogger;
+
+import static org.jboss.logging.Logger.Level.DEBUG;
+import static org.jboss.logging.Logger.Level.INFO;
+
+/**
+ * This interface to be fleshed out later when error messages are fully externalized.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc.
+ */
+@MessageLogger(projectCode = "KEYCLOAK")
+public interface KeycloakLogger extends BasicLogger {
+
+ /**
+ * A logger with a category of the package name.
+ */
+ KeycloakLogger ROOT_LOGGER = Logger.getMessageLogger(KeycloakLogger.class, "org.jboss.keycloak");
+
+ @LogMessage(level = INFO)
+ @Message(value = "Keycloak SAML subsystem override for deployment %s")
+ void deploymentSecured(String deployment);
+
+ @LogMessage(level = DEBUG)
+ @Message(value = "Keycloak SAML has overriden and secured deployment %s")
+ void warSecured(String deployment);
+
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/logging/KeycloakMessages.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/logging/KeycloakMessages.java
new file mode 100755
index 0000000..f4917b1
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/logging/KeycloakMessages.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7.logging;
+
+import org.jboss.logging.MessageBundle;
+import org.jboss.logging.Messages;
+
+/**
+ * This interface to be fleshed out later when error messages are fully externalized.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2012 Red Hat Inc.
+ */
+@MessageBundle(projectCode = "TLIP")
+public interface KeycloakMessages {
+
+ /**
+ * The messages
+ */
+ KeycloakMessages MESSAGES = Messages.getBundle(KeycloakMessages.class);
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SecureDeploymentAddHandler.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SecureDeploymentAddHandler.java
new file mode 100644
index 0000000..c5325f6
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SecureDeploymentAddHandler.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.AbstractAddStepHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.as.controller.ServiceVerificationHandler;
+import org.jboss.dmr.ModelNode;
+import org.jboss.msc.service.ServiceController;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+class SecureDeploymentAddHandler extends AbstractAddStepHandler {
+
+ static SecureDeploymentAddHandler INSTANCE = new SecureDeploymentAddHandler();
+
+ private SecureDeploymentAddHandler() {
+ }
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List<ServiceController<?>> newControllers) throws OperationFailedException {
+ Configuration.INSTANCE.updateModel(operation, model);
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SecureDeploymentDefinition.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SecureDeploymentDefinition.java
new file mode 100644
index 0000000..75f4dca
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SecureDeploymentDefinition.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.PathElement;
+import org.jboss.as.controller.ReloadRequiredRemoveStepHandler;
+import org.jboss.as.controller.SimpleResourceDefinition;
+import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
+import org.jboss.as.controller.registry.ManagementResourceRegistration;
+
+/**
+ * Defines attributes and operations for a secure-deployment.
+ */
+public class SecureDeploymentDefinition extends SimpleResourceDefinition {
+
+ static final SecureDeploymentDefinition INSTANCE = new SecureDeploymentDefinition();
+
+ private SecureDeploymentDefinition() {
+ super(PathElement.pathElement(Constants.Model.SECURE_DEPLOYMENT),
+ KeycloakSamlExtension.getResourceDescriptionResolver(Constants.Model.SECURE_DEPLOYMENT),
+ SecureDeploymentAddHandler.INSTANCE,
+ ReloadRequiredRemoveStepHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerOperations(ManagementResourceRegistration resourceRegistration) {
+ super.registerOperations(resourceRegistration);
+ resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/ServiceProviderAddHandler.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/ServiceProviderAddHandler.java
new file mode 100644
index 0000000..33d6015
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/ServiceProviderAddHandler.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.AbstractAddStepHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.as.controller.ServiceVerificationHandler;
+import org.jboss.dmr.ModelNode;
+import org.jboss.msc.service.ServiceController;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+class ServiceProviderAddHandler extends AbstractAddStepHandler {
+
+ static final ServiceProviderAddHandler INSTANCE = new ServiceProviderAddHandler();
+
+ ServiceProviderAddHandler() {
+ super(ServiceProviderDefinition.ALL_ATTRIBUTES);
+ }
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List<ServiceController<?>> newControllers) throws OperationFailedException {
+ Configuration.INSTANCE.updateModel(operation, model);
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/ServiceProviderDefinition.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/ServiceProviderDefinition.java
new file mode 100644
index 0000000..02ecc8f
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/ServiceProviderDefinition.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.AttributeDefinition;
+import org.jboss.as.controller.ListAttributeDefinition;
+import org.jboss.as.controller.OperationStepHandler;
+import org.jboss.as.controller.PathElement;
+import org.jboss.as.controller.ReloadRequiredRemoveStepHandler;
+import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler;
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.as.controller.SimpleResourceDefinition;
+import org.jboss.as.controller.StringListAttributeDefinition;
+import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
+import org.jboss.as.controller.registry.ManagementResourceRegistration;
+import org.jboss.dmr.ModelType;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ServiceProviderDefinition extends SimpleResourceDefinition {
+
+ static final SimpleAttributeDefinition SSL_POLICY =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SSL_POLICY, ModelType.STRING, true)
+ .setXmlName(Constants.XML.SSL_POLICY)
+ .build();
+
+ static final SimpleAttributeDefinition NAME_ID_POLICY_FORMAT =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.NAME_ID_POLICY_FORMAT, ModelType.STRING, true)
+ .setXmlName(Constants.XML.NAME_ID_POLICY_FORMAT)
+ .build();
+
+ static final SimpleAttributeDefinition LOGOUT_PAGE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.LOGOUT_PAGE, ModelType.STRING, true)
+ .setXmlName(Constants.XML.LOGOUT_PAGE)
+ .build();
+
+ static final SimpleAttributeDefinition FORCE_AUTHENTICATION =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.FORCE_AUTHENTICATION, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.FORCE_AUTHENTICATION)
+ .build();
+
+ static final SimpleAttributeDefinition PRINCIPAL_NAME_MAPPING_POLICY =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PRINCIPAL_NAME_MAPPING_POLICY, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PRINCIPAL_NAME_MAPPING_POLICY)
+ .build();
+
+ static final SimpleAttributeDefinition PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME)
+ .build();
+
+ static final ListAttributeDefinition ROLE_ATTRIBUTES =
+ new StringListAttributeDefinition.Builder(Constants.Model.ROLE_ATTRIBUTES)
+ .setAllowNull(false)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {SSL_POLICY, NAME_ID_POLICY_FORMAT, LOGOUT_PAGE, FORCE_AUTHENTICATION};
+ static final AttributeDefinition[] ELEMENTS = {PRINCIPAL_NAME_MAPPING_POLICY, PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, ROLE_ATTRIBUTES};
+
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+ static final HashMap<String, AttributeDefinition> ALL_MAP = new HashMap<>();
+ static final Collection<AttributeDefinition> ALL_ATTRIBUTES;
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+
+ ALL_MAP.putAll(ATTRIBUTE_MAP);
+ for (AttributeDefinition def : ELEMENTS) {
+ ALL_MAP.put(def.getXmlName(), def);
+ }
+ ALL_ATTRIBUTES = Collections.unmodifiableCollection(ALL_MAP.values());
+ }
+
+ static final ServiceProviderDefinition INSTANCE = new ServiceProviderDefinition();
+
+ private ServiceProviderDefinition() {
+ super(PathElement.pathElement(Constants.Model.SERVICE_PROVIDER),
+ KeycloakSamlExtension.getResourceDescriptionResolver(Constants.Model.SERVICE_PROVIDER),
+ ServiceProviderAddHandler.INSTANCE,
+ ReloadRequiredRemoveStepHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerOperations(ManagementResourceRegistration resourceRegistration) {
+ super.registerOperations(resourceRegistration);
+ resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
+ super.registerAttributes(resourceRegistration);
+
+ final OperationStepHandler writeHandler = new ReloadRequiredWriteAttributeHandler(ALL_ATTRIBUTES);
+ for (AttributeDefinition attribute : ALL_ATTRIBUTES) {
+ resourceRegistration.registerReadWriteAttribute(attribute, null, writeHandler);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SingleLogoutDefinition.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SingleLogoutDefinition.java
new file mode 100644
index 0000000..beb884c
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SingleLogoutDefinition.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+abstract class SingleLogoutDefinition {
+
+ static final SimpleAttributeDefinition VALIDATE_REQUEST_SIGNATURE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.VALIDATE_REQUEST_SIGNATURE, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.VALIDATE_REQUEST_SIGNATURE)
+ .build();
+
+ static final SimpleAttributeDefinition VALIDATE_RESPONSE_SIGNATURE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.VALIDATE_RESPONSE_SIGNATURE, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.VALIDATE_RESPONSE_SIGNATURE)
+ .build();
+
+ static final SimpleAttributeDefinition SIGN_REQUEST =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGN_REQUEST, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.SIGN_REQUEST)
+ .build();
+
+ static final SimpleAttributeDefinition SIGN_RESPONSE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGN_RESPONSE, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.SIGN_RESPONSE)
+ .build();
+
+ static final SimpleAttributeDefinition REQUEST_BINDING =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.REQUEST_BINDING, ModelType.STRING, true)
+ .setXmlName(Constants.XML.REQUEST_BINDING)
+ .build();
+
+ static final SimpleAttributeDefinition RESPONSE_BINDING =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.RESPONSE_BINDING, ModelType.STRING, true)
+ .setXmlName(Constants.XML.RESPONSE_BINDING)
+ .build();
+
+ static final SimpleAttributeDefinition POST_BINDING_URL =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.POST_BINDING_URL, ModelType.STRING, true)
+ .setXmlName(Constants.XML.POST_BINDING_URL)
+ .build();
+
+ static final SimpleAttributeDefinition REDIRECT_BINDING_URL =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.REDIRECT_BINDING_URL, ModelType.STRING, true)
+ .setXmlName(Constants.XML.REDIRECT_BINDING_URL)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {VALIDATE_REQUEST_SIGNATURE, VALIDATE_RESPONSE_SIGNATURE,
+ SIGN_REQUEST, SIGN_RESPONSE, REQUEST_BINDING, RESPONSE_BINDING, POST_BINDING_URL, REDIRECT_BINDING_URL};
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SingleSignOnDefinition.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SingleSignOnDefinition.java
new file mode 100644
index 0000000..827be9b
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/SingleSignOnDefinition.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.saml.as7;
+
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+abstract class SingleSignOnDefinition {
+
+ static final SimpleAttributeDefinition SIGN_REQUEST =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGN_REQUEST, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.SIGN_REQUEST)
+ .build();
+
+ static final SimpleAttributeDefinition VALIDATE_RESPONSE_SIGNATURE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.VALIDATE_RESPONSE_SIGNATURE, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.VALIDATE_RESPONSE_SIGNATURE)
+ .build();
+
+ static final SimpleAttributeDefinition REQUEST_BINDING =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.REQUEST_BINDING, ModelType.STRING, true)
+ .setXmlName(Constants.XML.REQUEST_BINDING)
+ .build();
+
+ static final SimpleAttributeDefinition RESPONSE_BINDING =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.RESPONSE_BINDING, ModelType.STRING, true)
+ .setXmlName(Constants.XML.RESPONSE_BINDING)
+ .build();
+
+ static final SimpleAttributeDefinition BINDING_URL =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.BINDING_URL, ModelType.STRING, true)
+ .setXmlName(Constants.XML.BINDING_URL)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {SIGN_REQUEST, VALIDATE_RESPONSE_SIGNATURE, REQUEST_BINDING, RESPONSE_BINDING, BINDING_URL};
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java
new file mode 100644
index 0000000..dda3ed3
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/FormattingXMLStreamWriter.java
@@ -0,0 +1,534 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2010, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+
+package org.keycloak.subsystem.saml.as7.xml;
+
+import org.jboss.staxmapper.XMLExtendedStreamWriter;
+
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.ArrayDeque;
+import java.util.Iterator;
+
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+
+/**
+ * An XML stream writer which nicely formats the XML for configuration files.
+ *
+ * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
+ */
+public final class FormattingXMLStreamWriter implements XMLExtendedStreamWriter, XMLStreamConstants {
+ private static final String NO_NAMESPACE = new String();
+ private final XMLStreamWriter delegate;
+ private final ArrayDeque<ArgRunnable> attrQueue = new ArrayDeque<ArgRunnable>();
+ private int level;
+ private int state = START_DOCUMENT;
+ private boolean indentEndElement = false;
+ private ArrayDeque<String> unspecifiedNamespaces = new ArrayDeque<String>();
+
+
+ public FormattingXMLStreamWriter(final XMLStreamWriter delegate) {
+ this.delegate = delegate;
+ unspecifiedNamespaces.push(NO_NAMESPACE);
+ }
+
+ private void nl() throws XMLStreamException {
+ delegate.writeCharacters("\n");
+ }
+
+ private void indent() throws XMLStreamException {
+ int level = this.level;
+ final XMLStreamWriter delegate = this.delegate;
+ for (int i = 0; i < level; i ++) {
+ delegate.writeCharacters(" ");
+ }
+ }
+
+ private interface ArgRunnable {
+ public void run(int arg) throws XMLStreamException;
+ }
+
+ @Override
+ public void setUnspecifiedElementNamespace(final String namespace) {
+ ArrayDeque<String> namespaces = this.unspecifiedNamespaces;
+ namespaces.pop();
+ namespaces.push(namespace == null ? NO_NAMESPACE : namespace);
+ }
+
+ private String nestUnspecifiedNamespace() {
+ ArrayDeque<String> namespaces = unspecifiedNamespaces;
+ String clone = namespaces.getFirst();
+ namespaces.push(clone);
+ return clone;
+ }
+
+ @Override
+ public void writeStartElement(final String localName) throws XMLStreamException {
+ ArrayDeque<String> namespaces = unspecifiedNamespaces;
+ String namespace = namespaces.getFirst();
+ if (namespace != NO_NAMESPACE) {
+ writeStartElement(namespace, localName);
+ return;
+ }
+
+ unspecifiedNamespaces.push(namespace);
+
+ // If this is a nested element flush the outer
+ runAttrQueue();
+ nl();
+ indent();
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ if (arg == 0) {
+ delegate.writeStartElement(localName);
+ } else {
+ delegate.writeEmptyElement(localName);
+ }
+ }
+ });
+
+ level++;
+ state = START_ELEMENT;
+ indentEndElement = false;
+ }
+
+ @Override
+ public void writeStartElement(final String namespaceURI, final String localName) throws XMLStreamException {
+ nestUnspecifiedNamespace();
+
+ // If this is a nested element flush the outer
+ runAttrQueue();
+ nl();
+ indent();
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ if (arg == 0) {
+ delegate.writeStartElement(namespaceURI, localName);
+ } else {
+ delegate.writeEmptyElement(namespaceURI, localName);
+ }
+ }
+ });
+ level++;
+ state = START_ELEMENT;
+ indentEndElement = false;
+ }
+
+ @Override
+ public void writeStartElement(final String prefix, final String localName, final String namespaceURI) throws XMLStreamException {
+ nestUnspecifiedNamespace();
+
+ // If this is a nested element flush the outer
+ runAttrQueue();
+ nl();
+ indent();
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ if (arg == 0) {
+ delegate.writeStartElement(prefix, namespaceURI, localName);
+ } else {
+ delegate.writeEmptyElement(prefix, namespaceURI, localName);
+ }
+ }
+ });
+ level++;
+ state = START_ELEMENT;
+ indentEndElement = false;
+ }
+
+ @Override
+ public void writeEmptyElement(final String namespaceURI, final String localName) throws XMLStreamException {
+ runAttrQueue();
+ nl();
+ indent();
+ delegate.writeEmptyElement(namespaceURI, localName);
+ state = END_ELEMENT;
+ }
+
+ @Override
+ public void writeEmptyElement(final String prefix, final String localName, final String namespaceURI) throws XMLStreamException {
+ runAttrQueue();
+ nl();
+ indent();
+ delegate.writeEmptyElement(prefix, namespaceURI, localName);
+ state = END_ELEMENT;
+ }
+
+ @Override
+ public void writeEmptyElement(final String localName) throws XMLStreamException {
+ String namespace = unspecifiedNamespaces.getFirst();
+ if (namespace != NO_NAMESPACE) {
+ writeEmptyElement(namespace, localName);
+ return;
+ }
+
+ runAttrQueue();
+ nl();
+ indent();
+ delegate.writeEmptyElement(localName);
+ state = END_ELEMENT;
+ }
+
+ @Override
+ public void writeEndElement() throws XMLStreamException {
+ level--;
+ if (state != START_ELEMENT) {
+ runAttrQueue();
+ if (state != CHARACTERS || indentEndElement) {
+ nl();
+ indent();
+ indentEndElement = false;
+ }
+ delegate.writeEndElement();
+ } else {
+ // Change the start element to an empty element
+ ArgRunnable start = attrQueue.poll();
+ if (start == null) {
+ delegate.writeEndElement();
+ } else {
+ start.run(1);
+ // Write everything else
+ runAttrQueue();
+ }
+ }
+
+ unspecifiedNamespaces.pop();
+ state = END_ELEMENT;
+ }
+
+ private void runAttrQueue() throws XMLStreamException {
+ ArgRunnable attr;
+ while ((attr = attrQueue.poll()) != null) {
+ attr.run(0);
+ }
+ }
+
+ @Override
+ public void writeEndDocument() throws XMLStreamException {
+ delegate.writeEndDocument();
+ state = END_DOCUMENT;
+ }
+
+ @Override
+ public void close() throws XMLStreamException {
+ delegate.close();
+ state = END_DOCUMENT;
+ }
+
+ @Override
+ public void flush() throws XMLStreamException {
+ delegate.flush();
+ }
+
+ @Override
+ public void writeAttribute(final String localName, final String value) throws XMLStreamException {
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ try {
+ delegate.writeAttribute(localName, value);
+ } catch (XMLStreamException e) {
+ throw new UndeclaredThrowableException(e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void writeAttribute(final String prefix, final String namespaceURI, final String localName, final String value) throws XMLStreamException {
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ delegate.writeAttribute(prefix, namespaceURI, localName, value);
+ }
+ });
+ }
+
+ @Override
+ public void writeAttribute(final String namespaceURI, final String localName, final String value) throws XMLStreamException {
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ delegate.writeAttribute(namespaceURI, localName, value);
+ }
+ });
+ }
+
+ @Override
+ public void writeAttribute(final String localName, final String[] values) throws XMLStreamException {
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ delegate.writeAttribute(localName, join(values));
+ }
+ });
+ }
+
+ @Override
+ public void writeAttribute(final String prefix, final String namespaceURI, final String localName, final String[] values) throws XMLStreamException {
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ delegate.writeAttribute(prefix, namespaceURI, localName, join(values));
+ }
+ });
+ }
+
+ @Override
+ public void writeAttribute(final String namespaceURI, final String localName, final String[] values) throws XMLStreamException {
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ delegate.writeAttribute(namespaceURI, localName, join(values));
+ }
+ });
+ }
+
+ @Override
+ public void writeAttribute(final String localName, final Iterable<String> values) throws XMLStreamException {
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ delegate.writeAttribute(localName, join(values));
+ }
+ });
+ }
+
+ @Override
+ public void writeAttribute(final String prefix, final String namespaceURI, final String localName, final Iterable<String> values) throws XMLStreamException {
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ delegate.writeAttribute(prefix, namespaceURI, localName, join(values));
+ }
+ });
+ }
+
+ @Override
+ public void writeAttribute(final String namespaceURI, final String localName, final Iterable<String> values) throws XMLStreamException {
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ delegate.writeAttribute(namespaceURI, localName, join(values));
+ }
+ });
+ }
+
+ @Override
+ public void writeNamespace(final String prefix, final String namespaceURI) throws XMLStreamException {
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ delegate.writeNamespace(prefix, namespaceURI);
+ }
+ });
+ }
+
+ @Override
+ public void writeDefaultNamespace(final String namespaceURI) throws XMLStreamException {
+ attrQueue.add(new ArgRunnable() {
+ public void run(int arg) throws XMLStreamException {
+ delegate.writeDefaultNamespace(namespaceURI);
+ }
+ });
+ }
+
+ @Override
+ public void writeComment(final String data) throws XMLStreamException {
+ runAttrQueue();
+ nl();
+ nl();
+ indent();
+ final StringBuilder b = new StringBuilder(data.length());
+ final Iterator<String> i = Spliterator.over(data, '\n');
+ if (! i.hasNext()) {
+ return;
+ } else {
+ final String first = i.next();
+ if (! i.hasNext()) {
+ delegate.writeComment(" " + first + " ");
+ state = COMMENT;
+ return;
+ } else {
+ b.append('\n');
+ for (int q = 0; q < level; q++) {
+ b.append(" ");
+ }
+ b.append(" ~ ");
+ b.append(first);
+ do {
+ b.append('\n');
+ for (int q = 0; q < level; q++) {
+ b.append(" ");
+ }
+ b.append(" ~ ");
+ b.append(i.next());
+ } while (i.hasNext());
+ }
+ b.append('\n');
+ for (int q = 0; q < level; q ++) {
+ b.append(" ");
+ }
+ b.append(" ");
+ delegate.writeComment(b.toString());
+ state = COMMENT;
+ }
+ }
+
+ @Override
+ public void writeProcessingInstruction(final String target) throws XMLStreamException {
+ runAttrQueue();
+ nl();
+ indent();
+ delegate.writeProcessingInstruction(target);
+ state = PROCESSING_INSTRUCTION;
+ }
+
+ @Override
+ public void writeProcessingInstruction(final String target, final String data) throws XMLStreamException {
+ runAttrQueue();
+ nl();
+ indent();
+ delegate.writeProcessingInstruction(target, data);
+ state = PROCESSING_INSTRUCTION;
+ }
+
+ @Override
+ public void writeCData(final String data) throws XMLStreamException {
+ runAttrQueue();
+ delegate.writeCData(data);
+ state = CDATA;
+ }
+
+ @Override
+ public void writeDTD(final String dtd) throws XMLStreamException {
+ nl();
+ indent();
+ delegate.writeDTD(dtd);
+ state = DTD;
+ }
+
+ @Override
+ public void writeEntityRef(final String name) throws XMLStreamException {
+ runAttrQueue();
+ delegate.writeEntityRef(name);
+ state = ENTITY_REFERENCE;
+ }
+
+ @Override
+ public void writeStartDocument() throws XMLStreamException {
+ delegate.writeStartDocument();
+ nl();
+ state = START_DOCUMENT;
+ }
+
+ @Override
+ public void writeStartDocument(final String version) throws XMLStreamException {
+ delegate.writeStartDocument(version);
+ nl();
+ state = START_DOCUMENT;
+ }
+
+ @Override
+ public void writeStartDocument(final String encoding, final String version) throws XMLStreamException {
+ delegate.writeStartDocument(encoding, version);
+ nl();
+ state = START_DOCUMENT;
+ }
+
+ @Override
+ public void writeCharacters(final String text) throws XMLStreamException {
+ runAttrQueue();
+ if (state != CHARACTERS) {
+ nl();
+ indent();
+ }
+ final Iterator<String> iterator = Spliterator.over(text, '\n');
+ while (iterator.hasNext()) {
+ final String t = iterator.next();
+ delegate.writeCharacters(t);
+ if (iterator.hasNext()) {
+ nl();
+ indent();
+ }
+ }
+ state = CHARACTERS;
+ indentEndElement = true;
+ }
+
+ @Override
+ public void writeCharacters(final char[] text, final int start, final int len) throws XMLStreamException {
+ runAttrQueue();
+ delegate.writeCharacters(text, start, len);
+ state = CHARACTERS;
+ }
+
+ @Override
+ public String getPrefix(final String uri) throws XMLStreamException {
+ return delegate.getPrefix(uri);
+ }
+
+ @Override
+ public void setPrefix(final String prefix, final String uri) throws XMLStreamException {
+ delegate.setPrefix(prefix, uri);
+ }
+
+ @Override
+ public void setDefaultNamespace(final String uri) throws XMLStreamException {
+ runAttrQueue();
+ delegate.setDefaultNamespace(uri);
+ }
+
+ @Override
+ public void setNamespaceContext(final NamespaceContext context) throws XMLStreamException {
+ delegate.setNamespaceContext(context);
+ }
+
+ @Override
+ public NamespaceContext getNamespaceContext() {
+ return delegate.getNamespaceContext();
+ }
+
+ @Override
+ public Object getProperty(final String name) throws IllegalArgumentException {
+ return delegate.getProperty(name);
+ }
+
+ private static String join(final String[] values) {
+ final StringBuilder b = new StringBuilder();
+ for (int i = 0, valuesLength = values.length; i < valuesLength; i++) {
+ final String s = values[i];
+ if (s != null) {
+ if (i > 0) {
+ b.append(' ');
+ }
+ b.append(s);
+ }
+ }
+ return b.toString();
+ }
+
+ private static String join(final Iterable<String> values) {
+ final StringBuilder b = new StringBuilder();
+ Iterator<String> iterator = values.iterator();
+ while (iterator.hasNext()) {
+ final String s = iterator.next();
+ if (s != null) {
+ b.append(s);
+ if (iterator.hasNext()) b.append(' ');
+ }
+ }
+ return b.toString();
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/Spliterator.java b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/Spliterator.java
new file mode 100644
index 0000000..e9cb5c6
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/xml/Spliterator.java
@@ -0,0 +1,66 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2010, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+
+package org.keycloak.subsystem.saml.as7.xml;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
+ */
+final class Spliterator implements Iterator<String> {
+ private final String subject;
+ private final char delimiter;
+ private int i;
+
+ Spliterator(final String subject, final char delimiter) {
+ this.subject = subject;
+ this.delimiter = delimiter;
+ i = 0;
+ }
+
+ static Spliterator over(String subject, char delimiter) {
+ return new Spliterator(subject, delimiter);
+ }
+
+ public boolean hasNext() {
+ return i != -1;
+ }
+
+ public String next() {
+ final int i = this.i;
+ if (i == -1) {
+ throw new NoSuchElementException();
+ }
+ int n = subject.indexOf(delimiter, i);
+ try {
+ return n == -1 ? subject.substring(i) : subject.substring(i, n);
+ } finally {
+ this.i = n == -1 ? -1 : n + 1;
+ }
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/resources/org/keycloak/subsystem/saml/as7/LocalDescriptions.properties b/saml/client-adapter/as7-eap6/subsystem/src/main/resources/org/keycloak/subsystem/saml/as7/LocalDescriptions.properties
new file mode 100755
index 0000000..f8a4a11
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/resources/org/keycloak/subsystem/saml/as7/LocalDescriptions.properties
@@ -0,0 +1,63 @@
+keycloak-saml.subsystem=Keycloak adapter subsystem
+keycloak-saml.subsystem.add=Operation Adds Keycloak adapter subsystem
+keycloak-saml.subsystem.remove=Operation removes Keycloak adapter subsystem
+keycloak-saml.subsystem.secure-deployment=A deployment secured by Keycloak.
+
+keycloak-saml.secure-deployment=A deployment secured by Keycloak
+keycloak-saml.secure-deployment.add=Add a deployment to be secured by Keycloak
+keycloak-saml.secure-deployment.remove=Remove a deployment to be secured by Keycloak
+keycloak-saml.secure-deployment.service-provider=A security provider configuration for secure deployment
+
+keycloak-saml.service-provider=A security provider configuration for secure deployment
+keycloak-saml.service-provider.add=Add a security provider configuration to deployment secured by Keycloak SAML
+keycloak-saml.service-provider.remove=Remove a security provider definition from deployment secured by Keycloak SAML
+keycloak-saml.service-provider.ssl-policy=SSL Policy to use
+keycloak-saml.service-provider.name-id-policy-format=Name ID policy format URN
+keycloak-saml.service-provider.logout-page=URI to a logout page
+keycloak-saml.service-provider.force-authentication=Redirected unauthenticated request to a login page
+keycloak-saml.service-provider.role-attributes=Role identifiers
+keycloak-saml.service-provider.principal-name-mapping-policy=Principal name mapping policy
+keycloak-saml.service-provider.principal-name-mapping-attribute-name=Principal name mapping attribute name
+keycloak-saml.service-provider.key=A key definition
+keycloak-saml.service-provider.identity-provider=Identity provider definition
+
+keycloak-saml.key=A key configuration for service provider or identity provider
+keycloak-saml.key.add=Add a key definition
+keycloak-saml.key.remove=Remove a key definition
+keycloak-saml.key.signing=Key can be used for signing
+keycloak-saml.key.encryption=Key can be used for encryption
+keycloak-saml.key.private-key-pem=Private key string in pem format
+keycloak-saml.key.public-key-pem=Public key string in pem format
+keycloak-saml.key.certificate-pem=Certificate key string in pem format
+keycloak-saml.key.key-store=Key store definition
+keycloak-saml.key.key-store.file=Key store filesystem path
+keycloak-saml.key.key-store.resource=Key store resource URI
+keycloak-saml.key.key-store.password=Key store password
+keycloak-saml.key.key-store.type=Key store format
+keycloak-saml.key.key-store.alias=Key alias
+keycloak-saml.key.key-store.private-key-alias=Private key alias
+keycloak-saml.key.key-store.private-key-password=Private key password
+keycloak-saml.key.key-store.certificate-alias=Certificate alias
+
+keycloak-saml.identity-provider=An identity provider configuration
+keycloak-saml.identity-provider.add=Add an identity provider
+keycloak-saml.identity-provider.remove=Remove an identity provider
+keycloak-saml.identity-provider.signatures-required=Require signatures for single-sign-on and single-logout
+keycloak-saml.identity-provider.signature-algorithm=Signature algorithm
+keycloak-saml.identity-provider.signature-canonicalization-method=Signature canonicalization method
+keycloak-saml.identity-provider.single-sign-on=Single sign-on configuration
+keycloak-saml.identity-provider.single-sign-on.sign-request=Sign SSO requests
+keycloak-saml.identity-provider.single-sign-on.validate-response-signature=Validate an SSO response signature
+keycloak-saml.identity-provider.single-sign-on.request-binding=HTTP method to use for requests
+keycloak-saml.identity-provider.single-sign-on.response-binding=HTTP method to use for responses
+keycloak-saml.identity-provider.single-sign-on.binding-url=SSO endpoint URL
+keycloak-saml.identity-provider.single-logout=Single logout configuration
+keycloak-saml.identity-provider.single-logout.validate-request-signature=Validate a single-logout request signature
+keycloak-saml.identity-provider.single-logout.validate-response-signature=Validate a single-logout response signature
+keycloak-saml.identity-provider.single-logout.sign-request=Sign single-logout requests
+keycloak-saml.identity-provider.single-logout.sign-response=Sign single-logout responses
+keycloak-saml.identity-provider.single-logout.request-binding=HTTP method to use for request
+keycloak-saml.identity-provider.single-logout.response-binding=HTTP method to use for response
+keycloak-saml.identity-provider.single-logout.post-binding-url=Endpoint URL for posting
+keycloak-saml.identity-provider.single-logout.redirect-binding-url=Endpoint URL for redirects
+keycloak-saml.identity-provider.key=Key definition for identity provider
\ No newline at end of file
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_1.xsd b/saml/client-adapter/as7-eap6/subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_1.xsd
new file mode 100644
index 0000000..725104b
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_1.xsd
@@ -0,0 +1,268 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ targetNamespace="urn:jboss:domain:keycloak-saml:1.1"
+ xmlns="urn:jboss:domain:keycloak-saml:1.1"
+ elementFormDefault="qualified"
+ attributeFormDefault="unqualified"
+ version="1.0">
+
+ <!-- The subsystem root element -->
+ <xs:element name="subsystem" type="subsystem-type"/>
+
+ <xs:complexType name="subsystem-type">
+ <xs:annotation>
+ <xs:documentation>
+ <![CDATA[
+ The Keycloak SAML adapter subsystem, used to register deployments managed by Keycloak SAML adapter
+ ]]>
+ </xs:documentation>
+ </xs:annotation>
+ <xs:all>
+ <xs:element name="secure-deployment" minOccurs="0" type="secure-deployment-type"/>
+ </xs:all>
+ </xs:complexType>
+
+ <xs:complexType name="secure-deployment-type">
+ <xs:all>
+ <xs:element name="SP" minOccurs="1" maxOccurs="1" type="sp-type"/>
+ </xs:all>
+ <xs:attribute name="name" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>The name of the realm.</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+
+ <xs:complexType name="sp-type">
+ <xs:all>
+ <xs:element name="Keys" minOccurs="0" maxOccurs="1" type="keys-type"/>
+ <xs:element name="PrincipalNameMapping" minOccurs="0" maxOccurs="1" type="principal-name-mapping-type"/>
+ <xs:element name="RoleIdentifiers" minOccurs="0" maxOccurs="1" type="role-identifiers-type"/>
+ <xs:element name="IDP" minOccurs="1" maxOccurs="1" type="identity-provider-type"/>
+ </xs:all>
+ <xs:attribute name="entityID" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>The entity ID for SAML service provider</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="sslPolicy" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>The ssl policy</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="nameIDPolicyFormat" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Name ID policy format URN</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="logoutPage" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>URI to a logout page</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="forceAuthentication" type="xs:boolean" use="required">
+ <xs:annotation>
+ <xs:documentation>Redirected unauthenticated request to a login page</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="identity-provider-type">
+ <xs:all minOccurs="1" maxOccurs="1">
+ <xs:element name="SingleSignOnService" minOccurs="1" maxOccurs="1" type="single-signon-type"/>
+ <xs:element name="SingleLogoutService" minOccurs="0" maxOccurs="1" type="single-logout-type"/>
+ <xs:element name="Keys" minOccurs="0" maxOccurs="1" type="keys-type"/>
+ </xs:all>
+ <xs:attribute name="entityID" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>The entity ID for SAML service provider</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="signaturesRequired" type="xs:boolean" use="required">
+ <xs:annotation>
+ <xs:documentation>Require signatures for single-sign-on and single-logout</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="signatureAlgorithm" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Algorithm used for signatures</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Canonicalization method used for signatures</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="single-signon-type">
+ <xs:attribute name="signRequest" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Sign the SSO requests</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Validate the SSO response signature</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="requestBinding" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>HTTP method to use for requests</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="responseBinding" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>HTTP method to use for response</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="bindingUrl" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>SSO endpoint URL</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="single-logout-type">
+ <xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Validate a single-logout request signature</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Validate a single-logout response signature</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="signRequest" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Sign single-logout requests</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="signResponse" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Sign single-logout responses</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="requestBinding" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>HTTP method to use for request</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="responseBinding" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>HTTP method to use for response</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="postBindingUrl" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Endpoint URL for posting</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="redirectBindingUrl" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Endpoint URL for redirects</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="keys-type">
+ <xs:sequence>
+ <xs:element name="Key" minOccurs="1" maxOccurs="2" type="key-type"/>
+ </xs:sequence>
+ </xs:complexType>
+ <xs:complexType name="key-type">
+ <xs:all>
+ <xs:element name="KeyStore" minOccurs="0" maxOccurs="1" type="keystore-type"/>
+ <xs:element name="PrivateKeyPem" minOccurs="0" maxOccurs="1" type="xs:string"/>
+ <xs:element name="PublicKeyPem" minOccurs="0" maxOccurs="1" type="xs:string"/>
+ <xs:element name="CertificatePem" minOccurs="0" maxOccurs="1" type="xs:string"/>
+ </xs:all>
+ <xs:attribute name="signing" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key can be used for signing</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="encryption" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key can be used for encryption</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="keystore-type">
+ <xs:sequence minOccurs="0" maxOccurs="1">
+ <xs:element name="PrivateKey" minOccurs="0" maxOccurs="1" type="privatekey-type"/>
+ <xs:element name="Certificate" minOccurs="0" maxOccurs="1" type="certificate-type"/>
+ </xs:sequence>
+ <xs:attribute name="file" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key store filesystem path</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="resource" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key store resource URI</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="password" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Key store password</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="type" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key store format</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="alias" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key alias</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+
+ <xs:complexType name="privatekey-type">
+ <xs:attribute name="alias" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Private key alias</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="password" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Private key password</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+
+ <xs:complexType name="certificate-type">
+ <xs:attribute name="alias" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Certificate alias</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+
+ <xs:complexType name="principal-name-mapping-type">
+ <xs:attribute name="policy" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Principal name mapping policy. Possible values: FROM_NAME_ID</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="attribute" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Name of the attribute to use for principal name mapping</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+
+ <xs:complexType name="role-identifiers-type">
+ <xs:sequence minOccurs="0" maxOccurs="unbounded">
+ <xs:element name="Attribute" minOccurs="0" maxOccurs="unbounded" type="attribute-type"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="attribute-type">
+ <xs:attribute name="name" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Role attribute</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+</xs:schema>
diff --git a/saml/client-adapter/as7-eap6/subsystem/src/test/resources/org/keycloak/subsystem/saml/as7/keycloak-saml-1.1.xml b/saml/client-adapter/as7-eap6/subsystem/src/test/resources/org/keycloak/subsystem/saml/as7/keycloak-saml-1.1.xml
new file mode 100644
index 0000000..19d7528
--- /dev/null
+++ b/saml/client-adapter/as7-eap6/subsystem/src/test/resources/org/keycloak/subsystem/saml/as7/keycloak-saml-1.1.xml
@@ -0,0 +1,49 @@
+<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1">
+ <secure-deployment name="my-app.war">
+ <SP entityID="http://localhost:8080/sales-post-enc/"
+ sslPolicy="EXTERNAL"
+ nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ logoutPage="/logout.jsp"
+ forceAuthentication="false">
+
+ <Keys>
+ <Key encryption="true" signing="true">
+ <PrivateKeyPem>my_key.pem</PrivateKeyPem>
+ <PublicKeyPem>my_key.pub</PublicKeyPem>
+ <CertificatePem>cert.cer</CertificatePem>
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
+ <PrivateKey alias="http://localhost:8080/sales-post-enc/" password="test123"/>
+ <Certificate alias="http://localhost:8080/sales-post-enc/"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ <PrincipalNameMapping policy="FROM_NAME_ID"/>
+ <RoleIdentifiers>
+ <Attribute name="Role"/>
+ <Attribute name="Role2"/>
+ </RoleIdentifiers>
+ <IDP entityID="idp">
+ <SingleSignOnService signRequest="true"
+ validateResponseSignature="true"
+ requestBinding="POST"
+ bindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"/>
+ <SingleLogoutService
+ validateRequestSignature="true"
+ validateResponseSignature="true"
+ signRequest="true"
+ signResponse="true"
+ requestBinding="POST"
+ responseBinding="POST"
+ postBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"
+ redirectBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"/>
+ <Keys>
+ <Key signing="true">
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
+ <Certificate alias="saml-demo"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ </IDP>
+ </SP>
+ </secure-deployment>
+</subsystem>
\ No newline at end of file
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java
index 280b5f8..e921d15 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/AdapterConstants.java
@@ -5,5 +5,5 @@ package org.keycloak.adapters.saml;
* @version $Revision: 1 $
*/
public class AdapterConstants {
- public static final String AUTH_DATA_PARAM_NAME="org.keycloak.auth.deployment.data";
+ public static final String AUTH_DATA_PARAM_NAME="org.keycloak.saml.xml.adapterConfig";
}
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java
index 404fc58..91a016c 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/ConfigXmlConstants.java
@@ -5,17 +5,17 @@ package org.keycloak.adapters.saml.config.parsers;
* @version $Revision: 1 $
*/
public class ConfigXmlConstants {
- public static final String KEYCLOAK_SAML_ADAPTER ="keycloak-saml-adapter";
- public static final String SP_ELEMENT="SP";
+ public static final String KEYCLOAK_SAML_ADAPTER = "keycloak-saml-adapter";
+ public static final String SP_ELEMENT = "SP";
public static final String ENTITY_ID_ATTR = "entityID";
public static final String SSL_POLICY_ATTR = "sslPolicy";
public static final String NAME_ID_POLICY_FORMAT_ATTR = "nameIDPolicyFormat";
public static final String FORCE_AUTHENTICATION_ATTR = "forceAuthentication";
+ public static final String IS_PASSIVE_ATTR = "isPassive";
public static final String SIGNATURE_ALGORITHM_ATTR = "signatureAlgorithm";
public static final String SIGNATURE_CANONICALIZATION_METHOD_ATTR = "signatureCanonicalizationMethod";
public static final String LOGOUT_PAGE_ATTR = "logoutPage";
-
public static final String KEYS_ELEMENT = "Keys";
public static final String KEY_ELEMENT = "Key";
public static final String SIGNING_ATTR = "signing";
@@ -36,7 +36,6 @@ public class ConfigXmlConstants {
public static final String POLICY_ATTR = "policy";
public static final String ATTRIBUTE_ATTR = "attribute";
-
public static final String ROLE_IDENTIFIERS_ELEMENT = "RoleIdentifiers";
public static final String ATTRIBUTE_ELEMENT = "Attribute";
public static final String NAME_ATTR = "name";
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
index b200b61..5de2072 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java
@@ -41,6 +41,7 @@ public class DeploymentBuilder {
deployment.setConfigured(true);
deployment.setEntityID(sp.getEntityID());
deployment.setForceAuthentication(sp.isForceAuthentication());
+ deployment.setIsPassive(sp.isIsPassive());
deployment.setNameIDPolicyFormat(sp.getNameIDPolicyFormat());
deployment.setLogoutPage(sp.getLogoutPage());
deployment.setSignatureCanonicalizationMethod(sp.getIdp().getSignatureCanonicalizationMethod());
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java
index 3446f20..14b04e1 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SPXmlParser.java
@@ -1,21 +1,22 @@
package org.keycloak.adapters.saml.config.parsers;
-import org.keycloak.adapters.saml.config.IDP;
-import org.keycloak.adapters.saml.config.Key;
-import org.keycloak.adapters.saml.config.SP;
-import org.keycloak.saml.common.exceptions.ParsingException;
-import org.keycloak.saml.common.parsers.AbstractParser;
-import org.keycloak.saml.common.util.StaxParserUtil;
-import org.keycloak.common.util.StringPropertyReplacer;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+
+import org.keycloak.adapters.saml.config.IDP;
+import org.keycloak.adapters.saml.config.Key;
+import org.keycloak.adapters.saml.config.SP;
+import org.keycloak.common.util.StringPropertyReplacer;
+import org.keycloak.saml.common.exceptions.ParsingException;
+import org.keycloak.saml.common.parsers.AbstractParser;
+import org.keycloak.saml.common.util.StaxParserUtil;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -25,13 +26,16 @@ public class SPXmlParser extends AbstractParser {
public static String getAttributeValue(StartElement startElement, String tag) {
String str = StaxParserUtil.getAttributeValue(startElement, tag);
- if (str != null) return StringPropertyReplacer.replaceProperties(str);
- else return str;
+ if (str != null)
+ return StringPropertyReplacer.replaceProperties(str);
+ else
+ return str;
}
public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) {
String result = getAttributeValue(startElement, tag);
- if (result == null) return defaultValue;
+ if (result == null)
+ return defaultValue;
return Boolean.valueOf(result);
}
@@ -41,11 +45,11 @@ public class SPXmlParser extends AbstractParser {
public static String getElementText(XMLEventReader xmlEventReader) throws ParsingException {
String result = StaxParserUtil.getElementText(xmlEventReader);
- if (result != null) result = StringPropertyReplacer.replaceProperties(result);
+ if (result != null)
+ result = StringPropertyReplacer.replaceProperties(result);
return result;
}
-
@Override
public Object parse(XMLEventReader xmlEventReader) throws ParsingException {
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
@@ -61,6 +65,7 @@ public class SPXmlParser extends AbstractParser {
sp.setLogoutPage(getAttributeValue(startElement, ConfigXmlConstants.LOGOUT_PAGE_ATTR));
sp.setNameIDPolicyFormat(getAttributeValue(startElement, ConfigXmlConstants.NAME_ID_POLICY_FORMAT_ATTR));
sp.setForceAuthentication(getBooleanAttributeValue(startElement, ConfigXmlConstants.FORCE_AUTHENTICATION_ATTR));
+ sp.setIsPassive(getBooleanAttributeValue(startElement, ConfigXmlConstants.IS_PASSIVE_ATTR));
while (xmlEventReader.hasNext()) {
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
if (xmlEvent == null)
@@ -79,7 +84,7 @@ public class SPXmlParser extends AbstractParser {
String tag = StaxParserUtil.getStartElementName(startElement);
if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) {
KeysXmlParser parser = new KeysXmlParser();
- List<Key> keys = (List<Key>)parser.parse(xmlEventReader);
+ List<Key> keys = (List<Key>) parser.parse(xmlEventReader);
sp.setKeys(keys);
} else if (tag.equals(ConfigXmlConstants.PRINCIPAL_NAME_MAPPING_ELEMENT)) {
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
@@ -98,7 +103,7 @@ public class SPXmlParser extends AbstractParser {
parseRoleMapping(xmlEventReader, sp);
} else if (tag.equals(ConfigXmlConstants.IDP_ELEMENT)) {
IDPXmlParser parser = new IDPXmlParser();
- IDP idp = (IDP)parser.parse(xmlEventReader);
+ IDP idp = (IDP) parser.parse(xmlEventReader);
sp.setIdp(idp);
} else {
StaxParserUtil.bypassElementBlock(xmlEventReader, tag);
@@ -108,7 +113,7 @@ public class SPXmlParser extends AbstractParser {
return sp;
}
- protected void parseRoleMapping(XMLEventReader xmlEventReader, SP sp) throws ParsingException {
+ protected void parseRoleMapping(XMLEventReader xmlEventReader, SP sp) throws ParsingException {
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
StaxParserUtil.validate(startElement, ConfigXmlConstants.ROLE_IDENTIFIERS_ELEMENT);
Set<String> roleAttributes = new HashSet<>();
@@ -144,7 +149,6 @@ public class SPXmlParser extends AbstractParser {
sp.setRoleAttributes(roleAttributes);
}
-
@Override
public boolean supports(QName qname) {
return false;
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/SP.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/SP.java
index f37f930..5203b13 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/SP.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/config/SP.java
@@ -33,6 +33,7 @@ public class SP implements Serializable {
private String entityID;
private String sslPolicy;
private boolean forceAuthentication;
+ private boolean isPassive;
private String logoutPage;
private List<Key> keys;
private String nameIDPolicyFormat;
@@ -64,6 +65,14 @@ public class SP implements Serializable {
this.forceAuthentication = forceAuthentication;
}
+ public boolean isIsPassive() {
+ return isPassive;
+ }
+
+ public void setIsPassive(boolean isPassive) {
+ this.isPassive = isPassive;
+ }
+
public List<Key> getKeys() {
return keys;
}
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
index 26fad92..7aab095 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java
@@ -1,13 +1,13 @@
package org.keycloak.adapters.saml;
-import org.keycloak.common.enums.SslRequired;
-import org.keycloak.saml.SignatureAlgorithm;
-
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Set;
+import org.keycloak.common.enums.SslRequired;
+import org.keycloak.saml.SignatureAlgorithm;
+
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@@ -31,7 +31,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
return validateResponseSignature;
}
- @Override
+ @Override
public Binding getRequestBinding() {
return requestBinding;
}
@@ -97,7 +97,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
return signResponse;
}
- @Override
+ @Override
public Binding getRequestBinding() {
return requestBinding;
}
@@ -150,12 +150,8 @@ public class DefaultSamlDeployment implements SamlDeployment {
}
}
-
-
public static class DefaultIDP implements IDP {
-
-
private String entityID;
private PublicKey signatureValidationKey;
private SingleSignOnService singleSignOnService;
@@ -204,6 +200,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
private String entityID;
private String nameIDPolicyFormat;
private boolean forceAuthentication;
+ private boolean isPassive;
private PrivateKey decryptionKey;
private KeyPair signingKeyPair;
private String assertionConsumerServiceUrl;
@@ -214,7 +211,6 @@ public class DefaultSamlDeployment implements SamlDeployment {
private SignatureAlgorithm signatureAlgorithm;
private String signatureCanonicalizationMethod;
-
@Override
public IDP getIDP() {
return idp;
@@ -244,6 +240,11 @@ public class DefaultSamlDeployment implements SamlDeployment {
public boolean isForceAuthentication() {
return forceAuthentication;
}
+
+ @Override
+ public boolean isIsPassive() {
+ return isPassive;
+ }
@Override
public PrivateKey getDecryptionKey() {
@@ -265,7 +266,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
return roleAttributeNames;
}
- @Override
+ @Override
public PrincipalNamePolicy getPrincipalNamePolicy() {
return principalNamePolicy;
}
@@ -298,6 +299,10 @@ public class DefaultSamlDeployment implements SamlDeployment {
public void setForceAuthentication(boolean forceAuthentication) {
this.forceAuthentication = forceAuthentication;
}
+
+ public void setIsPassive(boolean isPassive){
+ this.isPassive = isPassive;
+ }
public void setDecryptionKey(PrivateKey decryptionKey) {
this.decryptionKey = decryptionKey;
@@ -332,7 +337,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
this.logoutPage = logoutPage;
}
- @Override
+ @Override
public String getSignatureCanonicalizationMethod() {
return signatureCanonicalizationMethod;
}
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java
index a34fb1b..4c7cbab 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java
@@ -27,11 +27,6 @@ public class InitiateLogin implements AuthChallenge {
}
@Override
- public boolean errorPage() {
- return false;
- }
-
- @Override
public int getResponseCode() {
return 0;
}
@@ -53,7 +48,7 @@ public class InitiateLogin implements AuthChallenge {
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
.destination(destinationUrl)
.issuer(issuerURL)
- .forceAuthn(deployment.isForceAuthentication())
+ .forceAuthn(deployment.isForceAuthentication()).isPassive(deployment.isIsPassive())
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
if (deployment.getIDP().getSingleSignOnService().getResponseBinding() != null) {
String protocolBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get();
@@ -87,6 +82,7 @@ public class InitiateLogin implements AuthChallenge {
Document document = authnRequestBuilder.toDocument();
SamlDeployment.Binding samlBinding = deployment.getIDP().getSingleSignOnService().getRequestBinding();
SamlUtil.sendSaml(true, httpFacade, actionUrl, binding, document, samlBinding);
+ sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_IN);
} catch (Exception e) {
throw new RuntimeException("Could not create authentication request.", e);
}
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java
new file mode 100755
index 0000000..c85fd63
--- /dev/null
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticationError.java
@@ -0,0 +1,49 @@
+package org.keycloak.adapters.saml;
+
+import org.keycloak.adapters.spi.AuthenticationError;
+import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
+import org.keycloak.dom.saml.v2.protocol.StatusType;
+
+/**
+ * Object that describes the SAML error that happened.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class SamlAuthenticationError implements AuthenticationError {
+ public static enum Reason {
+ EXTRACTION_FAILURE,
+ INVALID_SIGNATURE,
+ ERROR_STATUS
+ }
+
+ private Reason reason;
+
+ private StatusResponseType status;
+
+ public SamlAuthenticationError(Reason reason) {
+ this.reason = reason;
+ }
+
+ public SamlAuthenticationError(Reason reason, StatusResponseType status) {
+ this.reason = reason;
+ this.status = status;
+ }
+
+ public SamlAuthenticationError(StatusResponseType statusType) {
+ this.status = statusType;
+ }
+
+ public Reason getReason() {
+ return reason;
+ }
+ public StatusResponseType getStatus() {
+ return status;
+ }
+
+ @Override
+ public String toString() {
+ return "SamlAuthenticationError [reason=" + reason + ", status=" + status + "]";
+ }
+
+}
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java
index fb66a70..02097db 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlAuthenticator.java
@@ -1,10 +1,12 @@
package org.keycloak.adapters.saml;
import org.jboss.logging.Logger;
-import org.keycloak.common.VerificationException;
import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.common.VerificationException;
+import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
@@ -15,24 +17,27 @@ import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
+import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
+import org.keycloak.dom.saml.v2.protocol.StatusType;
import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SAML2LogoutRequestBuilder;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAMLRequestParser;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.Base64;
+import org.keycloak.saml.common.util.StringUtil;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
import org.keycloak.saml.processing.web.util.PostBindingUtil;
-import org.keycloak.common.util.KeycloakUriBuilder;
-import org.keycloak.common.util.MultivaluedHashMap;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
+import java.net.URI;
import java.security.PublicKey;
import java.security.Signature;
import java.util.HashSet;
@@ -72,7 +77,7 @@ public abstract class SamlAuthenticator {
return handleSamlRequest(samlRequest, relayState);
} else if (samlResponse != null) {
return handleSamlResponse(samlResponse, relayState);
- } else if (sessionStore.isLoggedIn()) {
+ } else if (sessionStore.isLoggedIn()) {
if (globalLogout) {
return globalLogout();
}
@@ -104,6 +109,7 @@ public abstract class SamlAuthenticator {
try {
SamlUtil.sendSaml(true, facade, deployment.getIDP().getSingleLogoutService().getRequestBindingUrl(), binding, logoutBuilder.buildDocument(), deployment.getIDP().getSingleLogoutService().getRequestBinding());
+ sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.LOGGING_OUT);
} catch (Exception e) {
log.error("Could not send global logout SAML request", e);
return AuthOutcome.FAILED;
@@ -153,7 +159,7 @@ public abstract class SamlAuthenticator {
protected AuthOutcome logoutRequest(LogoutRequestType request, String relayState) {
if (request.getSessionIndex() == null || request.getSessionIndex().isEmpty()) {
sessionStore.logoutByPrincipal(request.getNameID().getValue());
- } else {
+ } else {
sessionStore.logoutBySsoId(request.getSessionIndex());
}
@@ -167,7 +173,8 @@ public abstract class SamlAuthenticator {
binding.signatureAlgorithm(deployment.getSignatureAlgorithm())
.signWith(deployment.getSigningKeyPair())
.signDocument();
- if (deployment.getSignatureCanonicalizationMethod() != null) binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
+ if (deployment.getSignatureCanonicalizationMethod() != null)
+ binding.canonicalizationMethod(deployment.getSignatureCanonicalizationMethod());
}
@@ -197,34 +204,89 @@ public abstract class SamlAuthenticator {
postBinding = true;
holder = extractPostBindingResponse(samlResponse);
}
- StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
+ final StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
// validate destination
if (!requestUri.equals(statusResponse.getDestination())) {
log.error("Request URI does not match SAML request destination");
return AuthOutcome.FAILED;
}
- if (statusResponse instanceof ResponseType) {
- if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
- try {
- validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
- } catch (VerificationException e) {
- log.error("Failed to verify saml response signature", e);
- return AuthOutcome.FAILED;
+
+ if (statusResponse instanceof ResponseType) {
+ try {
+ if (deployment.getIDP().getSingleSignOnService().validateResponseSignature()) {
+ try {
+ validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
+ } catch (VerificationException e) {
+ log.error("Failed to verify saml response signature", e);
+
+ challenge = new AuthChallenge() {
+ @Override
+ public boolean challenge(HttpFacade exchange) {
+ SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.INVALID_SIGNATURE);
+ exchange.getRequest().setError(error);
+ exchange.getResponse().sendError(403);
+ return true;
+ }
+
+ @Override
+ public int getResponseCode() {
+ return 403;
+ }
+ };
+ return AuthOutcome.FAILED;
+ }
}
+ return handleLoginResponse((ResponseType) statusResponse);
+ } finally {
+ sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
}
- return handleLoginResponse((ResponseType)statusResponse);
} else {
- if (deployment.getIDP().getSingleLogoutService().validateResponseSignature()) {
+ if (sessionStore.isLoggingOut()) {
try {
- validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
- } catch (VerificationException e) {
- log.error("Failed to verify saml response signature", e);
+ if (deployment.getIDP().getSingleLogoutService().validateResponseSignature()) {
+ try {
+ validateSamlSignature(holder, postBinding, GeneralConstants.SAML_RESPONSE_KEY);
+ } catch (VerificationException e) {
+ log.error("Failed to verify saml response signature", e);
+ return AuthOutcome.FAILED;
+ }
+ }
+ return handleLogoutResponse(holder, statusResponse, relayState);
+ } finally {
+ sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
+ }
+
+ } else if (sessionStore.isLoggingIn()) {
+
+ try {
+ // KEYCLOAK-2107 - handle user not authenticated due passive mode. Return special outcome so different authentication mechanisms can behave accordingly.
+ StatusType status = statusResponse.getStatus();
+ if(checkStatusCodeValue(status.getStatusCode(), JBossSAMLURIConstants.STATUS_RESPONDER.get()) && checkStatusCodeValue(status.getStatusCode().getStatusCode(), JBossSAMLURIConstants.STATUS_NO_PASSIVE.get())){
+ log.debug("Not authenticated due passive mode Status found in SAML response: " + status.toString());
+ return AuthOutcome.NOT_AUTHENTICATED;
+ }
+
+ challenge = new AuthChallenge() {
+ @Override
+ public boolean challenge(HttpFacade exchange) {
+ SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.ERROR_STATUS, statusResponse);
+ exchange.getRequest().setError(error);
+ exchange.getResponse().sendError(403);
+ return true;
+ }
+
+ @Override
+ public int getResponseCode() {
+ return 403;
+ }
+ };
return AuthOutcome.FAILED;
+ } finally {
+ sessionStore.setCurrentAction(SamlSessionStore.CurrentAction.NONE);
}
}
- // todo need to check that it is actually a LogoutResponse
- return handleLogoutResponse(holder, statusResponse, relayState);
+ return AuthOutcome.NOT_ATTEMPTED;
}
}
@@ -237,7 +299,16 @@ public abstract class SamlAuthenticator {
}
}
- protected AuthOutcome handleLoginResponse(ResponseType responseType) {
+ private boolean checkStatusCodeValue(StatusCodeType statusCode, String expectedValue){
+ if(statusCode != null && statusCode.getValue()!=null){
+ String v = statusCode.getValue().toString();
+ return expectedValue.equals(v);
+ }
+ return false;
+ }
+
+ protected AuthOutcome handleLoginResponse(ResponseType responseType) {
+
AssertionType assertion = null;
try {
assertion = AssertionUtil.getAssertion(responseType, deployment.getDecryptionKey());
@@ -245,8 +316,21 @@ public abstract class SamlAuthenticator {
return initiateLogin();
}
} catch (Exception e) {
- log.error("Error extracting SAML assertion, e");
- return AuthOutcome.FAILED;
+ log.error("Error extracting SAML assertion: " + e.getMessage());
+ challenge = new AuthChallenge() {
+ @Override
+ public boolean challenge(HttpFacade exchange) {
+ SamlAuthenticationError error = new SamlAuthenticationError(SamlAuthenticationError.Reason.EXTRACTION_FAILURE);
+ exchange.getRequest().setError(error);
+ exchange.getResponse().sendError(403);
+ return true;
+ }
+
+ @Override
+ public int getResponseCode() {
+ return 403;
+ }
+ };
}
SubjectType subject = assertion.getSubject();
@@ -306,13 +390,15 @@ public abstract class SamlAuthenticator {
AuthnStatementType authn = null;
for (Object statement : assertion.getStatements()) {
if (statement instanceof AuthnStatementType) {
- authn = (AuthnStatementType)statement;
+ authn = (AuthnStatementType) statement;
break;
}
}
- final SamlPrincipal principal = new SamlPrincipal(principalName, principalName, subjectNameID.getFormat().toString(), attributes, friendlyAttributes);
+ URI nameFormat = subjectNameID.getFormat();
+ String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString();
+ final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes);
String index = authn == null ? null : authn.getSessionIndex();
final String sessionIndex = index;
SamlSession account = new SamlSession(principal, roles, sessionIndex);
@@ -337,9 +423,9 @@ public abstract class SamlAuthenticator {
protected abstract void completeAuthentication(SamlSession account);
private String getAttributeValue(Object attrValue) {
- String value = null;
+ String value = null;
if (attrValue instanceof String) {
- value = (String)attrValue;
+ value = (String) attrValue;
} else if (attrValue instanceof Node) {
Node roleNode = (Node) attrValue;
value = roleNode.getFirstChild().getNodeValue();
@@ -368,14 +454,14 @@ public abstract class SamlAuthenticator {
protected SAMLDocumentHolder extractRedirectBindingResponse(String response) {
return SAMLRequestParser.parseRequestRedirectBinding(response);
}
+
+
protected SAMLDocumentHolder extractPostBindingResponse(String response) {
byte[] samlBytes = PostBindingUtil.base64Decode(response);
- String xml = new String(samlBytes);
return SAMLRequestParser.parseResponseDocument(samlBytes);
}
-
protected AuthOutcome initiateLogin() {
challenge = new InitiateLogin(deployment, sessionStore);
return AuthOutcome.NOT_ATTEMPTED;
@@ -441,5 +527,4 @@ public abstract class SamlAuthenticator {
}
-
}
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
index 0728447..8c53236 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java
@@ -56,6 +56,7 @@ public interface SamlDeployment {
String getEntityID();
String getNameIDPolicyFormat();
boolean isForceAuthentication();
+ boolean isIsPassive();
PrivateKey getDecryptionKey();
KeyPair getSigningKeyPair();
String getSignatureCanonicalizationMethod();
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java
index ef1599f..04c1c2f 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java
@@ -1,6 +1,7 @@
package org.keycloak.adapters.saml;
import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.dom.saml.v2.assertion.AssertionType;
import java.io.Serializable;
import java.security.Principal;
@@ -18,22 +19,43 @@ public class SamlPrincipal implements Serializable, Principal {
private String name;
private String samlSubject;
private String nameIDFormat;
+ private AssertionType assertion;
- public SamlPrincipal(String name, String samlSubject, String nameIDFormat, MultivaluedHashMap<String, String> attributes, MultivaluedHashMap<String, String> friendlyAttributes) {
+ public SamlPrincipal(AssertionType assertion, String name, String samlSubject, String nameIDFormat, MultivaluedHashMap<String, String> attributes, MultivaluedHashMap<String, String> friendlyAttributes) {
this.name = name;
this.attributes = attributes;
this.friendlyAttributes = friendlyAttributes;
this.samlSubject = samlSubject;
this.nameIDFormat = nameIDFormat;
+ this.assertion = assertion;
}
public SamlPrincipal() {
}
+ /**
+ * Get full saml assertion
+ *
+ * @return
+ */
+ public AssertionType getAssertion() {
+ return assertion;
+ }
+
+ /**
+ * Get SAML subject sent in assertion
+ *
+ * @return
+ */
public String getSamlSubject() {
return samlSubject;
}
+ /**
+ * Subject nameID format
+ *
+ * @return
+ */
public String getNameIDFormat() {
return nameIDFormat;
}
@@ -43,7 +65,12 @@ public class SamlPrincipal implements Serializable, Principal {
return name;
}
-
+ /**
+ * Convenience function that gets Attribute value by attribute name
+ *
+ * @param name
+ * @return
+ */
public List<String> getAttributes(String name) {
List<String> list = attributes.get(name);
if (list != null) {
@@ -53,6 +80,13 @@ public class SamlPrincipal implements Serializable, Principal {
}
}
+
+ /**
+ * Convenience function that gets Attribute value by attribute friendly name
+ *
+ * @param friendlyName
+ * @return
+ */
public List<String> getFriendlyAttributes(String friendlyName) {
List<String> list = friendlyAttributes.get(name);
if (list != null) {
@@ -63,19 +97,42 @@ public class SamlPrincipal implements Serializable, Principal {
}
+ /**
+ * Convenience function that gets first value of an attribute by attribute name
+ *
+ * @param name
+ * @return
+ */
public String getAttribute(String name) {
return attributes.getFirst(name);
}
+ /**
+ * Convenience function that gets first value of an attribute by attribute name
+ *
+ *
+ * @param friendlyName
+ * @return
+ */
public String getFriendlyAttribute(String friendlyName) {
return friendlyAttributes.getFirst(friendlyName);
}
+ /**
+ * Get set of all assertion attribute names
+ *
+ * @return
+ */
public Set<String> getAttributeNames() {
return Collections.unmodifiableSet(attributes.keySet());
}
+ /**
+ * Get set of all assertion friendly attribute names
+ *
+ * @return
+ */
public Set<String> getFriendlyNames() {
return Collections.unmodifiableSet(friendlyAttributes.keySet());
diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlSessionStore.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlSessionStore.java
index da20026..1a19464 100755
--- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlSessionStore.java
+++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/SamlSessionStore.java
@@ -1,6 +1,8 @@
package org.keycloak.adapters.saml;
import org.keycloak.adapters.spi.AdapterSessionStore;
+import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
+import org.keycloak.dom.saml.v2.protocol.StatusType;
import java.util.List;
@@ -9,6 +11,19 @@ import java.util.List;
* @version $Revision: 1 $
*/
public interface SamlSessionStore extends AdapterSessionStore {
+ public static final String CURRENT_ACTION = "SAML_CURRENT_ACTION";
+ public static final String SAML_LOGIN_ERROR_STATUS = "SAML_LOGIN_ERROR_STATUS";
+ public static final String SAML_LOGOUT_ERROR_STATUS = "SAML_LOGOUT_ERROR_STATUS";
+
+ enum CurrentAction {
+ NONE,
+ LOGGING_IN,
+ LOGGING_OUT
+ }
+ void setCurrentAction(CurrentAction action);
+ boolean isLoggingIn();
+ boolean isLoggingOut();
+
boolean isLoggedIn();
SamlSession getAccount();
void saveAccount(SamlSession account);
diff --git a/saml/client-adapter/core/src/main/resources/schema/keycloak_saml_adapter_1_6.xsd b/saml/client-adapter/core/src/main/resources/schema/keycloak_saml_adapter_1_6.xsd
index 534c9ae..d3e55f9 100755
--- a/saml/client-adapter/core/src/main/resources/schema/keycloak_saml_adapter_1_6.xsd
+++ b/saml/client-adapter/core/src/main/resources/schema/keycloak_saml_adapter_1_6.xsd
@@ -33,6 +33,7 @@
<xs:attribute name="nameIDPolicyFormat" type="xs:string" use="optional"/>
<xs:attribute name="logoutPage" type="xs:string" use="optional"/>
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional"/>
+ <xs:attribute name="isPassive" type="xs:boolean" use="optional"/>
</xs:complexType>
<xs:complexType name="keys-type">
diff --git a/saml/client-adapter/core/src/test/java/org/keycloak/test/adapters/saml/XmlParserTest.java b/saml/client-adapter/core/src/test/java/org/keycloak/test/adapters/saml/XmlParserTest.java
index c92fec6..5a6e037 100755
--- a/saml/client-adapter/core/src/test/java/org/keycloak/test/adapters/saml/XmlParserTest.java
+++ b/saml/client-adapter/core/src/test/java/org/keycloak/test/adapters/saml/XmlParserTest.java
@@ -68,6 +68,7 @@ public class XmlParserTest {
Assert.assertEquals("ssl", sp.getSslPolicy());
Assert.assertEquals("format", sp.getNameIDPolicyFormat());
Assert.assertTrue(sp.isForceAuthentication());
+ Assert.assertTrue(sp.isIsPassive());
Assert.assertEquals(2, sp.getKeys().size());
Key signing = sp.getKeys().get(0);
Assert.assertTrue(signing.isSigning());
diff --git a/saml/client-adapter/core/src/test/resources/keycloak-saml.xml b/saml/client-adapter/core/src/test/resources/keycloak-saml.xml
index ef910dc..ae8c96d 100755
--- a/saml/client-adapter/core/src/test/resources/keycloak-saml.xml
+++ b/saml/client-adapter/core/src/test/resources/keycloak-saml.xml
@@ -2,7 +2,8 @@
<SP entityID="sp"
sslPolicy="ssl"
nameIDPolicyFormat="format"
- forceAuthentication="true">
+ forceAuthentication="true"
+ isPassive="true">
<Keys>
<Key signing="true" >
<KeyStore file="file" resource="cp" password="pw">
diff --git a/saml/client-adapter/core/src/test/resources/keycloak-saml2.xml b/saml/client-adapter/core/src/test/resources/keycloak-saml2.xml
index ee6388d..7662e61 100755
--- a/saml/client-adapter/core/src/test/resources/keycloak-saml2.xml
+++ b/saml/client-adapter/core/src/test/resources/keycloak-saml2.xml
@@ -4,7 +4,8 @@
nameIDPolicyFormat="format"
signatureAlgorithm=""
signatureCanonicalizationMethod=""
- forceAuthentication="true">
+ forceAuthentication="true"
+ isPassive="true">
<Keys>
<Key signing="true" >
<KeyStore file="file" resource="cp" password="pw">
diff --git a/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java b/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java
index 60cea89..2df0fad 100755
--- a/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java
+++ b/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java
@@ -260,15 +260,6 @@ public abstract class AbstractSamlAuthenticator extends LoginAuthenticator {
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
challenge.challenge(facade);
- if (challenge.errorPage() && errorPage != null) {
- Response response = (Response)res;
- try {
- response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), errorPage)));
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
-
- }
}
return Authentication.SEND_CONTINUE;
}
diff --git a/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/JettySamlSessionStore.java b/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/JettySamlSessionStore.java
index d86184b..6e51f11 100755
--- a/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/JettySamlSessionStore.java
+++ b/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/JettySamlSessionStore.java
@@ -8,6 +8,7 @@ import org.keycloak.adapters.spi.SessionIdMapper;
import org.keycloak.adapters.jetty.spi.JettyUserSessionManagement;
import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.SamlSessionStore;
+import org.keycloak.dom.saml.v2.protocol.StatusType;
import javax.servlet.http.HttpSession;
@@ -38,6 +39,28 @@ public class JettySamlSessionStore implements SamlSessionStore {
}
@Override
+ public void setCurrentAction(CurrentAction action) {
+ if (action == CurrentAction.NONE && request.getSession(false) == null) return;
+ request.getSession().setAttribute(CURRENT_ACTION, action);
+ }
+
+ @Override
+ public boolean isLoggingIn() {
+ HttpSession session = request.getSession(false);
+ if (session == null) return false;
+ CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION);
+ return action == CurrentAction.LOGGING_IN;
+ }
+
+ @Override
+ public boolean isLoggingOut() {
+ HttpSession session = request.getSession(false);
+ if (session == null) return false;
+ CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION);
+ return action == CurrentAction.LOGGING_OUT;
+ }
+
+ @Override
public void logoutAccount() {
HttpSession session = request.getSession(false);
if (session != null) {
diff --git a/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java
index cbd036e..e690db5 100755
--- a/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java
+++ b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java
@@ -7,6 +7,7 @@ import org.keycloak.adapters.spi.SessionIdMapper;
import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.SamlSessionStore;
import org.keycloak.adapters.servlet.FilterSessionStore;
+import org.keycloak.dom.saml.v2.protocol.StatusType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
@@ -30,6 +31,28 @@ public class FilterSamlSessionStore extends FilterSessionStore implements SamlSe
}
@Override
+ public void setCurrentAction(CurrentAction action) {
+ if (action == CurrentAction.NONE && request.getSession(false) == null) return;
+ request.getSession().setAttribute(CURRENT_ACTION, action);
+ }
+
+ @Override
+ public boolean isLoggingIn() {
+ HttpSession session = request.getSession(false);
+ if (session == null) return false;
+ CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION);
+ return action == CurrentAction.LOGGING_IN;
+ }
+
+ @Override
+ public boolean isLoggingOut() {
+ HttpSession session = request.getSession(false);
+ if (session == null) return false;
+ CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION);
+ return action == CurrentAction.LOGGING_OUT;
+ }
+
+ @Override
public void logoutAccount() {
HttpSession session = request.getSession(false);
if (session == null) return;
diff --git a/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java
index bce4dbc..ac95784 100755
--- a/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java
+++ b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java
@@ -1,18 +1,11 @@
package org.keycloak.adapters.saml.servlet;
-import org.keycloak.adapters.spi.AuthChallenge;
-import org.keycloak.adapters.spi.AuthOutcome;
-import org.keycloak.adapters.spi.InMemorySessionIdMapper;
-import org.keycloak.adapters.spi.SessionIdMapper;
-import org.keycloak.adapters.saml.DefaultSamlDeployment;
-import org.keycloak.adapters.saml.SamlAuthenticator;
-import org.keycloak.adapters.saml.SamlDeployment;
-import org.keycloak.adapters.saml.SamlDeploymentContext;
-import org.keycloak.adapters.saml.SamlSession;
-import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
-import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
-import org.keycloak.adapters.servlet.ServletHttpFacade;
-import org.keycloak.saml.common.exceptions.ParsingException;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
@@ -24,12 +17,20 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+
+import org.keycloak.adapters.saml.DefaultSamlDeployment;
+import org.keycloak.adapters.saml.SamlAuthenticator;
+import org.keycloak.adapters.saml.SamlDeployment;
+import org.keycloak.adapters.saml.SamlDeploymentContext;
+import org.keycloak.adapters.saml.SamlSession;
+import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder;
+import org.keycloak.adapters.saml.config.parsers.ResourceLoader;
+import org.keycloak.adapters.servlet.ServletHttpFacade;
+import org.keycloak.adapters.spi.AuthChallenge;
+import org.keycloak.adapters.spi.AuthOutcome;
+import org.keycloak.adapters.spi.InMemorySessionIdMapper;
+import org.keycloak.adapters.spi.SessionIdMapper;
+import org.keycloak.saml.common.exceptions.ParsingException;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -38,7 +39,7 @@ import java.util.logging.Logger;
public class SamlFilter implements Filter {
protected SamlDeploymentContext deploymentContext;
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
- private final static Logger log = Logger.getLogger(""+SamlFilter.class);
+ private final static Logger log = Logger.getLogger("" + SamlFilter.class);
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
@@ -46,12 +47,14 @@ public class SamlFilter implements Filter {
if (configResolverClass != null) {
try {
throw new RuntimeException("Not implemented yet");
- //KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
- //deploymentContext = new SamlDeploymentContext(configResolver);
- //log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
+ // KeycloakConfigResolver configResolver = (KeycloakConfigResolver)
+ // context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
+ // deploymentContext = new SamlDeploymentContext(configResolver);
+ // log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.",
+ // configResolverClass);
} catch (Exception ex) {
- log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
- //deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
+ log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[] { configResolverClass, ex.getMessage() });
+ // deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
}
} else {
String fp = filterConfig.getInitParameter("keycloak.config.file");
@@ -65,7 +68,8 @@ public class SamlFilter implements Filter {
} else {
String path = "/WEB-INF/keycloak-saml.xml";
String pathParam = filterConfig.getInitParameter("keycloak.config.path");
- if (pathParam != null) path = pathParam;
+ if (pathParam != null)
+ path = pathParam;
is = filterConfig.getServletContext().getResourceAsStream(path);
}
final SamlDeployment deployment;
@@ -105,7 +109,6 @@ public class SamlFilter implements Filter {
}
FilterSamlSessionStore tokenStore = new FilterSamlSessionStore(request, facade, 100000, idMapper);
-
SamlAuthenticator authenticator = new SamlAuthenticator(facade, deployment, tokenStore) {
@Override
protected void completeAuthentication(SamlSession account) {
@@ -137,13 +140,18 @@ public class SamlFilter implements Filter {
if (challenge != null) {
log.fine("challenge");
challenge.challenge(facade);
- if (challenge.errorPage()) {
- response.sendError(challenge.getResponseCode());
+ return;
+ }
+
+ if (deployment.isIsPassive() && outcome == AuthOutcome.NOT_AUTHENTICATED) {
+ log.fine("PASSIVE_NOT_AUTHENTICATED");
+ if (facade.isEnded()) {
return;
}
- log.fine("sending challenge");
+ chain.doFilter(req, res);
return;
}
+
if (!facade.isEnded()) {
response.sendError(403);
}
diff --git a/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java b/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java
index d231265..3119ba7 100755
--- a/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java
+++ b/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java
@@ -213,10 +213,6 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
loginConfig = request.getContext().getLoginConfig();
}
challenge.challenge(facade);
- if (challenge.errorPage()) {
- log.fine("error page");
- if (forwardToErrorPageInternal(request, response, loginConfig))return false;
- }
}
return false;
}
diff --git a/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java b/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java
index adbc441..804acf3 100755
--- a/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java
+++ b/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/CatalinaSamlSessionStore.java
@@ -9,6 +9,8 @@ import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.SessionIdMapper;
import org.keycloak.adapters.tomcat.CatalinaUserSessionManagement;
import org.keycloak.adapters.tomcat.GenericPrincipalFactory;
+import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
+import org.keycloak.dom.saml.v2.protocol.StatusType;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@@ -42,6 +44,28 @@ public class CatalinaSamlSessionStore implements SamlSessionStore {
}
@Override
+ public void setCurrentAction(CurrentAction action) {
+ if (action == CurrentAction.NONE && request.getSession(false) == null) return;
+ request.getSession().setAttribute(CURRENT_ACTION, action);
+ }
+
+ @Override
+ public boolean isLoggingIn() {
+ HttpSession session = request.getSession(false);
+ if (session == null) return false;
+ CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION);
+ return action == CurrentAction.LOGGING_IN;
+ }
+
+ @Override
+ public boolean isLoggingOut() {
+ HttpSession session = request.getSession(false);
+ if (session == null) return false;
+ CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION);
+ return action == CurrentAction.LOGGING_OUT;
+ }
+
+ @Override
public void logoutAccount() {
Session sessionInternal = request.getSessionInternal(false);
if (sessionInternal == null) return;
diff --git a/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java b/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java
index 721f0c0..3d632dd 100755
--- a/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java
+++ b/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/AbstractSamlAuthMech.java
@@ -16,6 +16,15 @@
*/
package org.keycloak.adapters.saml.undertow;
+import org.keycloak.adapters.saml.SamlDeployment;
+import org.keycloak.adapters.saml.SamlDeploymentContext;
+import org.keycloak.adapters.saml.SamlSessionStore;
+import org.keycloak.adapters.spi.AuthChallenge;
+import org.keycloak.adapters.spi.AuthOutcome;
+import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.adapters.undertow.UndertowHttpFacade;
+import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
+
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.NotificationReceiver;
import io.undertow.security.api.SecurityContext;
@@ -24,14 +33,6 @@ import io.undertow.server.HttpServerExchange;
import io.undertow.util.AttachmentKey;
import io.undertow.util.Headers;
import io.undertow.util.StatusCodes;
-import org.keycloak.adapters.spi.AuthChallenge;
-import org.keycloak.adapters.spi.AuthOutcome;
-import org.keycloak.adapters.spi.HttpFacade;
-import org.keycloak.adapters.saml.SamlDeployment;
-import org.keycloak.adapters.saml.SamlDeploymentContext;
-import org.keycloak.adapters.saml.SamlSessionStore;
-import org.keycloak.adapters.undertow.UndertowHttpFacade;
-import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
/**
* Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism.
@@ -44,8 +45,7 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
protected UndertowUserSessionManagement sessionManagement;
protected String errorPage;
- public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement,
- String errorPage) {
+ public AbstractSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) {
this.deploymentContext = deploymentContext;
this.sessionManagement = sessionManagement;
this.errorPage = errorPage;
@@ -55,10 +55,6 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
AuthChallenge challenge = exchange.getAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY);
if (challenge != null) {
- if (challenge.errorPage() && errorPage != null) {
- Integer code = servePage(exchange, errorPage);
- return new ChallengeResult(true, code);
- }
UndertowHttpFacade facade = createFacade(exchange);
if (challenge.challenge(facade)) {
return new ChallengeResult(true, exchange.getResponseCode());
@@ -73,19 +69,19 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
}
static void sendRedirect(final HttpServerExchange exchange, final String location) {
- // TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better handle this.
+ // TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better
+ // handle this.
String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location;
exchange.getResponseHeaders().put(Headers.LOCATION, loc);
}
-
-
protected void registerNotifications(final SecurityContext securityContext) {
final NotificationReceiver logoutReceiver = new NotificationReceiver() {
@Override
public void handleNotification(SecurityNotification notification) {
- if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return;
+ if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT)
+ return;
HttpServerExchange exchange = notification.getExchange();
UndertowHttpFacade facade = createFacade(exchange);
@@ -108,13 +104,16 @@ public abstract class AbstractSamlAuthMech implements AuthenticationMechanism {
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
SamlSessionStore sessionStore = getTokenStore(exchange, facade, deployment, securityContext);
- UndertowSamlAuthenticator authenticator = new UndertowSamlAuthenticator(securityContext, facade,
- deploymentContext.resolveDeployment(facade), sessionStore);
+ UndertowSamlAuthenticator authenticator = new UndertowSamlAuthenticator(securityContext, facade, deploymentContext.resolveDeployment(facade), sessionStore);
AuthOutcome outcome = authenticator.authenticate();
if (outcome == AuthOutcome.AUTHENTICATED) {
registerNotifications(securityContext);
return AuthenticationMechanismOutcome.AUTHENTICATED;
}
+ if (outcome == AuthOutcome.NOT_AUTHENTICATED) {
+ // we are in passive mode and user is not authenticated, let app server to try another auth mechanism
+ return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
+ }
if (outcome == AuthOutcome.LOGGED_OUT) {
securityContext.logout();
if (deployment.getLogoutPage() != null) {
diff --git a/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java b/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java
index 615e4a6..ad22718 100755
--- a/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java
+++ b/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/SamlServletExtension.java
@@ -28,6 +28,7 @@ import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.LoginConfig;
import io.undertow.servlet.api.ServletSessionConfig;
import org.jboss.logging.Logger;
+import org.keycloak.adapters.saml.AdapterConstants;
import org.keycloak.adapters.saml.DefaultSamlDeployment;
import org.keycloak.adapters.saml.SamlConfigResolver;
import org.keycloak.adapters.saml.SamlDeployment;
@@ -38,6 +39,7 @@ import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
import org.keycloak.saml.common.exceptions.ParsingException;
import javax.servlet.ServletContext;
+import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
@@ -64,8 +66,16 @@ public class SamlServletExtension implements ServletExtension {
return false;
}
+ private static InputStream getXMLFromServletContext(ServletContext servletContext) {
+ String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME);
+ if (json == null) {
+ return null;
+ }
+ return new ByteArrayInputStream(json.getBytes());
+ }
+
private static InputStream getConfigInputStream(ServletContext context) {
- InputStream is = null;
+ InputStream is = getXMLFromServletContext(context);
if (is == null) {
String path = context.getInitParameter("keycloak.config.file");
if (path == null) {
diff --git a/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java b/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java
index 8afcc1f..34d718b 100755
--- a/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java
+++ b/saml/client-adapter/undertow/src/main/java/org/keycloak/adapters/saml/undertow/ServletSamlSessionStore.java
@@ -13,9 +13,12 @@ import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.SamlSessionStore;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.dom.saml.v2.protocol.StatusType;
import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import java.io.IOException;
import java.security.Principal;
import java.util.LinkedList;
import java.util.List;
@@ -34,6 +37,7 @@ public class ServletSamlSessionStore implements SamlSessionStore {
private final SecurityContext securityContext;
private final SessionIdMapper idMapper;
+
public ServletSamlSessionStore(HttpServerExchange exchange, UndertowUserSessionManagement sessionManagement,
SecurityContext securityContext,
SessionIdMapper idMapper) {
@@ -44,6 +48,28 @@ public class ServletSamlSessionStore implements SamlSessionStore {
}
@Override
+ public void setCurrentAction(CurrentAction action) {
+ if (action == CurrentAction.NONE && getRequest().getSession(false) == null) return;
+ getRequest().getSession().setAttribute(CURRENT_ACTION, action);
+ }
+
+ @Override
+ public boolean isLoggingIn() {
+ HttpSession session = getRequest().getSession(false);
+ if (session == null) return false;
+ CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION);
+ return action == CurrentAction.LOGGING_IN;
+ }
+
+ @Override
+ public boolean isLoggingOut() {
+ HttpSession session = getRequest().getSession(false);
+ if (session == null) return false;
+ CurrentAction action = (CurrentAction)session.getAttribute(CURRENT_ACTION);
+ return action == CurrentAction.LOGGING_OUT;
+ }
+
+ @Override
public void logoutAccount() {
HttpSession session = getSession(false);
if (session != null) {
@@ -170,8 +196,18 @@ public class ServletSamlSessionStore implements SamlSessionStore {
}
protected HttpSession getSession(boolean create) {
- final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
- HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
+ HttpServletRequest req = getRequest();
return req.getSession(create);
}
+
+ private HttpServletResponse getResponse() {
+ final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
+ return (HttpServletResponse)servletRequestContext.getServletResponse();
+
+ }
+
+ private HttpServletRequest getRequest() {
+ final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
+ return (HttpServletRequest) servletRequestContext.getServletRequest();
+ }
}
saml/client-adapter/wildfly/pom.xml 2(+1 -1)
diff --git a/saml/client-adapter/wildfly/pom.xml b/saml/client-adapter/wildfly/pom.xml
index 2b5eb25..d9014b6 100755
--- a/saml/client-adapter/wildfly/pom.xml
+++ b/saml/client-adapter/wildfly/pom.xml
@@ -15,6 +15,6 @@
<modules>
<module>wildfly-adapter</module>
- <module>wildfly9-subsystem</module>
+ <module>wildfly-subsystem</module>
</modules>
</project>
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Configuration.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Configuration.java
new file mode 100644
index 0000000..e364e2d
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Configuration.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.dmr.ModelNode;
+import org.jboss.dmr.Property;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class Configuration {
+
+ static Configuration INSTANCE = new Configuration();
+
+ private ModelNode config = new ModelNode();
+
+ private Configuration() {
+ }
+
+ void updateModel(ModelNode operation, ModelNode model) {
+ ModelNode node = config;
+ ModelNode addr = operation.get("address");
+ for (Property item : addr.asPropertyList()) {
+ node = getNodeForAddressElement(node, item);
+ }
+ node.set(model);
+ }
+
+ private ModelNode getNodeForAddressElement(ModelNode node, Property item) {
+ String key = item.getValue().asString();
+ ModelNode keymodel = node.get(item.getName());
+ return keymodel.get(key);
+ }
+
+ public ModelNode getSecureDeployment(String name) {
+ ModelNode secureDeployment = config.get("subsystem").get("keycloak-saml").get(Constants.Model.SECURE_DEPLOYMENT);
+ if (secureDeployment.hasDefined(name)) {
+ return secureDeployment.get(name);
+ }
+ return null;
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Constants.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Constants.java
new file mode 100644
index 0000000..9b89fb2
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Constants.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class Constants {
+
+ static class Model {
+ static final String SECURE_DEPLOYMENT = "secure-deployment";
+ static final String SERVICE_PROVIDER = "service-provider";
+
+ static final String SSL_POLICY = "ssl-policy";
+ static final String NAME_ID_POLICY_FORMAT = "name-id-policy-format";
+ static final String LOGOUT_PAGE = "logout-page";
+ static final String FORCE_AUTHENTICATION = "force-authentication";
+ static final String ROLE_ATTRIBUTES = "role-attributes";
+ static final String SIGNING = "signing";
+ static final String ENCRYPTION = "encryption";
+ static final String KEY = "key";
+ static final String RESOURCE = "resource";
+ static final String PASSWORD = "password";
+
+ static final String PRIVATE_KEY_ALIAS = "private-key-alias";
+ static final String PRIVATE_KEY_PASSWORD = "private-key-password";
+ static final String CERTIFICATE_ALIAS = "certificate-alias";
+ static final String KEY_STORE = "key-store";
+ static final String SIGN_REQUEST = "sign-request";
+ static final String VALIDATE_RESPONSE_SIGNATURE = "validate-response-signature";
+ static final String REQUEST_BINDING = "request-binding";
+ static final String BINDING_URL = "binding-url";
+ static final String VALIDATE_REQUEST_SIGNATURE = "validate-request-signature";
+ static final String SIGN_RESPONSE = "sign-response";
+ static final String RESPONSE_BINDING = "response-binding";
+ static final String POST_BINDING_URL = "post-binding-url";
+ static final String REDIRECT_BINDING_URL = "redirect-binding-url";
+ static final String SINGLE_SIGN_ON = "single-sign-on";
+ static final String SINGLE_LOGOUT = "single-logout";
+ static final String IDENTITY_PROVIDER = "identity-provider";
+ static final String PRINCIPAL_NAME_MAPPING_POLICY = "principal-name-mapping-policy";
+ static final String PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME = "principal-name-mapping-attribute-name";
+ static final String SIGNATURE_ALGORITHM = "signature-algorithm";
+ static final String SIGNATURE_CANONICALIZATION_METHOD = "signature-canonicalization-method";
+ static final String PRIVATE_KEY_PEM = "private-key-pem";
+ static final String PUBLIC_KEY_PEM = "public-key-pem";
+ static final String CERTIFICATE_PEM = "certificate-pem";
+ static final String TYPE = "type";
+ static final String ALIAS = "alias";
+ static final String FILE = "file";
+ static final String SIGNATURES_REQUIRED = "signatures-required";
+ }
+
+
+ static class XML {
+ static final String SECURE_DEPLOYMENT = "secure-deployment";
+ static final String SERVICE_PROVIDER = "SP";
+
+ static final String NAME = "name";
+ static final String ENTITY_ID = "entityID";
+ static final String SSL_POLICY = "sslPolicy";
+ static final String NAME_ID_POLICY_FORMAT = "nameIDPolicyFormat";
+ static final String LOGOUT_PAGE = "logoutPage";
+ static final String FORCE_AUTHENTICATION = "forceAuthentication";
+ static final String ROLE_IDENTIFIERS = "RoleIdentifiers";
+ static final String SIGNING = "signing";
+ static final String ENCRYPTION = "encryption";
+ static final String KEYS = "Keys";
+ static final String KEY = "Key";
+ static final String RESOURCE = "resource";
+ static final String PASSWORD = "password";
+ static final String KEY_STORE = "KeyStore";
+ static final String PRIVATE_KEY = "PrivateKey";
+ static final String CERTIFICATE = "Certificate";
+
+ static final String PRIVATE_KEY_ALIAS = "alias";
+ static final String PRIVATE_KEY_PASSWORD = "password";
+ static final String CERTIFICATE_ALIAS = "alias";
+ static final String SIGN_REQUEST = "signRequest";
+ static final String VALIDATE_RESPONSE_SIGNATURE = "validateResponseSignature";
+ static final String REQUEST_BINDING = "requestBinding";
+ static final String BINDING_URL = "bindingUrl";
+ static final String VALIDATE_REQUEST_SIGNATURE = "validateRequestSignature";
+ static final String SIGN_RESPONSE = "signResponse";
+ static final String RESPONSE_BINDING = "responseBinding";
+ static final String POST_BINDING_URL = "postBindingUrl";
+ static final String REDIRECT_BINDING_URL = "redirectBindingUrl";
+ static final String SINGLE_SIGN_ON = "SingleSignOnService";
+ static final String SINGLE_LOGOUT = "SingleLogoutService";
+ static final String IDENTITY_PROVIDER = "IDP";
+ static final String PRINCIPAL_NAME_MAPPING = "PrincipalNameMapping";
+ static final String PRINCIPAL_NAME_MAPPING_POLICY = "policy";
+ static final String PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME = "attribute";
+ static final String ATTRIBUTE = "Attribute";
+ static final String SIGNATURE_ALGORITHM = "signatureAlgorithm";
+ static final String SIGNATURE_CANONICALIZATION_METHOD = "signatureCanonicalizationMethod";
+ static final String PRIVATE_KEY_PEM = "PrivateKeyPem";
+ static final String PUBLIC_KEY_PEM = "PublicKeyPem";
+ static final String CERTIFICATE_PEM = "CertificatePem";
+ static final String TYPE = "type";
+ static final String ALIAS = "alias";
+ static final String FILE = "file";
+ static final String SIGNATURES_REQUIRED = "signaturesRequired";
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/IdentityProviderAddHandler.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/IdentityProviderAddHandler.java
new file mode 100644
index 0000000..7648ba3
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/IdentityProviderAddHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.AbstractAddStepHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.dmr.ModelNode;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+class IdentityProviderAddHandler extends AbstractAddStepHandler {
+
+ IdentityProviderAddHandler() {
+ super(IdentityProviderDefinition.ALL_ATTRIBUTES);
+ }
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
+ Configuration.INSTANCE.updateModel(operation, model);
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/IdentityProviderDefinition.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/IdentityProviderDefinition.java
new file mode 100644
index 0000000..1459f15
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/IdentityProviderDefinition.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.AttributeDefinition;
+import org.jboss.as.controller.ObjectTypeAttributeDefinition;
+import org.jboss.as.controller.OperationStepHandler;
+import org.jboss.as.controller.PathElement;
+import org.jboss.as.controller.ReloadRequiredRemoveStepHandler;
+import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler;
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.as.controller.SimpleResourceDefinition;
+import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
+import org.jboss.as.controller.registry.ManagementResourceRegistration;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class IdentityProviderDefinition extends SimpleResourceDefinition {
+
+ static final SimpleAttributeDefinition SIGNATURES_REQUIRED =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGNATURES_REQUIRED, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.SIGNATURES_REQUIRED)
+ .build();
+
+ static final SimpleAttributeDefinition SIGNATURE_ALGORITHM =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGNATURE_ALGORITHM, ModelType.STRING, true)
+ .setXmlName(Constants.XML.SIGNATURE_ALGORITHM)
+ .build();
+
+ static final SimpleAttributeDefinition SIGNATURE_CANONICALIZATION_METHOD =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGNATURE_CANONICALIZATION_METHOD, ModelType.STRING, true)
+ .setXmlName(Constants.XML.SIGNATURE_CANONICALIZATION_METHOD)
+ .build();
+
+ static final ObjectTypeAttributeDefinition SINGLE_SIGN_ON =
+ ObjectTypeAttributeDefinition.Builder.of(Constants.Model.SINGLE_SIGN_ON,
+ SingleSignOnDefinition.ATTRIBUTES)
+ .setAllowNull(false)
+ .build();
+
+ static final ObjectTypeAttributeDefinition SINGLE_LOGOUT =
+ ObjectTypeAttributeDefinition.Builder.of(Constants.Model.SINGLE_LOGOUT,
+ SingleLogoutDefinition.ATTRIBUTES)
+ .setAllowNull(false)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {SIGNATURES_REQUIRED, SIGNATURE_ALGORITHM, SIGNATURE_CANONICALIZATION_METHOD};
+
+ static final SimpleAttributeDefinition[] ALL_ATTRIBUTES = {SIGNATURES_REQUIRED, SIGNATURE_ALGORITHM, SIGNATURE_CANONICALIZATION_METHOD, SINGLE_SIGN_ON, SINGLE_LOGOUT};
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ALL_ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static final IdentityProviderDefinition INSTANCE = new IdentityProviderDefinition();
+
+ private IdentityProviderDefinition() {
+ super(PathElement.pathElement(Constants.Model.IDENTITY_PROVIDER),
+ KeycloakSamlExtension.getResourceDescriptionResolver(Constants.Model.IDENTITY_PROVIDER),
+ new IdentityProviderAddHandler(),
+ ReloadRequiredRemoveStepHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerOperations(ManagementResourceRegistration resourceRegistration) {
+ super.registerOperations(resourceRegistration);
+ resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
+ super.registerAttributes(resourceRegistration);
+
+ final OperationStepHandler writeHandler = new ReloadRequiredWriteAttributeHandler(ALL_ATTRIBUTES);
+ for (AttributeDefinition attribute : ALL_ATTRIBUTES) {
+ resourceRegistration.registerReadWriteAttribute(attribute, null, writeHandler);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
\ No newline at end of file
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyAddHandler.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyAddHandler.java
new file mode 100644
index 0000000..1799290
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyAddHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.AbstractAddStepHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.dmr.ModelNode;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+class KeyAddHandler extends AbstractAddStepHandler {
+
+ KeyAddHandler() {
+ super(KeyDefinition.ALL_ATTRIBUTES);
+ }
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
+ Configuration.INSTANCE.updateModel(operation, model);
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemParser.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemParser.java
new file mode 100755
index 0000000..8b20565
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemParser.java
@@ -0,0 +1,569 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.PathAddress;
+import org.jboss.as.controller.PathElement;
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
+import org.jboss.as.controller.operations.common.Util;
+import org.jboss.as.controller.parsing.ParseUtils;
+import org.jboss.as.controller.persistence.SubsystemMarshallingContext;
+import org.jboss.dmr.ModelNode;
+import org.jboss.dmr.Property;
+import org.jboss.staxmapper.XMLElementReader;
+import org.jboss.staxmapper.XMLElementWriter;
+import org.jboss.staxmapper.XMLExtendedStreamReader;
+import org.jboss.staxmapper.XMLExtendedStreamWriter;
+
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * The subsystem parser, which uses stax to read and write to and from xml
+ */
+class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<List<ModelNode>>, XMLElementWriter<SubsystemMarshallingContext> {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void readElement(final XMLExtendedStreamReader reader, final List<ModelNode> list) throws XMLStreamException {
+ // Require no attributes
+ ParseUtils.requireNoAttributes(reader);
+ ModelNode addKeycloakSub = Util.createAddOperation(PathAddress.pathAddress(KeycloakSamlExtension.PATH_SUBSYSTEM));
+ list.add(addKeycloakSub);
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ if (reader.getLocalName().equals(Constants.XML.SECURE_DEPLOYMENT)) {
+ readSecureDeployment(reader, list);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ }
+ }
+
+ // used for debugging
+ private int nextTag(XMLExtendedStreamReader reader) throws XMLStreamException {
+ return reader.nextTag();
+ }
+
+ void readSecureDeployment(XMLExtendedStreamReader reader, List<ModelNode> list) throws XMLStreamException {
+ String name = readRequiredAttribute(reader, Constants.XML.NAME);
+
+ PathAddress addr = PathAddress.pathAddress(
+ PathElement.pathElement(ModelDescriptionConstants.SUBSYSTEM, KeycloakSamlExtension.SUBSYSTEM_NAME),
+ PathElement.pathElement(Constants.Model.SECURE_DEPLOYMENT, name));
+ ModelNode addSecureDeployment = Util.createAddOperation(addr);
+ list.add(addSecureDeployment);
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+ if (tagName.equals(Constants.XML.SERVICE_PROVIDER)) {
+ readServiceProvider(reader, list, addr);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ }
+ }
+
+ void readServiceProvider(XMLExtendedStreamReader reader, List<ModelNode> list, PathAddress parentAddr) throws XMLStreamException {
+ String entityId = readRequiredAttribute(reader, Constants.XML.ENTITY_ID);
+
+ PathAddress addr = PathAddress.pathAddress(parentAddr,
+ PathElement.pathElement(Constants.Model.SERVICE_PROVIDER, entityId));
+ ModelNode addServiceProvider = Util.createAddOperation(addr);
+ list.add(addServiceProvider);
+
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ if (Constants.XML.ENTITY_ID.equals(name)) {
+ continue;
+ }
+
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = ServiceProviderDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addServiceProvider, reader);
+ }
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+
+ if (Constants.XML.KEYS.equals(tagName)) {
+ readKeys(list, reader, addr);
+ } else if (Constants.XML.PRINCIPAL_NAME_MAPPING.equals(tagName)) {
+ readPrincipalNameMapping(addServiceProvider, reader);
+ } else if (Constants.XML.ROLE_IDENTIFIERS.equals(tagName)) {
+ readRoleIdentifiers(addServiceProvider, reader);
+ } else if (Constants.XML.IDENTITY_PROVIDER.equals(tagName)) {
+ readIdentityProvider(list, reader, addr);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ }
+ }
+
+ void readIdentityProvider(List<ModelNode> list, XMLExtendedStreamReader reader, PathAddress parentAddr) throws XMLStreamException {
+ String entityId = readRequiredAttribute(reader, Constants.XML.ENTITY_ID);
+
+ PathAddress addr = PathAddress.pathAddress(parentAddr,
+ PathElement.pathElement(Constants.Model.IDENTITY_PROVIDER, entityId));
+ ModelNode addIdentityProvider = Util.createAddOperation(addr);
+ list.add(addIdentityProvider);
+
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ if (Constants.XML.ENTITY_ID.equals(name)
+ // don't break if encountering this noop attr from client-adapter/core keycloak_saml_adapter_1_6.xsd
+ || "encryption".equals(name)) {
+ continue;
+ }
+ SimpleAttributeDefinition attr = IdentityProviderDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addIdentityProvider, reader);
+ }
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+
+ if (Constants.XML.SINGLE_SIGN_ON.equals(tagName)) {
+ readSingleSignOn(addIdentityProvider, reader);
+ } else if (Constants.XML.SINGLE_LOGOUT.equals(tagName)) {
+ readSingleLogout(addIdentityProvider, reader);
+ } else if (Constants.XML.KEYS.equals(tagName)) {
+ readKeys(list, reader, addr);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ }
+ }
+
+ void readSingleSignOn(ModelNode addIdentityProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
+ ModelNode sso = addIdentityProvider.get(Constants.Model.SINGLE_SIGN_ON);
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = SingleSignOnDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, sso, reader);
+ }
+ ParseUtils.requireNoContent(reader);
+ }
+
+ void readSingleLogout(ModelNode addIdentityProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
+ ModelNode slo = addIdentityProvider.get(Constants.Model.SINGLE_LOGOUT);
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = SingleLogoutDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, slo, reader);
+ }
+ ParseUtils.requireNoContent(reader);
+ }
+
+ void readKeys(List<ModelNode> list, XMLExtendedStreamReader reader, PathAddress parentAddr) throws XMLStreamException {
+ ParseUtils.requireNoAttributes(reader);
+ List<ModelNode> keyList = new LinkedList<>();
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+ if (!Constants.XML.KEY.equals(tagName)) {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ readKey(keyList, reader, parentAddr);
+ }
+ list.addAll(keyList);
+ }
+
+ void readKey(List<ModelNode> list, XMLExtendedStreamReader reader, PathAddress parentAddr) throws XMLStreamException {
+ PathAddress addr = PathAddress.pathAddress(parentAddr,
+ PathElement.pathElement(Constants.Model.KEY, "key-" + list.size()));
+ ModelNode addKey = Util.createAddOperation(addr);
+ list.add(addKey);
+
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = KeyDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addKey, reader);
+ }
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+
+ if (Constants.XML.KEY_STORE.equals(tagName)) {
+ readKeyStore(addKey, reader);
+ } else if (Constants.XML.PRIVATE_KEY_PEM.equals(tagName)
+ || Constants.XML.PUBLIC_KEY_PEM.equals(tagName)
+ || Constants.XML.CERTIFICATE_PEM.equals(tagName)) {
+
+ readNoAttrElementContent(KeyDefinition.lookupElement(tagName), addKey, reader);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ }
+ }
+
+ void readNoAttrElementContent(SimpleAttributeDefinition attr, ModelNode model, XMLExtendedStreamReader reader) throws XMLStreamException {
+ ParseUtils.requireNoAttributes(reader);
+ String value = reader.getElementText();
+ attr.parseAndSetParameter(value, model, reader);
+ }
+
+ void readKeyStore(ModelNode addKey, XMLExtendedStreamReader reader) throws XMLStreamException {
+ ModelNode addKeyStore = addKey.get(Constants.Model.KEY_STORE);
+
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = KeyStoreDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addKeyStore, reader);
+ }
+
+ if (!addKeyStore.hasDefined(Constants.Model.FILE) && !addKeyStore.hasDefined(Constants.Model.RESOURCE)) {
+ throw new XMLStreamException("KeyStore element must have 'file' or 'resource' attribute set", reader.getLocation());
+ }
+ if (!addKeyStore.hasDefined(Constants.Model.PASSWORD)) {
+ throw ParseUtils.missingRequired(reader, Constants.XML.PASSWORD);
+ }
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+ if (Constants.XML.PRIVATE_KEY.equals(tagName)) {
+ readPrivateKey(reader, addKeyStore);
+ } else if (Constants.XML.CERTIFICATE.equals(tagName)) {
+ readCertificate(reader, addKeyStore);
+ } else {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+ }
+ }
+
+
+ void readPrivateKey(XMLExtendedStreamReader reader, ModelNode addKeyStore) throws XMLStreamException {
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = KeyStorePrivateKeyDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addKeyStore, reader);
+ }
+
+ if (!addKeyStore.hasDefined(Constants.Model.PRIVATE_KEY_ALIAS)) {
+ throw ParseUtils.missingRequired(reader, Constants.XML.PRIVATE_KEY_ALIAS);
+ }
+ if (!addKeyStore.hasDefined(Constants.Model.PRIVATE_KEY_PASSWORD)) {
+ throw ParseUtils.missingRequired(reader, Constants.XML.PRIVATE_KEY_PASSWORD);
+ }
+
+ ParseUtils.requireNoContent(reader);
+ }
+
+ void readCertificate(XMLExtendedStreamReader reader, ModelNode addKeyStore) throws XMLStreamException {
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ SimpleAttributeDefinition attr = KeyStoreCertificateDefinition.lookup(name);
+ if (attr == null) {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ attr.parseAndSetParameter(value, addKeyStore, reader);
+ }
+
+ if (!addKeyStore.hasDefined(Constants.Model.CERTIFICATE_ALIAS)) {
+ throw ParseUtils.missingRequired(reader, Constants.XML.CERTIFICATE_ALIAS);
+ }
+
+ ParseUtils.requireNoContent(reader);
+ }
+
+ void readRoleIdentifiers(ModelNode addServiceProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
+ ParseUtils.requireNoAttributes(reader);
+
+ while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
+ String tagName = reader.getLocalName();
+
+ if (!Constants.XML.ATTRIBUTE.equals(tagName)) {
+ throw ParseUtils.unexpectedElement(reader);
+ }
+
+ ParseUtils.requireSingleAttribute(reader, Constants.XML.NAME);
+ String name = ParseUtils.readStringAttributeElement(reader, Constants.XML.NAME);
+
+ ServiceProviderDefinition.ROLE_ATTRIBUTES.parseAndAddParameterElement(name, addServiceProvider, reader);
+ }
+ }
+
+ void readPrincipalNameMapping(ModelNode addServiceProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
+
+ boolean policySet = false;
+
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String name = reader.getAttributeLocalName(i);
+ String value = reader.getAttributeValue(i);
+
+ if (Constants.XML.PRINCIPAL_NAME_MAPPING_POLICY.equals(name)) {
+ policySet = true;
+ ServiceProviderDefinition.PRINCIPAL_NAME_MAPPING_POLICY.parseAndSetParameter(value, addServiceProvider, reader);
+ } else if (Constants.XML.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME.equals(name)) {
+ ServiceProviderDefinition.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME.parseAndSetParameter(value, addServiceProvider, reader);
+ } else {
+ throw ParseUtils.unexpectedAttribute(reader, i);
+ }
+ }
+
+ if (!policySet) {
+ throw ParseUtils.missingRequired(reader, Constants.XML.PRINCIPAL_NAME_MAPPING_POLICY);
+ }
+ ParseUtils.requireNoContent(reader);
+ }
+
+ /**
+ * Read an attribute, and throw exception if attribute is not present
+ */
+ String readRequiredAttribute(XMLExtendedStreamReader reader, String attrName) throws XMLStreamException {
+ String value = null;
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ String attr = reader.getAttributeLocalName(i);
+ if (attr.equals(attrName)) {
+ value = reader.getAttributeValue(i);
+ break;
+ }
+ }
+ if (value == null) {
+ throw ParseUtils.missingRequired(reader, Collections.singleton(attrName));
+ }
+ return value;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void writeContent(final XMLExtendedStreamWriter writer, final SubsystemMarshallingContext context) throws XMLStreamException {
+ context.startSubsystemElement(KeycloakSamlExtension.NAMESPACE, false);
+ writeSecureDeployment(writer, context.getModelNode());
+ writer.writeEndElement();
+ }
+
+ public void writeSecureDeployment(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.get(Constants.Model.SECURE_DEPLOYMENT).isDefined()) {
+ return;
+ }
+
+ for (Property sp : model.get(Constants.Model.SECURE_DEPLOYMENT).asPropertyList()) {
+ writer.writeStartElement(Constants.XML.SECURE_DEPLOYMENT);
+ writer.writeAttribute(Constants.XML.NAME, sp.getName());
+
+ writeSps(writer, sp.getValue());
+ writer.writeEndElement();
+ }
+ }
+
+ void writeSps(final XMLExtendedStreamWriter writer, final ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+ for (Property sp : model.get(Constants.Model.SERVICE_PROVIDER).asPropertyList()) {
+ writer.writeStartElement(Constants.XML.SERVICE_PROVIDER);
+ writer.writeAttribute(Constants.XML.ENTITY_ID, sp.getName());
+ ModelNode spAttributes = sp.getValue();
+ for (SimpleAttributeDefinition attr : ServiceProviderDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, spAttributes, false, writer);
+ }
+ writeKeys(writer, spAttributes.get(Constants.Model.KEY));
+ writePrincipalNameMapping(writer, spAttributes);
+ writeRoleIdentifiers(writer, spAttributes);
+ writeIdentityProvider(writer, spAttributes.get(Constants.Model.IDENTITY_PROVIDER));
+
+ writer.writeEndElement();
+ }
+ }
+
+ void writeIdentityProvider(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+
+ for (Property idp : model.asPropertyList()) {
+ writer.writeStartElement(Constants.XML.IDENTITY_PROVIDER);
+ writer.writeAttribute(Constants.XML.ENTITY_ID, idp.getName());
+
+ ModelNode idpAttributes = idp.getValue();
+ for (SimpleAttributeDefinition attr : IdentityProviderDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, idpAttributes, false, writer);
+ }
+
+ writeSingleSignOn(writer, idpAttributes.get(Constants.Model.SINGLE_SIGN_ON));
+ writeSingleLogout(writer, idpAttributes.get(Constants.Model.SINGLE_LOGOUT));
+ writeKeys(writer, idpAttributes.get(Constants.Model.KEY));
+ }
+ writer.writeEndElement();
+ }
+
+ void writeSingleSignOn(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+ writer.writeStartElement(Constants.XML.SINGLE_SIGN_ON);
+ for (SimpleAttributeDefinition attr : SingleSignOnDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, model, false, writer);
+ }
+ writer.writeEndElement();
+ }
+
+ void writeSingleLogout(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+ writer.writeStartElement(Constants.XML.SINGLE_LOGOUT);
+ for (SimpleAttributeDefinition attr : SingleLogoutDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, model, false, writer);
+ }
+ writer.writeEndElement();
+ }
+
+ void writeKeys(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+ boolean contains = false;
+ for (Property key : model.asPropertyList()) {
+ if (!contains) {
+ writer.writeStartElement(Constants.XML.KEYS);
+ contains = true;
+ }
+ writer.writeStartElement(Constants.XML.KEY);
+
+ ModelNode keyAttributes = key.getValue();
+ for (SimpleAttributeDefinition attr : KeyDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, keyAttributes, false, writer);
+ }
+ for (SimpleAttributeDefinition attr : KeyDefinition.ELEMENTS) {
+ attr.getAttributeMarshaller().marshallAsElement(attr, keyAttributes, false, writer);
+ }
+ writeKeyStore(writer, keyAttributes.get(Constants.Model.KEY_STORE));
+
+ writer.writeEndElement();
+ }
+ if (contains) {
+ writer.writeEndElement();
+ }
+ }
+
+ void writeKeyStore(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ if (!model.isDefined()) {
+ return;
+ }
+ writer.writeStartElement(Constants.XML.KEY_STORE);
+ for (SimpleAttributeDefinition attr : KeyStoreDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, model, false, writer);
+ }
+ writePrivateKey(writer, model);
+ writeCertificate(writer, model);
+ writer.writeEndElement();
+ }
+
+ void writeCertificate(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ ModelNode value = model.get(Constants.Model.CERTIFICATE_ALIAS);
+ if (!value.isDefined()) {
+ return;
+ }
+ writer.writeStartElement(Constants.XML.CERTIFICATE);
+ SimpleAttributeDefinition attr = KeyStoreCertificateDefinition.CERTIFICATE_ALIAS;
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, model, false, writer);
+ writer.writeEndElement();
+ }
+
+ void writePrivateKey(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ ModelNode pk_alias = model.get(Constants.Model.PRIVATE_KEY_ALIAS);
+ ModelNode pk_password = model.get(Constants.Model.PRIVATE_KEY_PASSWORD);
+
+ if (!pk_alias.isDefined() && !pk_password.isDefined()) {
+ return;
+ }
+ writer.writeStartElement(Constants.XML.PRIVATE_KEY);
+ for (SimpleAttributeDefinition attr : KeyStorePrivateKeyDefinition.ATTRIBUTES) {
+ attr.getAttributeMarshaller().marshallAsAttribute(attr, model, false, writer);
+ }
+ writer.writeEndElement();
+ }
+
+ void writeRoleIdentifiers(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ ModelNode value = model.get(Constants.Model.ROLE_ATTRIBUTES);
+ if (!value.isDefined()) {
+ return;
+ }
+
+ List<ModelNode> items = value.asList();
+ if (items.size() == 0) {
+ return;
+ }
+
+ writer.writeStartElement(Constants.XML.ROLE_IDENTIFIERS);
+ for (ModelNode item : items) {
+ writer.writeStartElement(Constants.XML.ATTRIBUTE);
+ writer.writeAttribute("name", item.asString());
+ writer.writeEndElement();
+ }
+ writer.writeEndElement();
+ }
+
+ void writePrincipalNameMapping(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
+ writer.writeStartElement(Constants.XML.PRINCIPAL_NAME_MAPPING);
+ ModelNode value = model.get(Constants.Model.PRINCIPAL_NAME_MAPPING_POLICY);
+ if (value.isDefined()) {
+ writer.writeAttribute(Constants.XML.PRINCIPAL_NAME_MAPPING_POLICY, value.asString());
+ }
+ value = model.get(Constants.Model.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME);
+ if (value.isDefined()) {
+ writer.writeAttribute(Constants.XML.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, value.asString());
+ }
+ writer.writeEndElement();
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyDefinition.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyDefinition.java
new file mode 100644
index 0000000..7d19994
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyDefinition.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.AttributeDefinition;
+import org.jboss.as.controller.ObjectTypeAttributeDefinition;
+import org.jboss.as.controller.OperationStepHandler;
+import org.jboss.as.controller.PathElement;
+import org.jboss.as.controller.ReloadRequiredRemoveStepHandler;
+import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler;
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.as.controller.SimpleResourceDefinition;
+import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
+import org.jboss.as.controller.registry.ManagementResourceRegistration;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KeyDefinition extends SimpleResourceDefinition {
+
+ static final SimpleAttributeDefinition SIGNING =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGNING, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.SIGNING)
+ .build();
+
+ static final SimpleAttributeDefinition ENCRYPTION =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.ENCRYPTION, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.ENCRYPTION)
+ .build();
+
+ static final SimpleAttributeDefinition PRIVATE_KEY_PEM =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PRIVATE_KEY_PEM, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PRIVATE_KEY_PEM)
+ .build();
+
+ static final SimpleAttributeDefinition PUBLIC_KEY_PEM =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PUBLIC_KEY_PEM, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PUBLIC_KEY_PEM)
+ .build();
+
+ static final SimpleAttributeDefinition CERTIFICATE_PEM =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.CERTIFICATE_PEM, ModelType.STRING, true)
+ .setXmlName(Constants.XML.CERTIFICATE_PEM)
+ .build();
+
+ static final ObjectTypeAttributeDefinition KEY_STORE =
+ ObjectTypeAttributeDefinition.Builder.of(Constants.Model.KEY_STORE,
+ KeyStoreDefinition.ALL_ATTRIBUTES)
+ .setAllowNull(false)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {SIGNING, ENCRYPTION};
+ static final SimpleAttributeDefinition[] ELEMENTS = {PRIVATE_KEY_PEM, PUBLIC_KEY_PEM, CERTIFICATE_PEM};
+ static final AttributeDefinition[] ALL_ATTRIBUTES = {SIGNING, ENCRYPTION, PRIVATE_KEY_PEM, PUBLIC_KEY_PEM, CERTIFICATE_PEM, KEY_STORE};
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static final HashMap<String, SimpleAttributeDefinition> ELEMENT_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ELEMENTS) {
+ ELEMENT_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static final KeyDefinition INSTANCE = new KeyDefinition();
+
+ private KeyDefinition() {
+ super(PathElement.pathElement(Constants.Model.KEY),
+ KeycloakSamlExtension.getResourceDescriptionResolver(Constants.Model.KEY),
+ new KeyAddHandler(),
+ ReloadRequiredRemoveStepHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerOperations(ManagementResourceRegistration resourceRegistration) {
+ super.registerOperations(resourceRegistration);
+ resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
+ super.registerAttributes(resourceRegistration);
+
+ final OperationStepHandler writeHandler = new ReloadRequiredWriteAttributeHandler(ALL_ATTRIBUTES);
+ for (AttributeDefinition attribute : ALL_ATTRIBUTES) {
+ resourceRegistration.registerReadWriteAttribute(attribute, null, writeHandler);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+
+ static SimpleAttributeDefinition lookupElement(String xmlName) {
+ return ELEMENT_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStoreCertificateDefinition.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStoreCertificateDefinition.java
new file mode 100644
index 0000000..83dc057
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStoreCertificateDefinition.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.dmr.ModelType;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KeyStoreCertificateDefinition {
+
+ static final SimpleAttributeDefinition CERTIFICATE_ALIAS =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.CERTIFICATE_ALIAS, ModelType.STRING, true)
+ .setXmlName(Constants.XML.CERTIFICATE_ALIAS)
+ .build();
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return Constants.XML.CERTIFICATE_ALIAS.equals(xmlName) ? CERTIFICATE_ALIAS : null;
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStoreDefinition.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStoreDefinition.java
new file mode 100644
index 0000000..a71e89a
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStoreDefinition.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+abstract class KeyStoreDefinition {
+
+ static final SimpleAttributeDefinition RESOURCE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.RESOURCE, ModelType.STRING, true)
+ .setXmlName(Constants.XML.RESOURCE)
+ .build();
+
+ static final SimpleAttributeDefinition PASSWORD =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PASSWORD, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PASSWORD)
+ .build();
+
+ static final SimpleAttributeDefinition FILE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.FILE, ModelType.STRING, true)
+ .setXmlName(Constants.XML.FILE)
+ .build();
+
+ static final SimpleAttributeDefinition TYPE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.TYPE, ModelType.STRING, true)
+ .setXmlName(Constants.XML.TYPE)
+ .build();
+
+ static final SimpleAttributeDefinition ALIAS =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.ALIAS, ModelType.STRING, true)
+ .setXmlName(Constants.XML.ALIAS)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {RESOURCE, PASSWORD, FILE, TYPE, ALIAS};
+ static final SimpleAttributeDefinition[] ALL_ATTRIBUTES = {RESOURCE, PASSWORD, FILE, TYPE, ALIAS,
+ KeyStorePrivateKeyDefinition.PRIVATE_KEY_ALIAS,
+ KeyStorePrivateKeyDefinition.PRIVATE_KEY_PASSWORD,
+ KeyStoreCertificateDefinition.CERTIFICATE_ALIAS
+ };
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStorePrivateKeyDefinition.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStorePrivateKeyDefinition.java
new file mode 100644
index 0000000..bbb398d
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeyStorePrivateKeyDefinition.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KeyStorePrivateKeyDefinition {
+ static final SimpleAttributeDefinition PRIVATE_KEY_ALIAS =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PRIVATE_KEY_ALIAS, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PRIVATE_KEY_ALIAS)
+ .build();
+
+ static final SimpleAttributeDefinition PRIVATE_KEY_PASSWORD =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PRIVATE_KEY_PASSWORD, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PRIVATE_KEY_PASSWORD)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {PRIVATE_KEY_ALIAS, PRIVATE_KEY_PASSWORD};
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/logging/KeycloakLogger.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/logging/KeycloakLogger.java
new file mode 100755
index 0000000..d068b64
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/logging/KeycloakLogger.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension.logging;
+
+import org.jboss.logging.BasicLogger;
+import org.jboss.logging.Logger;
+import org.jboss.logging.annotations.LogMessage;
+import org.jboss.logging.annotations.Message;
+import org.jboss.logging.annotations.MessageLogger;
+
+import static org.jboss.logging.Logger.Level.INFO;
+
+/**
+ * This interface to be fleshed out later when error messages are fully externalized.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc.
+ */
+@MessageLogger(projectCode = "KEYCLOAK")
+public interface KeycloakLogger extends BasicLogger {
+
+ /**
+ * A logger with a category of the package name.
+ */
+ KeycloakLogger ROOT_LOGGER = Logger.getMessageLogger(KeycloakLogger.class, "org.jboss.keycloak");
+
+ @LogMessage(level = INFO)
+ @Message(value = "Keycloak subsystem override for deployment %s")
+ void deploymentSecured(String deployment);
+
+
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/logging/KeycloakMessages.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/logging/KeycloakMessages.java
new file mode 100755
index 0000000..be44809
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/logging/KeycloakMessages.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension.logging;
+
+import org.jboss.logging.Messages;
+import org.jboss.logging.annotations.MessageBundle;
+
+/**
+ * This interface to be fleshed out later when error messages are fully externalized.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2012 Red Hat Inc.
+ */
+@MessageBundle(projectCode = "KEYCLOAK")
+public interface KeycloakMessages {
+
+ /**
+ * The messages
+ */
+ KeycloakMessages MESSAGES = Messages.getBundle(KeycloakMessages.class);
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SecureDeploymentAddHandler.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SecureDeploymentAddHandler.java
new file mode 100644
index 0000000..22de93d
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SecureDeploymentAddHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.AbstractAddStepHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.dmr.ModelNode;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+class SecureDeploymentAddHandler extends AbstractAddStepHandler {
+
+ static SecureDeploymentAddHandler INSTANCE = new SecureDeploymentAddHandler();
+
+ private SecureDeploymentAddHandler() {
+ }
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
+ Configuration.INSTANCE.updateModel(operation, model);
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SecureDeploymentDefinition.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SecureDeploymentDefinition.java
new file mode 100644
index 0000000..bf6cab5
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SecureDeploymentDefinition.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.*;
+import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
+import org.jboss.as.controller.registry.ManagementResourceRegistration;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Defines attributes and operations for a secure-deployment.
+ */
+public class SecureDeploymentDefinition extends SimpleResourceDefinition {
+
+ static final SecureDeploymentDefinition INSTANCE = new SecureDeploymentDefinition();
+
+ private SecureDeploymentDefinition() {
+ super(PathElement.pathElement(Constants.Model.SECURE_DEPLOYMENT),
+ KeycloakSamlExtension.getResourceDescriptionResolver(Constants.Model.SECURE_DEPLOYMENT),
+ SecureDeploymentAddHandler.INSTANCE,
+ ReloadRequiredRemoveStepHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerOperations(ManagementResourceRegistration resourceRegistration) {
+ super.registerOperations(resourceRegistration);
+ resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/ServiceProviderAddHandler.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/ServiceProviderAddHandler.java
new file mode 100644
index 0000000..f9f3070
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/ServiceProviderAddHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.AbstractAddStepHandler;
+import org.jboss.as.controller.OperationContext;
+import org.jboss.as.controller.OperationFailedException;
+import org.jboss.dmr.ModelNode;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+class ServiceProviderAddHandler extends AbstractAddStepHandler {
+
+ static final ServiceProviderAddHandler INSTANCE = new ServiceProviderAddHandler();
+
+ ServiceProviderAddHandler() {
+ super(ServiceProviderDefinition.ALL_ATTRIBUTES);
+ }
+
+ @Override
+ protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
+ Configuration.INSTANCE.updateModel(operation, model);
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/ServiceProviderDefinition.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/ServiceProviderDefinition.java
new file mode 100644
index 0000000..cb84f12
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/ServiceProviderDefinition.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.AttributeDefinition;
+import org.jboss.as.controller.ListAttributeDefinition;
+import org.jboss.as.controller.OperationStepHandler;
+import org.jboss.as.controller.PathElement;
+import org.jboss.as.controller.ReloadRequiredRemoveStepHandler;
+import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler;
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.as.controller.SimpleResourceDefinition;
+import org.jboss.as.controller.StringListAttributeDefinition;
+import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler;
+import org.jboss.as.controller.registry.ManagementResourceRegistration;
+import org.jboss.dmr.ModelType;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class ServiceProviderDefinition extends SimpleResourceDefinition {
+
+ static final SimpleAttributeDefinition SSL_POLICY =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SSL_POLICY, ModelType.STRING, true)
+ .setXmlName(Constants.XML.SSL_POLICY)
+ .build();
+
+ static final SimpleAttributeDefinition NAME_ID_POLICY_FORMAT =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.NAME_ID_POLICY_FORMAT, ModelType.STRING, true)
+ .setXmlName(Constants.XML.NAME_ID_POLICY_FORMAT)
+ .build();
+
+ static final SimpleAttributeDefinition LOGOUT_PAGE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.LOGOUT_PAGE, ModelType.STRING, true)
+ .setXmlName(Constants.XML.LOGOUT_PAGE)
+ .build();
+
+ static final SimpleAttributeDefinition FORCE_AUTHENTICATION =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.FORCE_AUTHENTICATION, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.FORCE_AUTHENTICATION)
+ .build();
+
+ static final SimpleAttributeDefinition PRINCIPAL_NAME_MAPPING_POLICY =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PRINCIPAL_NAME_MAPPING_POLICY, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PRINCIPAL_NAME_MAPPING_POLICY)
+ .build();
+
+ static final SimpleAttributeDefinition PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, ModelType.STRING, true)
+ .setXmlName(Constants.XML.PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME)
+ .build();
+
+ static final ListAttributeDefinition ROLE_ATTRIBUTES =
+ new StringListAttributeDefinition.Builder(Constants.Model.ROLE_ATTRIBUTES)
+ .setAllowNull(false)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {SSL_POLICY, NAME_ID_POLICY_FORMAT, LOGOUT_PAGE, FORCE_AUTHENTICATION};
+ static final AttributeDefinition[] ELEMENTS = {PRINCIPAL_NAME_MAPPING_POLICY, PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, ROLE_ATTRIBUTES};
+
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+ static final HashMap<String, AttributeDefinition> ALL_MAP = new HashMap<>();
+ static final Collection<AttributeDefinition> ALL_ATTRIBUTES;
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+
+ ALL_MAP.putAll(ATTRIBUTE_MAP);
+ for (AttributeDefinition def : ELEMENTS) {
+ ALL_MAP.put(def.getXmlName(), def);
+ }
+ ALL_ATTRIBUTES = Collections.unmodifiableCollection(ALL_MAP.values());
+ }
+
+ static final ServiceProviderDefinition INSTANCE = new ServiceProviderDefinition();
+
+ private ServiceProviderDefinition() {
+ super(PathElement.pathElement(Constants.Model.SERVICE_PROVIDER),
+ KeycloakSamlExtension.getResourceDescriptionResolver(Constants.Model.SERVICE_PROVIDER),
+ ServiceProviderAddHandler.INSTANCE,
+ ReloadRequiredRemoveStepHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerOperations(ManagementResourceRegistration resourceRegistration) {
+ super.registerOperations(resourceRegistration);
+ resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE);
+ }
+
+ @Override
+ public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
+ super.registerAttributes(resourceRegistration);
+
+ final OperationStepHandler writeHandler = new ReloadRequiredWriteAttributeHandler(ALL_ATTRIBUTES);
+ for (AttributeDefinition attribute : ALL_ATTRIBUTES) {
+ resourceRegistration.registerReadWriteAttribute(attribute, null, writeHandler);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SingleLogoutDefinition.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SingleLogoutDefinition.java
new file mode 100644
index 0000000..6ad99b1
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SingleLogoutDefinition.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+abstract class SingleLogoutDefinition {
+
+ static final SimpleAttributeDefinition VALIDATE_REQUEST_SIGNATURE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.VALIDATE_REQUEST_SIGNATURE, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.VALIDATE_REQUEST_SIGNATURE)
+ .build();
+
+ static final SimpleAttributeDefinition VALIDATE_RESPONSE_SIGNATURE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.VALIDATE_RESPONSE_SIGNATURE, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.VALIDATE_RESPONSE_SIGNATURE)
+ .build();
+
+ static final SimpleAttributeDefinition SIGN_REQUEST =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGN_REQUEST, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.SIGN_REQUEST)
+ .build();
+
+ static final SimpleAttributeDefinition SIGN_RESPONSE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGN_RESPONSE, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.SIGN_RESPONSE)
+ .build();
+
+ static final SimpleAttributeDefinition REQUEST_BINDING =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.REQUEST_BINDING, ModelType.STRING, true)
+ .setXmlName(Constants.XML.REQUEST_BINDING)
+ .build();
+
+ static final SimpleAttributeDefinition RESPONSE_BINDING =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.RESPONSE_BINDING, ModelType.STRING, true)
+ .setXmlName(Constants.XML.RESPONSE_BINDING)
+ .build();
+
+ static final SimpleAttributeDefinition POST_BINDING_URL =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.POST_BINDING_URL, ModelType.STRING, true)
+ .setXmlName(Constants.XML.POST_BINDING_URL)
+ .build();
+
+ static final SimpleAttributeDefinition REDIRECT_BINDING_URL =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.REDIRECT_BINDING_URL, ModelType.STRING, true)
+ .setXmlName(Constants.XML.REDIRECT_BINDING_URL)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {VALIDATE_REQUEST_SIGNATURE, VALIDATE_RESPONSE_SIGNATURE,
+ SIGN_REQUEST, SIGN_RESPONSE, REQUEST_BINDING, RESPONSE_BINDING, POST_BINDING_URL, REDIRECT_BINDING_URL};
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SingleSignOnDefinition.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SingleSignOnDefinition.java
new file mode 100644
index 0000000..fe78c02
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/SingleSignOnDefinition.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.controller.SimpleAttributeDefinition;
+import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
+import org.jboss.dmr.ModelType;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+abstract class SingleSignOnDefinition {
+
+ static final SimpleAttributeDefinition SIGN_REQUEST =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.SIGN_REQUEST, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.SIGN_REQUEST)
+ .build();
+
+ static final SimpleAttributeDefinition VALIDATE_RESPONSE_SIGNATURE =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.VALIDATE_RESPONSE_SIGNATURE, ModelType.BOOLEAN, true)
+ .setXmlName(Constants.XML.VALIDATE_RESPONSE_SIGNATURE)
+ .build();
+
+ static final SimpleAttributeDefinition REQUEST_BINDING =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.REQUEST_BINDING, ModelType.STRING, true)
+ .setXmlName(Constants.XML.REQUEST_BINDING)
+ .build();
+
+ static final SimpleAttributeDefinition RESPONSE_BINDING =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.RESPONSE_BINDING, ModelType.STRING, true)
+ .setXmlName(Constants.XML.RESPONSE_BINDING)
+ .build();
+
+ static final SimpleAttributeDefinition BINDING_URL =
+ new SimpleAttributeDefinitionBuilder(Constants.Model.BINDING_URL, ModelType.STRING, true)
+ .setXmlName(Constants.XML.BINDING_URL)
+ .build();
+
+ static final SimpleAttributeDefinition[] ATTRIBUTES = {SIGN_REQUEST, VALIDATE_RESPONSE_SIGNATURE, REQUEST_BINDING, RESPONSE_BINDING, BINDING_URL};
+
+ static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
+
+ static {
+ for (SimpleAttributeDefinition def : ATTRIBUTES) {
+ ATTRIBUTE_MAP.put(def.getXmlName(), def);
+ }
+ }
+
+ static SimpleAttributeDefinition lookup(String xmlName) {
+ return ATTRIBUTE_MAP.get(xmlName);
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/saml/extension/LocalDescriptions.properties b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/saml/extension/LocalDescriptions.properties
new file mode 100755
index 0000000..f8a4a11
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/saml/extension/LocalDescriptions.properties
@@ -0,0 +1,63 @@
+keycloak-saml.subsystem=Keycloak adapter subsystem
+keycloak-saml.subsystem.add=Operation Adds Keycloak adapter subsystem
+keycloak-saml.subsystem.remove=Operation removes Keycloak adapter subsystem
+keycloak-saml.subsystem.secure-deployment=A deployment secured by Keycloak.
+
+keycloak-saml.secure-deployment=A deployment secured by Keycloak
+keycloak-saml.secure-deployment.add=Add a deployment to be secured by Keycloak
+keycloak-saml.secure-deployment.remove=Remove a deployment to be secured by Keycloak
+keycloak-saml.secure-deployment.service-provider=A security provider configuration for secure deployment
+
+keycloak-saml.service-provider=A security provider configuration for secure deployment
+keycloak-saml.service-provider.add=Add a security provider configuration to deployment secured by Keycloak SAML
+keycloak-saml.service-provider.remove=Remove a security provider definition from deployment secured by Keycloak SAML
+keycloak-saml.service-provider.ssl-policy=SSL Policy to use
+keycloak-saml.service-provider.name-id-policy-format=Name ID policy format URN
+keycloak-saml.service-provider.logout-page=URI to a logout page
+keycloak-saml.service-provider.force-authentication=Redirected unauthenticated request to a login page
+keycloak-saml.service-provider.role-attributes=Role identifiers
+keycloak-saml.service-provider.principal-name-mapping-policy=Principal name mapping policy
+keycloak-saml.service-provider.principal-name-mapping-attribute-name=Principal name mapping attribute name
+keycloak-saml.service-provider.key=A key definition
+keycloak-saml.service-provider.identity-provider=Identity provider definition
+
+keycloak-saml.key=A key configuration for service provider or identity provider
+keycloak-saml.key.add=Add a key definition
+keycloak-saml.key.remove=Remove a key definition
+keycloak-saml.key.signing=Key can be used for signing
+keycloak-saml.key.encryption=Key can be used for encryption
+keycloak-saml.key.private-key-pem=Private key string in pem format
+keycloak-saml.key.public-key-pem=Public key string in pem format
+keycloak-saml.key.certificate-pem=Certificate key string in pem format
+keycloak-saml.key.key-store=Key store definition
+keycloak-saml.key.key-store.file=Key store filesystem path
+keycloak-saml.key.key-store.resource=Key store resource URI
+keycloak-saml.key.key-store.password=Key store password
+keycloak-saml.key.key-store.type=Key store format
+keycloak-saml.key.key-store.alias=Key alias
+keycloak-saml.key.key-store.private-key-alias=Private key alias
+keycloak-saml.key.key-store.private-key-password=Private key password
+keycloak-saml.key.key-store.certificate-alias=Certificate alias
+
+keycloak-saml.identity-provider=An identity provider configuration
+keycloak-saml.identity-provider.add=Add an identity provider
+keycloak-saml.identity-provider.remove=Remove an identity provider
+keycloak-saml.identity-provider.signatures-required=Require signatures for single-sign-on and single-logout
+keycloak-saml.identity-provider.signature-algorithm=Signature algorithm
+keycloak-saml.identity-provider.signature-canonicalization-method=Signature canonicalization method
+keycloak-saml.identity-provider.single-sign-on=Single sign-on configuration
+keycloak-saml.identity-provider.single-sign-on.sign-request=Sign SSO requests
+keycloak-saml.identity-provider.single-sign-on.validate-response-signature=Validate an SSO response signature
+keycloak-saml.identity-provider.single-sign-on.request-binding=HTTP method to use for requests
+keycloak-saml.identity-provider.single-sign-on.response-binding=HTTP method to use for responses
+keycloak-saml.identity-provider.single-sign-on.binding-url=SSO endpoint URL
+keycloak-saml.identity-provider.single-logout=Single logout configuration
+keycloak-saml.identity-provider.single-logout.validate-request-signature=Validate a single-logout request signature
+keycloak-saml.identity-provider.single-logout.validate-response-signature=Validate a single-logout response signature
+keycloak-saml.identity-provider.single-logout.sign-request=Sign single-logout requests
+keycloak-saml.identity-provider.single-logout.sign-response=Sign single-logout responses
+keycloak-saml.identity-provider.single-logout.request-binding=HTTP method to use for request
+keycloak-saml.identity-provider.single-logout.response-binding=HTTP method to use for response
+keycloak-saml.identity-provider.single-logout.post-binding-url=Endpoint URL for posting
+keycloak-saml.identity-provider.single-logout.redirect-binding-url=Endpoint URL for redirects
+keycloak-saml.identity-provider.key=Key definition for identity provider
\ No newline at end of file
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_1.xsd b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_1.xsd
new file mode 100755
index 0000000..725104b
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_1.xsd
@@ -0,0 +1,268 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ targetNamespace="urn:jboss:domain:keycloak-saml:1.1"
+ xmlns="urn:jboss:domain:keycloak-saml:1.1"
+ elementFormDefault="qualified"
+ attributeFormDefault="unqualified"
+ version="1.0">
+
+ <!-- The subsystem root element -->
+ <xs:element name="subsystem" type="subsystem-type"/>
+
+ <xs:complexType name="subsystem-type">
+ <xs:annotation>
+ <xs:documentation>
+ <![CDATA[
+ The Keycloak SAML adapter subsystem, used to register deployments managed by Keycloak SAML adapter
+ ]]>
+ </xs:documentation>
+ </xs:annotation>
+ <xs:all>
+ <xs:element name="secure-deployment" minOccurs="0" type="secure-deployment-type"/>
+ </xs:all>
+ </xs:complexType>
+
+ <xs:complexType name="secure-deployment-type">
+ <xs:all>
+ <xs:element name="SP" minOccurs="1" maxOccurs="1" type="sp-type"/>
+ </xs:all>
+ <xs:attribute name="name" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>The name of the realm.</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+
+ <xs:complexType name="sp-type">
+ <xs:all>
+ <xs:element name="Keys" minOccurs="0" maxOccurs="1" type="keys-type"/>
+ <xs:element name="PrincipalNameMapping" minOccurs="0" maxOccurs="1" type="principal-name-mapping-type"/>
+ <xs:element name="RoleIdentifiers" minOccurs="0" maxOccurs="1" type="role-identifiers-type"/>
+ <xs:element name="IDP" minOccurs="1" maxOccurs="1" type="identity-provider-type"/>
+ </xs:all>
+ <xs:attribute name="entityID" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>The entity ID for SAML service provider</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="sslPolicy" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>The ssl policy</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="nameIDPolicyFormat" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Name ID policy format URN</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="logoutPage" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>URI to a logout page</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="forceAuthentication" type="xs:boolean" use="required">
+ <xs:annotation>
+ <xs:documentation>Redirected unauthenticated request to a login page</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="identity-provider-type">
+ <xs:all minOccurs="1" maxOccurs="1">
+ <xs:element name="SingleSignOnService" minOccurs="1" maxOccurs="1" type="single-signon-type"/>
+ <xs:element name="SingleLogoutService" minOccurs="0" maxOccurs="1" type="single-logout-type"/>
+ <xs:element name="Keys" minOccurs="0" maxOccurs="1" type="keys-type"/>
+ </xs:all>
+ <xs:attribute name="entityID" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>The entity ID for SAML service provider</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="signaturesRequired" type="xs:boolean" use="required">
+ <xs:annotation>
+ <xs:documentation>Require signatures for single-sign-on and single-logout</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="signatureAlgorithm" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Algorithm used for signatures</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Canonicalization method used for signatures</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="single-signon-type">
+ <xs:attribute name="signRequest" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Sign the SSO requests</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Validate the SSO response signature</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="requestBinding" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>HTTP method to use for requests</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="responseBinding" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>HTTP method to use for response</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="bindingUrl" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>SSO endpoint URL</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="single-logout-type">
+ <xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Validate a single-logout request signature</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Validate a single-logout response signature</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="signRequest" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Sign single-logout requests</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="signResponse" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Sign single-logout responses</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="requestBinding" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>HTTP method to use for request</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="responseBinding" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>HTTP method to use for response</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="postBindingUrl" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Endpoint URL for posting</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="redirectBindingUrl" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Endpoint URL for redirects</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="keys-type">
+ <xs:sequence>
+ <xs:element name="Key" minOccurs="1" maxOccurs="2" type="key-type"/>
+ </xs:sequence>
+ </xs:complexType>
+ <xs:complexType name="key-type">
+ <xs:all>
+ <xs:element name="KeyStore" minOccurs="0" maxOccurs="1" type="keystore-type"/>
+ <xs:element name="PrivateKeyPem" minOccurs="0" maxOccurs="1" type="xs:string"/>
+ <xs:element name="PublicKeyPem" minOccurs="0" maxOccurs="1" type="xs:string"/>
+ <xs:element name="CertificatePem" minOccurs="0" maxOccurs="1" type="xs:string"/>
+ </xs:all>
+ <xs:attribute name="signing" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key can be used for signing</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="encryption" type="xs:boolean" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key can be used for encryption</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+ <xs:complexType name="keystore-type">
+ <xs:sequence minOccurs="0" maxOccurs="1">
+ <xs:element name="PrivateKey" minOccurs="0" maxOccurs="1" type="privatekey-type"/>
+ <xs:element name="Certificate" minOccurs="0" maxOccurs="1" type="certificate-type"/>
+ </xs:sequence>
+ <xs:attribute name="file" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key store filesystem path</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="resource" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key store resource URI</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="password" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Key store password</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="type" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key store format</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="alias" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Key alias</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+
+ <xs:complexType name="privatekey-type">
+ <xs:attribute name="alias" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Private key alias</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="password" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Private key password</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+
+ <xs:complexType name="certificate-type">
+ <xs:attribute name="alias" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Certificate alias</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+
+ <xs:complexType name="principal-name-mapping-type">
+ <xs:attribute name="policy" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Principal name mapping policy. Possible values: FROM_NAME_ID</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ <xs:attribute name="attribute" type="xs:string" use="optional">
+ <xs:annotation>
+ <xs:documentation>Name of the attribute to use for principal name mapping</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+
+ <xs:complexType name="role-identifiers-type">
+ <xs:sequence minOccurs="0" maxOccurs="unbounded">
+ <xs:element name="Attribute" minOccurs="0" maxOccurs="unbounded" type="attribute-type"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="attribute-type">
+ <xs:attribute name="name" type="xs:string" use="required">
+ <xs:annotation>
+ <xs:documentation>Role attribute</xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+ </xs:complexType>
+</xs:schema>
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/saml/extension/SubsystemParsingTestCase.java b/saml/client-adapter/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/saml/extension/SubsystemParsingTestCase.java
new file mode 100755
index 0000000..fdd8ee2
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/saml/extension/SubsystemParsingTestCase.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.subsystem.adapter.saml.extension;
+
+import org.jboss.as.subsystem.test.AbstractSubsystemBaseTest;
+
+import java.io.IOException;
+
+
+/**
+ * Tests all management expects for subsystem, parsing, marshaling, model definition and other
+ * Here is an example that allows you a fine grained controller over what is tested and how. So it can give you ideas what can be done and tested.
+ * If you have no need for advanced testing of subsystem you look at {@link SubsystemBaseParsingTestCase} that testes same stuff but most of the code
+ * is hidden inside of test harness
+ *
+ * @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
+ * @author Tomaz Cerar
+ * @author <a href="marko.strukelj@gmail.com">Marko Strukelj</a>
+ */
+public class SubsystemParsingTestCase extends AbstractSubsystemBaseTest {
+
+ public SubsystemParsingTestCase() {
+ super(KeycloakSamlExtension.SUBSYSTEM_NAME, new KeycloakSamlExtension());
+ }
+
+ @Override
+ protected String getSubsystemXml() throws IOException {
+ return readResource("keycloak-saml-1.1.xml");
+ }
+
+ @Override
+ protected String getSubsystemXsdPath() throws Exception {
+ return "schema/wildfly-keycloak-saml_1_1.xsd";
+ }
+
+ @Override
+ protected String[] getSubsystemTemplatePaths() throws IOException {
+ return new String[]{
+ "/subsystem-templates/keycloak-saml-adapter.xml"
+ };
+ }
+}
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.1.xml b/saml/client-adapter/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.1.xml
new file mode 100644
index 0000000..6f56fb0
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.1.xml
@@ -0,0 +1,50 @@
+<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1">
+ <secure-deployment name="my-app.war">
+ <SP entityID="http://localhost:8080/sales-post-enc/"
+ sslPolicy="EXTERNAL"
+ nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ logoutPage="/logout.jsp"
+ forceAuthentication="false">
+
+ <Keys>
+ <Key encryption="true" signing="true">
+ <PrivateKeyPem>my_key.pem</PrivateKeyPem>
+ <PublicKeyPem>my_key.pub</PublicKeyPem>
+ <CertificatePem>cert.cer</CertificatePem>
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123" file="test" alias="test" type="jks">
+ <PrivateKey alias="http://localhost:8080/sales-post-enc/" password="test123"/>
+ <Certificate alias="http://localhost:8080/sales-post-enc/"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ <PrincipalNameMapping policy="FROM_NAME_ID" attribute="test"/>
+ <RoleIdentifiers>
+ <Attribute name="Role"/>
+ <Attribute name="Role2"/>
+ </RoleIdentifiers>
+ <IDP entityID="idp" signaturesRequired="true" signatureAlgorithm="test" signatureCanonicalizationMethod="test">
+ <SingleSignOnService signRequest="true"
+ validateResponseSignature="true"
+ requestBinding="POST"
+ responseBinding="POST"
+ bindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"/>
+ <SingleLogoutService
+ validateRequestSignature="true"
+ validateResponseSignature="true"
+ signRequest="true"
+ signResponse="true"
+ requestBinding="POST"
+ responseBinding="POST"
+ postBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"
+ redirectBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"/>
+ <Keys>
+ <Key signing="true">
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
+ <Certificate alias="saml-demo"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ </IDP>
+ </SP>
+ </secure-deployment>
+</subsystem>
\ No newline at end of file
diff --git a/saml/client-adapter/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.1-err.xml b/saml/client-adapter/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.1-err.xml
new file mode 100644
index 0000000..26d0e98
--- /dev/null
+++ b/saml/client-adapter/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.1-err.xml
@@ -0,0 +1,50 @@
+<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1">
+ <secure-deployment name="my-app.war">
+ <SP entityID="http://localhost:8080/sales-post-enc/"
+ sslPolicy="EXTERNAL"
+ nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ logoutPage="/logout.jsp"
+ forceAuthentication="false">
+
+ <Keys>
+ <Key encryption="true" signing="true">
+ <PrivateKeyPem>my_key.pem</PrivateKeyPem>
+ <PublicKeyPem>my_key.pub</PublicKeyPem>
+ <CertificatePem>cert.cer</CertificatePem>
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123" file="test" alias="test" type="jks">
+ <PrivateKey alias="http://localhost:8080/sales-post-enc/" password="test123"/>
+ <Certificate alias="http://localhost:8080/sales-post-enc/"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ <PrincipalNameMapping policy="FROM_NAME_ID" attribute="test"/>
+ <RoleIdentifiers>
+ <Attribute name="Role"/>
+ <Attribute name="Role2"/>
+ </RoleIdentifiers>
+ <IDP entityID="idp" signaturesRequired="true" signatureAlgorithm="test" signatureCanonicalizationMethod="test" encryption="test">
+ <SingleSignOnService signRequest="true"
+ validateResponseSignature="true"
+ requestBinding="POST"
+ responseBinding="POST"
+ bindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"/>
+ <SingleLogoutService
+ validateRequestSignature="true"
+ validateResponseSignature="true"
+ signRequest="true"
+ signResponse="true"
+ requestBinding="POST"
+ responseBinding="POST"
+ postBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"
+ redirectBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"/>
+ <Keys>
+ <Key signing="true">
+ <KeyStore resource="/WEB-INF/keystore.jks" password="store123">
+ <Certificate alias="saml-demo"/>
+ </KeyStore>
+ </Key>
+ </Keys>
+ </IDP>
+ </SP>
+ </secure-deployment>
+</subsystem>
\ No newline at end of file
diff --git a/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusCodeType.java b/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusCodeType.java
index 2ce85eb..826928a 100755
--- a/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusCodeType.java
+++ b/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusCodeType.java
@@ -17,6 +17,7 @@
*/
package org.keycloak.dom.saml.v2.protocol;
+import java.io.Serializable;
import java.net.URI;
/**
@@ -39,7 +40,7 @@ import java.net.URI;
* </complexType>
* </pre>
*/
-public class StatusCodeType {
+public class StatusCodeType implements Serializable {
protected StatusCodeType statusCode;
protected URI value;
@@ -79,4 +80,10 @@ public class StatusCodeType {
public void setValue(URI value) {
this.value = value;
}
+
+ @Override
+ public String toString() {
+ return "StatusCodeType [value=" + value + ", statusCode=" + statusCode + "]";
+ }
+
}
\ No newline at end of file
diff --git a/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusType.java b/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusType.java
index 9918879..ec92dd4 100755
--- a/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusType.java
+++ b/saml/saml-core/src/main/java/org/keycloak/dom/saml/v2/protocol/StatusType.java
@@ -17,6 +17,8 @@
*/
package org.keycloak.dom.saml.v2.protocol;
+import java.io.Serializable;
+
/**
* <p>
* Java class for StatusType complex type.
@@ -38,7 +40,7 @@ package org.keycloak.dom.saml.v2.protocol;
* </complexType>
* </pre>
*/
-public class StatusType {
+public class StatusType implements Serializable {
protected String statusMessage;
protected StatusCodeType statusCode;
@@ -98,4 +100,9 @@ public class StatusType {
this.statusDetail = value;
}
+ @Override
+ public String toString() {
+ return "StatusType [statusCode=" + statusCode + ", statusMessage=" + statusMessage + ", statusDetail=" + statusDetail + "]";
+ }
+
}
diff --git a/saml/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java b/saml/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java
index 40db78c..b043a12 100755
--- a/saml/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java
+++ b/saml/saml-core/src/main/java/org/keycloak/saml/SAML2AuthnRequestBuilder.java
@@ -17,16 +17,16 @@
*/
package org.keycloak.saml;
+import java.net.URI;
+
+import org.keycloak.dom.saml.v2.assertion.NameIDType;
+import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
-import org.keycloak.dom.saml.v2.assertion.NameIDType;
-import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.w3c.dom.Document;
-import java.net.URI;
-
/**
* @author pedroigor
*/
@@ -64,6 +64,11 @@ public class SAML2AuthnRequestBuilder {
return this;
}
+ public SAML2AuthnRequestBuilder isPassive(boolean isPassive) {
+ this.authnRequestType.setIsPassive(isPassive);
+ return this;
+ }
+
public SAML2AuthnRequestBuilder nameIdPolicy(SAML2NameIDPolicyBuilder nameIDPolicy) {
this.authnRequestType.setNameIDPolicy(nameIDPolicy.build());
return this;
@@ -74,7 +79,7 @@ public class SAML2AuthnRequestBuilder {
return this;
}
- public Document toDocument() {
+ public Document toDocument() {
try {
AuthnRequestType authnRequestType = this.authnRequestType;
diff --git a/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java b/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java
index db7ac7e..2373656 100755
--- a/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java
+++ b/saml/saml-core/src/main/java/org/keycloak/saml/SAML2ErrorResponseBuilder.java
@@ -1,6 +1,12 @@
package org.keycloak.saml;
+import org.keycloak.dom.saml.v2.assertion.NameIDType;
+import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
+import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
+import org.keycloak.dom.saml.v2.protocol.StatusType;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
+import org.keycloak.saml.common.exceptions.ConfigurationException;
+import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
@@ -9,8 +15,11 @@ import org.keycloak.saml.processing.core.saml.v2.holders.IDPInfoHolder;
import org.keycloak.saml.processing.core.saml.v2.holders.IssuerInfoHolder;
import org.keycloak.saml.processing.core.saml.v2.holders.SPInfoHolder;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
+import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.w3c.dom.Document;
+import java.net.URI;
+
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@@ -38,29 +47,25 @@ public class SAML2ErrorResponseBuilder {
public Document buildDocument() throws ProcessingException {
- Document samlResponse = null;
- ResponseType responseType = null;
-
- SAML2Response saml2Response = new SAML2Response();
-
- // Create a response type
- String id = IDGenerator.create("ID_");
- IssuerInfoHolder issuerHolder = new IssuerInfoHolder(issuer);
- issuerHolder.setStatusCode(status);
+ try {
+ StatusResponseType statusResponse = new StatusResponseType(IDGenerator.create("ID_"), XMLTimeUtil.getIssueInstant());
- IDPInfoHolder idp = new IDPInfoHolder();
- idp.setNameIDFormatValue(null);
- idp.setNameIDFormat(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get());
+ statusResponse.setStatus(JBossSAMLAuthnResponseFactory.createStatusTypeForResponder(status));
+ NameIDType issuer = new NameIDType();
+ issuer.setValue(this.issuer);
- SPInfoHolder sp = new SPInfoHolder();
- sp.setResponseDestinationURI(destination);
+ statusResponse.setIssuer(issuer);
+ statusResponse.setDestination(destination);
- responseType = saml2Response.createResponseType(id);
- responseType.setStatus(JBossSAMLAuthnResponseFactory.createStatusTypeForResponder(status));
- responseType.setDestination(destination);
+ SAML2Response saml2Response = new SAML2Response();
+ return saml2Response.convert(statusResponse);
+ } catch (ConfigurationException e) {
+ throw new ProcessingException(e);
+ } catch (ParsingException e) {
+ throw new ProcessingException(e);
+ }
- return samlResponse;
}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java
index 298623d..1ee3cd2 100644
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/clientregistration/EntityDescriptorClientRegistrationProvider.java
@@ -1,19 +1,10 @@
package org.keycloak.protocol.saml.clientregistration;
-import org.jboss.logging.Logger;
-import org.keycloak.events.EventBuilder;
-import org.keycloak.events.EventType;
import org.keycloak.exportimport.ClientDescriptionConverter;
-import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelDuplicateException;
-import org.keycloak.models.utils.ModelToRepresentation;
-import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter;
import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.services.ErrorResponse;
-import org.keycloak.services.clientregistration.ClientRegAuth;
-import org.keycloak.services.clientregistration.ClientRegistrationProvider;
+import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
@@ -25,55 +16,20 @@ import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class EntityDescriptorClientRegistrationProvider implements ClientRegistrationProvider {
-
- private static final Logger logger = Logger.getLogger(EntityDescriptorClientRegistrationProvider.class);
-
- private KeycloakSession session;
- private EventBuilder event;
- private ClientRegAuth auth;
+public class EntityDescriptorClientRegistrationProvider extends AbstractClientRegistrationProvider {
public EntityDescriptorClientRegistrationProvider(KeycloakSession session) {
- this.session = session;
- }
-
-// @POST
-// @Consumes(MediaType.APPLICATION_XML)
-// @Produces(MediaType.APPLICATION_JSON)
-// public Response create(String descriptor) {
-// event.event(EventType.CLIENT_REGISTER);
-//
-// auth.requireCreate();
-//
-// ClientRepresentation client = session.getProvider(ClientDescriptionConverter.class, EntityDescriptorDescriptionConverter.ID).convertToInternal(descriptor);
-//
-// try {
-// ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
-// client = ModelToRepresentation.toRepresentation(clientModel);
-// URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
-//
-// logger.infov("Created client {0}", client.getClientId());
-//
-// event.client(client.getClientId()).success();
-//
-// return Response.created(uri).entity(client).build();
-// } catch (ModelDuplicateException e) {
-// return ErrorResponse.exists("Client " + client.getClientId() + " already exists");
-// }
-// }
-
- @Override
- public void close() {
- }
-
- @Override
- public void setAuth(ClientRegAuth auth) {
- this.auth = auth;
+ super(session);
}
- @Override
- public void setEvent(EventBuilder event) {
- this.event = event;
+ @POST
+ @Consumes(MediaType.APPLICATION_XML)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response createSaml(String descriptor) {
+ ClientRepresentation client = session.getProvider(ClientDescriptionConverter.class, EntityDescriptorDescriptionConverter.ID).convertToInternal(descriptor);
+ client = create(client);
+ URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
+ return Response.created(uri).entity(client).build();
}
}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java
new file mode 100755
index 0000000..2253e5c
--- /dev/null
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java
@@ -0,0 +1,157 @@
+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.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.protocol.ProtocolMapper;
+import org.keycloak.protocol.saml.SamlProtocol;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.services.managers.ClientSessionCode;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class GroupMembershipMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper {
+ public static final String PROVIDER_ID = "saml-group-membership-mapper";
+ public static final String SINGLE_GROUP_ATTRIBUTE = "single";
+
+ private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+ static {
+ ProviderConfigProperty property;
+ property = new ProviderConfigProperty();
+ property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAME);
+ property.setLabel("Group attribute name");
+ property.setDefaultValue("member");
+ property.setHelpText("Name of the SAML attribute you want to put your groups into. i.e. 'member', 'memberOf'.");
+ configProperties.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(AttributeStatementHelper.FRIENDLY_NAME);
+ property.setLabel(AttributeStatementHelper.FRIENDLY_NAME_LABEL);
+ property.setHelpText(AttributeStatementHelper.FRIENDLY_NAME_HELP_TEXT);
+ configProperties.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT);
+ property.setLabel("SAML Attribute NameFormat");
+ property.setHelpText("SAML Attribute NameFormat. Can be basic, URI reference, or unspecified.");
+ List<String> types = new ArrayList(3);
+ types.add(AttributeStatementHelper.BASIC);
+ types.add(AttributeStatementHelper.URI_REFERENCE);
+ types.add(AttributeStatementHelper.UNSPECIFIED);
+ property.setType(ProviderConfigProperty.LIST_TYPE);
+ property.setDefaultValue(types);
+ configProperties.add(property);
+ property = new ProviderConfigProperty();
+ property.setName(SINGLE_GROUP_ATTRIBUTE);
+ property.setLabel("Single Group Attribute");
+ property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ property.setDefaultValue("true");
+ property.setHelpText("If true, all groups will be stored under one attribute with multiple attribute values.");
+ configProperties.add(property);
+ property = new ProviderConfigProperty();
+ property.setName("full.path");
+ property.setLabel("Full group path");
+ property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ property.setDefaultValue("true");
+ property.setHelpText("Include full path to group i.e. /top/level1/level2, false will just specify the group name");
+ configProperties.add(property);
+
+
+ }
+
+
+ @Override
+ public String getDisplayCategory() {
+ return "Group Mapper";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Group list";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Group names are stored in an attribute value. There is either one attribute with multiple attribute values, or an attribute per group name depending on how you configure it. You can also specify the attribute name i.e. 'member' or 'memberOf' being examples.";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ public static boolean useFullPath(ProtocolMapperModel mappingModel) {
+ return "true".equals(mappingModel.getConfig().get("full.path"));
+ }
+
+
+ @Override
+ public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ String single = mappingModel.getConfig().get(SINGLE_GROUP_ATTRIBUTE);
+ boolean singleAttribute = Boolean.parseBoolean(single);
+
+ boolean fullPath = useFullPath(mappingModel);
+ AttributeType singleAttributeType = null;
+ for (GroupModel group : userSession.getUser().getGroups()) {
+ String groupName;
+ if (fullPath) {
+ groupName = ModelToRepresentation.buildGroupPath(group);
+ } else {
+ groupName = group.getName();
+ }
+ AttributeType attributeType = null;
+ if (singleAttribute) {
+ if (singleAttributeType == null) {
+ singleAttributeType = AttributeStatementHelper.createAttributeType(mappingModel);
+ attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(singleAttributeType));
+ }
+ attributeType = singleAttributeType;
+ } else {
+ attributeType = AttributeStatementHelper.createAttributeType(mappingModel);
+ attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attributeType));
+ }
+ attributeType.addAttributeValue(groupName);
+ }
+ }
+
+ public static ProtocolMapperModel create(String name, String samlAttributeName, String nameFormat, String friendlyName, boolean singleAttribute) {
+ ProtocolMapperModel mapper = new ProtocolMapperModel();
+ mapper.setName(name);
+ mapper.setProtocolMapper(PROVIDER_ID);
+ mapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
+ mapper.setConsentRequired(false);
+ Map<String, String> config = new HashMap<String, String>();
+ config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, samlAttributeName);
+ if (friendlyName != null) {
+ config.put(AttributeStatementHelper.FRIENDLY_NAME, friendlyName);
+ }
+ if (nameFormat != null) {
+ config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, nameFormat);
+ }
+ config.put(SINGLE_GROUP_ATTRIBUTE, Boolean.toString(singleAttribute));
+ mapper.setConfig(config);
+
+ return mapper;
+ }
+
+}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/RoleNameMapper.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/RoleNameMapper.java
index a6a295c..9974469 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/RoleNameMapper.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/RoleNameMapper.java
@@ -1,9 +1,9 @@
package org.keycloak.protocol.saml.mappers;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.RoleContainerModel;
-import org.keycloak.models.RoleModel;
+import org.keycloak.Config;
+import org.keycloak.models.*;
+import org.keycloak.protocol.ProtocolMapper;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
import org.keycloak.protocol.saml.SamlProtocol;
@@ -19,7 +19,7 @@ import java.util.Map;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class RoleNameMapper extends AbstractOIDCProtocolMapper implements SAMLRoleNameMapper {
+public class RoleNameMapper implements SAMLRoleNameMapper, ProtocolMapper {
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
@@ -49,23 +49,19 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements SAMLRo
return configProperties;
}
- @Override
public String getId() {
return PROVIDER_ID;
}
- @Override
public String getDisplayType() {
return "Role Name Mapper";
}
- @Override
public String getDisplayCategory() {
return "Role Mapper";
}
- @Override
public String getHelpText() {
return "Map an assigned role to a new name";
}
@@ -109,4 +105,26 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements SAMLRo
}
+ @Override
+ public String getProtocol() {
+ return SamlProtocol.LOGIN_PROTOCOL;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public final ProtocolMapper create(KeycloakSession session) {
+ throw new RuntimeException("UNSUPPORTED METHOD");
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/SAMLGroupNameMapper.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/SAMLGroupNameMapper.java
new file mode 100755
index 0000000..87a48c9
--- /dev/null
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/SAMLGroupNameMapper.java
@@ -0,0 +1,12 @@
+package org.keycloak.protocol.saml.mappers;
+
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.ProtocolMapperModel;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface SAMLGroupNameMapper {
+ public String mapName(ProtocolMapperModel model, GroupModel group);
+}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
index dc80760..8f5e3b8 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java
@@ -5,6 +5,7 @@ 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.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.provider.ProviderConfigProperty;
@@ -62,7 +63,7 @@ public class UserAttributeStatementMapper extends AbstractSAMLProtocolMapper imp
public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
UserModel user = userSession.getUser();
String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
- String attributeValue = user.getFirstAttribute(attributeName);
+ String attributeValue = KeycloakModelUtils.resolveFirstAttribute(user, attributeName);
if (attributeValue == null) return;
AttributeStatementHelper.addAttribute(attributeStatement, mappingModel, attributeValue);
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
index 8d6fa15..3a30c2e 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java
@@ -1,5 +1,22 @@
package org.keycloak.protocol.saml;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
@@ -36,30 +53,14 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
+import org.keycloak.services.ErrorPage;
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.services.resources.admin.ClientAttributeCertificateResource;
-import org.keycloak.services.ErrorPage;
import org.w3c.dom.Document;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriInfo;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.security.PublicKey;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@@ -67,7 +68,6 @@ import java.util.UUID;
public class SamlProtocol implements LoginProtocol {
protected static final Logger logger = Logger.getLogger(SamlProtocol.class);
-
public static final String ATTRIBUTE_TRUE_VALUE = "true";
public static final String ATTRIBUTE_FALSE_VALUE = "false";
public static final String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE;
@@ -115,7 +115,6 @@ public class SamlProtocol implements LoginProtocol {
protected EventBuilder event;
-
@Override
public SamlProtocol setSession(KeycloakSession session) {
this.session = session;
@@ -135,7 +134,7 @@ public class SamlProtocol implements LoginProtocol {
}
@Override
- public SamlProtocol setHttpHeaders(HttpHeaders headers){
+ public SamlProtocol setHttpHeaders(HttpHeaders headers) {
this.headers = headers;
return this;
}
@@ -146,48 +145,72 @@ public class SamlProtocol implements LoginProtocol {
return this;
}
-
@Override
- public Response cancelLogin(ClientSessionModel clientSession) {
- RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
- if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
- UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
- Map<String, String> params = new HashMap<>();
- params.put("realm", realm.getName());
- params.put("protocol", LOGIN_PROTOCOL);
- params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME));
- session.sessions().removeClientSession(realm, clientSession);
- URI redirect = builder.buildFromMap(params);
- return Response.status(302).location(redirect).build();
- } else {
+ public Response sendError(ClientSessionModel clientSession, Error error) {
+ try {
+ if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
+ if (error == Error.CANCELLED_BY_USER) {
+ UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
+ Map<String, String> params = new HashMap<>();
+ params.put("realm", realm.getName());
+ params.put("protocol", LOGIN_PROTOCOL);
+ params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME));
+ URI redirect = builder.buildFromMap(params);
+ return Response.status(302).location(redirect).build();
+ } else {
+ return ErrorPage.error(session, translateErrorToIdpInitiatedErrorMessage(error));
+ }
+ } else {
+ SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(clientSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get());
+ try {
+ JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE));
+ Document document = builder.buildDocument();
+ if (isPostBinding(clientSession)) {
+ return binding.postBinding(document).response(clientSession.getRedirectUri());
+ } else {
+ return binding.redirectBinding(document).response(clientSession.getRedirectUri());
+ }
+ } 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);
- return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
}
}
- protected String getResponseIssuer(RealmModel realm) {
- return RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString();
+ private JBossSAMLURIConstants translateErrorToSAMLStatus(Error error) {
+ switch (error) {
+ case CANCELLED_BY_USER:
+ case CONSENT_DENIED:
+ return JBossSAMLURIConstants.STATUS_REQUEST_DENIED;
+ case PASSIVE_INTERACTION_REQUIRED:
+ case PASSIVE_LOGIN_REQUIRED:
+ return JBossSAMLURIConstants.STATUS_NO_PASSIVE;
+ default:
+ logger.warn("Untranslated protocol Error: " + error.name() + " so we return default SAML error");
+ return JBossSAMLURIConstants.STATUS_REQUEST_DENIED;
+ }
}
- protected Response getErrorResponse(ClientSessionModel clientSession, String status) {
- SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder()
- .destination(clientSession.getRedirectUri())
- .issuer(getResponseIssuer(realm))
- .status(status);
- try {
- JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
- .relayState(clientSession.getNote(GeneralConstants.RELAY_STATE));
- Document document = builder.buildDocument();
- if (isPostBinding(clientSession)) {
- return binding.postBinding(document).response(clientSession.getRedirectUri());
- } else {
- return binding.redirectBinding(document).response(clientSession.getRedirectUri());
- }
- } catch (Exception e) {
- return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE);
+ private String translateErrorToIdpInitiatedErrorMessage(Error error) {
+ switch (error) {
+ case CONSENT_DENIED:
+ return Messages.CONSENT_DENIED;
+ case PASSIVE_INTERACTION_REQUIRED:
+ case PASSIVE_LOGIN_REQUIRED:
+ return Messages.UNEXPECTED_ERROR_HANDLING_REQUEST;
+ default:
+ logger.warn("Untranslated protocol Error: " + error.name() + " so we return default error message");
+ return Messages.UNEXPECTED_ERROR_HANDLING_REQUEST;
}
}
+ protected String getResponseIssuer(RealmModel realm) {
+ return RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString();
+ }
+
protected boolean isPostBinding(ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || forcePostBinding(client);
@@ -198,8 +221,6 @@ public class SamlProtocol implements LoginProtocol {
return SamlProtocol.SAML_POST_BINDING.equals(note);
}
-
-
protected boolean isLogoutPostBindingForClient(ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
String logoutPostUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
@@ -207,7 +228,8 @@ public class SamlProtocol implements LoginProtocol {
if (logoutPostUrl == null) {
// if we don't have a redirect uri either, return true and default to the admin url + POST binding
- if (logoutRedirectUrl == null) return true;
+ if (logoutRedirectUrl == null)
+ return true;
return false;
}
@@ -218,11 +240,13 @@ public class SamlProtocol implements LoginProtocol {
String bindingType = clientSession.getNote(SAML_BINDING);
// if the login binding was POST, return true
- if (SAML_POST_BINDING.equals(bindingType)) return true;
+ if (SAML_POST_BINDING.equals(bindingType))
+ return true;
- if (logoutRedirectUrl == null) return true; // we don't have a redirect binding url, so use post binding
+ if (logoutRedirectUrl == null)
+ return true; // we don't have a redirect binding url, so use post binding
- return false; // redirect binding
+ return false; // redirect binding
}
@@ -248,7 +272,8 @@ public class SamlProtocol implements LoginProtocol {
nameIdFormat = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get();
}
}
- if(nameIdFormat == null) return SAML_DEFAULT_NAMEID_FORMAT;
+ if (nameIdFormat == null)
+ return SAML_DEFAULT_NAMEID_FORMAT;
return nameIdFormat;
}
@@ -259,20 +284,21 @@ public class SamlProtocol implements LoginProtocol {
protected String getNameId(String nameIdFormat, ClientSessionModel clientSession, UserSessionModel userSession) {
if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
return userSession.getUser().getEmail();
- } else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) {
+ } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) {
// "G-" stands for "generated" Add this for the slight possibility of collisions.
return "G-" + UUID.randomUUID().toString();
- } else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())) {
+ } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())) {
// generate a persistent user id specifically for each client.
UserModel user = userSession.getUser();
String name = SAML_PERSISTENT_NAME_ID_FOR + "." + clientSession.getClient().getClientId();
String samlPersistentId = user.getFirstAttribute(name);
- if (samlPersistentId != null) return samlPersistentId;
+ if (samlPersistentId != null)
+ return samlPersistentId;
// "G-" stands for "generated"
samlPersistentId = "G-" + UUID.randomUUID().toString();
user.setSingleAttribute(name, samlPersistentId);
return samlPersistentId;
- } else if(nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())){
+ } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) {
// TODO: Support for persistent NameID (pseudo-random identifier persisted in user object)
return userSession.getUser().getUsername();
} else {
@@ -297,15 +323,8 @@ 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())
- .requestIssuer(clientSession.getClient().getClientId())
- .nameIdentifier(nameIdFormat, nameId)
- .authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get());
+ builder.requestID(requestID).destination(redirectUri).issuer(responseIssuer).assertionExpiration(realm.getAccessCodeLifespan()).subjectExpiration(realm.getAccessTokenLifespan()).sessionIndex(clientSession.getId())
+ .requestIssuer(clientSession.getClient().getClientId()).nameIdentifier(nameIdFormat, nameId).authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get());
if (!includeAuthnStatement(client)) {
builder.disableAuthnStatement(true);
}
@@ -317,20 +336,20 @@ public class SamlProtocol implements LoginProtocol {
Set<ProtocolMapperModel> mappings = accessCode.getRequestedProtocolMappers();
for (ProtocolMapperModel mapping : mappings) {
- ProtocolMapper mapper = (ProtocolMapper)session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
- if (mapper == null) continue;
+ ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
+ if (mapper == null)
+ continue;
if (mapper instanceof SAMLAttributeStatementMapper) {
- attributeStatementMappers.add(new ProtocolMapperProcessor<SAMLAttributeStatementMapper>((SAMLAttributeStatementMapper)mapper, mapping));
+ attributeStatementMappers.add(new ProtocolMapperProcessor<SAMLAttributeStatementMapper>((SAMLAttributeStatementMapper) mapper, mapping));
}
if (mapper instanceof SAMLLoginResponseMapper) {
- loginResponseMappers.add(new ProtocolMapperProcessor<SAMLLoginResponseMapper>((SAMLLoginResponseMapper)mapper, mapping));
+ loginResponseMappers.add(new ProtocolMapperProcessor<SAMLLoginResponseMapper>((SAMLLoginResponseMapper) mapper, mapping));
}
if (mapper instanceof SAMLRoleListMapper) {
- roleListMapper = new ProtocolMapperProcessor<SAMLRoleListMapper>((SAMLRoleListMapper)mapper, mapping);
+ roleListMapper = new ProtocolMapperProcessor<SAMLRoleListMapper>((SAMLRoleListMapper) mapper, mapping);
}
}
-
Document samlDocument = null;
try {
ResponseType samlModel = builder.buildModel();
@@ -351,18 +370,14 @@ public class SamlProtocol implements LoginProtocol {
if (canonicalization != null) {
bindingBuilder.canonicalizationMethod(canonicalization);
}
- bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client))
- .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
- .signDocument();
+ bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
}
if (requiresAssertionSignature(client)) {
String canonicalization = client.getAttribute(SAML_CANONICALIZATION_METHOD_ATTRIBUTE);
if (canonicalization != null) {
bindingBuilder.canonicalizationMethod(canonicalization);
}
- bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client))
- .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
- .signAssertions();
+ bindingBuilder.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signAssertions();
}
if (requiresEncryption(client)) {
PublicKey publicKey = null;
@@ -402,7 +417,8 @@ public class SamlProtocol implements LoginProtocol {
String alg = client.getAttribute(SAML_SIGNATURE_ALGORITHM);
if (alg != null) {
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg);
- if (algorithm != null) return algorithm;
+ if (algorithm != null)
+ return algorithm;
}
return SignatureAlgorithm.RSA_SHA256;
}
@@ -421,10 +437,8 @@ public class SamlProtocol implements LoginProtocol {
}
}
- public void transformAttributeStatement(List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers,
- ResponseType response,
- KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession) {
+ public void transformAttributeStatement(List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers, ResponseType response, KeycloakSession session, UserSessionModel userSession,
+ ClientSessionModel clientSession) {
AssertionType assertion = response.getAssertions().get(0).getAssertion();
AttributeStatementType attributeStatement = new AttributeStatementType();
@@ -432,50 +446,32 @@ public class SamlProtocol implements LoginProtocol {
processor.mapper.transformAttributeStatement(attributeStatement, processor.model, session, userSession, clientSession);
}
- //SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute
- if(attributeStatement.getAttributes().size() > 0) {
+ // SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute
+ if (attributeStatement.getAttributes().size() > 0) {
assertion.addStatement(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, ClientSessionModel clientSession) {
for (ProtocolMapperProcessor<SAMLLoginResponseMapper> processor : mappers) {
response = processor.mapper.transformLoginResponse(response, processor.model, session, userSession, clientSession);
}
return response;
}
- public void populateRoles(ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper,
- ResponseType response,
- KeycloakSession session,
- UserSessionModel userSession, ClientSessionModel clientSession) {
- if (roleListMapper == null) return;
+ public void populateRoles(ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper, ResponseType response, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ if (roleListMapper == null)
+ return;
AssertionType assertion = response.getAssertions().get(0).getAssertion();
AttributeStatementType attributeStatement = new AttributeStatementType();
roleListMapper.mapper.mapRoles(attributeStatement, roleListMapper.model, session, userSession, clientSession);
- //SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute
- if(attributeStatement.getAttributes().size() > 0) {
+ // SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute
+ if (attributeStatement.getAttributes().size() > 0) {
assertion.addStatement(attributeStatement);
}
}
-
- @Override
- public Response consentDenied(ClientSessionModel clientSession) {
- RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
- if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
- session.sessions().removeClientSession(realm, clientSession);
- return ErrorPage.error(session, Messages.CONSENT_DENIED);
- } else {
- session.sessions().removeClientSession(realm, clientSession);
- return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
- }
- }
-
public static String getLogoutServiceUrl(UriInfo uriInfo, ClientModel client, String bindingType) {
String logoutServiceUrl = null;
if (SAML_POST_BINDING.equals(bindingType)) {
@@ -483,8 +479,10 @@ 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.trim().equals("")) return null;
+ if (logoutServiceUrl == null && client instanceof ClientModel)
+ logoutServiceUrl = ((ClientModel) client).getManagementUrl();
+ if (logoutServiceUrl == null || logoutServiceUrl.trim().equals(""))
+ return null;
return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), client.getRootUrl(), logoutServiceUrl);
}
@@ -492,7 +490,8 @@ public class SamlProtocol implements LoginProtocol {
@Override
public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
- if (!(client instanceof ClientModel)) return null;
+ if (!(client instanceof ClientModel))
+ return null;
try {
if (isLogoutPostBindingForClient(clientSession)) {
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
@@ -541,9 +540,7 @@ public class SamlProtocol implements LoginProtocol {
if (canonicalization != null) {
binding.canonicalizationMethod(canonicalization);
}
- binding.signatureAlgorithm(algorithm)
- .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
- .signDocument();
+ binding.signatureAlgorithm(algorithm).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
}
try {
@@ -561,8 +558,6 @@ public class SamlProtocol implements LoginProtocol {
}
}
-
-
@Override
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
@@ -573,7 +568,6 @@ public class SamlProtocol implements LoginProtocol {
}
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(logoutUrl, clientSession, client);
-
String logoutRequestString = null;
try {
JaxrsSAML2BindingBuilder binding = createBindingBuilder(client);
@@ -583,20 +577,21 @@ public class SamlProtocol implements LoginProtocol {
return;
}
-
HttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
for (int i = 0; i < 2; i++) { // follow redirects once
try {
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString));
- formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink todo remove this
+ formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink
+ // todo remove
+ // this
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
HttpPost post = new HttpPost(logoutUrl);
post.setEntity(form);
HttpResponse response = httpClient.execute(post);
try {
int status = response.getStatusLine().getStatusCode();
- if (status == 302 && !logoutUrl.endsWith("/")) {
+ if (status == 302 && !logoutUrl.endsWith("/")) {
String redirect = response.getFirstHeader(HttpHeaders.LOCATION).getValue();
String withSlash = logoutUrl + "/";
if (withSlash.equals(redirect)) {
@@ -608,7 +603,8 @@ public class SamlProtocol implements LoginProtocol {
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream is = entity.getContent();
- if (is != null) is.close();
+ if (is != null)
+ is.close();
}
}
@@ -622,21 +618,15 @@ public class SamlProtocol implements LoginProtocol {
protected SAML2LogoutRequestBuilder createLogoutRequest(String logoutUrl, ClientSessionModel clientSession, ClientModel client) {
// build userPrincipal with subject used at login
- SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
- .assertionExpiration(realm.getAccessCodeLifespan())
- .issuer(getResponseIssuer(realm))
- .sessionIndex(clientSession.getId())
- .userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT))
- .destination(logoutUrl);
+ SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder().assertionExpiration(realm.getAccessCodeLifespan()).issuer(getResponseIssuer(realm)).sessionIndex(clientSession.getId())
+ .userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT)).destination(logoutUrl);
return logoutBuilder;
}
private JaxrsSAML2BindingBuilder createBindingBuilder(ClientModel client) {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder();
if (requiresRealmSignature(client)) {
- binding.signatureAlgorithm(getSignatureAlgorithm(client))
- .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
- .signDocument();
+ binding.signatureAlgorithm(getSignatureAlgorithm(client)).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
}
return binding;
}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
index c370d67..3402593 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -1,11 +1,23 @@
package org.keycloak.protocol.saml;
+import java.io.InputStream;
+import java.net.URI;
+import java.security.PublicKey;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
import org.jboss.logging.Logger;
-import org.jboss.resteasy.spi.HttpRequest;
-import org.jboss.resteasy.spi.HttpResponse;
-import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
-import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.common.util.StreamUtil;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
@@ -16,15 +28,12 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
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.IdentityProviderModel;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.protocol.RestartLoginCookie;
+import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAMLRequestParser;
@@ -33,33 +42,10 @@ import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.services.ErrorPage;
-import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
-import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.RealmsResource;
-import org.keycloak.common.util.StreamUtil;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.FormParam;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-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.SecurityContext;
-import javax.ws.rs.core.UriInfo;
-import javax.ws.rs.ext.Providers;
-import java.io.InputStream;
-import java.net.URI;
-import java.security.PublicKey;
-import java.util.List;
/**
* Resource class for the oauth/openid connect token service
@@ -67,40 +53,12 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class SamlService {
+public class SamlService extends AuthorizationEndpointBase {
protected static final Logger logger = Logger.getLogger(SamlService.class);
- protected RealmModel realm;
- private EventBuilder event;
- protected AuthenticationManager authManager;
-
- @Context
- protected Providers providers;
- @Context
- protected SecurityContext securityContext;
- @Context
- protected UriInfo uriInfo;
- @Context
- protected HttpHeaders headers;
- @Context
- protected HttpRequest request;
- @Context
- protected HttpResponse response;
- @Context
- protected KeycloakSession session;
- @Context
- protected ClientConnection clientConnection;
-
- /*
- @Context
- protected ResourceContext resourceContext;
- */
-
public SamlService(RealmModel realm, EventBuilder event, AuthenticationManager authManager) {
- this.realm = realm;
- this.event = event;
- this.authManager = authManager;
+ super(realm, event, authManager);
}
public abstract class BindingProtocol {
@@ -184,15 +142,15 @@ public class SamlService {
event.error(Errors.CLIENT_DISABLED);
return ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED);
}
- if ((client instanceof ClientModel) && ((ClientModel) client).isBearerOnly()) {
+ if (client.isBearerOnly()) {
event.event(EventType.LOGIN);
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.BEARER_ONLY);
}
- if (client.isDirectGrantsOnly()) {
+ if (!client.isStandardFlowEnabled()) {
event.event(EventType.LOGIN);
event.error(Errors.NOT_ALLOWED);
- return ErrorPage.error(session, Messages.DIRECT_GRANTS_ONLY);
+ return ErrorPage.error(session, Messages.STANDARD_FLOW_DISABLED);
}
session.getContext().setClient(client);
@@ -243,7 +201,7 @@ public class SamlService {
bindingType = SamlProtocol.SAML_POST_BINDING;
String redirect = null;
URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL();
- if (redirectUri != null && !"null".equals(redirectUri)) { // "null" is for testing purposes
+ if (redirectUri != null && !"null".equals(redirectUri)) { // "null" is for testing purposes
redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client);
} else {
if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) {
@@ -262,7 +220,6 @@ public class SamlService {
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
}
-
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL);
clientSession.setRedirectUri(redirect);
@@ -286,13 +243,9 @@ public class SamlService {
}
}
- return newBrowserAuthentication(clientSession);
+ return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive());
}
-
-
-
-
private String getBindingType(AuthnRequestType requestAbstractType) {
URI requestedProtocolBinding = requestAbstractType.getProtocolBinding();
@@ -308,10 +261,8 @@ public class SamlService {
}
private boolean isSupportedNameIdFormat(String nameIdFormat) {
- if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()) ||
- nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()) ||
- nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get()) ||
- nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) {
+ if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()) || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()) || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())
+ || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) {
return true;
}
return false;
@@ -340,7 +291,8 @@ public class SamlService {
userSession.setNote(SamlProtocol.SAML_LOGOUT_SIGNATURE_ALGORITHM, SamlProtocol.getSignatureAlgorithm(client).toString());
}
- if (relayState != null) userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
+ if (relayState != null)
+ userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, client.getAttribute(SamlProtocol.SAML_CANONICALIZATION_METHOD_ATTRIBUTE));
@@ -356,7 +308,8 @@ public class SamlService {
} else if (logoutRequest.getSessionIndex() != null) {
for (String sessionIndex : logoutRequest.getSessionIndex()) {
ClientSessionModel clientSession = session.sessions().getClientSession(realm, sessionIndex);
- if (clientSession == null) continue;
+ if (clientSession == null)
+ continue;
UserSessionModel userSession = clientSession.getUserSession();
if (clientSession.getClient().getClientId().equals(client.getClientId())) {
// remove requesting client from logout
@@ -391,13 +344,10 @@ public class SamlService {
builder.logoutRequestID(logoutRequest.getID());
builder.destination(logoutBindingUri);
builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
- JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
- .relayState(logoutRelayState);
+ JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(logoutRelayState);
if (SamlProtocol.requiresRealmSignature(client)) {
SignatureAlgorithm algorithm = SamlProtocol.getSignatureAlgorithm(client);
- binding.signatureAlgorithm(algorithm)
- .signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
- .signDocument();
+ binding.signatureAlgorithm(algorithm).signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()).signDocument();
}
try {
@@ -420,7 +370,6 @@ public class SamlService {
}
}
-
protected class PostBindingProtocol extends BindingProtocol {
@Override
@@ -443,12 +392,14 @@ public class SamlService {
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);
+ if (response != null)
+ return response;
+ if (samlRequest != null)
+ return handleSamlRequest(samlRequest, relayState);
+ else
+ return handleSamlResponse(samlResponse, relayState);
}
}
@@ -464,7 +415,6 @@ public class SamlService {
SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo, GeneralConstants.SAML_REQUEST_KEY);
}
-
@Override
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
return SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
@@ -480,74 +430,35 @@ public class SamlService {
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);
+ if (response != null)
+ return response;
+ if (samlRequest != null)
+ return handleSamlRequest(samlRequest, relayState);
+ else
+ return handleSamlResponse(samlResponse, relayState);
}
}
-
- private Response buildRedirectToIdentityProvider(String providerId, String accessCode) {
- logger.debug("Automatically redirect to identity provider: " + providerId);
- return Response.temporaryRedirect(
- Urls.identityProviderAuthnRequest(uriInfo.getBaseUri(), providerId, realm.getName(), accessCode))
- .build();
- }
-
- protected Response newBrowserAuthentication(ClientSessionModel clientSession) {
- List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
- for (IdentityProviderModel identityProvider : identityProviders) {
- if (identityProvider.isAuthenticateByDefault()) {
- return buildRedirectToIdentityProvider(identityProvider.getAlias(), new ClientSessionCode(realm, clientSession).getCode() );
- }
- }
- AuthenticationFlowModel flow = realm.getBrowserFlow();
- String flowId = flow.getId();
- AuthenticationProcessor processor = new AuthenticationProcessor();
- processor.setClientSession(clientSession)
- .setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
- .setFlowId(flowId)
- .setBrowserFlow(true)
- .setConnection(clientConnection)
- .setEventBuilder(event)
- .setProtector(authManager.getProtector())
- .setRealm(realm)
- .setSession(session)
- .setUriInfo(uriInfo)
- .setRequest(request);
-
- try {
- RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession);
- return processor.authenticate();
- } catch (Exception e) {
- return processor.handleBrowserException(e);
- }
+ protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive) {
+ return handleBrowserAuthenticationRequest(clientSession, new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo), isPassive);
}
-
-
/**
*/
@GET
- public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
- @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
- @QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
+ public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
logger.debug("SAML GET");
return new RedirectBindingProtocol().execute(samlRequest, samlResponse, relayState);
}
-
/**
*/
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
- public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
- @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
- @FormParam(GeneralConstants.RELAY_STATE) String relayState) {
+ public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @FormParam(GeneralConstants.RELAY_STATE) String relayState) {
logger.debug("SAML POST");
return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState);
}
@@ -570,13 +481,13 @@ public class SamlService {
@GET
@Path("clients/{client}")
@Produces(MediaType.TEXT_HTML)
- public Response idpInitiatedSSO(@PathParam("client") String clientUrlName,
- @QueryParam("RelayState") String relayState) {
+ public Response idpInitiatedSSO(@PathParam("client") String clientUrlName, @QueryParam("RelayState") String relayState) {
event.event(EventType.LOGIN);
ClientModel client = null;
for (ClientModel c : realm.getClients()) {
String urlName = c.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME);
- if (urlName == null) continue;
+ if (urlName == null)
+ continue;
if (urlName.equals(clientUrlName)) {
client = c;
break;
@@ -586,18 +497,14 @@ public class SamlService {
event.error(Errors.CLIENT_NOT_FOUND);
return ErrorPage.error(session, Messages.CLIENT_NOT_FOUND);
}
- if (client.getManagementUrl() == null
- && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null
- && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) == null) {
+ 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);
}
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) {
+ 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;
}
@@ -626,8 +533,7 @@ public class SamlService {
clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
}
-
- return newBrowserAuthentication(clientSession);
+ return newBrowserAuthentication(clientSession, false);
}
diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
index 163fed5..df62632 100755
--- a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
+++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
@@ -5,5 +5,6 @@ org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper
org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper
org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper
org.keycloak.protocol.saml.mappers.UserSessionNoteStatementMapper
+org.keycloak.protocol.saml.mappers.GroupMembershipMapper
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 951bb94..710c77d 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -21,6 +21,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
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.managers.AuthenticationManager;
@@ -469,8 +470,9 @@ public class AuthenticationProcessor {
LoginProtocol protocol = getSession().getProvider(LoginProtocol.class, getClientSession().getAuthMethod());
protocol.setRealm(getRealm())
.setHttpHeaders(getHttpRequest().getHttpHeaders())
- .setUriInfo(getUriInfo());
- Response response = protocol.cancelLogin(getClientSession());
+ .setUriInfo(getUriInfo())
+ .setEventBuilder(event);
+ Response response = protocol.sendError(getClientSession(), Error.CANCELLED_BY_USER);
forceChallenge(response);
}
@@ -753,7 +755,7 @@ public class AuthenticationProcessor {
if (!code.isValidAction(action)) {
throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CLIENT_SESSION);
}
- if (!code.isActionActive(action)) {
+ if (!code.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
throw new AuthenticationFlowException(AuthenticationFlowError.EXPIRED_CODE);
}
clientSession.setTimestamp(Time.currentTime());
@@ -807,7 +809,7 @@ public class AuthenticationProcessor {
public Response finishAuthentication() {
event.success();
RealmModel realm = clientSession.getRealm();
- return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, connection);
+ return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, connection, event);
}
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 ae28d3e..9780d6a 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
@@ -13,7 +13,7 @@ import org.keycloak.authentication.requiredactions.VerifyEmail;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.email.EmailException;
-import org.keycloak.email.EmailProvider;
+import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@@ -68,10 +68,10 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
try {
- context.getSession().getProvider(EmailProvider.class)
+ context.getSession().getProvider(EmailTemplateProvider.class)
.setRealm(realm)
.setUser(existingUser)
- .setAttribute(EmailProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
+ .setAttribute(EmailTemplateProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
.sendConfirmIdentityBrokerLink(link, expiration);
event.success();
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 0506f21..5afaa3d 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
@@ -148,24 +148,17 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> inputData) {
List<UserCredentialModel> credentials = new LinkedList<>();
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
- if (password == null || password.isEmpty()) {
- invalidPassword(context, user);
- return false;
- }
credentials.add(UserCredentialModel.password(password));
boolean valid = context.getSession().users().validCredentials(context.getRealm(), user, credentials);
if (!valid) {
- invalidPassword(context, user);
+ context.getEvent().user(user);
+ context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
+ Response challengeResponse = invalidCredentials(context);
+ context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse);
+ context.clearUser();
return false;
}
return true;
}
- private void invalidPassword(AuthenticationFlowContext context, UserModel user) {
- context.getEvent().user(user);
- context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
- Response challengeResponse = invalidCredentials(context);
- context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse);
- context.clearUser();
- }
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java
index cff7f37..21aa18d 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java
@@ -31,15 +31,6 @@ public class ValidatePassword extends AbstractDirectGrantAuthenticator {
MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
List<UserCredentialModel> credentials = new LinkedList<>();
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
- if (password == null || password.isEmpty()) {
- if (context.getUser() != null) {
- context.getEvent().user(context.getUser());
- }
- context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
- Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");
- context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
- return;
- }
credentials.add(UserCredentialModel.password(password));
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
if (!valid) {
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 a3538aa..e48d194 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
@@ -8,29 +8,20 @@ import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
-import org.keycloak.email.EmailException;
-import org.keycloak.email.EmailProvider;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
-import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
-import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.provider.ProviderConfigProperty;
-import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
import java.util.List;
-import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
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 678aac2..a66126d 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
@@ -8,7 +8,7 @@ import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.email.EmailException;
-import org.keycloak.email.EmailProvider;
+import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@@ -72,7 +72,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
try {
- context.getSession().getProvider(EmailProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expiration);
+ 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)
diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
new file mode 100644
index 0000000..1a06877
--- /dev/null
+++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
@@ -0,0 +1,102 @@
+package org.keycloak.email;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+
+import javax.mail.Message;
+import javax.mail.Multipart;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import java.util.Date;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class DefaultEmailSenderProvider implements EmailSenderProvider {
+
+ private static final Logger log = Logger.getLogger(DefaultEmailSenderProvider.class);
+
+ @Override
+ public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
+ try {
+ String address = user.getEmail();
+ Map<String, String> config = realm.getSmtpConfig();
+
+ Properties props = new Properties();
+ props.setProperty("mail.smtp.host", config.get("host"));
+
+ boolean auth = "true".equals(config.get("auth"));
+ boolean ssl = "true".equals(config.get("ssl"));
+ boolean starttls = "true".equals(config.get("starttls"));
+
+ if (config.containsKey("port")) {
+ props.setProperty("mail.smtp.port", config.get("port"));
+ }
+
+ if (auth) {
+ props.setProperty("mail.smtp.auth", "true");
+ }
+
+ if (ssl) {
+ props.setProperty("mail.smtp.ssl.enable", "true");
+ }
+
+ if (starttls) {
+ props.setProperty("mail.smtp.starttls.enable", "true");
+ }
+
+ props.setProperty("mail.smtp.timeout", "10000");
+ props.setProperty("mail.smtp.connectiontimeout", "10000");
+
+ String from = config.get("from");
+
+ Session session = Session.getInstance(props);
+
+ Multipart multipart = new MimeMultipart("alternative");
+
+ if(textBody != null) {
+ MimeBodyPart textPart = new MimeBodyPart();
+ textPart.setText(textBody, "UTF-8");
+ multipart.addBodyPart(textPart);
+ }
+
+ if(htmlBody != null) {
+ MimeBodyPart htmlPart = new MimeBodyPart();
+ htmlPart.setContent(htmlBody, "text/html; charset=UTF-8");
+ multipart.addBodyPart(htmlPart);
+ }
+
+ Message msg = new MimeMessage(session);
+ msg.setFrom(new InternetAddress(from));
+ msg.setHeader("To", address);
+ msg.setSubject(subject);
+ msg.setContent(multipart);
+ msg.saveChanges();
+ msg.setSentDate(new Date());
+
+ Transport transport = session.getTransport("smtp");
+ if (auth) {
+ transport.connect(config.get("user"), config.get("password"));
+ } else {
+ transport.connect();
+ }
+ transport.sendMessage(msg, new InternetAddress[]{new InternetAddress(address)});
+ } catch (Exception e) {
+ log.error("Failed to send email", e);
+ throw new EmailException(e);
+ }
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProviderFactory.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProviderFactory.java
new file mode 100644
index 0000000..9677000
--- /dev/null
+++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProviderFactory.java
@@ -0,0 +1,34 @@
+package org.keycloak.email;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class DefaultEmailSenderProviderFactory implements EmailSenderProviderFactory {
+
+ @Override
+ public EmailSenderProvider create(KeycloakSession session) {
+ return new DefaultEmailSenderProvider();
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return "default";
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/exportimport/ExportImportManager.java b/services/src/main/java/org/keycloak/exportimport/ExportImportManager.java
index 89dc097..cedcbdd 100644
--- a/services/src/main/java/org/keycloak/exportimport/ExportImportManager.java
+++ b/services/src/main/java/org/keycloak/exportimport/ExportImportManager.java
@@ -60,7 +60,8 @@ public class ExportImportManager {
// Check if master realm was exported. If it's not, then it needs to be created before other realms are imported
if (!importProvider.isMasterRealmExported()) {
- new ApplianceBootstrap().bootstrap(sessionFactory, contextPath);
+ ApplianceBootstrap.setupDefaultRealm(sessionFactory, contextPath);
+ ApplianceBootstrap.setupDefaultUser(sessionFactory);
}
importProvider.importModel(sessionFactory, strategy);
@@ -69,7 +70,8 @@ public class ExportImportManager {
if (!realmName.equals(Config.getAdminRealm())) {
// Check if master realm exists. If it's not, then it needs to be created before other realm is imported
- new ApplianceBootstrap().bootstrap(sessionFactory, contextPath);
+ ApplianceBootstrap.setupDefaultRealm(sessionFactory, contextPath);
+ ApplianceBootstrap.setupDefaultUser(sessionFactory);
}
importProvider.importRealm(sessionFactory, realmName, strategy);
diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
new file mode 100644
index 0000000..a1fc4a7
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
@@ -0,0 +1,137 @@
+package org.keycloak.protocol;
+
+import java.util.List;
+
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+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.EventBuilder;
+import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.LoginProtocol.Error;
+import org.keycloak.services.Urls;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.resources.LoginActionsService;
+
+/**
+ * Common base class for Authorization REST endpoints implementation, which have to be implemented by each protocol.
+ *
+ * @author Vlastimil Elias (velias at redhat dot com)
+ */
+public abstract class AuthorizationEndpointBase {
+
+ private static final Logger logger = Logger.getLogger(AuthorizationEndpointBase.class);
+
+ protected RealmModel realm;
+ protected EventBuilder event;
+ protected AuthenticationManager authManager;
+
+ @Context
+ protected UriInfo uriInfo;
+ @Context
+ protected HttpHeaders headers;
+ @Context
+ protected HttpRequest request;
+ @Context
+ protected KeycloakSession session;
+ @Context
+ protected ClientConnection clientConnection;
+
+ public AuthorizationEndpointBase(RealmModel realm, EventBuilder event, AuthenticationManager authManager) {
+ this.realm = realm;
+ this.event = event;
+ this.authManager = authManager;
+ }
+
+ protected AuthenticationProcessor createProcessor(ClientSessionModel clientSession, String flowId, String flowPath) {
+ AuthenticationProcessor processor = new AuthenticationProcessor();
+ processor.setClientSession(clientSession)
+ .setFlowPath(flowPath)
+ .setFlowId(flowId)
+ .setBrowserFlow(true)
+ .setConnection(clientConnection)
+ .setEventBuilder(event)
+ .setProtector(authManager.getProtector())
+ .setRealm(realm)
+ .setSession(session)
+ .setUriInfo(uriInfo)
+ .setRequest(request);
+ return processor;
+ }
+
+ /**
+ * Common method to handle browser authentication request in protocols unified way.
+ *
+ * @param clientSession 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)
+ * @return response to be returned to the browser
+ */
+ protected Response handleBrowserAuthenticationRequest(ClientSessionModel clientSession, LoginProtocol protocol, boolean isPassive) {
+
+ List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
+ for (IdentityProviderModel identityProvider : identityProviders) {
+ if (identityProvider.isAuthenticateByDefault()) {
+ // TODO if we are isPassive we should propagate this flag to default identity provider also if possible
+ return buildRedirectToIdentityProvider(identityProvider.getAlias(), new ClientSessionCode(realm, clientSession).getCode());
+ }
+ }
+
+ AuthenticationFlowModel flow = realm.getBrowserFlow();
+ String flowId = flow.getId();
+ AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
+
+ 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
+ Response challenge = null;
+ Response challenge2 = null;
+ try {
+ challenge = processor.authenticateOnly();
+ if (challenge == null) {
+ challenge2 = processor.attachSessionExecutionRequiredActions();
+ }
+ } catch (Exception e) {
+ return processor.handleBrowserException(e);
+ }
+
+ if (challenge != null || challenge2 != null) {
+ if (processor.isUserSessionCreated()) {
+ session.sessions().removeUserSession(realm, processor.getUserSession());
+ }
+ if (challenge != null)
+ return protocol.sendError(clientSession, Error.PASSIVE_LOGIN_REQUIRED);
+ else
+ return protocol.sendError(clientSession, Error.PASSIVE_INTERACTION_REQUIRED);
+ } else {
+ return processor.finishAuthentication();
+ }
+ } else {
+ try {
+ RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession);
+ return processor.authenticate();
+ } catch (Exception e) {
+ return processor.handleBrowserException(e);
+ }
+ }
+ }
+
+ protected Response buildRedirectToIdentityProvider(String providerId, String accessCode) {
+ logger.debug("Automatically redirect to identity provider: " + providerId);
+ return Response.temporaryRedirect(
+ Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), providerId, this.realm.getName(), accessCode))
+ .build();
+ }
+
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/protocol/LoginProtocol.java b/services/src/main/java/org/keycloak/protocol/LoginProtocol.java
index 4a7836d..5cc4503 100755
--- a/services/src/main/java/org/keycloak/protocol/LoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/LoginProtocol.java
@@ -17,6 +17,28 @@ import javax.ws.rs.core.UriInfo;
* @version $Revision: 1 $
*/
public interface LoginProtocol extends Provider {
+
+ public static enum Error {
+
+ /**
+ * Login cancelled by the user
+ */
+ CANCELLED_BY_USER,
+ /**
+ * Consent denied by the user
+ */
+ CONSENT_DENIED,
+ /**
+ * Passive authentication mode requested but nobody is logged in
+ */
+ PASSIVE_LOGIN_REQUIRED,
+ /**
+ * Passive authentication mode requested, user is logged in, but some other user interaction is necessary (eg. some required login actions exist or Consent approval is necessary for logged in
+ * user)
+ */
+ PASSIVE_INTERACTION_REQUIRED;
+ }
+
LoginProtocol setSession(KeycloakSession session);
LoginProtocol setRealm(RealmModel realm);
@@ -27,11 +49,12 @@ public interface LoginProtocol extends Provider {
LoginProtocol setEventBuilder(EventBuilder event);
- Response cancelLogin(ClientSessionModel clientSession);
Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode);
- Response consentDenied(ClientSessionModel clientSession);
+
+ Response sendError(ClientSessionModel clientSession, Error error);
void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession);
Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession);
Response finishLogout(UserSessionModel userSession);
+
}
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 e013857..9ed45de 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
@@ -1,9 +1,10 @@
package org.keycloak.protocol.oidc.endpoints;
+import javax.ws.rs.GET;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
import org.jboss.logging.Logger;
-import org.jboss.resteasy.spi.HttpRequest;
-import org.keycloak.common.ClientConnection;
-import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details;
@@ -15,11 +16,12 @@ import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.IdentityProviderModel;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.protocol.RestartLoginCookie;
+import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.Urls;
@@ -28,54 +30,30 @@ import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
-import javax.ws.rs.GET;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriInfo;
-import java.util.List;
-
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class AuthorizationEndpoint {
+public class AuthorizationEndpoint extends AuthorizationEndpointBase {
private static final Logger logger = Logger.getLogger(AuthorizationEndpoint.class);
+
public static final String CODE_AUTH_TYPE = "code";
private enum Action {
REGISTER, CODE, FORGOT_CREDENTIALS
}
- @Context
- private KeycloakSession session;
-
- @Context
- private HttpRequest request;
-
- @Context
- private HttpHeaders headers;
-
- @Context
- private UriInfo uriInfo;
-
- @Context
- private ClientConnection clientConnection;
-
- private final AuthenticationManager authManager;
- private final RealmModel realm;
- private final EventBuilder event;
-
private ClientModel client;
private ClientSessionModel clientSession;
private Action action;
+ private OIDCResponseType parsedResponseType;
private String clientId;
private String redirectUri;
private String redirectUriParam;
private String responseType;
+ private String responseMode;
private String state;
private String scope;
private String loginHint;
@@ -86,9 +64,7 @@ public class AuthorizationEndpoint {
private String legacyResponseType;
public AuthorizationEndpoint(AuthenticationManager authManager, RealmModel realm, EventBuilder event) {
- this.authManager = authManager;
- this.realm = realm;
- this.event = event;
+ super(realm, event, authManager);
event.event(EventType.LOGIN);
}
@@ -98,6 +74,7 @@ public class AuthorizationEndpoint {
clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM);
responseType = params.getFirst(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ responseMode = params.getFirst(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
redirectUriParam = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM);
state = params.getFirst(OIDCLoginProtocol.STATE_PARAM);
scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM);
@@ -108,8 +85,8 @@ public class AuthorizationEndpoint {
checkSsl();
checkRealm();
- checkClient();
checkResponseType();
+ checkClient();
checkRedirectUri();
createClientSession();
@@ -190,9 +167,14 @@ public class AuthorizationEndpoint {
throw new ErrorPageException(session, Messages.BEARER_ONLY);
}
- if (client.isDirectGrantsOnly()) {
+ if ((parsedResponseType.hasResponseType(OIDCResponseType.CODE) || parsedResponseType.hasResponseType(OIDCResponseType.NONE)) && !client.isStandardFlowEnabled()) {
event.error(Errors.NOT_ALLOWED);
- throw new ErrorPageException(session, Messages.DIRECT_GRANTS_ONLY);
+ throw new ErrorPageException(session, Messages.STANDARD_FLOW_DISABLED);
+ }
+
+ if (parsedResponseType.isImplicitOrHybridFlow() && !client.isImplicitFlowEnabled()) {
+ event.error(Errors.NOT_ALLOWED);
+ throw new ErrorPageException(session, Messages.IMPLICIT_FLOW_DISABLED);
}
session.getContext().setClient(client);
@@ -210,14 +192,32 @@ public class AuthorizationEndpoint {
event.detail(Details.RESPONSE_TYPE, responseType);
- if (responseType.equals(OAuth2Constants.CODE)) {
+ try {
+ parsedResponseType = OIDCResponseType.parse(responseType);
if (action == null) {
action = Action.CODE;
}
- } else {
+ } catch (IllegalArgumentException iae) {
+ logger.error(iae.getMessage());
event.error(Errors.INVALID_REQUEST);
throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
}
+
+ try {
+ OIDCResponseMode parsedResponseMode = OIDCResponseMode.parse(responseMode, parsedResponseType);
+ event.detail(Details.RESPONSE_MODE, parsedResponseMode.toString().toLowerCase());
+
+ // Disallowed by OIDC specs
+ if (parsedResponseType.isImplicitOrHybridFlow() && parsedResponseMode == OIDCResponseMode.QUERY) {
+ logger.error("Response_mode 'query' not allowed for implicit or hybrid flow");
+ event.error(Errors.INVALID_REQUEST);
+ throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ }
+
+ } catch (IllegalArgumentException iae) {
+ event.error(Errors.INVALID_REQUEST);
+ throw new ErrorPageException(session, Messages.INVALID_PARAMETER, OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ }
}
private void checkRedirectUri() {
@@ -246,10 +246,10 @@ public class AuthorizationEndpoint {
if (loginHint != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint);
if (prompt != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, prompt);
if (idpHint != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, idpHint);
+ if (responseMode != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, responseMode);
}
private Response buildAuthorizationCodeAuthorizationResponse() {
- String accessCode = new ClientSessionCode(realm, clientSession).getCode();
if (idpHint != null && !"".equals(idpHint)) {
IdentityProviderModel identityProviderModel = realm.getIdentityProviderByAlias(idpHint);
@@ -259,65 +259,13 @@ public class AuthorizationEndpoint {
.setError(Messages.IDENTITY_PROVIDER_NOT_FOUND, idpHint)
.createErrorPage();
}
- return buildRedirectToIdentityProvider(idpHint, accessCode);
+ return buildRedirectToIdentityProvider(idpHint, new ClientSessionCode(realm, clientSession).getCode());
}
- return browserAuthentication(accessCode);
- }
-
- protected Response browserAuthentication(String accessCode) {
this.event.event(EventType.LOGIN);
- List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
- for (IdentityProviderModel identityProvider : identityProviders) {
- if (identityProvider.isAuthenticateByDefault()) {
- return buildRedirectToIdentityProvider(identityProvider.getAlias(), accessCode);
- }
- }
clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);
-
- AuthenticationFlowModel flow = realm.getBrowserFlow();
- String flowId = flow.getId();
- AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.AUTHENTICATE_PATH);
-
- if (prompt != null && prompt.equals("none")) {
- // OIDC prompt == NONE
- // This means that client is just checking if the user is already completely logged in.
- //
- // here we cancel login if any authentication action or required action is required
- Response challenge = null;
- try {
- challenge = processor.authenticateOnly();
- if (challenge == null) {
- challenge = processor.attachSessionExecutionRequiredActions();
- }
- } catch (Exception e) {
- return processor.handleBrowserException(e);
- }
-
- if (challenge != null) {
- if (processor.isUserSessionCreated()) {
- session.sessions().removeUserSession(realm, processor.getUserSession());
- }
- OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo, headers, event);
- return oauth.cancelLogin(clientSession);
- }
-
- if (challenge == null) {
- return processor.finishAuthentication();
- } else {
- RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession);
- return challenge;
- }
- } else {
- try {
- RestartLoginCookie.setRestartCookie(realm, clientConnection, uriInfo, clientSession);
- return processor.authenticate();
- } catch (Exception e) {
- return processor.handleBrowserException(e);
- }
-
- }
+ return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), prompt != null && prompt.equals("none"));
}
private Response buildRegister() {
@@ -326,7 +274,7 @@ public class AuthorizationEndpoint {
AuthenticationFlowModel flow = realm.getRegistrationFlow();
String flowId = flow.getId();
- AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.REGISTRATION_PATH);
+ AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.REGISTRATION_PATH);
return processor.authenticate();
}
@@ -337,32 +285,12 @@ public class AuthorizationEndpoint {
AuthenticationFlowModel flow = realm.getResetCredentialsFlow();
String flowId = flow.getId();
- AuthenticationProcessor processor = createProcessor(flowId, LoginActionsService.RESET_CREDENTIALS_PATH);
+ AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.RESET_CREDENTIALS_PATH);
return processor.authenticate();
}
- private AuthenticationProcessor createProcessor(String flowId, String flowPath) {
- AuthenticationProcessor processor = new AuthenticationProcessor();
- processor.setClientSession(clientSession)
- .setFlowPath(flowPath)
- .setFlowId(flowId)
- .setBrowserFlow(true)
- .setConnection(clientConnection)
- .setEventBuilder(event)
- .setProtector(authManager.getProtector())
- .setRealm(realm)
- .setSession(session)
- .setUriInfo(uriInfo)
- .setRequest(request);
- return processor;
- }
- private Response buildRedirectToIdentityProvider(String providerId, String accessCode) {
- logger.debug("Automatically redirect to identity provider: " + providerId);
- return Response.temporaryRedirect(
- Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), providerId, this.realm.getName(), accessCode))
- .build();
- }
+
}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
index abea565..3340c35 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java
@@ -12,6 +12,7 @@ import org.keycloak.common.util.UriUtils;
import javax.ws.rs.GET;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@@ -38,13 +39,17 @@ public class LoginStatusIframeEndpoint {
@Produces(MediaType.TEXT_HTML)
public Response getLoginStatusIframe(@QueryParam("client_id") String client_id,
@QueryParam("origin") String origin) {
+ if (client_id == null || origin == null) {
+ throw new WebApplicationException(Response.Status.BAD_REQUEST);
+ }
+
if (!UriUtils.isOrigin(origin)) {
- throw new BadRequestException("Invalid origin");
+ throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
ClientModel client = realm.getClientByClientId(client_id);
if (client == null) {
- throw new NotFoundException("could not find client");
+ throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
InputStream is = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html");
@@ -71,7 +76,7 @@ public class LoginStatusIframeEndpoint {
}
if (!valid) {
- throw new BadRequestException("Invalid origin");
+ throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
try {
@@ -84,7 +89,7 @@ public class LoginStatusIframeEndpoint {
return Response.ok(file).cacheControl(cacheControl).build();
} catch (IOException e) {
- throw new RuntimeException(e);
+ throw new WebApplicationException(e, Response.Status.BAD_REQUEST);
}
}
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 34161d8..fe358e0 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -202,7 +202,7 @@ public class TokenEndpoint {
ClientSessionModel clientSession = accessCode.getClientSession();
event.detail(Details.CODE_ID, clientSession.getId());
- if (!accessCode.isValid(ClientSessionModel.Action.CODE_TO_TOKEN.name())) {
+ if (!accessCode.isValid(ClientSessionModel.Action.CODE_TO_TOKEN.name(), ClientSessionCode.ActionType.CLIENT)) {
event.error(Errors.INVALID_CODE);
throw new ErrorResponseException("invalid_grant", "Code is expired", Response.Status.BAD_REQUEST);
}
@@ -327,7 +327,7 @@ public class TokenEndpoint {
}
public Response buildResourceOwnerPasswordCredentialsGrant() {
- event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token");
+ event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD);
if (client.isConsentRequired()) {
event.error(Errors.CONSENT_DENIED);
@@ -393,7 +393,7 @@ public class TokenEndpoint {
throw new ErrorResponseException("unauthorized_client", "Client not enabled to retrieve service account", Response.Status.UNAUTHORIZED);
}
- event.detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH);
+ event.detail(Details.RESPONSE_TYPE, OAuth2Constants.CLIENT_CREDENTIALS);
UserModel clientUser = session.users().getUserByServiceAccountClient(client);
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
new file mode 100755
index 0000000..848a8e8
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java
@@ -0,0 +1,152 @@
+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.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.ModelToRepresentation;
+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;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Maps user group membership
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class GroupMembershipMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper {
+
+ private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+ static {
+ ProviderConfigProperty property;
+ ProviderConfigProperty property1;
+ property1 = new ProviderConfigProperty();
+ property1.setName(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
+ property1.setLabel(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME_LABEL);
+ property1.setType(ProviderConfigProperty.STRING_TYPE);
+ property1.setDefaultValue("groups");
+ property1.setHelpText(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME_TOOLTIP);
+ configProperties.add(property1);
+ property1 = new ProviderConfigProperty();
+ property1.setName("full.path");
+ property1.setLabel("Full group path");
+ property1.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ property1.setDefaultValue("true");
+ property1.setHelpText("Include full path to group i.e. /top/level1/level2, false will just specify the group name");
+ configProperties.add(property1);
+
+ property1 = new ProviderConfigProperty();
+ property1.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN);
+ property1.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_LABEL);
+ property1.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ property1.setDefaultValue("true");
+ property1.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_HELP_TEXT);
+ configProperties.add(property1);
+ property1 = new ProviderConfigProperty();
+ property1.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN);
+ property1.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_LABEL);
+ property1.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ property1.setDefaultValue("true");
+ property1.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_HELP_TEXT);
+ configProperties.add(property1);
+
+
+
+ }
+
+ public static final String PROVIDER_ID = "oidc-group-membership-mapper";
+
+
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Group Membership";
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return TOKEN_MAPPER_CATEGORY;
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Map user group membership";
+ }
+
+ public static boolean useFullPath(ProtocolMapperModel mappingModel) {
+ return "true".equals(mappingModel.getConfig().get("full.path"));
+ }
+
+
+ @Override
+ public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
+ UserSessionModel userSession, ClientSessionModel clientSession) {
+ if (!OIDCAttributeMapperHelper.includeInAccessToken(mappingModel)) return token;
+ buildMembership(token, mappingModel, userSession);
+ return token;
+ }
+
+ public void buildMembership(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
+ List<String> membership = new LinkedList<>();
+ boolean fullPath = useFullPath(mappingModel);
+ for (GroupModel group : userSession.getUser().getGroups()) {
+ if (fullPath) {
+ membership.add(ModelToRepresentation.buildGroupPath(group));
+ } else {
+ membership.add(group.getName());
+ }
+ }
+ String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
+
+ token.getOtherClaims().put(protocolClaim, membership);
+ }
+
+ @Override
+ public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+ if (!OIDCAttributeMapperHelper.includeInIDToken(mappingModel)) return token;
+ buildMembership(token, mappingModel, userSession);
+ return token;
+ }
+
+ public static ProtocolMapperModel create(String name,
+ String tokenClaimName,
+ boolean consentRequired, String consentText,
+ boolean accessToken, boolean idToken) {
+ ProtocolMapperModel mapper = new ProtocolMapperModel();
+ mapper.setName(name);
+ mapper.setProtocolMapper(PROVIDER_ID);
+ mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ mapper.setConsentRequired(consentRequired);
+ mapper.setConsentText(consentText);
+ Map<String, String> config = new HashMap<String, String>();
+ config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, tokenClaimName);
+ if (accessToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+ if (idToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+ mapper.setConfig(config);
+
+ return mapper;
+ }
+
+
+}
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 246b82e..3e96692 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
@@ -6,6 +6,7 @@ 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;
@@ -84,7 +85,7 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser();
String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
- List<String> attributeValue = user.getAttribute(attributeName);
+ List<String> attributeValue = KeycloakModelUtils.resolveAttribute(user, attributeName);
if (attributeValue == null) return;
OIDCAttributeMapperHelper.mapClaim(token, mappingModel, attributeValue);
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java
index 955dfe4..6caacd6 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java
@@ -5,8 +5,7 @@ import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.protocol.oidc.representations.OIDCClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.clientregistration.oidc.DescriptionConverter;
import org.keycloak.util.JsonSerialization;
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 332a547..40bcc67 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -33,6 +33,10 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie;
+import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
+import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager;
@@ -62,6 +66,8 @@ public class OIDCLoginProtocol implements LoginProtocol {
public static final String LOGOUT_REDIRECT_URI = "OIDC_LOGOUT_REDIRECT_URI";
public static final String ISSUER = "iss";
+ public static final String RESPONSE_MODE_PARAM = "response_mode";
+
private static final Logger log = Logger.getLogger(OIDCLoginProtocol.class);
protected KeycloakSession session;
@@ -74,6 +80,9 @@ public class OIDCLoginProtocol implements LoginProtocol {
protected EventBuilder event;
+ protected OIDCResponseType responseType;
+ protected OIDCResponseMode responseMode;
+
public OIDCLoginProtocol(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, EventBuilder event) {
this.session = session;
this.realm = realm;
@@ -82,8 +91,17 @@ public class OIDCLoginProtocol implements LoginProtocol {
this.event = event;
}
- public OIDCLoginProtocol(){
+ public OIDCLoginProtocol() {
+
+ }
+ private void setupResponseTypeAndMode(ClientSessionModel clientSession) {
+ String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ String responseMode = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ this.responseType = OIDCResponseType.parse(responseType);
+ this.responseMode = OIDCResponseMode.parse(responseMode, this.responseType);
+ this.event.detail(Details.RESPONSE_TYPE, responseType);
+ this.event.detail(Details.RESPONSE_MODE, this.responseMode.toString().toLowerCase());
}
@Override
@@ -105,7 +123,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
}
@Override
- public OIDCLoginProtocol setHttpHeaders(HttpHeaders headers){
+ public OIDCLoginProtocol setHttpHeaders(HttpHeaders headers) {
this.headers = headers;
return this;
}
@@ -116,62 +134,84 @@ public class OIDCLoginProtocol implements LoginProtocol {
return this;
}
- @Override
- public Response cancelLogin(ClientSessionModel clientSession) {
- String redirect = clientSession.getRedirectUri();
- String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM);
- UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, "access_denied");
- if (state != null) {
- redirectUri.queryParam(OAuth2Constants.STATE, state);
- }
- session.sessions().removeClientSession(realm, clientSession);
- RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
- return Response.status(302).location(redirectUri.build()).build();
- }
@Override
public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) {
ClientSessionModel clientSession = accessCode.getClientSession();
+ setupResponseTypeAndMode(clientSession);
+
String redirect = clientSession.getRedirectUri();
+ OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode);
String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM);
- accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN.name());
- UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, accessCode.getCode());
log.debugv("redirectAccessCode: state: {0}", state);
if (state != null)
- redirectUri.queryParam(OAuth2Constants.STATE, state);
- Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
+ redirectUri.addParam(OAuth2Constants.STATE, state);
- return location.build();
+ // Standard or hybrid flow
+ if (responseType.hasResponseType(OIDCResponseType.CODE)) {
+ accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN.name());
+ redirectUri.addParam(OAuth2Constants.CODE, accessCode.getCode());
+ }
+
+ // Implicit or hybrid flow
+ if (responseType.isImplicitOrHybridFlow()) {
+ TokenManager tokenManager = new TokenManager();
+ AccessTokenResponse res = tokenManager.responseBuilder(realm, clientSession.getClient(), event, session, userSession, clientSession)
+ .generateAccessToken()
+ .generateIDToken()
+ .build();
+
+ if (responseType.hasResponseType(OIDCResponseType.ID_TOKEN)) {
+ redirectUri.addParam("id_token", res.getIdToken());
+ }
+
+ if (responseType.hasResponseType(OIDCResponseType.TOKEN)) {
+ redirectUri.addParam("access_token", res.getToken());
+ redirectUri.addParam("token_type", res.getTokenType());
+ redirectUri.addParam("session-state", res.getSessionState());
+ redirectUri.addParam("expires_in", String.valueOf(res.getExpiresIn()));
+ }
+
+ redirectUri.addParam("not-before-policy", String.valueOf(res.getNotBeforePolicy()));
+ }
+
+ return redirectUri.build();
}
- public Response consentDenied(ClientSessionModel clientSession) {
+
+ @Override
+ public Response sendError(ClientSessionModel clientSession, Error error) {
+ setupResponseTypeAndMode(clientSession);
+
String redirect = clientSession.getRedirectUri();
String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM);
- UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, "access_denied");
+ OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode).addParam(OAuth2Constants.ERROR, translateError(error));
if (state != null)
- redirectUri.queryParam(OAuth2Constants.STATE, state);
+ redirectUri.addParam(OAuth2Constants.STATE, state);
session.sessions().removeClientSession(realm, clientSession);
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
- Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
- return location.build();
+ return redirectUri.build();
}
-
- public Response invalidSessionError(ClientSessionModel clientSession) {
- String redirect = clientSession.getRedirectUri();
- String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM);
- UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, "access_denied");
- if (state != null) {
- redirectUri.queryParam(OAuth2Constants.STATE, state);
+ private String translateError(Error error) {
+ switch (error) {
+ case CANCELLED_BY_USER:
+ case CONSENT_DENIED:
+ return "access_denied";
+ case PASSIVE_INTERACTION_REQUIRED:
+ return "interaction_required";
+ case PASSIVE_LOGIN_REQUIRED:
+ return "login_required";
+ default:
+ log.warn("Untranslated protocol Error: " + error.name() + " so we return default SAML error");
+ return "access_denied";
}
- return Response.status(302).location(redirectUri.build()).build();
}
@Override
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
- if (!(clientSession.getClient() instanceof ClientModel)) return;
- ClientModel app = clientSession.getClient();
- new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, app, clientSession);
+ ClientModel client = clientSession.getClient();
+ new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, client, clientSession);
}
@Override
@@ -190,10 +230,10 @@ public class OIDCLoginProtocol implements LoginProtocol {
}
event.user(userSession.getUser()).session(userSession).success();
-
if (redirectUri != null) {
UriBuilder uriBuilder = UriBuilder.fromUri(redirectUri);
- if (state != null) uriBuilder.queryParam(STATE_PARAM, state);
+ if (state != null)
+ uriBuilder.queryParam(STATE_PARAM, state);
return Response.status(302).location(uriBuilder.build()).build();
} else {
return Response.ok().build();
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
index 714c0d1..60e9f9d 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java
@@ -4,6 +4,8 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
+import org.keycloak.services.clientregistration.ClientRegistrationService;
+import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.Urls;
import org.keycloak.wellknown.WellKnownProvider;
@@ -48,6 +50,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setUserinfoEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "issueUserInfo").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
config.setLogoutEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "logout").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
config.setJwksUri(uriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
+ config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(uriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString());
config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED);
config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
index 9245e58..0226331 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
@@ -7,7 +7,6 @@ import org.codehaus.jackson.annotate.JsonProperty;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -48,6 +47,9 @@ public class OIDCConfigurationRepresentation {
@JsonProperty("response_modes_supported")
private List<String> responseModesSupported;
+ @JsonProperty("registration_endpoint")
+ private String registrationEndpoint;
+
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
public String getIssuer() {
@@ -138,6 +140,14 @@ public class OIDCConfigurationRepresentation {
this.responseModesSupported = responseModesSupported;
}
+ public String getRegistrationEndpoint() {
+ return registrationEndpoint;
+ }
+
+ public void setRegistrationEndpoint(String registrationEndpoint) {
+ this.registrationEndpoint = registrationEndpoint;
+ }
+
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
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 17d5b23..e76734e 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -9,9 +9,11 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel;
@@ -24,6 +26,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.protocol.oidc.utils.WebOriginsUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
@@ -50,7 +53,7 @@ import java.util.Map;
import java.util.Set;
/**
- * Stateful object that creates tokens and manages oauth access codes
+ * Stateless object that creates tokens and manages oauth access codes
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@@ -194,44 +197,46 @@ public class TokenManager {
}
public RefreshToken verifyRefreshToken(RealmModel realm, String encodedRefreshToken) throws OAuthErrorException {
- JWSInput jws = new JWSInput(encodedRefreshToken);
- RefreshToken refreshToken = null;
try {
+ JWSInput jws = new JWSInput(encodedRefreshToken);
+ RefreshToken refreshToken = null;
if (!RSAProvider.verify(jws, realm.getPublicKey())) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token");
}
refreshToken = jws.readJsonContent(RefreshToken.class);
- } catch (Exception e) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e);
- }
- if (refreshToken.getExpiration() != 0 && refreshToken.isExpired()) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired");
- }
- if (refreshToken.getIssuedAt() < realm.getNotBefore()) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
+ if (refreshToken.getExpiration() != 0 && refreshToken.isExpired()) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired");
+ }
+
+ if (refreshToken.getIssuedAt() < realm.getNotBefore()) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token");
+ }
+ return refreshToken;
+ } catch (JWSInputException e) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e);
}
- return refreshToken;
}
public IDToken verifyIDToken(RealmModel realm, String encodedIDToken) throws OAuthErrorException {
- JWSInput jws = new JWSInput(encodedIDToken);
- IDToken idToken = null;
try {
+ JWSInput jws = new JWSInput(encodedIDToken);
+ IDToken idToken;
if (!RSAProvider.verify(jws, realm.getPublicKey())) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken");
}
idToken = jws.readJsonContent(IDToken.class);
- } catch (IOException e) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken", e);
- }
- if (idToken.isExpired()) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "IDToken expired");
- }
- if (idToken.getIssuedAt() < realm.getNotBefore()) {
- throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale IDToken");
+ if (idToken.isExpired()) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "IDToken expired");
+ }
+
+ if (idToken.getIssuedAt() < realm.getNotBefore()) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale IDToken");
+ }
+ return idToken;
+ } catch (JWSInputException e) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken", e);
}
- return idToken;
}
public AccessToken createClientAccessToken(KeycloakSession session, Set<RoleModel> requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession, ClientSessionModel clientSession) {
@@ -289,10 +294,23 @@ public class TokenManager {
}
}
+ public static void addGroupRoles(GroupModel group, Set<RoleModel> roleMappings) {
+ roleMappings.addAll(group.getRoleMappings());
+ if (group.getParentId() == null) return;
+ addGroupRoles(group.getParent(), roleMappings);
+ }
+
public static Set<RoleModel> getAccess(String scopeParam, boolean applyScopeParam, ClientModel client, UserModel user) {
Set<RoleModel> requestedRoles = new HashSet<RoleModel>();
- Set<RoleModel> roleMappings = user.getRoleMappings();
+ Set<RoleModel> mappings = user.getRoleMappings();
+ Set<RoleModel> roleMappings = new HashSet<>();
+ roleMappings.addAll(mappings);
+ for (GroupModel group : user.getGroups()) {
+ addGroupRoles(group, roleMappings);
+ }
+
+
if (client.isFullScopeAllowed()) {
requestedRoles = roleMappings;
@@ -438,8 +456,9 @@ public class TokenManager {
if (session != null) {
token.setSessionState(session.getId());
}
- if (realm.getAccessTokenLifespan() > 0) {
- token.expiration(Time.currentTime() + realm.getAccessTokenLifespan());
+ int tokenLifespan = getTokenLifespan(realm, clientSession);
+ if (tokenLifespan > 0) {
+ token.expiration(Time.currentTime() + tokenLifespan);
}
Set<String> allowedOrigins = client.getWebOrigins();
if (allowedOrigins != null) {
@@ -448,6 +467,15 @@ public class TokenManager {
return token;
}
+ private int getTokenLifespan(RealmModel realm, ClientSessionModel clientSession) {
+ boolean implicitFlow = false;
+ String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ if (responseType != null) {
+ implicitFlow = OIDCResponseType.parse(responseType).isImplicitFlow();
+ }
+ return implicitFlow ? realm.getAccessTokenLifespanForImplicitFlow() : realm.getAccessTokenLifespan();
+ }
+
protected void addComposites(AccessToken token, RoleModel role) {
AccessToken.Access access = null;
if (role.getContainer() instanceof RealmModel) {
@@ -565,9 +593,7 @@ public class TokenManager {
idToken.issuer(accessToken.getIssuer());
idToken.setNonce(accessToken.getNonce());
idToken.setSessionState(accessToken.getSessionState());
- if (realm.getAccessTokenLifespan() > 0) {
- idToken.expiration(Time.currentTime() + realm.getAccessTokenLifespan());
- }
+ idToken.expiration(accessToken.getExpiration());
transformIDToken(session, idToken, realm, client, userSession.getUser(), userSession, clientSession);
return this;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
new file mode 100644
index 0000000..06cc3cc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCRedirectUriBuilder.java
@@ -0,0 +1,149 @@
+package org.keycloak.protocol.oidc.utils;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.keycloak.common.util.Encode;
+import org.keycloak.common.util.KeycloakUriBuilder;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class OIDCRedirectUriBuilder {
+
+ protected final KeycloakUriBuilder uriBuilder;
+
+ protected OIDCRedirectUriBuilder(KeycloakUriBuilder uriBuilder) {
+ this.uriBuilder = uriBuilder;
+ }
+
+ public abstract OIDCRedirectUriBuilder addParam(String paramName, String paramValue);
+ public abstract Response build();
+
+
+ public static OIDCRedirectUriBuilder fromUri(String baseUri, OIDCResponseMode responseMode) {
+ KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(baseUri);
+
+ switch (responseMode) {
+ case QUERY: return new QueryRedirectUriBuilder(uriBuilder);
+ case FRAGMENT: return new FragmentRedirectUriBuilder(uriBuilder);
+ case FORM_POST: return new FormPostRedirectUriBuilder(uriBuilder);
+ }
+
+ throw new IllegalStateException("Not possible to end here");
+ }
+
+
+ // Impl subclasses
+
+
+ // http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
+ public static class QueryRedirectUriBuilder extends OIDCRedirectUriBuilder {
+
+ protected QueryRedirectUriBuilder(KeycloakUriBuilder uriBuilder) {
+ super(uriBuilder);
+ }
+
+ @Override
+ public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) {
+ uriBuilder.queryParam(paramName, paramValue);
+ return this;
+ }
+
+ @Override
+ public Response build() {
+ URI redirectUri = uriBuilder.build();
+ Response.ResponseBuilder location = Response.status(302).location(redirectUri);
+ return location.build();
+ }
+ }
+
+
+ // http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
+ public static class FragmentRedirectUriBuilder extends OIDCRedirectUriBuilder {
+
+ private StringBuilder fragment;
+
+ protected FragmentRedirectUriBuilder(KeycloakUriBuilder uriBuilder) {
+ super(uriBuilder);
+ }
+
+ @Override
+ public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) {
+ String param = paramName + "=" + Encode.encodeQueryParam(paramValue);
+ if (fragment == null) {
+ fragment = new StringBuilder(param);
+ } else {
+ fragment.append("&").append(param);
+ }
+ return this;
+ }
+
+ @Override
+ public Response build() {
+ if (fragment != null) {
+ uriBuilder.encodedFragment(fragment.toString());
+ }
+ URI redirectUri = uriBuilder.build();
+
+ Response.ResponseBuilder location = Response.status(302).location(redirectUri);
+ return location.build();
+ }
+
+ }
+
+
+ // http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html
+ public static class FormPostRedirectUriBuilder extends OIDCRedirectUriBuilder {
+
+ private Map<String, String> params = new HashMap<>();
+
+ protected FormPostRedirectUriBuilder(KeycloakUriBuilder uriBuilder) {
+ super(uriBuilder);
+ }
+
+ @Override
+ public OIDCRedirectUriBuilder addParam(String paramName, String paramValue) {
+ params.put(paramName, Encode.encodeQueryParam(paramValue));
+ return this;
+ }
+
+ @Override
+ public Response build() {
+ StringBuilder builder = new StringBuilder();
+ URI redirectUri = uriBuilder.build();
+
+ builder.append("<HTML>");
+ builder.append(" <HEAD>");
+ builder.append(" <TITLE>OIDC Form_Post Response</TITLE>");
+ builder.append(" </HEAD>");
+ builder.append(" <BODY Onload=\"document.forms[0].submit()\">");
+
+ builder.append(" <FORM METHOD=\"POST\" ACTION=\"" + redirectUri.toString() + "\">");
+
+ for (Map.Entry<String, String> param : params.entrySet()) {
+ builder.append(" <INPUT TYPE=\"HIDDEN\" NAME=\"").append(param.getKey())
+ .append("\" VALUE=\"").append(param.getValue()).append("\" />");
+ }
+
+ builder.append(" <NOSCRIPT>");
+ builder.append(" <P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue .</P>");
+ builder.append(" <INPUT name=\"continue\" TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />");
+ builder.append(" </NOSCRIPT>");
+ builder.append(" </FORM>");
+ builder.append(" </BODY>");
+ builder.append("</HTML>");
+
+ return Response.status(Response.Status.OK)
+ .type(MediaType.TEXT_HTML_TYPE)
+ .entity(builder.toString()).build();
+ }
+
+ }
+
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseMode.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseMode.java
new file mode 100644
index 0000000..c255ccc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseMode.java
@@ -0,0 +1,25 @@
+package org.keycloak.protocol.oidc.utils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public enum OIDCResponseMode {
+
+ QUERY, FRAGMENT, FORM_POST;
+
+ public static OIDCResponseMode parse(String responseMode, OIDCResponseType responseType) {
+ if (responseMode == null) {
+ return getDefaultResponseMode(responseType);
+ } else {
+ return Enum.valueOf(OIDCResponseMode.class, responseMode.toUpperCase());
+ }
+ }
+
+ private static OIDCResponseMode getDefaultResponseMode(OIDCResponseType responseType) {
+ if (responseType.isImplicitOrHybridFlow()) {
+ return OIDCResponseMode.FRAGMENT;
+ } else {
+ return OIDCResponseMode.QUERY;
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseType.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseType.java
new file mode 100644
index 0000000..6377b22
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/OIDCResponseType.java
@@ -0,0 +1,93 @@
+package org.keycloak.protocol.oidc.utils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OIDCResponseType {
+
+ public static final String CODE = OIDCLoginProtocol.CODE_PARAM;
+ public static final String TOKEN = "token";
+ public static final String ID_TOKEN = "id_token";
+ public static final String NONE = "none";
+
+ private static final List<String> ALLOWED_RESPONSE_TYPES = Arrays.asList(CODE, TOKEN, ID_TOKEN, NONE);
+
+ private final List<String> responseTypes;
+
+
+ private OIDCResponseType(List<String> responseTypes) {
+ this.responseTypes = responseTypes;
+ }
+
+
+ public static OIDCResponseType parse(String responseTypeParam) {
+ if (responseTypeParam == null) {
+ throw new IllegalArgumentException("response_type is null");
+ }
+
+ String[] responseTypes = responseTypeParam.trim().split(" ");
+ List<String> allowedTypes = new ArrayList<>();
+ for (String current : responseTypes) {
+ if (ALLOWED_RESPONSE_TYPES.contains(current)) {
+ allowedTypes.add(current);
+ } else {
+ throw new IllegalArgumentException("Unsupported response_type: " + responseTypeParam);
+ }
+ }
+
+ validateAllowedTypes(allowedTypes);
+
+ return new OIDCResponseType(allowedTypes);
+ }
+
+ private static void validateAllowedTypes(List<String> responseTypes) {
+ if (responseTypes.size() == 0) {
+ throw new IllegalStateException("No responseType provided");
+ }
+ if (responseTypes.contains(NONE) && responseTypes.size() > 1) {
+ throw new IllegalArgumentException("None not allowed with some other response_type");
+ }
+ if (responseTypes.contains(ID_TOKEN) && responseTypes.size() == 1) {
+ throw new IllegalArgumentException("Not supported to use response_type=id_token alone");
+ }
+ if (responseTypes.contains(TOKEN) && responseTypes.size() == 1) {
+ throw new IllegalArgumentException("Not supported to use response_type=token alone");
+ }
+ }
+
+
+ public boolean hasResponseType(String responseType) {
+ return responseTypes.contains(responseType);
+ }
+
+
+ public boolean isImplicitOrHybridFlow() {
+ return hasResponseType(TOKEN) || hasResponseType(ID_TOKEN);
+ }
+
+ public boolean isImplicitFlow() {
+ return hasResponseType(TOKEN) && hasResponseType(ID_TOKEN) && !hasResponseType(CODE);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ boolean first = true;
+ for (String responseType : responseTypes) {
+ if (!first) {
+ builder.append(" ");
+ } else {
+ first = false;
+ }
+ builder.append(responseType);
+ }
+ return builder.toString();
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
new file mode 100644
index 0000000..0c95a37
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java
@@ -0,0 +1,125 @@
+package org.keycloak.services.clientregistration;
+
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.ClientInitialAccessModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.ForbiddenException;
+
+import javax.ws.rs.core.Response;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public abstract class AbstractClientRegistrationProvider implements ClientRegistrationProvider {
+
+ protected KeycloakSession session;
+ protected EventBuilder event;
+ protected ClientRegistrationAuth auth;
+
+ public AbstractClientRegistrationProvider(KeycloakSession session) {
+ this.session = session;
+ }
+
+ public ClientRepresentation create(ClientRepresentation client) {
+ event.event(EventType.CLIENT_REGISTER);
+
+ auth.requireCreate();
+
+ try {
+ ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
+ if (client.getClientId() == null) {
+ clientModel.setClientId(clientModel.getId());
+ }
+
+ client = ModelToRepresentation.toRepresentation(clientModel);
+
+ String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, clientModel);
+
+ client.setRegistrationAccessToken(registrationAccessToken);
+
+ if (auth.isInitialAccessToken()) {
+ ClientInitialAccessModel initialAccessModel = auth.getInitialAccessModel();
+ initialAccessModel.decreaseRemainingCount();
+ }
+
+ event.client(client.getClientId()).success();
+ return client;
+ } catch (ModelDuplicateException e) {
+ throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier in use", Response.Status.BAD_REQUEST);
+ }
+ }
+
+ public ClientRepresentation get(String clientId) {
+ event.event(EventType.CLIENT_INFO);
+
+ ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
+ auth.requireView(client);
+
+ ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
+
+ if (auth.isRegistrationAccessToken()) {
+ String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
+ rep.setRegistrationAccessToken(registrationAccessToken);
+ }
+
+ event.client(client.getClientId()).success();
+ return rep;
+ }
+
+ public ClientRepresentation update(String clientId, ClientRepresentation rep) {
+ event.event(EventType.CLIENT_UPDATE).client(clientId);
+
+ ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
+ auth.requireUpdate(client);
+
+ if (!client.getClientId().equals(rep.getClientId())) {
+ throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier modified", Response.Status.BAD_REQUEST);
+ }
+
+ RepresentationToModel.updateClient(rep, client);
+ rep = ModelToRepresentation.toRepresentation(client);
+
+ if (auth.isRegistrationAccessToken()) {
+ String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
+ rep.setRegistrationAccessToken(registrationAccessToken);
+ }
+
+ event.client(client.getClientId()).success();
+ return rep;
+ }
+
+ public void delete(String clientId) {
+ event.event(EventType.CLIENT_DELETE).client(clientId);
+
+ ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
+ auth.requireUpdate(client);
+
+ if (session.getContext().getRealm().removeClient(client.getId())) {
+ event.client(client.getClientId()).success();
+ } else {
+ throw new ForbiddenException();
+ }
+ }
+
+ @Override
+ public void setAuth(ClientRegistrationAuth auth) {
+ this.auth = auth;
+ }
+
+ @Override
+ public void setEvent(EventBuilder event) {
+ this.event = event;
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java
index feffc5f..c667b5e 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java
@@ -1,6 +1,5 @@
package org.keycloak.services.clientregistration;
-import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@@ -23,7 +22,7 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi
private KeycloakSession session;
private EventBuilder event;
- private ClientRegAuth auth;
+ private ClientRegistrationAuth auth;
public AdapterInstallationClientRegistrationProvider(KeycloakSession session) {
this.session = session;
@@ -51,7 +50,7 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi
}
@Override
- public void setAuth(ClientRegAuth auth) {
+ public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth;
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationProvider.java
index 0c6bc4e..98bd6f9 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationProvider.java
@@ -1,7 +1,6 @@
package org.keycloak.services.clientregistration;
import org.keycloak.events.EventBuilder;
-import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
/**
@@ -9,7 +8,7 @@ import org.keycloak.provider.Provider;
*/
public interface ClientRegistrationProvider extends Provider {
- void setAuth(ClientRegAuth auth);
+ void setAuth(ClientRegistrationAuth auth);
void setEvent(EventBuilder event);
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java
index 2aed3f1..621d5fb 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationService.java
@@ -25,7 +25,7 @@ public class ClientRegistrationService {
}
@Path("{provider}")
- public Object getProvider(@PathParam("provider") String providerId) {
+ public Object provider(@PathParam("provider") String providerId) {
checkSsl();
ClientRegistrationProvider provider = session.getProvider(ClientRegistrationProvider.class, providerId);
@@ -35,7 +35,7 @@ public class ClientRegistrationService {
}
provider.setEvent(event);
- provider.setAuth(new ClientRegAuth(session, event));
+ provider.setAuth(new ClientRegistrationAuth(session, event));
return provider;
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
new file mode 100644
index 0000000..4d54d5f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java
@@ -0,0 +1,100 @@
+package org.keycloak.services.clientregistration;
+
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.models.ClientInitialAccessModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.services.ForbiddenException;
+import org.keycloak.services.Urls;
+import org.keycloak.util.TokenUtil;
+
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientRegistrationTokenUtils {
+
+ public static final String TYPE_INITIAL_ACCESS_TOKEN = "InitialAccessToken";
+ public static final String TYPE_REGISTRATION_ACCESS_TOKEN = "RegistrationAccessToken";
+
+ public static String updateRegistrationAccessToken(KeycloakSession session, ClientModel client) {
+ return updateRegistrationAccessToken(session.getContext().getRealm(), session.getContext().getUri(), client);
+ }
+
+ public static String updateRegistrationAccessToken(RealmModel realm, UriInfo uri, ClientModel client) {
+ String id = KeycloakModelUtils.generateId();
+ client.setRegistrationToken(id);
+ String token = createToken(realm, uri, id, TYPE_REGISTRATION_ACCESS_TOKEN, 0);
+ return token;
+ }
+
+ public static String createInitialAccessToken(RealmModel realm, UriInfo uri, ClientInitialAccessModel model) {
+ return createToken(realm, uri, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getExpiration() > 0 ? model.getTimestamp() + model.getExpiration() : 0);
+ }
+
+ public static JsonWebToken parseToken(RealmModel realm, UriInfo uri, String token) {
+ JWSInput input;
+ try {
+ input = new JWSInput(token);
+ } catch (JWSInputException e) {
+ throw new ForbiddenException(e);
+ }
+
+ if (!RSAProvider.verify(input, realm.getPublicKey())) {
+ throw new ForbiddenException("Invalid signature");
+ }
+
+ JsonWebToken jwt;
+ try {
+ jwt = input.readJsonContent(JsonWebToken.class);
+ } catch (JWSInputException e) {
+ throw new ForbiddenException(e);
+ }
+
+ if (!getIssuer(realm, uri).equals(jwt.getIssuer())) {
+ throw new ForbiddenException("Issuer doesn't match");
+ }
+
+ if (!jwt.isActive()) {
+ throw new ForbiddenException("Expired token");
+ }
+
+ if (!(TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType()) ||
+ TYPE_INITIAL_ACCESS_TOKEN.equals(jwt.getType()) ||
+ TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType()))) {
+ throw new ForbiddenException("Invalid token type");
+ }
+
+ return jwt;
+ }
+
+ private static String createToken(RealmModel realm, UriInfo uri, String id, String type, int expiration) {
+ JsonWebToken jwt = new JsonWebToken();
+
+ String issuer = getIssuer(realm, uri);
+
+ jwt.type(type);
+ jwt.id(id);
+ jwt.issuedAt(Time.currentTime());
+ jwt.expiration(expiration);
+ jwt.issuer(issuer);
+ jwt.audience(issuer);
+
+ String token = new JWSBuilder().jsonContent(jwt).rsa256(realm.getPrivateKey());
+ return token;
+ }
+
+ private static String getIssuer(RealmModel realm, UriInfo uri) {
+ return Urls.realmIssuer(uri.getBaseUri(), realm.getName());
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java
index 0fad9c2..126d00a 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/DefaultClientRegistrationProvider.java
@@ -4,12 +4,9 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelDuplicateException;
-import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.services.ErrorResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
@@ -19,94 +16,45 @@ import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class DefaultClientRegistrationProvider implements ClientRegistrationProvider {
-
- private KeycloakSession session;
- private EventBuilder event;
- private ClientRegAuth auth;
+public class DefaultClientRegistrationProvider extends AbstractClientRegistrationProvider {
public DefaultClientRegistrationProvider(KeycloakSession session) {
- this.session = session;
+ super(session);
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
- public Response create(ClientRepresentation client) {
- event.event(EventType.CLIENT_REGISTER);
-
- auth.requireCreate();
-
- try {
- ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
- KeycloakModelUtils.generateRegistrationAccessToken(clientModel);
-
- client = ModelToRepresentation.toRepresentation(clientModel);
- URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
-
- event.client(client.getClientId()).success();
- return Response.created(uri).entity(client).build();
- } catch (ModelDuplicateException e) {
- return ErrorResponse.exists("Client " + client.getClientId() + " already exists");
- }
+ public Response createDefault(ClientRepresentation client) {
+ client = create(client);
+ URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
+ return Response.created(uri).entity(client).build();
}
@GET
@Path("{clientId}")
@Produces(MediaType.APPLICATION_JSON)
- public Response get(@PathParam("clientId") String clientId) {
- event.event(EventType.CLIENT_INFO);
-
- ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
- auth.requireView(client);
-
- if (auth.isRegistrationAccessToken()) {
- KeycloakModelUtils.generateRegistrationAccessToken(client);
- }
-
- event.client(client.getClientId()).success();
- return Response.ok(ModelToRepresentation.toRepresentation(client)).build();
+ public Response getDefault(@PathParam("clientId") String clientId) {
+ ClientRepresentation client = get(clientId);
+ return Response.ok(client).build();
}
@PUT
@Path("{clientId}")
@Consumes(MediaType.APPLICATION_JSON)
- public Response update(@PathParam("clientId") String clientId, ClientRepresentation rep) {
- event.event(EventType.CLIENT_UPDATE).client(clientId);
-
- ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
- auth.requireUpdate(client);
-
- RepresentationToModel.updateClient(rep, client);
-
- if (auth.isRegistrationAccessToken()) {
- KeycloakModelUtils.generateRegistrationAccessToken(client);
- }
-
- rep = ModelToRepresentation.toRepresentation(client);
-
- event.client(client.getClientId()).success();
- return Response.ok(rep).build();
+ public Response updateDefault(@PathParam("clientId") String clientId, ClientRepresentation client) {
+ client = update(clientId, client);
+ return Response.ok(client).build();
}
@DELETE
@Path("{clientId}")
- public Response delete(@PathParam("clientId") String clientId) {
- event.event(EventType.CLIENT_DELETE).client(clientId);
-
- ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
- auth.requireUpdate(client);
-
- if (session.getContext().getRealm().removeClient(client.getId())) {
- event.client(client.getClientId()).success();
- return Response.ok().build();
- } else {
- return Response.status(Response.Status.NOT_FOUND).build();
- }
+ public void deleteDefault(@PathParam("clientId") String clientId) {
+ delete(clientId);
}
@Override
- public void setAuth(ClientRegAuth auth) {
+ public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth;
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ErrorCodes.java b/services/src/main/java/org/keycloak/services/clientregistration/ErrorCodes.java
new file mode 100644
index 0000000..ed491de
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientregistration/ErrorCodes.java
@@ -0,0 +1,12 @@
+package org.keycloak.services.clientregistration;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface ErrorCodes {
+
+ String INVALID_REDIRECT_URI = "invalid_redirect_uri";
+
+ String INVALID_CLIENT_METADATA = "invalid_client_metadata";
+
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
index 1e0784c..a7f9f2c 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/DescriptionConverter.java
@@ -1,8 +1,9 @@
package org.keycloak.services.clientregistration.oidc;
-import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.protocol.oidc.representations.OIDCClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+
+import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -11,27 +12,22 @@ public class DescriptionConverter {
public static ClientRepresentation toInternal(OIDCClientRepresentation clientOIDC) {
ClientRepresentation client = new ClientRepresentation();
- client.setClientId(KeycloakModelUtils.generateId());
+ client.setClientId(clientOIDC.getClientId());
client.setName(clientOIDC.getClientName());
client.setRedirectUris(clientOIDC.getRedirectUris());
client.setBaseUrl(clientOIDC.getClientUri());
return client;
}
- public static OIDCClientResponseRepresentation toExternalResponse(ClientRepresentation client) {
- OIDCClientResponseRepresentation response = new OIDCClientResponseRepresentation();
+ public static OIDCClientRepresentation toExternalResponse(ClientRepresentation client, URI uri) {
+ OIDCClientRepresentation response = new OIDCClientRepresentation();
response.setClientId(client.getClientId());
-
response.setClientName(client.getName());
response.setClientUri(client.getBaseUrl());
-
response.setClientSecret(client.getSecret());
- response.setClientSecretExpiresAt(0);
-
response.setRedirectUris(client.getRedirectUris());
-
response.setRegistrationAccessToken(client.getRegistrationAccessToken());
-
+ response.setRegistrationClientUri(uri.toString());
return response;
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
index b27ddb0..e60720b 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProvider.java
@@ -1,24 +1,17 @@
package org.keycloak.services.clientregistration.oidc;
-import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.events.EventBuilder;
-import org.keycloak.events.EventType;
-import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelDuplicateException;
-import org.keycloak.models.utils.ModelToRepresentation;
-import org.keycloak.models.utils.RepresentationToModel;
-import org.keycloak.protocol.oidc.OIDCClientDescriptionConverter;
-import org.keycloak.protocol.oidc.representations.OIDCClientRepresentation;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.services.ErrorResponse;
-import org.keycloak.services.clientregistration.ClientRegAuth;
-import org.keycloak.services.clientregistration.ClientRegistrationProvider;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
+import org.keycloak.services.clientregistration.ClientRegistrationAuth;
+import org.keycloak.services.clientregistration.ErrorCodes;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.POST;
-import javax.ws.rs.Produces;
+import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
@@ -26,68 +19,56 @@ import java.net.URI;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class OIDCClientRegistrationProvider implements ClientRegistrationProvider {
+public class OIDCClientRegistrationProvider extends AbstractClientRegistrationProvider {
- private static final Logger logger = Logger.getLogger(OIDCClientRegistrationProvider.class);
+ public OIDCClientRegistrationProvider(KeycloakSession session) {
+ super(session);
+ }
- private KeycloakSession session;
- private EventBuilder event;
- private ClientRegAuth auth;
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response createOIDC(OIDCClientRepresentation clientOIDC) {
+ if (clientOIDC.getClientId() != null) {
+ throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier included", Response.Status.BAD_REQUEST);
+ }
- public OIDCClientRegistrationProvider(KeycloakSession session) {
- this.session = session;
+ ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
+ client = create(client);
+ URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
+ clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
+ clientOIDC.setClientIdIssuedAt(Time.currentTime());
+ return Response.created(uri).entity(clientOIDC).build();
}
-// @POST
-// @Consumes(MediaType.APPLICATION_JSON)
-// @Produces(MediaType.APPLICATION_JSON)
-// public Response create(OIDCClientRepresentation clientOIDC) {
-// event.event(EventType.CLIENT_REGISTER);
-//
-// auth.requireCreate();
-//
-// ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
-//
-// try {
-// ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
-//
-// client = ModelToRepresentation.toRepresentation(clientModel);
-//
-// String registrationAccessToken = TokenGenerator.createRegistrationAccessToken();
-//
-// clientModel.setRegistrationSecret(registrationAccessToken);
-//
-// URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build();
-//
-// logger.infov("Created client {0}", client.getClientId());
-//
-// event.client(client.getClientId()).success();
-//
-// OIDCClientResponseRepresentation response = DescriptionConverter.toExternalResponse(client);
-//
-// response.setClientName(client.getName());
-// response.setClientUri(client.getBaseUrl());
-//
-// response.setClientSecret(client.getSecret());
-// response.setClientSecretExpiresAt(0);
-//
-// response.setRedirectUris(client.getRedirectUris());
-//
-// response.setRegistrationAccessToken(registrationAccessToken);
-// response.setRegistrationClientUri(uri.toString());
-//
-// return Response.created(uri).entity(response).build();
-// } catch (ModelDuplicateException e) {
-// return ErrorResponse.exists("Client " + client.getClientId() + " already exists");
-// }
-// }
+ @GET
+ @Path("{clientId}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response getOIDC(@PathParam("clientId") String clientId) {
+ ClientRepresentation client = get(clientId);
+ OIDCClientRepresentation clientOIDC = DescriptionConverter.toExternalResponse(client, session.getContext().getUri().getRequestUri());
+ return Response.ok(clientOIDC).build();
+ }
- @Override
- public void close() {
+ @PUT
+ @Path("{clientId}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response updateOIDC(@PathParam("clientId") String clientId, OIDCClientRepresentation clientOIDC) {
+ ClientRepresentation client = DescriptionConverter.toInternal(clientOIDC);
+ client = update(clientId, client);
+ URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
+ clientOIDC = DescriptionConverter.toExternalResponse(client, uri);
+ return Response.ok(clientOIDC).build();
+ }
+
+ @DELETE
+ @Path("{clientId}")
+ public void deleteOIDC(@PathParam("clientId") String clientId) {
+ delete(clientId);
}
@Override
- public void setAuth(ClientRegAuth auth) {
+ public void setAuth(ClientRegistrationAuth auth) {
this.auth = auth;
}
@@ -96,4 +77,8 @@ public class OIDCClientRegistrationProvider implements ClientRegistrationProvide
this.event = event;
}
+ @Override
+ public void close() {
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProviderFactory.java b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProviderFactory.java
index 6f112f8..0144e79 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProviderFactory.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/oidc/OIDCClientRegistrationProviderFactory.java
@@ -11,6 +11,8 @@ import org.keycloak.services.clientregistration.ClientRegistrationProviderFactor
*/
public class OIDCClientRegistrationProviderFactory implements ClientRegistrationProviderFactory {
+ public static final String ID = "openid-connect";
+
@Override
public ClientRegistrationProvider create(KeycloakSession session) {
return new OIDCClientRegistrationProvider(session);
@@ -30,7 +32,7 @@ public class OIDCClientRegistrationProviderFactory implements ClientRegistration
@Override
public String getId() {
- return "openid-connect";
+ return ID;
}
}
diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
index 5178cec..a868aa8 100755
--- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
+++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
@@ -23,62 +23,72 @@ public class ApplianceBootstrap {
private static final Logger logger = Logger.getLogger(ApplianceBootstrap.class);
- public void bootstrap(KeycloakSessionFactory sessionFactory, String contextPath) {
+ public static boolean setupDefaultRealm(KeycloakSessionFactory sessionFactory, String contextPath) {
KeycloakSession session = sessionFactory.create();
session.getTransaction().begin();
try {
- bootstrap(session, contextPath);
+ String adminRealmName = Config.getAdminRealm();
+ if (session.realms().getRealm(adminRealmName) != null) {
+ return false;
+ }
+
+ logger.info("Initializing " + adminRealmName + " realm");
+
+ RealmManager manager = new RealmManager(session);
+ manager.setContextPath(contextPath);
+ RealmModel realm = manager.createRealm(adminRealmName, adminRealmName);
+ realm.setName(adminRealmName);
+ realm.setEnabled(true);
+ realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
+ realm.setSsoSessionIdleTimeout(1800);
+ realm.setAccessTokenLifespan(60);
+ realm.setAccessTokenLifespanForImplicitFlow(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT);
+ realm.setSsoSessionMaxLifespan(36000);
+ realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
+ realm.setAccessCodeLifespan(60);
+ realm.setAccessCodeLifespanUserAction(300);
+ realm.setAccessCodeLifespanLogin(1800);
+ realm.setSslRequired(SslRequired.EXTERNAL);
+ realm.setRegistrationAllowed(false);
+ realm.setRegistrationEmailAsUsername(false);
+ KeycloakModelUtils.generateRealmKeys(realm);
+
session.getTransaction().commit();
+ return true;
} finally {
session.close();
}
}
- public void bootstrap(KeycloakSession session, String contextPath) {
- String adminRealmName = Config.getAdminRealm();
- if (session.realms().getRealm(adminRealmName) != null) {
- return;
- }
-
- logger.info("Initializing " + adminRealmName + " realm");
-
- RealmManager manager = new RealmManager(session);
- manager.setContextPath(contextPath);
- RealmModel realm = manager.createRealm(adminRealmName, adminRealmName);
- realm.setName(adminRealmName);
- realm.setEnabled(true);
- realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
- realm.setSsoSessionIdleTimeout(1800);
- realm.setAccessTokenLifespan(60);
- realm.setSsoSessionMaxLifespan(36000);
- realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
- realm.setAccessCodeLifespan(60);
- realm.setAccessCodeLifespanUserAction(300);
- realm.setAccessCodeLifespanLogin(1800);
- realm.setSslRequired(SslRequired.EXTERNAL);
- realm.setRegistrationAllowed(false);
- realm.setRegistrationEmailAsUsername(false);
- KeycloakModelUtils.generateRealmKeys(realm);
+ public static boolean setupDefaultUser(KeycloakSessionFactory sessionFactory) {
+ KeycloakSession session = sessionFactory.create();
+ session.getTransaction().begin();
- UserModel adminUser = session.users().addUser(realm, "admin");
- setupAdminUser(session, realm, adminUser, "admin");
- }
+ try {
+ RealmModel realm = session.realms().getRealm(Config.getAdminRealm());
+ if (session.users().getUserByUsername("admin", realm) == null) {
+ UserModel adminUser = session.users().addUser(realm, "admin");
- public static void setupAdminUser(KeycloakSession session, RealmModel realm, UserModel adminUser, String password) {
- adminUser.setEnabled(true);
- UserCredentialModel usrCredModel = new UserCredentialModel();
- usrCredModel.setType(UserCredentialModel.PASSWORD);
- usrCredModel.setValue(password);
- session.users().updateCredential(realm, adminUser, usrCredModel);
- adminUser.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+ adminUser.setEnabled(true);
+ UserCredentialModel usrCredModel = new UserCredentialModel();
+ usrCredModel.setType(UserCredentialModel.PASSWORD);
+ usrCredModel.setValue("admin");
+ session.users().updateCredential(realm, adminUser, usrCredModel);
+ adminUser.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
- RoleModel adminRole = realm.getRole(AdminRoles.ADMIN);
- adminUser.grantRole(adminRole);
+ RoleModel adminRole = realm.getRole(AdminRoles.ADMIN);
+ adminUser.grantRole(adminRole);
- ClientModel accountApp = realm.getClientNameMap().get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
- for (String r : accountApp.getDefaultRoles()) {
- adminUser.grantRole(accountApp.getRole(r));
+ ClientModel accountApp = realm.getClientNameMap().get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
+ for (String r : accountApp.getDefaultRoles()) {
+ adminUser.grantRole(accountApp.getRole(r));
+ }
+ }
+ session.getTransaction().commit();
+ return true;
+ } finally {
+ session.close();
}
}
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 ea47ddc..0131b6d 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -36,6 +36,7 @@ import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol;
+import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken;
@@ -379,7 +380,8 @@ public class AuthenticationManager {
public static Response redirectAfterSuccessfulFlow(KeycloakSession session, RealmModel realm, UserSessionModel userSession,
ClientSessionModel clientSession,
- HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection) {
+ HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection,
+ EventBuilder event) {
Cookie sessionCookie = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_SESSION_COOKIE);
if (sessionCookie != null) {
@@ -406,7 +408,8 @@ public class AuthenticationManager {
LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
protocol.setRealm(realm)
.setHttpHeaders(request.getHttpHeaders())
- .setUriInfo(uriInfo);
+ .setUriInfo(uriInfo)
+ .setEventBuilder(event);
RestartLoginCookie.expireRestartCookie(realm, clientConnection, uriInfo);
return protocol.authenticated(userSession, new ClientSessionCode(realm, clientSession));
@@ -428,7 +431,7 @@ public class AuthenticationManager {
}
event.success();
RealmModel realm = clientSession.getRealm();
- return redirectAfterSuccessfulFlow(session, realm , userSession, clientSession, request, uriInfo, clientConnection);
+ return redirectAfterSuccessfulFlow(session, realm , userSession, clientSession, request, uriInfo, clientConnection, event);
}
@@ -521,9 +524,11 @@ public class AuthenticationManager {
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
- .setUriInfo(context.getUriInfo());
+ .setUriInfo(context.getUriInfo())
+ .setEventBuilder(event);
+ Response response = protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
- return protocol.consentDenied(context.getClientSession());
+ return response;
}
else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
clientSession.setNote(CURRENT_REQUIRED_ACTION, model.getProviderId());
diff --git a/services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java b/services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java
index 8107462..191f7fa 100755
--- a/services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java
+++ b/services/src/main/java/org/keycloak/services/managers/BruteForceProtector.java
@@ -53,12 +53,6 @@ public class BruteForceProtector implements Runnable {
}
}
- protected class SuccessfulLogin extends LoginEvent {
- public SuccessfulLogin(String realmId, String userId, String ip) {
- super(realmId, userId, ip);
- }
- }
-
protected class ShutdownEvent extends LoginEvent {
public ShutdownEvent() {
super(null, null, null);
@@ -83,7 +77,7 @@ public class BruteForceProtector implements Runnable {
logFailure(event);
UsernameLoginFailureModel user = getUserModel(session, event);
if (user == null) {
- user = session.sessions().addUserLoginFailure(realm, event.username);
+ user = session.sessions().addUserLoginFailure(realm, event.username.toLowerCase());
}
user.setLastIPFailure(event.ip);
long currentTime = System.currentTimeMillis();
@@ -122,7 +116,7 @@ public class BruteForceProtector implements Runnable {
protected UsernameLoginFailureModel getUserModel(KeycloakSession session, LoginEvent event) {
RealmModel realm = getRealmModel(session, event);
if (realm == null) return null;
- UsernameLoginFailureModel user = session.sessions().getUserLoginFailure(realm, event.username);
+ UsernameLoginFailureModel user = session.sessions().getUserLoginFailure(realm, event.username.toLowerCase());
if (user == null) return null;
return user;
}
@@ -147,7 +141,6 @@ public class BruteForceProtector implements Runnable {
}
}
-
public void run() {
final ArrayList<LoginEvent> events = new ArrayList<LoginEvent>(TRANSACTION_SIZE + 1);
try {
@@ -196,10 +189,6 @@ public class BruteForceProtector implements Runnable {
}
}
- protected void logSuccess(LoginEvent event) {
- logger.warn("login success for user " + event.username + " from ip " + event.ip);
- }
-
protected void logFailure(LoginEvent event) {
logger.warn("login failure for user " + event.username + " from ip " + event.ip);
failures++;
@@ -215,15 +204,6 @@ public class BruteForceProtector implements Runnable {
}
}
- public void successfulLogin(RealmModel realm, String username, ClientConnection clientConnection) {
- logger.info("successful login user: " + username + " from ip " + clientConnection.getRemoteAddr());
- }
-
- public void invalidUser(RealmModel realm, String username, ClientConnection clientConnection) {
- logger.warn("invalid user: " + username + " from ip " + clientConnection.getRemoteAddr());
- // todo more?
- }
-
public void failedLogin(RealmModel realm, String username, ClientConnection clientConnection) {
try {
FailedLogin event = new FailedLogin(realm.getId(), username, clientConnection.getRemoteAddr());
@@ -238,7 +218,7 @@ public class BruteForceProtector implements Runnable {
}
public boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, String username) {
- UsernameLoginFailureModel failure = session.sessions().getUserLoginFailure(realm, username);
+ UsernameLoginFailureModel failure = session.sessions().getUserLoginFailure(realm, username.toLowerCase());
if (failure == null) {
return false;
}
@@ -251,13 +231,4 @@ public class BruteForceProtector implements Runnable {
return false;
}
- public long getFailures() {
- return failures;
- }
-
- public long getLastFailure() {
- return lastFailure;
- }
-
-
}
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
index b994ef8..b0fd08e 100755
--- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
@@ -27,6 +27,12 @@ public class ClientSessionCode {
private final RealmModel realm;
private final ClientSessionModel clientSession;
+ public enum ActionType {
+ CLIENT,
+ LOGIN,
+ USER
+ }
+
public ClientSessionCode(RealmModel realm, ClientSessionModel clientSession) {
this.realm = realm;
this.clientSession = clientSession;
@@ -128,23 +134,29 @@ public class ClientSessionCode {
return clientSession;
}
- public boolean isValid(String requestedAction) {
+ public boolean isValid(String requestedAction, ActionType actionType) {
if (!isValidAction(requestedAction)) return false;
- return isActionActive(requestedAction);
+ return isActionActive(actionType);
}
- public boolean isActionActive(String requestedAction) {
+ public boolean isActionActive(ActionType actionType) {
int timestamp = clientSession.getTimestamp();
int lifespan;
- if (requestedAction.equals(ClientSessionModel.Action.CODE_TO_TOKEN.name())) {
- lifespan = realm.getAccessCodeLifespan();
-
- } else if (requestedAction.equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
- lifespan = realm.getAccessCodeLifespanLogin() > 0 ? realm.getAccessCodeLifespanLogin() : realm.getAccessCodeLifespanUserAction();
- } else {
- lifespan = realm.getAccessCodeLifespanUserAction();
+ switch (actionType) {
+ case CLIENT:
+ lifespan = realm.getAccessCodeLifespan();
+ break;
+ case LOGIN:
+ lifespan = realm.getAccessCodeLifespanLogin() > 0 ? realm.getAccessCodeLifespanLogin() : realm.getAccessCodeLifespanUserAction();
+ break;
+ case USER:
+ lifespan = realm.getAccessCodeLifespanUserAction();
+ break;
+ default:
+ throw new IllegalArgumentException();
}
+
return timestamp + lifespan > Time.currentTime();
}
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 5f2eeb3..ca1db96 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -110,7 +110,9 @@ public class Messages {
public static final String BEARER_ONLY = "bearerOnlyMessage";
- public static final String DIRECT_GRANTS_ONLY = "directGrantsOnlyMessage";
+ public static final String STANDARD_FLOW_DISABLED = "standardFlowDisabledMessage";
+
+ public static final String IMPLICIT_FLOW_DISABLED = "implicitFlowDisabledMessage";
public static final String INVALID_REDIRECT_URI = "invalidRedirectUriMessage";
diff --git a/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java b/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java
index 832331d..c4e2bff 100755
--- a/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AbstractSecuredLocalService.java
@@ -6,9 +6,11 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.AbstractOAuthClient;
import org.keycloak.common.ClientConnection;
import org.keycloak.OAuth2Constants;
+import org.keycloak.common.util.Base64Url;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.AppAuthManager;
@@ -40,6 +42,9 @@ import java.util.UUID;
*/
public abstract class AbstractSecuredLocalService {
private static final Logger logger = Logger.getLogger(AbstractSecuredLocalService.class);
+
+ private static final String KEYCLOAK_STATE_CHECKER = "KEYCLOAK_STATE_CHECKER";
+
protected final ClientModel client;
protected RealmModel realm;
@@ -106,14 +111,14 @@ public abstract class AbstractSecuredLocalService {
}
protected void updateCsrfChecks() {
- Cookie cookie = headers.getCookies().get(AccountService.KEYCLOAK_STATE_CHECKER);
+ Cookie cookie = headers.getCookies().get(KEYCLOAK_STATE_CHECKER);
if (cookie != null) {
stateChecker = cookie.getValue();
} else {
- stateChecker = UUID.randomUUID().toString();
+ stateChecker = KeycloakModelUtils.generateSecret();
String cookiePath = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
boolean secureOnly = realm.getSslRequired().isRequired(clientConnection);
- CookieHelper.addCookie(AccountService.KEYCLOAK_STATE_CHECKER, stateChecker, cookiePath, null, null, -1, secureOnly, true);
+ CookieHelper.addCookie(KEYCLOAK_STATE_CHECKER, stateChecker, cookiePath, null, null, -1, secureOnly, true);
}
}
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 af24034..1394832 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -117,8 +117,6 @@ public class AccountService extends AbstractSecuredLocalService {
LOG_DETAILS.add(Details.AUTH_METHOD);
}
- public static final String KEYCLOAK_STATE_CHECKER = "KEYCLOAK_STATE_CHECKER";
-
// Used when some other context (ie. IdentityBrokerService) wants to forward error to account management and display it here
public static final String ACCOUNT_MGMT_FORWARDED_ERROR_NOTE = "ACCOUNT_MGMT_FORWARDED_ERROR";
@@ -604,12 +602,6 @@ public class AccountService extends AbstractSecuredLocalService {
require(AccountRoles.MANAGE_ACCOUNT);
- String action = formData.getFirst("submitAction");
- if (action != null && action.equals("Cancel")) {
- setReferrerOnPage();
- return account.createResponse(AccountPages.PASSWORD);
- }
-
csrfCheck(formData);
UserModel user = auth.getUser();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
index 871bf05..5c22200 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
@@ -40,12 +40,7 @@ import javax.ws.rs.ext.Providers;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
+import java.util.*;
import javax.ws.rs.QueryParam;
/**
@@ -318,10 +313,10 @@ public class AdminConsole {
}
try {
- Properties msgs = AdminMessagesLoader.getMessages(getTheme(), lang);
+ Properties msgs = getTheme().getMessages("admin-messages", Locale.forLanguageTag(lang));
if (msgs.isEmpty()) {
logger.warn("Message bundle not found for language code '" + lang + "'");
- msgs = AdminMessagesLoader.getMessages(getTheme(), "en"); // fall back to en
+ msgs = getTheme().getMessages("admin-messages", Locale.ENGLISH);
}
if (msgs.isEmpty()) logger.fatal("Message bundle not found for language code 'en'");
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java
index a7bead8..1ac5dba 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java
@@ -9,6 +9,7 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.common.ClientConnection;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
@@ -137,11 +138,11 @@ public class AdminRoot {
protected AdminAuth authenticateRealmAdminRequest(HttpHeaders headers) {
String tokenString = authManager.extractAuthorizationHeaderToken(headers);
if (tokenString == null) throw new UnauthorizedException("Bearer");
- JWSInput input = new JWSInput(tokenString);
AccessToken token;
try {
+ JWSInput input = new JWSInput(tokenString);
token = input.readJsonContent(AccessToken.class);
- } catch (IOException e) {
+ } catch (JWSInputException e) {
throw new UnauthorizedException("Bearer token format error");
}
String realmName = token.getIssuer().substring(token.getIssuer().lastIndexOf('/') + 1);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java
index 3cdf988..2a8e924 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java
@@ -53,7 +53,7 @@ public class AttackDetectionResource {
this.realm = realm;
this.adminEvent = adminEvent.realm(realm);
- auth.init(RealmAuth.Resource.REALM);
+ auth.init(RealmAuth.Resource.USER);
}
/**
@@ -75,7 +75,7 @@ public class AttackDetectionResource {
data.put("lastIPFailure", "n/a");
if (!realm.isBruteForceProtected()) return data;
- UsernameLoginFailureModel model = session.sessions().getUserLoginFailure(realm, username);
+ UsernameLoginFailureModel model = session.sessions().getUserLoginFailure(realm, username.toLowerCase());
if (model == null) return data;
if (protector.isTemporarilyDisabled(session, realm, username)) {
data.put("disabled", true);
@@ -97,7 +97,7 @@ public class AttackDetectionResource {
@DELETE
public void clearBruteForceForUser(@PathParam("username") String username) {
auth.requireManage();
- UsernameLoginFailureModel model = session.sessions().getUserLoginFailure(realm, username);
+ UsernameLoginFailureModel model = session.sessions().getUserLoginFailure(realm, username.toLowerCase());
if (model != null) {
session.sessions().removeUserLoginFailure(realm, username);
adminEvent.operation(OperationType.DELETE).success();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java
new file mode 100644
index 0000000..7ac2036
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java
@@ -0,0 +1,102 @@
+package org.keycloak.services.resources.admin;
+
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.models.ClientInitialAccessModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
+import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.*;
+import javax.ws.rs.core.*;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientInitialAccessResource {
+
+ private final RealmAuth auth;
+ private final RealmModel realm;
+ private final AdminEventBuilder adminEvent;
+
+ @Context
+ protected KeycloakSession session;
+
+ @Context
+ protected UriInfo uriInfo;
+
+ public ClientInitialAccessResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) {
+ this.auth = auth;
+ this.realm = realm;
+ this.adminEvent = adminEvent;
+
+ auth.init(RealmAuth.Resource.CLIENT);
+ }
+
+ /**
+ * Create a new initial access token.
+ *
+ * @param config
+ * @return
+ */
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public ClientInitialAccessPresentation create(ClientInitialAccessCreatePresentation config, @Context final HttpServletResponse response) {
+ auth.requireManage();
+
+ int expiration = config.getExpiration() != null ? config.getExpiration() : 0;
+ int count = config.getCount() != null ? config.getCount() : 1;
+
+ ClientInitialAccessModel clientInitialAccessModel = session.sessions().createClientInitialAccessModel(realm, expiration, count);
+
+ adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientInitialAccessModel.getId()).representation(config).success();
+
+ if (session.getTransaction().isActive()) {
+ session.getTransaction().commit();
+ }
+
+ ClientInitialAccessPresentation rep = wrap(clientInitialAccessModel);
+
+ String token = ClientRegistrationTokenUtils.createInitialAccessToken(realm, uriInfo, clientInitialAccessModel);
+ rep.setToken(token);
+
+ response.setStatus(Response.Status.CREATED.getStatusCode());
+ response.setHeader(HttpHeaders.LOCATION, uriInfo.getAbsolutePathBuilder().path(clientInitialAccessModel.getId()).build().toString());
+
+ return rep;
+ }
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public List<ClientInitialAccessPresentation> list() {
+ List<ClientInitialAccessModel> models = session.sessions().listClientInitialAccess(realm);
+ List<ClientInitialAccessPresentation> reps = new LinkedList<>();
+ for (ClientInitialAccessModel m : models) {
+ ClientInitialAccessPresentation r = wrap(m);
+ reps.add(r);
+ }
+ return reps;
+ }
+
+ @DELETE
+ @Path("{id}")
+ public void delete(final @PathParam("id") String id) {
+ session.sessions().removeClientInitialAccessModel(realm, id);
+ }
+
+ private ClientInitialAccessPresentation wrap(ClientInitialAccessModel model) {
+ ClientInitialAccessPresentation rep = new ClientInitialAccessPresentation();
+ rep.setId(model.getId());
+ rep.setTimestamp(model.getTimestamp());
+ rep.setExpiration(model.getExpiration());
+ rep.setCount(model.getCount());
+ rep.setRemainingCount(model.getRemainingCount());
+ return rep;
+ }
+
+}
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 f729c7f..03b0636 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
@@ -22,6 +22,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
+import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
@@ -228,8 +229,11 @@ public class ClientResource {
public ClientRepresentation regenerateRegistrationAccessToken() {
auth.requireManage();
- KeycloakModelUtils.generateRegistrationAccessToken(client);
+ String token = ClientRegistrationTokenUtils.updateRegistrationAccessToken(realm, uriInfo, client);
+
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
+ rep.setRegistrationAccessToken(token);
+
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).representation(rep).success();
return rep;
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java
index 87253c1..668057e 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java
@@ -45,123 +45,47 @@ public class GroupResource {
private final KeycloakSession session;
private final RealmAuth auth;
private final AdminEventBuilder adminEvent;
+ private final GroupModel group;
- public GroupResource(RealmModel realm, KeycloakSession session, RealmAuth auth, AdminEventBuilder adminEvent) {
+ public GroupResource(RealmModel realm, GroupModel group, KeycloakSession session, RealmAuth auth, AdminEventBuilder adminEvent) {
this.realm = realm;
this.session = session;
this.auth = auth;
this.adminEvent = adminEvent;
+ this.group = group;
}
@Context private UriInfo uriInfo;
- public GroupResource(RealmAuth auth, RealmModel realm, KeycloakSession session, AdminEventBuilder adminEvent) {
- this.realm = realm;
- this.session = session;
- this.auth = auth;
- this.adminEvent = adminEvent;
- }
-
/**
- * Get group hierarchy. Only name and ids are returned.
*
- * @return
- */
- @GET
- @NoCache
- @Produces(MediaType.APPLICATION_JSON)
- public List<GroupRepresentation> getGroups() {
- this.auth.requireView();
- return ModelToRepresentation.toGroupHierarchy(realm, false);
- }
-
- /**
- * Set or create child as a top level group. This will update the group and set the parent if it exists. Create it and set the parent
- * if the group doesn't exist.
*
- * @param rep
- */
- @POST
- @Path("{id}")
- @NoCache
- @Produces(MediaType.APPLICATION_JSON)
- @Consumes(MediaType.APPLICATION_JSON)
- public Response addRealmGroup(@PathParam("id") String parentId, GroupRepresentation rep) {
- GroupModel parentModel = realm.getGroupById(parentId);
- Response.ResponseBuilder builder = Response.status(204);
- if (parentModel == null) {
- throw new NotFoundException("Could not find parent by id");
- }
- GroupModel child = null;
- if (rep.getId() != null) {
- child = realm.getGroupById(rep.getId());
- if (child == null) {
- throw new NotFoundException("Could not find child by id");
- }
- } else {
- child = realm.createGroup(rep.getName());
- updateGroup(rep, child);
- URI uri = uriInfo.getBaseUriBuilder()
- .path(uriInfo.getMatchedURIs().get(1))
- .path(child.getId()).build();
- builder.status(201).location(uri);
-
- }
- child.setParent(parentModel);
- GroupRepresentation childRep = ModelToRepresentation.toRepresentation(child, true);
- return builder.type(MediaType.APPLICATION_JSON_TYPE).entity(childRep).build();
- }
-
-
-
-
- /**
- * Does not expand hierarchy. Subgroups will not be set.
- *
- * @param id
* @return
*/
@GET
- @Path("{id}")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
- public GroupRepresentation getGroupById(@PathParam("id") String id) {
+ public GroupRepresentation getGroup() {
this.auth.requireView();
- GroupModel group = realm.getGroupById(id);
- if (group == null) {
- throw new NotFoundException("Could not find group by id");
- }
-
- return ModelToRepresentation.toRepresentation(group, true);
+ return ModelToRepresentation.toGroupHierarchy(group, true);
}
/**
- * Update group
+ * Update group, ignores subgroups.
*
* @param rep
*/
@PUT
- @Path("{id}")
@Consumes(MediaType.APPLICATION_JSON)
- public void updateGroup(@PathParam("id") String id, GroupRepresentation rep) {
- GroupModel model = realm.getGroupById(id);
- if (model == null) {
- throw new NotFoundException("Could not find group by id");
- }
-
- updateGroup(rep, model);
+ public void updateGroup(GroupRepresentation rep) {
+ updateGroup(rep, group);
}
@DELETE
- @Path("{id}")
- public void deleteGroup(@PathParam("id") String id) {
- GroupModel model = realm.getGroupById(id);
- if (model == null) {
- throw new NotFoundException("Could not find group by id");
- }
- realm.removeGroup(model);
+ public void deleteGroup() {
+ realm.removeGroup(group);
}
@@ -172,16 +96,12 @@ public class GroupResource {
* @param rep
*/
@POST
- @Path("{id}/children")
+ @Path("children")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
- public Response addGroup(@PathParam("id") String parentId, GroupRepresentation rep) {
- GroupModel parentModel = realm.getGroupById(parentId);
+ public Response addChild(GroupRepresentation rep) {
Response.ResponseBuilder builder = Response.status(204);
- if (parentModel == null) {
- throw new NotFoundException("Could not find parent by id");
- }
GroupModel child = null;
if (rep.getId() != null) {
child = realm.getGroupById(rep.getId());
@@ -197,39 +117,12 @@ public class GroupResource {
builder.status(201).location(uri);
}
- realm.moveGroup(child, parentModel);
- GroupRepresentation childRep = ModelToRepresentation.toRepresentation(child, true);
+ realm.moveGroup(child, group);
+ GroupRepresentation childRep = ModelToRepresentation.toGroupHierarchy(child, true);
return builder.type(MediaType.APPLICATION_JSON_TYPE).entity(childRep).build();
}
- /**
- * create or add a top level realm groupSet or create child. This will update the group and set the parent if it exists. Create it and set the parent
- * if the group doesn't exist.
- *
- * @param rep
- */
- @POST
- @Consumes(MediaType.APPLICATION_JSON)
- public Response addTopLevelGroup(GroupRepresentation rep) {
- GroupModel child = null;
- Response.ResponseBuilder builder = Response.status(204);
- if (rep.getId() != null) {
- child = realm.getGroupById(rep.getId());
- if (child == null) {
- throw new NotFoundException("Could not find child by id");
- }
- } else {
- child = realm.createGroup(rep.getName());
- updateGroup(rep, child);
- URI uri = uriInfo.getAbsolutePathBuilder()
- .path(child.getId()).build();
- builder.status(201).location(uri);
- }
- realm.moveGroup(child, null);
- return builder.build();
- }
-
- public void updateGroup(GroupRepresentation rep, GroupModel model) {
+ public static void updateGroup(GroupRepresentation rep, GroupModel model) {
if (rep.getName() != null) model.setName(rep.getName());
if (rep.getAttributes() != null) {
@@ -245,13 +138,8 @@ public class GroupResource {
}
}
- @Path("{id}/role-mappings")
- public RoleMapperResource getRoleMappings(@PathParam("id") String id) {
-
- GroupModel group = session.realms().getGroupById(id, realm);
- if (group == null) {
- throw new NotFoundException("Group not found");
- }
+ @Path("role-mappings")
+ public RoleMapperResource getRoleMappings() {
auth.init(RealmAuth.Resource.USER);
RoleMapperResource resource = new RoleMapperResource(realm, auth, group, adminEvent);
@@ -271,18 +159,11 @@ public class GroupResource {
*/
@GET
@NoCache
- @Path("{id}/members")
+ @Path("members")
@Produces(MediaType.APPLICATION_JSON)
- public List<UserRepresentation> getMembers(@PathParam("id") String id,
- @QueryParam("first") Integer firstResult,
+ public List<UserRepresentation> getMembers(@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults) {
auth.requireView();
-
- GroupModel group = session.realms().getGroupById(id, realm);
- if (group == null) {
- throw new NotFoundException("Group not found");
- }
-
firstResult = firstResult != null ? firstResult : -1;
maxResults = maxResults != null ? maxResults : -1;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java
new file mode 100755
index 0000000..6e3709d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java
@@ -0,0 +1,120 @@
+package org.keycloak.services.resources.admin;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.NotFoundException;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Bill Burke
+ */
+public class GroupsResource {
+
+ private static Logger logger = Logger.getLogger(GroupsResource.class);
+
+ private final RealmModel realm;
+ private final KeycloakSession session;
+ private final RealmAuth auth;
+ private final AdminEventBuilder adminEvent;
+
+ public GroupsResource(RealmModel realm, KeycloakSession session, RealmAuth auth, AdminEventBuilder adminEvent) {
+ this.realm = realm;
+ this.session = session;
+ this.auth = auth;
+ this.adminEvent = adminEvent;
+ }
+
+ @Context private UriInfo uriInfo;
+
+ public GroupsResource(RealmAuth auth, RealmModel realm, KeycloakSession session, AdminEventBuilder adminEvent) {
+ this.realm = realm;
+ this.session = session;
+ this.auth = auth;
+ this.adminEvent = adminEvent;
+ }
+
+ /**
+ * Get group hierarchy. Only name and ids are returned.
+ *
+ * @return
+ */
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public List<GroupRepresentation> getGroups() {
+ this.auth.requireView();
+ return ModelToRepresentation.toGroupHierarchy(realm, false);
+ }
+
+ /**
+ * Does not expand hierarchy. Subgroups will not be set.
+ *
+ * @param id
+ * @return
+ */
+ @Path("{id}")
+ public GroupResource getGroupById(@PathParam("id") String id) {
+ GroupModel group = realm.getGroupById(id);
+ if (group == null) {
+ throw new NotFoundException("Could not find group by id");
+ }
+
+ GroupResource resource = new GroupResource(realm, group, session, this.auth, adminEvent);
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+ return resource;
+ }
+
+ /**
+ * create or add a top level realm groupSet or create child. This will update the group and set the parent if it exists. Create it and set the parent
+ * if the group doesn't exist.
+ *
+ * @param rep
+ */
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response addTopLevelGroup(GroupRepresentation rep) {
+ GroupModel child = null;
+ Response.ResponseBuilder builder = Response.status(204);
+ if (rep.getId() != null) {
+ child = realm.getGroupById(rep.getId());
+ if (child == null) {
+ throw new NotFoundException("Could not find child by id");
+ }
+ } else {
+ child = realm.createGroup(rep.getName());
+ GroupResource.updateGroup(rep, child);
+ URI uri = uriInfo.getAbsolutePathBuilder()
+ .path(child.getId()).build();
+ builder.status(201).location(uri);
+ }
+ realm.moveGroup(child, null);
+ return builder.build();
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java
index 306d350..f53d256 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java
@@ -6,10 +6,12 @@ import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.services.ErrorResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -86,9 +88,17 @@ public class ProtocolMappersResource {
@Consumes(MediaType.APPLICATION_JSON)
public Response createMapper(ProtocolMapperRepresentation rep) {
auth.requireManage();
- ProtocolMapperModel model = RepresentationToModel.toModel(rep);
- model = client.addProtocolMapper(model);
- adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId()).representation(rep).success();
+
+ ProtocolMapperModel model = null;
+ try {
+ model = RepresentationToModel.toModel(rep);
+ model = client.addProtocolMapper(model);
+ adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId()).representation(rep).success();
+
+ } catch (ModelDuplicateException e) {
+ return ErrorResponse.exists("Protocol mapper exists with same name");
+ }
+
return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build();
}
/**
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 6ef9e1e..90eab4a 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -16,6 +16,7 @@ import org.keycloak.events.admin.OperationType;
import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
@@ -23,12 +24,14 @@ import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.CacheUserProvider;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
@@ -141,6 +144,18 @@ public class RealmAdminResource {
}
/**
+ * Base path for managing client initial access tokens
+ *
+ * @return
+ */
+ @Path("clients-initial-access")
+ public ClientInitialAccessResource getClientInitialAccess() {
+ ClientInitialAccessResource resource = new ClientInitialAccessResource(realm, auth, adminEvent);
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+ return resource;
+ }
+
+ /**
* base path for managing realm-level roles of this realm
*
* @return
@@ -221,7 +236,7 @@ public class RealmAdminResource {
} catch (ModelDuplicateException e) {
throw e;
} catch (Exception e) {
- logger.error(e);
+ logger.error(e.getMessage(), e);
return ErrorResponse.error("Failed to update " + rep.getRealm() + " Realm.", Response.Status.INTERNAL_SERVER_ERROR);
}
}
@@ -619,11 +634,69 @@ public class RealmAdminResource {
return new IdentityProvidersResource(realm, session, this.auth, adminEvent);
}
+ /**
+ * Get group hierarchy. Only name and ids are returned.
+ *
+ * @return
+ */
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ @Path("default-groups")
+ public List<GroupRepresentation> getDefaultGroups() {
+ this.auth.requireView();
+ List<GroupRepresentation> defaults = new LinkedList<>();
+ for (GroupModel group : realm.getDefaultGroups()) {
+ defaults.add(ModelToRepresentation.toRepresentation(group, false));
+ }
+ return defaults;
+ }
+ @PUT
+ @NoCache
+ @Path("default-groups/{groupId}")
+ public void addDefaultGroup(@PathParam("groupId") String groupId) {
+ this.auth.requireManage();
+ GroupModel group = realm.getGroupById(groupId);
+ if (group == null) {
+ throw new NotFoundException("Group not found");
+ }
+ realm.addDefaultGroup(group);
+ }
+
+ @DELETE
+ @NoCache
+ @Path("default-groups/{groupId}")
+ public void removeDefaultGroup(@PathParam("groupId") String groupId) {
+ this.auth.requireManage();
+ GroupModel group = realm.getGroupById(groupId);
+ if (group == null) {
+ throw new NotFoundException("Group not found");
+ }
+ realm.removeDefaultGroup(group);
+ }
+
+
@Path("groups")
- public GroupResource getGroups() {
- GroupResource resource = new GroupResource(realm, session, this.auth, adminEvent);
+ public GroupsResource getGroups() {
+ GroupsResource resource = new GroupsResource(realm, session, this.auth, adminEvent);
ResteasyProviderFactory.getInstance().injectProperties(resource);
return resource;
}
+
+ @GET
+ @Path("group-by-path/{path: .*}")
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public GroupRepresentation getGroupByPath(@PathParam("path") String path) {
+ this.auth.requireView();
+ GroupModel found = KeycloakModelUtils.findGroupByPath(realm, path);
+ if (found == null) {
+ throw new NotFoundException("Group path does not exist");
+
+ }
+ return ModelToRepresentation.toGroupHierarchy(found, true);
+ }
+
+
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java
index 4423c54..dfd2119 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java
@@ -2,61 +2,37 @@ package org.keycloak.services.resources.admin;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
-import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.common.ClientConnection;
-import org.keycloak.email.EmailException;
-import org.keycloak.email.EmailProvider;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelReadOnlyException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleMapperModel;
import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserCredentialModel;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.ModelToRepresentation;
-import org.keycloak.models.utils.RepresentationToModel;
-import org.keycloak.protocol.oidc.OIDCLoginProtocol;
-import org.keycloak.protocol.oidc.TokenManager;
-import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.representations.idm.ClientMappingsRepresentation;
-import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
-import org.keycloak.services.ErrorResponse;
-import org.keycloak.services.Urls;
import org.keycloak.services.managers.BruteForceProtector;
-import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
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.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
/**
* Base resource for managing users
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 a85c9d2..67bd67e 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
@@ -8,7 +8,7 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.ClientConnection;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.email.EmailException;
-import org.keycloak.email.EmailProvider;
+import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
@@ -23,7 +23,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelReadOnlyException;
import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
@@ -34,18 +33,14 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.provider.ProviderFactory;
-import org.keycloak.representations.idm.ClientMappingsRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
-import org.keycloak.representations.idm.MappingsRepresentation;
-import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserConsentRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
-import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.UserManager;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.Urls;
@@ -148,8 +143,8 @@ public class UsersResource {
attrsToRemove = Collections.emptySet();
}
- if (rep.isEnabled()) {
- UsernameLoginFailureModel failureModel = session.sessions().getUserLoginFailure(realm, rep.getUsername());
+ if (rep.isEnabled() != null && rep.isEnabled()) {
+ UsernameLoginFailureModel failureModel = session.sessions().getUserLoginFailure(realm, rep.getUsername().toLowerCase());
if (failureModel != null) {
failureModel.clearFailures();
}
@@ -219,9 +214,9 @@ public class UsersResource {
user.setFirstName(rep.getFirstName());
user.setLastName(rep.getLastName());
- user.setEnabled(rep.isEnabled());
- user.setOtpEnabled(rep.isTotp());
- user.setEmailVerified(rep.isEmailVerified());
+ if (rep.isEnabled() != null) user.setEnabled(rep.isEnabled());
+ if (rep.isTotp() != null) user.setOtpEnabled(rep.isTotp());
+ if (rep.isEmailVerified() != null) user.setEmailVerified(rep.isEmailVerified());
List<String> reqActions = rep.getRequiredActions();
@@ -708,7 +703,7 @@ public class UsersResource {
} catch (ModelReadOnlyException mre) {
throw new BadRequestException("Can't reset password as account is read only");
}
- if (pass.isTemporary()) user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+ if (pass.isTemporary() != null && pass.isTemporary()) user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
}
@@ -804,7 +799,7 @@ public class UsersResource {
String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
- this.session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendExecuteActions(link, expiration);
+ this.session.getProvider(EmailTemplateProvider.class).setRealm(realm).setUser(user).sendExecuteActions(link, expiration);
//audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getCodeId()).success();
@@ -833,40 +828,9 @@ public class UsersResource {
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public Response sendVerifyEmail(@PathParam("id") String id, @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) {
- auth.requireManage();
-
- UserModel user = session.users().getUserById(id, realm);
- if (user == null) {
- return ErrorResponse.error("User not found", Response.Status.NOT_FOUND);
- }
-
- if (user.getEmail() == null) {
- return ErrorResponse.error("User email missing", Response.Status.BAD_REQUEST);
- }
-
- ClientSessionModel clientSession = createClientSession(user, redirectUri, clientId);
- ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession);
-
- accessCode.setAction(ClientSessionModel.Action.VERIFY_EMAIL.name());
-
- try {
- UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
- builder.queryParam("key", accessCode.getCode());
-
- String link = builder.build(realm.getName()).toString();
- long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
-
- this.session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendVerifyEmail(link, expiration);
-
- //audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getCodeId()).success();
-
- adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
-
- return Response.ok().build();
- } catch (EmailException e) {
- logger.error("Failed to send verification email", e);
- return ErrorResponse.error("Failed to send email", Response.Status.INTERNAL_SERVER_ERROR);
- }
+ 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) {
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 5912536..b31d121 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -446,7 +446,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@Override
public Response cancelled(String code) {
ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel);
- if (clientCode.getClientSession() == null || !clientCode.isValid(AUTHENTICATE.name())) {
+ if (clientCode.getClientSession() == null || !clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return redirectToErrorPage(Messages.INVALID_CODE);
}
@@ -456,7 +456,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@Override
public Response error(String code, String message) {
ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel);
- if (clientCode.getClientSession() == null || !clientCode.isValid(AUTHENTICATE.name())) {
+ if (clientCode.getClientSession() == null || !clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return redirectToErrorPage(Messages.INVALID_CODE);
}
return browserAuthentication(clientCode.getClientSession(), message);
@@ -522,7 +522,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
private ClientSessionCode parseClientSessionCode(String code) {
ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel);
- if (clientCode != null && clientCode.isValid(AUTHENTICATE.name())) {
+ if (clientCode != null && clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
ClientSessionModel clientSession = clientCode.getClientSession();
if (clientSession != null) {
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index a07999d..c26e60c 100755
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -2,6 +2,7 @@ package org.keycloak.services.resources;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.type.TypeReference;
import org.jboss.logging.Logger;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
@@ -11,9 +12,11 @@ import org.keycloak.migration.MigrationModelManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
import org.keycloak.models.utils.PostMigrationEvent;
-import org.keycloak.offlineconfig.AdminRecovery;
+import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.DefaultKeycloakSessionFactory;
import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.managers.BruteForceProtector;
@@ -36,10 +39,7 @@ import javax.ws.rs.core.UriInfo;
import java.io.*;
import java.net.URI;
import java.net.URL;
-import java.util.HashSet;
-import java.util.Properties;
-import java.util.Set;
-import java.util.StringTokenizer;
+import java.util.*;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -81,7 +81,7 @@ public class KeycloakApplication extends Application {
singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));
- setupDefaultRealm(context.getContextPath());
+ boolean defaultRealmCreated = ApplianceBootstrap.setupDefaultRealm(sessionFactory, context.getContextPath());
migrateModel();
sessionFactory.publish(new PostMigrationEvent());
@@ -89,7 +89,11 @@ public class KeycloakApplication extends Application {
new ExportImportManager().checkExportImport(this.sessionFactory, context.getContextPath());
importRealms(context);
- AdminRecovery.recover(sessionFactory);
+ importAddUser();
+
+ if (defaultRealmCreated) {
+ ApplianceBootstrap.setupDefaultUser(sessionFactory);
+ }
setupScheduledTasks(sessionFactory);
}
@@ -137,8 +141,10 @@ public class KeycloakApplication extends Application {
if (node == null) {
URL resource = Thread.currentThread().getContextClassLoader().getResource("META-INF/keycloak-server.json");
- log.info("Load config from " + resource);
- node = new ObjectMapper().readTree(resource);
+ if (resource != null) {
+ log.info("Load config from " + resource);
+ node = new ObjectMapper().readTree(resource);
+ }
}
if (node != null) {
@@ -146,17 +152,13 @@ public class KeycloakApplication extends Application {
Config.init(new JsonConfigProvider(node, properties));
return;
} else {
- log.warn("Config 'keycloak-server.json' not found");
+ throw new RuntimeException("Config 'keycloak-server.json' not found");
}
} catch (IOException e) {
throw new RuntimeException("Failed to load config", e);
}
}
- protected void setupDefaultRealm(String contextPath) {
- new ApplianceBootstrap().bootstrap(sessionFactory, contextPath);
- }
-
public static KeycloakSessionFactory createSessionFactory() {
DefaultKeycloakSessionFactory factory = new DefaultKeycloakSessionFactory();
factory.init();
@@ -254,6 +256,44 @@ public class KeycloakApplication extends Application {
}
}
+ public void importAddUser() {
+ String configDir = System.getProperty("jboss.server.config.dir");
+ if (configDir != null) {
+ File addUserFile = new File(configDir + File.separator + "keycloak-add-user.json");
+ if (addUserFile.isFile()) {
+ log.info("Importing users from '" + addUserFile + "'");
+
+ KeycloakSession session = sessionFactory.create();
+ try {
+ session.getTransaction().begin();
+
+ List<RealmRepresentation> realms = JsonSerialization.readValue(new FileInputStream(addUserFile), new TypeReference<List<RealmRepresentation>>() {});
+ for (RealmRepresentation r : realms) {
+ RealmModel realm = session.realms().getRealmByName(r.getRealm());
+ if (realm == null) {
+ throw new Exception("Realm '" + r.getRealm() + "' not found");
+ }
+
+ for (UserRepresentation u : r.getUsers()) {
+ RepresentationToModel.createUser(session, realm, u, realm.getClientNameMap());
+ }
+ }
+
+ session.getTransaction().commit();
+
+ if (!addUserFile.delete()) {
+ log.error("Failed to delete '" + addUserFile + "'");
+ }
+ } catch (Throwable t) {
+ session.getTransaction().rollback();
+ log.error("Failed to import users from '" + addUserFile + "'", t);
+ } finally {
+ session.close();
+ }
+ }
+ }
+ }
+
private static <T> T loadJson(InputStream is, Class<T> type) {
try {
return JsonSerialization.readValue(is, type);
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 7f15d2d..36e4c6c 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -57,7 +57,10 @@ import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie;
+import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
@@ -164,25 +167,25 @@ public class LoginActionsService {
ClientSessionCode clientCode;
Response response;
- boolean verifyCode(String code, String requiredAction) {
+ boolean verifyCode(String code, String requiredAction, ClientSessionCode.ActionType actionType) {
if (!verifyCode(code)) {
return false;
}
- if (!verifyAction(requiredAction)) {
+ if (!verifyAction(requiredAction, actionType)) {
return false;
} else {
return true;
}
}
- public boolean verifyAction(String requiredAction) {
+ public boolean verifyAction(String requiredAction, ClientSessionCode.ActionType actionType) {
if (!clientCode.isValidAction(requiredAction)) {
event.client(clientCode.getClientSession().getClient());
event.error(Errors.INVALID_CODE);
response = ErrorPage.error(session, Messages.INVALID_CODE);
return false;
}
- if (!clientCode.isActionActive(requiredAction)) {
+ if (!clientCode.isActionActive(actionType)) {
event.client(clientCode.getClientSession().getClient());
event.clone().error(Errors.EXPIRED_CODE);
if (clientCode.getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
@@ -263,7 +266,7 @@ public class LoginActionsService {
@QueryParam("execution") String execution) {
event.event(EventType.LOGIN);
Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
+ if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
event.detail(Details.CODE_ID, code);
@@ -314,7 +317,7 @@ public class LoginActionsService {
@QueryParam("execution") String execution) {
event.event(EventType.LOGIN);
Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
+ if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
final ClientSessionCode clientCode = checks.clientCode;
@@ -373,7 +376,7 @@ public class LoginActionsService {
protected Response resetCredentials(String code, String execution) {
event.event(EventType.RESET_PASSWORD);
Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
+ if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
final ClientSessionCode clientCode = checks.clientCode;
@@ -437,7 +440,7 @@ public class LoginActionsService {
}
Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
+ if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
event.detail(Details.CODE_ID, code);
@@ -467,7 +470,7 @@ public class LoginActionsService {
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
}
Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
+ if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
@@ -495,7 +498,7 @@ public class LoginActionsService {
event.event(EventType.IDENTITY_PROVIDER_FIRST_LOGIN);
Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
+ if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
event.detail(Details.CODE_ID, code);
@@ -545,7 +548,7 @@ public class LoginActionsService {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processConsent(final MultivaluedMap<String, String> formData) {
- event.event(EventType.LOGIN).detail(Details.RESPONSE_TYPE, "code");
+ event.event(EventType.LOGIN);
if (!checkSsl()) {
@@ -555,43 +558,33 @@ public class LoginActionsService {
String code = formData.getFirst("code");
ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm);
- if (accessCode == null || !accessCode.isValid(ClientSessionModel.Action.OAUTH_GRANT.name())) {
+ if (accessCode == null || !accessCode.isValid(ClientSessionModel.Action.OAUTH_GRANT.name(), ClientSessionCode.ActionType.LOGIN)) {
event.error(Errors.INVALID_CODE);
return ErrorPage.error(session, Messages.INVALID_ACCESS_CODE);
}
ClientSessionModel clientSession = accessCode.getClientSession();
- event.detail(Details.CODE_ID, clientSession.getId());
- String redirect = clientSession.getRedirectUri();
+ initEvent(clientSession);
+
UserSessionModel userSession = clientSession.getUserSession();
UserModel user = userSession.getUser();
ClientModel client = clientSession.getClient();
- event.client(client)
- .user(user)
- .detail(Details.RESPONSE_TYPE, "code")
- .detail(Details.REDIRECT_URI, redirect);
-
- event.detail(Details.AUTH_METHOD, userSession.getAuthMethod());
- event.detail(Details.USERNAME, userSession.getLoginUsername());
- if (userSession.isRememberMe()) {
- event.detail(Details.REMEMBER_ME, "true");
- }
-
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
event.error(Errors.INVALID_CODE);
return ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
}
- event.session(userSession);
if (formData.containsKey("cancel")) {
LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
protocol.setRealm(realm)
.setHttpHeaders(headers)
- .setUriInfo(uriInfo);
+ .setUriInfo(uriInfo)
+ .setEventBuilder(event);
+ Response response = protocol.sendError(clientSession, Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
- return protocol.consentDenied(clientSession);
+ return response;
}
UserConsentModel grantedConsent = user.getConsentByClient(client.getId());
@@ -612,7 +605,7 @@ public class LoginActionsService {
event.detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED);
event.success();
- return authManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection);
+ return authManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection, event);
}
@Path("email-verification")
@@ -621,7 +614,7 @@ public class LoginActionsService {
event.event(EventType.VERIFY_EMAIL);
if (key != null) {
Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) {
+ if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
@@ -664,7 +657,7 @@ public class LoginActionsService {
return AuthenticationProcessor.createRequiredActionRedirect(realm, clientSession, uriInfo);
} else {
Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) {
+ if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
@@ -696,7 +689,7 @@ public class LoginActionsService {
event.event(EventType.EXECUTE_ACTIONS);
if (key != null) {
Checks checks = new Checks();
- if (!checks.verifyCode(key, ClientSessionModel.Action.EXECUTE_ACTIONS.name())) {
+ if (!checks.verifyCode(key, ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
ClientSessionModel clientSession = checks.clientCode.getClientSession();
@@ -726,22 +719,27 @@ public class LoginActionsService {
}
private void initEvent(ClientSessionModel clientSession) {
+ UserSessionModel userSession = clientSession.getUserSession();
+
+ String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
+ if (responseType == null) {
+ responseType = "code";
+ }
+ String respMode = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
+ OIDCResponseMode responseMode = OIDCResponseMode.parse(respMode, OIDCResponseType.parse(responseType));
+
event.event(EventType.LOGIN).client(clientSession.getClient())
- .user(clientSession.getUserSession().getUser())
- .session(clientSession.getUserSession().getId())
+ .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.RESPONSE_TYPE, "code");
-
- UserSessionModel userSession = clientSession.getUserSession();
-
- if (userSession != null) {
- event.detail(Details.AUTH_METHOD, userSession.getAuthMethod());
- event.detail(Details.USERNAME, userSession.getLoginUsername());
- if (userSession.isRememberMe()) {
- event.detail(Details.REMEMBER_ME, "true");
- }
+ .detail(Details.AUTH_METHOD, userSession.getAuthMethod())
+ .detail(Details.USERNAME, userSession.getLoginUsername())
+ .detail(Details.RESPONSE_TYPE, responseType)
+ .detail(Details.RESPONSE_MODE, responseMode.toString().toLowerCase());
+ if (userSession.isRememberMe()) {
+ event.detail(Details.REMEMBER_ME, "true");
}
}
@@ -766,7 +764,7 @@ public class LoginActionsService {
event.event(EventType.CUSTOM_REQUIRED_ACTION);
event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name())) {
+ if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
final ClientSessionCode clientCode = checks.clientCode;
@@ -826,9 +824,14 @@ public class LoginActionsService {
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
- .setUriInfo(context.getUriInfo());
- event.detail(Details.CUSTOM_REQUIRED_ACTION, action).error(Errors.REJECTED_BY_USER);
- return protocol.consentDenied(context.getClientSession());
+ .setUriInfo(context.getUriInfo())
+ .setEventBuilder(event);
+
+ event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
+ Response response = protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED);
+ event.error(Errors.REJECTED_BY_USER);
+ return response;
+
}
throw new RuntimeException("Unreachable");
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index 4b5e4f2..a1a3acc 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -3,6 +3,7 @@ package org.keycloak.services.resources;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.Config;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
@@ -17,6 +18,7 @@ import org.keycloak.services.clientregistration.ClientRegistrationService;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.wellknown.WellKnownProvider;
import javax.ws.rs.GET;
@@ -24,11 +26,7 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.core.*;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -59,6 +57,10 @@ public class RealmsResource {
return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getProtocol");
}
+ public static UriBuilder clientRegistrationUrl(UriInfo uriInfo) {
+ return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getClientsService");
+ }
+
public static UriBuilder brokerUrl(UriInfo uriInfo) {
return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getBrokerService");
}
@@ -186,7 +188,7 @@ public class RealmsResource {
init(name);
WellKnownProvider wellKnown = session.getProvider(WellKnownProvider.class, providerName);
- return Response.ok(wellKnown.getConfig()).build();
+ return Response.ok(wellKnown.getConfig()).cacheControl(CacheControlUtil.getDefaultCacheControl()).build();
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java
index 62d5a15..092505f 100755
--- a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java
@@ -7,6 +7,7 @@ import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.common.util.MimeTypeUtil;
+import org.keycloak.services.util.CacheControlUtil;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@@ -49,11 +50,7 @@ public class ThemeResource {
Theme theme = themeProvider.getTheme(themeName, Theme.Type.valueOf(themType.toUpperCase()));
InputStream resource = theme.getResourceAsStream(path);
if (resource != null) {
- CacheControl cacheControl = new CacheControl();
- cacheControl.setNoTransform(false);
- cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1));
-
- return Response.ok(resource).type(MimeTypeUtil.getContentType(path)).cacheControl(cacheControl).build();
+ return Response.ok(resource).type(MimeTypeUtil.getContentType(path)).cacheControl(CacheControlUtil.getDefaultCacheControl()).build();
} else {
return Response.status(Response.Status.NOT_FOUND).build();
}
diff --git a/services/src/main/java/org/keycloak/services/util/CacheControlUtil.java b/services/src/main/java/org/keycloak/services/util/CacheControlUtil.java
new file mode 100644
index 0000000..259083a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/util/CacheControlUtil.java
@@ -0,0 +1,25 @@
+package org.keycloak.services.util;
+
+import org.keycloak.Config;
+
+import javax.ws.rs.core.CacheControl;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class CacheControlUtil {
+
+ public static CacheControl getDefaultCacheControl() {
+ CacheControl cacheControl = new CacheControl();
+ cacheControl.setNoTransform(false);
+ Integer maxAge = Config.scope("theme").getInt("staticMaxAge");
+ if (maxAge != null && maxAge > 0) {
+ cacheControl.setMaxAge(maxAge);
+ } else {
+ cacheControl.setNoCache(true);
+ }
+ return cacheControl;
+
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/util/LocaleHelper.java b/services/src/main/java/org/keycloak/services/util/LocaleHelper.java
index 0a15bf1..eeefc81 100644
--- a/services/src/main/java/org/keycloak/services/util/LocaleHelper.java
+++ b/services/src/main/java/org/keycloak/services/util/LocaleHelper.java
@@ -113,15 +113,7 @@ public class LocaleHelper {
RealmModel realm,
String locale) {
boolean secure = realm.getSslRequired().isRequired(session.getContext().getUri().getRequestUri().getHost());
- addCookie(LOCALE_COOKIE, locale, AuthenticationManager.getRealmCookiePath(realm, session.getContext().getUri()), null, null, -1, secure, true);
- }
-
- private static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) {
- HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class);
- StringBuffer cookieBuf = new StringBuffer();
- ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure, httpOnly);
- String cookie = cookieBuf.toString();
- response.getOutputHeaders().add(HttpHeaders.SET_COOKIE, cookie);
+ CookieHelper.addCookie(LOCALE_COOKIE, locale, AuthenticationManager.getRealmCookiePath(realm, session.getContext().getUri()), null, null, -1, secure, true);
}
private static Locale findLocale(Set<String> supportedLocales, String... localeStrings) {
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.email.EmailSenderProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.email.EmailSenderProviderFactory
new file mode 100644
index 0000000..42c5890
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.keycloak.email.EmailSenderProviderFactory
@@ -0,0 +1 @@
+org.keycloak.email.DefaultEmailSenderProviderFactory
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
index e7a6450..59f0f29 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
+++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
@@ -6,5 +6,6 @@ org.keycloak.protocol.oidc.mappers.HardcodedClaim
org.keycloak.protocol.oidc.mappers.HardcodedRole
org.keycloak.protocol.oidc.mappers.RoleNameMapper
org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper
+org.keycloak.protocol.oidc.mappers.GroupMembershipMapper
diff --git a/services/src/test/java/org/keycloak/test/ResponseTypeTest.java b/services/src/test/java/org/keycloak/test/ResponseTypeTest.java
new file mode 100644
index 0000000..b3f77a7
--- /dev/null
+++ b/services/src/test/java/org/keycloak/test/ResponseTypeTest.java
@@ -0,0 +1,41 @@
+package org.keycloak.test;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ResponseTypeTest {
+
+ @Test
+ public void testResponseTypes() {
+ assertFail(null);
+ assertFail("");
+ assertFail("foo");
+ assertSuccess("code");
+ assertSuccess("none");
+ assertFail("id_token");
+ assertFail("token");
+ assertFail("refresh_token");
+ assertSuccess("id_token token");
+ assertSuccess("code token");
+ assertSuccess("code id_token");
+ assertSuccess("code id_token token");
+ assertFail("code none");
+ assertFail("code refresh_token");
+ }
+
+ private void assertSuccess(String responseType) {
+ OIDCResponseType.parse(responseType);
+ }
+
+ private void assertFail(String responseType) {
+ try {
+ OIDCResponseType.parse(responseType);
+ Assert.fail("Not expected to parse '" + responseType + "' with success");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+}
diff --git a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
index 5a4b47f..8e9ad53 100755
--- a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
+++ b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
@@ -166,7 +166,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
private ClientSessionCode parseClientSessionCode(String code) {
ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realm);
- if (clientCode != null && clientCode.isValid(AUTHENTICATE.name())) {
+ if (clientCode != null && clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
ClientSessionModel clientSession = clientCode.getClientSession();
if (clientSession != null) {
testsuite/docker-cluster/pom.xml 4(+2 -2)
diff --git a/testsuite/docker-cluster/pom.xml b/testsuite/docker-cluster/pom.xml
index f8e5055..00414bf 100755
--- a/testsuite/docker-cluster/pom.xml
+++ b/testsuite/docker-cluster/pom.xml
@@ -21,7 +21,7 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<type>zip</type>
</dependency>
<dependency>
@@ -69,7 +69,7 @@
</artifactItem>
<artifactItem>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<type>zip</type>
<version>${project.version}</version>
<outputDirectory>${project.build.directory}/wildfly-adapter</outputDirectory>
testsuite/integration/pom.xml 4(+4 -0)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index a14a006..2865040 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -32,6 +32,10 @@
<artifactId>keycloak-admin-client</artifactId>
</dependency>
<dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-wildfly-adduser</artifactId>
+ </dependency>
+ <dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 9458998..d02c255 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -28,6 +28,7 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
import org.keycloak.events.Event;
import org.keycloak.events.EventType;
import org.keycloak.migration.MigrationModel;
@@ -468,6 +469,104 @@ public class AccountTest {
}
}
+ @Test
+ public void changeUsernameLoginWithOldUsername() {
+ KeycloakSession session = keycloakRule.startSession();
+ UserModel user = session.users().addUser(session.realms().getRealm("test"), "change-username");
+ user.setEnabled(true);
+ user.setEmail("change-username@localhost");
+ user.setFirstName("first");
+ user.setLastName("last");
+ user.updateCredential(UserCredentialModel.password("password"));
+ keycloakRule.stopSession(session, true);
+
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setEditUsernameAllowed(true);
+ }
+ });
+
+ try {
+ profilePage.open();
+ loginPage.login("change-username", "password");
+
+ profilePage.updateUsername("change-username-updated");
+
+ Assert.assertEquals("Your account has been updated.", profilePage.getSuccess());
+
+ profilePage.logout();
+
+ profilePage.open();
+
+ Assert.assertTrue(loginPage.isCurrent());
+
+ loginPage.login("change-username", "password");
+
+ Assert.assertTrue(loginPage.isCurrent());
+ Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+ loginPage.login("change-username-updated", "password");
+ } finally {
+ events.clear();
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setEditUsernameAllowed(false);
+ }
+ });
+ }
+ }
+
+ @Test
+ public void changeEmailLoginWithOldEmail() {
+ KeycloakSession session = keycloakRule.startSession();
+ UserModel user = session.users().addUser(session.realms().getRealm("test"), "change-email");
+ user.setEnabled(true);
+ user.setEmail("change-username@localhost");
+ user.setFirstName("first");
+ user.setLastName("last");
+ user.updateCredential(UserCredentialModel.password("password"));
+ keycloakRule.stopSession(session, true);
+
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setEditUsernameAllowed(true);
+ }
+ });
+
+ try {
+ profilePage.open();
+ loginPage.login("change-username@localhost", "password");
+
+ profilePage.updateEmail("change-username-updated@localhost");
+
+ Assert.assertEquals("Your account has been updated.", profilePage.getSuccess());
+
+ profilePage.logout();
+
+ profilePage.open();
+
+ Assert.assertTrue(loginPage.isCurrent());
+
+ loginPage.login("change-username@localhost", "password");
+
+ Assert.assertTrue(loginPage.isCurrent());
+ Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+ loginPage.login("change-username-updated@localhost", "password");
+ } finally {
+ events.clear();
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setEditUsernameAllowed(false);
+ }
+ });
+ }
+ }
+
// KEYCLOAK-1534
@Test
public void changeEmailToExisting() {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index c7f075f..be609b8 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -277,7 +277,7 @@ public class RequiredActionEmailVerificationTest {
.assertEvent();
}
- private String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
+ public static String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
Multipart multipart = (Multipart) message.getContent();
final String textContentType = multipart.getBodyPart(0).getContentType();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
index 816be92..31b0550 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
@@ -24,6 +24,7 @@ package org.keycloak.testsuite.adapter;
import org.junit.Assert;
import org.junit.rules.ExternalResource;
import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.OIDCAuthenticationError;
import org.keycloak.common.Version;
import org.keycloak.representations.VersionRepresentation;
import org.keycloak.admin.client.Keycloak;
@@ -42,6 +43,7 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AccountSessionsPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
+import org.keycloak.testsuite.rule.ErrorServlet;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
@@ -385,6 +387,7 @@ public class AdapterTestStrategy extends ExternalResource {
* @throws Exception
*/
public void testNullBearerTokenCustomErrorPage() throws Exception {
+ ErrorServlet.authError = null;
Client client = ClientBuilder.newClient();
WebTarget target = client.target(APP_SERVER_BASE_URL + "/customer-db-error-page/");
@@ -396,11 +399,15 @@ public class AdapterTestStrategy extends ExternalResource {
response.close();
response = client.target(location).request().get();
}
- Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(401, response.getStatus());
String errorPageResponse = response.readEntity(String.class);
Assert.assertTrue(errorPageResponse.contains("Error Page"));
response.close();
+ Assert.assertNotNull(ErrorServlet.authError);
+ OIDCAuthenticationError error = (OIDCAuthenticationError)ErrorServlet.authError;
+ Assert.assertEquals(OIDCAuthenticationError.Reason.NO_BEARER_TOKEN, error.getReason());
+ ErrorServlet.authError = null;
response = target.request().header(HttpHeaders.AUTHORIZATION, "Bearer null").get();
// TODO: follow redirects automatically if possible
if (response.getStatus() == 302) {
@@ -408,10 +415,13 @@ public class AdapterTestStrategy extends ExternalResource {
response.close();
response = client.target(location).request().get();
}
- Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(401, response.getStatus());
errorPageResponse = response.readEntity(String.class);
Assert.assertTrue(errorPageResponse.contains("Error Page"));
response.close();
+ Assert.assertNotNull(ErrorServlet.authError);
+ error = (OIDCAuthenticationError)ErrorServlet.authError;
+ Assert.assertEquals(OIDCAuthenticationError.Reason.INVALID_TOKEN, error.getReason());
client.close();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adduser/AddUserTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adduser/AddUserTest.java
new file mode 100644
index 0000000..c3f68db
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adduser/AddUserTest.java
@@ -0,0 +1,64 @@
+package org.keycloak.testsuite.adduser;
+
+import org.junit.*;
+import org.junit.rules.TemporaryFolder;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.models.Constants;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.KeycloakServer;
+import org.keycloak.wildfly.adduser.AddUser;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AddUserTest {
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ private File dir;
+
+ @Before
+ public void before() throws IOException {
+ dir = folder.newFolder();
+ System.setProperty("jboss.server.config.user.dir", dir.getAbsolutePath());
+ System.setProperty("jboss.server.config.dir", dir.getAbsolutePath());
+ }
+
+ @After
+ public void after() {
+ System.getProperties().remove("jboss.server.config.user.dir");
+ System.getProperties().remove("jboss.server.config.dir");
+ }
+
+ @Test
+ public void addUserTest() throws Throwable {
+ AddUser.main(new String[]{"-u", "addusertest-admin", "-p", "password"});
+ Assert.assertEquals(1, dir.listFiles().length);
+
+ KeycloakServer server = new KeycloakServer();
+ try {
+ server.start();
+
+ Keycloak keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "addusertest-admin", "password", Constants.ADMIN_CONSOLE_CLIENT_ID);
+ keycloak.realms().findAll();
+
+ RealmRepresentation testRealm = new RealmRepresentation();
+ testRealm.setEnabled(true);
+ testRealm.setId("test");
+ testRealm.setRealm("test");
+
+ keycloak.realms().create(testRealm);
+
+ keycloak.close();
+
+ Assert.assertEquals(0, dir.listFiles().length);
+ } finally {
+ server.stop();
+ }
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
index 9e51065..8147cb3 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
@@ -255,6 +255,7 @@ public class AdminAPITest {
Assert.assertEquals(rep.getAccessCodeLifespanUserAction(), storedRealm.getAccessCodeLifespanUserAction());
if (rep.getNotBefore() != null) Assert.assertEquals(rep.getNotBefore(), storedRealm.getNotBefore());
if (rep.getAccessTokenLifespan() != null) Assert.assertEquals(rep.getAccessTokenLifespan(), storedRealm.getAccessTokenLifespan());
+ if (rep.getAccessTokenLifespanForImplicitFlow() != null) Assert.assertEquals(rep.getAccessTokenLifespanForImplicitFlow(), storedRealm.getAccessTokenLifespanForImplicitFlow());
if (rep.getSsoSessionIdleTimeout() != null) Assert.assertEquals(rep.getSsoSessionIdleTimeout(), storedRealm.getSsoSessionIdleTimeout());
if (rep.getSsoSessionMaxLifespan() != null) Assert.assertEquals(rep.getSsoSessionMaxLifespan(), storedRealm.getSsoSessionMaxLifespan());
if (rep.getRequiredCredentials() != null) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientInitialAccessTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientInitialAccessTest.java
new file mode 100644
index 0000000..79ef1ac
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientInitialAccessTest.java
@@ -0,0 +1,58 @@
+package org.keycloak.testsuite.admin;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.ClientInitialAccessResource;
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
+
+import javax.ws.rs.core.Response;
+import java.net.URI;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientInitialAccessTest extends AbstractClientTest {
+
+ @Test
+ public void create() {
+ ClientInitialAccessResource resource = keycloak.realm(REALM_NAME).clientInitialAccess();
+
+ ClientInitialAccessPresentation access = resource.create(new ClientInitialAccessCreatePresentation(1000, 2));
+ Assert.assertEquals(new Integer(2), access.getCount());
+ Assert.assertEquals(new Integer(2), access.getRemainingCount());
+ Assert.assertEquals(new Integer(1000), access.getExpiration());
+ Assert.assertNotNull(access.getTimestamp());
+ Assert.assertNotNull(access.getToken());
+
+ ClientInitialAccessPresentation access2 = resource.create(new ClientInitialAccessCreatePresentation());
+
+ List<ClientInitialAccessPresentation> list = resource.list();
+ Assert.assertEquals(2, list.size());
+
+ for (ClientInitialAccessPresentation r : list) {
+ if (r.getId().equals(access.getId())) {
+ Assert.assertEquals(new Integer(2), r.getCount());
+ Assert.assertEquals(new Integer(2), r.getRemainingCount());
+ Assert.assertEquals(new Integer(1000), r.getExpiration());
+ Assert.assertNotNull(r.getTimestamp());
+ Assert.assertNull(r.getToken());
+ } else if(r.getId().equals(access2.getId())) {
+ Assert.assertEquals(new Integer(1), r.getCount());
+ Assert.assertEquals(new Integer(1), r.getRemainingCount());
+ Assert.assertEquals(new Integer(0), r.getExpiration());
+ Assert.assertNotNull(r.getTimestamp());
+ Assert.assertNull(r.getToken());
+ } else {
+ Assert.fail("Unexpected id");
+ }
+ }
+
+ resource.delete(access.getId());
+ resource.delete(access2.getId());
+
+ Assert.assertTrue(resource.list().isEmpty());
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
index bb33515..42870a6 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
@@ -180,7 +180,6 @@ public class RealmTest extends AbstractClientTest {
String description = IOUtils.toString(getClass().getResourceAsStream("/client-descriptions/client-oidc.json"));
ClientRepresentation converted = realm.convertClientDescription(description);
- assertEquals(36, converted.getClientId().length());
assertEquals(1, converted.getRedirectUris().size());
assertEquals("http://localhost", converted.getRedirectUris().get(0));
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index f262f34..0acd7a0 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -6,6 +6,8 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
@@ -15,9 +17,9 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.actions.RequiredActionEmailVerificationTest;
import org.keycloak.testsuite.forms.ResetPasswordTest;
-import org.keycloak.testsuite.pages.LoginPasswordResetPage;
-import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.pages.*;
import org.keycloak.testsuite.rule.GreenMailRule;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
@@ -58,6 +60,9 @@ public class UserTest extends AbstractClientTest {
@WebResource
protected WebDriver driver;
+ @WebResource
+ protected InfoPage infoPage;
+
@Before
public void before() {
super.before();
@@ -475,7 +480,7 @@ public class UserTest extends AbstractClientTest {
@Test
- public void sendVerifyEmail() {
+ public void sendVerifyEmail() throws IOException, MessagingException {
UserRepresentation userRep = new UserRepresentation();
userRep.setUsername("user1");
Response response = realm.users().create(userRep);
@@ -517,6 +522,15 @@ public class UserTest extends AbstractClientTest {
ErrorRepresentation error = e.getResponse().readEntity(ErrorRepresentation.class);
Assert.assertEquals("invalidClientId not enabled", error.getErrorMessage());
}
+
+ user.sendVerifyEmail();
+ assertEquals(1, greenMail.getReceivedMessages().length);
+
+ String link = RequiredActionEmailVerificationTest.getPasswordResetEmailLink(greenMail.getReceivedMessages()[0]);
+
+ driver.navigate().to(link);
+
+ Assert.assertEquals("Your account has been updated.", infoPage.getInfo());
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
index 74c70ea..84e724b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java
@@ -8,6 +8,7 @@ import org.junit.Assert;
import org.junit.rules.TestRule;
import org.junit.runners.model.Statement;
import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.events.admin.AdminEvent;
@@ -134,7 +135,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
return expect(EventType.CLIENT_LOGIN)
.detail(Details.CODE_ID, isCodeId())
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
- .detail(Details.RESPONSE_TYPE, ServiceAccountConstants.CLIENT_AUTH)
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)
.removeDetail(Details.CODE_ID)
.session(isUUID());
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java
index 7a378aa..2962f0b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java
@@ -1,5 +1,6 @@
package org.keycloak.testsuite.events;
+import org.apache.commons.lang3.StringUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -170,6 +171,19 @@ public class EventStoreProviderTest {
}
@Test
+ public void lengthExceedLimit(){
+ eventStore.onEvent(create(System.currentTimeMillis() - 30000, EventType.LOGIN, "realmId", StringUtils.repeat("clientId", 100), "userId", "127.0.0.1", "error"));
+ eventStore.onEvent(create(System.currentTimeMillis() - 30000, EventType.LOGIN, StringUtils.repeat("realmId", 100), "clientId", "userId", "127.0.0.1", "error"));
+ eventStore.onEvent(create(System.currentTimeMillis() - 30000, EventType.LOGIN, "realmId", "clientId", StringUtils.repeat("userId", 100), "127.0.0.1", "error"));
+
+ }
+
+ @Test
+ public void maxLengthWithNull(){
+ eventStore.onEvent(create(System.currentTimeMillis() - 30000, EventType.LOGIN, null, null, null, "127.0.0.1", "error"));
+ }
+
+ @Test
public void clearOld() {
eventStore.onEvent(create(System.currentTimeMillis() - 30000, EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
eventStore.onEvent(create(System.currentTimeMillis() - 20000, EventType.LOGIN, "realmId", "clientId", "userId", "127.0.0.1", "error"));
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
index 1fc4fc2..2f89194 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
@@ -163,8 +163,9 @@ public class ExportImportTest {
testRealmExportImport();
- // There should be 3 files in target directory (1 realm, 2 user, 1 version)
- Assert.assertEquals(4, new File(targetDirPath).listFiles().length);
+ // There should be 3 files in target directory (1 realm, 3 user, 1 version)
+ File[] files = new File(targetDirPath).listFiles();
+ Assert.assertEquals(5, files.length);
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java
index 8f0778c..4e654aa 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java
@@ -303,6 +303,15 @@ public class BruteForceTest {
}
@Test
+ public void testBrowserInvalidPasswordDifferentCase() throws Exception {
+ loginSuccess("test-user@localhost");
+ loginInvalidPassword("test-User@localhost");
+ loginInvalidPassword("Test-user@localhost");
+ expectTemporarilyDisabled();
+ clearAllUserFailures();
+ }
+
+ @Test
public void testBrowserMissingPassword() throws Exception {
loginSuccess();
loginMissingPassword();
@@ -333,8 +342,12 @@ public class BruteForceTest {
}
public void expectTemporarilyDisabled() throws Exception {
+ expectTemporarilyDisabled("test-user@localhost");
+ }
+
+ public void expectTemporarilyDisabled(String username) throws Exception {
loginPage.open();
- loginPage.login("test-user@localhost", "password");
+ loginPage.login(username, "password");
loginPage.assertCurrent();
String src = driver.getPageSource();
@@ -345,9 +358,11 @@ public class BruteForceTest {
.assertEvent();
}
-
-
public void loginSuccess() throws Exception {
+ loginSuccess("test-user@localhost");
+ }
+
+ public void loginSuccess(String username) throws Exception {
loginPage.open();
loginPage.login("test-user@localhost", "password");
@@ -391,10 +406,13 @@ public class BruteForceTest {
events.clear();
}
-
public void loginInvalidPassword() throws Exception {
+ loginInvalidPassword("test-user@localhost");
+ }
+
+ public void loginInvalidPassword(String username) throws Exception {
loginPage.open();
- loginPage.login("test-user@localhost", "invalid");
+ loginPage.login(username, "invalid");
loginPage.assertCurrent();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
index f704553..4690308 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
@@ -218,7 +218,7 @@ public class CustomFlowTest {
.client(clientId)
.user(userId)
.session(accessToken.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, login)
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index 1acd654..0c653e0 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -21,10 +21,7 @@
*/
package org.keycloak.testsuite.forms;
-import org.junit.Before;
-import org.junit.ClassRule;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.*;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.Event;
@@ -39,13 +36,8 @@ import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.Constants;
import org.keycloak.testsuite.MailUtil;
import org.keycloak.testsuite.OAuthClient;
-import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.*;
import org.keycloak.testsuite.pages.AppPage.RequestType;
-import org.keycloak.testsuite.pages.ErrorPage;
-import org.keycloak.testsuite.pages.InfoPage;
-import org.keycloak.testsuite.pages.LoginPage;
-import org.keycloak.testsuite.pages.LoginPasswordResetPage;
-import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.rule.GreenMailRule;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
@@ -59,6 +51,7 @@ import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.util.Collections;
+import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.*;
@@ -115,6 +108,9 @@ public class ResetPasswordTest {
protected InfoPage infoPage;
@WebResource
+ protected VerifyEmailPage verifyEmailPage;
+
+ @WebResource
protected LoginPasswordResetPage resetPasswordPage;
@WebResource
@@ -423,6 +419,59 @@ public class ResetPasswordTest {
}
@Test
+ public void resetPasswordExpiredCodeShort() throws IOException, MessagingException, InterruptedException {
+ final AtomicInteger originalValue = new AtomicInteger();
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ originalValue.set(appRealm.getAccessCodeLifespan());
+ appRealm.setAccessCodeLifespanUserAction(60);
+ }
+ });
+
+ 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());
+
+ 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);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String changePasswordUrl = getPasswordResetEmailLink(message);
+
+ Time.setOffset(70);
+
+ driver.navigate().to(changePasswordUrl.trim());
+
+ loginPage.assertCurrent();
+
+ 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();
+ } finally {
+ Time.setOffset(0);
+
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setAccessCodeLifespanUserAction(originalValue.get());
+ }
+ });
+ }
+ }
+
+ @Test
public void resetPasswordDisabledUser() throws IOException, MessagingException, InterruptedException {
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
@Override
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java
index f97a05e..9f23b70 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java
@@ -19,6 +19,7 @@ public class SamlAdapterTest {
ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader);
+ initializeSamlSecuredWar("/keycloak-saml/simple-post-passive", "/sales-post-passive", "post-passive.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader);
@@ -48,12 +49,7 @@ public class SamlAdapterTest {
@Test
public void testPostBadRealmSignature() {
- testStrategy.testPostBadRealmSignature( new SamlAdapterTestStrategy.CheckAuthError() {
- @Override
- public void check(WebDriver driver) {
- Assert.assertTrue(driver.getPageSource().contains("Forbidden"));
- }
- });
+ testStrategy.testPostBadRealmSignature();
}
@Test
@@ -61,12 +57,17 @@ public class SamlAdapterTest {
testStrategy.testPostSimpleUnauthorized( new SamlAdapterTestStrategy.CheckAuthError() {
@Override
public void check(WebDriver driver) {
- Assert.assertTrue(driver.getPageSource().contains("Forbidden"));
+ String pageSource = driver.getPageSource();
+ Assert.assertTrue(pageSource.contains("Error Page"));
}
});
}
@Test
+ public void testErrorHandling() throws Exception {
+ testStrategy.testErrorHandling();
+ }
+ @Test
public void testMetadataPostSignedLoginLogout() throws Exception {
testStrategy.testMetadataPostSignedLoginLogout();
}
@@ -97,6 +98,11 @@ public class SamlAdapterTest {
}
@Test
+ public void testPostPassiveLoginLogout() {
+ testStrategy.testPostPassiveLoginLogout(true);
+ }
+
+ @Test
public void testPostSignedLoginLogoutTransientNameID() {
testStrategy.testPostSignedLoginLogoutTransientNameID();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java
index 2c32d51..f2d8b68 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java
@@ -1,60 +1,50 @@
package org.keycloak.testsuite.keycloaksaml;
import org.apache.commons.io.IOUtils;
-import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
import org.junit.Assert;
-import org.junit.ClassRule;
-import org.junit.Rule;
-import org.junit.Test;
import org.junit.rules.ExternalResource;
-import org.keycloak.Config;
+import org.keycloak.adapters.saml.SamlAuthenticationError;
import org.keycloak.adapters.saml.SamlPrincipal;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
-import org.keycloak.protocol.oidc.OIDCLoginProtocol;
-import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
+import org.keycloak.protocol.saml.mappers.GroupMembershipMapper;
import org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper;
import org.keycloak.protocol.saml.mappers.HardcodedRole;
import org.keycloak.protocol.saml.mappers.RoleListMapper;
import org.keycloak.protocol.saml.mappers.RoleNameMapper;
-import org.keycloak.representations.AccessToken;
+import org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.saml.BaseSAML2BindingBuilder;
+import org.keycloak.saml.SAML2ErrorResponseBuilder;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants;
import org.keycloak.services.managers.RealmManager;
-import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
+import org.keycloak.testsuite.rule.ErrorServlet;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
-import org.keycloak.util.JsonSerialization;
import org.openqa.selenium.WebDriver;
+import org.w3c.dom.Document;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
-import javax.ws.rs.client.ClientRequestContext;
-import javax.ws.rs.client.ClientRequestFilter;
-import javax.ws.rs.client.Entity;
-import javax.ws.rs.client.WebTarget;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
-import java.io.InputStream;
+import java.net.URI;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Set;
import static org.junit.Assert.assertEquals;
@@ -111,6 +101,33 @@ public class SamlAdapterTestStrategy extends ExternalResource {
Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml"));
}
+ public void testErrorHandling() throws Exception {
+ ErrorServlet.authError = null;
+ Client client = ClientBuilder.newClient();
+ // make sure
+ Response response = client.target(APP_SERVER_BASE_URL + "/employee-sig/").request().get();
+ response.close();
+ SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder()
+ .destination(APP_SERVER_BASE_URL + "/employee-sig/")
+ .issuer(AUTH_SERVER_URL + "/realms/demo")
+ .status(JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
+ BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder()
+ .relayState(null);
+ Document document = builder.buildDocument();
+ URI uri = binding.redirectBinding(document).generateURI(APP_SERVER_BASE_URL + "/employee-sig/", false);
+ response = client.target(uri).request().get();
+ String errorPage = response.readEntity(String.class);
+ response.close();
+ Assert.assertTrue(errorPage.contains("Error Page"));
+ client.close();
+ Assert.assertNotNull(ErrorServlet.authError);
+ SamlAuthenticationError error = (SamlAuthenticationError)ErrorServlet.authError;
+ Assert.assertEquals(SamlAuthenticationError.Reason.ERROR_STATUS, error.getReason());
+ Assert.assertNotNull(error.getStatus());
+ ErrorServlet.authError = null;
+
+ }
+
public void testPostSimpleLoginLogout() {
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
@@ -122,6 +139,38 @@ public class SamlAdapterTestStrategy extends ExternalResource {
checkLoggedOut(APP_SERVER_BASE_URL + "/sales-post/");
}
+ public void testPostPassiveLoginLogout(boolean forbiddenIfNotauthenticated) {
+ // first request on passive app - no login page shown, user not logged in as we are in passive mode.
+ // Shown page depends on used authentication mechanism, some may return forbidden error, some return requested page with anonymous user (not logged in)
+ driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/");
+ assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl());
+ System.out.println(driver.getPageSource());
+ if (forbiddenIfNotauthenticated) {
+ Assert.assertTrue(driver.getPageSource().contains("HTTP status code: 403"));
+ } else {
+ Assert.assertTrue(driver.getPageSource().contains("principal=null"));
+ }
+
+ // login user by asking login from other app
+ driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/");
+ loginPage.login("bburke", "password");
+
+ // navigate to the passive app again, we have to be logged in now
+ driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/");
+ assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl());
+ System.out.println(driver.getPageSource());
+ Assert.assertTrue(driver.getPageSource().contains("bburke"));
+
+ // logout from both app
+ driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive?GLO=true");
+ driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post?GLO=true");
+
+ // refresh passive app page, not logged in again as we are in passive mode
+ driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-passive/");
+ assertEquals(APP_SERVER_BASE_URL + "/sales-post-passive/", driver.getCurrentUrl());
+ Assert.assertFalse(driver.getPageSource().contains("bburke"));
+ }
+
public void testPostSimpleUnauthorized(CheckAuthError error) {
driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
@@ -202,6 +251,40 @@ public class SamlAdapterTestStrategy extends ExternalResource {
}
public void testAttributes() throws Exception {
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ ClientModel app = appRealm.getClientByClientId(APP_SERVER_BASE_URL + "/employee2/");
+ app.addProtocolMapper(GroupMembershipMapper.create("groups", "group", null, null, true));
+ app.addProtocolMapper(UserAttributeStatementMapper.createAttributeMapper("topAttribute", "topAttribute", "topAttribute", "Basic", null, false, null));
+ app.addProtocolMapper(UserAttributeStatementMapper.createAttributeMapper("level2Attribute", "level2Attribute", "level2Attribute", "Basic", null, false, null));
+ }
+ }, "demo");
+ {
+ SendUsernameServlet.sentPrincipal = null;
+ SendUsernameServlet.checkRoles = null;
+ driver.navigate().to(APP_SERVER_BASE_URL + "/employee2/");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml"));
+ List<String> requiredRoles = new LinkedList<>();
+ requiredRoles.add("manager");
+ requiredRoles.add("user");
+ SendUsernameServlet.checkRoles = requiredRoles;
+ loginPage.login("level2GroupUser", "password");
+ assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee2/");
+ SendUsernameServlet.checkRoles = null;
+ SamlPrincipal principal = (SamlPrincipal) SendUsernameServlet.sentPrincipal;
+ Assert.assertNotNull(principal);
+ assertEquals("level2@redhat.com", principal.getAttribute(X500SAMLProfileConstants.EMAIL.get()));
+ assertEquals("true", principal.getAttribute("topAttribute"));
+ assertEquals("true", principal.getAttribute("level2Attribute"));
+ List<String> groups = principal.getAttributes("group");
+ Assert.assertNotNull(groups);
+ Set<String> groupSet = new HashSet<>();
+ assertEquals("level2@redhat.com", principal.getFriendlyAttribute("email"));
+ driver.navigate().to(APP_SERVER_BASE_URL + "/employee2/?GLO=true");
+ checkLoggedOut(APP_SERVER_BASE_URL + "/employee2/");
+
+ }
{
SendUsernameServlet.sentPrincipal = null;
SendUsernameServlet.checkRoles = null;
@@ -345,13 +428,17 @@ public class SamlAdapterTestStrategy extends ExternalResource {
void check(WebDriver driver);
}
- public void testPostBadRealmSignature(CheckAuthError error) {
+ public void testPostBadRealmSignature() {
+ ErrorServlet.authError = null;
driver.navigate().to(APP_SERVER_BASE_URL + "/bad-realm-sales-post-sig/");
assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml");
loginPage.login("bburke", "password");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/bad-realm-sales-post-sig/");
System.out.println(driver.getPageSource());
- error.check(driver);
+ Assert.assertNotNull(ErrorServlet.authError);
+ SamlAuthenticationError error = (SamlAuthenticationError)ErrorServlet.authError;
+ Assert.assertEquals(SamlAuthenticationError.Reason.INVALID_SIGNATURE, error.getReason());
+ ErrorServlet.authError = null;
}
public void testMetadataPostSignedLoginLogout() throws Exception {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlKeycloakRule.java
index 34d17f0..5a1d01f 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlKeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlKeycloakRule.java
@@ -115,6 +115,7 @@ public abstract class SamlKeycloakRule extends AbstractKeycloakRule {
.addServlets(regularServletInfo)
.addSecurityConstraint(constraint)
.addServletExtension(new SamlServletExtension());
+ addErrorPage("/error.html", deploymentInfo);
server.getServer().deploy(deploymentInfo);
}
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 126d0e5..28f0915 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java
@@ -170,6 +170,10 @@ public class KeycloakServer {
System.setProperty("keycloak.theme.cacheTemplates", "false");
}
+ if (!System.getProperties().containsKey("keycloak.theme.cacheThemes")) {
+ System.setProperty("keycloak.theme.cacheThemes", "false");
+ }
+
if (!System.getProperties().containsKey("keycloak.theme.staticMaxAge")) {
System.setProperty("keycloak.theme.staticMaxAge", "-1");
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java
new file mode 100755
index 0000000..a5f2388
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java
@@ -0,0 +1,272 @@
+package org.keycloak.testsuite.model;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.GroupResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
+import org.keycloak.common.util.Time;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.PasswordPolicy;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
+import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
+import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
+import org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper;
+import org.keycloak.protocol.saml.mappers.HardcodedRole;
+import org.keycloak.protocol.saml.mappers.RoleListMapper;
+import org.keycloak.protocol.saml.mappers.RoleNameMapper;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.services.managers.ClientManager;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class GroupTest {
+
+ @ClassRule
+ public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ ClientModel app = new ClientManager(manager).createClient(appRealm, "resource-owner");
+ app.setSecret("secret");
+
+ UserModel user = session.users().addUser(appRealm, "direct-login");
+ user.setEmail("direct-login@localhost");
+ user.setEnabled(true);
+
+
+ session.users().updateCredential(appRealm, user, UserCredentialModel.password("password"));
+ keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CONSOLE_CLIENT_ID);
+ }
+ });
+
+ protected static Keycloak keycloak;
+
+ @Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @WebResource
+ protected WebDriver driver;
+
+ @WebResource
+ protected OAuthClient oauth;
+
+ @Test
+ public void createAndTestGroups() throws Exception {
+ RealmResource realm = keycloak.realms().realm("test");
+ {
+ RoleRepresentation groupRole = new RoleRepresentation();
+ groupRole.setName("topRole");
+ realm.roles().create(groupRole);
+ }
+ RoleRepresentation topRole = realm.roles().get("topRole").toRepresentation();
+ {
+ RoleRepresentation groupRole = new RoleRepresentation();
+ groupRole.setName("level2Role");
+ realm.roles().create(groupRole);
+ }
+ RoleRepresentation level2Role = realm.roles().get("level2Role").toRepresentation();
+ {
+ RoleRepresentation groupRole = new RoleRepresentation();
+ groupRole.setName("level3Role");
+ realm.roles().create(groupRole);
+ }
+ RoleRepresentation level3Role = realm.roles().get("level3Role").toRepresentation();
+
+
+ GroupRepresentation topGroup = new GroupRepresentation();
+ topGroup.setName("top");
+ Response response = realm.groups().add(topGroup);
+ response.close();
+ topGroup = realm.getGroupByPath("/top");
+ Assert.assertNotNull(topGroup);
+ List<RoleRepresentation> roles = new LinkedList<>();
+ roles.add(topRole);
+ realm.groups().group(topGroup.getId()).roles().realmLevel().add(roles);
+
+ GroupRepresentation level2Group = new GroupRepresentation();
+ level2Group.setName("level2");
+ response = realm.groups().group(topGroup.getId()).subGroup(level2Group);
+ response.close();
+ level2Group = realm.getGroupByPath("/top/level2");
+ Assert.assertNotNull(level2Group);
+ roles.clear();
+ roles.add(level2Role);
+ realm.groups().group(level2Group.getId()).roles().realmLevel().add(roles);
+
+ GroupRepresentation level3Group = new GroupRepresentation();
+ level3Group.setName("level3");
+ response = realm.groups().group(level2Group.getId()).subGroup(level3Group);
+ response.close();
+ level3Group = realm.getGroupByPath("/top/level2/level3");
+ Assert.assertNotNull(level3Group);
+ roles.clear();
+ roles.add(level3Role);
+ realm.groups().group(level3Group.getId()).roles().realmLevel().add(roles);
+
+ topGroup = realm.getGroupByPath("/top");
+ Assert.assertEquals(1, topGroup.getRealmRoles().size());
+ Assert.assertTrue(topGroup.getRealmRoles().contains("topRole"));
+ Assert.assertEquals(1, topGroup.getSubGroups().size());
+
+ level2Group = topGroup.getSubGroups().get(0);
+ Assert.assertEquals("level2", level2Group.getName());
+ Assert.assertEquals(1, level2Group.getRealmRoles().size());
+ Assert.assertTrue(level2Group.getRealmRoles().contains("level2Role"));
+ Assert.assertEquals(1, level2Group.getSubGroups().size());
+
+ level3Group = level2Group.getSubGroups().get(0);
+ Assert.assertEquals("level3", level3Group.getName());
+ Assert.assertEquals(1, level3Group.getRealmRoles().size());
+ Assert.assertTrue(level3Group.getRealmRoles().contains("level3Role"));
+
+ try {
+ GroupRepresentation notFound = realm.getGroupByPath("/notFound");
+ Assert.fail();
+ } catch (NotFoundException e) {
+
+ }
+ try {
+ GroupRepresentation notFound = realm.getGroupByPath("/top/notFound");
+ Assert.fail();
+ } catch (NotFoundException e) {
+
+ }
+
+ UserRepresentation user = realm.users().search("direct-login", -1, -1).get(0);
+ realm.users().get(user.getId()).joinGroup(level3Group.getId());
+ List<GroupRepresentation> membership = realm.users().get(user.getId()).groups();
+ Assert.assertEquals(1, membership.size());
+ Assert.assertEquals("level3", membership.get(0).getName());
+
+ AccessToken token = login("direct-login", "resource-owner", "secret", user.getId());
+ Assert.assertTrue(token.getRealmAccess().getRoles().contains("topRole"));
+ Assert.assertTrue(token.getRealmAccess().getRoles().contains("level2Role"));
+ Assert.assertTrue(token.getRealmAccess().getRoles().contains("level3Role"));
+
+ realm.addDefaultGroup(level3Group.getId());
+
+ List<GroupRepresentation> defaultGroups = realm.getDefaultGroups();
+ Assert.assertEquals(1, defaultGroups.size());
+ Assert.assertEquals(defaultGroups.get(0).getId(), level3Group.getId());
+
+ UserRepresentation newUser = new UserRepresentation();
+ newUser.setUsername("groupUser");
+ newUser.setEmail("group@group.com");
+ response = realm.users().create(newUser);
+ response.close();
+ newUser = realm.users().search("groupUser", -1, -1).get(0);
+ membership = realm.users().get(newUser.getId()).groups();
+ Assert.assertEquals(1, membership.size());
+ Assert.assertEquals("level3", membership.get(0).getName());
+
+ realm.removeDefaultGroup(level3Group.getId());
+ defaultGroups = realm.getDefaultGroups();
+ Assert.assertEquals(0, defaultGroups.size());
+
+ }
+
+ @Test
+ public void testGroupMappers() throws Exception {
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ ClientModel app = appRealm.getClientByClientId("test-app");
+ app.addProtocolMapper(GroupMembershipMapper.create("groups", "groups", false, null, true, true));
+ app.addProtocolMapper(UserAttributeMapper.createClaimMapper("topAttribute", "topAttribute", "topAttribute", ProviderConfigProperty.STRING_TYPE, false, null, true, true, false));
+ app.addProtocolMapper(UserAttributeMapper.createClaimMapper("level2Attribute", "level2Attribute", "level2Attribute", ProviderConfigProperty.STRING_TYPE, false, null, true, true, false));
+ }
+ }, "test");
+ RealmResource realm = keycloak.realms().realm("test");
+ {
+ UserRepresentation user = realm.users().search("topGroupUser", -1, -1).get(0);
+
+ AccessToken token = login(user.getUsername(), "test-app", "password", user.getId());
+ Assert.assertTrue(token.getRealmAccess().getRoles().contains("user"));
+ List<String> groups = (List<String>) token.getOtherClaims().get("groups");
+ Assert.assertNotNull(groups);
+ Assert.assertTrue(groups.size() == 1);
+ Assert.assertEquals("topGroup", groups.get(0));
+ Assert.assertEquals("true", token.getOtherClaims().get("topAttribute"));
+ }
+ {
+ UserRepresentation user = realm.users().search("level2GroupUser", -1, -1).get(0);
+
+ AccessToken token = login(user.getUsername(), "test-app", "password", user.getId());
+ Assert.assertTrue(token.getRealmAccess().getRoles().contains("user"));
+ Assert.assertTrue(token.getRealmAccess().getRoles().contains("admin"));
+ Assert.assertTrue(token.getResourceAccess("test-app").getRoles().contains("customer-user"));
+ List<String> groups = (List<String>) token.getOtherClaims().get("groups");
+ Assert.assertNotNull(groups);
+ Assert.assertTrue(groups.size() == 1);
+ Assert.assertEquals("level2group", groups.get(0));
+ Assert.assertEquals("true", token.getOtherClaims().get("topAttribute"));
+ Assert.assertEquals("true", token.getOtherClaims().get("level2Attribute"));
+ }
+
+ }
+
+ protected AccessToken login(String login, String clientId, String clientSecret, String userId) throws Exception {
+ oauth.clientId(clientId);
+
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(clientSecret, login, "password");
+
+ assertEquals(200, response.getStatusCode());
+
+ AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+ RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+ events.expectLogin()
+ .client(clientId)
+ .user(userId)
+ .session(accessToken.getSessionState())
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
+ .detail(Details.TOKEN_ID, accessToken.getId())
+ .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+ .detail(Details.USERNAME, login)
+ .removeDetail(Details.CODE_ID)
+ .removeDetail(Details.REDIRECT_URI)
+ .removeDetail(Details.CONSENT)
+ .assertEvent();
+ return accessToken;
+ }
+
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
index b2d14b9..99e5b95 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
@@ -74,6 +74,7 @@ public class ImportTest extends AbstractModelTest {
public static void assertDataImportedInRealm(KeycloakSession session, RealmModel realm) {
Assert.assertTrue(realm.isVerifyEmail());
Assert.assertEquals(3600000, realm.getOfflineSessionIdleTimeout());
+ Assert.assertEquals(1500, realm.getAccessTokenLifespanForImplicitFlow());
List<RequiredCredentialModel> creds = realm.getRequiredCredentials();
Assert.assertEquals(1, creds.size());
@@ -327,6 +328,13 @@ public class ImportTest extends AbstractModelTest {
Assert.assertFalse(otherAppAdminConsent.isRoleGranted(application.getRole("app-admin")));
Assert.assertTrue(otherAppAdminConsent.isProtocolMapperGranted(gssCredentialMapper));
+ Assert.assertTrue(application.isStandardFlowEnabled());
+ Assert.assertTrue(application.isImplicitFlowEnabled());
+ Assert.assertTrue(application.isDirectAccessGrantsEnabled());
+ Assert.assertFalse(otherApp.isStandardFlowEnabled());
+ Assert.assertFalse(otherApp.isImplicitFlowEnabled());
+ Assert.assertFalse(otherApp.isDirectAccessGrantsEnabled());
+
// Test service accounts
Assert.assertFalse(application.isServiceAccountsEnabled());
Assert.assertTrue(otherApp.isServiceAccountsEnabled());
@@ -344,6 +352,7 @@ public class ImportTest extends AbstractModelTest {
RealmModel realm =manager.importRealm(rep);
Assert.assertEquals(600, realm.getAccessCodeLifespanUserAction());
+ Assert.assertEquals(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT, realm.getAccessTokenLifespanForImplicitFlow());
Assert.assertEquals(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT, realm.getOfflineSessionIdleTimeout());
verifyRequiredCredentials(realm.getRequiredCredentials(), "password");
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index e191871..1b6cbd6 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -38,6 +38,7 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.Event;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
@@ -806,26 +807,15 @@ public class AccessTokenTest {
}
}
- private IDToken getIdToken(org.keycloak.representations.AccessTokenResponse tokenResponse) throws VerificationException {
+ private IDToken getIdToken(org.keycloak.representations.AccessTokenResponse tokenResponse) throws JWSInputException {
JWSInput input = new JWSInput(tokenResponse.getIdToken());
- IDToken idToken = null;
- try {
- idToken = input.readJsonContent(IDToken.class);
- } catch (IOException e) {
- throw new VerificationException();
- }
- return idToken;
+ return input.readJsonContent(IDToken.class);
}
- private AccessToken getAccessToken(org.keycloak.representations.AccessTokenResponse tokenResponse) throws VerificationException {
+ private AccessToken getAccessToken(org.keycloak.representations.AccessTokenResponse tokenResponse) throws JWSInputException {
JWSInput input = new JWSInput(tokenResponse.getToken());
- AccessToken idToken = null;
- try {
- idToken = input.readJsonContent(AccessToken.class);
- } catch (IOException e) {
- throw new VerificationException();
- }
- return idToken;
+ return input.readJsonContent(AccessToken.class);
+
}
protected Response executeGrantAccessTokenRequest(WebTarget grantTarget) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
index d3a47e0..5d5e0ed 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java
@@ -30,6 +30,7 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
@@ -160,12 +161,21 @@ public class AuthorizationCodeTest {
}
@Test
+ public void authorizationRequestImplicitFlowDisabled() throws IOException {
+ UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
+ b.replaceQueryParam(OAuth2Constants.RESPONSE_TYPE, "token id_token");
+ driver.navigate().to(b.build().toURL());
+ assertEquals("Client is not allowed to initiate browser login with given response_type. Implicit flow is disabled for the client.", errorPage.getError());
+ events.expectLogin().error(Errors.NOT_ALLOWED).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "token id_token").assertEvent();
+ }
+
+ @Test
public void authorizationRequestInvalidResponseType() throws IOException {
UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
b.replaceQueryParam(OAuth2Constants.RESPONSE_TYPE, "token");
driver.navigate().to(b.build().toURL());
assertEquals("Invalid parameter: response_type", errorPage.getError());
- events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "token").assertEvent();
+ events.expectLogin().error(Errors.INVALID_REQUEST).client((String) null).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "token").assertEvent();
}
private void assertCode(String expectedCodeId, String actualCode) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
index 537a297..898066a 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -189,7 +189,7 @@ public class ClientAuthSignedJWTTest {
events.expectLogin()
.client("client2")
.session(accessToken.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, "test-user@localhost")
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
index 6c205ad..d333f86 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
@@ -40,6 +40,7 @@ import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import java.io.IOException;
+import java.net.URL;
/**
* @author <a href="mailto:vrockai@redhat.com">Viliam Rockai</a>
@@ -174,7 +175,10 @@ public class OAuthRedirectUriTest {
OAuthClient.AuthorizationCodeResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode());
- Assert.assertTrue(driver.getCurrentUrl().startsWith("http://localhost:8081/app?code="));
+ URL url = new URL(driver.getCurrentUrl());
+ Assert.assertTrue(url.toString().startsWith("http://localhost:8081/app"));
+ Assert.assertTrue(url.getQuery().contains("code="));
+ Assert.assertTrue(url.getQuery().contains("state="));
}
@Test
@@ -192,7 +196,11 @@ public class OAuthRedirectUriTest {
OAuthClient.AuthorizationCodeResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode());
- Assert.assertTrue(driver.getCurrentUrl().startsWith("http://localhost:8081/app?key=value&code="));
+ URL url = new URL(driver.getCurrentUrl());
+ Assert.assertTrue(url.toString().startsWith("http://localhost:8081/app"));
+ Assert.assertTrue(url.getQuery().contains("key=value"));
+ Assert.assertTrue(url.getQuery().contains("state="));
+ Assert.assertTrue(url.getQuery().contains("code="));
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
index 219fa4f..a4b2855 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
@@ -25,6 +25,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.Event;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
@@ -318,7 +319,7 @@ public class OfflineTokenTest {
.client("offline-client")
.user(userId)
.session(token.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
@@ -360,7 +361,7 @@ public class OfflineTokenTest {
.client("offline-client")
.user(userId)
.session(token.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
@@ -645,7 +646,12 @@ public class OfflineTokenTest {
StringBuilder response = new StringBuilder("<html><head><title>Offline token servlet</title></head><body><pre>");
RefreshableKeycloakSecurityContext ctx = (RefreshableKeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
String accessTokenPretty = JsonSerialization.writeValueAsPrettyString(ctx.getToken());
- RefreshToken refreshToken = new JWSInput(ctx.getRefreshToken()).readJsonContent(RefreshToken.class);
+ RefreshToken refreshToken = null;
+ try {
+ refreshToken = new JWSInput(ctx.getRefreshToken()).readJsonContent(RefreshToken.class);
+ } catch (JWSInputException e) {
+ throw new IOException(e);
+ }
String refreshTokenPretty = JsonSerialization.writeValueAsPrettyString(refreshToken);
response = response.append(accessTokenPretty)
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
index 15f7f93..2373489 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
@@ -116,6 +116,14 @@ public class RefreshTokenTest {
}
@Test
+ public void invalidRefreshToken() throws Exception {
+ AccessTokenResponse response = oauth.doRefreshTokenRequest("invalid", "password");
+ Assert.assertEquals(400, response.getStatusCode());
+ Assert.assertEquals("invalid_grant", response.getError());
+ events.clear();
+ }
+
+ @Test
public void refreshTokenRequest() throws Exception {
oauth.doLogin("test-user@localhost", "password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
index 74e3d38..b64a683 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
@@ -6,6 +6,7 @@ import org.apache.http.impl.client.DefaultHttpClient;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@@ -93,7 +94,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
.client(clientId)
.user(userId)
.session(accessToken.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, login)
@@ -129,7 +130,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
events.expectLogin()
.client("resource-owner")
.session(accessToken.getSessionState())
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.removeDetail(Details.CODE_ID)
@@ -285,7 +286,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
events.expectLogin()
.client("resource-owner")
.session((String) null)
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI)
.removeDetail(Details.CONSENT)
@@ -307,7 +308,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
.client("resource-owner")
.user((String) null)
.session((String) null)
- .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.RESPONSE_TYPE, OAuth2Constants.PASSWORD)
.detail(Details.USERNAME, "invalid")
.removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI)
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java
index c013345..1fe6203 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java
@@ -87,6 +87,18 @@ public class AccountUpdateProfilePage extends AbstractAccountPage {
submitButton.click();
}
+ public void updateUsername(String username) {
+ usernameInput.clear();
+ usernameInput.sendKeys(username);
+ submitButton.click();
+ }
+
+ public void updateEmail(String email) {
+ emailInput.clear();
+ emailInput.sendKeys(email);
+ submitButton.click();
+ }
+
public void clickCancel() {
cancelButton.click();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
index fc2dc41..155bdf1 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
@@ -1,6 +1,7 @@
package org.keycloak.testsuite.rule;
import io.undertow.servlet.api.DeploymentInfo;
+import io.undertow.servlet.api.ErrorPage;
import io.undertow.servlet.api.FilterInfo;
import io.undertow.servlet.api.LoginConfig;
import io.undertow.servlet.api.SecurityConstraint;
@@ -156,7 +157,7 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
return new DeploymentBuilder();
}
- public void addErrorPage(DeploymentInfo di) {
+ public void addErrorPage(String errorPage, DeploymentInfo di) {
ServletInfo servlet = new ServletInfo("Error Page", ErrorServlet.class);
servlet.addMapping("/error.html");
SecurityConstraint constraint = new SecurityConstraint();
@@ -166,6 +167,11 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
constraint.setEmptyRoleSemantic(SecurityInfo.EmptyRoleSemantic.PERMIT);
di.addSecurityConstraint(constraint);
di.addServlet(servlet);
+ di
+ .addErrorPage(new ErrorPage(errorPage, 400))
+ .addErrorPage(new ErrorPage(errorPage, 401))
+ .addErrorPage(new ErrorPage(errorPage, 403))
+ .addErrorPage(new ErrorPage(errorPage, 500));
}
public void deployJaxrsApplication(String name, String contextPath, Class<? extends Application> applicationClass, Map<String,String> initParams) {
@@ -346,9 +352,9 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
constraint.addRoleAllowed(role);
di.addSecurityConstraint(constraint);
}
- LoginConfig loginConfig = new LoginConfig("KEYCLOAK", "demo", null, errorPage);
+ LoginConfig loginConfig = new LoginConfig("KEYCLOAK", "demo", null, null);
di.setLoginConfig(loginConfig);
- addErrorPage(di);
+ addErrorPage(errorPage, di);
server.getServer().deploy(di);
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java
index 47f7135..0cffa3c 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java
@@ -1,10 +1,11 @@
package org.keycloak.testsuite.rule;
+import org.keycloak.adapters.spi.AuthenticationError;
+
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
@@ -12,17 +13,30 @@ import java.io.PrintWriter;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ErrorServlet extends HttpServlet {
+ public static AuthenticationError authError;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ authError = (AuthenticationError)req.getAttribute(AuthenticationError.class.getName());
+ Integer statusCode = (Integer) req.getAttribute("javax.servlet.error.status_code");
resp.setContentType("text/html");
PrintWriter pw = resp.getWriter();
pw.printf("<html><head><title>%s</title></head><body>", "Error Page");
- pw.print("<h1>There was an error</h1></body></html>");
+ pw.print("<h1>There was an error</h1>");
+ if (statusCode != null)
+ pw.print("<br/>HTTP status code: " + statusCode);
+ if (authError != null)
+ pw.print("<br/>Error info: " + authError.toString());
+ pw.print("</body></html>");
pw.flush();
}
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ doGet(req, resp);
+ }
}
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 74df6c8..8fc4cd6 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
@@ -22,6 +22,8 @@
package org.keycloak.testsuite.rule;
import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
index 019bfea..7878843 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
@@ -1,35 +1,24 @@
package org.keycloak.testsuite.saml;
import org.apache.commons.io.IOUtils;
-import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
-import org.keycloak.Config;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
-import org.keycloak.protocol.oidc.OIDCLoginProtocol;
-import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper;
import org.keycloak.protocol.saml.mappers.HardcodedRole;
import org.keycloak.protocol.saml.mappers.RoleListMapper;
import org.keycloak.protocol.saml.mappers.RoleNameMapper;
-import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.managers.RealmManager;
-import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.testsuite.pages.LoginPage;
-import org.keycloak.testsuite.rule.AbstractKeycloakRule;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
@@ -47,19 +36,10 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import javax.ws.rs.client.Client;
-import javax.ws.rs.client.ClientBuilder;
-import javax.ws.rs.client.ClientRequestContext;
-import javax.ws.rs.client.ClientRequestFilter;
-import javax.ws.rs.client.Entity;
-import javax.ws.rs.client.WebTarget;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.io.InputStream;
import static org.junit.Assert.assertEquals;
@@ -166,6 +146,7 @@ public class SamlBindingTest {
driver.navigate().to("http://localhost:8081/sales-post?GLO=true");
checkLoggedOut("http://localhost:8081/sales-post/");
}
+
@Test
public void testPostSimpleLoginLogoutIdpInitiated() {
driver.navigate().to("http://localhost:8081/auth/realms/demo/protocol/saml/clients/sales-post");
@@ -188,6 +169,7 @@ public class SamlBindingTest {
checkLoggedOut("http://localhost:8081/sales-post-sig/");
}
+
@Test
public void testPostSignedLoginLogoutTransientNameID() {
driver.navigate().to("http://localhost:8081/sales-post-sig-transient/");
@@ -452,23 +434,10 @@ public class SamlBindingTest {
Assert.assertTrue(driver.getPageSource().contains("null"));
}
- private static String createToken() {
- KeycloakSession session = keycloakRule.startSession();
- try {
- RealmManager manager = new RealmManager(session);
-
- RealmModel adminRealm = manager.getRealm(Config.getAdminRealm());
- ClientModel adminConsole = adminRealm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
- TokenManager tm = new TokenManager();
- UserModel admin = session.users().getUserByUsername("admin", adminRealm);
- ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole);
- clientSession.setNote(OIDCLoginProtocol.ISSUER, "http://localhost:8081/auth/realms/master");
- UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null);
- AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
- return tm.encodeToken(adminRealm, token);
- } finally {
- keycloakRule.stopSession(session, true);
- }
+ @Test
+ public void testPassiveMode() {
+ // KEYCLOAK-2075 test SAML IsPassive handling - PicketLink SP client library doesn't support this option unfortunately.
+ // But the test of server side is included in test of SAML Keycloak adapter
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java
index 88f97a6..d0c5d21 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlAdapterTest.java
@@ -5,7 +5,6 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.testsuite.keycloaksaml.SamlAdapterTestStrategy;
-import org.keycloak.testsuite.keycloaksaml.SamlSPFacade;
import org.keycloak.testsuite.keycloaksaml.SendUsernameServlet;
import org.openqa.selenium.WebDriver;
@@ -25,6 +24,7 @@ public class SamlAdapterTest {
ClassLoader classLoader = SamlAdapterTest.class.getClassLoader();
initializeSamlSecuredWar("/keycloak-saml/simple-post", "/sales-post", "post.war", classLoader);
+ initializeSamlSecuredWar("/keycloak-saml/simple-post-passive", "/sales-post-passive", "post-passive.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post", "/sales-post-sig", "post-sig.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post-email", "/sales-post-sig-email", "post-sig-email.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/signed-post-transient", "/sales-post-sig-transient", "post-sig-transient.war", classLoader);
@@ -37,9 +37,6 @@ public class SamlAdapterTest {
initializeSamlSecuredWar("/keycloak-saml/bad-realm-signed-post", "/bad-realm-sales-post-sig", "bad-realm-post-sig.war", classLoader);
initializeSamlSecuredWar("/keycloak-saml/encrypted-post", "/sales-post-enc", "post-enc.war", classLoader);
SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
-
-
-
}
@Override
@@ -53,12 +50,7 @@ public class SamlAdapterTest {
@Test
public void testPostBadRealmSignature() {
- testStrategy.testPostBadRealmSignature( new SamlAdapterTestStrategy.CheckAuthError() {
- @Override
- public void check(WebDriver driver) {
- Assert.assertTrue(driver.getPageSource().contains("Forbidden"));
- }
- });
+ testStrategy.testPostBadRealmSignature();
}
@Test
@@ -72,7 +64,7 @@ public class SamlAdapterTest {
testStrategy.testPostSimpleUnauthorized(new SamlAdapterTestStrategy.CheckAuthError() {
@Override
public void check(WebDriver driver) {
- Assert.assertTrue(driver.getPageSource().contains("Forbidden"));
+ Assert.assertTrue(driver.getPageSource().contains("Error Page"));
}
});
} finally {
@@ -111,6 +103,11 @@ public class SamlAdapterTest {
}
@Test
+ public void testPostPassiveLoginLogout() {
+ testStrategy.testPostPassiveLoginLogout(false);
+ }
+
+ @Test
public void testPostSignedLoginLogoutTransientNameID() {
testStrategy.testPostSignedLoginLogoutTransientNameID();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlKeycloakRule.java
index e1fd3c8..a348408 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlKeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/samlfilter/SamlKeycloakRule.java
@@ -114,6 +114,7 @@ public abstract class SamlKeycloakRule extends AbstractKeycloakRule {
.addFilter(samlFilter)
.addFilterUrlMapping("saml-filter", "/*", DispatcherType.REQUEST)
.addServletExtension(new SamlServletExtension());
+ addErrorPage("/error.html", deploymentInfo);
server.getServer().deploy(deploymentInfo);
}
diff --git a/testsuite/integration/src/test/resources/adapter-test/demorealm.json b/testsuite/integration/src/test/resources/adapter-test/demorealm.json
index 5bf2bdd..b5cd399 100755
--- a/testsuite/integration/src/test/resources/adapter-test/demorealm.json
+++ b/testsuite/integration/src/test/resources/adapter-test/demorealm.json
@@ -34,12 +34,36 @@
"lastName": "Posolda",
"credentials" : [
{ "type" : "password",
- "value" : "password" }
+ "value" : "password" }
],
"realmRoles": [ "user" ],
"applicationRoles": {
"account": [ "manage-account" ]
}
+ },
+ {
+ "username" : "topGroupUser",
+ "enabled": true,
+ "email" : "top@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top"
+ ]
+ },
+ {
+ "username" : "level2GroupUser",
+ "enabled": true,
+ "email" : "level2@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top/level2"
+ ]
}
],
"roles" : {
@@ -54,6 +78,29 @@
}
]
},
+ "groups" : [
+ {
+ "name": "top",
+ "attributes": {
+ "topAttribute": ["true"]
+
+ },
+ "realmRoles": ["user"],
+ "clientRoles": {
+ "account": ["manage-account"]
+ },
+ "subGroups": [
+ {
+ "name": "level2",
+ "realmRoles": ["admin"],
+ "attributes": {
+ "level2Attribute": ["true"]
+
+ }
+ }
+ ]
+ }
+ ],
"scopeMappings": [
{
"client": "third-party",
diff --git a/testsuite/integration/src/test/resources/keycloak-saml/simple-post-passive/WEB-INF/keycloak-saml.xml b/testsuite/integration/src/test/resources/keycloak-saml/simple-post-passive/WEB-INF/keycloak-saml.xml
new file mode 100755
index 0000000..3154627
--- /dev/null
+++ b/testsuite/integration/src/test/resources/keycloak-saml/simple-post-passive/WEB-INF/keycloak-saml.xml
@@ -0,0 +1,25 @@
+<keycloak-saml-adapter>
+ <SP entityID="http://localhost:8081/sales-post-passive/"
+ sslPolicy="EXTERNAL"
+ nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ logoutPage="/logout.jsp"
+ forceAuthentication="false"
+ isPassive="true">
+ <PrincipalNameMapping policy="FROM_NAME_ID"/>
+ <RoleIdentifiers>
+ <Attribute name="Role"/>
+ </RoleIdentifiers>
+ <IDP entityID="idp">
+ <SingleSignOnService requestBinding="POST"
+ bindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
+ />
+
+ <SingleLogoutService
+ requestBinding="POST"
+ responseBinding="POST"
+ postBindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
+ redirectBindingUrl="http://localhost:8081/auth/realms/demo/protocol/saml"
+ />
+ </IDP>
+ </SP>
+</keycloak-saml-adapter>
\ No newline at end of file
diff --git a/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json b/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json
index 8d5576e..7a50a91 100755
--- a/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json
+++ b/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json
@@ -40,6 +40,30 @@
{ "type" : "password",
"value" : "password" }
]
+ },
+ {
+ "username" : "topGroupUser",
+ "enabled": true,
+ "email" : "top@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top"
+ ]
+ },
+ {
+ "username" : "level2GroupUser",
+ "enabled": true,
+ "email" : "level2@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top/level2"
+ ]
}
],
"applications": [
@@ -62,6 +86,24 @@
}
},
{
+ "name": "http://localhost:8081/sales-post-passive/",
+ "enabled": true,
+ "fullScopeAllowed": true,
+ "protocol": "saml",
+ "baseUrl": "http://localhost:8081/sales-post-passive",
+ "redirectUris": [
+ "http://localhost:8081/sales-post-passive/*"
+ ],
+ "attributes": {
+ "saml.authnstatement": "true",
+ "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-passive/",
+ "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-passive/",
+ "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-passive/",
+ "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-passive/",
+ "saml_idp_initiated_sso_url_name": "sales-post-passive"
+ }
+ },
+ {
"name": "http://localhost:8081/sales-post-sig/",
"enabled": true,
"protocol": "saml",
@@ -347,6 +389,27 @@
}
}
],
+ "groups" : [
+ {
+ "name": "top",
+ "attributes": {
+ "topAttribute": ["true"]
+
+ },
+ "realmRoles": ["manager"],
+ "subGroups": [
+ {
+ "name": "level2",
+ "realmRoles": ["user"],
+ "attributes": {
+ "level2Attribute": ["true"]
+
+ }
+ }
+ ]
+ }
+ ],
+
"roles" : {
"realm" : [
{
diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json
index 68ec4d3..af86592 100755
--- a/testsuite/integration/src/test/resources/model/testrealm.json
+++ b/testsuite/integration/src/test/resources/model/testrealm.json
@@ -2,6 +2,7 @@
"realm": "test-realm",
"enabled": true,
"accessTokenLifespan": 6000,
+ "accessTokenLifespanForImplicitFlow": 1500,
"accessCodeLifespan": 30,
"accessCodeLifespanUserAction": 600,
"offlineSessionIdleTimeout": 3600000,
@@ -154,6 +155,7 @@
"clientId": "Application",
"name": "Applicationn",
"enabled": true,
+ "implicitFlowEnabled": true,
"nodeReRegistrationTimeout": 50,
"registeredNodes": {
"node1": 10,
@@ -164,6 +166,8 @@
"clientId": "OtherApp",
"name": "Other Application",
"enabled": true,
+ "standardFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
"serviceAccountsEnabled": true,
"clientAuthenticatorType": "client-jwt",
"protocolMappers" : [
diff --git a/testsuite/integration/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json
index e16e3e2..b4718dd 100755
--- a/testsuite/integration/src/test/resources/testrealm.json
+++ b/testsuite/integration/src/test/resources/testrealm.json
@@ -61,6 +61,30 @@
"test-app": [ "customer-user" ],
"account": [ "view-profile", "manage-account" ]
}
+ },
+ {
+ "username" : "topGroupUser",
+ "enabled": true,
+ "email" : "top@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/topGroup"
+ ]
+ },
+ {
+ "username" : "level2GroupUser",
+ "enabled": true,
+ "email" : "level2@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/topGroup/level2group"
+ ]
}
],
"scopeMappings": [
@@ -120,6 +144,31 @@
}
},
+ "groups" : [
+ {
+ "name": "topGroup",
+ "attributes": {
+ "topAttribute": ["true"]
+
+ },
+ "realmRoles": ["user"],
+
+ "subGroups": [
+ {
+ "name": "level2group",
+ "realmRoles": ["admin"],
+ "clientRoles": {
+ "test-app": ["customer-user"]
+ },
+ "attributes": {
+ "level2Attribute": ["true"]
+
+ }
+ }
+ ]
+ }
+ ],
+
"clientScopeMappings": {
"test-app": [
diff --git a/testsuite/integration-arquillian/servers/migration/wildfly_kc15/assembly.xml b/testsuite/integration-arquillian/servers/migration/wildfly_kc15/assembly.xml
index 0eb108c..fb9b4d5 100644
--- a/testsuite/integration-arquillian/servers/migration/wildfly_kc15/assembly.xml
+++ b/testsuite/integration-arquillian/servers/migration/wildfly_kc15/assembly.xml
@@ -1,6 +1,6 @@
<assembly>
- <id>auth-server-wildfly-kc14</id>
+ <id>auth-server-wildfly-kc15</id>
<formats>
<format>zip</format>
diff --git a/testsuite/integration-arquillian/servers/wildfly/pom.xml b/testsuite/integration-arquillian/servers/wildfly/pom.xml
index 9e0ecdb..d7c6a8c 100644
--- a/testsuite/integration-arquillian/servers/wildfly/pom.xml
+++ b/testsuite/integration-arquillian/servers/wildfly/pom.xml
@@ -167,7 +167,7 @@
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<type>zip</type>
</dependency>
</dependencies>
@@ -187,7 +187,7 @@
<artifactItems>
<artifactItem>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
<outputDirectory>${keycloak.server.home}</outputDirectory>
diff --git a/testsuite/integration-arquillian/tests/adapters/as7/pom.xml b/testsuite/integration-arquillian/tests/adapters/as7/pom.xml
index 313c154..15b575c 100644
--- a/testsuite/integration-arquillian/tests/adapters/as7/pom.xml
+++ b/testsuite/integration-arquillian/tests/adapters/as7/pom.xml
@@ -25,14 +25,13 @@
<type>zip</type>
</dependency>
<dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-as7-adapter-dist</artifactId>
- <type>zip</type>
+ <groupId>org.wildfly</groupId>
+ <artifactId>wildfly-arquillian-container-managed</artifactId>
</dependency>
<dependency>
- <groupId>org.jboss.as</groupId>
- <artifactId>jboss-as-arquillian-container-managed</artifactId>
- <version>7.2.0.Final</version>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-eap6-adapter-dist</artifactId>
+ <type>zip</type>
</dependency>
</dependencies>
@@ -67,18 +66,20 @@
<artifactItem>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-as7-adapter-dist</artifactId>
+ <version>${project.version}</version>
<type>zip</type>
<outputDirectory>${adapter.libs.as7}</outputDirectory>
</artifactItem>
</artifactItems>
- <overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
+
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
+ <version>2.18.1</version>
<configuration>
<systemPropertyVariables>
<app.server.as7>true</app.server.as7>
@@ -90,7 +91,6 @@
</plugins>
</build>
-
<profiles>
<profile>
<id>adapter-libs-provided</id>
@@ -133,5 +133,4 @@
</build>
</profile>
</profiles>
-
</project>
diff --git a/testsuite/integration-arquillian/tests/adapters/as7/src/main/xslt/arquillian.xsl b/testsuite/integration-arquillian/tests/adapters/as7/src/main/xslt/arquillian.xsl
index 8970850..9ba1e94 100644
--- a/testsuite/integration-arquillian/tests/adapters/as7/src/main/xslt/arquillian.xsl
+++ b/testsuite/integration-arquillian/tests/adapters/as7/src/main/xslt/arquillian.xsl
@@ -18,6 +18,7 @@
<property name="jbossHome">${app.server.as7.home}</property>
<property name="javaVmArguments">-Djboss.socket.binding.port-offset=${app.server.port.offset} -Xms64m -Xmx512m -XX:MaxPermSize=256m ${adapter.test.props}</property>
<property name="managementAddress">localhost</property>
+ <property name="managementProtocol">remote</property>
<property name="managementPort">${app.server.management.port.jmx}</property>
</configuration>
</container>
diff --git a/testsuite/integration-arquillian/tests/adapters/as7/src/test/java/org/keycloak/testsuite/adapter/servlet/AS7DemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/adapters/as7/src/test/java/org/keycloak/testsuite/adapter/servlet/AS7DemoServletsAdapterTest.java
index f0258b2..1a4a68c 100644
--- a/testsuite/integration-arquillian/tests/adapters/as7/src/test/java/org/keycloak/testsuite/adapter/servlet/AS7DemoServletsAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/adapters/as7/src/test/java/org/keycloak/testsuite/adapter/servlet/AS7DemoServletsAdapterTest.java
@@ -1,5 +1,6 @@
package org.keycloak.testsuite.adapter.servlet;
+import org.keycloak.testsuite.arquillian.annotation.AdapterLibsLocationProperty;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
/**
@@ -7,6 +8,7 @@ import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
* @author tkyjovsk
*/
@AppServerContainer("app-server-as7")
+@AdapterLibsLocationProperty("adapter.libs.as7")
public class AS7DemoServletsAdapterTest extends AbstractDemoServletsAdapterTest {
}
diff --git a/testsuite/integration-arquillian/tests/adapters/as7/src/test/java/org/keycloak/testsuite/adapter/servlet/AS7SessionServletAdapterTest.java b/testsuite/integration-arquillian/tests/adapters/as7/src/test/java/org/keycloak/testsuite/adapter/servlet/AS7SessionServletAdapterTest.java
index 9362ecd..4b88033 100644
--- a/testsuite/integration-arquillian/tests/adapters/as7/src/test/java/org/keycloak/testsuite/adapter/servlet/AS7SessionServletAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/adapters/as7/src/test/java/org/keycloak/testsuite/adapter/servlet/AS7SessionServletAdapterTest.java
@@ -1,5 +1,6 @@
package org.keycloak.testsuite.adapter.servlet;
+import org.keycloak.testsuite.arquillian.annotation.AdapterLibsLocationProperty;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
/**
@@ -7,6 +8,7 @@ import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
* @author tkyjovsk
*/
@AppServerContainer("app-server-as7")
+@AdapterLibsLocationProperty("adapter.libs.as7")
public class AS7SessionServletAdapterTest extends AbstractSessionServletAdapterTest {
}
diff --git a/testsuite/integration-arquillian/tests/adapters/eap6/pom.xml b/testsuite/integration-arquillian/tests/adapters/eap6/pom.xml
new file mode 100644
index 0000000..433ca63
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/adapters/eap6/pom.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <parent>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>integration-arquillian-tests-adapters</artifactId>
+ <version>1.7.0.Final-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>integration-arquillian-adapters-eap6</artifactId>
+ <name>Adapter Tests on EAP 6</name>
+
+ <properties>
+ <app.server.eap6.home>${containers.home}/jboss-eap-6.4</app.server.eap6.home>
+ <adapter.libs.eap6>${containers.home}/keycloak-eap6-adapter-dist</adapter.libs.eap6>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.as</groupId>
+ <artifactId>jboss-as-dist</artifactId>
+ <version>${jboss.version}</version>
+ <type>zip</type>
+ </dependency>
+ <dependency>
+ <groupId>org.wildfly</groupId>
+ <artifactId>wildfly-arquillian-container-managed</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-eap6-adapter-dist</artifactId>
+ <type>zip</type>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>unpack-as7-and-adapter</id>
+ <phase>generate-resources</phase>
+ <goals>
+ <goal>unpack</goal>
+ </goals>
+ <configuration>
+ <artifactItems>
+ <artifactItem>
+ <groupId>org.jboss.as</groupId>
+ <artifactId>jboss-as-dist</artifactId>
+ <version>${jboss.version}</version>
+ <type>zip</type>
+ <outputDirectory>${containers.home}</outputDirectory>
+ </artifactItem>
+ <artifactItem>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-eap6-adapter-dist</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ <outputDirectory>${adapter.libs.eap6}</outputDirectory>
+ </artifactItem>
+ </artifactItems>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.18.1</version>
+ <configuration>
+ <systemPropertyVariables>
+ <app.server.eap6>true</app.server.eap6>
+ <app.server.eap6.home>${app.server.eap6.home}</app.server.eap6.home>
+ <adapter.libs.eap6>${adapter.libs.eap6}</adapter.libs.eap6>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>adapter-libs-provided</id>
+ <activation>
+ <property>
+ <name>!adapter.libs.bundled</name>
+ </property>
+ </activation>
+ <properties>
+ <adapter.libs.eap6>${app.server.eap6.home}</adapter.libs.eap6>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>xml-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>configure-adapter-subsystem</id>
+ <phase>process-resources</phase>
+ <goals>
+ <goal>transform</goal>
+ </goals>
+ <configuration>
+ <transformationSets>
+ <transformationSet>
+ <dir>${app.server.eap6.home}/standalone/configuration</dir>
+ <includes>
+ <include>standalone.xml</include>
+ </includes>
+ <stylesheet>src/main/xslt/standalone.xsl</stylesheet>
+ <outputDir>${app.server.eap6.home}/standalone/configuration</outputDir>
+ </transformationSet>
+ </transformationSets>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
diff --git a/testsuite/integration-arquillian/tests/adapters/eap6/src/main/xslt/arquillian.xsl b/testsuite/integration-arquillian/tests/adapters/eap6/src/main/xslt/arquillian.xsl
new file mode 100644
index 0000000..fbfd50d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/adapters/eap6/src/main/xslt/arquillian.xsl
@@ -0,0 +1,37 @@
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xalan="http://xml.apache.org/xalan"
+ xmlns:a="http://jboss.org/schema/arquillian"
+ version="2.0"
+ exclude-result-prefixes="xalan a">
+
+ <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" xalan:indent-amount="4" standalone="no"/>
+ <xsl:strip-space elements="*"/>
+
+ <xsl:template match="/a:arquillian">
+ <xsl:copy>
+ <xsl:apply-templates select="node()|@*"/>
+
+ <container qualifier="app-server-eap6" mode="manual" >
+ <configuration>
+ <property name="enabled">${app.server.eap6}</property>
+ <property name="adapterImplClass">org.jboss.as.arquillian.container.managed.ManagedDeployableContainer</property>
+ <property name="jbossHome">${app.server.eap6.home}</property>
+ <property name="javaVmArguments">-Djboss.socket.binding.port-offset=${app.server.port.offset} -Xms64m -Xmx512m -XX:MaxPermSize=256m ${adapter.test.props}</property>
+ <property name="managementAddress">localhost</property>
+ <property name="managementProtocol">remote</property>
+ <property name="managementPort">${app.server.management.port.jmx}</property>
+ </configuration>
+ </container>
+
+ </xsl:copy>
+ </xsl:template>
+
+
+ <xsl:template match="@*|node()">
+ <xsl:copy>
+ <xsl:apply-templates select="@*|node()" />
+ </xsl:copy>
+ </xsl:template>
+
+
+</xsl:stylesheet>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/adapters/eap6/src/main/xslt/standalone.xsl b/testsuite/integration-arquillian/tests/adapters/eap6/src/main/xslt/standalone.xsl
new file mode 100644
index 0000000..fb3612b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/adapters/eap6/src/main/xslt/standalone.xsl
@@ -0,0 +1,51 @@
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xalan="http://xml.apache.org/xalan"
+ xmlns:j="urn:jboss:domain:1.7"
+ xmlns:ds="urn:jboss:domain:datasources:1.2"
+ xmlns:k="urn:jboss:domain:keycloak:1.1"
+ xmlns:sec="urn:jboss:domain:security:1.2"
+ version="2.0"
+ exclude-result-prefixes="xalan j ds k sec">
+
+ <xsl:param name="config"/>
+
+ <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" xalan:indent-amount="4" standalone="no"/>
+ <xsl:strip-space elements="*"/>
+
+ <xsl:template match="//j:extensions">
+ <xsl:copy>
+ <xsl:apply-templates select="node()|@*"/>
+ <extension module="org.keycloak.keycloak-adapter-subsystem"/>
+ </xsl:copy>
+ </xsl:template>
+
+ <xsl:template match="//j:profile">
+ <xsl:copy>
+ <xsl:apply-templates select="node()|@*"/>
+ <subsystem xmlns="urn:jboss:domain:keycloak:1.1"/>
+ </xsl:copy>
+ </xsl:template>
+
+ <xsl:template match="//sec:security-domains">
+ <xsl:copy>
+ <xsl:apply-templates select="node()[name(.)='security-domain']"/>
+ <security-domain name="keycloak">
+ <authentication>
+ <login-module code="org.keycloak.adapters.jboss.KeycloakLoginModule" flag="required"/>
+ </authentication>
+ </security-domain>
+ <security-domain name="sp" cache-type="default">
+ <authentication>
+ <login-module code="org.picketlink.identity.federation.bindings.wildfly.SAML2LoginModule" flag="required"/>
+ </authentication>
+ </security-domain>
+ </xsl:copy>
+ </xsl:template>
+
+ <xsl:template match="@*|node()">
+ <xsl:copy>
+ <xsl:apply-templates select="@*|node()" />
+ </xsl:copy>
+ </xsl:template>
+
+</xsl:stylesheet>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/adapters/eap6/src/test/java/org/keycloak/testsuite/adapter/servlet/EAP6DemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/adapters/eap6/src/test/java/org/keycloak/testsuite/adapter/servlet/EAP6DemoServletsAdapterTest.java
new file mode 100644
index 0000000..5eb363e
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/adapters/eap6/src/test/java/org/keycloak/testsuite/adapter/servlet/EAP6DemoServletsAdapterTest.java
@@ -0,0 +1,14 @@
+package org.keycloak.testsuite.adapter.servlet;
+
+import org.keycloak.testsuite.arquillian.annotation.AdapterLibsLocationProperty;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author tkyjovsk
+ */
+@AppServerContainer("app-server-eap6")
+@AdapterLibsLocationProperty("adapter.libs.eap6")
+public class EAP6DemoServletsAdapterTest extends AbstractDemoServletsAdapterTest {
+
+}
diff --git a/testsuite/integration-arquillian/tests/adapters/eap6/src/test/java/org/keycloak/testsuite/adapter/servlet/EAP6SessionServletAdapterTest.java b/testsuite/integration-arquillian/tests/adapters/eap6/src/test/java/org/keycloak/testsuite/adapter/servlet/EAP6SessionServletAdapterTest.java
new file mode 100644
index 0000000..c187910
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/adapters/eap6/src/test/java/org/keycloak/testsuite/adapter/servlet/EAP6SessionServletAdapterTest.java
@@ -0,0 +1,14 @@
+package org.keycloak.testsuite.adapter.servlet;
+
+import org.keycloak.testsuite.arquillian.annotation.AdapterLibsLocationProperty;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author tkyjovsk
+ */
+@AppServerContainer("app-server-eap6")
+@AdapterLibsLocationProperty("adapter.libs.eap6")
+public class EAP6SessionServletAdapterTest extends AbstractSessionServletAdapterTest {
+
+}
diff --git a/testsuite/integration-arquillian/tests/adapters/wildfly/pom.xml b/testsuite/integration-arquillian/tests/adapters/wildfly/pom.xml
index 428bdf1..81d9804 100644
--- a/testsuite/integration-arquillian/tests/adapters/wildfly/pom.xml
+++ b/testsuite/integration-arquillian/tests/adapters/wildfly/pom.xml
@@ -13,7 +13,7 @@
<properties>
<app.server.wildfly.home>${containers.home}/wildfly-${wildfly.version}</app.server.wildfly.home>
- <adapter.libs.wildfly>${containers.home}/keycloak-wf9-adapter-dist</adapter.libs.wildfly>
+ <adapter.libs.wildfly>${containers.home}/keycloak-wildfly-adapter-dist</adapter.libs.wildfly>
</properties>
<dependencies>
@@ -28,7 +28,7 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<type>zip</type>
</dependency>
</dependencies>
@@ -63,7 +63,7 @@
</artifactItem>
<artifactItem>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
<outputDirectory>${adapter.libs.wildfly}</outputDirectory>
diff --git a/testsuite/integration-arquillian/tests/adapters/wildfly-relative/pom.xml b/testsuite/integration-arquillian/tests/adapters/wildfly-relative/pom.xml
index 92d757a..6c78ff2 100644
--- a/testsuite/integration-arquillian/tests/adapters/wildfly-relative/pom.xml
+++ b/testsuite/integration-arquillian/tests/adapters/wildfly-relative/pom.xml
@@ -13,7 +13,7 @@
<properties>
- <adapter.libs.wildfly>${containers.home}/keycloak-wf9-adapter-dist</adapter.libs.wildfly>
+ <adapter.libs.wildfly>${containers.home}/keycloak-wildfly-adapter-dist</adapter.libs.wildfly>
<!--this is needed for adapter tests that load system properties in adapter config-->
<app.server.http.port>${auth.server.http.port}</app.server.http.port>
@@ -27,7 +27,7 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<type>zip</type>
</dependency>
</dependencies>
@@ -56,7 +56,7 @@
<artifactItems>
<artifactItem>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-wf9-adapter-dist</artifactId>
+ <artifactId>keycloak-wildfly-adapter-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
<outputDirectory>${adapter.libs.wildfly}</outputDirectory>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Authentication.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Authentication.java
index 858de40..f15d37e 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Authentication.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Authentication.java
@@ -20,7 +20,7 @@ public class Authentication extends AdminConsoleRealm {
@FindBy(xpath = "//div[contains(@class, 'alert-success')]")
private WebElement success;
- @FindBy(xpath = "//button[@class='close']/span")
+ @FindBy(id = "notification-close")
private WebElement close;
public String getSuccessMessage() {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateExecution.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateExecution.java
new file mode 100644
index 0000000..f572cdf
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateExecution.java
@@ -0,0 +1,40 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.console.page.authentication.flows;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.testsuite.console.page.authentication.Authentication;
+
+/**
+ *
+ * @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
+ */
+public class CreateExecution extends Authentication {
+
+ @Page
+ private CreateExecutionForm form;
+
+ public CreateExecutionForm form() {
+ return form;
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateExecutionForm.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateExecutionForm.java
new file mode 100644
index 0000000..eef0389
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateExecutionForm.java
@@ -0,0 +1,63 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.console.page.authentication.flows;
+
+import org.keycloak.testsuite.page.Form;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.ui.Select;
+
+/**
+ *
+ * @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
+ */
+public class CreateExecutionForm extends Form {
+ public enum ProviderOption {
+ DIRECT_GRANT_VALIDATE_USERNAME("direct-grant-validate-username"),
+ RESET_OTP("reset-otp"),
+ AUTH_COOKIE("auth-cookie"),
+ RESET_CREDENTIALS_CHOOSE_USER("reset-credentials-choose-user"),
+ DIRECT_GRANT_VALIDATE_PASSWORD("direct-grant-validate-password"),
+ AUTH_USERNAME_PASSWORD_FORM("auth-username-password-form"),
+ AUTH_OTP_FORM("auth-otp-form"),
+ AUTH_SPNEGO("auth-spnego"),
+ DIRECT_GRANT_VALIDATE_OPT("direct-grant-validate-otp"),
+ RESET_CREDENTIALS_EMAIL("reset-credential-email"),
+ RESET_PASSWORD("reset-password");
+
+ private final String name;
+
+ private ProviderOption(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ @FindBy(id = "provider")
+ private Select providerSelect;
+
+ public void selectProviderOption(ProviderOption value) {
+ providerSelect.selectByVisibleText(value.getName());
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateFlow.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateFlow.java
new file mode 100644
index 0000000..18c29f2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateFlow.java
@@ -0,0 +1,44 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.console.page.authentication.flows;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.testsuite.console.page.authentication.Authentication;
+
+/**
+ *
+ * @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
+ */
+public class CreateFlow extends Authentication {
+
+ @Page
+ private CreateFlowForm form;
+
+ @Override
+ public String getUriFragment() {
+ return super.getUriFragment() + "/create/flow";
+ }
+
+ public CreateFlowForm form() {
+ return form;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateFlowForm.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateFlowForm.java
new file mode 100644
index 0000000..8eb1483
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/CreateFlowForm.java
@@ -0,0 +1,67 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.console.page.authentication.flows;
+
+import org.keycloak.testsuite.page.Form;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.ui.Select;
+
+/**
+ *
+ * @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
+ */
+public class CreateFlowForm extends Form {
+
+ @FindBy(id = "alias")
+ private WebElement aliasInput;
+
+ @FindBy(id = "description")
+ private WebElement descriptionTextarea;
+
+ @FindBy(id = "flowType")
+ private Select flowTypeSelect;
+
+ public enum FlowType {
+
+ GENERIC("generic"),
+ FORM("form"),
+ CLIENT("client");
+
+ private final String name;
+
+ private FlowType(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ public void setValues(String alias, String description, FlowType flowType) {
+ setInputValue(aliasInput, alias);
+ setInputValue(descriptionTextarea, description);
+ flowTypeSelect.selectByVisibleText(flowType.getName());
+ save();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/Flows.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/Flows.java
new file mode 100644
index 0000000..63694cc
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/Flows.java
@@ -0,0 +1,90 @@
+package org.keycloak.testsuite.console.page.authentication.flows;
+
+import org.keycloak.testsuite.console.page.authentication.Authentication;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.ui.Select;
+
+/**
+ * @author tkyjovsk
+ * @author mhajas
+ */
+public class Flows extends Authentication {
+
+ @Override
+ public String getUriFragment() {
+ return super.getUriFragment() + "/flows";
+ }
+
+ @FindBy(tagName = "select")
+ private Select flowSelect;
+
+ @FindBy(xpath = "//button[text() = 'New']")
+ private WebElement newButton;
+
+ @FindBy(xpath = "//button[text() = 'Copy']")
+ private WebElement copyButton;
+
+ @FindBy(xpath = "//button[text() = 'Delete']")
+ private WebElement deleteButton;
+
+ @FindBy(xpath = "//button[text() = 'Add Execution']")
+ private WebElement addExecutionButton;
+
+ @FindBy(xpath = "//button[text() = 'Add Flow']")
+ private WebElement addFlowButton;
+
+ @FindBy(tagName = "table")
+ private FlowsTable flowsTable;
+
+ public enum FlowOption {
+
+ DIRECT_GRANT("Direct grant"),
+ REGISTRATION("Registration"),
+ BROWSER("Browser"),
+ RESET_CREDENTIALS("Reset credentials"),
+ CLIENTS("Clients");
+
+ private final String name;
+
+ private FlowOption(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ public void selectFlowOption(FlowOption option) {
+ flowSelect.selectByVisibleText(option.getName());
+ }
+
+ public String getFlowSelectValue() {
+ return flowSelect.getFirstSelectedOption().getText();
+ }
+
+ public FlowsTable table() {
+ return flowsTable;
+ }
+
+ public void clickNew() {
+ newButton.click();
+ }
+
+ public void clickCopy() {
+ copyButton.click();
+ }
+
+ public void clickDelete() {
+ deleteButton.click();
+ }
+
+ public void clickAddExecution() {
+ addExecutionButton.click();
+ }
+
+ public void clickAddFlow() {
+ addFlowButton.click();
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/FlowsTable.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/FlowsTable.java
new file mode 100644
index 0000000..ad64d37
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/flows/FlowsTable.java
@@ -0,0 +1,93 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.console.page.authentication.flows;
+
+import static org.keycloak.testsuite.util.WaitUtils.waitAjaxForElement;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ *
+ * @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
+ */
+public class FlowsTable {
+ public enum RequirementOption {
+
+ ALTERNATIVE("ALTERNATIVE"),
+ DISABLED("DISABLED"),
+ OPTIONAL("OPTIONAL"),
+ REQUIRED("REQUIRED");
+
+ private final String name;
+
+ private RequirementOption(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ public enum Action {
+
+ DELETE("Delete"),
+ ADD_EXECUTION("Add Execution"),
+ ADD_FLOW("Add Flow");
+
+ private final String name;
+
+ private Action(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ @FindBy(tagName = "tbody")
+ private WebElement tbody;
+
+ private WebElement getRowByLabelText(String text) {
+ WebElement row = tbody.findElement(By.xpath("//span[text() = '" + text + "']/../.."));
+ waitAjaxForElement(row);
+ return row;
+ }
+
+ public void clickLevelUpButton(String rowLabel) {
+ getRowByLabelText(rowLabel).findElement(By.xpath("//i[contains(@class, 'up')]/..")).click();
+ }
+
+ public void clickLevelDownButton(String rowLabel) {
+ getRowByLabelText(rowLabel).findElement(By.xpath("//i[contains(@class, 'down')]/..")).click();
+ }
+
+ public void changeRequirement(String rowLabel, RequirementOption option) {
+ getRowByLabelText(rowLabel).findElement(By.xpath("//input[@value = '" + option + "']")).click();
+ }
+
+ public void performAction(String rowLabel, Action action) {
+
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/otppolicy/OTPPolicyForm.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/otppolicy/OTPPolicyForm.java
index 1e88cb3..8414de9 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/otppolicy/OTPPolicyForm.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/otppolicy/OTPPolicyForm.java
@@ -26,6 +26,8 @@ import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.ui.Select;
+import java.util.List;
+
/**
*
* @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
@@ -53,7 +55,8 @@ public class OTPPolicyForm extends Form {
public void setValues(OTPType otpType, OTPHashAlg otpHashAlg, Digits digits, String lookAhead, String periodOrCounter) {
this.otpType.selectByValue(otpType.getName());
this.otpHashAlg.selectByValue(otpHashAlg.getName());
- this.digits.selectByValue(digits.getName());
+ this.digits.selectByVisibleText("" + digits.getName());
+
setInputValue(this.lookAhead, lookAhead);
switch (otpType) {
@@ -102,17 +105,16 @@ public class OTPPolicyForm extends Form {
public enum Digits {
- EMPTY("? number:6 ?"),
- SIX("6"),
- EIGHT("8");
+ SIX(6),
+ EIGHT(8);
- private final String name;
+ private final int name;
- private Digits(String name) {
+ private Digits(int name) {
this.name = name;
}
- public String getName() {
+ public int getName() {
return name;
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/clients/CreateClientForm.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/clients/CreateClientForm.java
index 40772ae..c3f9428 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/clients/CreateClientForm.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/clients/CreateClientForm.java
@@ -33,8 +33,14 @@ public class CreateClientForm extends Form {
@FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='consentRequired']]")
private OnOffSwitch consentRequiredSwitch;
- @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='directGrantsOnly']]")
- private OnOffSwitch directGrantsOnlySwitch;
+ @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='standardFlowEnabled']]")
+ private OnOffSwitch standardFlowEnabledSwitch;
+
+ @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='implicitFlowEnabled']]")
+ private OnOffSwitch implicitFlowEnabledSwitch;
+
+ @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='directAccessGrantsEnabled']]")
+ private OnOffSwitch directAccessGrantsEnabledSwitch;
@FindBy(id = "protocol")
private Select protocolSelect;
@@ -67,7 +73,9 @@ public class CreateClientForm extends Form {
setName(client.getName());
setEnabled(client.isEnabled());
setConsentRequired(client.isConsentRequired());
- setDirectGrantsOnly(client.isDirectGrantsOnly());
+ setStandardFlowEnabled(client.isStandardFlowEnabled());
+ setImplicitFlowEnabled(client.isImplicitFlowEnabled());
+ setDirectAccessGrantsEnabled(client.isDirectAccessGrantsEnabled());
setProtocol(client.getProtocol());
if (OIDC.equals(client.getProtocol())) {
setAccessType(client);
@@ -86,7 +94,9 @@ public class CreateClientForm extends Form {
values.setName(getName());
values.setEnabled(isEnabled());
values.setConsentRequired(isConsentRequired());
- values.setDirectGrantsOnly(isDirectGrantsOnly());
+ values.setStandardFlowEnabled(isStandardFlowEnabled());
+ values.setImplicitFlowEnabled(isImplicitFlowEnabled());
+ values.setDirectAccessGrantsEnabled(isDirectAccessGrantsEnabled());
values.setProtocol(getProtocol());
if (OIDC.equals(values.getProtocol())) {
values.setBearerOnly(isBearerOnly());
@@ -186,12 +196,28 @@ public class CreateClientForm extends Form {
consentRequiredSwitch.setOn(consentRequired);
}
- public boolean isDirectGrantsOnly() {
- return directGrantsOnlySwitch.isOn();
+ public boolean isStandardFlowEnabled() {
+ return standardFlowEnabledSwitch.isOn();
+ }
+
+ public void setStandardFlowEnabled(boolean standardFlowEnabled) {
+ standardFlowEnabledSwitch.setOn(standardFlowEnabled);
+ }
+
+ public boolean isImplicitFlowEnabled() {
+ return implicitFlowEnabledSwitch.isOn();
+ }
+
+ public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
+ implicitFlowEnabledSwitch.setOn(implicitFlowEnabled);
+ }
+
+ public boolean isDirectAccessGrantsEnabled() {
+ return directAccessGrantsEnabledSwitch.isOn();
}
- public void setDirectGrantsOnly(boolean directGrantsOnly) {
- directGrantsOnlySwitch.setOn(directGrantsOnly);
+ public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
+ directAccessGrantsEnabledSwitch.setOn(directAccessGrantsEnabled);
}
public String getProtocol() {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserAttributesForm.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserAttributesForm.java
index 85b32e6..e508e71 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserAttributesForm.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/users/UserAttributesForm.java
@@ -130,8 +130,8 @@ public class UserAttributesForm extends Form {
setEmail(user.getEmail());
setFirstName(user.getFirstName());
setLastName(user.getLastName());
- setEnabled(user.isEnabled());
- setEmailVerified(user.isEmailVerified());
+ if (user.isEnabled() != null) setEnabled(user.isEnabled());
+ if (user.isEmailVerified() != null) setEmailVerified(user.isEmailVerified());
setRequiredActions(user.getRequiredActions());
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java
index 8b8dfad..6278653 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientRegistrationTest.java
@@ -2,6 +2,7 @@ package org.keycloak.testsuite.client;
import org.junit.After;
import org.junit.Before;
+import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistration;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.models.AdminRoles;
@@ -13,7 +14,6 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import javax.ws.rs.NotFoundException;
-import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -29,7 +29,7 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
@Before
public void before() throws Exception {
- reg = new ClientRegistration(testContext.getAuthServerContextRoot() + "/auth", "test");
+ reg = ClientRegistration.create().url(testContext.getAuthServerContextRoot() + "/auth", "test").build();
}
@After
@@ -76,13 +76,11 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
testRealms.add(rep);
}
- public ClientRepresentation createClient(ClientRepresentation client) {
- Response response = adminClient.realm(REALM_NAME).clients().create(client);
- String id = response.getLocation().toString();
- id = id.substring(id.lastIndexOf('/') + 1);
- client.setId(id);
- response.close();
- return client;
+ public ClientRepresentation createClient(ClientRepresentation client) throws ClientRegistrationException {
+ authManageClients();
+ ClientRepresentation response = reg.create(client);
+ reg.auth(null);
+ return response;
}
public ClientRepresentation getClient(String clientId) {
@@ -93,4 +91,20 @@ public abstract class AbstractClientRegistrationTest extends AbstractKeycloakTes
}
}
+ void authCreateClients() {
+ reg.auth(Auth.token(getToken("create-clients", "password")));
+ }
+
+ void authManageClients() {
+ reg.auth(Auth.token(getToken("manage-clients", "password")));
+ }
+
+ void authNoAccess() {
+ reg.auth(Auth.token(getToken("no-access", "password")));
+ }
+
+ private String getToken(String username, String password) {
+ return oauthClient.getToken(REALM_NAME, "security-admin-console", null, username, password).getToken();
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java
index bf98364..4e0712e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java
@@ -9,8 +9,6 @@ import org.keycloak.common.enums.SslRequired;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.idm.ClientRepresentation;
-import javax.ws.rs.core.Response;
-
import static org.junit.Assert.*;
/**
@@ -37,6 +35,7 @@ public class AdapterInstallationConfigTest extends AbstractClientRegistrationTes
client.setRegistrationAccessToken("RegistrationAccessTokenTestRegistrationAccessToken");
client.setRootUrl("http://root");
client = createClient(client);
+ client.setSecret("RegistrationAccessTokenTestClientSecret");
client2 = new ClientRepresentation();
client2.setEnabled(true);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
index 8b84b10..3390988 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTest.java
@@ -196,20 +196,4 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
}
}
- private void authCreateClients() {
- reg.auth(Auth.token(getToken("create-clients", "password")));
- }
-
- private void authManageClients() {
- reg.auth(Auth.token(getToken("manage-clients", "password")));
- }
-
- private void authNoAccess() {
- reg.auth(Auth.token(getToken("no-access", "password")));
- }
-
- private String getToken(String username, String password) {
- return oauthClient.getToken(REALM_NAME, "security-admin-console", null, username, password).getToken();
- }
-
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/InitialAccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/InitialAccessTokenTest.java
new file mode 100644
index 0000000..6698b5e
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/InitialAccessTokenTest.java
@@ -0,0 +1,103 @@
+package org.keycloak.testsuite.client;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.ClientInitialAccessResource;
+import org.keycloak.client.registration.Auth;
+import org.keycloak.client.registration.ClientRegistrationException;
+import org.keycloak.client.registration.HttpErrorException;
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class InitialAccessTokenTest extends AbstractClientRegistrationTest {
+
+ private ClientInitialAccessResource resource;
+
+ @Before
+ public void before() throws Exception {
+ super.before();
+
+ resource = adminClient.realm(REALM_NAME).clientInitialAccess();
+ }
+
+ @Test
+ public void create() throws ClientRegistrationException, InterruptedException {
+ ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation());
+
+ reg.auth(Auth.token(response));
+
+ ClientRepresentation rep = new ClientRepresentation();
+
+ Thread.sleep(2);
+
+ ClientRepresentation created = reg.create(rep);
+ Assert.assertNotNull(created);
+
+ try {
+ reg.create(rep);
+ } catch (ClientRegistrationException e) {
+ Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void createMultiple() throws ClientRegistrationException {
+ ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation(0, 2));
+
+ reg.auth(Auth.token(response));
+
+ ClientRepresentation rep = new ClientRepresentation();
+
+ ClientRepresentation created = reg.create(rep);
+ Assert.assertNotNull(created);
+
+ created = reg.create(rep);
+ Assert.assertNotNull(created);
+
+ try {
+ reg.create(rep);
+ } catch (ClientRegistrationException e) {
+ Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void createExpired() throws ClientRegistrationException, InterruptedException {
+ ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation(1, 1));
+
+ reg.auth(Auth.token(response));
+
+ ClientRepresentation rep = new ClientRepresentation();
+
+ Thread.sleep(2);
+
+ try {
+ reg.create(rep);
+ } catch (ClientRegistrationException e) {
+ Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void createDeleted() throws ClientRegistrationException, InterruptedException {
+ ClientInitialAccessPresentation response = resource.create(new ClientInitialAccessCreatePresentation());
+
+ reg.auth(Auth.token(response));
+
+ resource.delete(response.getId());
+
+ ClientRepresentation rep = new ClientRepresentation();
+
+ try {
+ reg.create(rep);
+ } catch (ClientRegistrationException e) {
+ Assert.assertEquals(403, ((HttpErrorException) e.getCause()).getStatusLine().getStatusCode());
+ }
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
new file mode 100644
index 0000000..9696274
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/OIDCClientRegistrationTest.java
@@ -0,0 +1,85 @@
+package org.keycloak.testsuite.client;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.client.registration.Auth;
+import org.keycloak.client.registration.ClientRegistrationException;
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+
+import java.util.Collections;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
+
+ @Before
+ public void before() throws Exception {
+ super.before();
+
+ ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
+ reg.auth(Auth.token(token));
+ }
+
+ public OIDCClientRepresentation create() throws ClientRegistrationException {
+ OIDCClientRepresentation client = new OIDCClientRepresentation();
+ client.setClientName("RegistrationAccessTokenTest");
+ client.setClientUri("http://root");
+ client.setRedirectUris(Collections.singletonList("http://redirect"));
+
+ OIDCClientRepresentation response = reg.oidc().create(client);
+
+ return response;
+ }
+
+ @Test
+ public void createClient() throws ClientRegistrationException {
+ OIDCClientRepresentation response = create();
+
+ assertNotNull(response.getRegistrationAccessToken());
+ assertNotNull(response.getClientIdIssuedAt());
+ assertNotNull(response.getClientId());
+ assertNull(response.getClientSecretExpiresAt());
+ assertNotNull(response.getRegistrationClientUri());
+ assertEquals("RegistrationAccessTokenTest", response.getClientName());
+ assertEquals("http://root", response.getClientUri());
+ assertEquals(1, response.getRedirectUris().size());
+ assertEquals("http://redirect", response.getRedirectUris().get(0));
+ }
+
+ @Test
+ public void getClient() throws ClientRegistrationException {
+ OIDCClientRepresentation response = create();
+ reg.auth(Auth.token(response));
+
+ OIDCClientRepresentation rep = reg.oidc().get(response.getClientId());
+ assertNotNull(rep);
+ assertNotEquals(response.getRegistrationAccessToken(), rep.getRegistrationAccessToken());
+ }
+
+ @Test
+ public void updateClient() throws ClientRegistrationException {
+ OIDCClientRepresentation response = create();
+ reg.auth(Auth.token(response));
+
+ response.setRedirectUris(Collections.singletonList("http://newredirect"));
+
+ OIDCClientRepresentation updated = reg.oidc().update(response);
+
+ assertEquals(1, updated.getRedirectUris().size());
+ assertEquals("http://newredirect", updated.getRedirectUris().get(0));
+ }
+
+ @Test
+ public void deleteClient() throws ClientRegistrationException {
+ OIDCClientRepresentation response = create();
+ reg.auth(Auth.token(response));
+
+ reg.oidc().delete(response);
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
index be880bf..cad28ab 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/RegistrationAccessTokenTest.java
@@ -7,8 +7,6 @@ import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.representations.idm.ClientRepresentation;
-import javax.ws.rs.core.Response;
-
import static org.junit.Assert.*;
/**
@@ -22,13 +20,13 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
public void before() throws Exception {
super.before();
- client = new ClientRepresentation();
- client.setEnabled(true);
- client.setClientId("RegistrationAccessTokenTest");
- client.setSecret("RegistrationAccessTokenTestClientSecret");
- client.setRegistrationAccessToken("RegistrationAccessTokenTestRegistrationAccessToken");
- client.setRootUrl("http://root");
- client = createClient(client);
+ ClientRepresentation c = new ClientRepresentation();
+ c.setEnabled(true);
+ c.setClientId("RegistrationAccessTokenTest");
+ c.setSecret("RegistrationAccessTokenTestClientSecret");
+ c.setRootUrl("http://root");
+
+ client = createClient(c);
reg.auth(Auth.token(client.getRegistrationAccessToken()));
}
@@ -36,7 +34,7 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
private ClientRepresentation assertRead(String id, String registrationAccess, boolean expectSuccess) throws ClientRegistrationException {
if (expectSuccess) {
reg.auth(Auth.token(registrationAccess));
- ClientRepresentation rep = reg.get(client.getClientId());
+ ClientRepresentation rep = reg.get(id);
assertNotNull(rep);
return rep;
} else {
@@ -76,6 +74,7 @@ public class RegistrationAccessTokenTest extends AbstractClientRegistrationTest
@Test
public void updateClientWithRegistrationToken() throws ClientRegistrationException {
client.setRootUrl("http://newroot");
+
ClientRepresentation rep = reg.update(client);
assertEquals("http://newroot", getClient(client.getId()).getRootUrl());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java
new file mode 100644
index 0000000..1f5eab8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/SAMLClientRegistrationTest.java
@@ -0,0 +1,42 @@
+package org.keycloak.testsuite.client;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.client.registration.Auth;
+import org.keycloak.client.registration.ClientRegistrationException;
+import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
+import org.keycloak.representations.idm.ClientInitialAccessPresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest {
+
+ @Before
+ public void before() throws Exception {
+ super.before();
+
+ ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
+ reg.auth(Auth.token(token));
+ }
+
+ @Test
+ public void createClient() throws ClientRegistrationException, IOException {
+ String entityDescriptor = IOUtils.toString(getClass().getResourceAsStream("/clientreg-test/saml-entity-descriptor.xml"));
+ ClientRepresentation response = reg.saml().create(entityDescriptor);
+
+ assertNotNull(response.getRegistrationAccessToken());
+ assertEquals("loadbalancer-9.siroe.com", response.getClientId());
+ assertEquals(1, response.getRedirectUris().size());
+ assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", response.getRedirectUris().get(0));
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/FlowsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/FlowsTest.java
new file mode 100644
index 0000000..7b2a874
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/FlowsTest.java
@@ -0,0 +1,196 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.console.authentication;
+
+import org.jboss.arquillian.graphene.page.Page;
+import static org.junit.Assert.*;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.keycloak.testsuite.console.AbstractConsoleTest;
+import org.keycloak.testsuite.console.page.authentication.flows.CreateExecution;
+import org.keycloak.testsuite.console.page.authentication.flows.CreateExecutionForm;
+import org.keycloak.testsuite.console.page.authentication.flows.CreateFlow;
+import org.keycloak.testsuite.console.page.authentication.flows.CreateFlowForm;
+import org.keycloak.testsuite.console.page.authentication.flows.Flows;
+import org.keycloak.testsuite.console.page.authentication.flows.FlowsTable;
+
+/**
+ *
+ * @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
+ */
+@Ignore //waiting for KEYCLOAK-1967(KEYCLOAK-1966)
+public class FlowsTest extends AbstractConsoleTest {
+
+ @Page
+ private Flows flowsPage;
+
+ @Page
+ private CreateFlow createFlowPage;
+
+ @Page
+ private CreateExecution createExecutionPage;
+
+ @Before
+ public void beforeFlowsTest() {
+ flowsPage.navigateTo();
+ }
+
+ @Test
+ public void createDeleteFlowTest() {
+ log.info("add new flow");
+ flowsPage.clickNew();
+ createFlowPage.form().setValues("testFlow", "testDesc", CreateFlowForm.FlowType.GENERIC);
+ assertEquals("Success! Flow Created.", createFlowPage.getSuccessMessage());
+ log.debug("new flow created via UI");
+
+ log.info("check if test flow is created via rest");
+ //rest: flow is present
+ log.debug("checked");
+
+ log.debug("check if testFlow is selected in UI");
+ assertEquals("TestFlow", flowsPage.getFlowSelectValue());
+
+ log.info("add new execution flow within testFlow");
+ flowsPage.clickAddFlow();
+ createFlowPage.form().setValues("testExecutionFlow", "executionDesc", CreateFlowForm.FlowType.GENERIC);
+ assertEquals("Success! Flow Created.", createFlowPage.getSuccessMessage());
+ log.debug("new execution flow created via UI");
+
+ log.info("check if execution flow is created via rest");
+ //rest: flow within nested flow is present
+ log.debug("checked");
+
+ log.debug("check if testFlow is selected in UI");
+ assertEquals("TestFlow", flowsPage.getFlowSelectValue());
+
+ log.info("delete test flow");
+ flowsPage.clickDelete();
+ assertEquals("Success! Flow removed", createFlowPage.getSuccessMessage());
+ log.debug("test flow removed via UI");
+
+ log.info("check if both test flow and execution flow is removed via rest");
+ //rest
+ log.debug("checked");
+ }
+
+ @Test
+ public void createFlowWithEmptyAliasTest() {
+ flowsPage.clickNew();
+ createFlowPage.form().setValues("", "testDesc", CreateFlowForm.FlowType.GENERIC);
+ assertEquals("Error! Missing or invalid field(s). Please verify the fields in red.", createFlowPage.getErrorMessage());
+
+ //rest:flow isn't present
+
+ //best-efford: check empty alias in nested flow
+ }
+
+ @Test
+ public void copyFlowTest() {
+ flowsPage.selectFlowOption(Flows.FlowOption.BROWSER);
+ flowsPage.clickCopy();
+
+ modalDialog.setName("test copy of browser");
+ modalDialog.ok();
+ assertEquals("Success! Flow copied.", createFlowPage.getSuccessMessage());
+
+ //rest: copied flow present
+ }
+
+ @Test
+ public void createDeleteExecutionTest() {
+ //rest: add new flow
+
+ log.info("add new execution within testFlow");
+ flowsPage.clickAddExecution();
+ createExecutionPage.form().selectProviderOption(CreateExecutionForm.ProviderOption.RESET_PASSWORD);
+ createExecutionPage.form().save();
+
+ assertEquals("Success! Execution Created.", createExecutionPage.getSuccessMessage());
+ log.debug("new execution flow created via UI");
+
+ //rest:check new execution
+
+ log.debug("check if testFlow is selected in UI");
+ assertEquals("TestFlow", flowsPage.getFlowSelectValue());
+
+ log.info("delete test flow");
+ flowsPage.clickDelete();
+ assertEquals("Success! Flow removed", createFlowPage.getSuccessMessage());
+ log.debug("test flow removed via UI");
+
+ log.info("check if both test flow and execution flow is removed via rest");
+ //rest
+ log.debug("checked");
+ }
+
+ @Test
+ public void navigationTest() {
+ //rest: add or copy flow to test navigation (browser)
+
+ //rest:
+ log.debug("check if there is expected structure of the flow");
+ //first should be Cookie
+ //second Kerberos
+ //third Test Copy Of Browser Forms
+ //a) Username Password Form
+ //b) OTP Form
+
+
+ flowsPage.table().clickLevelDownButton("Cookie");
+ assertEquals("Success! Priority lowered", flowsPage.getSuccessMessage());
+
+ flowsPage.table().clickLevelUpButton("Test Copy Of Browser Forms");
+ assertEquals("Success! Priority raised", flowsPage.getSuccessMessage());
+
+ flowsPage.table().clickLevelUpButton("OTP Forms");
+ assertEquals("Success! Priority raised", flowsPage.getSuccessMessage());
+
+ //rest:check if navigation was changed properly
+ }
+
+ @Test
+ public void requirementTest() {
+ //rest: add or copy flow to test navigation (browser), add reset, password
+
+ flowsPage.table().changeRequirement("Cookie", FlowsTable.RequirementOption.DISABLED);
+ flowsPage.table().changeRequirement("Kerberos", FlowsTable.RequirementOption.ALTERNATIVE);
+ flowsPage.table().changeRequirement("Copy Of Browser Forms", FlowsTable.RequirementOption.REQUIRED);
+ flowsPage.table().changeRequirement("Reset Password", FlowsTable.RequirementOption.REQUIRED);
+
+ //rest:check
+ }
+
+ @Test
+ public void actionsTest() {
+ //rest: add or copy flow to test navigation (browser)
+
+ flowsPage.table().performAction("Kerberos", FlowsTable.Action.DELETE);
+ flowsPage.table().performAction("Copy Of Browser Forms", FlowsTable.Action.ADD_FLOW);
+
+ createFlowPage.form().setValues("nestedFlow", "", CreateFlowForm.FlowType.CLIENT);
+
+ //todo: perform all remaining actions
+
+ //rest: check
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/OTPPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/OTPPolicyTest.java
index d9f8f89..2bd3a6b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/OTPPolicyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/OTPPolicyTest.java
@@ -67,19 +67,18 @@ public class OTPPolicyTest extends AbstractConsoleTest {
}
@Test
- @Ignore //KEYCLOAK-2051 when you close notification, it is not displayed again
public void invalidValuesTest() {
- otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.EMPTY, "", "30");
+ otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.SIX, "", "30");
assertEquals("Error! Missing or invalid field(s). Please verify the fields in red.", otpPolicyPage.getErrorMessage());
otpPolicyPage.closeNotification();
otpPolicyPage.navigateTo();// workaround: input.clear() doesn't work when <input type="number" ...
- otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.EMPTY, " ", "30");
+ otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.SIX, " ", "30");
assertEquals("Error! Missing or invalid field(s). Please verify the fields in red.", otpPolicyPage.getErrorMessage());
otpPolicyPage.closeNotification();
otpPolicyPage.navigateTo();
- otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.EMPTY, "no number", "30");
+ otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.SIX, "no number", "30");
assertEquals("Error! Missing or invalid field(s). Please verify the fields in red.", otpPolicyPage.getErrorMessage());
otpPolicyPage.closeNotification();
otpPolicyPage.navigateTo();
@@ -87,17 +86,17 @@ public class OTPPolicyTest extends AbstractConsoleTest {
RealmRepresentation realm = testRealmResource().toRepresentation();
assertEquals(Integer.valueOf(1), realm.getOtpPolicyLookAheadWindow());
- otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.EMPTY, "1", "");
+ otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.SIX, "1", "");
assertEquals("Error! Missing or invalid field(s). Please verify the fields in red.", otpPolicyPage.getErrorMessage());
otpPolicyPage.closeNotification();
otpPolicyPage.navigateTo();
- otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.EMPTY, "1", " ");
+ otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.SIX, "1", " ");
assertEquals("Error! Missing or invalid field(s). Please verify the fields in red.", otpPolicyPage.getErrorMessage());
otpPolicyPage.closeNotification();
otpPolicyPage.navigateTo();
- otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.EMPTY, "1", "no number");
+ otpPolicyPage.form().setValues(OTPType.TIME_BASED, OTPHashAlg.SHA1, Digits.SIX, "1", "no number");
assertEquals("Error! Missing or invalid field(s). Please verify the fields in red.", otpPolicyPage.getErrorMessage());
otpPolicyPage.closeNotification();
otpPolicyPage.navigateTo();
@@ -105,17 +104,17 @@ public class OTPPolicyTest extends AbstractConsoleTest {
realm = testRealmResource().toRepresentation();
assertEquals(Integer.valueOf(30), realm.getOtpPolicyPeriod());
- otpPolicyPage.form().setValues(OTPType.COUNTER_BASED, OTPHashAlg.SHA1, Digits.EMPTY, "1", "");
+ otpPolicyPage.form().setValues(OTPType.COUNTER_BASED, OTPHashAlg.SHA1, Digits.SIX, "1", "");
assertEquals("Error! Missing or invalid field(s). Please verify the fields in red.", otpPolicyPage.getErrorMessage());
otpPolicyPage.closeNotification();
otpPolicyPage.navigateTo();
- otpPolicyPage.form().setValues(OTPType.COUNTER_BASED, OTPHashAlg.SHA1, Digits.EMPTY, "1", " ");
+ otpPolicyPage.form().setValues(OTPType.COUNTER_BASED, OTPHashAlg.SHA1, Digits.SIX, "1", " ");
assertEquals("Error! Missing or invalid field(s). Please verify the fields in red.", otpPolicyPage.getErrorMessage());
otpPolicyPage.closeNotification();
otpPolicyPage.navigateTo();
- otpPolicyPage.form().setValues(OTPType.COUNTER_BASED, OTPHashAlg.SHA1, Digits.EMPTY, "1", "no number");
+ otpPolicyPage.form().setValues(OTPType.COUNTER_BASED, OTPHashAlg.SHA1, Digits.SIX, "1", "no number");
assertEquals("Error! Missing or invalid field(s). Please verify the fields in red.", otpPolicyPage.getErrorMessage());
otpPolicyPage.closeNotification();
otpPolicyPage.navigateTo();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java
index 7bb39c7..1b25373 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java
@@ -55,7 +55,9 @@ public abstract class AbstractClientTest extends AbstractConsoleTest {
client.setClientId(clientId);
client.setEnabled(true);
client.setConsentRequired(false);
- client.setDirectGrantsOnly(false);
+ client.setStandardFlowEnabled(true);
+ client.setImplicitFlowEnabled(false);
+ client.setDirectAccessGrantsEnabled(true);
client.setProtocol(OIDC);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/ConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/ConfigTest.java
new file mode 100644
index 0000000..eebb8a2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/ConfigTest.java
@@ -0,0 +1,53 @@
+package org.keycloak.testsuite.console.events;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.console.AbstractConsoleTest;
+import org.keycloak.testsuite.console.page.events.Config;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author mhajas
+ */
+public class ConfigTest extends AbstractConsoleTest {
+
+ @Page
+ private Config configPage;
+
+ @Before
+ public void beforeConfigTest() {
+ configPage.navigateTo();
+ }
+
+ @Test
+ public void configLoginEventsTest() {
+ configPage.form().setSaveEvents(true);
+ configPage.form().addSaveType("REGISTER_NODE");
+ //after removeSavedType method stay input focused -> in phantomjs drop menu doesn't appear after first click
+ configPage.form().removeSaveType("LOGIN");
+ configPage.form().setExpiration("50", "Days");
+ configPage.form().save();
+ assertFlashMessageSuccess();
+
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertTrue(realm.isEventsEnabled());
+ assertFalse(realm.getEnabledEventTypes().contains("LOGIN"));
+ assertTrue(realm.getEnabledEventTypes().contains("REGISTER_NODE"));
+ assertEquals(4320000L, realm.getEventsExpiration().longValue());
+ }
+
+ @Test
+ public void configAdminEventsTest() {
+ configPage.form().setSaveAdminEvents(true);
+ configPage.form().setIncludeRepresentation(true);
+ configPage.form().save();
+ assertFlashMessageSuccess();
+
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ assertTrue(realm.isAdminEventsEnabled());
+ assertTrue(realm.isAdminEventsDetailsEnabled());
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java
index 3b32274..5174acf 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/events/LoginEventsTest.java
@@ -6,6 +6,7 @@ import org.junit.Test;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.admin.Users;
import org.keycloak.testsuite.console.AbstractConsoleTest;
+import org.keycloak.testsuite.console.page.clients.Client;
import org.keycloak.testsuite.console.page.events.Config;
import org.keycloak.testsuite.console.page.events.LoginEvents;
import org.openqa.selenium.By;
@@ -54,7 +55,7 @@ public class LoginEventsTest extends AbstractConsoleTest {
List<WebElement> resultList = loginEventsPage.table().rows();
- assertEquals(7, resultList.size());
+ assertEquals(8, resultList.size());
resultList.get(0).findElement(By.xpath("//td[text()='LOGIN']"));
resultList.get(0).findElement(By.xpath("//td[text()='User']/../td[text()='" + testUser.getId() + "']"));
resultList.get(0).findElement(By.xpath("//td[text()='Client']/../td[text()='security-admin-console']"));
@@ -83,5 +84,7 @@ public class LoginEventsTest extends AbstractConsoleTest {
resultList.get(0).findElement(By.xpath("//td[text()='Client']/../td[text()='security-admin-console']"));
resultList.get(0).findElement(By.xpath("//td[text()='Error']/../td[text()='invalid_user_credentials']"));
resultList.get(0).findElement(By.xpath("//td[text()='IP Address']/../td[text()='127.0.0.1']"));
+
+
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
index e040362..8d40845 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
@@ -5,6 +5,7 @@ import static org.junit.Assert.assertEquals;
import java.util.Properties;
import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserFederationProviderRepresentation;
@@ -81,6 +82,7 @@ public class LdapUserFederationTest extends AbstractConsoleTest {
}
@Test
+ @Ignore
public void invalidSettingsTest() {
createLdapUserProvider.navigateTo();
createLdapUserProvider.form().selectVendor(ACTIVE_DIRECTORY);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/URLAssert.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/URLAssert.java
index 255e450..f4b043d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/URLAssert.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/URLAssert.java
@@ -47,7 +47,7 @@ public class URLAssert {
// }
// };
// wait.until(urlStartsWith);
- assertTrue(startsWithNormalized(driver.getCurrentUrl(), url));
+ assertTrue("'" + driver.getCurrentUrl() + " does not start with '" + url + "'", startsWithNormalized(driver.getCurrentUrl(), url));
}
public static void assertCurrentUrlDoesntStartWith(AbstractPage page) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml
new file mode 100644
index 0000000..b00ab25
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/clientreg-test/saml-entity-descriptor.xml
@@ -0,0 +1,82 @@
+<EntityDescriptor
+ xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+ entityID="loadbalancer-9.siroe.com">
+ <SPSSODescriptor
+ AuthnRequestsSigned="false"
+ WantAssertionsSigned="false"
+ protocolSupportEnumeration=
+ "urn:oasis:names:tc:SAML:2.0:protocol">
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIICYDCCAgqgAwIBAgICBoowDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz
+dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh
+dGUgTWFuYWdlcjAeFw0wNjExMDIxOTExMzRaFw0xMDA3MjkxOTExMzRaMDcxEjAQBgNVBAoTCXNp
+cm9lLmNvbTEhMB8GA1UEAxMYbG9hZGJhbGFuY2VyLTkuc2lyb2UuY29tMIGfMA0GCSqGSIb3DQEB
+AQUAA4GNADCBiQKBgQCjOwa5qoaUuVnknqf5pdgAJSEoWlvx/jnUYbkSDpXLzraEiy2UhvwpoBgB
+EeTSUaPPBvboCItchakPI6Z/aFdH3Wmjuij9XD8r1C+q//7sUO0IGn0ORycddHhoo0aSdnnxGf9V
+tREaqKm9dJ7Yn7kQHjo2eryMgYxtr/Z5Il5F+wIDAQABo2AwXjARBglghkgBhvhCAQEEBAMCBkAw
+DgYDVR0PAQH/BAQDAgTwMB8GA1UdIwQYMBaAFDugITflTCfsWyNLTXDl7cMDUKuuMBgGA1UdEQQR
+MA+BDW1hbGxhQHN1bi5jb20wDQYJKoZIhvcNAQEEBQADQQB/6DOB6sRqCZu2OenM9eQR0gube85e
+nTTxU4a7x1naFxzYXK1iQ1vMARKMjDb19QEJIEJKZlDK4uS7yMlf1nFS
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="encryption">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIICTDCCAfagAwIBAgICBo8wDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz
+dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh
+dGUgTWFuYWdlcjAeFw0wNjExMDcyMzU2MTdaFw0xMDA4MDMyMzU2MTdaMCMxITAfBgNVBAMTGGxv
+YWRiYWxhbmNlci05LnNpcm9lLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAw574iRU6
+HsSO4LXW/OGTXyfsbGv6XRVOoy3v+J1pZ51KKejcDjDJXNkKGn3/356AwIaqbcymWd59T0zSqYfR
+Hn+45uyjYxRBmVJseLpVnOXLub9jsjULfGx0yjH4w+KsZSZCXatoCHbj/RJtkzuZY6V9to/hkH3S
+InQB4a3UAgMCAwEAAaNgMF4wEQYJYIZIAYb4QgEBBAQDAgZAMA4GA1UdDwEB/wQEAwIE8DAfBgNV
+HSMEGDAWgBQ7oCE35Uwn7FsjS01w5e3DA1CrrjAYBgNVHREEETAPgQ1tYWxsYUBzdW4uY29tMA0G
+CSqGSIb3DQEBBAUAA0EAMlbfBg/ff0Xkv4DOR5LEqmfTZKqgdlD81cXynfzlF7XfnOqI6hPIA90I
+x5Ql0ejivIJAYcMGUyA+/YwJg2FGoA==
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ <EncryptionMethod Algorithm=
+ "https://www.w3.org/2001/04/xmlenc#aes128-cbc">
+ <KeySize xmlns="https://www.w3.org/2001/04/xmlenc#">128</KeySize>
+ </EncryptionMethod>
+ </KeyDescriptor>
+ <SingleLogoutService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"
+ ResponseLocation="https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"/>
+ <SingleLogoutService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/SPSloSoap/metaAlias/sp"/>
+ <ManageNameIDService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/SPMniRedirect/metaAlias/sp"
+ ResponseLocation="https://LoadBalancer-9.siroe.com:3443/federation/SPMniRedirect/metaAlias/sp"/>
+ <ManageNameIDService
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/SPMniSoap/metaAlias/sp"
+ ResponseLocation="https://LoadBalancer-9.siroe.com:3443/federation/SPMniSoap/metaAlias/sp"/>
+ <NameIDFormat>
+ urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
+ </NameIDFormat>
+ <NameIDFormat>
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+ </NameIDFormat>
+ <AssertionConsumerService
+ isDefault="true"
+ index="0"
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp"/>
+ <AssertionConsumerService
+ index="1"
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ Location="https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp"/>
+ </SPSSODescriptor>
+</EntityDescriptor>
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index 074a08a..13a2059 100644
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -230,7 +230,7 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-client-api</artifactId>
+ <artifactId>keycloak-client-registration-api</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
diff --git a/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java
index 1506b0c..6228a68 100755
--- a/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java
+++ b/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java
@@ -105,6 +105,11 @@ public class JettySamlTest {
}
@Test
+ public void testErrorHandling() throws Exception {
+ testStrategy.testErrorHandling();
+ }
+
+ @Test
public void testPostSimpleLoginLogout() {
testStrategy.testPostSimpleLoginLogout();
}
@@ -166,12 +171,7 @@ public class JettySamlTest {
@Test
public void testPostBadRealmSignature() {
- testStrategy.testPostBadRealmSignature( new SamlAdapterTestStrategy.CheckAuthError() {
- @Override
- public void check(WebDriver driver) {
- Assert.assertEquals(driver.getPageSource(), "");
- }
- });
+ testStrategy.testPostBadRealmSignature();
}
@Test
diff --git a/testsuite/jetty/jetty81/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml b/testsuite/jetty/jetty81/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
index f44a60b..e41448a 100755
--- a/testsuite/jetty/jetty81/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
+++ b/testsuite/jetty/jetty81/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
@@ -25,6 +25,26 @@
<url-pattern>/error.html</url-pattern>
</servlet-mapping>
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
+
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml b/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
index 86db4a4..71eff52 100755
--- a/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
+++ b/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
@@ -10,11 +10,40 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
+
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml b/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
index 86db4a4..ed4f018 100755
--- a/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
+++ b/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
@@ -10,11 +10,39 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/testsaml.json b/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/testsaml.json
index 0b13fb9..04c5dcd 100755
--- a/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/testsaml.json
+++ b/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/testsaml.json
@@ -40,6 +40,30 @@
{ "type" : "password",
"value" : "password" }
]
+ },
+ {
+ "username" : "topGroupUser",
+ "enabled": true,
+ "email" : "top@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top"
+ ]
+ },
+ {
+ "username" : "level2GroupUser",
+ "enabled": true,
+ "email" : "level2@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top/level2"
+ ]
}
],
"applications": [
@@ -347,6 +371,26 @@
}
}
],
+ "groups" : [
+ {
+ "name": "top",
+ "attributes": {
+ "topAttribute": ["true"]
+
+ },
+ "realmRoles": ["manager"],
+ "subGroups": [
+ {
+ "name": "level2",
+ "realmRoles": ["user"],
+ "attributes": {
+ "level2Attribute": ["true"]
+
+ }
+ }
+ ]
+ }
+ ],
"roles" : {
"realm" : [
{
diff --git a/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java
index e71887e..40edb45 100755
--- a/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java
+++ b/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java
@@ -104,6 +104,11 @@ public class JettySamlTest {
}
@Test
+ public void testErrorHandling() throws Exception {
+ testStrategy.testErrorHandling();
+ }
+
+ @Test
public void testPostSimpleLoginLogout() {
testStrategy.testPostSimpleLoginLogout();
}
@@ -165,12 +170,7 @@ public class JettySamlTest {
@Test
public void testPostBadRealmSignature() {
- testStrategy.testPostBadRealmSignature( new SamlAdapterTestStrategy.CheckAuthError() {
- @Override
- public void check(WebDriver driver) {
- Assert.assertEquals(driver.getPageSource(), "");
- }
- });
+ testStrategy.testPostBadRealmSignature();
}
@Test
diff --git a/testsuite/jetty/jetty91/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml b/testsuite/jetty/jetty91/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
index f44a60b..e41448a 100755
--- a/testsuite/jetty/jetty91/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
+++ b/testsuite/jetty/jetty91/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
@@ -25,6 +25,26 @@
<url-pattern>/error.html</url-pattern>
</servlet-mapping>
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
+
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml b/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
index 86db4a4..2f7ef22 100755
--- a/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
+++ b/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
@@ -10,12 +10,39 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
- <security-constraint>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page> <security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
<url-pattern>/*</url-pattern>
diff --git a/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml b/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
index 86db4a4..ed4f018 100755
--- a/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
+++ b/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
@@ -10,11 +10,39 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/testsaml.json b/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/testsaml.json
index 0b13fb9..04c5dcd 100755
--- a/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/testsaml.json
+++ b/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/testsaml.json
@@ -40,6 +40,30 @@
{ "type" : "password",
"value" : "password" }
]
+ },
+ {
+ "username" : "topGroupUser",
+ "enabled": true,
+ "email" : "top@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top"
+ ]
+ },
+ {
+ "username" : "level2GroupUser",
+ "enabled": true,
+ "email" : "level2@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top/level2"
+ ]
}
],
"applications": [
@@ -347,6 +371,26 @@
}
}
],
+ "groups" : [
+ {
+ "name": "top",
+ "attributes": {
+ "topAttribute": ["true"]
+
+ },
+ "realmRoles": ["manager"],
+ "subGroups": [
+ {
+ "name": "level2",
+ "realmRoles": ["user"],
+ "attributes": {
+ "level2Attribute": ["true"]
+
+ }
+ }
+ ]
+ }
+ ],
"roles" : {
"realm" : [
{
diff --git a/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java
index e71887e..cd3c11a 100755
--- a/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java
+++ b/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java
@@ -104,6 +104,11 @@ public class JettySamlTest {
}
@Test
+ public void testErrorHandling() throws Exception {
+ testStrategy.testErrorHandling();
+ }
+
+ @Test
public void testPostSimpleLoginLogout() {
testStrategy.testPostSimpleLoginLogout();
}
@@ -165,12 +170,7 @@ public class JettySamlTest {
@Test
public void testPostBadRealmSignature() {
- testStrategy.testPostBadRealmSignature( new SamlAdapterTestStrategy.CheckAuthError() {
- @Override
- public void check(WebDriver driver) {
- Assert.assertEquals(driver.getPageSource(), "");
- }
- });
+ testStrategy.testPostBadRealmSignature( );
}
@Test
diff --git a/testsuite/jetty/jetty92/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml b/testsuite/jetty/jetty92/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
index f44a60b..e41448a 100755
--- a/testsuite/jetty/jetty92/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
+++ b/testsuite/jetty/jetty92/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
@@ -25,6 +25,26 @@
<url-pattern>/error.html</url-pattern>
</servlet-mapping>
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
+
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml b/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
index 86db4a4..ed4f018 100755
--- a/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
+++ b/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
@@ -10,11 +10,39 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml b/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
index 86db4a4..ed4f018 100755
--- a/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
+++ b/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
@@ -10,11 +10,39 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/testsaml.json b/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/testsaml.json
index 0b13fb9..04c5dcd 100755
--- a/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/testsaml.json
+++ b/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/testsaml.json
@@ -40,6 +40,30 @@
{ "type" : "password",
"value" : "password" }
]
+ },
+ {
+ "username" : "topGroupUser",
+ "enabled": true,
+ "email" : "top@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top"
+ ]
+ },
+ {
+ "username" : "level2GroupUser",
+ "enabled": true,
+ "email" : "level2@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top/level2"
+ ]
}
],
"applications": [
@@ -347,6 +371,26 @@
}
}
],
+ "groups" : [
+ {
+ "name": "top",
+ "attributes": {
+ "topAttribute": ["true"]
+
+ },
+ "realmRoles": ["manager"],
+ "subGroups": [
+ {
+ "name": "level2",
+ "realmRoles": ["user"],
+ "attributes": {
+ "level2Attribute": ["true"]
+
+ }
+ }
+ ]
+ }
+ ],
"roles" : {
"realm" : [
{
diff --git a/testsuite/tomcat6/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java b/testsuite/tomcat6/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
index c72ab01..0e6973a 100755
--- a/testsuite/tomcat6/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
+++ b/testsuite/tomcat6/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
@@ -113,6 +113,11 @@ public class TomcatSamlTest {
}
@Test
+ public void testErrorHandling() throws Exception {
+ testStrategy.testErrorHandling();
+ }
+
+ @Test
public void testPostSignedLoginLogoutEmailNameID() {
testStrategy.testPostSignedLoginLogoutEmailNameID();
}
@@ -149,12 +154,7 @@ public class TomcatSamlTest {
@Test
public void testPostBadRealmSignature() {
- testStrategy.testPostBadRealmSignature( new SamlAdapterTestStrategy.CheckAuthError() {
- @Override
- public void check(WebDriver driver) {
- Assert.assertEquals(driver.getPageSource(), "");
- }
- });
+ testStrategy.testPostBadRealmSignature();
}
@Test
diff --git a/testsuite/tomcat6/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml b/testsuite/tomcat6/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
index c2cef86..fa6a47b 100755
--- a/testsuite/tomcat6/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
+++ b/testsuite/tomcat6/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
@@ -25,6 +25,26 @@
<url-pattern>/error.html</url-pattern>
</servlet-mapping>
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
+
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
@@ -44,11 +64,7 @@
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>demo</realm-name>
- <form-login-config>
- <form-login-page>/error.html</form-login-page>
- <form-error-page>/error.html</form-error-page>
- </form-login-config>
- </login-config>
+ </login-config>
<security-role>
<role-name>admin</role-name>
diff --git a/testsuite/tomcat6/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml b/testsuite/tomcat6/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
index 86db4a4..2f7ef22 100755
--- a/testsuite/tomcat6/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
+++ b/testsuite/tomcat6/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
@@ -10,12 +10,39 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
- <security-constraint>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page> <security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
<url-pattern>/*</url-pattern>
diff --git a/testsuite/tomcat6/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml b/testsuite/tomcat6/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
index 86db4a4..ed4f018 100755
--- a/testsuite/tomcat6/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
+++ b/testsuite/tomcat6/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
@@ -10,11 +10,39 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/tomcat6/src/test/resources/keycloak-saml/testsaml.json b/testsuite/tomcat6/src/test/resources/keycloak-saml/testsaml.json
index 0b13fb9..04c5dcd 100755
--- a/testsuite/tomcat6/src/test/resources/keycloak-saml/testsaml.json
+++ b/testsuite/tomcat6/src/test/resources/keycloak-saml/testsaml.json
@@ -40,6 +40,30 @@
{ "type" : "password",
"value" : "password" }
]
+ },
+ {
+ "username" : "topGroupUser",
+ "enabled": true,
+ "email" : "top@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top"
+ ]
+ },
+ {
+ "username" : "level2GroupUser",
+ "enabled": true,
+ "email" : "level2@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top/level2"
+ ]
}
],
"applications": [
@@ -347,6 +371,26 @@
}
}
],
+ "groups" : [
+ {
+ "name": "top",
+ "attributes": {
+ "topAttribute": ["true"]
+
+ },
+ "realmRoles": ["manager"],
+ "subGroups": [
+ {
+ "name": "level2",
+ "realmRoles": ["user"],
+ "attributes": {
+ "level2Attribute": ["true"]
+
+ }
+ }
+ ]
+ }
+ ],
"roles" : {
"realm" : [
{
diff --git a/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java b/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
index f9cb853..2483333 100755
--- a/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
+++ b/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
@@ -93,6 +93,11 @@ public class TomcatSamlTest {
@Test
+ public void testErrorHandling() throws Exception {
+ testStrategy.testErrorHandling();
+ }
+
+ @Test
public void testPostSimpleLoginLogout() {
testStrategy.testPostSimpleLoginLogout();
}
@@ -154,12 +159,7 @@ public class TomcatSamlTest {
@Test
public void testPostBadRealmSignature() {
- testStrategy.testPostBadRealmSignature( new SamlAdapterTestStrategy.CheckAuthError() {
- @Override
- public void check(WebDriver driver) {
- Assert.assertEquals(driver.getPageSource(), "");
- }
- });
+ testStrategy.testPostBadRealmSignature();
}
@Test
diff --git a/testsuite/tomcat7/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml b/testsuite/tomcat7/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
index c2cef86..fdd69a3 100755
--- a/testsuite/tomcat7/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
+++ b/testsuite/tomcat7/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
@@ -25,6 +25,26 @@
<url-pattern>/error.html</url-pattern>
</servlet-mapping>
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
+
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/tomcat7/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml b/testsuite/tomcat7/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
index 86db4a4..ed4f018 100755
--- a/testsuite/tomcat7/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
+++ b/testsuite/tomcat7/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
@@ -10,11 +10,39 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/tomcat7/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml b/testsuite/tomcat7/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
index 86db4a4..ed4f018 100755
--- a/testsuite/tomcat7/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
+++ b/testsuite/tomcat7/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
@@ -10,11 +10,39 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/tomcat7/src/test/resources/keycloak-saml/testsaml.json b/testsuite/tomcat7/src/test/resources/keycloak-saml/testsaml.json
index 0b13fb9..04c5dcd 100755
--- a/testsuite/tomcat7/src/test/resources/keycloak-saml/testsaml.json
+++ b/testsuite/tomcat7/src/test/resources/keycloak-saml/testsaml.json
@@ -40,6 +40,30 @@
{ "type" : "password",
"value" : "password" }
]
+ },
+ {
+ "username" : "topGroupUser",
+ "enabled": true,
+ "email" : "top@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top"
+ ]
+ },
+ {
+ "username" : "level2GroupUser",
+ "enabled": true,
+ "email" : "level2@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top/level2"
+ ]
}
],
"applications": [
@@ -347,6 +371,26 @@
}
}
],
+ "groups" : [
+ {
+ "name": "top",
+ "attributes": {
+ "topAttribute": ["true"]
+
+ },
+ "realmRoles": ["manager"],
+ "subGroups": [
+ {
+ "name": "level2",
+ "realmRoles": ["user"],
+ "attributes": {
+ "level2Attribute": ["true"]
+
+ }
+ }
+ ]
+ }
+ ],
"roles" : {
"realm" : [
{
diff --git a/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java b/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
index 405c6ee..a4a3829 100755
--- a/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
+++ b/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
@@ -93,6 +93,11 @@ public class TomcatSamlTest {
public SamlAdapterTestStrategy testStrategy = new SamlAdapterTestStrategy("http://localhost:8081/auth", "http://localhost:8082", keycloakRule);
@Test
+ public void testErrorHandling() throws Exception {
+ testStrategy.testErrorHandling();
+ }
+
+ @Test
public void testPostSimpleLoginLogout() {
testStrategy.testPostSimpleLoginLogout();
}
@@ -154,12 +159,7 @@ public class TomcatSamlTest {
@Test
public void testPostBadRealmSignature() {
- testStrategy.testPostBadRealmSignature( new SamlAdapterTestStrategy.CheckAuthError() {
- @Override
- public void check(WebDriver driver) {
- Assert.assertEquals(driver.getPageSource(), "");
- }
- });
+ testStrategy.testPostBadRealmSignature();
}
@Test
diff --git a/testsuite/tomcat8/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml b/testsuite/tomcat8/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
index c2cef86..fdd69a3 100755
--- a/testsuite/tomcat8/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
+++ b/testsuite/tomcat8/src/test/resources/adapter-test/customer-db-error-page/WEB-INF/web.xml
@@ -25,6 +25,26 @@
<url-pattern>/error.html</url-pattern>
</servlet-mapping>
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
+
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/tomcat8/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml b/testsuite/tomcat8/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
index 86db4a4..ed4f018 100755
--- a/testsuite/tomcat8/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
+++ b/testsuite/tomcat8/src/test/resources/keycloak-saml/bad-realm-signed-post/WEB-INF/web.xml
@@ -10,11 +10,39 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/tomcat8/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml b/testsuite/tomcat8/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
index 86db4a4..ed4f018 100755
--- a/testsuite/tomcat8/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
+++ b/testsuite/tomcat8/src/test/resources/keycloak-saml/signed-get/WEB-INF/web.xml
@@ -10,11 +10,39 @@
<servlet-name>SendUsernameServlet</servlet-name>
<servlet-class>org.keycloak.testsuite.keycloaksaml.SendUsernameServlet</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>Error Servlet</servlet-name>
+ <servlet-class>org.keycloak.testsuite.rule.ErrorServlet</servlet-class>
+ </servlet>
<servlet-mapping>
<servlet-name>SendUsernameServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>Error Servlet</servlet-name>
+ <url-pattern>/error.html</url-pattern>
+ </servlet-mapping>
+
+ <error-page>
+ <error-code>400</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>401</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/error.html</location>
+ </error-page>
+
+ <error-page>
+ <error-code>500</error-code>
+ <location>/error.html</location>
+ </error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
diff --git a/testsuite/tomcat8/src/test/resources/keycloak-saml/testsaml.json b/testsuite/tomcat8/src/test/resources/keycloak-saml/testsaml.json
index 0b13fb9..04c5dcd 100755
--- a/testsuite/tomcat8/src/test/resources/keycloak-saml/testsaml.json
+++ b/testsuite/tomcat8/src/test/resources/keycloak-saml/testsaml.json
@@ -40,6 +40,30 @@
{ "type" : "password",
"value" : "password" }
]
+ },
+ {
+ "username" : "topGroupUser",
+ "enabled": true,
+ "email" : "top@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top"
+ ]
+ },
+ {
+ "username" : "level2GroupUser",
+ "enabled": true,
+ "email" : "level2@redhat.com",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "groups": [
+ "/top/level2"
+ ]
}
],
"applications": [
@@ -347,6 +371,26 @@
}
}
],
+ "groups" : [
+ {
+ "name": "top",
+ "attributes": {
+ "topAttribute": ["true"]
+
+ },
+ "realmRoles": ["manager"],
+ "subGroups": [
+ {
+ "name": "level2",
+ "realmRoles": ["user"],
+ "attributes": {
+ "level2Attribute": ["true"]
+
+ }
+ }
+ ]
+ }
+ ],
"roles" : {
"realm" : [
{
wildfly/adduser/pom.xml 54(+54 -0)
diff --git a/wildfly/adduser/pom.xml b/wildfly/adduser/pom.xml
new file mode 100755
index 0000000..8a2bc8b
--- /dev/null
+++ b/wildfly/adduser/pom.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+~ Copyright 2013 JBoss Inc
+~
+~ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-wildfly-parent</artifactId>
+ <version>1.7.0.Final-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>keycloak-wildfly-adduser</artifactId>
+ <name>Keycloak WildFly Add User Script</name>
+ <description/>
+ <packaging>jar</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-model-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-common</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.wildfly.core</groupId>
+ <artifactId>wildfly-domain-management</artifactId>
+ <version>${wildfly.core.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.aesh</groupId>
+ <artifactId>aesh</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/wildfly/adduser/src/main/java/org/keycloak/wildfly/adduser/AddUser.java b/wildfly/adduser/src/main/java/org/keycloak/wildfly/adduser/AddUser.java
new file mode 100644
index 0000000..7be53c6
--- /dev/null
+++ b/wildfly/adduser/src/main/java/org/keycloak/wildfly/adduser/AddUser.java
@@ -0,0 +1,292 @@
+package org.keycloak.wildfly.adduser;
+
+import org.codehaus.jackson.type.TypeReference;
+import org.jboss.aesh.cl.CommandDefinition;
+import org.jboss.aesh.cl.Option;
+import org.jboss.aesh.cl.parser.ParserGenerator;
+import org.jboss.aesh.console.command.Command;
+import org.jboss.aesh.console.command.CommandNotFoundException;
+import org.jboss.aesh.console.command.CommandResult;
+import org.jboss.aesh.console.command.container.CommandContainer;
+import org.jboss.aesh.console.command.invocation.CommandInvocation;
+import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
+import org.jboss.aesh.console.command.registry.CommandRegistry;
+import org.keycloak.common.util.Base64;
+import org.keycloak.models.Constants;
+import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AddUser {
+
+ private static final String COMMAND_NAME = "add-user";
+ private static final int DEFAULT_HASH_ITERATIONS = 100000;
+
+ public static void main(String[] args) throws Exception {
+ AddUserCommand command = new AddUserCommand();
+ try {
+ ParserGenerator.parseAndPopulate(command, COMMAND_NAME, args);
+ } catch (Exception e) {
+ System.err.println(e.getMessage());
+ System.exit(1);
+ }
+
+ if (command.isContainer()) {
+ List<String> l = new LinkedList<>(Arrays.asList(args));
+ l.remove("--container");
+ args = l.toArray(new String[l.size()]);
+
+ org.jboss.as.domain.management.security.adduser.AddUser.main(args);
+ } else if (command.isHelp()) {
+ printHelp(command);
+ } else {
+ try {
+ checkRequired(command, "user");
+ checkRequired(command, "password");
+
+ File addUserFile = getAddUserFile(command);
+
+ createUser(addUserFile, command.getRealm(), command.getUser(), command.getPassword(), command.getRoles(), command.getIterations());
+ } catch (Exception e) {
+ System.err.println(e.getMessage());
+ System.exit(1);
+ }
+ }
+ }
+
+ private static File getAddUserFile(AddUserCommand command) throws Exception {
+ File configDir;
+ if (command.isDomain()) {
+ if (command.getDc() != null) {
+ configDir = new File(command.getDc());
+ } else if (System.getProperty("jboss.domain.config.user.dir") != null) {
+ configDir = new File(System.getProperty("jboss.domain.config.user.dir"));
+ } else if (System.getenv("JBOSS_HOME") != null) {
+ configDir = new File(System.getenv("JBOSS_HOME") + File.separator + "domain" + File.separator + "configuration");
+ } else {
+ throw new Exception("Could not find domain configuration directory");
+ }
+ } else {
+ if (command.getSc() != null) {
+ configDir = new File(command.getSc());
+ } else if (System.getProperty("jboss.server.config.user.dir") != null) {
+ configDir = new File(System.getProperty("jboss.server.config.user.dir"));
+ } else if (System.getenv("JBOSS_HOME") != null) {
+ configDir = new File(System.getenv("JBOSS_HOME") + File.separator + "standalone" + File.separator + "configuration");
+ } else {
+ throw new Exception("Could not find standalone configuration directory");
+ }
+ }
+
+ if (!configDir.isDirectory()) {
+ throw new Exception("'" + configDir + "' does not exist or is not a directory");
+ }
+
+ File addUserFile = new File(configDir, "keycloak-add-user.json");
+ return addUserFile;
+ }
+
+ private static void createUser(File addUserFile, String realmName, String userName, String password, String rolesString, int iterations) throws Exception {
+ List<RealmRepresentation> realms;
+ if (addUserFile.isFile()) {
+ realms = JsonSerialization.readValue(new FileInputStream(addUserFile), new TypeReference<List<RealmRepresentation>>() {});
+ } else {
+ realms = new LinkedList<>();
+ }
+
+ if (realmName == null) {
+ realmName = "master";
+ }
+
+ RealmRepresentation realm = null;
+ for (RealmRepresentation r : realms) {
+ if (r.getRealm().equals(realmName)) {
+ realm = r;
+ }
+ }
+
+ if (realm == null) {
+ realm = new RealmRepresentation();
+ realm.setRealm(realmName);
+ realms.add(realm);
+ realm.setUsers(new LinkedList<UserRepresentation>());
+ }
+
+ for (UserRepresentation u : realm.getUsers()) {
+ if (u.getUsername().equals(userName)) {
+ throw new Exception("User with username '" + userName + "' already added to '" + addUserFile + "'");
+ }
+ }
+
+ UserRepresentation user = new UserRepresentation();
+ user.setEnabled(true);
+ user.setUsername(userName);
+ user.setCredentials(new LinkedList<CredentialRepresentation>());
+
+ byte[] salt = Pbkdf2PasswordEncoder.getSalt();
+ iterations = iterations > 0 ? iterations : DEFAULT_HASH_ITERATIONS;
+
+ CredentialRepresentation credentials = new CredentialRepresentation();
+ credentials.setType(CredentialRepresentation.PASSWORD);
+ credentials.setHashIterations(iterations);
+ credentials.setSalt(Base64.encodeBytes(salt));
+ credentials.setHashedSaltedValue(new Pbkdf2PasswordEncoder(salt).encode(password, iterations));
+
+ user.getCredentials().add(credentials);
+
+ String[] roles;
+ if (rolesString != null) {
+ roles = rolesString.split(",");
+ } else {
+ if (realmName.equals("master")) {
+ roles = new String[] { "admin" };
+ } else {
+ roles = new String[] { "realm-management/realm-admin" };
+ }
+ }
+
+ for (String r : roles) {
+ if (r.indexOf('/') != -1) {
+ String[] cr = r.split("/");
+ String client = cr[0];
+ String clientRole = cr[1];
+
+ if (user.getClientRoles() == null) {
+ user.setClientRoles(new HashMap<String, List<String>>());
+ }
+
+ if (user.getClientRoles().get(client) == null) {
+ user.getClientRoles().put(client, new LinkedList<String>());
+ }
+
+ user.getClientRoles().get(client).add(clientRole);
+ } else {
+ if (user.getRealmRoles() == null) {
+ user.setRealmRoles(new LinkedList<String>());
+ }
+ user.getRealmRoles().add(r);
+ }
+ }
+
+ realm.getUsers().add(user);
+
+ JsonSerialization.writeValuePrettyToStream(new FileOutputStream(addUserFile), realms);
+ System.out.println("Added '" + userName + "' to '" + addUserFile + "', restart server to load user");
+ }
+
+ private static void checkRequired(Command command, String field) throws Exception {
+ Method m = command.getClass().getMethod("get" + Character.toUpperCase(field.charAt(0)) + field.substring(1));
+ if (m.invoke(command) == null) {
+ Option option = command.getClass().getDeclaredField(field).getAnnotation(Option.class);
+ String optionName;
+ if (option != null && option.shortName() != '\u0000') {
+ optionName = "-" + option.shortName() + ", --" + field;
+ } else {
+ optionName = "--" + field;
+ }
+ throw new Exception("Option: " + optionName + " is required");
+ }
+ }
+
+ private static void printHelp(Command command) throws CommandNotFoundException {
+ CommandRegistry registry = new AeshCommandRegistryBuilder().command(command).create();
+ CommandContainer commandContainer = registry.getCommand(command.getClass().getAnnotation(CommandDefinition.class).name(), null);
+ String help = commandContainer.printHelp(null);
+ System.out.println(help);
+ }
+
+ @CommandDefinition(name= COMMAND_NAME, description = "[options...]")
+ public static class AddUserCommand implements Command {
+
+ @Option(shortName = 'r', hasValue = true, description = "Name of realm to add user to")
+ private String realm;
+
+ @Option(shortName = 'u', hasValue = true, description = "Name of the user")
+ private String user;
+
+ @Option(shortName = 'p', hasValue = true, description = "Password of the user")
+ private String password;
+
+ @Option(hasValue = true, description = "Roles to add to the user")
+ private String roles;
+
+ @Option(hasValue = true, description = "Hash iterations")
+ private int iterations;
+
+ @Option(hasValue = false, description = "Enable domain mode")
+ private boolean domain;
+
+ @Option(hasValue = false, description = "Add user to underlying container. For usage use '--container --help'")
+ private boolean container;
+
+ @Option(hasValue = true, description = "Define the location of the server config directory")
+ private String sc;
+
+ @Option(hasValue = true, description = "Define the location of the domain config directory")
+ private String dc;
+
+ @Option(shortName = 'h', hasValue = false, description = "Display this help and exit")
+ private boolean help;
+
+ @Override
+ public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
+ return CommandResult.SUCCESS;
+ }
+
+ public String getRealm() {
+ return realm;
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public String getRoles() {
+ return roles;
+ }
+
+ public int getIterations() {
+ return iterations;
+ }
+
+ public boolean isDomain() {
+ return domain;
+ }
+
+ public boolean isContainer() {
+ return container;
+ }
+
+ public String getSc() {
+ return sc;
+ }
+
+ public String getDc() {
+ return dc;
+ }
+
+ public boolean isHelp() {
+ return help;
+ }
+ }
+
+}
\ No newline at end of file
wildfly/pom.xml 22(+22 -0)
diff --git a/wildfly/pom.xml b/wildfly/pom.xml
new file mode 100755
index 0000000..0d24ee8
--- /dev/null
+++ b/wildfly/pom.xml
@@ -0,0 +1,22 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.7.0.Final-SNAPSHOT</version>
+ </parent>
+
+ <name>Keycloak WildFly Integration</name>
+ <description/>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-wildfly-parent</artifactId>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>adduser</module>
+ <module>extensions</module>
+ <module>server-subsystem</module>
+ <module>server-eap6-subsystem</module>
+ </modules>
+</project>