keycloak-uncached
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java 106(+56 -50)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java 15(+6 -9)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java 142(+79 -63)
adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KcinitDriver.java 702(+702 -0)
adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java 281(+0 -281)
adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java 368(+235 -133)
adapters/oidc/kcinit/pom.xml 4(+2 -2)
adapters/oidc/kcinit/README.md 0(+0 -0)
adapters/oidc/kcinit/src/main/bin/kcinit 26(+26 -0)
adapters/oidc/pom.xml 2(+1 -1)
authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java 218(+0 -218)
authz/client/src/main/java/org/keycloak/authorization/client/representation/ScopeRepresentation.java 98(+0 -98)
authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java 39(+32 -7)
core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java 54(+41 -13)
core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java 39(+34 -5)
pom.xml 11(+11 -0)
server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java 12(+12 -0)
server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeAuthenticatorFactory.java 21(+21 -0)
server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeRequiredActionFactory.java 13(+13 -0)
services/src/main/java/org/keycloak/authentication/authenticators/AttemptedAuthenticator.java 46(+46 -0)
services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticatorFactory.java 12(+11 -1)
services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java 11(+9 -2)
services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticatorFactory.java 12(+11 -1)
services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticatorFactory.java 12(+11 -1)
services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticatorFactory.java 11(+10 -1)
services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java 1(+0 -1)
services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordFormFactory.java 12(+11 -1)
services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java 73(+73 -0)
services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java 122(+122 -0)
services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticatorFactory.java 102(+102 -0)
services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java 77(+77 -0)
services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java 103(+103 -0)
services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateProfile.java 68(+68 -0)
services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java 113(+113 -0)
services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java 152(+152 -0)
services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java 16(+12 -4)
services/src/main/java/org/keycloak/authorization/protection/resource/RegistrationResponse.java 50(+0 -50)
services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java 176(+0 -176)
services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaScopeRepresentation.java 98(+0 -98)
services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java 119(+31 -88)
services/src/main/java/org/keycloak/authorization/protection/resource/UmaResourceRepresentation.java 72(+72 -0)
services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java 18(+17 -1)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java 4(+4 -0)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java 2(+2 -0)
services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java 2(+1 -1)
services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java 4(+4 -0)
services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java 16(+15 -1)
services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory 2(+1 -1)
testsuite/integration-arquillian/test-apps/photoz/keycloak-lazy-load-path-authz-service.json 78(+78 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java 4(+2 -2)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json 10(+10 -0)
testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-lazy-load-authz-service.json 15(+15 -0)
testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java 14(+10 -4)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java 24(+23 -1)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcinitExec.java 58(+58 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java 211(+211 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPermissiveModeAdapterTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java 12(+1 -11)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleLazyLoadPathsAdapterTest.java 88(+88 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleNoLazyLoadPathsAdapterTest.java 42(+16 -26)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java 193(+8 -185)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java 10(+1 -9)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzLazyLoadPathsAdapterTest.java 58(+58 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java 8(+4 -4)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java 73(+61 -12)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java 170(+170 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java 4(+2 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java 564(+564 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ChallengeFlowTest.java 15(+2 -13)
testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleAdapterTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleLazyLoadPathsAdapterTest.java 29(+29 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzAdapterTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzLazyLoadPathsAdapterTest.java 31(+31 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/app-routing.module.ts 44(+44 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/assets/img/keycloak-logo-min.png 0(+0 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/account.module.ts 34(+34 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/account-routing.module.ts 31(+31 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/applications-page/applications.module.ts 44(+44 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/applications-page/applications-routing.module.ts 31(+31 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/authenticator-page/authenticator.module.ts 33(+33 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/authenticator-page/authenticator-routing.module.ts 31(+31 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.css 0(+0 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.html 68(+68 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.ts 28(+28 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/page-not-found/page-not-found.module.ts 34(+34 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/page-not-found/page-not-found-routing.module.ts 31(+31 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password.module.ts 34(+34 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-routing.module.ts 31(+31 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions.module.ts 43(+43 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions-page.component.ts 2(+1 -1)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions-routing.module.ts 31(+31 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/widgets/filterby.pipe.ts 0(+0 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/widgets/orderby.pipe.ts 0(+0 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/widgets/widgets.module.ts 36(+36 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.guard.ts 36(+36 -0)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.js 467(+303 -164)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.service.ts 15(+10 -5)
themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.html 18(+4 -14)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
index 96fbe5d..452583b 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
@@ -19,7 +19,6 @@ package org.keycloak.adapters.authorization;
import java.util.Collections;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import org.jboss.logging.Logger;
@@ -30,10 +29,12 @@ import org.keycloak.adapters.spi.HttpFacade.Request;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthorizationContext;
import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessToken.Authorization;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.ScopeEnforcementMode;
import org.keycloak.representations.idm.authorization.Permission;
/**
@@ -42,31 +43,23 @@ import org.keycloak.representations.idm.authorization.Permission;
public abstract class AbstractPolicyEnforcer {
private static Logger LOGGER = Logger.getLogger(AbstractPolicyEnforcer.class);
- private final PolicyEnforcerConfig enforcerConfig;
- private final PolicyEnforcer policyEnforcer;
+ private static final String HTTP_METHOD_DELETE = "DELETE";
- private Map<String, PathConfig> paths;
- private AuthzClient authzClient;
- private PathMatcher pathMatcher;
+ private final PolicyEnforcer policyEnforcer;
- public AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
+ protected AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
this.policyEnforcer = policyEnforcer;
- this.enforcerConfig = policyEnforcer.getEnforcerConfig();
- this.authzClient = policyEnforcer.getClient();
- this.pathMatcher = policyEnforcer.getPathMatcher();
- this.paths = policyEnforcer.getPaths();
}
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
- EnforcementMode enforcementMode = this.enforcerConfig.getEnforcementMode();
+ EnforcementMode enforcementMode = getEnforcerConfig().getEnforcementMode();
if (EnforcementMode.DISABLED.equals(enforcementMode)) {
return createEmptyAuthorizationContext(true);
}
Request request = httpFacade.getRequest();
- String path = getPath(request);
- PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
+ PathConfig pathConfig = getPathConfig(request);
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
if (securityContext == null) {
@@ -79,16 +72,20 @@ public abstract class AbstractPolicyEnforcer {
AccessToken accessToken = securityContext.getToken();
if (accessToken != null) {
- LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
+ }
if (pathConfig == null) {
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
return createAuthorizationContext(accessToken, null);
}
- LOGGER.debugf("Could not find a configuration for path [%s]", path);
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Could not find a configuration for path [%s]", getPath(request));
+ }
- if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
+ if (isDefaultAccessDeniedUri(request)) {
return createAuthorizationContext(accessToken, null);
}
@@ -111,10 +108,18 @@ public abstract class AbstractPolicyEnforcer {
}
}
- LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
+ if (methodConfig != null && ScopeEnforcementMode.DISABLED.equals(methodConfig.getScopesEnforcementMode())) {
+ return createEmptyAuthorizationContext(true);
+ }
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
+ }
if (!challenge(pathConfig, methodConfig, httpFacade)) {
- LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
+ }
handleAccessDenied(httpFacade);
}
}
@@ -126,22 +131,21 @@ public abstract class AbstractPolicyEnforcer {
protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
Request request = httpFacade.getRequest();
- PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
- if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
+ if (isDefaultAccessDeniedUri(request)) {
return true;
}
- AccessToken.Authorization authorization = accessToken.getAuthorization();
+ Authorization authorization = accessToken.getAuthorization();
if (authorization == null) {
return false;
}
- List<Permission> permissions = authorization.getPermissions();
boolean hasPermission = false;
+ List<Permission> grantedPermissions = authorization.getPermissions();
- for (Permission permission : permissions) {
+ for (Permission permission : grantedPermissions) {
if (permission.getResourceId() != null) {
if (isResourcePermission(actualPathConfig, permission)) {
hasPermission = true;
@@ -151,9 +155,11 @@ public abstract class AbstractPolicyEnforcer {
}
if (hasResourceScopePermission(methodConfig, permission)) {
- LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
- if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
- this.paths.remove(actualPathConfig);
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions);
+ }
+ if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
+ policyEnforcer.getPaths().remove(actualPathConfig);
}
return true;
}
@@ -170,7 +176,9 @@ public abstract class AbstractPolicyEnforcer {
return true;
}
- LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, permissions);
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, grantedPermissions);
+ }
return false;
}
@@ -179,15 +187,21 @@ public abstract class AbstractPolicyEnforcer {
httpFacade.getResponse().sendError(403);
}
- private boolean isDefaultAccessDeniedUri(Request request, PolicyEnforcerConfig enforcerConfig) {
- String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
+ protected AuthzClient getAuthzClient() {
+ return policyEnforcer.getClient();
+ }
- if (accessDeniedPath != null) {
- if (request.getURI().contains(accessDeniedPath)) {
- return true;
- }
- }
- return false;
+ protected PolicyEnforcerConfig getEnforcerConfig() {
+ return policyEnforcer.getEnforcerConfig();
+ }
+
+ protected PolicyEnforcer getPolicyEnforcer() {
+ return policyEnforcer;
+ }
+
+ private boolean isDefaultAccessDeniedUri(Request request) {
+ String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
+ return accessDeniedPath != null && request.getURI().contains(accessDeniedPath);
}
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
@@ -215,20 +229,8 @@ public abstract class AbstractPolicyEnforcer {
return requiredScopes.isEmpty();
}
- protected AuthzClient getAuthzClient() {
- return this.authzClient;
- }
-
- protected PolicyEnforcerConfig getEnforcerConfig() {
- return enforcerConfig;
- }
-
- protected PolicyEnforcer getPolicyEnforcer() {
- return policyEnforcer;
- }
-
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
- return new ClientAuthorizationContext(authzClient) {
+ return new ClientAuthorizationContext(getAuthzClient()) {
@Override
public boolean hasPermission(String resourceName, String scopeName) {
return granted;
@@ -279,7 +281,7 @@ public abstract class AbstractPolicyEnforcer {
}
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
- return new ClientAuthorizationContext(accessToken, pathConfig, this.paths, authzClient);
+ return new ClientAuthorizationContext(accessToken, pathConfig, policyEnforcer.getPaths(), getAuthzClient());
}
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
@@ -297,4 +299,8 @@ public abstract class AbstractPolicyEnforcer {
private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
return permission.getResourceId().equals(actualPathConfig.getId());
}
+
+ private PathConfig getPathConfig(Request request) {
+ return isDefaultAccessDeniedUri(request) ? null : policyEnforcer.getPathMatcher().matches(getPath(request));
+ }
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java
index e699203..cf8815c 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathCache.java
@@ -24,6 +24,8 @@ import java.util.concurrent.locks.LockSupport;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
/**
+ * A simple LRU cache implementation supporting expiration and maximum number of entries.
+ *
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PathCache {
@@ -43,15 +45,6 @@ public class PathCache {
* Creates a new instance.
*
* @param maxEntries the maximum number of entries to keep in the cache
- */
- public PathCache(int maxEntries) {
- this(maxEntries, -1);
- }
-
- /**
- * Creates a new instance.
- *
- * @param maxEntries the maximum number of entries to keep in the cache
* @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire
*/
public PathCache(final int maxEntries, long maxAge) {
@@ -80,6 +73,10 @@ public class PathCache {
}
}
+ public boolean containsKey(String uri) {
+ return cache.containsKey(uri);
+ }
+
public PathConfig get(String uri) {
if (parkForReadAndCheckInterrupt()) {
return null;
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
index fe8aa1a..2d5f0cc 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -17,11 +17,11 @@
*/
package org.keycloak.adapters.authorization;
-import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -34,13 +34,13 @@ import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.ResourceRepresentation;
-import org.keycloak.authorization.client.representation.ScopeRepresentation;
import org.keycloak.authorization.client.resource.ProtectedResource;
+import org.keycloak.common.util.PathMatcher;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -69,8 +69,9 @@ public class PolicyEnforcer {
}
}
});
- this.pathMatcher = new PathMatcher(this.authzClient);
+
this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
+ this.pathMatcher = createPathMatcher(authzClient);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Initialization complete. Path configurations:");
@@ -104,11 +105,11 @@ public class PolicyEnforcer {
return context;
}
- PolicyEnforcerConfig getEnforcerConfig() {
+ public PolicyEnforcerConfig getEnforcerConfig() {
return enforcerConfig;
}
- AuthzClient getClient() {
+ public AuthzClient getClient() {
return authzClient;
}
@@ -116,11 +117,11 @@ public class PolicyEnforcer {
return paths;
}
- void addPath(PathConfig pathConfig) {
- paths.put(pathConfig.getPath(), pathConfig);
+ public PathMatcher<PathConfig> getPathMatcher() {
+ return pathMatcher;
}
- KeycloakDeployment getDeployment() {
+ public KeycloakDeployment getDeployment() {
return deployment;
}
@@ -144,7 +145,7 @@ public class PolicyEnforcer {
}
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
- Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
+ Map<String, PathConfig> paths = Collections.synchronizedMap(new LinkedHashMap<String, PathConfig>());
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
ResourceRepresentation resource;
@@ -168,36 +169,11 @@ public class PolicyEnforcer {
}
if (resource == null) {
- if (enforcerConfig.isCreateResources()) {
- LOGGER.debugf("Creating resource on server for path [%s].", pathConfig);
- ResourceRepresentation representation = new ResourceRepresentation();
-
- representation.setName(resourceName);
- representation.setType(pathConfig.getType());
- representation.setUri(path);
-
- HashSet<ScopeRepresentation> scopes = new HashSet<>();
-
- for (String scopeName : pathConfig.getScopes()) {
- ScopeRepresentation scope = new ScopeRepresentation();
-
- scope.setName(scopeName);
-
- scopes.add(scope);
- }
-
- representation.setScopes(scopes);
-
- ResourceRepresentation registrationResponse = protectedResource.create(representation);
-
- pathConfig.setId(registrationResponse.getId());
- } else {
- throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + "]. Make sure you have created a resource on the server that matches with the path configuration.");
- }
- } else {
- pathConfig.setId(resource.getId());
+ throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + "]. Make sure you have created a resource on the server that matches with the path configuration.");
}
+ pathConfig.setId(resource.getId());
+
PathConfig existingPath = null;
for (PathConfig current : paths.values()) {
@@ -222,45 +198,85 @@ public class PolicyEnforcer {
LOGGER.info("Querying the server for all resources associated with this application.");
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
- for (String id : protectedResource.findAll()) {
- ResourceRepresentation resourceDescription = protectedResource.findById(id);
+ if (!enforcerConfig.getLazyLoadPaths()) {
+ for (String id : protectedResource.findAll()) {
+ ResourceRepresentation resourceDescription = protectedResource.findById(id);
- if (resourceDescription.getUri() != null) {
- PathConfig pathConfig = createPathConfig(resourceDescription);
- paths.put(pathConfig.getPath(), pathConfig);
+ if (resourceDescription.getUri() != null) {
+ PathConfig pathConfig = PathConfig.createPathConfig(resourceDescription);
+ paths.put(pathConfig.getPath(), pathConfig);
+ }
}
}
return paths;
}
- static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
- PathConfig pathConfig = new PathConfig();
+ private PathMatcher<PathConfig> createPathMatcher(final AuthzClient authzClient) {
+ final PathCache pathCache = new PathCache(100, 30000);
- pathConfig.setId(resourceDescription.getId());
- pathConfig.setName(resourceDescription.getName());
+ return new PathMatcher<PathConfig>() {
+ @Override
+ public PathConfig matches(String targetUri) {
+ PathConfig pathConfig = pathCache.get(targetUri);
- String uri = resourceDescription.getUri();
+ if (pathCache.containsKey(targetUri) || pathConfig != null) {
+ return pathConfig;
+ }
- if (uri == null || "".equals(uri.trim())) {
- throw new RuntimeException("Failed to configure paths. Resource [" + resourceDescription.getName() + "] has an invalid or empty URI [" + uri + "].");
- }
+ pathConfig = super.matches(targetUri);
- pathConfig.setPath(uri);
+ if (enforcerConfig.getLazyLoadPaths() && (pathConfig == null || pathConfig.getPath().contains("*"))) {
+ try {
+ List<ResourceRepresentation> matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri);
- List<String> scopeNames = new ArrayList<>();
+ if (!matchingResources.isEmpty()) {
+ pathConfig = PathConfig.createPathConfig(matchingResources.get(0));
+ paths.put(pathConfig.getPath(), pathConfig);
+ }
+ } catch (Exception cause) {
+ LOGGER.errorf(cause, "Could not lazy load paths from server");
+ return null;
+ }
+ }
- for (ScopeRepresentation scope : resourceDescription.getScopes()) {
- scopeNames.add(scope.getName());
- }
+ pathCache.put(targetUri, pathConfig);
- pathConfig.setScopes(scopeNames);
- pathConfig.setType(resourceDescription.getType());
+ return pathConfig;
+ }
- return pathConfig;
- }
+ @Override
+ protected String getPath(PathConfig entry) {
+ return entry.getPath();
+ }
- public PathMatcher getPathMatcher() {
- return pathMatcher;
+ @Override
+ protected Collection<PathConfig> getPaths() {
+ return paths.values();
+ }
+
+ @Override
+ protected PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
+ if (originalConfig.hasPattern()) {
+ ProtectedResource resource = authzClient.protection().resource();
+ List<ResourceRepresentation> search = resource.findByUri(path);
+
+ if (!search.isEmpty()) {
+ // resource does exist on the server, cache it
+ ResourceRepresentation targetResource = search.get(0);
+ PathConfig config = PathConfig.createPathConfig(targetResource);
+
+ config.setScopes(originalConfig.getScopes());
+ config.setMethods(originalConfig.getMethods());
+ config.setParentConfig(originalConfig);
+ config.setEnforcementMode(originalConfig.getEnforcementMode());
+
+ return config;
+ }
+ }
+
+ return null;
+ }
+ };
}
}
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KcinitDriver.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KcinitDriver.java
new file mode 100644
index 0000000..4693f62
--- /dev/null
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KcinitDriver.java
@@ -0,0 +1,702 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.adapters.installed;
+
+import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.ServerRequest;
+import org.keycloak.common.util.Base64;
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jwe.*;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.*;
+import java.nio.file.Paths;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.util.*;
+
+/**
+ * All kcinit commands that take input ask for
+ * <p>
+ * 1. . kcinit
+ * - setup and export KC_SESSION_KEY env var if not set.
+ * - checks to see if master token valid, refresh is possible, exit if token valid
+ * - performs command line login
+ * - stores master token for master client
+ * 2. app.sh is a wrapper for app cli.
+ * - token=`kcinit token app`
+ * - checks to see if token for app client has been fetched, refresh if valid, output token to sys.out if exists
+ * - if no token, login. Prompts go to stderr.
+ * - pass token as cmd line param to app or as environment variable.
+ * <p>
+ * 3. kcinit password {password}
+ * - outputs password key that is used for encryption.
+ * - can be used in .bashrc as export KC_SESSSION_KEY=`kcinit password {password}` or just set it in .bat file
+ * <p>
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class KcinitDriver {
+
+ public static final String KC_SESSION_KEY = "KC_SESSION_KEY";
+ public static final String KC_LOGIN_CONFIG_PATH = "KC_LOGIN_CONFIG_PATH";
+ protected Map<String, String> config;
+ protected boolean debug = true;
+
+ protected static byte[] salt = new byte[]{-4, 88, 66, -101, 78, -94, 21, 105};
+
+ String[] args = null;
+
+ protected boolean forceLogin;
+ protected boolean browserLogin;
+
+ public void mainCmd(String[] args) throws Exception {
+
+ this.args = args;
+
+
+ if (args.length == 0) {
+ printHelp();
+ return;
+ }
+
+ if (args[0].equalsIgnoreCase("token")) {
+ //System.err.println("executing token");
+ token();
+ } else if (args[0].equalsIgnoreCase("login")) {
+ login();
+ } else if (args[0].equalsIgnoreCase("logout")) {
+ logout();
+ } else if (args[0].equalsIgnoreCase("env")) {
+ System.out.println(System.getenv().toString());
+ } else if (args[0].equalsIgnoreCase("install")) {
+ install();
+ } else if (args[0].equalsIgnoreCase("uninstall")) {
+ uninstall();
+ } else if (args[0].equalsIgnoreCase("password")) {
+ passwordKey();
+ } else {
+ KeycloakInstalled.console().writer().println("Unknown command: " + args[0]);
+ KeycloakInstalled.console().writer().println();
+ printHelp();
+ }
+ }
+
+ public String getHome() {
+ String home = System.getenv("HOME");
+ if (home == null) {
+ home = System.getProperty("HOME");
+ if (home == null) {
+ home = Paths.get("").toAbsolutePath().normalize().toString();
+ }
+ }
+ return home;
+ }
+
+ public void passwordKey() {
+ if (args.length < 2) {
+ printHelp();
+ System.exit(1);
+ }
+ String password = args[1];
+ try {
+ String encodedKey = generateEncryptionKey(password);
+ System.out.printf(encodedKey);
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ protected String generateEncryptionKey(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+ KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
+ SecretKey tmp = factory.generateSecret(spec);
+ byte[] aeskey = tmp.getEncoded();
+ return Base64.encodeBytes(aeskey);
+ }
+
+ public JWE createJWE() {
+ String key = getEncryptionKey();
+ if (key == null) {
+ throw new RuntimeException(KC_SESSION_KEY + " env var not set");
+ }
+ byte[] aesKey = null;
+ try {
+ aesKey = Base64.decode(key.getBytes("UTF-8"));
+ } catch (IOException e) {
+ throw new RuntimeException("invalid " + KC_SESSION_KEY + "env var");
+ }
+
+ JWE jwe = new JWE();
+ final SecretKey aesSecret = new SecretKeySpec(aesKey, "AES");
+ jwe.getKeyStorage()
+ .setEncryptionKey(aesSecret);
+ return jwe;
+ }
+
+ protected String encryptionKey;
+
+ protected String getEncryptionKey() {
+ if (encryptionKey != null) return encryptionKey;
+ return System.getenv(KC_SESSION_KEY);
+ }
+
+ public String encrypt(String payload) {
+ JWE jwe = createJWE();
+ JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
+ try {
+ jwe.header(jweHeader).content(payload.getBytes("UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("cannot encode payload as UTF-8");
+ }
+ try {
+ return jwe.encodeJwe();
+ } catch (JWEException e) {
+ throw new RuntimeException("cannot encrypt payload", e);
+ }
+ }
+
+ public String decrypt(String encoded) {
+ JWE jwe = createJWE();
+ try {
+ jwe.verifyAndDecodeJwe(encoded);
+ byte[] content = jwe.getContent();
+ if (content == null) return null;
+ return new String(content, "UTF-8");
+ } catch (Exception ex) {
+ throw new RuntimeException("cannot decrypt payload", ex);
+
+ }
+
+ }
+
+ public static String getenv(String name, String defaultValue) {
+ String val = System.getenv(name);
+ return val == null ? defaultValue : val;
+ }
+
+ public File getConfigDirectory() {
+ return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit").toFile();
+ }
+
+
+ public File getConfigFile() {
+ return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "config.json").toFile();
+ }
+
+ public File getTokenFilePath(String client) {
+ return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "tokens", client).toFile();
+ }
+
+ public File getTokenDirectory() {
+ return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "tokens").toFile();
+ }
+
+ protected boolean encrypted = false;
+
+ protected void checkEnv() {
+ File configFile = getConfigFile();
+ if (!configFile.exists()) {
+ KeycloakInstalled.console().writer().println("You have not configured kcinit. Please run 'kcinit install' to configure.");
+ System.exit(1);
+ }
+ byte[] data = new byte[0];
+ try {
+ data = readFileRaw(configFile);
+ } catch (IOException e) {
+
+ }
+ if (data == null) {
+ KeycloakInstalled.console().writer().println("Config file unreadable. Please run 'kcinit install' to configure.");
+ System.exit(1);
+
+ }
+ String encodedJwe = null;
+ try {
+ encodedJwe = new String(data, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+
+ if (encodedJwe.contains("realm")) {
+ encrypted = false;
+ return;
+ } else {
+ encrypted = true;
+ }
+
+ if (System.getenv(KC_SESSION_KEY) == null) {
+ promptLocalPassword();
+ }
+ }
+
+ protected void promptLocalPassword() {
+ String password = KeycloakInstalled.console().passwordPrompt("Enter password to unlock kcinit config files: ");
+ try {
+ encryptionKey = generateEncryptionKey(password);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ protected String readFile(File fp) {
+ try {
+ byte[] data = readFileRaw(fp);
+ if (data == null) return null;
+ String file = new String(data, "UTF-8");
+ if (!encrypted) {
+ return file;
+ }
+ String decrypted = decrypt(file);
+ if (decrypted == null)
+ throw new RuntimeException("Unable to decrypt file. Did you set your local password correctly?");
+ return decrypted;
+ } catch (IOException e) {
+ throw new RuntimeException("failed to decrypt file: " + fp.getAbsolutePath() + " Did you set your local password correctly?", e);
+ }
+
+
+ }
+
+ protected byte[] readFileRaw(File fp) throws IOException {
+ if (!fp.exists()) return null;
+ FileInputStream fis = new FileInputStream(fp);
+ byte[] data = new byte[(int) fp.length()];
+ fis.read(data);
+ fis.close();
+ return data;
+ }
+
+ protected void writeFile(File fp, String payload) {
+ try {
+ String data = payload;
+ if (encrypted) data = encrypt(payload);
+ FileOutputStream fos = new FileOutputStream(fp);
+ fos.write(data.getBytes("UTF-8"));
+ fos.flush();
+ fos.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ public void install() {
+ if (getEncryptionKey() == null) {
+ if (KeycloakInstalled.console().confirm("Do you want to protect tokens stored locally with a password? (y/n): ")) {
+ String password = "p";
+ String confirm = "c";
+ do {
+ password = KeycloakInstalled.console().passwordPrompt("Enter local password: ");
+ confirm = KeycloakInstalled.console().passwordPrompt("Confirm local password: ");
+ if (!password.equals(confirm)) {
+ KeycloakInstalled.console().writer().println();
+ KeycloakInstalled.console().writer().println("Confirmation does not match. Try again.");
+ KeycloakInstalled.console().writer().println();
+ }
+ } while (!password.equals(confirm));
+ try {
+ this.encrypted = true;
+ this.encryptionKey = generateEncryptionKey(password);
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+ } else {
+ if (!KeycloakInstalled.console().confirm("KC_SESSION_KEY env var already set. Do you want to use this as your local encryption key? (y/n): ")) {
+ KeycloakInstalled.console().writer().println("Unset KC_SESSION_KEY env var and run again");
+ System.exit(1);
+ }
+ this.encrypted = true;
+ this.encryptionKey = getEncryptionKey();
+ }
+ String server = KeycloakInstalled.console().readLine("Authentication server URL [http://localhost:8080/auth]: ").trim();
+ String realm = KeycloakInstalled.console().readLine("Name of realm [master]: ").trim();
+ String client = KeycloakInstalled.console().readLine("CLI client id [kcinit]: ").trim();
+ String secret = KeycloakInstalled.console().readLine("CLI client secret [none]: ").trim();
+ if (server.equals("")) {
+ server = "http://localhost:8080/auth";
+ }
+ if (realm.equals("")) {
+ realm = "master";
+ }
+ if (client.equals("")) {
+ client = "kcinit";
+ }
+ File configDir = getTokenDirectory();
+ configDir.mkdirs();
+
+ File configFile = getConfigFile();
+ Map<String, String> props = new HashMap<>();
+ props.put("server", server);
+ props.put("realm", realm);
+ props.put("client", client);
+ props.put("secret", secret);
+
+ try {
+ String json = JsonSerialization.writeValueAsString(props);
+ writeFile(configFile, json);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ KeycloakInstalled.console().writer().println();
+ KeycloakInstalled.console().writer().println("Installation complete!");
+ KeycloakInstalled.console().writer().println();
+ }
+
+
+ public void printHelp() {
+ KeycloakInstalled.console().writer().println("Commands:");
+ KeycloakInstalled.console().writer().println(" login [-f] -f forces login");
+ KeycloakInstalled.console().writer().println(" logout");
+ KeycloakInstalled.console().writer().println(" token [client] - print access token of desired client. Defaults to default master client. Will print either 'error', 'not-allowed', or 'login-required' on error.");
+ KeycloakInstalled.console().writer().println(" install - Install this utility. Will store in $HOME/.keycloak/kcinit unless " + KC_LOGIN_CONFIG_PATH + " env var is set");
+ System.exit(1);
+ }
+
+
+ public AdapterConfig getConfig() {
+ File configFile = getConfigFile();
+ if (!configFile.exists()) {
+ KeycloakInstalled.console().writer().println("You have not configured kcinit. Please run 'kcinit install' to configure.");
+ System.exit(1);
+ return null;
+ }
+
+ AdapterConfig config = new AdapterConfig();
+ config.setAuthServerUrl((String) getConfigProperties().get("server"));
+ config.setRealm((String) getConfigProperties().get("realm"));
+ config.setResource((String) getConfigProperties().get("client"));
+ config.setSslRequired("external");
+ String secret = (String) getConfigProperties().get("secret");
+ if (secret != null && !secret.trim().equals("")) {
+ Map<String, Object> creds = new HashMap<>();
+ creds.put("secret", secret);
+ config.setCredentials(creds);
+ } else {
+ config.setPublicClient(true);
+ }
+ return config;
+ }
+
+ private Map<String, String> getConfigProperties() {
+ if (this.config != null) return this.config;
+ if (!getConfigFile().exists()) {
+ KeycloakInstalled.console().writer().println();
+ KeycloakInstalled.console().writer().println(("Config file does not exist. Run kcinit install to set it up."));
+ System.exit(1);
+ }
+ String json = readFile(getConfigFile());
+ try {
+ Map map = JsonSerialization.readValue(json, Map.class);
+ config = (Map<String, String>) map;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this.config;
+ }
+
+ public String readToken(String client) throws Exception {
+ String json = getTokenResponse(client);
+ if (json == null) return null;
+
+
+ if (json != null) {
+ try {
+ AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
+ if (Time.currentTime() < tokenResponse.getExpiresIn()) {
+ return tokenResponse.getToken();
+ }
+ AdapterConfig config = getConfig();
+ KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
+ installed.refreshToken(tokenResponse.getRefreshToken());
+ processResponse(installed, client);
+ return tokenResponse.getToken();
+ } catch (Exception e) {
+ File tokenFile = getTokenFilePath(client);
+ if (tokenFile.exists()) {
+ tokenFile.delete();
+ }
+
+ return null;
+ }
+ }
+ return null;
+
+ }
+
+ public String readRefreshToken(String client) throws Exception {
+ String json = getTokenResponse(client);
+ if (json == null) return null;
+
+
+ if (json != null) {
+ try {
+ AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
+ return tokenResponse.getRefreshToken();
+ } catch (Exception e) {
+ if (debug) {
+ e.printStackTrace();
+ }
+ File tokenFile = getTokenFilePath(client);
+ if (tokenFile.exists()) {
+ tokenFile.delete();
+ }
+
+ return null;
+ }
+ }
+ return null;
+
+ }
+
+
+ private String getTokenResponse(String client) throws IOException {
+ File tokenFile = getTokenFilePath(client);
+ try {
+ return readFile(tokenFile);
+ } catch (Exception e) {
+ if (debug) {
+ System.err.println("Failed to read encrypted file");
+ e.printStackTrace();
+ }
+ if (tokenFile.exists()) tokenFile.delete();
+ return null;
+ }
+ }
+
+
+ public void token() throws Exception {
+ KeycloakInstalled.console().stderrOutput();
+
+ checkEnv();
+ String masterClient = getMasterClient();
+ String client = masterClient;
+ if (args.length > 1) {
+ client = args[1];
+ }
+ //System.err.println("readToken: " + client);
+ String token = readToken(client);
+ if (token != null) {
+ System.out.print(token);
+ return;
+ }
+ if (token == null && client.equals(masterClient)) {
+ //System.err.println("not logged in, logging in.");
+ doConsoleLogin();
+ token = readToken(client);
+ if (token != null) {
+ System.out.print(token);
+ return;
+ }
+
+ }
+ String masterToken = readToken(masterClient);
+ if (masterToken == null) {
+ //System.err.println("not logged in, logging in.");
+ doConsoleLogin();
+ masterToken = readToken(masterClient);
+ if (masterToken == null) {
+ System.err.println("Login failed. Cannot retrieve token");
+ System.exit(1);
+ }
+ }
+
+ //System.err.println("exchange: " + client);
+ Client httpClient = getHttpClient();
+
+ WebTarget exchangeUrl = httpClient.target(getServer())
+ .path("/realms")
+ .path(getRealm())
+ .path("protocol/openid-connect/token");
+
+ Form form = new Form()
+ .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+ .param(OAuth2Constants.CLIENT_ID, masterClient)
+ .param(OAuth2Constants.SUBJECT_TOKEN, masterToken)
+ .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+ .param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE)
+ .param(OAuth2Constants.AUDIENCE, client);
+ if (getMasterClientSecret() != null) {
+ form.param(OAuth2Constants.CLIENT_SECRET, getMasterClientSecret());
+ }
+ Response response = exchangeUrl.request().post(Entity.form(
+ form
+ ));
+
+ if (response.getStatus() == 401 || response.getStatus() == 403) {
+ response.close();
+ System.err.println("Not allowed to exchange for client token");
+ System.exit(1);
+ }
+
+ if (response.getStatus() != 200) {
+ if (response.getMediaType() != null && response.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE)) {
+ try {
+ String json = response.readEntity(String.class);
+ OAuth2ErrorRepresentation error = JsonSerialization.readValue(json, OAuth2ErrorRepresentation.class);
+ System.err.println("Failed to exchange token: " + error.getError() + ". " + error.getErrorDescription());
+ System.exit(1);
+ } catch (Exception ignore) {
+ ignore.printStackTrace();
+
+ }
+ }
+
+ response.close();
+ System.err.println("Unknown error exchanging for client token: " + response.getStatus());
+ System.exit(1);
+ }
+
+ String json = response.readEntity(String.class);
+ response.close();
+ AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
+ if (tokenResponse.getToken() != null) {
+ getTokenDirectory().mkdirs();
+ tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
+ tokenResponse.setIdToken(null);
+ json = JsonSerialization.writeValueAsString(tokenResponse);
+ writeFile(getTokenFilePath(client), json);
+ System.out.printf(tokenResponse.getToken());
+ } else {
+ System.err.println("Error processing token");
+ System.exit(1);
+ }
+ }
+
+ protected String getMasterClientSecret() {
+ return getProperty("secret");
+ }
+
+ protected String getServer() {
+ return getProperty("server");
+ }
+
+ protected String getRealm() {
+ return getProperty("realm");
+ }
+
+ public String getProperty(String name) {
+ return (String) getConfigProperties().get(name);
+ }
+
+ protected boolean forceLogin() {
+ return args.length > 0 && args[0].equals("-f");
+
+ }
+
+ public Client getHttpClient() {
+ return new ResteasyClientBuilder().disableTrustManager().build();
+ }
+
+ public void login() throws Exception {
+ checkEnv();
+ this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
+ for (String arg : args) {
+ if (arg.equals("-f") || arg.equals("-force")) {
+ forceLogin = true;
+ this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
+ } else if (arg.equals("-browser") || arg.equals("-b")) {
+ browserLogin = true;
+ this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
+ } else {
+ System.err.println("Illegal argument: " + arg);
+ printHelp();
+ System.exit(1);
+ }
+ }
+
+ String masterClient = getMasterClient();
+ if (!forceLogin && readToken(masterClient) != null) {
+ KeycloakInstalled.console().writer().println("Already logged in. `kcinit -f` to force relogin");
+ return;
+ }
+ doConsoleLogin();
+ KeycloakInstalled.console().writer().println("Login successful!");
+ }
+
+ public void doConsoleLogin() throws Exception {
+ String masterClient = getMasterClient();
+ AdapterConfig config = getConfig();
+ KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
+ //System.err.println("calling loginCommandLine");
+ if (!installed.loginCommandLine()) {
+ System.exit(1);
+ }
+ processResponse(installed, masterClient);
+ }
+
+ private String getMasterClient() {
+ return getProperty("client");
+ }
+
+ private void processResponse(KeycloakInstalled installed, String client) throws IOException {
+ AccessTokenResponse tokenResponse = installed.getTokenResponse();
+ tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
+ tokenResponse.setIdToken(null);
+ String json = JsonSerialization.writeValueAsString(tokenResponse);
+ getTokenDirectory().mkdirs();
+ writeFile(getTokenFilePath(client), json);
+ }
+
+ public void logout() throws Exception {
+ String token = readRefreshToken(getMasterClient());
+ if (token != null) {
+ try {
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getConfig());
+ ServerRequest.invokeLogout(deployment, token);
+ } catch (Exception e) {
+ if (debug) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+ if (getTokenDirectory().exists()) {
+ for (File fp : getTokenDirectory().listFiles()) fp.delete();
+ }
+ }
+ public void uninstall() throws Exception {
+ File configFile = getConfigFile();
+ if (configFile.exists()) configFile.delete();
+ if (getTokenDirectory().exists()) {
+ for (File fp : getTokenDirectory().listFiles()) fp.delete();
+ }
+ }
+}
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
index f1fee42..4f311c2 100644
--- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
@@ -39,15 +39,7 @@ import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.awt.*;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.PrintStream;
-import java.io.PrintWriter;
-import java.io.PushbackInputStream;
-import java.io.Reader;
+import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
@@ -65,6 +57,7 @@ public class KeycloakInstalled {
public interface HttpResponseWriter {
void success(PrintWriter pw, KeycloakInstalled ki);
+
void failure(PrintWriter pw, KeycloakInstalled ki);
}
@@ -86,12 +79,12 @@ public class KeycloakInstalled {
private Locale locale;
private HttpResponseWriter loginResponseWriter;
private HttpResponseWriter logoutResponseWriter;
+ private ResteasyClient resteasyClient;
Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
Pattern codePattern = Pattern.compile("code=([^&]+)");
-
public KeycloakInstalled() {
InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON);
deployment = KeycloakDeploymentBuilder.build(config);
@@ -179,6 +172,10 @@ public class KeycloakInstalled {
this.logoutResponseWriter = logoutResponseWriter;
}
+ public void setResteasyClient(ResteasyClient resteasyClient) {
+ this.resteasyClient = resteasyClient;
+ }
+
public Locale getLocale() {
return locale;
}
@@ -302,6 +299,139 @@ public class KeycloakInstalled {
status = Status.LOGGED_MANUAL;
}
+ public static class Console {
+ protected java.io.Console console = System.console();
+ protected PrintWriter writer;
+ protected BufferedReader reader;
+
+ static Console SINGLETON = new Console();
+
+ private Console() {
+ }
+
+
+ public PrintWriter writer() {
+ if (console == null) {
+ if (writer == null) {
+ writer = new PrintWriter(System.err, true);
+ }
+ return writer;
+ }
+ return console.writer();
+ }
+
+ public Reader reader() {
+ if (console == null) {
+ return getReader();
+ }
+ return console.reader();
+ }
+
+ protected BufferedReader getReader() {
+ if (reader != null) return reader;
+ reader = new BufferedReader(new BufferedReader(new InputStreamReader(System.in)));
+ return reader;
+ }
+
+ public Console format(String fmt, Object... args) {
+ if (console == null) {
+ writer().format(fmt, args);
+ return this;
+ }
+ console.format(fmt, args);
+ return this;
+ }
+
+ public Console printf(String format, Object... args) {
+ if (console == null) {
+ writer().printf(format, args);
+ return this;
+ }
+ console.printf(format, args);
+ return this;
+ }
+
+ public String readLine(String fmt, Object... args) {
+ if (console == null) {
+ format(fmt, args);
+ return readLine();
+ }
+ return console.readLine(fmt, args);
+ }
+
+ public boolean confirm(String fmt, Object... args) {
+ String prompt = "";
+ while (!"y".equals(prompt) && !"n".equals(prompt)) {
+ prompt = readLine(fmt, args);
+ }
+ return "y".equals(prompt);
+
+ }
+
+ public String prompt(String fmt, Object... args) {
+ String prompt = "";
+ while (prompt.equals("")) {
+ prompt = readLine(fmt, args).trim();
+ }
+ return prompt;
+
+ }
+
+ public String passwordPrompt(String fmt, Object... args) {
+ String prompt = "";
+ while (prompt.equals("")) {
+ char[] val = readPassword(fmt, args);
+ prompt = new String(val);
+ prompt = prompt.trim();
+ }
+ return prompt;
+
+ }
+
+ public String readLine() {
+ if (console == null) {
+ try {
+ return getReader().readLine();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return console.readLine();
+ }
+
+ public char[] readPassword(String fmt, Object... args) {
+ if (console == null) {
+ return readLine(fmt, args).toCharArray();
+
+ }
+ return console.readPassword(fmt, args);
+ }
+
+ public char[] readPassword() {
+ if (console == null) {
+ return readLine().toCharArray();
+ }
+ return console.readPassword();
+ }
+
+ public void flush() {
+ if (console == null) {
+ System.err.flush();
+ return;
+ }
+ console.flush();
+ }
+
+ public void stderrOutput() {
+ //System.err.println("not using System.console()");
+ console = null;
+ }
+ }
+
+ public static Console console() {
+ return Console.SINGLETON;
+ }
+
public boolean loginCommandLine() throws IOException, ServerRequest.HttpFailure, VerificationException {
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
@@ -309,7 +439,6 @@ public class KeycloakInstalled {
}
-
/**
* Experimental proprietary WWW-Authentication challenge protocol.
* WWW-Authentication: X-Text-Form-Challenge callback="{url}" param="{param-name}" label="{param-display-label}"
@@ -325,62 +454,116 @@ public class KeycloakInstalled {
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
+ .queryParam("display", "console")
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
.build().toString();
- ResteasyClient client = new ResteasyClientBuilder().disableTrustManager().build();
+ ResteasyClient client = createResteasyClient();
try {
+ //System.err.println("initial request");
Response response = client.target(authUrl).request().get();
- if (response.getStatus() != 401) {
- return false;
- }
while (true) {
- String authenticationHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
- if (authenticationHeader == null) {
- return false;
- }
- if (!authenticationHeader.contains("X-Text-Form-Challenge")) {
+ if (response.getStatus() == 403) {
+ if (response.getMediaType() != null) {
+ String splash = response.readEntity(String.class);
+ console().writer().println(splash);
+ } else {
+ System.err.println("Forbidden to login");
+ }
return false;
- }
- if (response.getMediaType() != null) {
- String splash = response.readEntity(String.class);
- System.console().writer().println(splash);
- }
- Matcher m = callbackPattern.matcher(authenticationHeader);
- if (!m.find()) return false;
- String callback = m.group(1);
- //System.err.println("callback: " + callback);
- m = paramPattern.matcher(authenticationHeader);
- Form form = new Form();
- while (m.find()) {
- String param = m.group(1);
- String label = m.group(2);
- String mask = m.group(3).trim();
- boolean maskInput = mask.equals("true");
- String value = null;
- if (maskInput) {
- char[] txt = System.console().readPassword(label);
- value = new String(txt);
+ } else if (response.getStatus() == 401) {
+ String authenticationHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
+ if (authenticationHeader == null) {
+ System.err.println("Failure: Invalid protocol. No WWW-Authenticate header");
+ return false;
+ }
+ //System.err.println("got header: " + authenticationHeader);
+ if (!authenticationHeader.contains("X-Text-Form-Challenge")) {
+ System.err.println("Failure: Invalid WWW-Authenticate header.");
+ return false;
+ }
+ if (response.getMediaType() != null) {
+ String splash = response.readEntity(String.class);
+ console().writer().println(splash);
} else {
- value = System.console().readLine(label);
+ response.close();
+ }
+ Matcher m = callbackPattern.matcher(authenticationHeader);
+ if (!m.find()) {
+ System.err.println("Failure: Invalid WWW-Authenticate header.");
+ return false;
+ }
+ String callback = m.group(1);
+ //System.err.println("callback: " + callback);
+ m = paramPattern.matcher(authenticationHeader);
+ Form form = new Form();
+ while (m.find()) {
+ String param = m.group(1);
+ String label = m.group(2);
+ String mask = m.group(3).trim();
+ boolean maskInput = mask.equals("true");
+ String value = null;
+ if (maskInput) {
+ char[] txt = console().readPassword(label);
+ value = new String(txt);
+ } else {
+ value = console().readLine(label);
+ }
+ form.param(param, value);
}
- form.param(param, value);
+ response.close();
+ client.close();
+ client = createResteasyClient();
+ response = client.target(callback).request().post(Entity.form(form));
+ } else if (response.getStatus() == 302) {
+ int redirectCount = 0;
+ do {
+ String location = response.getLocation().toString();
+ Matcher m = codePattern.matcher(location);
+ if (!m.find()) {
+ response.close();
+ client.close();
+ client = createResteasyClient();
+ response = client.target(location).request().get();
+ } else {
+ response.close();
+ client.close();
+ String code = m.group(1);
+ processCode(code, redirectUri);
+ return true;
+ }
+ if (response.getStatus() == 302 && redirectCount++ > 4) {
+ System.err.println("Too many redirects. Aborting");
+ return false;
+ }
+ } while (response.getStatus() == 302);
+ } else {
+ System.err.println("Unknown response from server: " + response.getStatus());
+ return false;
}
- response = client.target(callback).request().post(Entity.form(form));
- if (response.getStatus() == 401) continue;
- if (response.getStatus() != 302) return false;
- String location = response.getLocation().toString();
- m = codePattern.matcher(location);
- if (!m.find()) return false;
- String code = m.group(1);
- processCode(code, redirectUri);
- return true;
}
+ } catch (Exception ex) {
+ throw ex;
} finally {
client.close();
}
}
+ protected ResteasyClient getResteasyClient() {
+ if (this.resteasyClient == null) {
+ this.resteasyClient = createResteasyClient();
+ }
+ return this.resteasyClient;
+ }
+
+ protected ResteasyClient createResteasyClient() {
+ return new ResteasyClientBuilder()
+ .connectionCheckoutTimeout(1, TimeUnit.HOURS)
+ .connectionTTL(1, TimeUnit.HOURS)
+ .socketTimeout(1, TimeUnit.HOURS)
+ .disableTrustManager().build();
+ }
+
public String getTokenString() throws VerificationException, IOException, ServerRequest.HttpFailure {
return tokenString;
@@ -400,7 +583,7 @@ public class KeycloakInstalled {
parseAccessToken(tokenResponse);
}
- public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
+ public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
parseAccessToken(tokenResponse);
@@ -452,7 +635,6 @@ public class KeycloakInstalled {
}
-
private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
parseAccessToken(tokenResponse);
@@ -474,86 +656,6 @@ public class KeycloakInstalled {
return sb.toString();
}
- public static class MaskingThread extends Thread {
- private volatile boolean stop;
- private char echochar = '*';
-
- public MaskingThread() {
- }
-
- /**
- * Begin masking until asked to stop.
- */
- public void run() {
-
- int priority = Thread.currentThread().getPriority();
- Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
-
- try {
- stop = true;
- while(stop) {
- System.out.print("\010" + echochar);
- try {
- // attempt masking at this rate
- Thread.currentThread().sleep(1);
- }catch (InterruptedException iex) {
- Thread.currentThread().interrupt();
- return;
- }
- }
- } finally { // restore the original priority
- Thread.currentThread().setPriority(priority);
- }
- }
-
- /**
- * Instruct the thread to stop masking.
- */
- public void stopMasking() {
- this.stop = false;
- }
- }
-
- public static String readMasked(Reader reader) {
- MaskingThread et = new MaskingThread();
- Thread mask = new Thread(et);
- mask.start();
-
- BufferedReader in = new BufferedReader(reader);
- String password = "";
-
- try {
- password = in.readLine();
- } catch (IOException ioe) {
- ioe.printStackTrace();
- }
- // stop masking
- et.stopMasking();
- // return the password entered by the user
- return password;
- }
-
- private String readLine(Reader reader, boolean mask) throws IOException {
- if (mask) {
- System.out.print(" ");
- return readMasked(reader);
- }
-
- StringBuilder sb = new StringBuilder();
-
- char cb[] = new char[1];
- while (reader.read(cb) != -1) {
- char c = cb[0];
- if ((c == '\n') || (c == '\r')) {
- break;
- } else {
- sb.append(c);
- }
- }
-
- return sb.toString();
- }
-
public class CallbackListener extends Thread {
adapters/oidc/kcinit/src/main/bin/kcinit 26(+26 -0)
diff --git a/adapters/oidc/kcinit/src/main/bin/kcinit b/adapters/oidc/kcinit/src/main/bin/kcinit
new file mode 100755
index 0000000..4f5c2c6
--- /dev/null
+++ b/adapters/oidc/kcinit/src/main/bin/kcinit
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+case "`uname`" in
+ CYGWIN*)
+ CFILE = `cygpath "$0"`
+ RESOLVED_NAME=`readlink -f "$CFILE"`
+ ;;
+ Darwin*)
+ RESOLVED_NAME=`readlink "$0"`
+ ;;
+ FreeBSD)
+ RESOLVED_NAME=`readlink -f "$0"`
+ ;;
+ Linux)
+ RESOLVED_NAME=`readlink -f "$0"`
+ ;;
+esac
+
+if [ "x$RESOLVED_NAME" = "x" ]; then
+ RESOLVED_NAME="$0"
+fi
+
+SCRIPTPATH=`dirname "$RESOLVED_NAME"`
+JAR=$SCRIPTPATH/kcinit-${project.version}.jar
+
+java -jar $JAR $@
diff --git a/adapters/oidc/kcinit/src/main/bin/kcinit.bat b/adapters/oidc/kcinit/src/main/bin/kcinit.bat
new file mode 100755
index 0000000..9055309
--- /dev/null
+++ b/adapters/oidc/kcinit/src/main/bin/kcinit.bat
@@ -0,0 +1,8 @@
+@echo off
+
+if "%OS%" == "Windows_NT" (
+ set "DIRNAME=%~dp0%"
+) else (
+ set DIRNAME=.\
+)
+java -jar %DIRNAME%\kcinit-${project.version}.jar %*
adapters/oidc/pom.xml 2(+1 -1)
diff --git a/adapters/oidc/pom.xml b/adapters/oidc/pom.xml
index c380735..6bc8473 100755
--- a/adapters/oidc/pom.xml
+++ b/adapters/oidc/pom.xml
@@ -34,7 +34,7 @@
<module>adapter-core</module>
<module>as7-eap6</module>
<module>installed</module>
- <module>cli-sso</module>
+ <module>kcinit</module>
<module>jaxrs-oauth-client</module>
<module>jetty</module>
<module>js</module>
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java
index cf2e91a..80c9e42 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java
@@ -23,11 +23,11 @@ import java.util.List;
import java.util.concurrent.Callable;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.util.Http;
import org.keycloak.authorization.client.util.Throwables;
import org.keycloak.authorization.client.util.TokenCallable;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.util.JsonSerialization;
/**
@@ -124,11 +124,11 @@ public class ProtectedResource {
/**
* Query the server for a resource given its <code>name</code> where the owner is the resource server itself.
*
- * @param id the resource name
+ * @param name the resource name
* @return a {@link ResourceRepresentation}
*/
public ResourceRepresentation findByName(String name) {
- String[] representations = find(null, name, null, configuration.getResource(), null, null, null, null);
+ String[] representations = find(null, name, null, configuration.getResource(), null, null, false, null, null);
if (representations.length == 0) {
return null;
@@ -145,7 +145,7 @@ public class ProtectedResource {
* @return a {@link ResourceRepresentation}
*/
public ResourceRepresentation findByName(String name, String ownerId) {
- String[] representations = find(null, name, null, ownerId, null, null, null, null);
+ String[] representations = find(null, name, null, ownerId, null, null, false, null, null);
if (representations.length == 0) {
return null;
@@ -163,11 +163,12 @@ public class ProtectedResource {
* @param owner the resource owner
* @param type the resource type
* @param scope the resource scope
+ * @param matchingUri the resource uri. Use this parameter to lookup a resource that best match the given uri
* @param firstResult the position of the first resource to retrieve
* @param maxResult the maximum number of resources to retrieve
* @return an array of strings with the resource ids
*/
- public String[] find(final String id, final String name, final String uri, final String owner, final String type, final String scope, final Integer firstResult, final Integer maxResult) {
+ public String[] find(final String id, final String name, final String uri, final String owner, final String type, final String scope, final boolean matchingUri, final Integer firstResult, final Integer maxResult) {
Callable<String[]> callable = new Callable<String[]>() {
@Override
public String[] call() throws Exception {
@@ -179,6 +180,7 @@ public class ProtectedResource {
.param("owner", owner)
.param("type", type)
.param("scope", scope)
+ .param("matchingUri", Boolean.valueOf(matchingUri).toString())
.param("deep", Boolean.FALSE.toString())
.param("first", firstResult != null ? firstResult.toString() : null)
.param("max", maxResult != null ? maxResult.toString() : null)
@@ -199,7 +201,7 @@ public class ProtectedResource {
*/
public String[] findAll() {
try {
- return find(null,null , null, null, null, null, null, null);
+ return find(null,null , null, null, null, null, false, null, null);
} catch (Exception cause) {
throw Throwables.handleWrapException("Could not find resource", cause);
}
@@ -233,7 +235,30 @@ public class ProtectedResource {
* @param uri the resource uri
*/
public List<ResourceRepresentation> findByUri(String uri) {
- String[] ids = find(null, null, uri, null, null, null, null, null);
+ String[] ids = find(null, null, uri, null, null, null, false, null, null);
+
+ if (ids.length == 0) {
+ return Collections.emptyList();
+ }
+
+ List<ResourceRepresentation> representations = new ArrayList<>();
+
+ for (String id : ids) {
+ representations.add(findById(id));
+ }
+
+ return representations;
+ }
+
+ /**
+ * Returns a list of resources that best matches the given {@code uri}. This method queries the server for resources whose
+ * {@link ResourceRepresentation#uri} best matches the given {@code uri}.
+ *
+ * @param uri the resource uri to match
+ * @return a list of resources
+ */
+ public List<ResourceRepresentation> findByMatchingUri(String uri) {
+ String[] ids = find(null, null, uri, null, null, null, true, null, null);
if (ids.length == 0) {
return Collections.emptyList();
diff --git a/common/src/main/java/org/keycloak/common/util/RandomString.java b/common/src/main/java/org/keycloak/common/util/RandomString.java
new file mode 100644
index 0000000..70ce02d
--- /dev/null
+++ b/common/src/main/java/org/keycloak/common/util/RandomString.java
@@ -0,0 +1,66 @@
+package org.keycloak.common.util;
+
+import java.security.SecureRandom;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Random;
+
+public class RandomString {
+
+ /**
+ * Generate a random string.
+ */
+ public String nextString() {
+ for (int idx = 0; idx < buf.length; ++idx)
+ buf[idx] = symbols[random.nextInt(symbols.length)];
+ return new String(buf);
+ }
+
+ public static final String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ public static final String lower = upper.toLowerCase(Locale.ROOT);
+
+ public static final String digits = "0123456789";
+
+ public static final String alphanum = upper + lower + digits;
+
+ private final Random random;
+
+ private final char[] symbols;
+
+ private final char[] buf;
+
+ public RandomString(int length, Random random, String symbols) {
+ if (length < 1) throw new IllegalArgumentException();
+ if (symbols.length() < 2) throw new IllegalArgumentException();
+ this.random = Objects.requireNonNull(random);
+ this.symbols = symbols.toCharArray();
+ this.buf = new char[length];
+ }
+
+ /**
+ * Create an alphanumeric string generator.
+ */
+ public RandomString(int length, Random random) {
+ this(length, random, alphanum);
+ }
+
+ /**
+ * Create an alphanumeric strings from a secure generator.
+ */
+ public RandomString(int length) {
+ this(length, new SecureRandom());
+ }
+
+ /**
+ * Create session identifiers.
+ */
+ public RandomString() {
+ this(21);
+ }
+
+ public static String randomCode(int length) {
+ return new RandomString(length).nextString();
+ }
+
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/keycloak/AuthorizationContext.java b/core/src/main/java/org/keycloak/AuthorizationContext.java
index 0a9b332..538a70f 100644
--- a/core/src/main/java/org/keycloak/AuthorizationContext.java
+++ b/core/src/main/java/org/keycloak/AuthorizationContext.java
@@ -59,53 +59,31 @@ public class AuthorizationContext {
return false;
}
- if (current != null) {
- if (current.getName().equals(resourceName)) {
- return true;
- }
- }
+ for (Permission permission : authorization.getPermissions()) {
+ if (resourceName.equalsIgnoreCase(permission.getResourceName()) || resourceName.equalsIgnoreCase(permission.getResourceId())) {
+ if (scopeName == null) {
+ return true;
+ }
- if (hasResourcePermission(resourceName)) {
- for (Permission permission : authorization.getPermissions()) {
- for (PathConfig pathHolder : paths.values()) {
- if (pathHolder.getId().equals(permission.getResourceId())) {
- if (permission.getScopes().contains(scopeName)) {
- return true;
- }
- }
+ if (permission.getScopes().contains(scopeName)) {
+ return true;
}
}
}
- return false;
- }
-
- public boolean hasResourcePermission(String resourceName) {
- if (this.authzToken == null) {
- return false;
- }
-
- Authorization authorization = this.authzToken.getAuthorization();
-
- if (authorization == null) {
- return false;
- }
-
if (current != null) {
if (current.getName().equals(resourceName)) {
return true;
}
}
- for (Permission permission : authorization.getPermissions()) {
- if (permission.getResourceName().equals(resourceName) || permission.getResourceId().equals(resourceName)) {
- return true;
- }
- }
-
return false;
}
+ public boolean hasResourcePermission(String resourceName) {
+ return hasPermission(resourceName, null);
+ }
+
public boolean hasScopePermission(String scopeName) {
if (this.authzToken == null) {
return false;
diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWE.java b/core/src/main/java/org/keycloak/jose/jwe/JWE.java
index 75759dd..8d954ea 100644
--- a/core/src/main/java/org/keycloak/jose/jwe/JWE.java
+++ b/core/src/main/java/org/keycloak/jose/jwe/JWE.java
@@ -18,13 +18,23 @@
package org.keycloak.jose.jwe;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
import org.keycloak.util.JsonSerialization;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@@ -193,4 +203,66 @@ public class JWE {
}
}
+ public static String encryptUTF8(String password, String saltString, String payload) {
+ byte[] bytes = null;
+ try {
+ bytes = payload.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ return encrypt(password, saltString, bytes);
+
+ }
+
+
+ public static String encrypt(String password, String saltString, byte[] payload) {
+ try {
+ byte[] salt = Base64.decode(saltString);
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+ KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
+ SecretKey tmp = factory.generateSecret(spec);
+ SecretKey aesKey = new SecretKeySpec(tmp.getEncoded(), "AES");
+
+ JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
+ JWE jwe = new JWE()
+ .header(jweHeader)
+ .content(payload);
+
+ jwe.getKeyStorage()
+ .setEncryptionKey(aesKey);
+
+ return jwe.encodeJwe();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static byte[] decrypt(String password, String saltString, String encodedJwe) {
+ try {
+ byte[] salt = Base64.decode(saltString);
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+ KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
+ SecretKey tmp = factory.generateSecret(spec);
+ SecretKey aesKey = new SecretKeySpec(tmp.getEncoded(), "AES");
+
+ JWE jwe = new JWE();
+ jwe.getKeyStorage()
+ .setEncryptionKey(aesKey);
+
+ jwe.verifyAndDecodeJwe(encodedJwe);
+ return jwe.getContent();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String decryptUTF8(String password, String saltString, String encodedJwe) {
+ byte[] payload = decrypt(password, saltString, encodedJwe);
+ try {
+ return new String(payload, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
}
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index df54112..2ef01cb 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -34,6 +34,8 @@ public interface OAuth2Constants {
String REDIRECT_URI = "redirect_uri";
+ String DISPLAY = "display";
+
String SCOPE = "scope";
String STATE = "state";
@@ -114,6 +116,7 @@ public interface OAuth2Constants {
String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";
+ String DISPLAY_CONSOLE = "console";
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
index 89dadbf..71b44a4 100644
--- a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
@@ -23,16 +23,14 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PolicyEnforcerConfig {
- @JsonProperty("create-resources")
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private Boolean createResources = Boolean.FALSE;
-
@JsonProperty("enforcement-mode")
private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
@@ -40,6 +38,9 @@ public class PolicyEnforcerConfig {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<PathConfig> paths = new ArrayList<>();
+ @JsonProperty("lazy-load-paths")
+ private Boolean lazyLoadPaths = Boolean.FALSE;
+
@JsonProperty("on-deny-redirect-to")
@JsonInclude(JsonInclude.Include.NON_NULL)
private String onDenyRedirectTo;
@@ -48,14 +49,18 @@ public class PolicyEnforcerConfig {
@JsonInclude(JsonInclude.Include.NON_NULL)
private UserManagedAccessConfig userManagedAccess;
- public Boolean isCreateResources() {
- return this.createResources;
- }
-
public List<PathConfig> getPaths() {
return this.paths;
}
+ public Boolean getLazyLoadPaths() {
+ return lazyLoadPaths;
+ }
+
+ public void setLazyLoadPaths(Boolean lazyLoadPaths) {
+ this.lazyLoadPaths = lazyLoadPaths;
+ }
+
public EnforcementMode getEnforcementMode() {
return this.enforcementMode;
}
@@ -68,10 +73,6 @@ public class PolicyEnforcerConfig {
return this.userManagedAccess;
}
- public void setCreateResources(Boolean createResources) {
- this.createResources = createResources;
- }
-
public void setPaths(List<PathConfig> paths) {
this.paths = paths;
}
@@ -90,6 +91,32 @@ public class PolicyEnforcerConfig {
public static class PathConfig {
+ public static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
+ PathConfig pathConfig = new PathConfig();
+
+ pathConfig.setId(resourceDescription.getId());
+ pathConfig.setName(resourceDescription.getName());
+
+ String uri = resourceDescription.getUri();
+
+ if (uri == null || "".equals(uri.trim())) {
+ throw new RuntimeException("Failed to configure paths. Resource [" + resourceDescription.getName() + "] has an invalid or empty URI [" + uri + "].");
+ }
+
+ pathConfig.setPath(uri);
+
+ List<String> scopeNames = new ArrayList<>();
+
+ for (ScopeRepresentation scope : resourceDescription.getScopes()) {
+ scopeNames.add(scope.getName());
+ }
+
+ pathConfig.setScopes(scopeNames);
+ pathConfig.setType(resourceDescription.getType());
+
+ return pathConfig;
+ }
+
private String name;
private String type;
private String path;
@@ -231,7 +258,8 @@ public class PolicyEnforcerConfig {
public enum ScopeEnforcementMode {
ALL,
- ANY
+ ANY,
+ DISABLED
}
public static class UserManagedAccessConfig {
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
index 071bc32..92f4717 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
@@ -24,8 +24,10 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.keycloak.json.StringListMapDeserializer;
@@ -45,6 +47,7 @@ public class ResourceRepresentation {
private String uri;
private String type;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @JsonProperty("scopes")
private Set<ScopeRepresentation> scopes;
@JsonProperty("icon_uri")
@@ -52,9 +55,6 @@ public class ResourceRepresentation {
private ResourceOwnerRepresentation owner;
private Boolean ownerManagedAccess;
- @JsonInclude(JsonInclude.Include.NON_EMPTY)
- private List<PolicyRepresentation> policies;
-
private String displayName;
@JsonDeserialize(using = StringListMapDeserializer.class)
@@ -162,17 +162,31 @@ public class ResourceRepresentation {
}
public void setUri(String uri) {
- this.uri = uri;
+ if (uri != null && !"".equalsIgnoreCase(uri.trim())) {
+ this.uri = uri;
+ }
}
public void setType(String type) {
- this.type = type;
+ if (type != null && !"".equalsIgnoreCase(type.trim())) {
+ this.type = type;
+ }
}
public void setScopes(Set<ScopeRepresentation> scopes) {
this.scopes = scopes;
}
+ /**
+ * TODO: This is a workaround to allow deserialization of UMA resource representation. Jackson 2.19+ support aliases, once we upgrade, change this.
+ *
+ * @param scopes
+ */
+ @JsonSetter("resource_scopes")
+ private void setScopesUma(Set<ScopeRepresentation> scopes) {
+ this.scopes = scopes;
+ }
+
public void setIconUri(String iconUri) {
this.iconUri = iconUri;
}
@@ -181,10 +195,25 @@ public class ResourceRepresentation {
return this.owner;
}
+ @JsonProperty
public void setOwner(ResourceOwnerRepresentation owner) {
this.owner = owner;
}
+ @JsonIgnore
+ public void setOwner(String ownerId) {
+ if (ownerId == null) {
+ owner = null;
+ return;
+ }
+
+ if (owner == null) {
+ owner = new ResourceOwnerRepresentation();
+ }
+
+ owner.setId(ownerId);
+ }
+
public Boolean getOwnerManagedAccess() {
return ownerManagedAccess;
}
diff --git a/core/src/test/java/org/keycloak/jose/JWETest.java b/core/src/test/java/org/keycloak/jose/JWETest.java
index 31d8a8a..cc179bf 100644
--- a/core/src/test/java/org/keycloak/jose/JWETest.java
+++ b/core/src/test/java/org/keycloak/jose/JWETest.java
@@ -19,18 +19,18 @@ package org.keycloak.jose;
import java.io.UnsupportedEncodingException;
import java.security.Key;
+import java.security.spec.KeySpec;
import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.junit.Assert;
import org.junit.Test;
+import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Base64Url;
-import org.keycloak.jose.jwe.JWE;
-import org.keycloak.jose.jwe.JWEConstants;
-import org.keycloak.jose.jwe.JWEException;
-import org.keycloak.jose.jwe.JWEHeader;
-import org.keycloak.jose.jwe.JWEKeyStorage;
+import org.keycloak.jose.jwe.*;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -53,7 +53,6 @@ public class JWETest {
testDirectEncryptAndDecrypt(aesKey, hmacKey, JWEConstants.A128CBC_HS256, PAYLOAD, true);
}
-
// Works just on OpenJDK 8. Other JDKs (IBM, Oracle) have restrictions on maximum key size of AES to be 128
// @Test
public void testDirect_Aes256CbcHmacSha512() throws Exception {
@@ -119,9 +118,24 @@ public class JWETest {
}
@Test
+ public void testPassword() throws Exception {
+ byte[] salt = JWEUtils.generateSecret(8);
+ String encodedSalt = Base64.encodeBytes(salt);
+ String jwe = JWE.encryptUTF8("geheim", encodedSalt, PAYLOAD);
+ String decodedContent = JWE.decryptUTF8("geheim", encodedSalt, jwe);
+ Assert.assertEquals(PAYLOAD, decodedContent);
+ }
+
+
+
+ @Test
public void testAesKW_Aes128CbcHmacSha256() throws Exception {
SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES");
+ testAesKW_Aes128CbcHmacSha256(aesKey);
+ }
+
+ private void testAesKW_Aes128CbcHmacSha256(SecretKey aesKey) throws UnsupportedEncodingException, JWEException {
JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
JWE jwe = new JWE()
.header(jweHeader)
@@ -146,6 +160,15 @@ public class JWETest {
Assert.assertEquals(PAYLOAD, decodedContent);
}
+ @Test
+ public void testSalt() {
+ byte[] random = JWEUtils.generateSecret(8);
+ System.out.print("new byte[] = {");
+ for (byte b : random) {
+ System.out.print(""+Byte.toString(b)+",");
+ }
+ }
+
@Test
public void externalJweAes128CbcHmacSha256Test() throws UnsupportedEncodingException, JWEException {
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java
index 9b7e3b1..984c6ba 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java
@@ -41,6 +41,7 @@ import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
+import org.hibernate.annotations.Nationalized;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.Logic;
@@ -77,6 +78,7 @@ public class PolicyEntity {
@Column(name = "NAME")
private String name;
+ @Nationalized
@Column(name = "DESCRIPTION")
private String description;
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
index 0c82dc0..b9cecf7 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
@@ -181,6 +181,10 @@ public class JPAResourceStore implements ResourceStore {
predicates.add(root.join("scopes").get("id").in(value));
} else if ("ownerManagedAccess".equals(name)) {
predicates.add(builder.equal(root.get(name), Boolean.valueOf(value[0])));
+ } else if ("uri".equals(name)) {
+ predicates.add(builder.equal(builder.lower(root.get(name)), value[0].toLowerCase()));
+ } else if ("uri_not_null".equals(name)) {
+ predicates.add(builder.isNotNull(root.get("uri")));
} else {
predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%"));
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationFlowEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationFlowEntity.java
index 2413a46..a56c2ee 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationFlowEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/AuthenticationFlowEntity.java
@@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
+import org.hibernate.annotations.Nationalized;
+
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
@@ -53,6 +55,7 @@ public class AuthenticationFlowEntity {
@Column(name="PROVIDER_ID")
protected String providerId;
+ @Nationalized
@Column(name="DESCRIPTION")
protected String description;
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 7ede55c..ea8a580 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
@@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
+import org.hibernate.annotations.Nationalized;
+
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
@@ -61,8 +63,10 @@ public class ClientEntity {
@Column(name="ID", length = 36)
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
private String id;
+ @Nationalized
@Column(name = "NAME")
private String name;
+ @Nationalized
@Column(name = "DESCRIPTION")
private String description;
@Column(name = "CLIENT_ID")
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java
index ef57b11..4aa1c6c 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientTemplateEntity.java
@@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
+import org.hibernate.annotations.Nationalized;
+
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
@@ -51,6 +53,7 @@ public class ClientTemplateEntity {
private String id;
@Column(name = "NAME")
private String name;
+ @Nationalized
@Column(name = "DESCRIPTION")
private String description;
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "clientTemplate")
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ComponentConfigEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ComponentConfigEntity.java
index e2e9304..c5a22fd 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ComponentConfigEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ComponentConfigEntity.java
@@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
+import org.hibernate.annotations.Nationalized;
+
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
@@ -46,6 +48,7 @@ public class ComponentConfigEntity {
@Column(name = "NAME")
protected String name;
+ @Nationalized
@Column(name = "VALUE")
protected String value;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupAttributeEntity.java
index b65febd..d762661 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupAttributeEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupAttributeEntity.java
@@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
+import org.hibernate.annotations.Nationalized;
+
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
@@ -51,6 +53,7 @@ public class GroupAttributeEntity {
@Column(name = "NAME")
protected String name;
+ @Nationalized
@Column(name = "VALUE")
protected String value;
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 e01ad0c..997dda7 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
@@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
+import org.hibernate.annotations.Nationalized;
+
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
@@ -40,6 +42,7 @@ public class GroupEntity {
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
protected String id;
+ @Nationalized
@Column(name = "NAME")
protected String name;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributeEntity.java
index 0e02f23..6474bec 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributeEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributeEntity.java
@@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
+import org.hibernate.annotations.Nationalized;
+
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@@ -49,6 +51,7 @@ public class RealmAttributeEntity {
@Id
@Column(name = "NAME")
protected String name;
+ @Nationalized
@Column(name = "VALUE")
protected String value;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java
index 49264a5..a8d30aa 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java
@@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
+import org.hibernate.annotations.Nationalized;
+
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
@@ -61,8 +63,10 @@ public class RoleEntity {
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
private String id;
+ @Nationalized
@Column(name = "NAME")
private String name;
+ @Nationalized
@Column(name = "DESCRIPTION")
private String description;
@Column(name = "SCOPE_PARAM_REQUIRED")
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java
index bbb0f0f..fd211e4 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserAttributeEntity.java
@@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
+import org.hibernate.annotations.Nationalized;
+
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
@@ -54,6 +56,7 @@ public class UserAttributeEntity {
@Column(name = "NAME")
protected String name;
+ @Nationalized
@Column(name = "VALUE")
protected String value;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
index 82062cc..538d17a 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
@@ -19,6 +19,7 @@ package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
+import org.hibernate.annotations.Nationalized;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.Access;
@@ -69,12 +70,15 @@ public class UserEntity {
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
protected String id;
+ @Nationalized
@Column(name = "USERNAME")
protected String username;
+ @Nationalized
@Column(name = "FIRST_NAME")
protected String firstName;
@Column(name = "CREATED_TIMESTAMP")
protected Long createdTimestamp;
+ @Nationalized
@Column(name = "LAST_NAME")
protected String lastName;
@Column(name = "EMAIL")
pom.xml 11(+11 -0)
diff --git a/pom.xml b/pom.xml
index eab1938..ddc8d25 100755
--- a/pom.xml
+++ b/pom.xml
@@ -1414,6 +1414,17 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>kcinit</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>kcinit-dist</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
</dependencies>
</dependencyManagement>
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
index 2f17f77..1d60052 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java
@@ -43,5 +43,6 @@ public enum AuthenticationFlowError {
IDENTITY_PROVIDER_NOT_FOUND,
IDENTITY_PROVIDER_DISABLED,
- IDENTITY_PROVIDER_ERROR
+ IDENTITY_PROVIDER_ERROR,
+ DISPLAY_NOT_SUPPORTED
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java
index bf8fbcf..e15386a 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowException.java
@@ -17,6 +17,8 @@
package org.keycloak.authentication;
+import javax.ws.rs.core.Response;
+
/**
* Throw this exception from an Authenticator, FormAuthenticator, or FormAction if you want to completely abort the flow.
*
@@ -25,11 +27,17 @@ package org.keycloak.authentication;
*/
public class AuthenticationFlowException extends RuntimeException {
private AuthenticationFlowError error;
+ private Response response;
public AuthenticationFlowException(AuthenticationFlowError error) {
this.error = error;
}
+ public AuthenticationFlowException(AuthenticationFlowError error, Response response) {
+ this.error = error;
+ this.response = response;
+ }
+
public AuthenticationFlowException(String message, AuthenticationFlowError error) {
super(message);
this.error = error;
@@ -53,4 +61,8 @@ public class AuthenticationFlowException extends RuntimeException {
public AuthenticationFlowError getError() {
return error;
}
+
+ public Response getResponse() {
+ return response;
+ }
}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeAuthenticatorFactory.java b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeAuthenticatorFactory.java
new file mode 100644
index 0000000..0754383
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeAuthenticatorFactory.java
@@ -0,0 +1,21 @@
+package org.keycloak.authentication;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * Implement this interface when declaring your authenticator factory
+ * if your provider has support for multiple oidc display query parameter parameter types
+ * if the display query parameter is set and your factory implements this interface, this method
+ * will be called.
+ *
+ */
+public interface DisplayTypeAuthenticatorFactory {
+ /**
+ *
+ *
+ * @param session
+ * @param displayType i.e. "console", "wap", "popup" are examples
+ * @return null if display type isn't support.
+ */
+ Authenticator createDisplay(KeycloakSession session, String displayType);
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeRequiredActionFactory.java b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeRequiredActionFactory.java
new file mode 100644
index 0000000..e22e9ff
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/DisplayTypeRequiredActionFactory.java
@@ -0,0 +1,13 @@
+package org.keycloak.authentication;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * Implement this interface when declaring your required action factory
+ * has support for multiple oidc display query parameter parameter types
+ * if the display query parameter is set and your factory implements this interface, this method
+ * will be called.
+ */
+public interface DisplayTypeRequiredActionFactory {
+ RequiredActionProvider createDisplay(KeycloakSession session, String displayType);
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java
index caaa14e..1289842 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java
@@ -60,6 +60,15 @@ public interface RequiredActionContext {
URI getActionUrl();
/**
+ * Get the action URL for the required action. This auto-generates the access code.
+ *
+ * @param authSessionIdParam if true, will embed session id as query param. Useful for clients that don't support cookies (i.e. console)
+ *
+ * @return
+ */
+ URI getActionUrl(boolean authSessionIdParam);
+
+ /**
* Create a Freemarker form builder that presets the user, action URI, and a generated access code
*
* @return
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java b/server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java
new file mode 100644
index 0000000..b1dc9a2
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/TextChallenge.java
@@ -0,0 +1,303 @@
+package org.keycloak.authentication;
+
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.KeycloakSession;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+/**
+ * This class encapsulates a proprietary HTTP challenge protocol designed by keycloak team which is used by text-based console
+ * clients to dynamically render and prompt for information in a textual manner. The class is a builder which can
+ * build the challenge response (the header and response body).
+ *
+ * When doing code to token flow in OAuth, server could respond with
+ *
+ * 401
+ * WWW-Authenticate: X-Text-Form-Challenge callback="http://localhost/..."
+ * param="username" label="Username: " mask=false
+ * param="password" label="Password: " mask=true
+ * Content-Type: text/plain
+ *
+ * Please login with your username and password
+ *
+ *
+ * The client receives this challenge. It first outputs whatever the text body of the message contains. It will
+ * then prompt for username and password using the label values as prompt messages for each parameter.
+ *
+ * After the input has been entered by the user, the client does a form POST to the callback url with the values of the
+ * input parameters entered.
+ *
+ * The server can challenge with 401 as many times as it wants. The client will look for 302 responses. It will will
+ * follow all redirects unless the Location url has an OAuth "code" parameter. If there is a code parameter, then the
+ * client will stop and finish the OAuth flow to obtain a token. Any other response code other than 401 or 302 the client
+ * should abort with an error message.
+ *
+ */
+public class TextChallenge {
+
+ /**
+ * Browser is required to login. This will abort client from doing a console login.
+ *
+ * @param session
+ * @return
+ */
+ public static Response browserRequired(KeycloakSession session) {
+ return Response.status(Response.Status.UNAUTHORIZED)
+ .header("WWW-Authenticate", "X-Text-Form-Challenge browserRequired")
+ .type(MediaType.TEXT_PLAIN)
+ .entity("\n" + session.getProvider(LoginFormsProvider.class).getMessage("browserRequired") + "\n").build();
+ }
+
+
+ /**
+ * Build challenge response for required actions
+ *
+ * @param context
+ * @return
+ */
+ public static TextChallenge challenge(RequiredActionContext context) {
+ return new TextChallenge(context);
+
+ }
+
+ /**
+ * Build challenge response for authentication flows
+ *
+ * @param context
+ * @return
+ */
+ public static TextChallenge challenge(AuthenticationFlowContext context) {
+ return new TextChallenge(context);
+
+ }
+ /**
+ * Build challenge response header only for required actions
+ *
+ * @param context
+ * @return
+ */
+ public static HeaderBuilder header(RequiredActionContext context) {
+ return new TextChallenge(context).header();
+
+ }
+
+ /**
+ * Build challenge response header only for authentication flows
+ *
+ * @param context
+ * @return
+ */
+ public static HeaderBuilder header(AuthenticationFlowContext context) {
+ return new TextChallenge(context).header();
+
+ }
+ TextChallenge(RequiredActionContext requiredActionContext) {
+ this.requiredActionContext = requiredActionContext;
+ }
+
+ TextChallenge(AuthenticationFlowContext flowContext) {
+ this.flowContext = flowContext;
+ }
+
+
+ protected RequiredActionContext requiredActionContext;
+ protected AuthenticationFlowContext flowContext;
+ protected HeaderBuilder header;
+
+ /**
+ * Create a theme form pre-populated with challenge
+ *
+ * @return
+ */
+ public LoginFormsProvider form() {
+ if (header == null) throw new RuntimeException("Header Not Set");
+ return formInternal()
+ .setStatus(Response.Status.UNAUTHORIZED)
+ .setMediaType(MediaType.TEXT_PLAIN_TYPE)
+ .setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, header.build());
+ }
+
+ /**
+ * Create challenge response with a body generated from localized
+ * message.properties of your theme
+ *
+ * @param msg message id
+ * @param params parameters to use to format the message
+ *
+ * @return
+ */
+ public Response message(String msg, String... params) {
+ if (header == null) throw new RuntimeException("Header Not Set");
+ Response response = Response.status(401)
+ .header(HttpHeaders.WWW_AUTHENTICATE, header.build())
+ .type(MediaType.TEXT_PLAIN)
+ .entity("\n" + formInternal().getMessage(msg, params) + "\n").build();
+ return response;
+ }
+
+ /**
+ * Create challenge response with a text message body
+ *
+ * @param text plain text of http response body
+ *
+ * @return
+ */
+ public Response text(String text) {
+ if (header == null) throw new RuntimeException("Header Not Set");
+ Response response = Response.status(401)
+ .header(HttpHeaders.WWW_AUTHENTICATE, header.build())
+ .type(MediaType.TEXT_PLAIN)
+ .entity("\n" + text + "\n").build();
+ return response;
+
+ }
+
+
+ /**
+ * Generate response with empty http response body
+ *
+ * @return
+ */
+ public Response response() {
+ if (header == null) throw new RuntimeException("Header Not Set");
+ Response response = Response.status(401)
+ .header(HttpHeaders.WWW_AUTHENTICATE, header.build()).build();
+ return response;
+
+ }
+
+
+
+ protected LoginFormsProvider formInternal() {
+ if (requiredActionContext != null) {
+ return requiredActionContext.form();
+ } else {
+ return flowContext.form();
+
+ }
+ }
+
+ /**
+ * Start building the header
+ *
+ * @return
+ */
+ public HeaderBuilder header() {
+ String callback;
+ if (requiredActionContext != null) {
+ callback = requiredActionContext.getActionUrl(true).toString();
+ } else {
+ callback = flowContext.getActionUrl(flowContext.generateAccessCode(), true).toString();
+
+ }
+ header = new HeaderBuilder(callback);
+ return header;
+ }
+
+ public class HeaderBuilder {
+ protected StringBuilder builder = new StringBuilder();
+
+ protected HeaderBuilder(String callback) {
+ builder.append("X-Text-Form-Challenge callback=\"").append(callback).append("\" ");
+ }
+
+ protected ParamBuilder param;
+
+ protected void checkParam() {
+ if (param != null) {
+ param.buildInternal();
+ param = null;
+ }
+ }
+
+ /**
+ * Build header string
+ *
+ * @return
+ */
+ public String build() {
+ checkParam();
+ return builder.toString();
+ }
+
+ /**
+ * Define a param
+ *
+ * @param name
+ * @return
+ */
+ public ParamBuilder param(String name) {
+ checkParam();
+ builder.append("param=\"").append(name).append("\" ");
+ param = new ParamBuilder(name);
+ return param;
+ }
+
+ public class ParamBuilder {
+ protected boolean mask;
+ protected String label;
+
+ protected ParamBuilder(String name) {
+ this.label = name;
+ }
+
+ public ParamBuilder label(String msg) {
+ this.label = formInternal().getMessage(msg);
+ return this;
+ }
+
+ public ParamBuilder labelText(String txt) {
+ this.label = txt;
+ return this;
+ }
+
+ /**
+ * Should input be masked by the client. For example, when entering password, you don't want to show password on console.
+ *
+ * @param mask
+ * @return
+ */
+ public ParamBuilder mask(boolean mask) {
+ this.mask = mask;
+ return this;
+ }
+
+ public void buildInternal() {
+ builder.append("label=\"").append(label).append(" \" ");
+ builder.append("mask=").append(mask).append(" ");
+ }
+
+ /**
+ * Build header string
+ *
+ * @return
+ */
+ public String build() {
+ return HeaderBuilder.this.build();
+ }
+
+ public TextChallenge challenge() {
+ return TextChallenge.this;
+ }
+
+ public LoginFormsProvider form() {
+ return TextChallenge.this.form();
+ }
+
+ public Response message(String msg, String... params) {
+ return TextChallenge.this.message(msg, params);
+ }
+
+ public Response text(String text) {
+ return TextChallenge.this.text(text);
+
+ }
+
+ public ParamBuilder param(String name) {
+ return HeaderBuilder.this.param(name);
+ }
+ }
+ }
+}
diff --git a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
index a60ebc0..425ec52 100755
--- a/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/email/EmailTemplateProvider.java
@@ -23,6 +23,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
import org.keycloak.sessions.AuthenticationSessionModel;
+import java.util.List;
import java.util.Map;
/**
@@ -76,4 +77,24 @@ public interface EmailTemplateProvider extends Provider {
public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException;
+ /**
+ * Send formatted email
+ *
+ * @param subjectFormatKey message property that will be used to format email subject
+ * @param bodyTemplate freemarker template file
+ * @param bodyAttributes attributes used to fill template
+ * @throws EmailException
+ */
+ void send(String subjectFormatKey, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException;
+
+ /**
+ * Send formatted email
+ *
+ * @param subjectFormatKey message property that will be used to format email subject
+ * @param subjectAttributes attributes used to fill subject format message
+ * @param bodyTemplate freemarker template file
+ * @param bodyAttributes attributes used to fill template
+ * @throws EmailException
+ */
+ void send(String subjectFormatKey, List<Object> subjectAttributes, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/events/Errors.java b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
index 632c21c..95bcd81 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/Errors.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
@@ -90,5 +90,6 @@ public interface Errors {
String NOT_LOGGED_IN = "not_logged_in";
String UNKNOWN_IDENTITY_PROVIDER = "unknown_identity_provider";
String ILLEGAL_ORIGIN = "illegal_origin";
+ String DISPLAY_UNSUPPORTED = "display_unsupported";
}
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
index 256b87f..31f430d 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
@@ -54,6 +54,8 @@ public interface LoginFormsProvider extends Provider {
String getMessage(String message);
+ String getMessage(String message, String... parameters);
+
Response createLogin();
Response createPasswordReset();
diff --git a/server-spi-private/src/main/java/org/keycloak/models/AdminRoles.java b/server-spi-private/src/main/java/org/keycloak/models/AdminRoles.java
index c2304e8..6178dc8 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/AdminRoles.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/AdminRoles.java
@@ -65,5 +65,6 @@ public class AdminRoles {
ALL_ROLES.add(ADMIN);
ALL_ROLES.add(CREATE_REALM);
ALL_ROLES.add(CREATE_CLIENT);
+ ALL_ROLES.add(REALM_ADMIN);
}
}
diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
index 5cb1ec5..ae2f4c7 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
@@ -52,6 +52,7 @@ public interface Constants {
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
+ String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
String EXECUTION = "execution";
String CLIENT_ID = "client_id";
String TAB_ID = "tab_id";
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 538dcef..06545a2 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -2314,7 +2314,7 @@ public class RepresentationToModel {
String ownerId = owner.getId();
if (ownerId == null) {
- throw new RuntimeException("No owner specified for resource [" + resource.getName() + "].");
+ ownerId = resourceServer.getId();
}
if (!resourceServer.getId().equals(ownerId)) {
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 537581a..db96f11 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -653,27 +653,33 @@ public class AuthenticationProcessor {
public Response handleBrowserException(Exception failure) {
if (failure instanceof AuthenticationFlowException) {
AuthenticationFlowException e = (AuthenticationFlowException) failure;
+
if (e.getError() == AuthenticationFlowError.INVALID_USER) {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.USER_NOT_FOUND);
+ if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
} else if (e.getError() == AuthenticationFlowError.USER_DISABLED) {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.USER_DISABLED);
+ if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session,authenticationSession, Response.Status.BAD_REQUEST, Messages.ACCOUNT_DISABLED);
} else if (e.getError() == AuthenticationFlowError.USER_TEMPORARILY_DISABLED) {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.USER_TEMPORARILY_DISABLED);
+ if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session,authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
} else if (e.getError() == AuthenticationFlowError.INVALID_CLIENT_SESSION) {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.INVALID_CODE);
+ if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_CODE);
} else if (e.getError() == AuthenticationFlowError.EXPIRED_CODE) {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.EXPIRED_CODE);
+ if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.EXPIRED_CODE);
} else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
@@ -701,9 +707,15 @@ public class AuthenticationProcessor {
CacheControlUtil.noBackButtonCacheControlHeader();
return processor.authenticate();
+ } else if (e.getError() == AuthenticationFlowError.DISPLAY_NOT_SUPPORTED) {
+ ServicesLogger.LOGGER.failedAuthentication(e);
+ event.error(Errors.DISPLAY_UNSUPPORTED);
+ if (e.getResponse() != null) return e.getResponse();
+ return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED);
} else {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.INVALID_USER_CREDENTIALS);
+ if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/AttemptedAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/AttemptedAuthenticator.java
new file mode 100644
index 0000000..fc866fd
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/AttemptedAuthenticator.java
@@ -0,0 +1,46 @@
+package org.keycloak.authentication.authenticators;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+
+/**
+ * Pass-thru atheneticator that just sets the context to attempted.
+ */
+public class AttemptedAuthenticator implements Authenticator {
+
+ public static final AttemptedAuthenticator SINGLETON = new AttemptedAuthenticator();
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ context.attempted();
+
+ }
+
+ @Override
+ public void action(AuthenticationFlowContext context) {
+ throw new RuntimeException("Unreachable!");
+
+ }
+
+ @Override
+ public boolean requiresUser() {
+ return false;
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return true;
+ }
+
+ @Override
+ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticatorFactory.java
index 7e40298..b87dbe9 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticatorFactory.java
@@ -18,8 +18,11 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
+import org.keycloak.authentication.authenticators.AttemptedAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -31,7 +34,7 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class CookieAuthenticatorFactory implements AuthenticatorFactory {
+public class CookieAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
public static final String PROVIDER_ID = "auth-cookie";
static CookieAuthenticator SINGLETON = new CookieAuthenticator();
@@ -41,6 +44,13 @@ public class CookieAuthenticatorFactory implements AuthenticatorFactory {
}
@Override
+ public Authenticator createDisplay(KeycloakSession session, String displayType) {
+ if (displayType == null) return SINGLETON;
+ if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+ return AttemptedAuthenticator.SINGLETON; // ignore this authenticator
+ }
+
+ @Override
public void init(Config.Scope config) {
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java
index 5218347..170f9d7 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java
@@ -18,6 +18,7 @@
package org.keycloak.authentication.authenticators.browser;
import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.constants.AdapterConstants;
@@ -25,10 +26,13 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.ClientSessionCode;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
import java.util.List;
/**
@@ -66,8 +70,11 @@ public class IdentityProviderAuthenticator implements Authenticator {
String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();
String clientId = context.getAuthenticationSession().getClient().getClientId();
String tabId = context.getAuthenticationSession().getTabId();
- Response response = Response.seeOther(
- Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId))
+ URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId);
+ if (context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY) != null) {
+ location = UriBuilder.fromUri(location).queryParam(OAuth2Constants.DISPLAY, context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY)).build();
+ }
+ Response response = Response.seeOther(location)
.build();
LOG.debugf("Redirecting to %s", providerId);
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticatorFactory.java
index 635c95e..b136d33 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticatorFactory.java
@@ -18,8 +18,11 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
+import org.keycloak.authentication.authenticators.AttemptedAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -33,7 +36,7 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactory {
+public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
protected static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED
@@ -83,6 +86,13 @@ public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactor
}
@Override
+ public Authenticator createDisplay(KeycloakSession session, String displayType) {
+ if (displayType == null) return new IdentityProviderAuthenticator();
+ if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+ return AttemptedAuthenticator.SINGLETON; // ignore this authenticator
+ }
+
+ @Override
public void init(Config.Scope config) {
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticatorFactory.java
index f443d28..d71659c 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticatorFactory.java
@@ -18,8 +18,11 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
+import org.keycloak.authentication.authenticators.console.ConsoleOTPFormAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -32,7 +35,7 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class OTPFormAuthenticatorFactory implements AuthenticatorFactory {
+public class OTPFormAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
public static final String PROVIDER_ID = "auth-otp-form";
public static final OTPFormAuthenticator SINGLETON = new OTPFormAuthenticator();
@@ -43,6 +46,13 @@ public class OTPFormAuthenticatorFactory implements AuthenticatorFactory {
}
@Override
+ public Authenticator createDisplay(KeycloakSession session, String displayType) {
+ if (displayType == null) return SINGLETON;
+ if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+ return ConsoleOTPFormAuthenticator.SINGLETON;
+ }
+
+ @Override
public void init(Config.Scope config) {
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticatorFactory.java
index 9d837c6..ae5dd0c 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticatorFactory.java
@@ -18,8 +18,10 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -32,7 +34,7 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class SpnegoAuthenticatorFactory implements AuthenticatorFactory {
+public class SpnegoAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
public static final String PROVIDER_ID = "auth-spnego";
public static final SpnegoAuthenticator SINGLETON = new SpnegoAuthenticator();
@@ -43,6 +45,13 @@ public class SpnegoAuthenticatorFactory implements AuthenticatorFactory {
}
@Override
+ public Authenticator createDisplay(KeycloakSession session, String displayType) {
+ if (displayType == null) return SINGLETON;
+ if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+ return SINGLETON;
+ }
+
+ @Override
public void init(Config.Scope config) {
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
index bd81263..43383a0 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
@@ -19,7 +19,6 @@ package org.keycloak.authentication.authenticators.browser;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.authentication.AuthenticationFlowContext;
-import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.Authenticator;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordFormFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordFormFactory.java
index ef0c9b1..fe42f48 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordFormFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordFormFactory.java
@@ -18,8 +18,11 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
+import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -32,7 +35,7 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class UsernamePasswordFormFactory implements AuthenticatorFactory {
+public class UsernamePasswordFormFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
public static final String PROVIDER_ID = "auth-username-password-form";
public static final UsernamePasswordForm SINGLETON = new UsernamePasswordForm();
@@ -43,6 +46,13 @@ public class UsernamePasswordFormFactory implements AuthenticatorFactory {
}
@Override
+ public Authenticator createDisplay(KeycloakSession session, String displayType) {
+ if (displayType == null) return SINGLETON;
+ if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+ return ConsoleUsernamePasswordAuthenticator.SINGLETON;
+ }
+
+ @Override
public void init(Config.Scope config) {
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java
new file mode 100755
index 0000000..fff2c80
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleOTPFormAuthenticator.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.authenticators.console;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.TextChallenge;
+import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;
+import org.keycloak.representations.idm.CredentialRepresentation;
+
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConsoleOTPFormAuthenticator extends OTPFormAuthenticator implements Authenticator {
+ public static final ConsoleOTPFormAuthenticator SINGLETON = new ConsoleOTPFormAuthenticator();
+
+ public static URI getCallbackUrl(AuthenticationFlowContext context) {
+ return context.getActionUrl(context.generateAccessCode(), true);
+ }
+
+ protected TextChallenge challenge(AuthenticationFlowContext context) {
+ return TextChallenge.challenge(context)
+ .header()
+ .param(CredentialRepresentation.TOTP)
+ .label("console-otp")
+ .challenge();
+ }
+
+ @Override
+ public void action(AuthenticationFlowContext context) {
+ validateOTP(context);
+ }
+
+
+
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ Response challengeResponse = challenge(context, null);
+ context.challenge(challengeResponse);
+ }
+
+ @Override
+ protected Response challenge(AuthenticationFlowContext context, String msg) {
+ if (msg == null) {
+ return challenge(context).response();
+ }
+ return challenge(context).message(msg);
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java
new file mode 100755
index 0000000..4595df5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticator.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.authenticators.console;
+
+import org.keycloak.authentication.*;
+import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.messages.Messages;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConsoleUsernamePasswordAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
+
+ public static final ConsoleUsernamePasswordAuthenticator SINGLETON = new ConsoleUsernamePasswordAuthenticator();
+
+ @Override
+ public boolean requiresUser() {
+ return false;
+ }
+
+ protected TextChallenge challenge(AuthenticationFlowContext context) {
+ return TextChallenge.challenge(context)
+ .header()
+ .param("username")
+ .label("console-username")
+ .param("password")
+ .label("console-password")
+ .mask(true)
+ .challenge();
+ }
+
+
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ Response response = challenge(context).form().createForm("cli_splash.ftl");
+ context.challenge(response);
+
+
+ }
+
+ @Override
+ protected Response invalidUser(AuthenticationFlowContext context) {
+ Response response = challenge(context).message(Messages.INVALID_USER);
+ return response;
+ }
+
+ @Override
+ protected Response disabledUser(AuthenticationFlowContext context) {
+ Response response = challenge(context).message(Messages.ACCOUNT_DISABLED);
+ return response;
+ }
+
+ @Override
+ protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
+ Response response = challenge(context).message(Messages.INVALID_USER);
+ return response;
+ }
+
+ @Override
+ protected Response invalidCredentials(AuthenticationFlowContext context) {
+ Response response = challenge(context).message(Messages.INVALID_USER);
+ return response;
+ }
+
+ @Override
+ protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
+ context.getEvent().error(eventError);
+ Response response = challenge(context).message(loginFormError);
+
+ context.failureChallenge(authenticatorError, response);
+ return response;
+ }
+
+ @Override
+ public void action(AuthenticationFlowContext context) {
+ MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+ if (!validateUserAndPassword(context, formData)) {
+ return;
+ }
+
+ context.success();
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return true;
+ }
+
+ @Override
+ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticatorFactory.java
new file mode 100755
index 0000000..05aa235
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/console/ConsoleUsernamePasswordAuthenticatorFactory.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.authenticators.console;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConsoleUsernamePasswordAuthenticatorFactory implements AuthenticatorFactory {
+
+ public static final String PROVIDER_ID = "console-username-password";
+
+ @Override
+ public Authenticator create(KeycloakSession session) {
+ return ConsoleUsernamePasswordAuthenticator.SINGLETON;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return UserCredentialModel.PASSWORD;
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return false;
+ }
+ public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+ AuthenticationExecutionModel.Requirement.REQUIRED
+ };
+
+ @Override
+ public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Username Password Challenge";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Proprietary challenge protocol for CLI clients that queries for username password";
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return null;
+ }
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
index 89471e7..ac0c5e1 100755
--- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
@@ -18,6 +18,7 @@
package org.keycloak.authentication;
import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.UserModel;
@@ -58,6 +59,24 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|| status == AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED;
}
+ protected Authenticator createAuthenticator(AuthenticatorFactory factory) {
+ String display = processor.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY);
+ if (display == null) return factory.create(processor.getSession());
+
+
+ if (factory instanceof DisplayTypeAuthenticatorFactory) {
+ Authenticator authenticator = ((DisplayTypeAuthenticatorFactory)factory).createDisplay(processor.getSession(), display);
+ if (authenticator != null) return authenticator;
+ }
+ // todo create a provider for handling lack of display support
+ if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
+ throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, TextChallenge.browserRequired(processor.getSession()));
+
+ } else {
+ return factory.create(processor.getSession());
+ }
+ }
+
@Override
public Response processAction(String actionExecution) {
@@ -86,7 +105,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
if (factory == null) {
throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
}
- Authenticator authenticator = factory.create(processor.getSession());
+ Authenticator authenticator = createAuthenticator(factory);
AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
logger.debugv("action: {0}", model.getAuthenticator());
authenticator.action(result);
@@ -161,7 +180,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
if (factory == null) {
throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
}
- Authenticator authenticator = factory.create(processor.getSession());
+ Authenticator authenticator = createAuthenticator(factory);
logger.debugv("authenticator: {0}", factory.getId());
UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser();
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
index 5e9a546..38b9c2f 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
@@ -33,6 +33,7 @@ import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
@@ -163,6 +164,16 @@ public class RequiredActionContextResult implements RequiredActionContext {
}
@Override
+ public URI getActionUrl(boolean authSessionIdParam) {
+ URI uri = getActionUrl();
+ if (authSessionIdParam) {
+ uri = UriBuilder.fromUri(uri).queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()).build();
+ }
+ return uri;
+
+ }
+
+ @Override
public LoginFormsProvider form() {
String accessCode = generateCode();
URI action = getActionUrl(accessCode);
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java
new file mode 100755
index 0000000..24c6938
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleTermsAndConditions.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.requiredactions;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.authentication.TextChallenge;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+import javax.ws.rs.core.Response;
+import java.util.Arrays;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConsoleTermsAndConditions implements RequiredActionProvider {
+ public static final ConsoleTermsAndConditions SINGLETON = new ConsoleTermsAndConditions();
+ public static final String USER_ATTRIBUTE = TermsAndConditions.PROVIDER_ID;
+
+ @Override
+ public void evaluateTriggers(RequiredActionContext context) {
+
+ }
+
+
+ @Override
+ public void requiredActionChallenge(RequiredActionContext context) {
+ Response challenge = TextChallenge.challenge(context)
+ .header()
+ .param("accept")
+ .label("console-accept-terms")
+ .message("termsPlainText");
+ context.challenge(challenge);
+ }
+
+ @Override
+ public void processAction(RequiredActionContext context) {
+ String accept = context.getHttpRequest().getDecodedFormParameters().getFirst("accept");
+
+ String yes = context.form().getMessage("console-accept");
+
+ if (!accept.equals(yes)) {
+ context.getUser().removeAttribute(USER_ATTRIBUTE);
+ requiredActionChallenge(context);
+ return;
+ }
+
+ context.getUser().setAttribute(USER_ATTRIBUTE, Arrays.asList(Integer.toString(Time.currentTime())));
+
+ context.success();
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java
new file mode 100755
index 0000000..d499ead
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdatePassword.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.requiredactions;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.authentication.*;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.*;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.validation.Validation;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConsoleUpdatePassword extends UpdatePassword implements RequiredActionProvider {
+ public static final ConsoleUpdatePassword SINGLETON = new ConsoleUpdatePassword();
+
+ private static final Logger logger = Logger.getLogger(ConsoleUpdatePassword.class);
+ public static final String PASSWORD_NEW = "password-new";
+ public static final String PASSWORD_CONFIRM = "password-confirm";
+
+ protected TextChallenge challenge(RequiredActionContext context) {
+ return TextChallenge.challenge(context)
+ .header()
+ .param(PASSWORD_NEW)
+ .label("console-new-password")
+ .mask(true)
+ .param(PASSWORD_CONFIRM)
+ .label("console-confirm-password")
+ .mask(true)
+ .challenge();
+ }
+
+
+
+ @Override
+ public void requiredActionChallenge(RequiredActionContext context) {
+ context.challenge(
+ challenge(context).message("console-update-password"));
+ }
+
+ @Override
+ public void processAction(RequiredActionContext context) {
+ EventBuilder event = context.getEvent();
+ MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+ event.event(EventType.UPDATE_PASSWORD);
+ String passwordNew = formData.getFirst(PASSWORD_NEW);
+ String passwordConfirm = formData.getFirst(PASSWORD_CONFIRM);
+
+ EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PASSWORD_ERROR)
+ .client(context.getAuthenticationSession().getClient())
+ .user(context.getAuthenticationSession().getAuthenticatedUser());
+
+ if (Validation.isBlank(passwordNew)) {
+ context.challenge(challenge(context).message(Messages.MISSING_PASSWORD));
+ errorEvent.error(Errors.PASSWORD_MISSING);
+ return;
+ } else if (!passwordNew.equals(passwordConfirm)) {
+ context.challenge(challenge(context).message(Messages.NOTMATCH_PASSWORD));
+ errorEvent.error(Errors.PASSWORD_CONFIRM_ERROR);
+ return;
+ }
+
+ try {
+ context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), UserCredentialModel.password(passwordNew, false));
+ context.success();
+ } catch (ModelException me) {
+ errorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED);
+ context.challenge(challenge(context).text(me.getMessage()));
+ return;
+ } catch (Exception ape) {
+ errorEvent.detail(Details.REASON, ape.getMessage()).error(Errors.PASSWORD_REJECTED);
+ context.challenge(challenge(context).text(ape.getMessage()));
+ return;
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateProfile.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateProfile.java
new file mode 100644
index 0000000..0b66bef
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateProfile.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.requiredactions;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+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.FormMessage;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.resources.AttributeFormDataProcessor;
+import org.keycloak.services.validation.Validation;
+
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConsoleUpdateProfile implements RequiredActionProvider {
+ public static final ConsoleUpdateProfile SINGLETON = new ConsoleUpdateProfile();
+
+ @Override
+ public void evaluateTriggers(RequiredActionContext context) {
+ }
+
+ @Override
+ public void requiredActionChallenge(RequiredActionContext context) {
+ // do nothing right now. I think this behavior is ok. We just defer this action until a browser login happens.
+ context.ignore();
+ }
+
+ @Override
+ public void processAction(RequiredActionContext context) {
+ throw new RuntimeException("Should be unreachable");
+
+ }
+
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java
new file mode 100644
index 0000000..89ef89b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleUpdateTotp.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.requiredactions;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.authentication.TextChallenge;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.forms.login.freemarker.model.TotpBean;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.CredentialValidation;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.validation.Validation;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConsoleUpdateTotp implements RequiredActionProvider {
+ public static final ConsoleUpdateTotp SINGLETON = new ConsoleUpdateTotp();
+
+ @Override
+ public void evaluateTriggers(RequiredActionContext context) {
+ }
+ @Override
+ public void requiredActionChallenge(RequiredActionContext context) {
+ TotpBean totpBean = new TotpBean(context.getSession(), context.getRealm(), context.getUser(), context.getUriInfo().getRequestUriBuilder());
+ String totpSecret = totpBean.getTotpSecret();
+ context.getAuthenticationSession().setAuthNote("totpSecret", totpSecret);
+ Response challenge = challenge(context).form()
+ .setAttribute("totp", totpBean)
+ .createForm("login-config-totp-text.ftl");
+ context.challenge(challenge);
+ }
+
+ protected TextChallenge challenge(RequiredActionContext context) {
+ return TextChallenge.challenge(context)
+ .header()
+ .param("totp")
+ .label("console-otp")
+ .challenge();
+ }
+
+ @Override
+ public void processAction(RequiredActionContext context) {
+ EventBuilder event = context.getEvent();
+ event.event(EventType.UPDATE_TOTP);
+ MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+ String totp = formData.getFirst("totp");
+ String totpSecret = context.getAuthenticationSession().getAuthNote("totpSecret");
+
+ if (Validation.isBlank(totp)) {
+ context.challenge(
+ challenge(context).message(Messages.MISSING_TOTP)
+ );
+ return;
+ } else if (!CredentialValidation.validOTP(context.getRealm(), totp, totpSecret)) {
+ context.challenge(
+ challenge(context).message(Messages.INVALID_TOTP)
+ );
+ return;
+ }
+
+ UserCredentialModel credentials = new UserCredentialModel();
+ credentials.setType(context.getRealm().getOTPPolicy().getType());
+ credentials.setValue(totpSecret);
+ context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), credentials);
+
+
+ // if type is HOTP, to update counter we execute validation based on supplied token
+ UserCredentialModel cred = new UserCredentialModel();
+ cred.setType(context.getRealm().getOTPPolicy().getType());
+ cred.setValue(totp);
+ context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), cred);
+
+ context.getAuthenticationSession().removeAuthNote("totpSecret");
+ context.success();
+ }
+
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java
new file mode 100755
index 0000000..e136ceb
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/ConsoleVerifyEmail.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.requiredactions;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.authentication.TextChallenge;
+import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
+import org.keycloak.common.util.RandomString;
+import org.keycloak.common.util.Time;
+import org.keycloak.email.EmailException;
+import org.keycloak.email.EmailTemplateProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.*;
+import org.keycloak.services.Urls;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.validation.Validation;
+import org.keycloak.sessions.AuthenticationSessionCompoundId;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+import javax.ws.rs.core.*;
+import java.net.URI;
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConsoleVerifyEmail implements RequiredActionProvider {
+ public static final ConsoleVerifyEmail SINGLETON = new ConsoleVerifyEmail();
+ private static final Logger logger = Logger.getLogger(ConsoleVerifyEmail.class);
+ @Override
+ public void evaluateTriggers(RequiredActionContext context) {
+ if (context.getRealm().isVerifyEmail() && !context.getUser().isEmailVerified()) {
+ context.getUser().addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
+ logger.debug("User is required to verify email");
+ }
+ }
+
+ @Override
+ public void requiredActionChallenge(RequiredActionContext context) {
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+
+ if (context.getUser().isEmailVerified()) {
+ context.success();
+ authSession.removeAuthNote(Constants.VERIFY_EMAIL_KEY);
+ return;
+ }
+
+ String email = context.getUser().getEmail();
+ if (Validation.isBlank(email)) {
+ context.ignore();
+ return;
+ }
+
+ Response challenge = sendVerifyEmail(context);
+ context.challenge(challenge);
+ }
+
+
+ @Override
+ public void processAction(RequiredActionContext context) {
+ EventBuilder event = context.getEvent().clone().event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail());
+ String code = context.getAuthenticationSession().getAuthNote(Constants.VERIFY_EMAIL_CODE);
+ if (code == null) {
+ requiredActionChallenge(context);
+ return;
+ }
+
+ MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+ String emailCode = formData.getFirst(EMAIL_CODE);
+
+ if (!code.equals(emailCode)) {
+ context.challenge(
+ challenge(context).message(Messages.INVALID_CODE)
+ );
+ event.error(Errors.INVALID_CODE);
+ return;
+ }
+ event.success();
+ context.success();
+ }
+
+
+ @Override
+ public void close() {
+
+ }
+
+ public static String EMAIL_CODE="email_code";
+ protected TextChallenge challenge(RequiredActionContext context) {
+ return TextChallenge.challenge(context)
+ .header()
+ .param(EMAIL_CODE)
+ .label("console-email-code")
+ .challenge();
+ }
+
+ private Response sendVerifyEmail(RequiredActionContext context) throws UriBuilderException, IllegalArgumentException {
+ KeycloakSession session = context.getSession();
+ UserModel user = context.getUser();
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+ EventBuilder event = context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail());
+ String code = RandomString.randomCode(8);
+ authSession.setAuthNote(Constants.VERIFY_EMAIL_CODE, code);
+ RealmModel realm = session.getContext().getRealm();
+
+ Map<String, Object> attributes = new HashMap<>();
+ attributes.put("code", code);
+
+ try {
+ session
+ .getProvider(EmailTemplateProvider.class)
+ .setAuthenticationSession(authSession)
+ .setRealm(realm)
+ .setUser(user)
+ .send("emailVerificationSubject", "email-verification-with-code.ftl", attributes);
+ event.success();
+ } catch (EmailException e) {
+ logger.error("Failed to send verification email", e);
+ event.error(Errors.EMAIL_SEND_FAILED);
+ }
+
+ return challenge(context).text(context.form().getMessage("console-verify-email", user.getEmail()));
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
index f4a1566..2f68782 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
@@ -18,9 +18,8 @@
package org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.*;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -32,7 +31,7 @@ import java.util.Arrays;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
+public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
public static final String PROVIDER_ID = "terms_and_conditions";
public static final String USER_ATTRIBUTE = PROVIDER_ID;
@@ -42,6 +41,15 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
}
@Override
+ public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
+ if (displayType == null) return this;
+ if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+ return ConsoleTermsAndConditions.SINGLETON;
+ }
+
+
+
+ @Override
public void init(Config.Scope config) {
}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
index 1e9f37a..45fb05f 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
@@ -19,9 +19,8 @@ package org.keycloak.authentication.requiredactions;
import org.jboss.logging.Logger;
import org.keycloak.Config;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.*;
import org.keycloak.common.util.Time;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.CredentialProvider;
@@ -47,7 +46,7 @@ import java.util.concurrent.TimeUnit;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class UpdatePassword implements RequiredActionProvider, RequiredActionFactory {
+public class UpdatePassword implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
private static final Logger logger = Logger.getLogger(UpdatePassword.class);
@Override
public void evaluateTriggers(RequiredActionContext context) {
@@ -142,6 +141,15 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
return this;
}
+
+ @Override
+ public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
+ if (displayType == null) return this;
+ if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+ return ConsoleUpdatePassword.SINGLETON;
+ }
+
+
@Override
public void init(Config.Scope config) {
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
index 3ed1f12..ccaf729 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
@@ -18,9 +18,8 @@
package org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.*;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
@@ -41,7 +40,7 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class UpdateProfile implements RequiredActionProvider, RequiredActionFactory {
+public class UpdateProfile implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
@Override
public void evaluateTriggers(RequiredActionContext context) {
}
@@ -142,6 +141,16 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
return this;
}
+
+ @Override
+ public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
+ if (displayType == null) return this;
+ if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+ return ConsoleUpdateProfile.SINGLETON;
+ }
+
+
+
@Override
public void init(Config.Scope config) {
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
index e85ec7e..188ba2a 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
@@ -18,9 +18,8 @@
package org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.*;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession;
@@ -38,7 +37,7 @@ import javax.ws.rs.core.Response;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory {
+public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
@Override
public void evaluateTriggers(RequiredActionContext context) {
}
@@ -105,6 +104,15 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
return this;
}
+
+ @Override
+ public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
+ if (displayType == null) return this;
+ if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+ return ConsoleUpdateTotp.SINGLETON;
+ }
+
+
@Override
public void init(Config.Scope config) {
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
index 969f350..c29c616 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
@@ -19,9 +19,8 @@ package org.keycloak.authentication.requiredactions;
import org.jboss.logging.Logger;
import org.keycloak.Config;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.*;
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
import org.keycloak.common.util.Time;
import org.keycloak.email.EmailException;
@@ -45,7 +44,7 @@ import javax.ws.rs.core.*;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class VerifyEmail implements RequiredActionProvider, RequiredActionFactory {
+public class VerifyEmail implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
private static final Logger logger = Logger.getLogger(VerifyEmail.class);
@Override
public void evaluateTriggers(RequiredActionContext context) {
@@ -107,6 +106,14 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
return this;
}
+
+ @Override
+ public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
+ if (displayType == null) return this;
+ if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
+ return ConsoleVerifyEmail.SINGLETON;
+ }
+
@Override
public void init(Config.Scope config) {
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
index b69d991..edde9cc 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -21,6 +21,8 @@ import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
import static org.keycloak.models.utils.RepresentationToModel.toModel;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -46,6 +48,7 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.OAuthErrorException;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
@@ -54,6 +57,7 @@ import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.common.util.PathMatcher;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.ClientModel;
@@ -64,7 +68,7 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
-import org.keycloak.services.ErrorResponse;
+import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
@@ -90,32 +94,18 @@ public class ResourceSetService {
@Consumes("application/json")
@Produces("application/json")
public Response create(@Context UriInfo uriInfo, ResourceRepresentation resource) {
- return create(uriInfo, resource, (Function<Resource, ResourceRepresentation>) resource1 -> {
- ResourceRepresentation representation = new ResourceRepresentation();
-
- representation.setId(resource1.getId());
+ if (resource == null) {
+ return Response.status(Status.BAD_REQUEST).build();
+ }
- return representation;
- });
- }
+ ResourceRepresentation newResource = create(resource);
- public Response create(@Context UriInfo uriInfo, ResourceRepresentation resource, Function<Resource, ?> toRepresentation) {
- Response response = create(resource, toRepresentation);
audit(uriInfo, resource, resource.getId(), OperationType.CREATE);
- return response;
- }
-
- public Response create(ResourceRepresentation resource) {
- return create(resource, (Function<Resource, ResourceRepresentation>) resource1 -> {
- ResourceRepresentation representation = new ResourceRepresentation();
-
- representation.setId(resource1.getId());
- return representation;
- });
+ return Response.status(Status.CREATED).entity(newResource).build();
}
- public Response create(ResourceRepresentation resource, Function<Resource, ?> toRepresentation) {
+ public ResourceRepresentation create(ResourceRepresentation resource) {
requireManage();
StoreFactory storeFactory = this.authorization.getStoreFactory();
ResourceOwnerRepresentation owner = resource.getOwner();
@@ -123,21 +113,22 @@ public class ResourceSetService {
if (owner == null) {
owner = new ResourceOwnerRepresentation();
owner.setId(resourceServer.getId());
+ resource.setOwner(owner);
}
String ownerId = owner.getId();
if (ownerId == null) {
- return ErrorResponse.error("You must specify the resource owner.", Status.BAD_REQUEST);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "You must specify the resource owner.", Status.BAD_REQUEST);
}
Resource existingResource = storeFactory.getResourceStore().findByName(resource.getName(), ownerId, this.resourceServer.getId());
if (existingResource != null) {
- return ErrorResponse.exists("Resource with name [" + resource.getName() + "] already exists.");
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Resource with name [" + resource.getName() + "] already exists.", Status.CONFLICT);
}
- return Response.status(Status.CREATED).entity(toRepresentation.apply(toModel(resource, this.resourceServer, authorization))).build();
+ return toRepresentation(toModel(resource, this.resourceServer, authorization), resourceServer, authorization);
}
@Path("{id}")
@@ -198,10 +189,10 @@ public class ResourceSetService {
@NoCache
@Produces("application/json")
public Response findById(@PathParam("id") String id) {
- return findById(id, (Function<Resource, ResourceRepresentation>) resource -> toRepresentation(resource, resourceServer, authorization, true));
+ return findById(id, resource -> toRepresentation(resource, resourceServer, authorization, true));
}
- public Response findById(@PathParam("id") String id, Function<Resource, ?> toRepresentation) {
+ public Response findById(String id, Function<Resource, ? extends ResourceRepresentation> toRepresentation) {
requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
Resource model = storeFactory.getResourceStore().findById(id, resourceServer.getId());
@@ -340,10 +331,11 @@ public class ResourceSetService {
@QueryParam("owner") String owner,
@QueryParam("type") String type,
@QueryParam("scope") String scope,
+ @QueryParam("matchingUri") Boolean matchingUri,
@QueryParam("deep") Boolean deep,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
- return find(id, name, uri, owner, type, scope, deep, firstResult, maxResult, (BiFunction<Resource, Boolean, ResourceRepresentation>) (resource, deep1) -> toRepresentation(resource, resourceServer, authorization, deep1));
+ return find(id, name, uri, owner, type, scope, matchingUri, deep, firstResult, maxResult, (BiFunction<Resource, Boolean, ResourceRepresentation>) (resource, deep1) -> toRepresentation(resource, resourceServer, authorization, deep1));
}
public Response find(@QueryParam("_id") String id,
@@ -352,6 +344,7 @@ public class ResourceSetService {
@QueryParam("owner") String owner,
@QueryParam("type") String type,
@QueryParam("scope") String scope,
+ @QueryParam("matchingUri") Boolean matchingUri,
@QueryParam("deep") Boolean deep,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult,
@@ -413,9 +406,38 @@ public class ResourceSetService {
search.put("scope", scopes.stream().map(Scope::getId).toArray(String[]::new));
}
+ List<Resource> resources = storeFactory.getResourceStore().findByResourceServer(search, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS);
+
+ if (matchingUri != null && matchingUri && resources.isEmpty()) {
+ HashMap<String, String[]> attributes = new HashMap<>();
+
+ attributes.put("uri_not_null", new String[] {"true"});
+ attributes.put("owner", new String[] {resourceServer.getId()});
+
+ List<Resource> serverResources = storeFactory.getResourceStore().findByResourceServer(attributes, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS);
+ PathMatcher<Resource> pathMatcher = new PathMatcher<Resource>() {
+ @Override
+ protected String getPath(Resource entry) {
+ return entry.getUri();
+ }
+
+ @Override
+ protected Collection<Resource> getPaths() {
+ return serverResources;
+ }
+ };
+
+ Resource matches = pathMatcher.matches(uri);
+
+ if (matches != null) {
+ resources = Arrays.asList(matches);
+ }
+ }
+
Boolean finalDeep = deep;
+
return Response.ok(
- storeFactory.getResourceStore().findByResourceServer(search, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS).stream()
+ resources.stream()
.map(resource -> toRepresentation.apply(resource, finalDeep))
.collect(Collectors.toList()))
.build();
@@ -437,7 +459,7 @@ public class ResourceSetService {
audit(uriInfo, resource, null, operation);
}
- private void audit(@Context UriInfo uriInfo, ResourceRepresentation resource, String id, OperationType operation) {
+ public void audit(@Context UriInfo uriInfo, ResourceRepresentation resource, String id, OperationType operation) {
if (authorization.getRealm().isAdminEventsEnabled()) {
if (id != null) {
adminEvent.operation(operation).resourcePath(uriInfo, id).representation(resource).success();
diff --git a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
index 8a811f1..80226cf 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
@@ -65,7 +65,7 @@ public class ProtectionService {
ResteasyProviderFactory.getInstance().injectProperties(resourceManager);
- ResourceService resource = new ResourceService(resourceServer, identity, resourceManager, this.authorization);
+ ResourceService resource = new ResourceService(resourceServer, identity, resourceManager);
ResteasyProviderFactory.getInstance().injectProperties(resource);
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
index 21fd27c..1fbe5f9 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
@@ -18,8 +18,6 @@
package org.keycloak.authorization.protection.resource;
import java.util.function.BiFunction;
-import java.util.function.Function;
-import java.util.stream.Collectors;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -36,16 +34,13 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.annotations.cache.NoCache;
-import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.admin.ResourceSetService;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
-import org.keycloak.authorization.protection.resource.representation.UmaResourceRepresentation;
-import org.keycloak.authorization.protection.resource.representation.UmaScopeRepresentation;
+import org.keycloak.events.admin.OperationType;
import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
-import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponseException;
/**
@@ -56,39 +51,51 @@ public class ResourceService {
private final ResourceServer resourceServer;
private final ResourceSetService resourceManager;
private final Identity identity;
- private final AuthorizationProvider authorization;
- public ResourceService(ResourceServer resourceServer, Identity identity, ResourceSetService resourceManager, AuthorizationProvider authorization) {
+ public ResourceService(ResourceServer resourceServer, Identity identity, ResourceSetService resourceManager) {
this.identity = identity;
this.resourceServer = resourceServer;
this.resourceManager = resourceManager;
- this.authorization = authorization;
}
@POST
@Consumes("application/json")
@Produces("application/json")
- public Response create(@Context UriInfo uriInfo, UmaResourceRepresentation umaResource) {
+ public Response create(@Context UriInfo uriInfo, UmaResourceRepresentation resource) {
checkResourceServerSettings();
- if (umaResource == null) {
+
+ if (resource == null) {
return Response.status(Status.BAD_REQUEST).build();
}
- return this.resourceManager.create(uriInfo, toResourceRepresentation(umaResource), (Function<Resource, UmaResourceRepresentation>) this::toUmaRepresentation);
+
+ ResourceOwnerRepresentation owner = resource.getOwner();
+
+ if (owner == null) {
+ owner = new ResourceOwnerRepresentation();
+ resource.setOwner(owner);
+ }
+
+ String ownerId = owner.getId();
+
+ if (ownerId == null) {
+ ownerId = this.identity.getId();
+ }
+
+ owner.setId(ownerId);
+
+ ResourceRepresentation newResource = resourceManager.create(resource);
+
+ resourceManager.audit(uriInfo, resource, resource.getId(), OperationType.CREATE);
+
+ return Response.status(Status.CREATED).entity(new UmaResourceRepresentation(newResource)).build();
}
@Path("{id}")
@PUT
@Consumes("application/json")
@Produces("application/json")
- public Response update(@Context UriInfo uriInfo, @PathParam("id") String id, UmaResourceRepresentation representation) {
- ResourceRepresentation resource = toResourceRepresentation(representation);
- Response response = this.resourceManager.update(uriInfo, id, resource);
-
- if (response.getEntity() instanceof ResourceRepresentation) {
- return Response.noContent().build();
- }
-
- return response;
+ public Response update(@Context UriInfo uriInfo, @PathParam("id") String id, ResourceRepresentation resource) {
+ return this.resourceManager.update(uriInfo, id, resource);
}
@Path("/{id}")
@@ -102,7 +109,7 @@ public class ResourceService {
@GET
@Produces("application/json")
public Response findById(@PathParam("id") String id) {
- return this.resourceManager.findById(id, (Function<Resource, UmaResourceRepresentation>) resource -> toUmaRepresentation(resource));
+ return this.resourceManager.findById(id, UmaResourceRepresentation::new);
}
@GET
@@ -114,75 +121,11 @@ public class ResourceService {
@QueryParam("owner") String owner,
@QueryParam("type") String type,
@QueryParam("scope") String scope,
+ @QueryParam("matchingUri") Boolean matchingUri,
@QueryParam("deep") Boolean deep,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
- return resourceManager.find(id, name, uri, owner, type, scope, deep, firstResult, maxResult, (BiFunction<Resource, Boolean, String>) (resource, deep1) -> resource.getId());
- }
-
- private ResourceRepresentation toResourceRepresentation(UmaResourceRepresentation umaResource) {
- ResourceRepresentation resource = new ResourceRepresentation();
-
- resource.setId(umaResource.getId());
- resource.setIconUri(umaResource.getIconUri());
- resource.setName(umaResource.getName());
- resource.setUri(umaResource.getUri());
- resource.setType(umaResource.getType());
- resource.setOwnerManagedAccess(umaResource.getOwnerManagedAccess());
-
- ResourceOwnerRepresentation owner = new ResourceOwnerRepresentation();
- String ownerId = umaResource.getOwner();
-
- if (ownerId == null) {
- ownerId = this.identity.getId();
- }
-
- owner.setId(ownerId);
- resource.setOwner(owner);
-
- resource.setScopes(umaResource.getScopes().stream().map(representation -> {
- ScopeRepresentation scopeRepresentation = new ScopeRepresentation();
-
- scopeRepresentation.setId(representation.getId());
- scopeRepresentation.setName(representation.getName());
- scopeRepresentation.setIconUri(representation.getIconUri());
-
- return scopeRepresentation;
- }).collect(Collectors.toSet()));
-
- resource.setAttributes(umaResource.getAttributes());
-
- return resource;
- }
-
- private UmaResourceRepresentation toUmaRepresentation(Resource model) {
- if (model == null) {
- return null;
- }
-
- UmaResourceRepresentation resource = new UmaResourceRepresentation();
-
- resource.setId(model.getId());
- resource.setIconUri(model.getIconUri());
- resource.setName(model.getName());
- resource.setUri(model.getUri());
- resource.setType(model.getType());
-
- if (model.getOwner() != null) {
- resource.setOwner(model.getOwner());
- }
-
- resource.setScopes(model.getScopes().stream().map(scopeRepresentation -> {
- UmaScopeRepresentation umaScopeRep = new UmaScopeRepresentation();
- umaScopeRep.setId(scopeRepresentation.getId());
- umaScopeRep.setName(scopeRepresentation.getName());
- umaScopeRep.setIconUri(scopeRepresentation.getIconUri());
- return umaScopeRep;
- }).collect(Collectors.toSet()));
-
- resource.setAttributes(model.getAttributes());
-
- return resource;
+ return resourceManager.find(id, name, uri, owner, type, scope, matchingUri, deep, firstResult, maxResult, (BiFunction<Resource, Boolean, String>) (resource, deep1) -> resource.getId());
}
private void checkResourceServerSettings() {
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/UmaResourceRepresentation.java b/services/src/main/java/org/keycloak/authorization/protection/resource/UmaResourceRepresentation.java
new file mode 100644
index 0000000..302dd96
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/UmaResourceRepresentation.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.protection.resource;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UmaResourceRepresentation extends ResourceRepresentation {
+
+ public UmaResourceRepresentation() {
+
+ }
+
+ public UmaResourceRepresentation(ResourceRepresentation resource) {
+ setId(resource.getId());
+ setName(resource.getName());
+ setType(resource.getType());
+ setUri(resource.getUri());
+ setIconUri(resource.getIconUri());
+ setOwner(resource.getOwner());
+ setScopes(resource.getScopes());
+ setDisplayName(resource.getDisplayName());
+ setOwnerManagedAccess(resource.getOwnerManagedAccess());
+ }
+
+ public UmaResourceRepresentation(Resource resource) {
+ setId(resource.getId());
+ setName(resource.getName());
+ setType(resource.getType());
+ setUri(resource.getUri());
+ setIconUri(resource.getIconUri());
+ setOwner(resource.getOwner());
+ setScopes(resource.getScopes().stream().map(scope -> new ScopeRepresentation(scope.getName())).collect(Collectors.toSet()));
+ setDisplayName(resource.getDisplayName());
+ setOwnerManagedAccess(resource.isOwnerManagedAccess());
+ setAttributes(resource.getAttributes());
+ }
+
+ @JsonProperty("resource_scopes")
+ @Override
+ public Set<ScopeRepresentation> getScopes() {
+ return super.getScopes();
+ }
+
+ @JsonProperty("resource_scopes")
+ @Override
+ public void setScopes(Set<ScopeRepresentation> scopes) {
+ super.setScopes(scopes);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
index c55a585..e3e447f 100755
--- a/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
+++ b/services/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailTemplateProvider.java
@@ -192,8 +192,9 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
}
}
- protected void send(String subjectKey, String template, Map<String, Object> attributes) throws EmailException {
- send(subjectKey, Collections.emptyList(), template, attributes);
+ @Override
+ public void send(String subjectFormatKey, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException {
+ send(subjectFormatKey, Collections.emptyList(), bodyTemplate, bodyAttributes);
}
protected EmailTemplate processTemplate(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
@@ -229,9 +230,10 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
return session.theme().getTheme(Theme.Type.EMAIL);
}
- protected void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
+ @Override
+ public void send(String subjectFormatKey, List<Object> subjectAttributes, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException {
try {
- EmailTemplate email = processTemplate(subjectKey, subjectAttributes, template, attributes);
+ EmailTemplate email = processTemplate(subjectFormatKey, subjectAttributes, bodyTemplate, bodyAttributes);
send(email.getSubject(), email.getTextBody(), email.getHtmlBody());
} catch (EmailException e) {
throw e;
diff --git a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
index f6beac5..a84765b 100755
--- a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
+++ b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
@@ -68,6 +68,7 @@ import org.keycloak.representations.idm.ScopeMappingRepresentation;
import org.keycloak.representations.idm.UserConsentRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
@@ -315,7 +316,7 @@ public class ExportUtils {
ResourceRepresentation rep = toRepresentation(resource, settingsModel, authorization);
if (rep.getOwner().getId().equals(settingsModel.getId())) {
- rep.setOwner(null);
+ rep.setOwner((ResourceOwnerRepresentation) null);
} else {
rep.getOwner().setId(null);
}
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index cf480fc..3d0f52d 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -332,9 +332,25 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
Properties messagesBundle = handleThemeResources(theme, locale);
FormMessage msg = new FormMessage(null, message);
return formatMessage(msg, messagesBundle, locale);
+ }
+ @Override
+ public String getMessage(String message, String... parameters) {
+ Theme theme;
+ try {
+ theme = getTheme();
+ } catch (IOException e) {
+ logger.error("Failed to create theme", e);
+ throw new RuntimeException("Failed to create theme");
+ }
+
+ Locale locale = session.getContext().resolveLocale(user);
+ Properties messagesBundle = handleThemeResources(theme, locale);
+ FormMessage msg = new FormMessage(message, parameters);
+ return formatMessage(msg, messagesBundle, locale);
}
-
+
+
/**
* Create common attributes used in all templates.
*
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 cf0bb4a..65c66e2 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
@@ -371,6 +371,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
+ if (request.getDisplay() != null) authenticationSession.setClientNote(OAuth2Constants.DISPLAY, request.getDisplay());
// https://tools.ietf.org/html/rfc7636#section-4
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
index 7f5048a..4062021 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
@@ -164,12 +164,11 @@ public class LogoutEndpoint {
*
* returns 204 if successful, 400 if not with a json error response.
*
- * @param authorizationHeader
* @return
*/
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
- public Response logoutToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader) {
+ public Response logoutToken() {
MultivaluedMap<String, String> form = request.getDecodedFormParameters();
checkSsl();
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
index 29edb03..083c2c3 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
@@ -32,6 +32,7 @@ public class AuthorizationEndpointRequest {
String state;
String scope;
String loginHint;
+ String display;
String prompt;
String nonce;
Integer maxAge;
@@ -111,4 +112,7 @@ public class AuthorizationEndpointRequest {
return codeChallengeMethod;
}
+ public String getDisplay() {
+ return display;
+ }
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
index 90160ee..d6cb1b7 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
@@ -18,6 +18,7 @@
package org.keycloak.protocol.oidc.endpoints.request;
import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@@ -91,6 +92,7 @@ abstract class AuthzEndpointRequestParser {
request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM));
request.claims = replaceIfNotNull(request.claims, getParameter(OIDCLoginProtocol.CLAIMS_PARAM));
request.acr = replaceIfNotNull(request.acr, getParameter(OIDCLoginProtocol.ACR_PARAM));
+ request.display = replaceIfNotNull(request.display, getParameter(OAuth2Constants.DISPLAY));
// https://tools.ietf.org/html/rfc7636#section-6.1
request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM));
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 05f9f4c..38d2489 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -776,8 +776,15 @@ public class TokenEndpoint {
String audience = formParams.getFirst(OAuth2Constants.AUDIENCE);
if (audience != null) {
targetClient = realm.getClientByClientId(audience);
+ if (targetClient == null) {
+ event.detail(Details.REASON, "audience not found");
+ event.error(Errors.CLIENT_NOT_FOUND);
+ throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Audience not found", Response.Status.BAD_REQUEST);
+
+ }
}
+
if (targetClient.isConsentRequired()) {
event.detail(Details.REASON, "audience requires consent");
event.error(Errors.CONSENT_DENIED);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
index 60970d8..8dadaaf 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
@@ -155,7 +155,7 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide
PolicyEnforcerConfig enforcerConfig = new PolicyEnforcerConfig();
enforcerConfig.setEnforcementMode(null);
- enforcerConfig.setCreateResources(null);
+ enforcerConfig.setLazyLoadPaths(null);
rep.setEnforcerConfig(enforcerConfig);
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 148d840..56c0022 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -71,6 +71,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
public static final String MAX_AGE_PARAM = OAuth2Constants.MAX_AGE;
public static final String PROMPT_PARAM = OAuth2Constants.PROMPT;
public static final String LOGIN_HINT_PARAM = "login_hint";
+ public static final String DISPLAY_PARAM = "display";
public static final String REQUEST_PARAM = "request";
public static final String REQUEST_URI_PARAM = "request_uri";
public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM;
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 803e778..edb9b51 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -21,11 +21,7 @@ import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
import org.keycloak.TokenVerifier;
-import org.keycloak.authentication.AuthenticationProcessor;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionContextResult;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.authentication.*;
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.common.ClientConnection;
@@ -761,6 +757,11 @@ public class AuthenticationManager {
uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId());
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
+ if (uriInfo.getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
+ uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, authSession.getParentSession().getId());
+
+ }
+
URI redirect = uriBuilder.build(realm.getName());
return Response.status(302).location(redirect).build();
@@ -965,6 +966,25 @@ public class AuthenticationManager {
authSession.setProtocolMappers(requestedProtocolMappers);
}
+ public static RequiredActionProvider createRequiredAction(KeycloakSession session, RequiredActionFactory factory, AuthenticationSessionModel authSession) {
+ String display = authSession.getClientNote(OAuth2Constants.DISPLAY);
+ if (display == null) return factory.create(session);
+
+
+ if (factory instanceof DisplayTypeRequiredActionFactory) {
+ RequiredActionProvider provider = ((DisplayTypeRequiredActionFactory)factory).createDisplay(session, display);
+ if (provider != null) return provider;
+ }
+ // todo create a provider for handling lack of display support
+ if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
+ throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, TextChallenge.browserRequired(session));
+
+ } else {
+ return factory.create(session);
+ }
+ }
+
+
protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession,
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user,
Set<String> requiredActions) {
@@ -982,7 +1002,15 @@ public class AuthenticationManager {
if (factory == null) {
throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?");
}
- RequiredActionProvider actionProvider = factory.create(session);
+ RequiredActionProvider actionProvider = null;
+ try {
+ actionProvider = createRequiredAction(session, factory, authSession);
+ } catch (AuthenticationFlowException e) {
+ if (e.getResponse() != null) {
+ return e.getResponse();
+ }
+ throw e;
+ }
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory);
actionProvider.requiredActionChallenge(context);
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 c6fb6c8..425a889 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -21,6 +21,7 @@ package org.keycloak.services.messages;
*/
public class Messages {
+ public static final String DISPLAY_UNSUPPORTED = "displayUnsupported";
public static final String LOGIN_TIMEOUT = "loginTimeout";
public static final String INVALID_USER = "invalidUserMessage";
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java
index de29fd7..85cf2a7 100644
--- a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java
@@ -40,6 +40,7 @@ import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation;
+import org.keycloak.theme.beans.MessageFormatterMethod;
/**
* Created by st on 29/03/17.
@@ -101,7 +102,8 @@ public class AccountConsole {
if (auth != null) {
Locale locale = session.getContext().resolveLocale(auth.getUser());
map.put("locale", locale.toLanguageTag());
- map.put("msg", messagesToJsonString(theme.getMessages(locale)));
+ Properties messages = theme.getMessages(locale);
+ map.put("msg", messagesToJsonString(messages));
}
} catch (Exception e) {
logger.warn("Failed to load messages", e);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
index 6fa044f..8994718 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
@@ -196,6 +196,10 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
return admin;
}
+ public RealmModel adminsRealm() {
+ return adminsRealm;
+ }
+
@Override
public RolePermissions roles() {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java
index 361cb0c..464f512 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java
@@ -230,7 +230,20 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
} else {
return true;
}
- } else {
+ } else if (role.getName().equals(AdminRoles.REALM_ADMIN)) {
+ // check to see if we have masterRealm.admin role. Otherwise abort
+ if (root.adminsRealm() == null || !root.adminsRealm().getName().equals(Config.getAdminRealm())) {
+ return adminConflictMessage(role);
+ }
+
+ RealmModel masterRealm = root.adminsRealm();
+ RoleModel adminRole = masterRealm.getRole(AdminRoles.ADMIN);
+ if (root.admin().hasRole(adminRole)) {
+ return true;
+ } else {
+ return adminConflictMessage(role);
+ }
+ } else {
return adminConflictMessage(role);
}
@@ -239,6 +252,7 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
if (role.getContainer() instanceof RealmModel) {
RealmModel realm = (RealmModel)role.getContainer();
// If realm role is master admin role then abort
+ // if realm name is master realm, than we know this is a admin role in master realm.
if (realm.getName().equals(Config.getAdminRealm())) {
return adminConflictMessage(role);
}
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 7c38ba8..ef9525a 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -16,17 +16,12 @@
*/
package org.keycloak.services.resources;
+import org.keycloak.authentication.*;
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
-import org.keycloak.authentication.AuthenticationProcessor;
-import org.keycloak.authentication.RequiredActionContext;
-import org.keycloak.authentication.RequiredActionContextResult;
-import org.keycloak.authentication.RequiredActionFactory;
-import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.TokenVerifier;
-import org.keycloak.authentication.ExplainedVerificationException;
import org.keycloak.authentication.actiontoken.*;
import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
@@ -934,7 +929,15 @@ public class LoginActionsService {
event.error(Errors.INVALID_CODE);
throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_CODE));
}
- RequiredActionProvider provider = factory.create(session);
+ RequiredActionProvider provider = null;
+ try {
+ provider = AuthenticationManager.createRequiredAction(session, factory, authSession);
+ } catch (AuthenticationFlowException e) {
+ if (e.getResponse() != null) {
+ return e.getResponse();
+ }
+ throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED));
+ }
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, authSession.getAuthenticatedUser(), factory) {
@Override
diff --git a/services/src/main/java/org/keycloak/utils/TotpUtils.java b/services/src/main/java/org/keycloak/utils/TotpUtils.java
index 67ff697..d076735 100644
--- a/services/src/main/java/org/keycloak/utils/TotpUtils.java
+++ b/services/src/main/java/org/keycloak/utils/TotpUtils.java
@@ -45,6 +45,12 @@ public class TotpUtils {
return sb.toString();
}
+ public static String decode(String totpSecretEncoded) {
+ String encoded = totpSecretEncoded.replace(" ", "");
+ byte[] bytes = Base32.decode(encoded);
+ return new String(bytes);
+ }
+
public static String qrCode(String totpSecret, RealmModel realm, UserModel user) {
try {
String keyUri = realm.getOTPPolicy().getKeyURI(realm, user, totpSecret);
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
index 76b7507..ee29448 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -38,4 +38,4 @@ org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFacto
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
org.keycloak.protocol.docker.DockerAuthenticatorFactory
-org.keycloak.authentication.authenticators.cli.CliUsernamePasswordAuthenticatorFactory
+org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory
diff --git a/testsuite/integration-arquillian/test-apps/photoz/keycloak-lazy-load-path-authz-service.json b/testsuite/integration-arquillian/test-apps/photoz/keycloak-lazy-load-path-authz-service.json
new file mode 100644
index 0000000..47437dc
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/keycloak-lazy-load-path-authz-service.json
@@ -0,0 +1,78 @@
+{
+ "realm": "photoz",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url": "http://localhost:8180/auth",
+ "ssl-required": "external",
+ "resource": "photoz-restful-api",
+ "bearer-only" : true,
+ "credentials": {
+ "jwt": {
+ "client-key-password": "password",
+ "client-keystore-file": "classpath:keystore.jks",
+ "client-keystore-password": "password",
+ "client-key-alias": "secure-portal",
+ "token-timeout": 10,
+ "client-keystore-type": "jks"
+ }
+ },
+ "policy-enforcer": {
+ "enforcement-mode": "PERMISSIVE",
+ "user-managed-access": {},
+ "lazy-load-paths": true,
+ "paths": [
+ {
+ "name" : "Album Resource",
+ "path" : "/album",
+ "methods" : [
+ {
+ "method": "GET",
+ "scopes-enforcement-mode" : "DISABLED"
+ }
+ ]
+ },
+ {
+ "name" : "Album Resource",
+ "path" : "/album/{id}",
+ "methods" : [
+ {
+ "method": "DELETE",
+ "scopes" : ["album:delete"]
+ },
+ {
+ "method": "GET",
+ "scopes" : ["album:view"]
+ }
+ ]
+ },
+ {
+ "path" : "/profile"
+ },
+ {
+ "name" : "Admin Resources",
+ "path" : "/admin/*"
+ },
+ {
+ "name" : "Scope Protected Resource",
+ "path" : "/scope-any",
+ "methods": [
+ {
+ "method": "GET",
+ "scopes": ["scope-a", "scope-b"],
+ "scopes-enforcement-mode": "ANY"
+ }
+ ]
+ },
+ {
+ "name" : "Scope Protected Resource",
+ "path" : "/scope-all",
+ "methods": [
+ {
+ "method": "GET",
+ "scopes": ["scope-a", "scope-b"],
+ "scopes-enforcement-mode": "ALL"
+ }
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
index 94feb72..40b1242 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
@@ -3,8 +3,8 @@ package org.keycloak.example.photoz.album;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthorizationContext;
-import org.keycloak.authorization.client.representation.ResourceRepresentation;
-import org.keycloak.authorization.client.representation.ScopeRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.example.photoz.entity.Album;
import org.keycloak.example.photoz.util.Transaction;
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
index a0f8711..dd0cab1 100644
--- a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
@@ -21,6 +21,16 @@
"paths": [
{
"name" : "Album Resource",
+ "path" : "/album",
+ "methods" : [
+ {
+ "method": "GET",
+ "scopes-enforcement-mode" : "DISABLED"
+ }
+ ]
+ },
+ {
+ "name" : "Album Resource",
"path" : "/album/{id}",
"methods" : [
{
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-lazy-load-authz-service.json b/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-lazy-load-authz-service.json
new file mode 100644
index 0000000..35f76d2
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-lazy-load-authz-service.json
@@ -0,0 +1,15 @@
+{
+ "realm": "servlet-authz",
+ "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url" : "http://localhost:8180/auth",
+ "ssl-required" : "external",
+ "resource" : "servlet-authz-app",
+ "public-client" : false,
+ "credentials": {
+ "secret": "secret"
+ },
+ "policy-enforcer": {
+ "on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp",
+ "lazy-load-paths": true
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json
index 0dd6a14..d6e7f00 100644
--- a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json
+++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json
@@ -8,6 +8,7 @@
},
"policy-enforcer": {
"on-deny-redirect-to": "/servlet-policy-enforcer/denied.jsp",
+ "lazy-load-paths": false,
"paths": [
{
"name": "Welcome Resource",
@@ -31,7 +32,7 @@
},
{
"name": "Pattern 5",
- "path": "/resource/{pattern}/resource-d"
+ "path": "/a/{pattern}/resource-d"
},
{
"name": "Pattern 6",
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index d379474..1e5d755 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -228,11 +228,47 @@
<type>zip</type>
<outputDirectory>${containers.home}</outputDirectory>
</artifactItem>
- </artifactItems>
+ </artifactItems>
</configuration>
</execution>
</executions>
</plugin>
+ <plugin>
+ <groupId>com.igormaznitsa</groupId>
+ <artifactId>mvn-golang-wrapper</artifactId>
+ <version>2.1.6</version>
+ <extensions>true</extensions>
+ <configuration>
+ <goVersion>1.9.2</goVersion>
+ </configuration>
+ <executions>
+ <execution>
+ <id>get-mousetrap</id>
+ <goals>
+ <goal>get</goal>
+ </goals>
+ <configuration>
+ <packages>
+ <package>github.com/inconshreveable/mousetrap</package>
+ </packages>
+ <goPath>${project.build.directory}/gopath</goPath>
+ </configuration>
+ </execution>
+ <execution>
+ <id>get-kcinit</id>
+ <goals>
+ <goal>get</goal>
+ </goals>
+ <configuration>
+ <packages>
+ <package>github.com/keycloak/kcinit</package>
+ </packages>
+ <goPath>${project.build.directory}/gopath</goPath>
+ <tag>0.3</tag>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
</plugins>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java
index f87d481..90a6692 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java
@@ -203,10 +203,16 @@ public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl {
this.loginPage.form().login(username, password);
waitForPageToLoad();//guess
-
- // simple check if we are at the consent page, if so just click 'Yes'
- if (this.consentPage.isCurrent()) {
- consentPage.confirm();
+
+ try {
+ if (!isCurrent()) {
+ // simple check if we are at the consent page, if so just click 'Yes'
+ if (this.consentPage.isCurrent()) {
+ consentPage.confirm();
+ }
+ }
+ } catch (Exception ignore) {
+ // ignore errors when checking consent page, if an error tests will also fail
}
pause(WAIT_AFTER_OPERATION);
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
index b5476b5..ddfe91d 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/exec/AbstractExec.java
@@ -1,5 +1,6 @@
package org.keycloak.testsuite.cli.exec;
+import org.keycloak.client.admin.cli.util.OsUtil;
import org.keycloak.testsuite.cli.OsArch;
import org.keycloak.testsuite.cli.OsUtils;
@@ -33,7 +34,7 @@ public abstract class AbstractExec {
private boolean logStreams = Boolean.valueOf(System.getProperty("cli.log.output", "true"));
- protected boolean dumpStreams;
+ protected boolean dumpStreams = true;
protected String workDir = WORK_DIR;
@@ -177,6 +178,7 @@ public abstract class AbstractExec {
return new String(stdout.toByteArray());
}
+
public InputStream stderr() {
return new ByteArrayInputStream(stderr.toByteArray());
}
@@ -224,6 +226,22 @@ public abstract class AbstractExec {
throw new RuntimeException("Timed while waiting for content to appear in stdout");
}
+ public void waitForStderr(String content) {
+ long start = System.currentTimeMillis();
+ while (System.currentTimeMillis() - start < waitTimeout) {
+ if (stderrString().indexOf(content) != -1) {
+ return;
+ }
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted ...", e);
+ }
+ }
+
+ throw new RuntimeException("Timed while waiting for content to appear in stdout");
+ }
+
public void sendToStdin(String s) {
if (stdin instanceof InteractiveInputStream) {
((InteractiveInputStream) stdin).pushBytes(s.getBytes());
@@ -232,6 +250,10 @@ public abstract class AbstractExec {
}
}
+ public void sendLine(String s) {
+ sendToStdin(s + OsUtil.EOL);
+ }
+
static void copyStream(InputStream is, OutputStream os) throws IOException {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcinitExec.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcinitExec.java
new file mode 100644
index 0000000..513eb54
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/cli/KcinitExec.java
@@ -0,0 +1,58 @@
+package org.keycloak.testsuite.cli;
+
+import org.keycloak.testsuite.cli.exec.AbstractExec;
+import org.keycloak.testsuite.cli.exec.AbstractExecBuilder;
+
+import java.io.InputStream;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class KcinitExec extends AbstractExec {
+
+ public static final String WORK_DIR = System.getProperty("user.dir") + "/target";
+
+ public static final String CMD = OS_ARCH.isWindows() ? "kcinit" : "kcinit";
+
+ private KcinitExec(String workDir, String argsLine, InputStream stdin) {
+ this(workDir, argsLine, null, stdin);
+ }
+
+ private KcinitExec(String workDir, String argsLine, String env, InputStream stdin) {
+ super(workDir, argsLine, env, stdin);
+ }
+
+ @Override
+ public String getCmd() {
+ return "./" + CMD;
+ }
+
+ public static KcinitExec.Builder newBuilder() {
+ return (KcinitExec.Builder) new KcinitExec.Builder().workDir(WORK_DIR);
+ }
+
+ public static KcinitExec execute(String args) {
+ return newBuilder()
+ .argsLine(args)
+ .execute();
+ }
+
+ public static class Builder extends AbstractExecBuilder<KcinitExec> {
+
+ @Override
+ public KcinitExec execute() {
+ KcinitExec exe = new KcinitExec(workDir, argsLine, env, stdin);
+ exe.dumpStreams = dumpStreams;
+ exe.execute();
+ return exe;
+ }
+
+ @Override
+ public KcinitExec executeAsync() {
+ KcinitExec exe = new KcinitExec(workDir, argsLine, env, stdin);
+ exe.dumpStreams = dumpStreams;
+ exe.executeAsync();
+ return exe;
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java
new file mode 100644
index 0000000..7f47bb9
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example.authorization;
+
+import static org.junit.Assert.assertFalse;
+import static org.keycloak.testsuite.util.IOUtil.loadJson;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+import static org.keycloak.testsuite.util.WaitUtils.pause;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+
+import javax.ws.rs.core.Response;
+
+import org.jboss.arquillian.container.test.api.Deployer;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.junit.BeforeClass;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
+import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
+import org.keycloak.testsuite.ProfileAssume;
+import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
+import org.keycloak.testsuite.util.WaitUtils;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class AbstractBaseServletAuthzAdapterTest extends AbstractExampleAdapterTest {
+
+ protected static final String REALM_NAME = "servlet-authz";
+ protected static final String RESOURCE_SERVER_ID = "servlet-authz-app";
+
+ @BeforeClass
+ public static void enabled() { ProfileAssume.assumePreview(); }
+
+ @ArquillianResource
+ private Deployer deployer;
+
+ @Override
+ public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
+ testRealms.add(
+ loadRealm(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/servlet-authz-realm.json")));
+ }
+
+ protected void performTests(ExceptionRunnable assertion) {
+ performTests(() -> importResourceServerSettings(), assertion);
+ }
+
+ protected void performTests(ExceptionRunnable beforeDeploy, ExceptionRunnable assertion) {
+ try {
+ beforeDeploy.run();
+ deployer.deploy(RESOURCE_SERVER_ID);
+ assertion.run();
+ } catch (FileNotFoundException cause) {
+ throw new RuntimeException("Failed to import authorization settings", cause);
+ } catch (Exception cause) {
+ throw new RuntimeException("Error while executing tests", cause);
+ } finally {
+ deployer.undeploy(RESOURCE_SERVER_ID);
+ }
+ }
+
+ protected boolean hasLink(String text) {
+ return getLink(text) != null;
+ }
+
+ protected boolean hasText(String text) {
+ return this.driver.getPageSource().contains(text);
+ }
+
+ private WebElement getLink(String text) {
+ return this.driver.findElement(By.xpath("//a[text() = '" + text + "']"));
+ }
+
+ protected void importResourceServerSettings() throws FileNotFoundException {
+ getAuthorizationResource().importSettings(loadJson(new FileInputStream(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/servlet-authz-app-authz-service.json")), ResourceServerRepresentation.class));
+ }
+
+ protected AuthorizationResource getAuthorizationResource() {
+ return getClientResource(RESOURCE_SERVER_ID).authorization();
+ }
+
+ protected ClientResource getClientResource(String clientId) {
+ ClientsResource clients = this.realmsResouce().realm(REALM_NAME).clients();
+ ClientRepresentation resourceServer = clients.findByClientId(clientId).get(0);
+ return clients.get(resourceServer.getId());
+ }
+
+ private void logOut() {
+ navigateTo();
+ By by = By.xpath("//a[text() = 'Sign Out']");
+ WaitUtils.waitUntilElement(by);
+ this.driver.findElement(by).click();
+ pause(500);
+ }
+
+ protected void login(String username, String password) {
+ try {
+ navigateTo();
+ Thread.sleep(2000);
+ if (this.driver.getCurrentUrl().startsWith(getResourceServerUrl().toString())) {
+ Thread.sleep(2000);
+ logOut();
+ navigateTo();
+ }
+
+ Thread.sleep(2000);
+
+ this.loginPage.form().login(username, password);
+ } catch (Exception cause) {
+ throw new RuntimeException("Login failed", cause);
+ }
+ }
+
+ private void navigateTo() {
+ this.driver.navigate().to(getResourceServerUrl());
+ WaitUtils.waitUntilElement(By.xpath("//a[text() = 'Dynamic Menu']"));
+ }
+
+ protected boolean wasDenied() {
+ return this.driver.getPageSource().contains("You can not access this resource.");
+ }
+
+ protected URL getResourceServerUrl() {
+ try {
+ return new URL(this.appServerContextRootPage + "/" + RESOURCE_SERVER_ID);
+ } catch (MalformedURLException e) {
+ throw new RuntimeException("Could not obtain resource server url.", e);
+ }
+ }
+
+ protected void navigateToDynamicMenuPage() {
+ navigateTo();
+ getLink("Dynamic Menu").click();
+ }
+
+ protected void navigateToUserPremiumPage() {
+ navigateTo();
+ getLink("User Premium").click();
+ }
+
+ protected void navigateToAdminPage() {
+ navigateTo();
+ getLink("Administration").click();
+ }
+
+ protected void updatePermissionPolicies(String permissionName, String... policyNames) {
+ for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) {
+ if (permissionName.equalsIgnoreCase(policy.getName())) {
+ StringBuilder policies = new StringBuilder("[");
+
+ for (String policyName : policyNames) {
+ if (policies.length() > 1) {
+ policies.append(",");
+ }
+ policies.append("\"").append(policyName).append("\"");
+
+ }
+
+ policies.append("]");
+
+ policy.getConfig().put("applyPolicies", policies.toString());
+ getAuthorizationResource().policies().policy(policy.getId()).update(policy);
+ }
+ }
+ }
+
+ protected void createUserPolicy(String name, String... userNames) {
+ UserPolicyRepresentation policy = new UserPolicyRepresentation();
+
+ policy.setName(name);
+
+ for (String userName : userNames) {
+ policy.addUser(userName);
+ }
+
+ assertFalse(policy.getUsers().isEmpty());
+
+ Response response = getAuthorizationResource().policies().user().create(policy);
+ response.close();
+ }
+
+ protected interface ExceptionRunnable {
+ void run() throws Exception;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPermissiveModeAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPermissiveModeAdapterTest.java
index e1ad409..052150c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPermissiveModeAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPermissiveModeAdapterTest.java
@@ -28,7 +28,7 @@ import org.junit.Test;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
-public abstract class AbstractPermissiveModeAdapterTest extends AbstractServletAuthzAdapterTest {
+public abstract class AbstractPermissiveModeAdapterTest extends AbstractBaseServletAuthzAdapterTest {
@Deployment(name = RESOURCE_SERVER_ID, managed = false)
public static WebArchive deployment() throws IOException {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
index 4cc9d4a..80ddd05 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
@@ -75,7 +75,7 @@ import org.keycloak.util.JsonSerialization;
public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAdapterTest {
private static final String REALM_NAME = "photoz";
- private static final String RESOURCE_SERVER_ID = "photoz-restful-api";
+ protected static final String RESOURCE_SERVER_ID = "photoz-restful-api";
private static final int TOKEN_LIFESPAN_LEEWAY = 3; // seconds
@ArquillianResource
@@ -118,16 +118,6 @@ public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAd
testRealms.add(realm);
}
- @Deployment(name = PhotozClientAuthzTestApp.DEPLOYMENT_NAME)
- public static WebArchive deploymentClient() throws IOException {
- return exampleDeployment(PhotozClientAuthzTestApp.DEPLOYMENT_NAME);
- }
-
- @Deployment(name = RESOURCE_SERVER_ID, managed = false, testable = false)
- public static WebArchive deploymentResourceServer() throws IOException {
- return exampleDeployment(RESOURCE_SERVER_ID);
- }
-
@Override
public void beforeAbstractKeycloakTest() throws Exception {
super.beforeAbstractKeycloakTest();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleLazyLoadPathsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleLazyLoadPathsAdapterTest.java
new file mode 100644
index 0000000..3e35a33
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleLazyLoadPathsAdapterTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example.authorization;
+
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.util.IOUtil.loadJson;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.LaxRedirectStrategy;
+import org.jboss.arquillian.container.test.api.Deployer;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.ResourcesResource;
+import org.keycloak.admin.client.resource.RoleResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
+import org.keycloak.testsuite.ProfileAssume;
+import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
+import org.keycloak.testsuite.adapter.page.PhotozClientAuthzTestApp;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class AbstractPhotozExampleLazyLoadPathsAdapterTest extends AbstractPhotozExampleAdapterTest {
+
+ @Deployment(name = PhotozClientAuthzTestApp.DEPLOYMENT_NAME)
+ public static WebArchive deploymentClient() throws IOException {
+ return exampleDeployment(PhotozClientAuthzTestApp.DEPLOYMENT_NAME);
+ }
+
+ @Deployment(name = RESOURCE_SERVER_ID, managed = false, testable = false)
+ public static WebArchive deploymentResourceServer() throws IOException {
+ return exampleDeployment(RESOURCE_SERVER_ID)
+ .addAsWebInfResource(new File(TEST_APPS_HOME_DIR + "/photoz/keycloak-lazy-load-path-authz-service.json"), "keycloak.json");
+ }
+
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java
index f6a8bb2..71297a9 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,196 +16,19 @@
*/
package org.keycloak.testsuite.adapter.example.authorization;
-import static org.junit.Assert.assertFalse;
-import static org.keycloak.testsuite.util.IOUtil.loadJson;
-import static org.keycloak.testsuite.util.IOUtil.loadRealm;
-import static org.keycloak.testsuite.util.WaitUtils.pause;
+import java.io.IOException;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.List;
-
-import javax.ws.rs.core.Response;
-
-import org.jboss.arquillian.container.test.api.Deployer;
-import org.jboss.arquillian.test.api.ArquillianResource;
-import org.junit.BeforeClass;
-import org.keycloak.admin.client.resource.AuthorizationResource;
-import org.keycloak.admin.client.resource.ClientResource;
-import org.keycloak.admin.client.resource.ClientsResource;
-import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.authorization.PolicyRepresentation;
-import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
-import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
-import org.keycloak.testsuite.ProfileAssume;
-import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
-import org.keycloak.testsuite.util.WaitUtils;
-import org.openqa.selenium.By;
-import org.openqa.selenium.WebElement;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
-public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAdapterTest {
-
- protected static final String REALM_NAME = "servlet-authz";
- protected static final String RESOURCE_SERVER_ID = "servlet-authz-app";
-
- @BeforeClass
- public static void enabled() { ProfileAssume.assumePreview(); }
-
- @ArquillianResource
- private Deployer deployer;
-
- @Override
- public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
- testRealms.add(
- loadRealm(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/servlet-authz-realm.json")));
- }
-
- protected void performTests(ExceptionRunnable assertion) {
- performTests(() -> importResourceServerSettings(), assertion);
- }
-
- protected void performTests(ExceptionRunnable beforeDeploy, ExceptionRunnable assertion) {
- try {
- beforeDeploy.run();
- deployer.deploy(RESOURCE_SERVER_ID);
- assertion.run();
- } catch (FileNotFoundException cause) {
- throw new RuntimeException("Failed to import authorization settings", cause);
- } catch (Exception cause) {
- throw new RuntimeException("Error while executing tests", cause);
- } finally {
- deployer.undeploy(RESOURCE_SERVER_ID);
- }
- }
-
- protected boolean hasLink(String text) {
- return getLink(text) != null;
- }
-
- protected boolean hasText(String text) {
- return this.driver.getPageSource().contains(text);
- }
-
- private WebElement getLink(String text) {
- return this.driver.findElement(By.xpath("//a[text() = '" + text + "']"));
- }
-
- protected void importResourceServerSettings() throws FileNotFoundException {
- getAuthorizationResource().importSettings(loadJson(new FileInputStream(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/servlet-authz-app-authz-service.json")), ResourceServerRepresentation.class));
- }
-
- protected AuthorizationResource getAuthorizationResource() {
- return getClientResource(RESOURCE_SERVER_ID).authorization();
- }
-
- protected ClientResource getClientResource(String clientId) {
- ClientsResource clients = this.realmsResouce().realm(REALM_NAME).clients();
- ClientRepresentation resourceServer = clients.findByClientId(clientId).get(0);
- return clients.get(resourceServer.getId());
- }
-
- private void logOut() {
- navigateTo();
- By by = By.xpath("//a[text() = 'Sign Out']");
- WaitUtils.waitUntilElement(by);
- this.driver.findElement(by).click();
- pause(500);
- }
-
- protected void login(String username, String password) {
- try {
- navigateTo();
- Thread.sleep(2000);
- if (this.driver.getCurrentUrl().startsWith(getResourceServerUrl().toString())) {
- Thread.sleep(2000);
- logOut();
- navigateTo();
- }
+public abstract class AbstractServletAuthzAdapterTest extends AbstractServletAuthzFunctionalAdapterTest {
- Thread.sleep(2000);
-
- this.loginPage.form().login(username, password);
- } catch (Exception cause) {
- throw new RuntimeException("Login failed", cause);
- }
- }
-
- private void navigateTo() {
- this.driver.navigate().to(getResourceServerUrl());
- WaitUtils.waitUntilElement(By.xpath("//a[text() = 'Dynamic Menu']"));
- }
-
- protected boolean wasDenied() {
- return this.driver.getPageSource().contains("You can not access this resource.");
- }
-
- protected URL getResourceServerUrl() {
- try {
- return new URL(this.appServerContextRootPage + "/" + RESOURCE_SERVER_ID);
- } catch (MalformedURLException e) {
- throw new RuntimeException("Could not obtain resource server url.", e);
- }
- }
-
- protected void navigateToDynamicMenuPage() {
- navigateTo();
- getLink("Dynamic Menu").click();
- }
-
- protected void navigateToUserPremiumPage() {
- navigateTo();
- getLink("User Premium").click();
- }
-
- protected void navigateToAdminPage() {
- navigateTo();
- getLink("Administration").click();
+ @Deployment(name = RESOURCE_SERVER_ID, managed = false)
+ public static WebArchive deployment() throws IOException {
+ return exampleDeployment(RESOURCE_SERVER_ID);
}
- protected void updatePermissionPolicies(String permissionName, String... policyNames) {
- for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) {
- if (permissionName.equalsIgnoreCase(policy.getName())) {
- StringBuilder policies = new StringBuilder("[");
-
- for (String policyName : policyNames) {
- if (policies.length() > 1) {
- policies.append(",");
- }
- policies.append("\"").append(policyName).append("\"");
-
- }
-
- policies.append("]");
-
- policy.getConfig().put("applyPolicies", policies.toString());
- getAuthorizationResource().policies().policy(policy.getId()).update(policy);
- }
- }
- }
-
- protected void createUserPolicy(String name, String... userNames) {
- UserPolicyRepresentation policy = new UserPolicyRepresentation();
-
- policy.setName(name);
-
- for (String userName : userNames) {
- policy.addUser(userName);
- }
-
- assertFalse(policy.getUsers().isEmpty());
-
- Response response = getAuthorizationResource().policies().user().create(policy);
- response.close();
- }
-
- protected interface ExceptionRunnable {
- void run() throws Exception;
- }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java
index 63852d0..35c936b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java
@@ -19,14 +19,11 @@ package org.keycloak.testsuite.adapter.example.authorization;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import javax.ws.rs.core.Response;
-import org.jboss.arquillian.container.test.api.Deployment;
-import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.keycloak.admin.client.resource.ClientPoliciesResource;
import org.keycloak.admin.client.resource.RealmResource;
@@ -46,12 +43,7 @@ import org.keycloak.testsuite.util.WaitUtils;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
-public abstract class AbstractServletAuthzFunctionalAdapterTest extends AbstractServletAuthzAdapterTest {
-
- @Deployment(name = RESOURCE_SERVER_ID, managed = false)
- public static WebArchive deployment() throws IOException {
- return exampleDeployment(RESOURCE_SERVER_ID);
- }
+public abstract class AbstractServletAuthzFunctionalAdapterTest extends AbstractBaseServletAuthzAdapterTest {
@Test
public void testCanNotAccessWhenEnforcing() throws Exception {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzLazyLoadPathsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzLazyLoadPathsAdapterTest.java
new file mode 100644
index 0000000..0989aa5
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzLazyLoadPathsAdapterTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example.authorization;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.ws.rs.core.Response;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.ClientPoliciesResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.ResourcesResource;
+import org.keycloak.admin.client.resource.RolePoliciesResource;
+import org.keycloak.admin.client.resource.RoleScopeResource;
+import org.keycloak.admin.client.resource.RolesResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.testsuite.util.WaitUtils;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class AbstractServletAuthzLazyLoadPathsAdapterTest extends AbstractServletAuthzFunctionalAdapterTest {
+
+ @Deployment(name = RESOURCE_SERVER_ID, managed = false)
+ public static WebArchive deployment() throws IOException {
+ return exampleDeployment(RESOURCE_SERVER_ID)
+ .addAsWebInfResource(new File(TEST_APPS_HOME_DIR + "/servlet-authz-app/keycloak-lazy-load-authz-service.json"), "keycloak.json");
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java
index 5c6b0eb..9afed01 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletPolicyEnforcerTest.java
@@ -177,21 +177,21 @@ public abstract class AbstractServletPolicyEnforcerTest extends AbstractExampleA
performTests(() -> {
login("alice", "alice");
- navigateTo("/resource/a/resource-d");
+ navigateTo("/a/a/resource-d");
assertFalse(wasDenied());
navigateTo("/resource/b/resource-d");
assertFalse(wasDenied());
updatePermissionPolicies("Pattern 5 Permission", "Deny Policy");
login("alice", "alice");
- navigateTo("/resource/a/resource-d");
+ navigateTo("/a/a/resource-d");
assertTrue(wasDenied());
- navigateTo("/resource/b/resource-d");
+ navigateTo("/a/b/resource-d");
assertTrue(wasDenied());
updatePermissionPolicies("Pattern 5 Permission", "Default Policy");
login("alice", "alice");
- navigateTo("/resource/b/resource-d");
+ navigateTo("/a/b/resource-d");
assertFalse(wasDenied());
});
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index 2642a11..e5d9471 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -146,7 +146,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates a username and password from login form.");
addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username Form",
"Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
- addProviderInfo(result, "cli-username-password", "Username Password Challenge",
+ addProviderInfo(result, "console-username-password", "Username Password Challenge",
"Proprietary challenge protocol for CLI clients that queries for username password");
addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username",
"Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java
index 32865ec..bbff687 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java
@@ -17,12 +17,17 @@
package org.keycloak.testsuite.admin.client.authorization;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
import java.util.stream.Collectors;
+import org.junit.Test;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.util.JsonSerialization;
@@ -35,12 +40,60 @@ public class ResourceManagementWithAuthzClientTest extends ResourceManagementTes
private AuthzClient authzClient;
+ @Test
+ public void testFindMatchingUri() {
+ doCreateResource(new ResourceRepresentation("/*", Collections.emptySet(), "/*", null));
+ doCreateResource(new ResourceRepresentation("/resources/*", Collections.emptySet(), "/resources/*", null));
+ doCreateResource(new ResourceRepresentation("/resources/{pattern}/*", Collections.emptySet(), "/resources/{pattern}/*", null));
+ doCreateResource(new ResourceRepresentation("/resources/{pattern}/{pattern}/*", Collections.emptySet(), "/resources/{pattern}/{pattern}/*", null));
+ doCreateResource(new ResourceRepresentation("/resources/{pattern}/sub-resources/{pattern}/*", Collections.emptySet(), "/resources/{pattern}/sub-resources/{pattern}/*", null));
+ doCreateResource(new ResourceRepresentation("/resources/{pattern}/sub-resource", Collections.emptySet(), "/resources/{pattern}/sub-resources/{pattern}/*", null));
+
+ AuthzClient authzClient = getAuthzClient();
+
+ List<ResourceRepresentation> resources = authzClient.protection().resource().findByMatchingUri("/test");
+
+ assertNotNull(resources);
+ assertEquals(1, resources.size());
+ assertEquals("/*", resources.get(0).getUri());
+
+ resources = authzClient.protection().resource().findByMatchingUri("/resources/test");
+
+ assertNotNull(resources);
+ assertEquals(1, resources.size());
+ assertEquals("/resources/*", resources.get(0).getUri());
+
+ resources = authzClient.protection().resource().findByMatchingUri("/resources");
+
+ assertNotNull(resources);
+ assertEquals(1, resources.size());
+ assertEquals("/resources/*", resources.get(0).getUri());
+
+ resources = authzClient.protection().resource().findByMatchingUri("/resources/a/b");
+
+ assertNotNull(resources);
+ assertEquals(1, resources.size());
+ assertEquals("/resources/{pattern}/*", resources.get(0).getUri());
+
+ resources = authzClient.protection().resource().findByMatchingUri("/resources/a/b/c");
+
+ assertNotNull(resources);
+ assertEquals(1, resources.size());
+ assertEquals("/resources/{pattern}/{pattern}/*", resources.get(0).getUri());
+
+ resources = authzClient.protection().resource().findByMatchingUri("/resources/a/sub-resources/c/d");
+
+ assertNotNull(resources);
+ assertEquals(1, resources.size());
+ assertEquals("/resources/{pattern}/sub-resources/{pattern}/*", resources.get(0).getUri());
+ }
+
@Override
protected ResourceRepresentation doCreateResource(ResourceRepresentation newResource) {
- org.keycloak.authorization.client.representation.ResourceRepresentation resource = toResourceRepresentation(newResource);
+ ResourceRepresentation resource = toResourceRepresentation(newResource);
AuthzClient authzClient = getAuthzClient();
- org.keycloak.authorization.client.representation.ResourceRepresentation response = authzClient.protection().resource().create(resource);
+ ResourceRepresentation response = authzClient.protection().resource().create(resource);
return toResourceRepresentation(authzClient, response.getId());
}
@@ -60,7 +113,7 @@ public class ResourceManagementWithAuthzClientTest extends ResourceManagementTes
}
private ResourceRepresentation toResourceRepresentation(AuthzClient authzClient, String id) {
- org.keycloak.authorization.client.representation.ResourceRepresentation created = authzClient.protection().resource().findById(id);
+ ResourceRepresentation created = authzClient.protection().resource().findById(id);
ResourceRepresentation resourceRepresentation = new ResourceRepresentation();
resourceRepresentation.setId(created.getId());
@@ -68,11 +121,7 @@ public class ResourceManagementWithAuthzClientTest extends ResourceManagementTes
resourceRepresentation.setIconUri(created.getIconUri());
resourceRepresentation.setUri(created.getUri());
resourceRepresentation.setType(created.getType());
- ResourceOwnerRepresentation owner = new ResourceOwnerRepresentation();
-
- owner.setId(created.getOwner());
-
- resourceRepresentation.setOwner(owner);
+ resourceRepresentation.setOwner(created.getOwner());
resourceRepresentation.setScopes(created.getScopes().stream().map(scopeRepresentation -> {
ScopeRepresentation scope = new ScopeRepresentation();
@@ -88,8 +137,8 @@ public class ResourceManagementWithAuthzClientTest extends ResourceManagementTes
return resourceRepresentation;
}
- private org.keycloak.authorization.client.representation.ResourceRepresentation toResourceRepresentation(ResourceRepresentation newResource) {
- org.keycloak.authorization.client.representation.ResourceRepresentation resource = new org.keycloak.authorization.client.representation.ResourceRepresentation();
+ private ResourceRepresentation toResourceRepresentation(ResourceRepresentation newResource) {
+ ResourceRepresentation resource = new ResourceRepresentation();
resource.setId(newResource.getId());
resource.setName(newResource.getName());
@@ -102,7 +151,7 @@ public class ResourceManagementWithAuthzClientTest extends ResourceManagementTes
}
resource.setScopes(newResource.getScopes().stream().map(scopeRepresentation -> {
- org.keycloak.authorization.client.representation.ScopeRepresentation scope = new org.keycloak.authorization.client.representation.ScopeRepresentation();
+ ScopeRepresentation scope = new ScopeRepresentation();
scope.setName(scopeRepresentation.getName());
scope.setIconUri(scopeRepresentation.getIconUri());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java
index 3318a6d..e155cb5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java
@@ -21,6 +21,7 @@ import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.models.AdminRoles;
@@ -115,6 +116,12 @@ public class IllegalAdminUpgradeTest extends AbstractKeycloakTest {
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
}
+ //@Test
+ public void testConsole() throws Exception {
+ testingClient.server().run(IllegalAdminUpgradeTest::setupUsers);
+ Thread.sleep(10000000);
+ }
+
@Test
public void testRestEvaluation() throws Exception {
testingClient.server().run(IllegalAdminUpgradeTest::setupUsers);
@@ -141,6 +148,7 @@ public class IllegalAdminUpgradeTest extends AbstractKeycloakTest {
RoleRepresentation realmQueryUsers = adminClient.realm(TEST).clients().get(realmAdminClient.getId()).roles().get(AdminRoles.QUERY_USERS).toRepresentation();
RoleRepresentation realmQueryClients = adminClient.realm(TEST).clients().get(realmAdminClient.getId()).roles().get(AdminRoles.QUERY_CLIENTS).toRepresentation();
RoleRepresentation realmQueryGroups = adminClient.realm(TEST).clients().get(realmAdminClient.getId()).roles().get(AdminRoles.QUERY_GROUPS).toRepresentation();
+ RoleRepresentation realmAdmin = adminClient.realm(TEST).clients().get(realmAdminClient.getId()).roles().get(AdminRoles.REALM_ADMIN).toRepresentation();
ClientRepresentation masterClient = adminClient.realm("master").clients().findByClientId(TEST + "-realm").get(0);
RoleRepresentation masterManageAuthorization = adminClient.realm("master").clients().get(masterClient.getId()).roles().get(AdminRoles.MANAGE_AUTHORIZATION).toRepresentation();
@@ -187,6 +195,168 @@ public class IllegalAdminUpgradeTest extends AbstractKeycloakTest {
}
roles.clear();
+ roles.add(realmAdmin);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmManageClients);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmViewClients);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmManageEvents);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmViewEvents);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmManageIdentityProviders);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmViewIdentityProviders);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmManageRealm);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmViewRealm);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmImpersonate);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmManageUsers);
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).remove(roles);
+
+ roles.clear();
+ roles.add(realmViewUsers);
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).remove(roles);
+
+ roles.clear();
+ roles.add(realmQueryUsers);
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).remove(roles);
+
+ roles.clear();
+ roles.add(realmQueryGroups);
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).remove(roles);
+
+ roles.clear();
+ roles.add(realmQueryClients);
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).remove(roles);
+
+ realmClient.close();
+ }
+ // test master user with manage_users can't assign realm's admin roles
+ {
+ ClientRepresentation client = realmAdminClient;
+ Keycloak realmClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(),
+ "master", "userAdmin", "password", Constants.ADMIN_CLI_CLIENT_ID, null);
+ roles.clear();
+ roles.add(realmManageAuthorization);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmViewAuthorization);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
+ roles.add(realmAdmin);
+ try {
+ realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
+ Assert.fail("should fail with forbidden exception");
+ } catch (ClientErrorException e) {
+ Assert.assertEquals(e.getResponse().getStatus(), 403);
+
+ }
+
+ roles.clear();
roles.add(realmManageClients);
try {
realmClient.realm(TEST).users().get(realmUser.getId()).roles().clientLevel(client.getId()).add(roles);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java
index 7fece47..63726b5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java
@@ -42,7 +42,6 @@ import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.authorization.client.util.HttpResponseException;
import org.keycloak.jose.jws.JWSInput;
@@ -56,6 +55,7 @@ import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.PermissionRequest;
import org.keycloak.representations.idm.authorization.PermissionResponse;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java
index b55f348..7393a94 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java
@@ -38,8 +38,6 @@ import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
-import org.keycloak.authorization.client.representation.ResourceRepresentation;
-import org.keycloak.authorization.client.representation.ScopeRepresentation;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.AccessToken;
@@ -48,7 +46,9 @@ import org.keycloak.representations.idm.authorization.AuthorizationResponse;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java
new file mode 100644
index 0000000..f23a63c
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/KcinitTest.java
@@ -0,0 +1,564 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.cli;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory;
+import org.keycloak.authentication.requiredactions.TermsAndConditions;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.credential.CredentialModel;
+import org.keycloak.models.*;
+import org.keycloak.models.utils.TimeBasedOTP;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
+import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
+import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
+import org.keycloak.services.resources.admin.permissions.AdminPermissions;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.forms.PassThroughAuthenticator;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
+import org.keycloak.testsuite.util.GreenMailRule;
+import org.keycloak.testsuite.util.MailUtils;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.util.JsonSerialization;
+import org.keycloak.utils.TotpUtils;
+
+import javax.mail.internet.MimeMessage;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Test that clients can override auth flows
+ *
+ * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
+ */
+public class KcinitTest extends AbstractTestRealmKeycloakTest {
+
+ public static final String KCINIT_CLIENT = "kcinit";
+ public static final String APP = "app";
+ public static final String UNAUTHORIZED_APP = "unauthorized_app";
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+ }
+
+ @Deployment
+ public static WebArchive deploy() {
+ return RunOnServerDeployment.create(UserResource.class)
+ .addPackages(true, "org.keycloak.testsuite");
+ }
+
+
+ @Before
+ public void setupFlows() {
+ RequiredActionProviderRepresentation rep = adminClient.realm("test").flows().getRequiredAction("terms_and_conditions");
+ rep.setEnabled(true);
+ adminClient.realm("test").flows().updateRequiredAction("terms_and_conditions", rep);
+
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+
+ ClientModel client = session.realms().getClientByClientId("kcinit", realm);
+ if (client != null) {
+ return;
+ }
+
+ ClientModel kcinit = realm.addClient(KCINIT_CLIENT);
+ kcinit.setSecret("password");
+ kcinit.setEnabled(true);
+ kcinit.addRedirectUri("urn:ietf:wg:oauth:2.0:oob");
+ kcinit.setPublicClient(false);
+
+ ClientModel app = realm.addClient(APP);
+ app.setSecret("password");
+ app.setEnabled(true);
+ app.setPublicClient(false);
+
+ ClientModel unauthorizedApp = realm.addClient(UNAUTHORIZED_APP);
+ unauthorizedApp.setSecret("password");
+ unauthorizedApp.setEnabled(true);
+ unauthorizedApp.setPublicClient(false);
+
+ // permission for client to client exchange to "target" client
+ AdminPermissionManagement management = AdminPermissions.management(session, realm);
+ management.clients().setPermissionsEnabled(app, true);
+ ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
+ clientRep.setName("to");
+ clientRep.addClient(kcinit.getId());
+ ResourceServer server = management.realmResourceServer();
+ Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
+ management.clients().exchangeToPermission(app).addAssociatedPolicy(clientPolicy);
+ PasswordPolicy policy = realm.getPasswordPolicy();
+ policy = PasswordPolicy.parse(session, "hashIterations(1)");
+ realm.setPasswordPolicy(policy);
+
+ UserModel user = session.users().addUser(realm, "bburke");
+ session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
+ user.setEnabled(true);
+ user.setEmail("p@p.com");
+ user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+ user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
+ user.addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
+ user.addRequiredAction(TermsAndConditions.PROVIDER_ID);
+ user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
+
+ user = session.users().addUser(realm, "wburke");
+ session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
+ user.setEnabled(true);
+ user = session.users().addUser(realm, "tbrady");
+ session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
+ user.setEnabled(true);
+ user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+
+ // Parent flow
+ AuthenticationFlowModel browser = new AuthenticationFlowModel();
+ browser.setAlias("no-console-flow");
+ browser.setDescription("browser based authentication");
+ browser.setProviderId("basic-flow");
+ browser.setTopLevel(true);
+ browser.setBuiltIn(true);
+ browser = realm.addAuthenticationFlow(browser);
+
+ AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
+ execution.setParentFlow(browser.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+ execution.setPriority(20);
+ execution.setAuthenticator(PassThroughAuthenticator.PROVIDER_ID);
+ realm.addAuthenticatorExecution(execution);
+
+ });
+ }
+
+ //@Test
+ public void testDemo() throws Exception {
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ Map<String, String> smtp = new HashMap<>();
+ smtp.put("host", "smtp.gmail.com");
+ smtp.put("port", "465");
+ smtp.put("fromDisplayName", "Keycloak SSO");
+ smtp.put("from", "****");
+ smtp.put("replyToDisplayName", "Keycloak no-reply");
+ smtp.put("replyTo", "reply-to@keycloak.org");
+ smtp.put("ssl", "true");
+ smtp.put("auth", "true");
+ smtp.put("user", "*****");
+ smtp.put("password", "****");
+ realm.setSmtpConfig(smtp);
+
+ });
+
+ Thread.sleep(100000000);
+ }
+
+ @Test
+ public void testBrowserRequired() throws Exception {
+ // that that a browser require challenge is sent back if authentication flow doesn't support console display mode
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ ClientModel kcinit = realm.getClientByClientId(KCINIT_CLIENT);
+ AuthenticationFlowModel flow = realm.getFlowByAlias("no-console-flow");
+ kcinit.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, flow.getId());
+
+
+ });
+
+ testInstall();
+ // login
+ //System.out.println("login....");
+ KcinitExec exe = KcinitExec.newBuilder()
+ .argsLine("login")
+ .executeAsync();
+ exe.waitCompletion();
+ Assert.assertEquals(1, exe.exitCode());
+ Assert.assertTrue(exe.stderrString().contains("Browser required to login"));
+ //Assert.assertEquals("stderr first line", "Browser required to login", exe.stderrLines().get(1));
+
+
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ ClientModel kcinit = realm.getClientByClientId(KCINIT_CLIENT);
+ kcinit.removeAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING);
+
+
+ });
+ }
+
+
+ @Test
+ public void testBadCommand() throws Exception {
+ KcinitExec exe = KcinitExec.execute("covfefe");
+ Assert.assertEquals(1, exe.exitCode());
+ Assert.assertEquals("stderr first line", "Error: unknown command \"covfefe\" for \"kcinit\"", exe.stderrLines().get(0));
+ }
+
+ //@Test
+ public void testInstall() throws Exception {
+ KcinitExec exe = KcinitExec.execute("uninstall");
+ Assert.assertEquals(0, exe.exitCode());
+
+ exe = KcinitExec.newBuilder()
+ .argsLine("install")
+ .executeAsync();
+ //System.out.println(exe.stderrString());
+ //exe.waitForStderr("(y/n):");
+ //exe.sendLine("n");
+ exe.waitForStderr("Authentication server URL [http://localhost:8080/auth]:");
+ exe.sendLine(OAuthClient.AUTH_SERVER_ROOT);
+ //System.out.println(exe.stderrString());
+ exe.waitForStderr("Name of realm [master]:");
+ exe.sendLine("test");
+ //System.out.println(exe.stderrString());
+ exe.waitForStderr("client id [kcinit]:");
+ exe.sendLine("");
+ //System.out.println(exe.stderrString());
+ exe.waitForStderr("Client secret [none]:");
+ exe.sendLine("password");
+ //System.out.println(exe.stderrString());
+ exe.waitCompletion();
+ Assert.assertEquals(0, exe.exitCode());
+ }
+
+ @Test
+ public void testBasic() throws Exception {
+ testInstall();
+ // login
+ //System.out.println("login....");
+ KcinitExec exe = KcinitExec.newBuilder()
+ .argsLine("login")
+ .executeAsync();
+ //System.out.println(exe.stderrString());
+ exe.waitForStderr("Username:");
+ exe.sendLine("wburke");
+ //System.out.println(exe.stderrString());
+ exe.waitForStderr("Password:");
+ exe.sendLine("password");
+ //System.out.println(exe.stderrString());
+ exe.waitForStderr("Login successful");
+ exe.waitCompletion();
+ Assert.assertEquals(0, exe.exitCode());
+ Assert.assertEquals(0, exe.stdoutLines().size());
+
+ exe = KcinitExec.execute("token");
+ Assert.assertEquals(0, exe.exitCode());
+ Assert.assertEquals(1, exe.stdoutLines().size());
+ String token = exe.stdoutLines().get(0).trim();
+ //System.out.println("token: " + token);
+ String introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", token);
+ Map json = JsonSerialization.readValue(introspect, Map.class);
+ Assert.assertTrue(json.containsKey("active"));
+ Assert.assertTrue((Boolean)json.get("active"));
+ //System.out.println("introspect");
+ //System.out.println(introspect);
+
+ exe = KcinitExec.execute("token app");
+ Assert.assertEquals(0, exe.exitCode());
+ Assert.assertEquals(1, exe.stdoutLines().size());
+ String appToken = exe.stdoutLines().get(0).trim();
+ Assert.assertFalse(appToken.equals(token));
+ //System.out.println("token: " + token);
+ introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", appToken);
+ json = JsonSerialization.readValue(introspect, Map.class);
+ Assert.assertTrue(json.containsKey("active"));
+ Assert.assertTrue((Boolean)json.get("active"));
+
+
+ exe = KcinitExec.execute("token badapp");
+ Assert.assertEquals(1, exe.exitCode());
+ Assert.assertEquals(0, exe.stdoutLines().size());
+ Assert.assertEquals(1, exe.stderrLines().size());
+ Assert.assertTrue(exe.stderrLines().get(0), exe.stderrLines().get(0).contains("failed to exchange token: invalid_client Audience not found"));
+
+ exe = KcinitExec.execute("logout");
+ Assert.assertEquals(0, exe.exitCode());
+
+ introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", token);
+ json = JsonSerialization.readValue(introspect, Map.class);
+ Assert.assertTrue(json.containsKey("active"));
+ Assert.assertFalse((Boolean)json.get("active"));
+
+
+
+ }
+
+ @Test
+ public void testTerms() throws Exception {
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ UserModel user = session.users().getUserByUsername("wburke", realm);
+ user.addRequiredAction(TermsAndConditions.PROVIDER_ID);
+ });
+
+ testInstall();
+
+ KcinitExec exe = KcinitExec.newBuilder()
+ .argsLine("login")
+ .executeAsync();
+ exe.waitForStderr("Username:");
+ exe.sendLine("wburke");
+ exe.waitForStderr("Password:");
+ exe.sendLine("password");
+ exe.waitForStderr("Accept Terms? [y/n]:");
+ exe.sendLine("y");
+ exe.waitForStderr("Login successful");
+ exe.waitCompletion();
+ Assert.assertEquals(0, exe.exitCode());
+ Assert.assertEquals(0, exe.stdoutLines().size());
+ }
+
+
+ @Test
+ public void testUpdateProfile() throws Exception {
+ // expects that updateProfile is a passthrough
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ UserModel user = session.users().getUserByUsername("wburke", realm);
+ user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
+ });
+
+ try {
+ testInstall();
+
+ //Thread.sleep(100000000);
+
+ KcinitExec exe = KcinitExec.newBuilder()
+ .argsLine("login")
+ .executeAsync();
+ try {
+ exe.waitForStderr("Username:");
+ exe.sendLine("wburke");
+ exe.waitForStderr("Password:");
+ exe.sendLine("password");
+
+ exe.waitForStderr("Login successful");
+ exe.waitCompletion();
+ Assert.assertEquals(0, exe.exitCode());
+ Assert.assertEquals(0, exe.stdoutLines().size());
+ } catch (Exception ex) {
+ System.out.println(exe.stderrString());
+ throw ex;
+ }
+ } finally {
+
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ UserModel user = session.users().getUserByUsername("wburke", realm);
+ user.removeRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
+ });
+ }
+ }
+
+
+ @Test
+ public void testUpdatePassword() throws Exception {
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ UserModel user = session.users().getUserByUsername("wburke", realm);
+ user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+ });
+
+ try {
+ testInstall();
+
+ KcinitExec exe = KcinitExec.newBuilder()
+ .argsLine("login")
+ .executeAsync();
+ exe.waitForStderr("Username:");
+ exe.sendLine("wburke");
+ exe.waitForStderr("Password:");
+ exe.sendLine("password");
+ exe.waitForStderr("New Password:");
+ exe.sendLine("pw");
+ exe.waitForStderr("Confirm Password:");
+ exe.sendLine("pw");
+ exe.waitForStderr("Login successful");
+ exe.waitCompletion();
+ Assert.assertEquals(0, exe.exitCode());
+ Assert.assertEquals(0, exe.stdoutLines().size());
+
+ exe = KcinitExec.newBuilder()
+ .argsLine("login -f")
+ .executeAsync();
+ exe.waitForStderr("Username:");
+ exe.sendLine("wburke");
+ exe.waitForStderr("Password:");
+ exe.sendLine("pw");
+ exe.waitForStderr("Login successful");
+ exe.waitCompletion();
+ Assert.assertEquals(0, exe.exitCode());
+ Assert.assertEquals(0, exe.stdoutLines().size());
+
+ exe = KcinitExec.execute("logout");
+ Assert.assertEquals(0, exe.exitCode());
+ } finally {
+
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ UserModel user = session.users().getUserByUsername("wburke", realm);
+ session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
+ });
+ }
+
+ }
+
+ protected TimeBasedOTP totp = new TimeBasedOTP();
+
+
+ @Test
+ public void testConfigureTOTP() throws Exception {
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ UserModel user = session.users().getUserByUsername("wburke", realm);
+ user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
+ });
+
+ try {
+
+ testInstall();
+
+ KcinitExec exe = KcinitExec.newBuilder()
+ .argsLine("login")
+ .executeAsync();
+ exe.waitForStderr("Username:");
+ exe.sendLine("wburke");
+ exe.waitForStderr("Password:");
+ exe.sendLine("password");
+ exe.waitForStderr("One Time Password:");
+
+ Pattern p = Pattern.compile("Open the application and enter the key\\s+(.+)\\s+Use the following configuration values");
+ //Pattern p = Pattern.compile("Open the application and enter the key");
+
+ String stderr = exe.stderrString();
+ //System.out.println("***************");
+ //System.out.println(stderr);
+ //System.out.println("***************");
+ Matcher m = p.matcher(stderr);
+ Assert.assertTrue(m.find());
+ String otpSecret = m.group(1).trim();
+
+ //System.out.println("***************");
+ //System.out.println(otpSecret);
+ //System.out.println("***************");
+
+ otpSecret = TotpUtils.decode(otpSecret);
+ String code = totp.generateTOTP(otpSecret);
+ //System.out.println("***************");
+ //System.out.println("code: " + code);
+ //System.out.println("***************");
+ exe.sendLine(code);
+ Thread.sleep(100);
+ //stderr = exe.stderrString();
+ //System.out.println("***************");
+ //System.out.println(stderr);
+ //System.out.println("***************");
+ exe.waitForStderr("Login successful");
+ exe.waitCompletion();
+ Assert.assertEquals(0, exe.exitCode());
+ Assert.assertEquals(0, exe.stdoutLines().size());
+
+
+ exe = KcinitExec.execute("logout");
+ Assert.assertEquals(0, exe.exitCode());
+
+ exe = KcinitExec.newBuilder()
+ .argsLine("login")
+ .executeAsync();
+ exe.waitForStderr("Username:");
+ exe.sendLine("wburke");
+ exe.waitForStderr("Password:");
+ exe.sendLine("password");
+ exe.waitForStderr("One Time Password:");
+ exe.sendLine(totp.generateTOTP(otpSecret));
+ exe.waitForStderr("Login successful");
+ exe.waitCompletion();
+
+ exe = KcinitExec.execute("logout");
+ Assert.assertEquals(0, exe.exitCode());
+ } finally {
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ UserModel user = session.users().getUserByUsername("wburke", realm);
+ session.userCredentialManager().disableCredentialType(realm, user, CredentialModel.OTP);
+ });
+ }
+
+
+ }
+
+ @Rule
+ public GreenMailRule greenMail = new GreenMailRule();
+
+ @Test
+ public void testVerifyEmail() throws Exception {
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+ UserModel user = session.users().getUserByUsername("test-user@localhost", realm);
+ user.addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
+ });
+
+ testInstall();
+
+ KcinitExec exe = KcinitExec.newBuilder()
+ .argsLine("login")
+ .executeAsync();
+ exe.waitForStderr("Username:");
+ exe.sendLine("test-user@localhost");
+ exe.waitForStderr("Password:");
+ exe.sendLine("password");
+ exe.waitForStderr("Email Code:");
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String text = MailUtils.getBody(message).getText();
+ Assert.assertTrue(text.contains("Please verify your email address by entering in the following code."));
+ String code = text.substring("Please verify your email address by entering in the following code.".length()).trim();
+
+ exe.sendLine(code);
+
+ exe.waitForStderr("Login successful");
+ exe.waitCompletion();
+ Assert.assertEquals(0, exe.exitCode());
+ Assert.assertEquals(0, exe.stdoutLines().size());
+
+
+ exe = KcinitExec.execute("logout");
+ Assert.assertEquals(0, exe.exitCode());
+ }
+
+
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ChallengeFlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ChallengeFlowTest.java
index 72024f6..5855979 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ChallengeFlowTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ChallengeFlowTest.java
@@ -24,29 +24,21 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import org.keycloak.OAuth2Constants;
-import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.UserResource;
-import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
-import org.keycloak.authentication.authenticators.cli.CliUsernamePasswordAuthenticatorFactory;
-import org.keycloak.events.Details;
+import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowBindings;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
-import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
-import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.OAuthClient;
-import org.keycloak.util.BasicAuthHelper;
-import org.openqa.selenium.By;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
@@ -55,9 +47,6 @@ import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
-import java.net.URI;
-import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
@@ -120,7 +109,7 @@ public class ChallengeFlowTest extends AbstractTestRealmKeycloakTest {
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(browser.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
- execution.setAuthenticator(CliUsernamePasswordAuthenticatorFactory.PROVIDER_ID);
+ execution.setAuthenticator(ConsoleUsernamePasswordAuthenticatorFactory.PROVIDER_ID);
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleAdapterTest.java
index 42cde44..6139c22 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleAdapterTest.java
@@ -24,6 +24,6 @@ import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
*/
@AppServerContainer("app-server-wildfly")
//@AdapterLibsLocationProperty("adapter.libs.wildfly")
-public class WildflyPhotozExampleAdapterTest extends AbstractPhotozExampleAdapterTest {
+public class WildflyPhotozExampleAdapterTest extends AbstractPhotozExampleNoLazyLoadPathsAdapterTest {
}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleLazyLoadPathsAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleLazyLoadPathsAdapterTest.java
new file mode 100644
index 0000000..a23ba15
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyPhotozExampleLazyLoadPathsAdapterTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example.authorization;
+
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@AppServerContainer("app-server-wildfly")
+//@AdapterLibsLocationProperty("adapter.libs.wildfly")
+public class WildflyPhotozExampleLazyLoadPathsAdapterTest extends AbstractPhotozExampleLazyLoadPathsAdapterTest {
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzAdapterTest.java
index 13a444f..7a7223d 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzAdapterTest.java
@@ -26,6 +26,6 @@ import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
@RunAsClient
@AppServerContainer("app-server-wildfly")
//@AdapterLibsLocationProperty("adapter.libs.wildfly")
-public class WildflyServletAuthzAdapterTest extends AbstractServletAuthzFunctionalAdapterTest {
+public class WildflyServletAuthzAdapterTest extends AbstractServletAuthzAdapterTest {
}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzLazyLoadPathsAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzLazyLoadPathsAdapterTest.java
new file mode 100644
index 0000000..7964736
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/authorization/WildflyServletAuthzLazyLoadPathsAdapterTest.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example.authorization;
+
+import org.jboss.arquillian.container.test.api.RunAsClient;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author tkyjovsk
+ */
+@RunAsClient
+@AppServerContainer("app-server-wildfly")
+//@AdapterLibsLocationProperty("adapter.libs.wildfly")
+public class WildflyServletAuthzLazyLoadPathsAdapterTest extends AbstractServletAuthzLazyLoadPathsAdapterTest {
+
+}
diff --git a/themes/src/main/resources/theme/base/email/html/email-verification-with-code.ftl b/themes/src/main/resources/theme/base/email/html/email-verification-with-code.ftl
new file mode 100644
index 0000000..b4a01c9
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/html/email-verification-with-code.ftl
@@ -0,0 +1,5 @@
+<html>
+<body>
+${msg("emailVerificationBodyCodeHtml",code)?no_esc}
+</body>
+</html>
diff --git a/themes/src/main/resources/theme/base/email/messages/messages_en.properties b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
index e04e947..b2fd0c0 100755
--- a/themes/src/main/resources/theme/base/email/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/email/messages/messages_en.properties
@@ -45,3 +45,7 @@ linkExpirationFormatter.timePeriodUnit.hours=hours
linkExpirationFormatter.timePeriodUnit.hours.1=hour
linkExpirationFormatter.timePeriodUnit.days=days
linkExpirationFormatter.timePeriodUnit.days.1=day
+
+emailVerificationBodyCode=Please verify your email address by entering in the following code.\n\n{0}\n\n.
+emailVerificationBodyCodeHtml=<p>Please verify your email address by entering in the following code.</p><p><b>{0}</b></p>
+
diff --git a/themes/src/main/resources/theme/base/email/text/email-verification-with-code.ftl b/themes/src/main/resources/theme/base/email/text/email-verification-with-code.ftl
new file mode 100644
index 0000000..4ffb7d8
--- /dev/null
+++ b/themes/src/main/resources/theme/base/email/text/email-verification-with-code.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("emailVerificationBodyCode",code)}
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/login/login-config-totp-text.ftl b/themes/src/main/resources/theme/base/login/login-config-totp-text.ftl
new file mode 100755
index 0000000..d609182
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/login-config-totp-text.ftl
@@ -0,0 +1,31 @@
+<#ftl output_format="plainText">
+${msg("loginTotpIntro")}
+
+${msg("loginTotpStep1")}
+
+<#list totp.policy.supportedApplications as app>
+* ${app}
+</#list>
+
+${msg("loginTotpManualStep2")}
+
+ ${totp.totpSecretEncoded}
+
+
+${msg("loginTotpManualStep3")}
+
+- ${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}
+- ${msg("loginTotpAlgorithm")}: ${totp.policy.getAlgorithmKey()}
+- ${msg("loginTotpDigits")}: ${totp.policy.digits}
+<#if totp.policy.type = "totp">
+- ${msg("loginTotpInterval")}: ${totp.policy.period}
+
+<#elseif totp.policy.type = "hotp">
+- ${msg("loginTotpCounter")}: ${totp.policy.initialCounter}
+
+</#if>
+
+Enter in your one time password so we can verify you have installed it correctly.
+
+
+
diff --git a/themes/src/main/resources/theme/base/login/login-verify-email-code-text.ftl b/themes/src/main/resources/theme/base/login/login-verify-email-code-text.ftl
new file mode 100644
index 0000000..87abcd7
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/login-verify-email-code-text.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("console-verify-email",email, code)}
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
index d4c12e4..5f4c6a2 100755
--- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -34,9 +34,12 @@ emailForgotTitle=Forgot Your Password?
updatePasswordTitle=Update password
codeSuccessTitle=Success code
codeErrorTitle=Error code\: {0}
+displayUnsupported=Requested display type unsupported
+browserRequired=Browser required to login
termsTitle=Terms and Conditions
termsText=<p>Terms and conditions to be defined</p>
+termsPlainText=Terms and conditions to be defined.
recaptchaFailed=Invalid Recaptcha
recaptchaNotConfigured=Recaptcha is required, but not configured
@@ -66,6 +69,7 @@ country=Country
emailVerified=Email verified
gssDelegationCredential=GSS Delegation Credential
+loginTotpIntro=You are required to set up a One Time Password generator to access this account
loginTotpStep1=Install one of the following applications on your mobile
loginTotpStep2=Open the application and scan the barcode
loginTotpStep3=Enter the one-time code provided by the application and click Submit to finish the setup
@@ -278,3 +282,14 @@ noCertificate=[No Certificate]
pageNotFound=Page not found
internalServerError=An internal server error has occurred
+
+console-username=Username:
+console-password=Password:
+console-otp=One Time Password:
+console-new-password=New Password:
+console-confirm-password=Confirm Password:
+console-update-password=Update of your password is required.
+console-verify-email=You are required to verify your email address. An email has been sent to {0} that contains a verification code. Please enter this code into the input below.
+console-email-code=Email Code:
+console-accept-terms=Accept Terms? [y/n]:
+console-accept=y
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/index.ftl b/themes/src/main/resources/theme/keycloak-preview/account/index.ftl
index 872e42e..3cc9398 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/index.ftl
+++ b/themes/src/main/resources/theme/keycloak-preview/account/index.ftl
@@ -8,7 +8,7 @@
var baseUrl = '${baseUrl}';
var realm = '${realm}';
var resourceUrl = '${resourceUrl}';
-
+
<#if referrer??>
var referrer = '${referrer}';
var referrer_uri = '${referrer_uri}';
@@ -17,6 +17,9 @@
<#if msg??>
var locale = '${locale}';
var l18n_msg = JSON.parse('${msg?no_esc}');
+ <#else>
+ var locale = 'en';
+ var l18n_msg = {};
</#if>
</script>
@@ -29,16 +32,6 @@
<link rel="icon" href="${resourceUrl}/app/assets/img/favicon.ico" type="image/x-icon"/>
- <#if properties.styles?has_content>
- <#list properties.styles?split(' ') as style>
- <link href="${resourceUrl}/${style}" rel="stylesheet"/>
- </#list>
- </#if>
-
- <link rel="stylesheet" href="${resourceUrl}/styles.css">
-
- <!--<script src="${authUrl}/js/${resourceVersion}/keycloak.js" type="text/javascript"></script>-->
-
<!-- PatternFly -->
<!-- iPad retina icon -->
<link rel="apple-touch-icon-precomposed" sizes="152x152"
@@ -65,56 +58,150 @@
media="screen, print">
<link href="${resourceUrl}/node_modules/patternfly/dist/css/patternfly-additions.min.css" rel="stylesheet"
media="screen, print">
- <script src="${resourceUrl}/node_modules/jquery/dist/jquery.min.js"></script>
+
<script src="${resourceUrl}/node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
- <script src="${resourceUrl}/node_modules/jquery-match-height/dist/jquery.matchHeight-min.js"></script>
<script src="${resourceUrl}/node_modules/patternfly/dist/js/patternfly.min.js"></script>
+ <script src="${authUrl}/js/keycloak.js"></script>
- <!-- Polyfill(s) for older browsers -->
- <script src="${resourceUrl}/node_modules/core-js/client/shim.min.js"></script>
-
- <#if properties.scripts?has_content>
- <#list properties.scripts?split(' ') as script>
- <script type="text/javascript" src="${resourceUrl}/${script}"></script>
- </#list>
- </#if>
-
- <script src="${resourceUrl}/node_modules/zone.js/dist/zone.js"></script>
- <script src="${resourceUrl}/node_modules/systemjs/dist/system.src.js"></script>
-
- <script src="${resourceUrl}/systemjs.config.js"></script>
<script>
- System.import('${resourceUrl}/main.js').catch(function (err) {
- console.error(err);
+ var keycloak = Keycloak('${authUrl}/realms/${realm}/account/keycloak.json');
+ keycloak.init({onLoad: 'check-sso'}).success(function(authenticated) {
+ var loadjs = function (url,loadListener) {
+ const script = document.createElement("script");
+ script.src = resourceUrl + url;
+ if (loadListener) script.addEventListener("load", loadListener);
+ document.head.appendChild(script);
+ };
+ loadjs("/node_modules/core-js/client/shim.min.js", function(){
+ loadjs("/node_modules/zone.js/dist/zone.min.js");
+ loadjs("/node_modules/systemjs/dist/system.src.js", function() {
+ loadjs("/systemjs.config.js");
+ System.import('${resourceUrl}/main.js').catch(function (err) {
+ console.error(err);
+ });
+ if (!keycloak.authenticated) document.getElementById("signInButton").style.visibility='visible';
+ });
+ });
+ }).error(function() {
+ alert('failed to initialize keycloak');
});
</script>
- </head>
-
- <app-root>
- <style>
- .kc-background {
- background: url('${resourceUrl}/img/keycloak-bg.png') top left no-repeat;
- background-size: cover;
- }
- .logo-centered {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- }
- .kc-logo-text {
- background-image: url("${resourceUrl}/img/keycloak-logo-text.png");
- background-repeat: no-repeat;
- width: 250px;
- height: 38px;
- }
- </style>
+ <!-- TODO: We should save these css and js into variables and then load in
+ main.ts for better performance. These might be loaded twice.
+ -->
+ <#if properties.styles?has_content>
+ <#list properties.styles?split(' ') as style>
+ <link href="${resourceUrl}/${style}" rel="stylesheet"/>
+ </#list>
+ <a href="../../../../../../../../keycloak-quickstarts/app-profile-jee-html5/src/main/webapp/index.html"></a>
+ </#if>
- <body class="cards-pf kc-background">
- <div class='logo-centered kc-logo-text'/>
- </body>
- </app-root>
+ <#if properties.scripts?has_content>
+ <#list properties.scripts?split(' ') as script>
+ <script type="text/javascript" src="${resourceUrl}/${script}"></script>
+ </#list>
+ </#if>
+ </head>
+ <body>
+
+
+<!-- Top Navigation -->
+ <nav class="navbar navbar-pf-alt">
+
+ <div class="navbar-header">
+ <a href="http://www.keycloak.org" class="navbar-brand">
+ <img class="navbar-brand-icon" type="image/svg+xml" src="${resourceUrl}/app/assets/img/keycloak-logo-min.png" alt="" width="auto" height="30px"/>
+ </a>
+ </div>
+ <nav class="collapse navbar-collapse">
+ <ul class="nav navbar-nav">
+ </ul>
+
+ <!-- This sign in button is only displayed in the rare case where we go directly to this page and we aren't logged in.
+ Note javascript code above that changes its visibility for that case. Also, because we are not logged in
+ we are unable to localize the button's message. Not sure what to do about that yet.
+ -->
+ <ul class="nav navbar-nav navbar-right navbar-iconic">
+ <li><button id="signInButton" style="visibility:hidden" onclick="keycloak.login();" class="btn btn-primary btn-lg" type="button">Sign In</button></li>
+ </ul>
+ </nav>
+ </nav>
+
+<!--Top Nav -->
+
+<!-- Home Page --->
+ <div class="cards-pf" id="welcomeScreen">
+ <div><h1 class="text-center">Welcome to Keycloak Account Management</h1></div>
+ <div class="container-fluid container-cards-pf">
+ <div class="row row-cards-pf">
+ <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3">
+ <div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
+ <div class="card-pf-body">
+ <div class="card-pf-top-element">
+ <span class="fa pficon-user card-pf-icon-circle"></span>
+ </div>
+ <h2 class="card-pf-title text-center">
+ Personal Info
+ </h2>
+ <h3 class="card-pf-info text-center">
+ <a href="${baseUrl}/#/account">Account</a>
+ </h3>
+ </div>
+ </div>
+ </div>
+ <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3">
+ <div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
+ <div class="card-pf-body">
+ <div class="card-pf-top-element">
+ <span class="fa fa-shield card-pf-icon-circle"></span>
+ </div>
+ <h2 class="card-pf-title text-center">
+ Account Security
+ </h2>
+ <h3 class="card-pf-info text-center">
+ More stuff goes here
+ </h3>
+ </div>
+ </div>
+ </div>
+ <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3">
+ <div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
+ <div class="card-pf-body">
+ <div class="card-pf-top-element">
+ <span class="fa fa-th card-pf-icon-circle"></span>
+ </div>
+ <h2 class="card-pf-title text-center">
+ Applications
+ </h2>
+ <h3 class="card-pf-info text-center">
+ More stuff goes here
+ </h3>
+ </div>
+ </div>
+ </div>
+ <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3">
+ <div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
+ <div class="card-pf-body">
+ <div class="card-pf-top-element">
+ <span class="fa pficon-repository card-pf-icon-circle"></span>
+ </div>
+ <h2 class="card-pf-title text-center">
+ My Resources
+ </h2>
+ <h3 class="card-pf-info text-center">
+ More stuff goes here
+ </h3>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+
+ <app-root></app-root>
+ </body>
</html>
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.component.html b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.component.html
index 2a65d8b..cd1e829 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.component.html
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.component.html
@@ -1,9 +1,11 @@
-<app-top-nav></app-top-nav>
-<app-side-nav></app-side-nav>
+<div *ngIf="kcService.authenticated()">
+ <app-top-nav [showSideNav]="showSideNav"></app-top-nav>
+</div>
+
+<app-side-nav *ngIf="showSideNav"></app-side-nav>
<div class="container-fluid container-pf-alt-nav-pf-vertical-alt {{this.contentWidthClass}}"> <!-- collapsed-nav hidden-nav -->
<router-outlet></router-outlet>
-
</div>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.component.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.component.ts
index 04b2c2e..72a19af 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.component.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.component.ts
@@ -15,10 +15,12 @@
* limitations under the License.
*/
import {Component, HostListener} from '@angular/core';
-
+import {Router, NavigationEnd} from '@angular/router';
+import 'rxjs/add/observable/of'; // needed for ngx-translate
import {TranslateService} from '@ngx-translate/core';
import {ResponsivenessService, ContentWidthClass, MenuClickListener} from "./responsiveness-service/responsiveness.service";
+import {KeycloakService} from "./keycloak-service/keycloak.service";
declare const locale: string;
@@ -30,8 +32,12 @@ declare const locale: string;
export class AppComponent implements MenuClickListener {
private contentWidthClass: ContentWidthClass = this.respSvc.calcSideContentWidthClass();
+ private showSideNav: boolean = false;
- constructor(translate: TranslateService, private respSvc: ResponsivenessService) {
+ constructor(translate: TranslateService,
+ private respSvc: ResponsivenessService,
+ private router: Router,
+ private kcService: KeycloakService) {
// this language will be used as a fallback when a translation isn't found in the current language
translate.setDefaultLang('en');
@@ -39,6 +45,19 @@ export class AppComponent implements MenuClickListener {
translate.use(locale);
this.respSvc.addMenuClickListener(this);
+
+ // show side nav if we are past the welcome screen
+ this.router.events.subscribe(value => {
+ if (value instanceof NavigationEnd) {
+ const navEnd = value as NavigationEnd;
+ console.log(navEnd.url);
+ if (navEnd.url !== '/') {
+ this.showSideNav = true;
+ var welcomeScreen = document.getElementById('welcomeScreen')
+ if (welcomeScreen) welcomeScreen.remove();
+ }
+ }
+ });
}
public menuClicked() : void {
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.module.ts
index 6fdd6db..3aec819 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.module.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app.module.ts
@@ -26,6 +26,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { KeycloakService } from './keycloak-service/keycloak.service';
import { KEYCLOAK_HTTP_PROVIDER } from './keycloak-service/keycloak.http';
+import {KeycloakGuard} from './keycloak-service/keycloak.guard';
import {ResponsivenessService} from './responsiveness-service/responsiveness.service'
@@ -38,55 +39,15 @@ import { TopNavComponent } from './top-nav/top-nav.component';
import { NotificationComponent } from './top-nav/notification.component';
import { ToastNotifier } from './top-nav/toast.notifier';
import { SideNavComponent } from './side-nav/side-nav.component';
-import { AccountPageComponent } from './content/account-page/account-page.component';
-import { PasswordPageComponent } from './content/password-page/password-page.component';
-import { PageNotFoundComponent } from './content/page-not-found/page-not-found.component';
-import { AuthenticatorPageComponent } from './content/authenticator-page/authenticator-page.component';
-
-import { SessionsPageComponent } from './content/sessions-page/sessions-page.component';
-import { LargeSessionCardComponent } from './content/sessions-page/large-session-card.component';
-import { SmallSessionCardComponent } from './content/sessions-page/small-session-card.component';
-
-import { ApplicationsPageComponent } from './content/applications-page/applications-page.component';
-import { LargeAppCardComponent } from './content/applications-page/large-app-card.component';
-import { SmallAppCardComponent } from './content/applications-page/small-app-card.component';
-import { RowAppCardComponent } from './content/applications-page/row-app-card.component';
-
-import { ToolbarComponent } from './content/widgets/toolbar.component';
-
-import {OrderbyPipe} from './pipes/orderby.pipe';
-import {FilterbyPipe} from './pipes/filterby.pipe';
-
-const routes: Routes = [
- { path: 'account', component: AccountPageComponent },
- { path: 'password', component: PasswordPageComponent },
- { path: 'authenticator', component: AuthenticatorPageComponent },
- { path: 'sessions', component: SessionsPageComponent },
- { path: 'applications', component: ApplicationsPageComponent },
- { path: '', redirectTo: '/account', pathMatch: 'full' },
- { path: '**', component: PageNotFoundComponent}
-];
+/* Routing Module */
+import { AppRoutingModule } from './app-routing.module';
const decs = [
AppComponent,
TopNavComponent,
NotificationComponent,
SideNavComponent,
- AccountPageComponent,
- PasswordPageComponent,
- PageNotFoundComponent,
- AuthenticatorPageComponent,
- SessionsPageComponent,
- LargeSessionCardComponent,
- SmallSessionCardComponent,
- ApplicationsPageComponent,
- LargeAppCardComponent,
- SmallAppCardComponent,
- RowAppCardComponent,
- ToolbarComponent,
- OrderbyPipe,
- FilterbyPipe
];
export const ORIGINAL_INCOMING_URL: Location = window.location;
@@ -100,10 +61,11 @@ export const ORIGINAL_INCOMING_URL: Location = window.location;
TranslateModule.forRoot({
loader: {provide: TranslateLoader, useClass: DeclaredVarTranslateLoader}
}),
- RouterModule.forRoot(routes)
+ AppRoutingModule,
],
providers: [
KeycloakService,
+ KeycloakGuard,
KEYCLOAK_HTTP_PROVIDER,
ResponsivenessService,
AccountServiceClient,
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app-routing.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app-routing.module.ts
new file mode 100644
index 0000000..f11cdaf
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/app-routing.module.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import {KeycloakGuard} from './keycloak-service/keycloak.guard';
+
+import { HomePageComponent } from './content/home-page/home-page.component';
+
+declare const resourceUrl: string;
+
+export const routes: Routes = [
+ {path: '', canActivateChild:[KeycloakGuard], children: [
+ { path: 'account', loadChildren: resourceUrl + '/app/content/account-page/account.module.js#AccountModule' },
+ { path: 'password', loadChildren: resourceUrl + '/app/content/password-page/password.module.js#PasswordModule' },
+ { path: 'authenticator', loadChildren: resourceUrl + '/app/content/authenticator-page/authenticator.module.js#AuthenticatorModule' },
+ { path: 'sessions', loadChildren: resourceUrl + '/app/content/sessions-page/sessions.module.js#SessionsModule' },
+ { path: 'applications', loadChildren: resourceUrl + '/app/content/applications-page/applications.module.js#ApplicationsModule' },
+ { path: ':**', loadChildren: resourceUrl + '/app/content/page-not-found/page-not-found.module.js#PageNotFoundModule' },
+ ]
+ }
+ ];
+
+@NgModule({
+ imports: [RouterModule.forRoot(routes)],
+ exports: [RouterModule],
+ declarations: [HomePageComponent]
+})
+export class AppRoutingModule {}
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/assets/img/keycloak-logo-min.png b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/assets/img/keycloak-logo-min.png
new file mode 100644
index 0000000..0bbf1d5
Binary files /dev/null and b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/assets/img/keycloak-logo-min.png differ
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/account.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/account.module.ts
new file mode 100644
index 0000000..1e65b14
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/account.module.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { TranslateModule } from '@ngx-translate/core';
+
+import { AccountPageComponent } from './account-page.component';
+import { AccountRoutingModule } from './account-routing.module';
+
+@NgModule({
+ imports: [ CommonModule, FormsModule, TranslateModule, AccountRoutingModule ],
+ declarations: [ AccountPageComponent ],
+ providers: [ ]
+})
+export class AccountModule {}
+
+
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/account-routing.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/account-routing.module.ts
new file mode 100644
index 0000000..635f4b8
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/account-page/account-routing.module.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { AccountPageComponent } from './account-page.component';
+
+const routes: Routes = [
+ { path: '**', component: AccountPageComponent },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class AccountRoutingModule {}
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/applications-page/applications.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/applications-page/applications.module.ts
new file mode 100644
index 0000000..5cc64fc
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/applications-page/applications.module.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { TranslateModule } from '@ngx-translate/core';
+
+import {WidgetsModule} from '../widgets/widgets.module';
+
+import { ApplicationsPageComponent } from './applications-page.component';
+import { ApplicationsRoutingModule } from './applications-routing.module';
+import { LargeAppCardComponent } from './large-app-card.component';
+import { SmallAppCardComponent } from './small-app-card.component';
+import { RowAppCardComponent } from './row-app-card.component';
+
+@NgModule({
+ imports: [ CommonModule,
+ TranslateModule,
+ ApplicationsRoutingModule,
+ WidgetsModule ],
+ declarations: [ ApplicationsPageComponent,
+ LargeAppCardComponent,
+ SmallAppCardComponent,
+ RowAppCardComponent ],
+ providers: [ ]
+})
+export class ApplicationsModule {}
+
+
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/applications-page/applications-routing.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/applications-page/applications-routing.module.ts
new file mode 100644
index 0000000..f35db61
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/applications-page/applications-routing.module.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { ApplicationsPageComponent } from './applications-page.component';
+
+const routes: Routes = [
+ { path: '**', component: ApplicationsPageComponent },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class ApplicationsRoutingModule {}
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/authenticator-page/authenticator.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/authenticator-page/authenticator.module.ts
new file mode 100644
index 0000000..780bd7c
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/authenticator-page/authenticator.module.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { TranslateModule } from '@ngx-translate/core';
+
+import { AuthenticatorPageComponent } from './authenticator-page.component';
+import { AuthenticatorRoutingModule } from './authenticator-routing.module';
+
+@NgModule({
+ imports: [ CommonModule, TranslateModule, AuthenticatorRoutingModule ],
+ declarations: [ AuthenticatorPageComponent ],
+ providers: [ ]
+})
+export class AuthenticatorModule {}
+
+
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/authenticator-page/authenticator-routing.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/authenticator-page/authenticator-routing.module.ts
new file mode 100644
index 0000000..47c4d73
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/authenticator-page/authenticator-routing.module.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { AuthenticatorPageComponent } from './authenticator-page.component';
+
+const routes: Routes = [
+ { path: '**', component: AuthenticatorPageComponent },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class AuthenticatorRoutingModule {}
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.css b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.css
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.html b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.html
new file mode 100644
index 0000000..27a4940
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.html
@@ -0,0 +1,68 @@
+<div class="cards-pf">
+ <div><h1 class="text-center">Welcome to Keycloak Account Management</h1></div>
+ <div class="container-fluid container-cards-pf">
+ <div class="row row-cards-pf">
+ <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3">
+ <div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
+ <div class="card-pf-body">
+ <div class="card-pf-top-element">
+ <span class="fa pficon-user card-pf-icon-circle"></span>
+ </div>
+ <h2 class="card-pf-title text-center">
+ Personal Info
+ </h2>
+ <h3 class="card-pf-info text-center">
+ More stuff goes here
+ </h3>
+ </div>
+ </div>
+ </div>
+ <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3">
+ <div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
+ <div class="card-pf-body">
+ <div class="card-pf-top-element">
+ <span class="fa fa-shield card-pf-icon-circle"></span>
+ </div>
+ <h2 class="card-pf-title text-center">
+ Account Security
+ </h2>
+ <h3 class="card-pf-info text-center">
+ More stuff goes here
+ </h3>
+ </div>
+ </div>
+ </div>
+ <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3">
+ <div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
+ <div class="card-pf-body">
+ <div class="card-pf-top-element">
+ <span class="fa fa-th card-pf-icon-circle"></span>
+ </div>
+ <h2 class="card-pf-title text-center">
+ Applications
+ </h2>
+ <h3 class="card-pf-info text-center">
+ More stuff goes here
+ </h3>
+ </div>
+ </div>
+ </div>
+ <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3">
+ <div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
+ <div class="card-pf-body">
+ <div class="card-pf-top-element">
+ <span class="fa pficon-repository card-pf-icon-circle"></span>
+ </div>
+ <h2 class="card-pf-title text-center">
+ My Resources
+ </h2>
+ <h3 class="card-pf-info text-center">
+ More stuff goes here
+ </h3>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.ts
new file mode 100644
index 0000000..1a0c781
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/home-page/home-page.component.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component} from '@angular/core';
+
+@Component({
+ selector: 'home-page',
+ templateUrl: './home-page.component.html',
+ styleUrls: ['./home-page.component.css']
+})
+export class HomePageComponent {
+
+ constructor() {}
+
+}
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/page-not-found/page-not-found.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/page-not-found/page-not-found.module.ts
new file mode 100644
index 0000000..9274bb1
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/page-not-found/page-not-found.module.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { TranslateModule } from '@ngx-translate/core';
+
+import { PageNotFoundComponent } from './page-not-found.component';
+import { PageNotFoundRoutingModule } from './page-not-found-routing.module';
+
+@NgModule({
+ imports: [ CommonModule, FormsModule, TranslateModule, PageNotFoundRoutingModule ],
+ declarations: [ PageNotFoundComponent ],
+ providers: [ ]
+})
+export class PageNotFoundModule {}
+
+
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/page-not-found/page-not-found-routing.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/page-not-found/page-not-found-routing.module.ts
new file mode 100644
index 0000000..a23ca00
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/page-not-found/page-not-found-routing.module.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { PageNotFoundComponent } from './page-not-found.component';
+
+const routes: Routes = [
+ { path: '**', component: PageNotFoundComponent}
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class PageNotFoundRoutingModule {}
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password.module.ts
new file mode 100644
index 0000000..9bd5d9d
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password.module.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { TranslateModule } from '@ngx-translate/core';
+
+import { PasswordPageComponent } from './password-page.component';
+import { PasswordRoutingModule } from './password-routing.module';
+
+@NgModule({
+ imports: [ CommonModule, FormsModule, TranslateModule, PasswordRoutingModule ],
+ declarations: [ PasswordPageComponent ],
+ providers: [ ]
+})
+export class PasswordModule {}
+
+
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-routing.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-routing.module.ts
new file mode 100644
index 0000000..3208b40
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/password-page/password-routing.module.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { PasswordPageComponent } from './password-page.component';
+
+const routes: Routes = [
+ { path: '**', component: PasswordPageComponent },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class PasswordRoutingModule {}
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions.module.ts
new file mode 100644
index 0000000..cec77c8
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions.module.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { TranslateModule } from '@ngx-translate/core';
+
+import {WidgetsModule} from '../widgets/widgets.module';
+
+import { SessionsRoutingModule } from './sessions-routing.module';
+
+import { SessionsPageComponent } from './sessions-page.component';
+import { LargeSessionCardComponent } from './large-session-card.component';
+import { SmallSessionCardComponent } from './small-session-card.component';
+
+@NgModule({
+ imports: [ CommonModule,
+ TranslateModule,
+ SessionsRoutingModule,
+ WidgetsModule ],
+ declarations: [ SessionsPageComponent,
+ LargeSessionCardComponent,
+ SmallSessionCardComponent ],
+ providers: [ ]
+})
+export class SessionsModule {}
+
+
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions-page.component.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions-page.component.ts
index 95a519a..2ffb989 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions-page.component.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions-page.component.ts
@@ -18,7 +18,7 @@ import {Component, OnInit} from '@angular/core';
import {Response} from '@angular/http';
import {AccountServiceClient} from '../../account-service/account.service';
- import {TranslateUtil} from '../../ngx-translate/translate.util';
+import {TranslateUtil} from '../../ngx-translate/translate.util';
import {View} from '../widgets/toolbar.component';
import {PropertyLabel} from '../widgets/property.label';
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions-routing.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions-routing.module.ts
new file mode 100644
index 0000000..da6e3ab
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/sessions-page/sessions-routing.module.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { SessionsPageComponent } from './sessions-page.component';
+
+const routes: Routes = [
+ { path: '**', component: SessionsPageComponent },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class SessionsRoutingModule {}
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/widgets/widgets.module.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/widgets/widgets.module.ts
new file mode 100644
index 0000000..422fd30
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/content/widgets/widgets.module.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { ToolbarComponent } from './toolbar.component';
+import {OrderbyPipe} from './orderby.pipe';
+import {FilterbyPipe} from './filterby.pipe';
+
+@NgModule({
+ imports: [ CommonModule, FormsModule ],
+ declarations: [ ToolbarComponent, OrderbyPipe, FilterbyPipe ],
+ exports: [ ToolbarComponent,
+ OrderbyPipe,
+ FilterbyPipe ],
+ providers: [ ]
+})
+export class WidgetsModule {}
+
+
+
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.guard.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.guard.ts
new file mode 100644
index 0000000..39239a1
--- /dev/null
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.guard.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Injectable} from '@angular/core';
+import {CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
+import {KeycloakService} from './keycloak.service';
+
+@Injectable()
+export class KeycloakGuard implements CanActivateChild {
+ constructor(private keycloakService: KeycloakService) {}
+
+ canActivateChild(route: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot): boolean {
+
+ if (this.keycloakService.authenticated()) {
+ return true;
+ }
+
+ this.keycloakService.login();
+ return false;
+ }
+}
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.js b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.js
index a784936..80b4477 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.js
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.js
@@ -40,6 +40,8 @@
}
}
+ var useNonce = true;
+
kc.init = function (initOptions) {
kc.authenticated = false;
@@ -50,7 +52,7 @@
} else if (initOptions && initOptions.adapter === 'default') {
adapter = loadAdapter();
} else {
- if (window.Cordova) {
+ if (window.Cordova || window.cordova) {
adapter = loadAdapter('cordova');
} else {
adapter = loadAdapter();
@@ -58,6 +60,10 @@
}
if (initOptions) {
+ if (typeof initOptions.useNonce !== 'undefined') {
+ useNonce = initOptions.useNonce;
+ }
+
if (typeof initOptions.checkLoginIframe !== 'undefined') {
loginIframe.enable = initOptions.checkLoginIframe;
}
@@ -159,10 +165,15 @@
var callback = parseCallback(window.location.href);
if (callback) {
- setupCheckLoginIframe();
window.history.replaceState({}, null, callback.newUrl);
- processCallback(callback, initPromise);
- return;
+ }
+
+ if (callback && callback.valid) {
+ return setupCheckLoginIframe().success(function() {
+ processCallback(callback, initPromise);
+ }).error(function (e) {
+ initPromise.setError();
+ });
} else if (initOptions) {
if (initOptions.token && initOptions.refreshToken) {
setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken);
@@ -221,7 +232,7 @@
var callbackState = {
state: state,
nonce: nonce,
- redirectUri: encodeURIComponent(redirectUri),
+ redirectUri: encodeURIComponent(redirectUri)
}
if (options && options.prompt) {
@@ -230,22 +241,25 @@
callbackStorage.add(callbackState);
- var action = 'auth';
+ var baseUrl;
if (options && options.action == 'register') {
- action = 'registrations';
+ baseUrl = kc.endpoints.register();
+ } else {
+ baseUrl = kc.endpoints.authorize();
}
var scope = (options && options.scope) ? "openid " + options.scope : "openid";
- var url = getRealmUrl()
- + '/protocol/openid-connect/' + action
+ var url = baseUrl
+ '?client_id=' + encodeURIComponent(kc.clientId)
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
+ '&state=' + encodeURIComponent(state)
- + '&nonce=' + encodeURIComponent(nonce)
+ '&response_mode=' + encodeURIComponent(kc.responseMode)
+ '&response_type=' + encodeURIComponent(kc.responseType)
+ '&scope=' + encodeURIComponent(scope);
+ if (useNonce) {
+ url = url + '&nonce=' + encodeURIComponent(nonce);
+ }
if (options && options.prompt) {
url += '&prompt=' + encodeURIComponent(options.prompt);
@@ -275,8 +289,7 @@
}
kc.createLogoutUrl = function(options) {
- var url = getRealmUrl()
- + '/protocol/openid-connect/logout'
+ var url = kc.endpoints.logout()
+ '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));
return url;
@@ -295,11 +308,14 @@
}
kc.createAccountUrl = function(options) {
- var url = getRealmUrl()
+ var realm = getRealmUrl();
+ var url = undefined;
+ if (typeof realm !== 'undefined') {
+ url = realm
+ '/account'
+ '?referrer=' + encodeURIComponent(kc.clientId)
+ '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options));
-
+ }
return url;
}
@@ -347,7 +363,7 @@
}
kc.loadUserInfo = function() {
- var url = getRealmUrl() + '/protocol/openid-connect/userinfo';
+ var url = kc.endpoints.userinfo();
var req = new XMLHttpRequest();
req.open('GET', url, true);
req.setRequestHeader('Accept', 'application/json');
@@ -412,7 +428,7 @@
promise.setSuccess(false);
} else {
var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
- var url = getRealmUrl() + '/protocol/openid-connect/token';
+ var url = kc.endpoints.token();
refreshQueue.push(promise);
@@ -486,10 +502,14 @@
}
function getRealmUrl() {
- if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') {
- return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm);
+ if (typeof kc.authServerUrl !== 'undefined') {
+ if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') {
+ return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm);
+ } else {
+ return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm);
+ }
} else {
- return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm);
+ return undefined;
}
}
@@ -523,7 +543,7 @@
if ((kc.flow != 'implicit') && code) {
var params = 'code=' + code + '&grant_type=authorization_code';
- var url = getRealmUrl() + '/protocol/openid-connect/token';
+ var url = kc.endpoints.token();
var req = new XMLHttpRequest();
req.open('POST', url, true);
@@ -560,9 +580,9 @@
setToken(accessToken, refreshToken, idToken, timeLocal);
- if ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) ||
+ if (useNonce && ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) ||
(kc.refreshTokenParsed && kc.refreshTokenParsed.nonce != oauth.storedNonce) ||
- (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce)) {
+ (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce))) {
console.info('[KEYCLOAK] Invalid nonce, clearing token');
kc.clearToken();
@@ -587,6 +607,65 @@
configUrl = config;
}
+ function setupOidcEndoints(oidcConfiguration) {
+ if (! oidcConfiguration) {
+ kc.endpoints = {
+ authorize: function() {
+ return getRealmUrl() + '/protocol/openid-connect/auth';
+ },
+ token: function() {
+ return getRealmUrl() + '/protocol/openid-connect/token';
+ },
+ logout: function() {
+ return getRealmUrl() + '/protocol/openid-connect/logout';
+ },
+ checkSessionIframe: function() {
+ var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';
+ if (kc.iframeVersion) {
+ src = src + '?version=' + kc.iframeVersion;
+ }
+ return src;
+ },
+ register: function() {
+ return getRealmUrl() + '/protocol/openid-connect/registrations';
+ },
+ userinfo: function() {
+ return getRealmUrl() + '/protocol/openid-connect/userinfo';
+ }
+ };
+ } else {
+ kc.endpoints = {
+ authorize: function() {
+ return oidcConfiguration.authorization_endpoint;
+ },
+ token: function() {
+ return oidcConfiguration.token_endpoint;
+ },
+ logout: function() {
+ if (!oidcConfiguration.end_session_endpoint) {
+ throw "Not supported by the OIDC server";
+ }
+ return oidcConfiguration.end_session_endpoint;
+ },
+ checkSessionIframe: function() {
+ if (!oidcConfiguration.check_session_iframe) {
+ throw "Not supported by the OIDC server";
+ }
+ return oidcConfiguration.check_session_iframe;
+ },
+ register: function() {
+ throw 'Redirection to "Register user" page not supported in standard OIDC mode';
+ },
+ userinfo: function() {
+ if (!oidcConfiguration.userinfo_endpoint) {
+ throw "Not supported by the OIDC server";
+ }
+ return oidcConfiguration.userinfo_endpoint;
+ }
+ }
+ }
+ }
+
if (configUrl) {
var req = new XMLHttpRequest();
req.open('GET', configUrl, true);
@@ -601,7 +680,7 @@
kc.realm = config['realm'];
kc.clientId = config['resource'];
kc.clientSecret = (config['credentials'] || {})['secret'];
-
+ setupOidcEndoints(null);
promise.setSuccess();
} else {
promise.setError();
@@ -611,30 +690,62 @@
req.send();
} else {
- if (!config['url']) {
- var scripts = document.getElementsByTagName('script');
- for (var i = 0; i < scripts.length; i++) {
- if (scripts[i].src.match(/.*keycloak\.js/)) {
- config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js'));
- break;
- }
- }
- }
-
- if (!config.realm) {
- throw 'realm missing';
- }
-
if (!config.clientId) {
throw 'clientId missing';
}
- kc.authServerUrl = config.url;
- kc.realm = config.realm;
kc.clientId = config.clientId;
kc.clientSecret = (config.credentials || {}).secret;
- promise.setSuccess();
+ var oidcProvider = config['oidcProvider'];
+ if (!oidcProvider) {
+ if (!config['url']) {
+ var scripts = document.getElementsByTagName('script');
+ for (var i = 0; i < scripts.length; i++) {
+ if (scripts[i].src.match(/.*keycloak\.js/)) {
+ config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js'));
+ break;
+ }
+ }
+ }
+ if (!config.realm) {
+ throw 'realm missing';
+ }
+
+ kc.authServerUrl = config.url;
+ kc.realm = config.realm;
+ setupOidcEndoints(null);
+ promise.setSuccess();
+ } else {
+ if (typeof oidcProvider === 'string') {
+ var oidcProviderConfigUrl;
+ if (oidcProvider.charAt(oidcProvider.length - 1) == '/') {
+ oidcProviderConfigUrl = oidcProvider + '.well-known/openid-configuration';
+ } else {
+ oidcProviderConfigUrl = oidcProvider + '/.well-known/openid-configuration';
+ }
+ var req = new XMLHttpRequest();
+ req.open('GET', oidcProviderConfigUrl, true);
+ req.setRequestHeader('Accept', 'application/json');
+
+ req.onreadystatechange = function () {
+ if (req.readyState == 4) {
+ if (req.status == 200 || fileLoaded(req)) {
+ var oidcProviderConfig = JSON.parse(req.responseText);
+ setupOidcEndoints(oidcProviderConfig);
+ promise.setSuccess();
+ } else {
+ promise.setError();
+ }
+ }
+ };
+
+ req.send();
+ } else {
+ setupOidcEndoints(oidcProvider);
+ promise.setSuccess();
+ }
+ }
}
return promise.promise;
@@ -753,23 +864,137 @@
}
function parseCallback(url) {
- var oauth = new CallbackParser(url, kc.responseMode).parseUri();
+ var oauth = parseCallbackUrl(url);
+ if (!oauth) {
+ return;
+ }
+
var oauthState = callbackStorage.get(oauth.state);
- if (oauthState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token)) {
+ if (oauthState) {
+ oauth.valid = true;
oauth.redirectUri = oauthState.redirectUri;
oauth.storedNonce = oauthState.nonce;
oauth.prompt = oauthState.prompt;
+ }
- if (oauth.fragment) {
- oauth.newUrl += '#' + oauth.fragment;
+ return oauth;
+ }
+
+ function parseCallbackUrl(url) {
+ var supportedParams;
+ switch (kc.flow) {
+ case 'standard':
+ supportedParams = ['code', 'state', 'session_state'];
+ break;
+ case 'implicit':
+ supportedParams = ['access_token', 'id_token', 'state', 'session_state'];
+ break;
+ case 'hybrid':
+ supportedParams = ['access_token', 'id_token', 'code', 'state', 'session_state'];
+ break;
+ }
+
+ supportedParams.push('error');
+ supportedParams.push('error_description');
+ supportedParams.push('error_uri');
+
+ var queryIndex = url.indexOf('?');
+ var fragmentIndex = url.indexOf('#');
+
+ var newUrl;
+ var parsed;
+
+ if (kc.responseMode === 'query' && queryIndex !== -1) {
+ newUrl = url.substring(0, queryIndex);
+ parsed = parseCallbackParams(url.substring(queryIndex + 1, fragmentIndex !== -1 ? fragmentIndex : url.length), supportedParams);
+ if (parsed.paramsString !== '') {
+ newUrl += '?' + parsed.paramsString;
+ }
+ if (fragmentIndex !== -1) {
+ newUrl += url.substring(fragmentIndex);
}
+ } else if (kc.responseMode === 'fragment' && fragmentIndex !== -1) {
+ newUrl = url.substring(0, fragmentIndex);
+ parsed = parseCallbackParams(url.substring(fragmentIndex + 1), supportedParams);
+ if (parsed.paramsString !== '') {
+ newUrl += '#' + parsed.paramsString;
+ }
+ }
- return oauth;
+ if (parsed && parsed.oauthParams) {
+ if (kc.flow === 'standard' || kc.flow === 'hybrid') {
+ if ((parsed.oauthParams.code || parsed.oauthParams.error) && parsed.oauthParams.state) {
+ parsed.oauthParams.newUrl = newUrl;
+ return parsed.oauthParams;
+ }
+ } else if (kc.flow === 'implicit') {
+ if ((parsed.oauthParams.access_token || parsed.oauthParams.error) && parsed.oauthParams.state) {
+ parsed.oauthParams.newUrl = newUrl;
+ return parsed.oauthParams;
+ }
+ }
}
}
+ function parseCallbackParams(paramsString, supportedParams) {
+ var p = paramsString.split('&');
+ var result = {
+ paramsString: '',
+ oauthParams: {}
+ }
+ for (var i = 0; i < p.length; i++) {
+ var t = p[i].split('=');
+ if (supportedParams.indexOf(t[0]) !== -1) {
+ result.oauthParams[t[0]] = t[1];
+ } else {
+ if (result.paramsString !== '') {
+ result.paramsString += '&';
+ }
+ result.paramsString += p[i];
+ }
+ }
+ return result;
+ }
+
function createPromise() {
+ if (typeof Promise === "function") {
+ return createNativePromise();
+ } else {
+ return createLegacyPromise();
+ }
+ }
+
+ function createNativePromise() {
+ // Need to create a native Promise which also preserves the
+ // interface of the custom promise type previously used by the API
+ var p = {
+ setSuccess: function(result) {
+ p.success = true;
+ p.resolve(result);
+ },
+
+ setError: function(result) {
+ p.success = false;
+ p.reject(result);
+ }
+ };
+ p.promise = new Promise(function(resolve, reject) {
+ p.resolve = resolve;
+ p.reject = reject;
+ });
+ p.promise.success = function(callback) {
+ p.promise.then(callback);
+ return p.promise;
+ }
+ p.promise.error = function(callback) {
+ p.promise.catch(callback);
+ return p.promise;
+ }
+ return p;
+ }
+
+ function createLegacyPromise() {
var p = {
setSuccess: function(result) {
p.success = true;
@@ -826,23 +1051,20 @@
loginIframe.iframe = iframe;
iframe.onload = function() {
- var realmUrl = getRealmUrl();
- if (realmUrl.charAt(0) === '/') {
+ var authUrl = kc.endpoints.authorize();
+ if (authUrl.charAt(0) === '/') {
loginIframe.iframeOrigin = getOrigin();
} else {
- loginIframe.iframeOrigin = realmUrl.substring(0, realmUrl.indexOf('/', 8));
+ loginIframe.iframeOrigin = authUrl.substring(0, authUrl.indexOf('/', 8));
}
promise.setSuccess();
setTimeout(check, loginIframe.interval * 1000);
}
- var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';
- if (kc.iframeVersion) {
- src = src + '?version=' + kc.iframeVersion;
- }
-
+ var src = kc.endpoints.checkSessionIframe();
iframe.setAttribute('src', src );
+ iframe.setAttribute('title', 'keycloak-session-iframe' );
iframe.style.display = 'none';
document.body.appendChild(iframe);
@@ -920,7 +1142,12 @@
},
accountManagement : function() {
- window.location.href = kc.createAccountUrl();
+ var accountUrl = kc.createAccountUrl();
+ if (typeof accountUrl !== 'undefined') {
+ window.location.href = accountUrl;
+ } else {
+ throw "Not supported by the OIDC server";
+ }
return createPromise().promise;
},
@@ -934,12 +1161,7 @@
} else if (kc.redirectUri) {
return kc.redirectUri;
} else {
- var redirectUri = location.href;
- if (location.hash && encodeHash) {
- redirectUri = redirectUri.substring(0, location.href.indexOf('#'));
- redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'redirect_fragment=' + encodeURIComponent(location.hash.substring(1));
- }
- return redirectUri;
+ return location.href;
}
}
};
@@ -947,7 +1169,14 @@
if (type == 'cordova') {
loginIframe.enable = false;
-
+ var cordovaOpenWindowWrapper = function(loginUrl, target, options) {
+ if (window.cordova && window.cordova.InAppBrowser) {
+ // Use inappbrowser for IOS and Android if available
+ return window.cordova.InAppBrowser.open(loginUrl, target, options);
+ } else {
+ return window.open(loginUrl, target, options);
+ }
+ };
return {
login: function(options) {
var promise = createPromise();
@@ -958,8 +1187,7 @@
}
var loginUrl = kc.createLoginUrl(options);
- var ref = window.open(loginUrl, '_blank', o);
-
+ var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', o);
var completed = false;
ref.addEventListener('loadstart', function(event) {
@@ -992,7 +1220,7 @@
var promise = createPromise();
var logoutUrl = kc.createLogoutUrl(options);
- var ref = window.open(logoutUrl, '_blank', 'location=no,hidden=yes');
+ var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes');
var error;
@@ -1025,7 +1253,7 @@
register : function() {
var registerUrl = kc.createRegisterUrl();
- var ref = window.open(registerUrl, '_blank', 'location=no');
+ var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', 'location=no');
ref.addEventListener('loadstart', function(event) {
if (event.url.indexOf('http://localhost') == 0) {
ref.close();
@@ -1035,12 +1263,16 @@
accountManagement : function() {
var accountUrl = kc.createAccountUrl();
- var ref = window.open(accountUrl, '_blank', 'location=no');
- ref.addEventListener('loadstart', function(event) {
- if (event.url.indexOf('http://localhost') == 0) {
- ref.close();
- }
- });
+ if (typeof accountUrl !== 'undefined') {
+ var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no');
+ ref.addEventListener('loadstart', function(event) {
+ if (event.url.indexOf('http://localhost') == 0) {
+ ref.close();
+ }
+ });
+ } else {
+ throw "Not supported by the OIDC server";
+ }
},
redirectUri: function(options) {
@@ -1170,99 +1402,6 @@
return new CookieStorage();
}
-
- 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', 'state', 'error', 'error_description' ];
-
- 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;
- default:
- if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) {
- oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + encodeURIComponent(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/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.service.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.service.ts
index 54f2eaa..97a7407 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.service.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/keycloak-service/keycloak.service.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import {Injectable} from '@angular/core';
+import {KeycloakLoginOptions} from './keycloak.d';
// If using a local keycloak.js, uncomment this import. With keycloak.js fetched
// from the server, you get a compile-time warning on use of the Keycloak()
@@ -23,7 +24,7 @@ import {Injectable} from '@angular/core';
//
import * as Keycloak from './keycloak';
-type KeycloakClient = Keycloak.KeycloakInstance;
+export type KeycloakClient = Keycloak.KeycloakInstance;
type InitOptions = Keycloak.KeycloakInitOptions;
@Injectable()
@@ -52,17 +53,21 @@ export class KeycloakService {
});
});
}
+
+ static setKeycloakAuth(kc:KeycloakClient) {
+ this.keycloakAuth = kc;
+ }
authenticated(): boolean {
return KeycloakService.keycloakAuth.authenticated;
}
- login() {
- KeycloakService.keycloakAuth.login();
+ login(options?: KeycloakLoginOptions) {
+ KeycloakService.keycloakAuth.login(options);
}
- logout() {
- KeycloakService.keycloakAuth.logout();
+ logout(redirectUri?: string) {
+ KeycloakService.keycloakAuth.logout({redirectUri: redirectUri});
}
account() {
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.html b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.html
index 4b30962..0669b26 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.html
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.html
@@ -1,17 +1,17 @@
-
+<!-- Top Nav -->
<nav class="navbar navbar-pf-alt">
<notification></notification>
<div class="navbar-header">
- <button (click)="menuClicked()" type="button" class="navbar-toggle">
+ <button *ngIf="keycloakService.authenticated() && showSideNav" (click)="menuClicked()" type="button" class="navbar-toggle">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="http://www.keycloak.org" class="navbar-brand">
- <img class="navbar-brand-icon" type="image/svg+xml" src="{{resourceUrl}}/app/assets/img/keycloak-logo.png" alt="" width="auto" height="30px"/>
+ <img class="navbar-brand-icon" type="image/svg+xml" src="{{resourceUrl}}/app/assets/img/keycloak-logo-min.png" alt="" width="auto" height="30px"/>
</a>
</div>
<nav class="collapse navbar-collapse">
@@ -21,17 +21,7 @@
<li *ngIf="referrer.exists()">
<a class="nav-item-iconic" href="{{referrer.getUri()}}"><span class="pficon-arrow"></span> {{'backTo' | translate:referrer.getName()}}</a>
</li>
- <li class="dropdown">
- <a class="dropdown-toggle nav-item-iconic" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
- <span title="Help" class="fa pficon-help"></span>
- <span class="caret"></span>
- </a>
- <ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
- <li><a href="#">Help</a></li>
- <li><a href="#">About</a></li>
- </ul>
- </li>
- <li class="dropdown">
+ <li class="dropdown" (click)="logout()">
<a class="dropdown-toggle nav-item-iconic" id="dropdownMenu2" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span title="Username" class="fa pficon-user"></span>
<span class="caret"></span>
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.ts
index 88a0c14..65f3ea0 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/app/top-nav/top-nav.component.ts
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {Component, OnInit} from '@angular/core';
+import {Component, OnInit, Input} from '@angular/core';
import {TranslateUtil} from '../ngx-translate/translate.util';
import {KeycloakService} from '../keycloak-service/keycloak.service';
import {ResponsivenessService} from "../responsiveness-service/responsiveness.service";
@@ -30,6 +30,7 @@ declare const referrer_uri: string;
styleUrls: ['./top-nav.component.css']
})
export class TopNavComponent implements OnInit {
+ @Input() showSideNav: String;
public resourceUrl: string = resourceUrl;
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/main.ts b/themes/src/main/resources/theme/keycloak-preview/account/resources/main.ts
index b5db7b0..ded194f 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/main.ts
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/main.ts
@@ -21,7 +21,8 @@ import { platformBrowser } from '@angular/platform-browser';
import { AppModule } from './app/app.module';
//import { environment } from './environments/environment';
-import { KeycloakService } from './app/keycloak-service/keycloak.service';
+import { KeycloakService, KeycloakClient } from './app/keycloak-service/keycloak.service';
+
//if (environment.production) {
// enableProdMode();
@@ -30,17 +31,17 @@ import { KeycloakService } from './app/keycloak-service/keycloak.service';
declare const authUrl: string;
declare const resourceUrl: string;
declare const realm: string;
+declare const keycloak: KeycloakClient;
+
+KeycloakService.setKeycloakAuth(keycloak);
+
+loadCss('/styles.css');
+platformBrowserDynamic().bootstrapModule(AppModule);
-const noLogin: boolean = false; // convenient for development
-if (noLogin) {
- platformBrowserDynamic().bootstrapModule(AppModule);
-} else {
- KeycloakService.init(authUrl + '/realms/' + realm + '/account/keycloak.json',
- {onLoad: 'login-required'})
- .then(() => {
- platformBrowserDynamic().bootstrapModule(AppModule);
- })
- .catch((e: any) => {
- console.log('Error in bootstrap: ' + JSON.stringify(e));
- });
+function loadCss(url:string) {
+ const link = document.createElement("link");
+ link.href = resourceUrl + url;
+ link.rel = "stylesheet";
+ link.media = "screen, print";
+ document.head.appendChild(link);
}
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/package.json b/themes/src/main/resources/theme/keycloak-preview/account/resources/package.json
index 1ea8c3c..d2d733a 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/package.json
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/package.json
@@ -24,18 +24,22 @@
"author": "Stan Silvert",
"license": "Apache 2.0",
"dependencies": {
- "@angular/common": "~4.0.0",
- "@angular/compiler": "~4.0.0",
- "@angular/core": "~4.0.0",
- "@angular/forms": "~4.0.0",
- "@angular/http": "~4.0.0",
- "@angular/platform-browser": "~4.0.0",
- "@angular/platform-browser-dynamic": "~4.0.0",
- "@angular/router": "~4.0.0",
- "@ngx-translate/core": "^7.1.0",
+ "@angular/animations": "'5.0.0'",
+ "@angular/common": "'5.0.0'",
+ "@angular/compiler": "'5.0.0'",
+ "@angular/compiler-cli": "'5.0.0'",
+ "@angular/core": "'5.0.0'",
+ "@angular/forms": "'5.0.0'",
+ "@angular/http": "'5.0.0'",
+ "@angular/platform-browser": "'5.0.0'",
+ "@angular/platform-browser-dynamic": "'5.0.0'",
+ "@angular/platform-server": "'5.0.0'",
+ "@angular/router": "'5.0.0'",
+ "@ngx-translate/core": "^9.1.1",
"core-js": "^2.4.1",
"patternfly": "^3.23.2",
- "rxjs": "^5.4.2",
+ "rxjs": "5.5.2",
+ "rxjs-system-bundle": "^5.5.6",
"systemjs": "^0.20.17",
"zone.js": "^0.8.4"
},
@@ -55,7 +59,7 @@
"protractor": "~4.0.14",
"rimraf": "^2.5.4",
"tslint": "^3.15.1",
- "typescript": "^2.4.2"
+ "typescript": "2.4.2"
},
"repository": {}
}
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css b/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css
index 58e1a7d..e69de29 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/styles.css
@@ -1,5 +0,0 @@
-h1 {
- color: #369;
- font-family: Arial, Helvetica, sans-serif;
- font-size: 250%;
-}
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js b/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js
index 7814857..6a100cd 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/systemjs.config.js
@@ -14,20 +14,32 @@
'app': resourceUrl + '/app',
// angular bundles
- '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
- '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
- '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
- '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
- '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
- '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
- '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
- '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
+ '@angular/core': 'npm:@angular/core/bundles/core.umd.min.js',
+ '@angular/common': 'npm:@angular/common/bundles/common.umd.min.js',
+ '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.min.js',
+ '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.min.js',
+ '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.min.js',
+ '@angular/http': 'npm:@angular/http/bundles/http.umd.min.js',
+ '@angular/router': 'npm:@angular/router/bundles/router.umd.min.js',
+ '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.min.js',
// other libraries
- '@ngx-translate/core': 'npm:@ngx-translate/core/bundles/core.umd.js',
- 'rxjs': 'npm:rxjs',
+ '@ngx-translate/core': 'npm:@ngx-translate/core/bundles/core.umd.min.js',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
},
+ bundles: {
+ "npm:rxjs-system-bundle/Rx.system.min.js": [
+ "rxjs",
+ "rxjs/*",
+ "rxjs/operator/*",
+ "rxjs/observable/*",
+ "rxjs/scheduler/*",
+ "rxjs/symbol/*",
+ "rxjs/add/operator/*",
+ "rxjs/add/observable/*",
+ "rxjs/util/*"
+ ]
+ },
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
@@ -42,7 +54,7 @@
defaultExtension: 'js'
},
rxjs: {
- defaultExtension: 'js'
+ defaultExtension: false
}
}
});
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/resources/yarn.lock b/themes/src/main/resources/theme/keycloak-preview/account/resources/yarn.lock
index c335da6..4bdfbbf 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/resources/yarn.lock
+++ b/themes/src/main/resources/theme/keycloak-preview/account/resources/yarn.lock
@@ -2,41 +2,80 @@
# yarn lockfile v1
-"@angular/common@~4.0.0":
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/@angular/common/-/common-4.0.3.tgz#17472895eb425f2812b3a79162b5b494d2506a5b"
+"@angular/animations@'5.0.0'":
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-5.2.7.tgz#a99fe128f3809e3f8082441a6e676992b9352db7"
+ dependencies:
+ tslib "^1.7.1"
+
+"@angular/common@'5.0.0'":
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/@angular/common/-/common-5.2.7.tgz#11bb9f00afe91af8d772ecdff83c2e179f9a67a0"
+ dependencies:
+ tslib "^1.7.1"
+
+"@angular/compiler-cli@'5.0.0'":
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-5.2.7.tgz#3bad9d5e4c25ebc51e1bb67b71b26a97d6d3bbc5"
+ dependencies:
+ chokidar "^1.4.2"
+ minimist "^1.2.0"
+ reflect-metadata "^0.1.2"
+ tsickle "^0.27.2"
+
+"@angular/compiler@'5.0.0'":
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-5.2.7.tgz#31dcc7ac18cc23cb115e76c541b982737ff93ec9"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/compiler@~4.0.0":
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-4.0.3.tgz#8b3cad338ac539328e10a6a4bfaa057094f7bc89"
+"@angular/core@'5.0.0'":
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/@angular/core/-/core-5.2.7.tgz#e5607fc39d90f9fe4fbaaeeeb6cdb9371966bfe3"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/core@~4.0.0":
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/@angular/core/-/core-4.0.3.tgz#61be21db6aa5778e33159ffd38cbbebaf16120d9"
+"@angular/forms@'5.0.0'":
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-5.2.7.tgz#a0c41fdb6b8ba2c32653cc3058d5fa47db91fbf8"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/forms@~4.0.0":
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-4.0.3.tgz#fb8e6e0aede782bf58730a31d1179b323271816e"
+"@angular/http@'5.0.0'":
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/@angular/http/-/http-5.2.7.tgz#a163f6958f12d2665419123861b0d613c1c82afb"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/http@~4.0.0":
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/@angular/http/-/http-4.0.3.tgz#efbb701a215ec7704c021676484b85ed47392f4b"
+"@angular/platform-browser-dynamic@'5.0.0'":
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.2.7.tgz#91f903c1f73de2fba1004bc31ed1c3117c7f0406"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/platform-browser-dynamic@~4.0.0":
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.0.3.tgz#5fa3b98f725999b631d7d7174e6e43dcbf6aa9ac"
+"@angular/platform-browser@'5.0.0'":
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-5.2.7.tgz#cfd86040c25380aabf0322ef47aecc61f23cc532"
+ dependencies:
+ tslib "^1.7.1"
-"@angular/platform-browser@~4.0.0":
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-4.0.3.tgz#170b18d5af4ee02b248aa6a1f1e0584ac841681e"
+"@angular/platform-server@'5.0.0'":
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-5.2.7.tgz#e2eb290952dbe6ab04b4edae115a0d02b8b2dc4c"
+ dependencies:
+ domino "^1.0.29"
+ tslib "^1.7.1"
+ xhr2 "^0.1.4"
-"@angular/router@~4.0.0":
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/@angular/router/-/router-4.0.3.tgz#24184e9b1266c4ad017b2be81573464b1e4c5dfa"
+"@angular/router@'5.0.0'":
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/@angular/router/-/router-5.2.7.tgz#691c375fe32f01bea56d169469ad8f254b136af9"
+ dependencies:
+ tslib "^1.7.1"
-"@ngx-translate/core@^7.1.0":
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/@ngx-translate/core/-/core-7.1.0.tgz#5087a65c8ff312e4244ca0646ed45cde83b170cd"
+"@ngx-translate/core@^9.1.1":
+ version "9.1.1"
+ resolved "https://registry.yarnpkg.com/@ngx-translate/core/-/core-9.1.1.tgz#ae103928836b8a9e069fd2e2e76fa2198cc7e628"
"@types/jasmine@2.5.36", "@types/jasmine@^2.5.36":
version "2.5.36"
@@ -471,7 +510,7 @@ chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
-chokidar@1.7.0, chokidar@^1.4.1:
+chokidar@1.7.0, chokidar@^1.4.1, chokidar@^1.4.2:
version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
dependencies:
@@ -769,6 +808,10 @@ dom-serialize@^2.2.0:
extend "^3.0.0"
void-elements "^2.0.0"
+domino@^1.0.29:
+ version "1.0.30"
+ resolved "https://registry.yarnpkg.com/domino/-/domino-1.0.30.tgz#54a4154ecae968616680f8feba3cedff355c71f4"
+
drmonty-datatables-colvis@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/drmonty-datatables-colvis/-/drmonty-datatables-colvis-1.1.2.tgz#96ab9edfb48643cc2edda3f87b88933cdee8127c"
@@ -2196,6 +2239,10 @@ readdirp@^2.0.0:
readable-stream "^2.0.2"
set-immediate-shim "^1.0.1"
+reflect-metadata@^0.1.2:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2"
+
regex-cache@^0.4.2:
version "0.4.3"
resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145"
@@ -2310,9 +2357,13 @@ rx@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
-rxjs@^5.4.2:
- version "5.4.2"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.2.tgz#2a3236fcbf03df57bae06fd6972fd99e5c08fcf7"
+rxjs-system-bundle@^5.5.6:
+ version "5.5.6"
+ resolved "https://registry.yarnpkg.com/rxjs-system-bundle/-/rxjs-system-bundle-5.5.6.tgz#3af9c02a36938f750fe33751cca2397fcf3d04b8"
+
+rxjs@5.5.2:
+ version "5.5.2"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.2.tgz#28d403f0071121967f18ad665563255d54236ac3"
dependencies:
symbol-observable "^1.0.1"
@@ -2497,6 +2548,12 @@ socket.io@1.7.3:
socket.io-client "1.7.3"
socket.io-parser "2.3.1"
+source-map-support@^0.5.0:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.3.tgz#2b3d5fff298cfa4d1afd7d4352d569e9a0158e76"
+ dependencies:
+ source-map "^0.6.0"
+
source-map-support@~0.4.0:
version "0.4.15"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1"
@@ -2507,6 +2564,10 @@ source-map@^0.5.3, source-map@^0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
+source-map@^0.6.0:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
spawn-command@^0.0.2-1:
version "0.0.2"
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e"
@@ -2614,8 +2675,8 @@ supports-color@^3.2.3:
has-flag "^1.0.0"
symbol-observable@^1.0.1:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
systemjs@^0.20.17:
version "0.20.17"
@@ -2673,6 +2734,19 @@ tree-kill@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.1.0.tgz#c963dcf03722892ec59cba569e940b71954d1729"
+tsickle@^0.27.2:
+ version "0.27.2"
+ resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.27.2.tgz#f33d46d046f73dd5c155a37922e422816e878736"
+ dependencies:
+ minimist "^1.2.0"
+ mkdirp "^0.5.1"
+ source-map "^0.6.0"
+ source-map-support "^0.5.0"
+
+tslib@^1.7.1:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8"
+
tslint@^3.15.1:
version "3.15.1"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-3.15.1.tgz#da165ca93d8fdc2c086b51165ee1bacb48c98ea5"
@@ -2706,7 +2780,7 @@ type-is@~1.6.14:
media-typer "0.3.0"
mime-types "~2.1.15"
-typescript@^2.4.2:
+typescript@2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.2.tgz#f8395f85d459276067c988aa41837a8f82870844"
@@ -2868,6 +2942,10 @@ wtf-8@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a"
+xhr2@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"
+
xml2js@0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.4.tgz#3111010003008ae19240eba17497b57c729c555d"
diff --git a/themes/src/main/resources/theme/keycloak-preview/account/theme.properties b/themes/src/main/resources/theme/keycloak-preview/account/theme.properties
index 4cd0e08..fb2e245 100644
--- a/themes/src/main/resources/theme/keycloak-preview/account/theme.properties
+++ b/themes/src/main/resources/theme/keycloak-preview/account/theme.properties
@@ -1,3 +1,2 @@
parent=base
-import=login/keycloak
deprecatedMode=false
\ No newline at end of file