Compare commits
165
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e93c4724b | ||
|
|
8261ffeeeb | ||
|
|
1e438cdc45 | ||
|
|
d70859ef1b | ||
|
|
3ab8ff2ea1 | ||
|
|
adea414695 | ||
|
|
cace03c3cc | ||
|
|
d55b1d7259 | ||
|
|
78e963688c | ||
|
|
b2df872ad4 | ||
|
|
fea4201f46 | ||
|
|
073a2f4d65 | ||
|
|
06df678d33 | ||
|
|
dd65c231f9 | ||
|
|
ae690e0679 | ||
|
|
06f108df3b | ||
|
|
a8319d1131 | ||
|
|
f800290d89 | ||
|
|
e79de47f5e | ||
|
|
08c22416a2 | ||
|
|
dac5d313b3 | ||
|
|
21d139c6c2 | ||
|
|
e5d997a6c0 | ||
|
|
a2e54dc94f | ||
|
|
28040109ac | ||
|
|
39b6bf62ba | ||
|
|
6a19a8fb7e | ||
|
|
e914b6fe8d | ||
|
|
e5a6dbd77a | ||
|
|
343dc05f82 | ||
|
|
76076cdb3c | ||
|
|
487539542a | ||
|
|
1634bef28a | ||
|
|
6882f7a9c0 | ||
|
|
943c8507f7 | ||
|
|
99119562bb | ||
|
|
5cbe595fe3 | ||
|
|
eb8257a52e | ||
|
|
27d8afe4a7 | ||
|
|
88f21eae87 | ||
|
|
1258923a0d | ||
|
|
344286e037 | ||
|
|
3ced81a2c2 | ||
|
|
032c48fb25 | ||
|
|
f6125a2542 | ||
|
|
49a03a86fb | ||
|
|
e84acd9898 | ||
|
|
8022a74565 | ||
|
|
fdc6fc59b8 | ||
|
|
f3340b113f | ||
|
|
20f24bffc4 | ||
|
|
7ebcc69cb9 | ||
|
|
90e5c7f3eb | ||
|
|
81918713dd | ||
|
|
9505925363 | ||
|
|
6e123bcea2 | ||
|
|
ddceaaf3d5 | ||
|
|
a3fb0a5ac5 | ||
|
|
1ef5379648 | ||
|
|
5ec50461ee | ||
|
|
e960642399 | ||
|
|
2bab2acf5b | ||
|
|
5a56a822b0 | ||
|
|
35154db50f | ||
|
|
fe2ae6ec68 | ||
|
|
b249a48dcf | ||
|
|
035ebc881a | ||
|
|
8b5fd2b4ac | ||
|
|
e813fcd9c8 | ||
|
|
634e7170e3 | ||
|
|
b6a0303a4c | ||
|
|
824b900a43 | ||
|
|
49a83db2e9 | ||
|
|
af7e42d640 | ||
|
|
9183cba02e | ||
|
|
c3d297dd05 | ||
|
|
527d6ca4d8 | ||
|
|
5e4d173f1d | ||
|
|
c3c1a0fb4e | ||
|
|
7b8c691402 | ||
|
|
a1e7351072 | ||
|
|
7efa45126c | ||
|
|
c12cd7fd57 | ||
|
|
4ddff9ee16 | ||
|
|
615cb24cce | ||
|
|
3bef6d5066 | ||
|
|
e8a07c9a6c | ||
|
|
3041499cd4 | ||
|
|
03590f117e | ||
|
|
c1311e4619 | ||
|
|
9a94004fc9 | ||
|
|
095fec95e5 | ||
|
|
b78f0aa9db | ||
|
|
fac3118b0a | ||
|
|
cf056b3464 | ||
|
|
9ed221b168 | ||
|
|
e12752f9ac | ||
|
|
70b3f6bd92 | ||
|
|
5022015e4a | ||
|
|
f41a7000de | ||
|
|
a67da7bc59 | ||
|
|
943bd9e48e | ||
|
|
2b14469a58 | ||
|
|
2be6dec238 | ||
|
|
e1f5245145 | ||
|
|
b3513e3203 | ||
|
|
ffd9d957f4 | ||
|
|
3cea54e3a6 | ||
|
|
ef4223858b | ||
|
|
5514812a4d | ||
|
|
87abe5e648 | ||
|
|
7e3703b2bf | ||
|
|
f652b13520 | ||
|
|
baf6b96db6 | ||
|
|
701c318b60 | ||
|
|
b3b81d6a76 | ||
|
|
8a5428808e | ||
|
|
a939c45d58 | ||
|
|
0b2fe75828 | ||
|
|
28d0ab9da8 | ||
|
|
103a3c4ca7 | ||
|
|
5c52da80c6 | ||
|
|
8fe10b3331 | ||
|
|
04a72b9608 | ||
|
|
7280234422 | ||
|
|
4078e84fb6 | ||
|
|
06f32a47ec | ||
|
|
f4a5e49b63 | ||
|
|
5d74b776d6 | ||
|
|
5cae1bb134 | ||
|
|
8d3dc790df | ||
|
|
f5bacb79c1 | ||
|
|
4a425c2674 | ||
|
|
c38b6d585e | ||
|
|
ad5f3fefc5 | ||
|
|
707b80f436 | ||
|
|
80feb67fc2 | ||
|
|
aae63de755 | ||
|
|
e9e376419d | ||
|
|
ffeb0420bf | ||
|
|
a63bb44ba2 | ||
|
|
91135c95ae | ||
|
|
bff7831912 | ||
|
|
66186273ca | ||
|
|
07fea02146 | ||
|
|
593f57fd2c | ||
|
|
d40e9bd3c1 | ||
|
|
92aba77cc1 | ||
|
|
e4e14cd9f4 | ||
|
|
327a159a10 | ||
|
|
d57fb445eb | ||
|
|
fe98c30077 | ||
|
|
758ae41999 | ||
|
|
67229912e6 | ||
|
|
51c593b8cd | ||
|
|
39f93dfa33 | ||
|
|
3559c5dc3c | ||
|
|
f000cedcbb | ||
|
|
681e3d751e | ||
|
|
8926837a3e | ||
|
|
08896ee9c9 | ||
|
|
4bba11cd94 | ||
|
|
9599003bc3 | ||
|
|
986f39838c | ||
|
|
c8c86d2bad |
No files matched your search
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+8
@@ -102,6 +102,14 @@ public class AuthenticatedActionsHandler {
|
|||||||
KeycloakSecurityContext securityContext = facade.getSecurityContext();
|
KeycloakSecurityContext securityContext = facade.getSecurityContext();
|
||||||
String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN);
|
String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN);
|
||||||
String exposeHeaders = deployment.getCorsExposedHeaders();
|
String exposeHeaders = deployment.getCorsExposedHeaders();
|
||||||
|
|
||||||
|
if (deployment.getPolicyEnforcer() != null) {
|
||||||
|
if (exposeHeaders != null) {
|
||||||
|
exposeHeaders += ",";
|
||||||
|
}
|
||||||
|
exposeHeaders += "WWW-Authenticate";
|
||||||
|
}
|
||||||
|
|
||||||
String requestOrigin = UriUtils.getOrigin(facade.getRequest().getURI());
|
String requestOrigin = UriUtils.getOrigin(facade.getRequest().getURI());
|
||||||
log.debugv("Origin: {0} uri: {1}", origin, facade.getRequest().getURI());
|
log.debugv("Origin: {0} uri: {1}", origin, facade.getRequest().getURI());
|
||||||
if (securityContext != null && origin != null && !origin.equals(requestOrigin)) {
|
if (securityContext != null && origin != null && !origin.equals(requestOrigin)) {
|
||||||
|
|||||||
+91
-46
@@ -18,8 +18,10 @@
|
|||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
@@ -30,10 +32,12 @@ import org.keycloak.adapters.spi.HttpFacade.Request;
|
|||||||
import org.keycloak.authorization.client.AuthzClient;
|
import org.keycloak.authorization.client.AuthzClient;
|
||||||
import org.keycloak.authorization.client.ClientAuthorizationContext;
|
import org.keycloak.authorization.client.ClientAuthorizationContext;
|
||||||
import org.keycloak.representations.AccessToken;
|
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;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.ScopeEnforcementMode;
|
||||||
import org.keycloak.representations.idm.authorization.Permission;
|
import org.keycloak.representations.idm.authorization.Permission;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,31 +46,23 @@ import org.keycloak.representations.idm.authorization.Permission;
|
|||||||
public abstract class AbstractPolicyEnforcer {
|
public abstract class AbstractPolicyEnforcer {
|
||||||
|
|
||||||
private static Logger LOGGER = Logger.getLogger(AbstractPolicyEnforcer.class);
|
private static Logger LOGGER = Logger.getLogger(AbstractPolicyEnforcer.class);
|
||||||
private final PolicyEnforcerConfig enforcerConfig;
|
private static final String HTTP_METHOD_DELETE = "DELETE";
|
||||||
|
|
||||||
private final PolicyEnforcer policyEnforcer;
|
private final PolicyEnforcer policyEnforcer;
|
||||||
|
|
||||||
private Map<String, PathConfig> paths;
|
protected AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
|
||||||
private AuthzClient authzClient;
|
|
||||||
private PathMatcher pathMatcher;
|
|
||||||
|
|
||||||
public AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
|
|
||||||
this.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) {
|
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
|
||||||
EnforcementMode enforcementMode = this.enforcerConfig.getEnforcementMode();
|
EnforcementMode enforcementMode = getEnforcerConfig().getEnforcementMode();
|
||||||
|
|
||||||
if (EnforcementMode.DISABLED.equals(enforcementMode)) {
|
if (EnforcementMode.DISABLED.equals(enforcementMode)) {
|
||||||
return createEmptyAuthorizationContext(true);
|
return createEmptyAuthorizationContext(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Request request = httpFacade.getRequest();
|
Request request = httpFacade.getRequest();
|
||||||
String path = getPath(request);
|
PathConfig pathConfig = getPathConfig(request);
|
||||||
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
|
|
||||||
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
||||||
|
|
||||||
if (securityContext == null) {
|
if (securityContext == null) {
|
||||||
@@ -79,16 +75,20 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
AccessToken accessToken = securityContext.getToken();
|
AccessToken accessToken = securityContext.getToken();
|
||||||
|
|
||||||
if (accessToken != null) {
|
if (accessToken != null) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
|
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
|
||||||
|
}
|
||||||
|
|
||||||
if (pathConfig == null) {
|
if (pathConfig == null) {
|
||||||
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
||||||
return createAuthorizationContext(accessToken, null);
|
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);
|
return createAuthorizationContext(accessToken, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
|
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
|
||||||
return createEmptyAuthorizationContext(true);
|
return createAuthorizationContext(accessToken, pathConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
|
MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
|
||||||
@@ -111,10 +111,18 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (methodConfig != null && ScopeEnforcementMode.DISABLED.equals(methodConfig.getScopesEnforcementMode())) {
|
||||||
|
return createEmptyAuthorizationContext(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
|
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
|
||||||
|
}
|
||||||
|
|
||||||
if (!challenge(pathConfig, methodConfig, httpFacade)) {
|
if (!challenge(pathConfig, methodConfig, httpFacade)) {
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
|
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
|
||||||
|
}
|
||||||
handleAccessDenied(httpFacade);
|
handleAccessDenied(httpFacade);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,22 +134,21 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
|
|
||||||
protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
|
protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
|
||||||
Request request = httpFacade.getRequest();
|
Request request = httpFacade.getRequest();
|
||||||
PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
|
|
||||||
|
|
||||||
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
|
if (isDefaultAccessDeniedUri(request)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
AccessToken.Authorization authorization = accessToken.getAuthorization();
|
Authorization authorization = accessToken.getAuthorization();
|
||||||
|
|
||||||
if (authorization == null) {
|
if (authorization == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Permission> permissions = authorization.getPermissions();
|
|
||||||
boolean hasPermission = false;
|
boolean hasPermission = false;
|
||||||
|
List<Permission> grantedPermissions = authorization.getPermissions();
|
||||||
|
|
||||||
for (Permission permission : permissions) {
|
for (Permission permission : grantedPermissions) {
|
||||||
if (permission.getResourceId() != null) {
|
if (permission.getResourceId() != null) {
|
||||||
if (isResourcePermission(actualPathConfig, permission)) {
|
if (isResourcePermission(actualPathConfig, permission)) {
|
||||||
hasPermission = true;
|
hasPermission = true;
|
||||||
@@ -151,11 +158,14 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasResourceScopePermission(methodConfig, permission)) {
|
if (hasResourceScopePermission(methodConfig, permission)) {
|
||||||
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
|
if (LOGGER.isDebugEnabled()) {
|
||||||
if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
|
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions);
|
||||||
this.paths.remove(actualPathConfig);
|
|
||||||
}
|
}
|
||||||
return true;
|
if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
|
||||||
|
policyEnforcer.getPathMatcher().removeFromCache(getPath(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasValidClaims(actualPathConfig, httpFacade, authorization);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -170,24 +180,67 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasValidClaims(PathConfig actualPathConfig, OIDCHttpFacade httpFacade, Authorization authorization) {
|
||||||
|
Map<String, Map<String, Object>> claimInformationPointConfig = actualPathConfig.getClaimInformationPointConfig();
|
||||||
|
|
||||||
|
if (claimInformationPointConfig != null) {
|
||||||
|
Map<String, List<String>> claims = new HashMap<>();
|
||||||
|
|
||||||
|
for (Entry<String, Map<String, Object>> entry : claimInformationPointConfig.entrySet()) {
|
||||||
|
ClaimInformationPointProviderFactory factory = policyEnforcer.getClaimInformationPointProviderFactories().get(entry.getKey());
|
||||||
|
|
||||||
|
if (factory == null) {
|
||||||
|
throw new RuntimeException("Could not find claim information provider with name [" + entry.getKey() + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
claims.putAll(factory.create(entry.getValue()).resolve(httpFacade));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> grantedClaims = authorization.getClaims();
|
||||||
|
|
||||||
|
if (grantedClaims != null) {
|
||||||
|
if (claims.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (Entry<String, List<String>> entry : grantedClaims.entrySet()) {
|
||||||
|
List<String> requestClaims = claims.get(entry.getKey());
|
||||||
|
|
||||||
|
if (requestClaims == null || requestClaims.isEmpty() || !entry.getValue().containsAll(requestClaims)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected void handleAccessDenied(OIDCHttpFacade httpFacade) {
|
protected void handleAccessDenied(OIDCHttpFacade httpFacade) {
|
||||||
httpFacade.getResponse().sendError(403);
|
httpFacade.getResponse().sendError(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDefaultAccessDeniedUri(Request request, PolicyEnforcerConfig enforcerConfig) {
|
protected AuthzClient getAuthzClient() {
|
||||||
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
|
return policyEnforcer.getClient();
|
||||||
|
}
|
||||||
|
|
||||||
if (accessDeniedPath != null) {
|
protected PolicyEnforcerConfig getEnforcerConfig() {
|
||||||
if (request.getURI().contains(accessDeniedPath)) {
|
return policyEnforcer.getEnforcerConfig();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected PolicyEnforcer getPolicyEnforcer() {
|
||||||
|
return policyEnforcer;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
private boolean isDefaultAccessDeniedUri(Request request) {
|
||||||
|
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
|
||||||
|
return accessDeniedPath != null && request.getURI().contains(accessDeniedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
|
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
|
||||||
@@ -215,20 +268,8 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
return requiredScopes.isEmpty();
|
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) {
|
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
|
||||||
return new ClientAuthorizationContext(authzClient) {
|
return new ClientAuthorizationContext(getAuthzClient()) {
|
||||||
@Override
|
@Override
|
||||||
public boolean hasPermission(String resourceName, String scopeName) {
|
public boolean hasPermission(String resourceName, String scopeName) {
|
||||||
return granted;
|
return granted;
|
||||||
@@ -279,7 +320,7 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
|
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
|
||||||
return new ClientAuthorizationContext(accessToken, pathConfig, this.paths, authzClient);
|
return new ClientAuthorizationContext(accessToken, pathConfig, getAuthzClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
||||||
@@ -297,4 +338,8 @@ public abstract class AbstractPolicyEnforcer {
|
|||||||
private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
||||||
return permission.getResourceId().equals(actualPathConfig.getId());
|
return permission.getResourceId().equals(actualPathConfig.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PathConfig getPathConfig(Request request) {
|
||||||
|
return isDefaultAccessDeniedUri(request) ? null : policyEnforcer.getPathMatcher().matches(getPath(request));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
-81
@@ -1,81 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.authorization;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.keycloak.adapters.OIDCHttpFacade;
|
|
||||||
import org.keycloak.adapters.spi.HttpFacade;
|
|
||||||
import org.keycloak.authorization.client.AuthzClient;
|
|
||||||
import org.keycloak.authorization.client.resource.PermissionResource;
|
|
||||||
import org.keycloak.authorization.client.resource.ProtectionResource;
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
|
||||||
import org.keycloak.representations.idm.authorization.PermissionRequest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
|
||||||
*/
|
|
||||||
public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
|
|
||||||
|
|
||||||
private static Logger LOGGER = Logger.getLogger(BearerTokenPolicyEnforcer.class);
|
|
||||||
|
|
||||||
public BearerTokenPolicyEnforcer(PolicyEnforcer enforcer) {
|
|
||||||
super(enforcer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
|
|
||||||
HttpFacade.Response response = facade.getResponse();
|
|
||||||
AuthzClient authzClient = getAuthzClient();
|
|
||||||
String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient);
|
|
||||||
|
|
||||||
if (ticket == null) {
|
|
||||||
response.setStatus(403);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String realm = authzClient.getConfiguration().getRealm();
|
|
||||||
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString();
|
|
||||||
response.setStatus(401);
|
|
||||||
StringBuilder wwwAuthenticate = new StringBuilder("UMA realm=\"").append(realm).append("\"").append(",as_uri=\"").append(authorizationServerUri).append("\"");
|
|
||||||
|
|
||||||
if (ticket != null) {
|
|
||||||
wwwAuthenticate.append(",ticket=\"").append(ticket).append("\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
response.setHeader("WWW-Authenticate", wwwAuthenticate.toString());
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
|
||||||
LOGGER.debug("Sending UMA challenge");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient) {
|
|
||||||
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
|
||||||
ProtectionResource protection = authzClient.protection();
|
|
||||||
PermissionResource permission = protection.permission();
|
|
||||||
PermissionRequest permissionRequest = new PermissionRequest();
|
|
||||||
permissionRequest.setResourceId(pathConfig.getId());
|
|
||||||
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
|
|
||||||
return permission.create(permissionRequest).getTicket();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.authorization;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public interface ClaimInformationPointProvider {
|
||||||
|
|
||||||
|
Map<String, List<String>> resolve(HttpFacade httpFacade);
|
||||||
|
}
|
||||||
+33
@@ -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.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public interface ClaimInformationPointProviderFactory<C extends ClaimInformationPointProvider> {
|
||||||
|
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
void init(PolicyEnforcer policyEnforcer);
|
||||||
|
|
||||||
|
C create(Map<String, Object> config);
|
||||||
|
}
|
||||||
+124
-21
@@ -18,7 +18,11 @@
|
|||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
@@ -28,6 +32,9 @@ import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
|
|||||||
import org.keycloak.adapters.spi.HttpFacade;
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
||||||
import org.keycloak.authorization.client.AuthzClient;
|
import org.keycloak.authorization.client.AuthzClient;
|
||||||
|
import org.keycloak.authorization.client.resource.PermissionResource;
|
||||||
|
import org.keycloak.authorization.client.resource.ProtectionResource;
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
@@ -35,7 +42,7 @@ import org.keycloak.representations.idm.authorization.AuthorizationRequest;
|
|||||||
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
|
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
|
||||||
import org.keycloak.representations.idm.authorization.Permission;
|
import org.keycloak.representations.idm.authorization.Permission;
|
||||||
import org.keycloak.representations.idm.authorization.PermissionRequest;
|
import org.keycloak.representations.idm.authorization.PermissionRequest;
|
||||||
import org.keycloak.representations.idm.authorization.PermissionResponse;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
@@ -72,7 +79,27 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
|
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
|
||||||
|
|
||||||
if (newAuthorization != null) {
|
if (newAuthorization != null) {
|
||||||
authorization.getPermissions().addAll(newAuthorization.getPermissions());
|
List<Permission> grantedPermissions = authorization.getPermissions();
|
||||||
|
List<Permission> newPermissions = newAuthorization.getPermissions();
|
||||||
|
|
||||||
|
for (Permission newPermission : newPermissions) {
|
||||||
|
if (!grantedPermissions.contains(newPermission)) {
|
||||||
|
grantedPermissions.add(newPermission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> newClaims = newAuthorization.getClaims();
|
||||||
|
|
||||||
|
if (newClaims != null) {
|
||||||
|
Map<String, List<String>> claims = authorization.getClaims();
|
||||||
|
|
||||||
|
if (claims == null) {
|
||||||
|
claims = new HashMap<>();
|
||||||
|
authorization.setClaims(claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
claims.putAll(newClaims);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
original.setAuthorization(authorization);
|
original.setAuthorization(authorization);
|
||||||
@@ -81,8 +108,29 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
|
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
|
||||||
handleAccessDenied(facade);
|
if (isBearerAuthorization(httpFacade)) {
|
||||||
|
HttpFacade.Response response = httpFacade.getResponse();
|
||||||
|
AuthzClient authzClient = getAuthzClient();
|
||||||
|
String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient, httpFacade);
|
||||||
|
|
||||||
|
if (ticket != null) {
|
||||||
|
response.setStatus(401);
|
||||||
|
response.setHeader("WWW-Authenticate", new StringBuilder("UMA realm=\"").append(authzClient.getConfiguration().getRealm()).append("\"").append(",as_uri=\"")
|
||||||
|
.append(authzClient.getServerConfiguration().getIssuer()).append("\"").append(",ticket=\"").append(ticket).append("\"").toString());
|
||||||
|
} else {
|
||||||
|
response.setStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LOGGER.isDebugEnabled()) {
|
||||||
|
LOGGER.debug("Sending challenge");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAccessDenied(httpFacade);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,28 +154,31 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
|
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
|
||||||
|
if (getPolicyEnforcer().getDeployment().isBearerOnly() || (isBearerAuthorization(httpFacade) && getEnforcerConfig().getUserManagedAccess() != null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
||||||
String accessTokenString = securityContext.getTokenString();
|
String accessTokenString = securityContext.getTokenString();
|
||||||
AuthzClient authzClient = getAuthzClient();
|
|
||||||
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
|
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
|
||||||
PermissionRequest permissionRequest = new PermissionRequest();
|
|
||||||
|
|
||||||
permissionRequest.setResourceId(pathConfig.getId());
|
|
||||||
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
|
|
||||||
|
|
||||||
AccessToken accessToken = securityContext.getToken();
|
AccessToken accessToken = securityContext.getToken();
|
||||||
AuthorizationRequest authzRequest;
|
AuthorizationRequest authzRequest = new AuthorizationRequest();
|
||||||
|
|
||||||
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
||||||
PermissionResponse permissionResponse = authzClient.protection().permission().create(permissionRequest);
|
String ticket = getPermissionTicket(pathConfig, methodConfig, getAuthzClient(), httpFacade);
|
||||||
authzRequest = new AuthorizationRequest();
|
authzRequest.setTicket(ticket);
|
||||||
authzRequest.setTicket(permissionResponse.getTicket());
|
|
||||||
} else {
|
} else {
|
||||||
authzRequest = new AuthorizationRequest();
|
|
||||||
if (accessToken.getAuthorization() != null) {
|
if (accessToken.getAuthorization() != null) {
|
||||||
authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes());
|
authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> claims = getClaims(pathConfig, httpFacade);
|
||||||
|
|
||||||
|
if (!claims.isEmpty()) {
|
||||||
|
authzRequest.setClaimTokenFormat("urn:ietf:params:oauth:token-type:jwt");
|
||||||
|
authzRequest.setClaimToken(Base64.encodeBytes(JsonSerialization.writeValueAsBytes(claims)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessToken.getAuthorization() != null) {
|
if (accessToken.getAuthorization() != null) {
|
||||||
@@ -135,18 +186,70 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.debug("Obtaining authorization for authenticated user.");
|
LOGGER.debug("Obtaining authorization for authenticated user.");
|
||||||
AuthorizationResponse authzResponse = authzClient.authorization(accessTokenString).authorize(authzRequest);
|
AuthorizationResponse authzResponse = getAuthzClient().authorization(accessTokenString).authorize(authzRequest);
|
||||||
|
|
||||||
if (authzResponse != null) {
|
if (authzResponse != null) {
|
||||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getToken(), deployment);
|
return AdapterRSATokenVerifier.verifyToken(authzResponse.getToken(), deployment);
|
||||||
}
|
}
|
||||||
|
} catch (AuthorizationDeniedException ignore) {
|
||||||
return null;
|
LOGGER.debug("Authorization denied", ignore);
|
||||||
} catch (AuthorizationDeniedException e) {
|
|
||||||
LOGGER.debug("Authorization denied", e);
|
|
||||||
return null;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Unexpected error during authorization request.", e);
|
throw new RuntimeException("Unexpected error during authorization request.", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient, HttpFacade httpFacade) {
|
||||||
|
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
||||||
|
ProtectionResource protection = authzClient.protection();
|
||||||
|
PermissionResource permission = protection.permission();
|
||||||
|
PermissionRequest permissionRequest = new PermissionRequest();
|
||||||
|
|
||||||
|
permissionRequest.setResourceId(pathConfig.getId());
|
||||||
|
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
|
||||||
|
|
||||||
|
Map<String, List<String>> claims = getClaims(pathConfig, httpFacade);
|
||||||
|
|
||||||
|
if (!claims.isEmpty()) {
|
||||||
|
permissionRequest.setClaims(claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
return permission.create(permissionRequest).getTicket();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, List<String>> getClaims(PathConfig pathConfig, HttpFacade httpFacade) {
|
||||||
|
Map<String, List<String>> claims = new HashMap<>();
|
||||||
|
Map<String, Map<String, Object>> claimInformationPointConfig = pathConfig.getClaimInformationPointConfig();
|
||||||
|
|
||||||
|
if (claimInformationPointConfig != null) {
|
||||||
|
for (Entry<String, Map<String, Object>> claimDef : claimInformationPointConfig.entrySet()) {
|
||||||
|
ClaimInformationPointProviderFactory factory = getPolicyEnforcer().getClaimInformationPointProviderFactories().get(claimDef.getKey());
|
||||||
|
|
||||||
|
if (factory != null) {
|
||||||
|
claims.putAll(factory.create(claimDef.getValue()).resolve(httpFacade));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBearerAuthorization(OIDCHttpFacade httpFacade) {
|
||||||
|
List<String> authHeaders = httpFacade.getRequest().getHeaders("Authorization");
|
||||||
|
if (authHeaders == null || authHeaders.size() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String authHeader : authHeaders) {
|
||||||
|
String[] split = authHeader.trim().split("\\s+");
|
||||||
|
if (split == null || split.length != 2) continue;
|
||||||
|
if (!split[0].equalsIgnoreCase("Bearer")) continue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+12
-9
@@ -24,6 +24,8 @@ import java.util.concurrent.locks.LockSupport;
|
|||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
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>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
public class PathCache {
|
public class PathCache {
|
||||||
@@ -38,15 +40,7 @@ public class PathCache {
|
|||||||
private final AtomicBoolean writing = new AtomicBoolean(false);
|
private final AtomicBoolean writing = new AtomicBoolean(false);
|
||||||
|
|
||||||
private final long maxAge;
|
private final long maxAge;
|
||||||
|
private final boolean enabled;
|
||||||
/**
|
|
||||||
* 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.
|
* Creates a new instance.
|
||||||
@@ -62,9 +56,14 @@ public class PathCache {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.maxAge = maxAge;
|
this.maxAge = maxAge;
|
||||||
|
this.enabled = maxAge > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void put(String uri, PathConfig newValue) {
|
public void put(String uri, PathConfig newValue) {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (parkForWriteAndCheckInterrupt()) {
|
if (parkForWriteAndCheckInterrupt()) {
|
||||||
return;
|
return;
|
||||||
@@ -80,6 +79,10 @@ public class PathCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean containsKey(String uri) {
|
||||||
|
return cache.containsKey(uri);
|
||||||
|
}
|
||||||
|
|
||||||
public PathConfig get(String uri) {
|
public PathConfig get(String uri) {
|
||||||
if (parkForReadAndCheckInterrupt()) {
|
if (parkForReadAndCheckInterrupt()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+137
-69
@@ -17,14 +17,16 @@
|
|||||||
*/
|
*/
|
||||||
package org.keycloak.adapters.authorization;
|
package org.keycloak.adapters.authorization;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.AuthorizationContext;
|
import org.keycloak.AuthorizationContext;
|
||||||
@@ -34,13 +36,14 @@ import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
|
|||||||
import org.keycloak.authorization.client.AuthzClient;
|
import org.keycloak.authorization.client.AuthzClient;
|
||||||
import org.keycloak.authorization.client.ClientAuthenticator;
|
import org.keycloak.authorization.client.ClientAuthenticator;
|
||||||
import org.keycloak.authorization.client.Configuration;
|
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.authorization.client.resource.ProtectedResource;
|
||||||
|
import org.keycloak.common.util.PathMatcher;
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||||
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCacheConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||||
import org.keycloak.representations.idm.authorization.Permission;
|
import org.keycloak.representations.idm.authorization.Permission;
|
||||||
|
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
@@ -52,8 +55,9 @@ public class PolicyEnforcer {
|
|||||||
private final KeycloakDeployment deployment;
|
private final KeycloakDeployment deployment;
|
||||||
private final AuthzClient authzClient;
|
private final AuthzClient authzClient;
|
||||||
private final PolicyEnforcerConfig enforcerConfig;
|
private final PolicyEnforcerConfig enforcerConfig;
|
||||||
|
private final PathConfigMatcher pathMatcher;
|
||||||
private final Map<String, PathConfig> paths;
|
private final Map<String, PathConfig> paths;
|
||||||
private final PathMatcher pathMatcher;
|
private final Map<String, ClaimInformationPointProviderFactory> claimInformationPointProviderFactories = new HashMap<>();
|
||||||
|
|
||||||
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
|
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
|
||||||
this.deployment = deployment;
|
this.deployment = deployment;
|
||||||
@@ -69,8 +73,9 @@ public class PolicyEnforcer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.pathMatcher = new PathMatcher(this.authzClient);
|
|
||||||
this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
|
paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
|
||||||
|
pathMatcher = new PathConfigMatcher(paths, enforcerConfig, authzClient);
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debug("Initialization complete. Path configurations:");
|
LOGGER.debug("Initialization complete. Path configurations:");
|
||||||
@@ -78,20 +83,17 @@ public class PolicyEnforcer {
|
|||||||
LOGGER.debug(pathConfig);
|
LOGGER.debug(pathConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, ClaimInformationPointProviderFactory.class.getClassLoader()));
|
||||||
|
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, Thread.currentThread().getContextClassLoader()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthorizationContext enforce(OIDCHttpFacade facade) {
|
public AuthorizationContext enforce(OIDCHttpFacade facade) {
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debugv("Policy enforcement is enable. Enforcing policy decisions for path [{0}].", facade.getRequest().getURI());
|
LOGGER.debugv("Policy enforcement is enabled. Enforcing policy decisions for path [{0}].", facade.getRequest().getURI());
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthorizationContext context;
|
AuthorizationContext context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
|
||||||
|
|
||||||
if (deployment.isBearerOnly()) {
|
|
||||||
context = new BearerTokenPolicyEnforcer(this).authorize(facade);
|
|
||||||
} else {
|
|
||||||
context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LOGGER.isDebugEnabled()) {
|
if (LOGGER.isDebugEnabled()) {
|
||||||
LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", facade.getRequest().getURI(), context.isGranted() ? "GRANTED" : "DENIED");
|
LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", facade.getRequest().getURI(), context.isGranted() ? "GRANTED" : "DENIED");
|
||||||
@@ -104,11 +106,11 @@ public class PolicyEnforcer {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
PolicyEnforcerConfig getEnforcerConfig() {
|
public PolicyEnforcerConfig getEnforcerConfig() {
|
||||||
return enforcerConfig;
|
return enforcerConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthzClient getClient() {
|
public AuthzClient getClient() {
|
||||||
return authzClient;
|
return authzClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,14 +118,30 @@ public class PolicyEnforcer {
|
|||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addPath(PathConfig pathConfig) {
|
public PathConfigMatcher getPathMatcher() {
|
||||||
paths.put(pathConfig.getPath(), pathConfig);
|
return pathMatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
KeycloakDeployment getDeployment() {
|
public KeycloakDeployment getDeployment() {
|
||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, ClaimInformationPointProviderFactory> getClaimInformationPointProviderFactories() {
|
||||||
|
return claimInformationPointProviderFactories;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadClaimInformationPointProviders(ServiceLoader<ClaimInformationPointProviderFactory> loader) {
|
||||||
|
Iterator<ClaimInformationPointProviderFactory> iterator = loader.iterator();
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
ClaimInformationPointProviderFactory factory = iterator.next();
|
||||||
|
|
||||||
|
factory.init(this);
|
||||||
|
|
||||||
|
claimInformationPointProviderFactories.put(factory.getName(), factory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
private Map<String, PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
||||||
boolean loadPathsFromServer = true;
|
boolean loadPathsFromServer = true;
|
||||||
|
|
||||||
@@ -136,7 +154,11 @@ public class PolicyEnforcer {
|
|||||||
|
|
||||||
if (loadPathsFromServer) {
|
if (loadPathsFromServer) {
|
||||||
LOGGER.info("No path provided in configuration.");
|
LOGGER.info("No path provided in configuration.");
|
||||||
return configureAllPathsForResourceServer(protectedResource);
|
Map<String, PathConfig> paths = configureAllPathsForResourceServer(protectedResource);
|
||||||
|
|
||||||
|
paths.putAll(configureDefinedPaths(protectedResource, enforcerConfig));
|
||||||
|
|
||||||
|
return paths;
|
||||||
} else {
|
} else {
|
||||||
LOGGER.info("Paths provided in configuration.");
|
LOGGER.info("Paths provided in configuration.");
|
||||||
return configureDefinedPaths(protectedResource, enforcerConfig);
|
return configureDefinedPaths(protectedResource, enforcerConfig);
|
||||||
@@ -144,7 +166,7 @@ public class PolicyEnforcer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
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()) {
|
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
|
||||||
ResourceRepresentation resource;
|
ResourceRepresentation resource;
|
||||||
@@ -158,6 +180,10 @@ public class PolicyEnforcer {
|
|||||||
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
|
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
|
||||||
List<ResourceRepresentation> resources = protectedResource.findByUri(path);
|
List<ResourceRepresentation> resources = protectedResource.findByUri(path);
|
||||||
|
|
||||||
|
if (resources.isEmpty()) {
|
||||||
|
resources = protectedResource.findByMatchingUri(path);
|
||||||
|
}
|
||||||
|
|
||||||
if (resources.size() == 1) {
|
if (resources.size() == 1) {
|
||||||
resource = resources.get(0);
|
resource = resources.get(0);
|
||||||
} else if (resources.size() > 1) {
|
} else if (resources.size() > 1) {
|
||||||
@@ -167,41 +193,14 @@ public class PolicyEnforcer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resource == null) {
|
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());
|
pathConfig.setId(resource.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
PathConfig existingPath = null;
|
PathConfig existingPath = null;
|
||||||
|
|
||||||
for (PathConfig current : paths.values()) {
|
for (PathConfig current : paths.values()) {
|
||||||
if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) {
|
if (current.getPath().equals(pathConfig.getPath())) {
|
||||||
existingPath = current;
|
existingPath = current;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -222,45 +221,114 @@ public class PolicyEnforcer {
|
|||||||
LOGGER.info("Querying the server for all resources associated with this application.");
|
LOGGER.info("Querying the server for all resources associated with this application.");
|
||||||
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
|
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
|
||||||
|
|
||||||
|
if (!enforcerConfig.getLazyLoadPaths()) {
|
||||||
for (String id : protectedResource.findAll()) {
|
for (String id : protectedResource.findAll()) {
|
||||||
ResourceRepresentation resourceDescription = protectedResource.findById(id);
|
ResourceRepresentation resourceDescription = protectedResource.findById(id);
|
||||||
|
|
||||||
if (resourceDescription.getUri() != null) {
|
if (resourceDescription.getUri() != null) {
|
||||||
PathConfig pathConfig = createPathConfig(resourceDescription);
|
PathConfig pathConfig = PathConfig.createPathConfig(resourceDescription);
|
||||||
paths.put(pathConfig.getPath(), pathConfig);
|
paths.put(pathConfig.getPath(), pathConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
|
public class PathConfigMatcher extends PathMatcher<PathConfig> {
|
||||||
PathConfig pathConfig = new PathConfig();
|
|
||||||
|
|
||||||
pathConfig.setId(resourceDescription.getId());
|
private final Map<String, PathConfig> paths;
|
||||||
pathConfig.setName(resourceDescription.getName());
|
private final PathCache pathCache;
|
||||||
|
private final AuthzClient authzClient;
|
||||||
|
private final PolicyEnforcerConfig enforcerConfig;
|
||||||
|
|
||||||
String uri = resourceDescription.getUri();
|
public PathConfigMatcher(Map<String, PathConfig> paths, PolicyEnforcerConfig enforcerConfig, AuthzClient authzClient) {
|
||||||
|
this.paths = paths;
|
||||||
|
this.enforcerConfig = enforcerConfig;
|
||||||
|
PathCacheConfig cacheConfig = enforcerConfig.getPathCacheConfig();
|
||||||
|
|
||||||
if (uri == null || "".equals(uri.trim())) {
|
if (cacheConfig == null) {
|
||||||
throw new RuntimeException("Failed to configure paths. Resource [" + resourceDescription.getName() + "] has an invalid or empty URI [" + uri + "].");
|
cacheConfig = new PathCacheConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
pathConfig.setPath(uri);
|
pathCache = new PathCache(cacheConfig.getMaxEntries(), cacheConfig.getLifespan());
|
||||||
|
this.authzClient = authzClient;
|
||||||
List<String> scopeNames = new ArrayList<>();
|
|
||||||
|
|
||||||
for (ScopeRepresentation scope : resourceDescription.getScopes()) {
|
|
||||||
scopeNames.add(scope.getName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pathConfig.setScopes(scopeNames);
|
@Override
|
||||||
pathConfig.setType(resourceDescription.getType());
|
public PathConfig matches(String targetUri) {
|
||||||
|
PathConfig pathConfig = pathCache.get(targetUri);
|
||||||
|
|
||||||
|
if (pathCache.containsKey(targetUri) || pathConfig != null) {
|
||||||
|
return pathConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
pathConfig = super.matches(targetUri);
|
||||||
|
|
||||||
|
if (enforcerConfig.getLazyLoadPaths() || enforcerConfig.getPathCacheConfig() != null) {
|
||||||
|
if ((pathConfig == null || (pathConfig.getPath().contains("*")))) {
|
||||||
|
try {
|
||||||
|
List<ResourceRepresentation> matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri);
|
||||||
|
|
||||||
|
if (!matchingResources.isEmpty()) {
|
||||||
|
pathConfig = PathConfig.createPathConfig(matchingResources.get(0));
|
||||||
|
}
|
||||||
|
} catch (Exception cause) {
|
||||||
|
LOGGER.errorf(cause, "Could not lazy load resource with path [" + targetUri + "] from server");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pathCache.put(targetUri, pathConfig);
|
||||||
|
|
||||||
return pathConfig;
|
return pathConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PathMatcher getPathMatcher() {
|
@Override
|
||||||
return pathMatcher;
|
protected String getPath(PathConfig entry) {
|
||||||
|
return entry.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Collection<PathConfig> getPaths() {
|
||||||
|
return paths.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
|
||||||
|
if (originalConfig.hasPattern()) {
|
||||||
|
ProtectedResource resource = authzClient.protection().resource();
|
||||||
|
|
||||||
|
// search by an exact match
|
||||||
|
List<ResourceRepresentation> search = resource.findByUri(path);
|
||||||
|
|
||||||
|
// if exact match not found, try to obtain from current path the parent path.
|
||||||
|
// if path is /resource/1/test and pattern from pathConfig is /resource/{id}/*, parent path is /resource/1
|
||||||
|
// this logic allows to match sub resources of a resource instance (/resource/1) to the parent resource,
|
||||||
|
// so any permission granted to parent also applies to sub resources
|
||||||
|
if (search.isEmpty()) {
|
||||||
|
search = resource.findByUri(buildUriFromTemplate(originalConfig.getPath(), path, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!search.isEmpty()) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeFromCache(String pathConfig) {
|
||||||
|
pathCache.remove(pathConfig);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.authorization.cip;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.authorization.ClaimInformationPointProvider;
|
||||||
|
import org.keycloak.adapters.authorization.util.PlaceHolders;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class ClaimsInformationPointProvider implements ClaimInformationPointProvider {
|
||||||
|
|
||||||
|
private final Map<String, Object> config;
|
||||||
|
|
||||||
|
public ClaimsInformationPointProvider(Map<String, Object> config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<String>> resolve(HttpFacade httpFacade) {
|
||||||
|
Map<String, List<String>> claims = new HashMap<>();
|
||||||
|
|
||||||
|
for (Entry<String, Object> configEntry : config.entrySet()) {
|
||||||
|
String claimName = configEntry.getKey();
|
||||||
|
Object claimValue = configEntry.getValue();
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
|
||||||
|
if (claimValue instanceof String) {
|
||||||
|
values = getValues(claimValue.toString(), httpFacade);
|
||||||
|
} else if (claimValue instanceof Collection) {
|
||||||
|
Iterator iterator = Collection.class.cast(claimValue).iterator();
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
List<String> resolvedValues = getValues(iterator.next().toString(), httpFacade);
|
||||||
|
|
||||||
|
if (!resolvedValues.isEmpty()) {
|
||||||
|
values.addAll(resolvedValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.isEmpty()) {
|
||||||
|
claims.put(claimName, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getValues(String value, HttpFacade httpFacade) {
|
||||||
|
return PlaceHolders.resolve(value, httpFacade);
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -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.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.authorization.cip;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
|
||||||
|
import org.keycloak.adapters.authorization.PolicyEnforcer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class ClaimsInformationPointProviderFactory implements ClaimInformationPointProviderFactory<ClaimsInformationPointProvider> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "claims";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(PolicyEnforcer policyEnforcer) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClaimsInformationPointProvider create(Map<String, Object> config) {
|
||||||
|
return new ClaimsInformationPointProvider(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.authorization.cip;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.apache.http.HttpEntity;
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
|
import org.apache.http.StatusLine;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.methods.RequestBuilder;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.keycloak.adapters.authorization.ClaimInformationPointProvider;
|
||||||
|
import org.keycloak.adapters.authorization.PolicyEnforcer;
|
||||||
|
import org.keycloak.adapters.authorization.util.JsonUtils;
|
||||||
|
import org.keycloak.adapters.authorization.util.PlaceHolders;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.authorization.client.util.HttpResponseException;
|
||||||
|
import org.keycloak.common.util.StreamUtil;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class HttpClaimInformationPointProvider implements ClaimInformationPointProvider {
|
||||||
|
|
||||||
|
private final Map<String, Object> config;
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
|
public HttpClaimInformationPointProvider(Map<String, Object> config, PolicyEnforcer policyEnforcer) {
|
||||||
|
this.config = config;
|
||||||
|
this.httpClient = policyEnforcer.getDeployment().getClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<String>> resolve(HttpFacade httpFacade) {
|
||||||
|
try {
|
||||||
|
InputStream responseStream = executeRequest(httpFacade);
|
||||||
|
|
||||||
|
try (InputStream inputStream = new BufferedInputStream(responseStream)) {
|
||||||
|
JsonNode jsonNode = JsonSerialization.mapper.readTree(inputStream);
|
||||||
|
Map<String, List<String>> claims = new HashMap<>();
|
||||||
|
Map<String, Object> claimsDef = (Map<String, Object>) config.get("claims");
|
||||||
|
|
||||||
|
if (claimsDef == null) {
|
||||||
|
Iterator<String> nodeNames = jsonNode.fieldNames();
|
||||||
|
|
||||||
|
while (nodeNames.hasNext()) {
|
||||||
|
String nodeName = nodeNames.next();
|
||||||
|
claims.put(nodeName, JsonUtils.getValues(jsonNode.get(nodeName)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (Entry<String, Object> claimDef : claimsDef.entrySet()) {
|
||||||
|
List<String> jsonPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
if (claimDef.getValue() instanceof Collection) {
|
||||||
|
jsonPaths.addAll(Collection.class.cast(claimDef.getValue()));
|
||||||
|
} else {
|
||||||
|
jsonPaths.add(claimDef.getValue().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> claimValues = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String path : jsonPaths) {
|
||||||
|
claimValues.addAll(JsonUtils.getValues(jsonNode, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
claims.put(claimDef.getKey(), claimValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Could not obtain claims from http claim information point [" + config.get("url") + "] response", cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream executeRequest(HttpFacade httpFacade) {
|
||||||
|
String method = config.get("method").toString();
|
||||||
|
|
||||||
|
if (method == null) {
|
||||||
|
method = "GET";
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder builder = null;
|
||||||
|
|
||||||
|
if ("GET".equalsIgnoreCase(method)) {
|
||||||
|
builder = RequestBuilder.get();
|
||||||
|
} else {
|
||||||
|
builder = RequestBuilder.post();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setUri(config.get("url").toString());
|
||||||
|
|
||||||
|
byte[] bytes = new byte[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
setParameters(builder, httpFacade);
|
||||||
|
|
||||||
|
if (config.containsKey("headers")) {
|
||||||
|
setHeaders(builder, httpFacade);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse response = httpClient.execute(builder.build());
|
||||||
|
HttpEntity entity = response.getEntity();
|
||||||
|
|
||||||
|
if (entity != null) {
|
||||||
|
bytes = EntityUtils.toByteArray(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusLine statusLine = response.getStatusLine();
|
||||||
|
int statusCode = statusLine.getStatusCode();
|
||||||
|
|
||||||
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
|
throw new HttpResponseException("Unexpected response from server: " + statusCode + " / " + statusLine.getReasonPhrase(), statusCode, statusLine.getReasonPhrase(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ByteArrayInputStream(bytes);
|
||||||
|
} catch (Exception cause) {
|
||||||
|
try {
|
||||||
|
throw new RuntimeException("Error executing http method [" + builder + "]. Response : " + StreamUtil.readString(new ByteArrayInputStream(bytes), Charset.forName("UTF-8")), cause);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error executing http method [" + builder + "]", cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHeaders(RequestBuilder builder, HttpFacade httpFacade) {
|
||||||
|
Object headersDef = config.get("headers");
|
||||||
|
|
||||||
|
if (headersDef != null) {
|
||||||
|
Map<String, Object> headers = Map.class.cast(headersDef);
|
||||||
|
|
||||||
|
for (Entry<String, Object> header : headers.entrySet()) {
|
||||||
|
Object value = header.getValue();
|
||||||
|
List<String> headerValues = new ArrayList<>();
|
||||||
|
|
||||||
|
if (value instanceof Collection) {
|
||||||
|
Collection values = Collection.class.cast(value);
|
||||||
|
Iterator iterator = values.iterator();
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
headerValues.addAll(PlaceHolders.resolve(iterator.next().toString(), httpFacade));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headerValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String headerValue : headerValues) {
|
||||||
|
builder.addHeader(header.getKey(), headerValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setParameters(RequestBuilder builder, HttpFacade httpFacade) {
|
||||||
|
Object config = this.config.get("parameters");
|
||||||
|
|
||||||
|
if (config != null) {
|
||||||
|
Map<String, Object> paramsDef = Map.class.cast(config);
|
||||||
|
|
||||||
|
for (Entry<String, Object> paramDef : paramsDef.entrySet()) {
|
||||||
|
Object value = paramDef.getValue();
|
||||||
|
List<String> paramValues = new ArrayList<>();
|
||||||
|
|
||||||
|
if (value instanceof Collection) {
|
||||||
|
Collection values = Collection.class.cast(value);
|
||||||
|
Iterator iterator = values.iterator();
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
paramValues.addAll(PlaceHolders.resolve(iterator.next().toString(), httpFacade));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
paramValues.addAll(PlaceHolders.resolve(value.toString(), httpFacade));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String paramValue : paramValues) {
|
||||||
|
builder.addParameter(paramDef.getKey(), paramValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.authorization.cip;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.authorization.ClaimInformationPointProviderFactory;
|
||||||
|
import org.keycloak.adapters.authorization.PolicyEnforcer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class HttpClaimInformationPointProviderFactory implements ClaimInformationPointProviderFactory<HttpClaimInformationPointProvider> {
|
||||||
|
|
||||||
|
private PolicyEnforcer policyEnforcer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "http";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(PolicyEnforcer policyEnforcer) {
|
||||||
|
this.policyEnforcer = policyEnforcer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClaimInformationPointProvider create(Map<String, Object> config) {
|
||||||
|
return new HttpClaimInformationPointProvider(config, policyEnforcer);
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
@@ -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.adapters.authorization.util;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class JsonUtils {
|
||||||
|
|
||||||
|
public static List<String> getValues(JsonNode jsonNode, String path) {
|
||||||
|
return getValues(jsonNode.at(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> getValues(JsonNode jsonNode) {
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
|
||||||
|
if (jsonNode.isArray()) {
|
||||||
|
Iterator<JsonNode> iterator = jsonNode.iterator();
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
String value = iterator.next().textValue();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
values.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String value = jsonNode.textValue();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
values.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.authorization.util;
|
||||||
|
|
||||||
|
import static org.keycloak.adapters.authorization.util.PlaceHolders.getParameter;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.OIDCHttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class KeycloakSecurityContextPlaceHolderResolver implements PlaceHolderResolver {
|
||||||
|
|
||||||
|
public static final String NAME = "keycloak";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> resolve(String placeHolder, HttpFacade httpFacade) {
|
||||||
|
String source = placeHolder.substring(placeHolder.indexOf('.') + 1);
|
||||||
|
OIDCHttpFacade oidcHttpFacade = OIDCHttpFacade.class.cast(httpFacade);
|
||||||
|
KeycloakSecurityContext securityContext = oidcHttpFacade.getSecurityContext();
|
||||||
|
|
||||||
|
if (securityContext == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.endsWith("access_token")) {
|
||||||
|
return Arrays.asList(securityContext.getTokenString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.endsWith("id_token")) {
|
||||||
|
return Arrays.asList(securityContext.getIdTokenString());
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode jsonNode;
|
||||||
|
|
||||||
|
if (source.startsWith("access_token[")) {
|
||||||
|
jsonNode = JsonSerialization.mapper.valueToTree(securityContext.getToken());
|
||||||
|
} else if (source.startsWith("id_token[")) {
|
||||||
|
jsonNode = JsonSerialization.mapper.valueToTree(securityContext.getIdToken());
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Invalid placeholder [" + placeHolder + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonUtils.getValues(jsonNode, getParameter(source, "Invalid placeholder [" + placeHolder + "]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.authorization.util;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public interface PlaceHolderResolver {
|
||||||
|
|
||||||
|
List<String> resolve(String placeHolder, HttpFacade httpFacade);
|
||||||
|
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.authorization.util;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class PlaceHolders {
|
||||||
|
|
||||||
|
private static Map<String, PlaceHolderResolver> resolvers = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
resolvers.put(RequestPlaceHolderResolver.NAME, new RequestPlaceHolderResolver());
|
||||||
|
resolvers.put(KeycloakSecurityContextPlaceHolderResolver.NAME, new KeycloakSecurityContextPlaceHolderResolver());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(.+?)\\}");
|
||||||
|
private static Pattern PLACEHOLDER_PARAM_PATTERN = Pattern.compile("\\[(.+?)\\]");
|
||||||
|
|
||||||
|
public static List<String> resolve(String value, HttpFacade httpFacade) {
|
||||||
|
Map<String, List<String>> placeHolders = parsePlaceHolders(value, httpFacade);
|
||||||
|
|
||||||
|
if (!placeHolders.isEmpty()) {
|
||||||
|
value = formatPlaceHolder(value);
|
||||||
|
|
||||||
|
for (Entry<String, List<String>> entry : placeHolders.entrySet()) {
|
||||||
|
List<String> values = entry.getValue();
|
||||||
|
|
||||||
|
if (values.isEmpty() || values.size() > 1) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.replaceAll(entry.getKey(), values.get(0)).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.asList(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getParameter(String source, String messageIfNotFound) {
|
||||||
|
Matcher matcher = PLACEHOLDER_PARAM_PATTERN.matcher(source);
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
return matcher.group(1).replaceAll("'", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageIfNotFound != null) {
|
||||||
|
throw new RuntimeException(messageIfNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, List<String>> parsePlaceHolders(String value, HttpFacade httpFacade) {
|
||||||
|
Map<String, List<String>> placeHolders = new HashMap<>();
|
||||||
|
Matcher matcher = PLACEHOLDER_PATTERN.matcher(value);
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
String placeHolder = matcher.group(1);
|
||||||
|
int resolverNameIdx = placeHolder.indexOf('.');
|
||||||
|
|
||||||
|
if (resolverNameIdx == -1) {
|
||||||
|
throw new RuntimeException("Invalid placeholder [" + value + "]. Could not find resolver name.");
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaceHolderResolver resolver = resolvers.get(placeHolder.substring(0, resolverNameIdx));
|
||||||
|
|
||||||
|
if (resolver != null) {
|
||||||
|
List<String> resolved = resolver.resolve(placeHolder, httpFacade);
|
||||||
|
|
||||||
|
if (resolved != null) {
|
||||||
|
placeHolders.put(formatPlaceHolder(placeHolder), resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return placeHolders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatPlaceHolder(String placeHolder) {
|
||||||
|
return placeHolder.replaceAll("\\{", "").replace("}", "").replace("[", "").replace("]", "").replace("[", "").replace("]", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.authorization.util;
|
||||||
|
|
||||||
|
import static org.keycloak.adapters.authorization.util.PlaceHolders.getParameter;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade.Cookie;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade.Request;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class RequestPlaceHolderResolver implements PlaceHolderResolver {
|
||||||
|
|
||||||
|
static String NAME = "request";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> resolve(String placeHolder, HttpFacade httpFacade) {
|
||||||
|
String source = placeHolder.substring(placeHolder.indexOf('.') + 1);
|
||||||
|
Request request = httpFacade.getRequest();
|
||||||
|
|
||||||
|
if (source.startsWith("parameter")) {
|
||||||
|
String parameterName = getParameter(source, "Could not obtain parameter name from placeholder [" + source + "]");
|
||||||
|
String parameterValue = request.getQueryParamValue(parameterName);
|
||||||
|
|
||||||
|
if (parameterValue == null) {
|
||||||
|
parameterValue = request.getFirstParam(parameterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterValue != null) {
|
||||||
|
return Arrays.asList(parameterValue);
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("header")) {
|
||||||
|
String headerName = getParameter(source, "Could not obtain header name from placeholder [" + source + "]");
|
||||||
|
List<String> headerValue = request.getHeaders(headerName);
|
||||||
|
|
||||||
|
if (headerValue != null) {
|
||||||
|
return headerValue;
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("cookie")) {
|
||||||
|
String cookieName = getParameter(source, "Could not obtain cookie name from placeholder [" + source + "]");
|
||||||
|
Cookie cookieValue = request.getCookie(cookieName);
|
||||||
|
|
||||||
|
if (cookieValue != null) {
|
||||||
|
return Arrays.asList(cookieValue.getValue());
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("remoteAddr")) {
|
||||||
|
String value = request.getRemoteAddr();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
return Arrays.asList(value);
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("method")) {
|
||||||
|
String value = request.getMethod();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
return Arrays.asList(value);
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("uri")) {
|
||||||
|
String value = request.getURI();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
return Arrays.asList(value);
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("relativePath")) {
|
||||||
|
String value = request.getRelativePath();
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
return Arrays.asList(value);
|
||||||
|
}
|
||||||
|
} else if (source.startsWith("secure")) {
|
||||||
|
return Arrays.asList(String.valueOf(request.isSecure()));
|
||||||
|
} else if (source.startsWith("body")) {
|
||||||
|
String contentType = request.getHeader("Content-Type");
|
||||||
|
|
||||||
|
if (contentType == null) {
|
||||||
|
contentType = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream body = request.getInputStream(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (body == null || body.available() == 0) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Failed to check available bytes in request input stream", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.markSupported()) {
|
||||||
|
body.mark(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (contentType) {
|
||||||
|
case "application/json":
|
||||||
|
try {
|
||||||
|
JsonNode jsonNode = JsonSerialization.mapper.readTree(new BufferedInputStream(body) {
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// we can't close the stream because it may be used later by the application
|
||||||
|
}
|
||||||
|
});
|
||||||
|
String path = getParameter(source, null);
|
||||||
|
|
||||||
|
if (path == null) {
|
||||||
|
values.addAll(JsonUtils.getValues(jsonNode));
|
||||||
|
} else {
|
||||||
|
values.addAll(JsonUtils.getValues(jsonNode, path));
|
||||||
|
}
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Could not extract claim from request JSON body", cause);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
StringBuilder value = new StringBuilder();
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(body));
|
||||||
|
|
||||||
|
try {
|
||||||
|
int ch;
|
||||||
|
|
||||||
|
while ((ch = reader.read()) != -1) {
|
||||||
|
value.append((char) ch);
|
||||||
|
}
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Could not extract claim from request body", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
values.add(value.toString());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (body.markSupported()) {
|
||||||
|
try {
|
||||||
|
body.reset();
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Failed to reset request input stream", cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#
|
||||||
|
# * 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
org.keycloak.adapters.authorization.cip.ClaimsInformationPointProviderFactory
|
||||||
|
org.keycloak.adapters.authorization.cip.HttpClaimInformationPointProviderFactory
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak AS7 / JBoss EAP 6 Integration</name>
|
<name>Keycloak AS7 / JBoss EAP 6 Integration</name>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
export KC_AUTH_SERVER=http://localhost:8080/auth
|
|
||||||
export KC_REALM=master
|
|
||||||
export KC_CLIENT=cli
|
|
||||||
|
|
||||||
export KC_ACCESS_TOKEN=`java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar login`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar logout
|
|
||||||
|
|
||||||
unset KC_ACCESS_TOKEN
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-fuse7-integration-pom</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Beta3</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-camel-undertow</artifactId>
|
||||||
|
<name>Keycloak Fuse 7.0 Adapter - Camel + Undertow</name>
|
||||||
|
<packaging>bundle</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<keycloak.osgi.export>
|
||||||
|
org.keycloak.adapters.camel.undertow;version="${project.version}"
|
||||||
|
</keycloak.osgi.export>
|
||||||
|
<keycloak.osgi.import>
|
||||||
|
org.keycloak.*;version="${project.version}",
|
||||||
|
org.apache.camel.*,
|
||||||
|
org.apache.camel.component.undertow,
|
||||||
|
io.undertow.*,
|
||||||
|
*;resolution:=optional
|
||||||
|
</keycloak.osgi.import>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.core</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.enterprise</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-runtime</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-spi</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-api</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.undertow</groupId>
|
||||||
|
<artifactId>undertow-servlet</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-undertow-adapter</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel</groupId>
|
||||||
|
<artifactId>camel-undertow</artifactId>
|
||||||
|
<version>2.21.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.camel</groupId>
|
||||||
|
<artifactId>camel-core</artifactId>
|
||||||
|
<version>2.21.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.felix</groupId>
|
||||||
|
<artifactId>maven-bundle-plugin</artifactId>
|
||||||
|
<extensions>true</extensions>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>bundle-manifest</id>
|
||||||
|
<phase>process-classes</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>manifest</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<instructions>
|
||||||
|
<Bundle-Name>${project.name}</Bundle-Name>
|
||||||
|
<Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName>
|
||||||
|
<Import-Package>${keycloak.osgi.import}</Import-Package>
|
||||||
|
<Export-Package>${keycloak.osgi.export}</Export-Package>
|
||||||
|
<Export-Service>org.apache.camel.spi.ComponentResolver;component=undertow-keycloak</Export-Service>
|
||||||
|
</instructions>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.camel.undertow;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.apache.camel.CamelContext;
|
||||||
|
import org.apache.camel.Consumer;
|
||||||
|
import org.apache.camel.Processor;
|
||||||
|
import org.apache.camel.component.undertow.RestUndertowHttpBinding;
|
||||||
|
import org.apache.camel.component.undertow.UndertowComponent;
|
||||||
|
import org.apache.camel.component.undertow.UndertowEndpoint;
|
||||||
|
import org.apache.camel.spi.RestConfiguration;
|
||||||
|
import org.apache.camel.util.FileUtil;
|
||||||
|
import org.apache.camel.util.HostUtils;
|
||||||
|
import org.apache.camel.util.ObjectHelper;
|
||||||
|
import org.apache.camel.util.URISupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class UndertowKeycloakComponent extends UndertowComponent {
|
||||||
|
|
||||||
|
public UndertowKeycloakComponent() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public UndertowKeycloakComponent(CamelContext context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected UndertowEndpoint createEndpointInstance(URI endpointUri, UndertowComponent component) throws URISyntaxException {
|
||||||
|
return new UndertowKeycloakEndpoint(endpointUri.toString(), component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: uncomment line below after backport of https://issues.apache.org/jira/browse/CAMEL-12514 into fuse
|
||||||
|
// @Override
|
||||||
|
protected String getComponentName() {
|
||||||
|
return "undertow-keycloak";
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove all below this line after backport of https://issues.apache.org/jira/browse/CAMEL-12514 into fuse
|
||||||
|
@Override
|
||||||
|
public Consumer createConsumer(CamelContext camelContext, Processor processor, String verb, String basePath, String uriTemplate,
|
||||||
|
String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters) throws Exception {
|
||||||
|
return doCreateConsumer(camelContext, processor, verb, basePath, uriTemplate, consumes, produces, configuration, parameters, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Consumer createApiConsumer(CamelContext camelContext, Processor processor, String contextPath,
|
||||||
|
RestConfiguration configuration, Map<String, Object> parameters) throws Exception {
|
||||||
|
// reuse the createConsumer method we already have. The api need to use GET and match on uri prefix
|
||||||
|
return doCreateConsumer(camelContext, processor, "GET", contextPath, null, null, null, configuration, parameters, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Consumer doCreateConsumer(CamelContext camelContext, Processor processor, String verb, String basePath, String uriTemplate,
|
||||||
|
String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters, boolean api) throws Exception {
|
||||||
|
String path = basePath;
|
||||||
|
if (uriTemplate != null) {
|
||||||
|
// make sure to avoid double slashes
|
||||||
|
if (uriTemplate.startsWith("/")) {
|
||||||
|
path = path + uriTemplate;
|
||||||
|
} else {
|
||||||
|
path = path + "/" + uriTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path = FileUtil.stripLeadingSeparator(path);
|
||||||
|
String scheme = "http";
|
||||||
|
String host = "";
|
||||||
|
int port = 0;
|
||||||
|
|
||||||
|
RestConfiguration config = configuration;
|
||||||
|
if (config == null) {
|
||||||
|
config = camelContext.getRestConfiguration(getComponentName(), true);
|
||||||
|
}
|
||||||
|
if (config.getScheme() != null) {
|
||||||
|
scheme = config.getScheme();
|
||||||
|
}
|
||||||
|
if (config.getHost() != null) {
|
||||||
|
host = config.getHost();
|
||||||
|
}
|
||||||
|
int num = config.getPort();
|
||||||
|
if (num > 0) {
|
||||||
|
port = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix path with context-path if configured in rest-dsl configuration
|
||||||
|
String contextPath = config.getContextPath();
|
||||||
|
if (ObjectHelper.isNotEmpty(contextPath)) {
|
||||||
|
contextPath = FileUtil.stripTrailingSeparator(contextPath);
|
||||||
|
contextPath = FileUtil.stripLeadingSeparator(contextPath);
|
||||||
|
if (ObjectHelper.isNotEmpty(contextPath)) {
|
||||||
|
path = contextPath + "/" + path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no explicit hostname set then resolve the hostname
|
||||||
|
if (ObjectHelper.isEmpty(host)) {
|
||||||
|
if (config.getRestHostNameResolver() == RestConfiguration.RestHostNameResolver.allLocalIp) {
|
||||||
|
host = "0.0.0.0";
|
||||||
|
} else if (config.getRestHostNameResolver() == RestConfiguration.RestHostNameResolver.localHostName) {
|
||||||
|
host = HostUtils.getLocalHostName();
|
||||||
|
} else if (config.getRestHostNameResolver() == RestConfiguration.RestHostNameResolver.localIp) {
|
||||||
|
host = HostUtils.getLocalIp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> map = new HashMap<String, Object>();
|
||||||
|
// build query string, and append any endpoint configuration properties
|
||||||
|
if (config.getComponent() == null || config.getComponent().equals(getComponentName())) {
|
||||||
|
// setup endpoint options
|
||||||
|
if (config.getEndpointProperties() != null && !config.getEndpointProperties().isEmpty()) {
|
||||||
|
map.putAll(config.getEndpointProperties());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean explicitOptions = true;
|
||||||
|
// must use upper case for restrict
|
||||||
|
String restrict = verb.toUpperCase(Locale.US);
|
||||||
|
// allow OPTIONS in rest-dsl to allow clients to call the API and have responses with ALLOW headers
|
||||||
|
if (!restrict.contains("OPTIONS")) {
|
||||||
|
restrict += ",OPTIONS";
|
||||||
|
// this is not an explicit OPTIONS path in the rest-dsl
|
||||||
|
explicitOptions = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean cors = config.isEnableCORS();
|
||||||
|
if (cors) {
|
||||||
|
// allow HTTP Options as we want to handle CORS in rest-dsl
|
||||||
|
map.put("optionsEnabled", "true");
|
||||||
|
} else if (explicitOptions) {
|
||||||
|
// the rest-dsl is using OPTIONS
|
||||||
|
map.put("optionsEnabled", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
String query = URISupport.createQueryString(map);
|
||||||
|
|
||||||
|
String url;
|
||||||
|
if (api) {
|
||||||
|
url = getComponentName() + ":%s://%s:%s/%s?matchOnUriPrefix=true&httpMethodRestrict=%s";
|
||||||
|
} else {
|
||||||
|
url = getComponentName() + ":%s://%s:%s/%s?matchOnUriPrefix=false&httpMethodRestrict=%s";
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the endpoint
|
||||||
|
url = String.format(url, scheme, host, port, path, restrict);
|
||||||
|
|
||||||
|
if (!query.isEmpty()) {
|
||||||
|
url = url + "&" + query;
|
||||||
|
}
|
||||||
|
|
||||||
|
UndertowEndpoint endpoint = camelContext.getEndpoint(url, UndertowEndpoint.class);
|
||||||
|
setProperties(camelContext, endpoint, parameters);
|
||||||
|
|
||||||
|
if (!map.containsKey("undertowHttpBinding")) {
|
||||||
|
// use the rest binding, if not using a custom http binding
|
||||||
|
endpoint.setUndertowHttpBinding(new RestUndertowHttpBinding());
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure consumer properties
|
||||||
|
Consumer consumer = endpoint.createConsumer(processor);
|
||||||
|
if (config.getConsumerProperties() != null && !config.getConsumerProperties().isEmpty()) {
|
||||||
|
setProperties(camelContext, consumer, config.getConsumerProperties());
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumer;
|
||||||
|
}
|
||||||
|
}
|
||||||
+219
@@ -0,0 +1,219 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.camel.undertow;
|
||||||
|
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.AuthenticatedActionsHandler;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.NodesRegistrationManagement;
|
||||||
|
import org.keycloak.adapters.PreAuthActionsHandler;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
import org.keycloak.adapters.spi.AuthChallenge;
|
||||||
|
import org.keycloak.adapters.spi.AuthOutcome;
|
||||||
|
import org.keycloak.adapters.spi.HttpFacade;
|
||||||
|
import org.keycloak.adapters.spi.InMemorySessionIdMapper;
|
||||||
|
import org.keycloak.adapters.spi.SessionIdMapper;
|
||||||
|
import org.keycloak.adapters.undertow.KeycloakUndertowAccount;
|
||||||
|
import org.keycloak.adapters.undertow.OIDCUndertowHttpFacade;
|
||||||
|
import org.keycloak.adapters.undertow.SessionManagementBridge;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowCookieTokenStore;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowRequestAuthenticator;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowSessionTokenStore;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
|
import io.undertow.security.api.SecurityContext;
|
||||||
|
import io.undertow.security.idm.Account;
|
||||||
|
import io.undertow.security.idm.Credential;
|
||||||
|
import io.undertow.security.idm.IdentityManager;
|
||||||
|
import io.undertow.security.impl.SecurityContextImpl;
|
||||||
|
import io.undertow.server.HttpServerExchange;
|
||||||
|
import io.undertow.server.session.InMemorySessionManager;
|
||||||
|
import io.undertow.server.session.SessionManager;
|
||||||
|
import io.undertow.util.AttachmentKey;
|
||||||
|
import io.undertow.util.StatusCodes;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.apache.camel.Processor;
|
||||||
|
import org.apache.camel.component.undertow.UndertowConsumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class UndertowKeycloakConsumer extends UndertowConsumer {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(UndertowKeycloakConsumer.class.getName());
|
||||||
|
|
||||||
|
public static final AttachmentKey<KeycloakPrincipal> KEYCLOAK_PRINCIPAL_KEY = AttachmentKey.create(KeycloakPrincipal.class);
|
||||||
|
|
||||||
|
private static final IdentityManager IDENTITY_MANAGER = new IdentityManager() {
|
||||||
|
@Override
|
||||||
|
public Account verify(Account account) {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account verify(String id, Credential credential) {
|
||||||
|
throw new IllegalStateException("Should never be called in Keycloak flow");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account verify(Credential credential) {
|
||||||
|
throw new IllegalStateException("Should never be called in Keycloak flow");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
|
||||||
|
|
||||||
|
protected final NodesRegistrationManagement nodesRegistrationManagement = new NodesRegistrationManagement();
|
||||||
|
|
||||||
|
private final UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement();
|
||||||
|
|
||||||
|
protected final AdapterDeploymentContext deploymentContext;
|
||||||
|
|
||||||
|
protected final SessionManager sessionManager;
|
||||||
|
|
||||||
|
protected final List<String> allowedRoles;
|
||||||
|
|
||||||
|
private final int confidentialPort;
|
||||||
|
|
||||||
|
private final Pattern skipPattern;
|
||||||
|
|
||||||
|
public UndertowKeycloakConsumer(UndertowKeycloakEndpoint endpoint, Processor processor,
|
||||||
|
AdapterDeploymentContext deploymentContext, Pattern skipPattern, List<String> allowedRoles, int confidentialPort) {
|
||||||
|
super(endpoint, processor);
|
||||||
|
this.sessionManager = new InMemorySessionManager(endpoint.getEndpointUri());
|
||||||
|
this.deploymentContext = deploymentContext;
|
||||||
|
this.skipPattern = skipPattern;
|
||||||
|
this.confidentialPort = confidentialPort;
|
||||||
|
this.allowedRoles = allowedRoles == null ? Collections.<String>emptyList() : allowedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getConfidentialPort() {
|
||||||
|
return confidentialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleRequest(HttpServerExchange httpExchange) throws Exception {
|
||||||
|
if (shouldSkip(httpExchange.getRequestPath())) {
|
||||||
|
super.handleRequest(httpExchange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//perform only non-blocking operation on exchange
|
||||||
|
if (httpExchange.isInIoThread()) {
|
||||||
|
httpExchange.dispatch(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OIDCUndertowHttpFacade facade = new OIDCUndertowHttpFacade(httpExchange);
|
||||||
|
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
|
||||||
|
|
||||||
|
if (deployment == null || !deployment.isConfigured()) {
|
||||||
|
httpExchange.setStatusCode(StatusCodes.FORBIDDEN);
|
||||||
|
LOG.fine("deployment not configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.fine("executing PreAuthActionsHandler");
|
||||||
|
SessionManagementBridge bridge = new SessionManagementBridge(userSessionManagement, sessionManager);
|
||||||
|
PreAuthActionsHandler preAuth = new PreAuthActionsHandler(bridge, deploymentContext, facade);
|
||||||
|
if (preAuth.handleRequest()) return;
|
||||||
|
|
||||||
|
SecurityContext securityContext = httpExchange.getSecurityContext();
|
||||||
|
if (securityContext == null) {
|
||||||
|
securityContext = new SecurityContextImpl(httpExchange, IDENTITY_MANAGER);
|
||||||
|
}
|
||||||
|
AdapterTokenStore tokenStore = getTokenStore(httpExchange, facade, deployment, securityContext);
|
||||||
|
tokenStore.checkCurrentToken();
|
||||||
|
|
||||||
|
LOG.fine("executing AuthenticatedActionsHandler");
|
||||||
|
RequestAuthenticator authenticator = new UndertowRequestAuthenticator(facade, deployment, confidentialPort, securityContext, httpExchange, tokenStore);
|
||||||
|
AuthOutcome outcome = authenticator.authenticate();
|
||||||
|
|
||||||
|
if (outcome == AuthOutcome.AUTHENTICATED) {
|
||||||
|
LOG.fine("AUTHENTICATED");
|
||||||
|
if (httpExchange.isResponseComplete()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AuthenticatedActionsHandler actions = new AuthenticatedActionsHandler(deployment, facade);
|
||||||
|
if (actions.handledRequest()) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
final Account authenticatedAccount = securityContext.getAuthenticatedAccount();
|
||||||
|
if (authenticatedAccount instanceof KeycloakUndertowAccount) {
|
||||||
|
final KeycloakUndertowAccount kua = (KeycloakUndertowAccount) authenticatedAccount;
|
||||||
|
httpExchange.putAttachment(KEYCLOAK_PRINCIPAL_KEY, (KeycloakPrincipal) kua.getPrincipal());
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> roles = Optional
|
||||||
|
.ofNullable(authenticatedAccount.getRoles())
|
||||||
|
.orElse((Set<String>) Collections.EMPTY_SET);
|
||||||
|
|
||||||
|
LOG.log(Level.FINE, "Allowed roles: {0}, current roles: {1}", new Object[] {allowedRoles, roles});
|
||||||
|
|
||||||
|
if (isRoleAllowed(roles, httpExchange)) {
|
||||||
|
super.handleRequest(httpExchange);
|
||||||
|
} else {
|
||||||
|
httpExchange.setStatusCode(StatusCodes.FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthChallenge challenge = authenticator.getChallenge();
|
||||||
|
if (challenge != null) {
|
||||||
|
LOG.fine("challenge");
|
||||||
|
challenge.challenge(facade);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpExchange.setStatusCode(StatusCodes.FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRoleAllowed(Set<String> roles, HttpServerExchange httpExchange) throws Exception {
|
||||||
|
for (String role : allowedRoles) {
|
||||||
|
if (roles.contains(role)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AdapterTokenStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, KeycloakDeployment deployment, SecurityContext securityContext) {
|
||||||
|
if (deployment.getTokenStore() == TokenStore.SESSION) {
|
||||||
|
return new UndertowSessionTokenStore(exchange, deployment, userSessionManagement, securityContext);
|
||||||
|
} else {
|
||||||
|
return new UndertowCookieTokenStore(facade, deployment, securityContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldSkip(String requestPath) {
|
||||||
|
return skipPattern != null && skipPattern.matcher(requestPath).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.camel.undertow;
|
||||||
|
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
|
import io.undertow.server.HttpServerExchange;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.apache.camel.Consumer;
|
||||||
|
import org.apache.camel.Exchange;
|
||||||
|
import org.apache.camel.Processor;
|
||||||
|
import org.apache.camel.component.undertow.UndertowComponent;
|
||||||
|
import org.apache.camel.component.undertow.UndertowEndpoint;
|
||||||
|
import static org.keycloak.adapters.camel.undertow.UndertowKeycloakConsumer.KEYCLOAK_PRINCIPAL_KEY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class UndertowKeycloakEndpoint extends UndertowEndpoint {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(UndertowKeycloakEndpoint.class.getName());
|
||||||
|
|
||||||
|
private KeycloakConfigResolver configResolver;
|
||||||
|
|
||||||
|
private AdapterConfig adapterConfig;
|
||||||
|
|
||||||
|
private String skipPattern;
|
||||||
|
|
||||||
|
private List<String> allowedRoles = Collections.emptyList();
|
||||||
|
|
||||||
|
private int confidentialPort = 8443;
|
||||||
|
|
||||||
|
public UndertowKeycloakEndpoint(String uri, UndertowComponent component) {
|
||||||
|
super(uri, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdapterConfig getAdapterConfig() {
|
||||||
|
return adapterConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdapterConfig(AdapterConfig adapterConfig) {
|
||||||
|
LOG.info("adapterConfig");
|
||||||
|
this.adapterConfig = adapterConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSkipPattern() {
|
||||||
|
return skipPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSkipPattern(String skipPattern) {
|
||||||
|
this.skipPattern = skipPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowedRoles() {
|
||||||
|
return allowedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedRoles(List<String> allowedRoles) {
|
||||||
|
this.allowedRoles = allowedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedRoles(String allowedRoles) {
|
||||||
|
this.allowedRoles = allowedRoles == null ? null : Arrays.asList(allowedRoles.split("\\s*,\\s*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getConfidentialPort() {
|
||||||
|
return confidentialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfidentialPort(int confidentialPort) {
|
||||||
|
this.confidentialPort = confidentialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeycloakConfigResolver getConfigResolver() {
|
||||||
|
return configResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfigResolver(KeycloakConfigResolver configResolver) {
|
||||||
|
this.configResolver = configResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Consumer createConsumer(Processor processor) throws Exception {
|
||||||
|
return new UndertowKeycloakConsumer(this, processor, getDeploymentContext(), getSkipPatternAsPattern(), computeAllowedRoles(), this.confidentialPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> computeAllowedRoles() {
|
||||||
|
List<String> res = this.allowedRoles == null ? Collections.<String>emptyList() : this.allowedRoles;
|
||||||
|
if (res.isEmpty()) {
|
||||||
|
LOG.warning("No roles were configured, Keycloak will deny every request");
|
||||||
|
}
|
||||||
|
LOG.log(Level.FINE, "Allowed roles: {0}", res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Exchange createExchange(HttpServerExchange httpExchange) throws Exception {
|
||||||
|
final Exchange res = super.createExchange(httpExchange);
|
||||||
|
|
||||||
|
KeycloakPrincipal principal = httpExchange.getAttachment(KEYCLOAK_PRINCIPAL_KEY);
|
||||||
|
LOG.log(Level.FINE, "principal: {0}", principal);
|
||||||
|
if (principal != null) {
|
||||||
|
res.setProperty(KeycloakPrincipal.class.getName(), principal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdapterDeploymentContext getDeploymentContext() {
|
||||||
|
if (configResolver != null) {
|
||||||
|
LOG.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolver.getClass());
|
||||||
|
return new AdapterDeploymentContext(configResolver);
|
||||||
|
} else if (adapterConfig != null) {
|
||||||
|
KeycloakDeployment kd = KeycloakDeploymentBuilder.build(adapterConfig);
|
||||||
|
return new AdapterDeploymentContext(kd);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.warning("Adapter is unconfigured, Keycloak will deny every request");
|
||||||
|
return new AdapterDeploymentContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pattern getSkipPatternAsPattern() {
|
||||||
|
return skipPattern == null
|
||||||
|
? null
|
||||||
|
: Pattern.compile(skipPattern, Pattern.DOTALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
class=org.keycloak.adapters.camel.undertow.UndertowKeycloakComponent
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-fuse7-integration-pom</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Beta3</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-pax-web-jetty94</artifactId>
|
||||||
|
<name>Keycloak Fuse 7.0 Adapter - Jetty 9.4</name>
|
||||||
|
<packaging>bundle</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<keycloak.osgi.export>
|
||||||
|
org.keycloak.adapters.osgi.jetty94.*;version="${project.version}"
|
||||||
|
</keycloak.osgi.export>
|
||||||
|
<keycloak.osgi.import>
|
||||||
|
!org.keycloak.adapters.osgi.jetty94,
|
||||||
|
org.keycloak.*;version="${project.version}",
|
||||||
|
*;resolution:=optional
|
||||||
|
</keycloak.osgi.import>
|
||||||
|
<keycloak.osgi.fragment>org.ops4j.pax.web.pax-web-jetty</keycloak.osgi.fragment>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-api</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-spi</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.core</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.enterprise</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-jetty94-adapter</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-security</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.felix</groupId>
|
||||||
|
<artifactId>maven-bundle-plugin</artifactId>
|
||||||
|
<extensions>true</extensions>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>bundle-manifest</id>
|
||||||
|
<phase>process-classes</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>manifest</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<instructions>
|
||||||
|
<Bundle-Name>${project.name}</Bundle-Name>
|
||||||
|
<Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName>
|
||||||
|
<Import-Package>${keycloak.osgi.import}</Import-Package>
|
||||||
|
<Export-Package>${keycloak.osgi.export}</Export-Package>
|
||||||
|
<Fragment-Host>${keycloak.osgi.fragment}</Fragment-Host>
|
||||||
|
</instructions>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.osgi.jetty94;
|
||||||
|
|
||||||
|
import org.ops4j.pax.web.service.AuthenticatorService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class KeycloakAuthenticatorService implements AuthenticatorService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getAuthenticatorService(String method, Class<T> iface) {
|
||||||
|
if (method == null || iface != org.eclipse.jetty.security.Authenticator.class) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("KEYCLOAK".equalsIgnoreCase(method)) {
|
||||||
|
return iface.cast(new org.keycloak.adapters.jetty.KeycloakJettyAuthenticator());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+275
@@ -0,0 +1,275 @@
|
|||||||
|
/*
|
||||||
|
* 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.osgi.jetty94;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.security.ConstraintMapping;
|
||||||
|
import org.eclipse.jetty.util.security.Constraint;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.ops4j.pax.web.service.WebContainer;
|
||||||
|
import org.ops4j.pax.web.service.spi.model.SecurityConstraintMappingModel;
|
||||||
|
import org.osgi.framework.BundleContext;
|
||||||
|
import org.osgi.framework.ServiceReference;
|
||||||
|
import org.osgi.service.http.HttpContext;
|
||||||
|
import org.osgi.util.tracker.ServiceTracker;
|
||||||
|
import org.osgi.util.tracker.ServiceTrackerCustomizer;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration with pax-web, which allows to inject custom jetty-web.xml configuration from current bundle classpath into {@link WebContainer}
|
||||||
|
* and allows to inject custom security constraint for securing resources by Keycloak.
|
||||||
|
*
|
||||||
|
* <p>It assumes that pax-web {@link WebContainer} is used as implementation of OSGI {@link org.osgi.service.http.HttpService}, which
|
||||||
|
* is true in karaf/fuse environment</p>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class PaxWebIntegrationService {
|
||||||
|
|
||||||
|
protected static final Logger log = Logger.getLogger(PaxWebIntegrationService.class);
|
||||||
|
|
||||||
|
private BundleContext bundleContext;
|
||||||
|
private String jettyWebXmlLocation;
|
||||||
|
private List<Object> constraintMappings;
|
||||||
|
|
||||||
|
private ServiceTracker webContainerTracker;
|
||||||
|
private HttpContext httpContext;
|
||||||
|
|
||||||
|
public BundleContext getBundleContext() {
|
||||||
|
return bundleContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBundleContext(BundleContext bundleContext) {
|
||||||
|
this.bundleContext = bundleContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getJettyWebXmlLocation() {
|
||||||
|
return jettyWebXmlLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJettyWebXmlLocation(String jettyWebXmlLocation) {
|
||||||
|
this.jettyWebXmlLocation = jettyWebXmlLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Object> getConstraintMappings() {
|
||||||
|
return constraintMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstraintMappings(List<Object> constraintMappings) {
|
||||||
|
this.constraintMappings = constraintMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ServiceTracker getWebContainerTracker() {
|
||||||
|
return webContainerTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpContext getHttpContext() {
|
||||||
|
return httpContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
ServiceTrackerCustomizer trackerCustomizer = new ServiceTrackerCustomizer() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object addingService(ServiceReference reference) {
|
||||||
|
return addingWebContainerCallback(reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modifiedService(ServiceReference reference, Object service) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removedService(ServiceReference reference, Object service) {
|
||||||
|
removingWebContainerCallback(reference);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
webContainerTracker = new ServiceTracker(bundleContext, WebContainer.class.getName(), trackerCustomizer);
|
||||||
|
webContainerTracker.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
webContainerTracker.remove(webContainerTracker.getServiceReference());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected WebContainer addingWebContainerCallback(ServiceReference webContainerServiceReference) {
|
||||||
|
WebContainer service = (WebContainer) bundleContext.getService(webContainerServiceReference);
|
||||||
|
httpContext = service.createDefaultHttpContext();
|
||||||
|
|
||||||
|
addJettyWebXml(service);
|
||||||
|
|
||||||
|
if (constraintMappings == null) {
|
||||||
|
throw new IllegalStateException("constraintMappings was null!");
|
||||||
|
}
|
||||||
|
List<ConstraintHandler> handlers = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
handlers.add(new JettyConstraintHandler());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
handlers.add(new PaxWebConstraintHandler());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
for (Object constraintMapping : constraintMappings) {
|
||||||
|
boolean handled = false;
|
||||||
|
for (ConstraintHandler handler : handlers) {
|
||||||
|
handled |= handler.addConstraintMapping(httpContext, service, constraintMapping);
|
||||||
|
}
|
||||||
|
if (!handled) {
|
||||||
|
log.warnv("Unable to add constraint mapping for constraint of type " + constraintMapping.getClass().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.registerLoginConfig("BASIC", "does-not-matter", null, null, httpContext);
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addJettyWebXml(WebContainer service) {
|
||||||
|
String jettyWebXmlLoc;
|
||||||
|
if (this.jettyWebXmlLocation == null) {
|
||||||
|
jettyWebXmlLoc = "/WEB-INF/jetty-web.xml";
|
||||||
|
} else {
|
||||||
|
jettyWebXmlLoc = this.jettyWebXmlLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
URL jettyWebXml = bundleContext.getBundle().getResource(jettyWebXmlLoc);
|
||||||
|
if (jettyWebXml != null) {
|
||||||
|
log.debug("Found jetty-web XML configuration on bundle classpath on " + jettyWebXmlLoc);
|
||||||
|
service.registerJettyWebXml(jettyWebXml, httpContext);
|
||||||
|
} else {
|
||||||
|
log.debug("Not found jetty-web XML configuration on bundle classpath on " + jettyWebXmlLoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addConstraintMapping(WebContainer service, SecurityConstraintMappingModel constraintMapping) {
|
||||||
|
String name = constraintMapping.getConstraintName();
|
||||||
|
if (name == null) {
|
||||||
|
name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getUrl() + ", dataConstraint=" + constraintMapping.getDataConstraint() + ", canAuthenticate="
|
||||||
|
+ constraintMapping.isAuthentication() + ", roles=" + constraintMapping.getRoles());
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getUrl(), constraintMapping.getMapping(), constraintMapping.getDataConstraint(), constraintMapping.isAuthentication(), constraintMapping.getRoles(), httpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addConstraintMapping(WebContainer service, ConstraintMapping constraintMapping) {
|
||||||
|
Constraint constraint = constraintMapping.getConstraint();
|
||||||
|
String[] roles = constraint.getRoles();
|
||||||
|
// name property is unavailable on constraint object :/
|
||||||
|
|
||||||
|
String name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
|
||||||
|
int dataConstraint = constraint.getDataConstraint();
|
||||||
|
String dataConstraintStr;
|
||||||
|
switch (dataConstraint) {
|
||||||
|
case Constraint.DC_UNSET: dataConstraintStr = null; break;
|
||||||
|
case Constraint.DC_NONE: dataConstraintStr = "NONE"; break;
|
||||||
|
case Constraint.DC_CONFIDENTIAL: dataConstraintStr = "CONFIDENTIAL"; break;
|
||||||
|
case Constraint.DC_INTEGRAL: dataConstraintStr = "INTEGRAL"; break;
|
||||||
|
default:
|
||||||
|
log.warnv("Unknown data constraint: " + dataConstraint);
|
||||||
|
dataConstraintStr = "CONFIDENTIAL";
|
||||||
|
}
|
||||||
|
List<String> rolesList = Arrays.asList(roles);
|
||||||
|
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getPathSpec() + ", dataConstraint=" + dataConstraintStr + ", canAuthenticate="
|
||||||
|
+ constraint.getAuthenticate() + ", roles=" + rolesList);
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getPathSpec(), null, dataConstraintStr, constraint.getAuthenticate(), rolesList, httpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void removingWebContainerCallback(ServiceReference serviceReference) {
|
||||||
|
WebContainer service = (WebContainer)bundleContext.getService(serviceReference);
|
||||||
|
if (service != null) {
|
||||||
|
service.unregisterLoginConfig(httpContext);
|
||||||
|
service.unregisterConstraintMapping(httpContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ConstraintHandler {
|
||||||
|
boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PaxWebConstraintHandler implements ConstraintHandler {
|
||||||
|
|
||||||
|
public boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm) {
|
||||||
|
if (cm instanceof SecurityConstraintMappingModel) {
|
||||||
|
SecurityConstraintMappingModel constraintMapping = (SecurityConstraintMappingModel) cm;
|
||||||
|
String name = constraintMapping.getConstraintName();
|
||||||
|
if (name == null) {
|
||||||
|
name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getUrl() + ", dataConstraint=" + constraintMapping.getDataConstraint() + ", canAuthenticate="
|
||||||
|
+ constraintMapping.isAuthentication() + ", roles=" + constraintMapping.getRoles());
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getUrl(), constraintMapping.getMapping(), constraintMapping.getDataConstraint(), constraintMapping.isAuthentication(), constraintMapping.getRoles(), httpContext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class JettyConstraintHandler implements ConstraintHandler {
|
||||||
|
|
||||||
|
public boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm) {
|
||||||
|
if (cm instanceof ConstraintMapping) {
|
||||||
|
ConstraintMapping constraintMapping = (ConstraintMapping) cm;
|
||||||
|
Constraint constraint = constraintMapping.getConstraint();
|
||||||
|
String[] roles = constraint.getRoles();
|
||||||
|
// name property is unavailable on constraint object :/
|
||||||
|
|
||||||
|
String name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
|
||||||
|
int dataConstraint = constraint.getDataConstraint();
|
||||||
|
String dataConstraintStr;
|
||||||
|
switch (dataConstraint) {
|
||||||
|
case Constraint.DC_UNSET:
|
||||||
|
dataConstraintStr = null;
|
||||||
|
break;
|
||||||
|
case Constraint.DC_NONE:
|
||||||
|
dataConstraintStr = "NONE";
|
||||||
|
break;
|
||||||
|
case Constraint.DC_CONFIDENTIAL:
|
||||||
|
dataConstraintStr = "CONFIDENTIAL";
|
||||||
|
break;
|
||||||
|
case Constraint.DC_INTEGRAL:
|
||||||
|
dataConstraintStr = "INTEGRAL";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warnv("Unknown data constraint: " + dataConstraint);
|
||||||
|
dataConstraintStr = "CONFIDENTIAL";
|
||||||
|
}
|
||||||
|
List<String> rolesList = Arrays.asList(roles);
|
||||||
|
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getPathSpec() + ", dataConstraint=" + dataConstraintStr + ", canAuthenticate="
|
||||||
|
+ constraint.getAuthenticate() + ", roles=" + rolesList);
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getPathSpec(), null, dataConstraintStr, constraint.getAuthenticate(), rolesList, httpContext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
org.keycloak.adapters.osgi.jetty94.KeycloakAuthenticatorService
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Beta3</version>
|
||||||
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-fuse7-integration-pom</artifactId>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
<name>Keycloak Fuse 7.0 Integration</name>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<fuse7.version>7.0.1-SNAPSHOT</fuse7.version>
|
||||||
|
<tomcat.version>8.0.14</tomcat.version>
|
||||||
|
<jetty9.version>9.4.2.v20170220</jetty9.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>camel-undertow</module>
|
||||||
|
<module>jetty94</module>
|
||||||
|
<module>tomcat8</module>
|
||||||
|
<module>undertow</module>
|
||||||
|
</modules>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-fuse7-integration-pom</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Beta3</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-pax-web-tomcat8</artifactId>
|
||||||
|
<name>Keycloak Fuse 7.0 Adapter - Tomcat 8</name>
|
||||||
|
<packaging>bundle</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<keycloak.osgi.export>
|
||||||
|
org.keycloak.adapters.osgi.tomcat.*;version="${project.version}"
|
||||||
|
</keycloak.osgi.export>
|
||||||
|
<keycloak.osgi.import>
|
||||||
|
!org.keycloak.adapters.osgi.tomcat,
|
||||||
|
org.keycloak.*;version="${project.version}",
|
||||||
|
*;resolution:=optional
|
||||||
|
</keycloak.osgi.import>
|
||||||
|
<keycloak.osgi.fragment>org.ops4j.pax.web.pax-web-tomcat</keycloak.osgi.fragment>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-api</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-tomcat8-adapter</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tomcat</groupId>
|
||||||
|
<artifactId>tomcat-catalina</artifactId>
|
||||||
|
<version>${tomcat.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.felix</groupId>
|
||||||
|
<artifactId>maven-bundle-plugin</artifactId>
|
||||||
|
<extensions>true</extensions>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>bundle-manifest</id>
|
||||||
|
<phase>process-classes</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>manifest</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<instructions>
|
||||||
|
<Bundle-Name>${project.name}</Bundle-Name>
|
||||||
|
<Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName>
|
||||||
|
<Import-Package>${keycloak.osgi.import}</Import-Package>
|
||||||
|
<Export-Package>${keycloak.osgi.export}</Export-Package>
|
||||||
|
<Fragment-Host>${keycloak.osgi.fragment}</Fragment-Host>
|
||||||
|
</instructions>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.osgi.tomcat;
|
||||||
|
|
||||||
|
import org.ops4j.pax.web.service.AuthenticatorService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class KeycloakAuthenticatorService implements AuthenticatorService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getAuthenticatorService(String method, Class<T> iface) {
|
||||||
|
if (method == null || iface != org.apache.catalina.Valve.class) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("KEYCLOAK".equalsIgnoreCase(method)) {
|
||||||
|
return iface.cast(new org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
org.keycloak.adapters.osgi.tomcat.KeycloakAuthenticatorService
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-fuse7-integration-pom</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Beta3</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-pax-web-undertow</artifactId>
|
||||||
|
<name>Keycloak Fuse 7.0 Adapter - Undertow</name>
|
||||||
|
<packaging>bundle</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<keycloak.osgi.export>
|
||||||
|
org.keycloak.adapters.osgi.undertow.*;version="${project.version}"
|
||||||
|
</keycloak.osgi.export>
|
||||||
|
<keycloak.osgi.import>
|
||||||
|
!org.keycloak.adapters.osgi.undertow,
|
||||||
|
org.keycloak.*;version="${project.version}",
|
||||||
|
org.keycloak.adapters.osgi.*;version="${project.version}",
|
||||||
|
org.ops4j.pax.web.*;version="[3.0,8)",
|
||||||
|
javax.servlet.*;version="[2.5,4)";resolution:=optional,
|
||||||
|
org.apache.cxf.transport.http;resolution:=optional;version="[3,4)",
|
||||||
|
org.apache.cxf.transport.http_undertow;resolution:=optional;version="[3,4)",
|
||||||
|
org.apache.cxf.transport.servlet;resolution:=optional;version="[3,4)",
|
||||||
|
io.undertow.*,
|
||||||
|
*;resolution:=optional
|
||||||
|
</keycloak.osgi.import>
|
||||||
|
<keycloak.osgi.fragment>org.ops4j.pax.web.pax-web-undertow</keycloak.osgi.fragment>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.core</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.osgi</groupId>
|
||||||
|
<artifactId>org.osgi.enterprise</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-runtime</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-spi</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ops4j.pax.web</groupId>
|
||||||
|
<artifactId>pax-web-api</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.cxf</groupId>
|
||||||
|
<artifactId>cxf-rt-transports-http</artifactId>
|
||||||
|
<version>${cxf.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.cxf</groupId>
|
||||||
|
<artifactId>cxf-rt-transports-http-undertow</artifactId>
|
||||||
|
<version>${cxf.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.undertow</groupId>
|
||||||
|
<artifactId>undertow-servlet</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-undertow-adapter</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-osgi-adapter</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.felix</groupId>
|
||||||
|
<artifactId>maven-bundle-plugin</artifactId>
|
||||||
|
<extensions>true</extensions>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>bundle-manifest</id>
|
||||||
|
<phase>process-classes</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>manifest</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<instructions>
|
||||||
|
<Bundle-Name>${project.name}</Bundle-Name>
|
||||||
|
<Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName>
|
||||||
|
<Import-Package>${keycloak.osgi.import}</Import-Package>
|
||||||
|
<Export-Package>${keycloak.osgi.export}</Export-Package>
|
||||||
|
<Fragment-Host>${keycloak.osgi.fragment}</Fragment-Host>
|
||||||
|
</instructions>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.osgi.undertow;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
|
import org.keycloak.adapters.NodesRegistrationManagement;
|
||||||
|
import org.keycloak.adapters.spi.InMemorySessionIdMapper;
|
||||||
|
import org.keycloak.adapters.spi.SessionIdMapper;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowAuthenticationMechanism;
|
||||||
|
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
|
||||||
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
|
import io.undertow.security.api.AuthenticationMechanism;
|
||||||
|
import io.undertow.security.api.AuthenticationMode;
|
||||||
|
import io.undertow.security.handlers.AuthenticationCallHandler;
|
||||||
|
import io.undertow.security.handlers.AuthenticationConstraintHandler;
|
||||||
|
import io.undertow.security.handlers.AuthenticationMechanismsHandler;
|
||||||
|
import io.undertow.security.handlers.SecurityInitialHandler;
|
||||||
|
import io.undertow.security.idm.Account;
|
||||||
|
import io.undertow.security.idm.Credential;
|
||||||
|
import io.undertow.security.idm.IdentityManager;
|
||||||
|
import io.undertow.server.HttpHandler;
|
||||||
|
import io.undertow.server.HttpServerExchange;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.apache.cxf.transport.http_undertow.CXFUndertowHttpHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class CxfKeycloakAuthHandler implements CXFUndertowHttpHandler {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(CxfKeycloakAuthHandler.class.getName());
|
||||||
|
|
||||||
|
private static final IdentityManager IDENTITY_MANAGER = new IdentityManager() {
|
||||||
|
@Override
|
||||||
|
public Account verify(Account account) {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account verify(String id, Credential credential) {
|
||||||
|
throw new IllegalStateException("Should never be called in Keycloak flow");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account verify(Credential credential) {
|
||||||
|
throw new IllegalStateException("Should never be called in Keycloak flow");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement();
|
||||||
|
|
||||||
|
protected final NodesRegistrationManagement nodesRegistrationManagement = new NodesRegistrationManagement();
|
||||||
|
|
||||||
|
protected final SessionIdMapper idMapper = new InMemorySessionIdMapper();
|
||||||
|
|
||||||
|
private final AtomicReference<HttpHandler> securityHandler = new AtomicReference<>();
|
||||||
|
|
||||||
|
private Pattern skipPattern;
|
||||||
|
|
||||||
|
private int confidentialPort = 8443;
|
||||||
|
|
||||||
|
private HttpHandler next;
|
||||||
|
|
||||||
|
private KeycloakConfigResolver configResolver;
|
||||||
|
|
||||||
|
private AdapterConfig adapterConfig;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleRequest(HttpServerExchange exchange) throws Exception {
|
||||||
|
if (shouldSkip(exchange.getRequestPath())) {
|
||||||
|
next.handleRequest(exchange);
|
||||||
|
} else {
|
||||||
|
getSecurityHandler().handleRequest(exchange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpHandler getSecurityHandler() {
|
||||||
|
if (this.securityHandler.get() == null) {
|
||||||
|
HttpHandler handler = this.next;
|
||||||
|
|
||||||
|
handler = new AuthenticationCallHandler(handler);
|
||||||
|
handler = new AuthenticationConstraintHandler(handler);
|
||||||
|
|
||||||
|
AdapterDeploymentContext deploymentContext = buildDeploymentContext();
|
||||||
|
|
||||||
|
final List<AuthenticationMechanism> mechanisms
|
||||||
|
= Collections.<AuthenticationMechanism>singletonList(
|
||||||
|
new UndertowAuthenticationMechanism(deploymentContext, userSessionManagement, nodesRegistrationManagement, confidentialPort, null));
|
||||||
|
handler = new AuthenticationMechanismsHandler(handler, mechanisms);
|
||||||
|
|
||||||
|
this.securityHandler.compareAndSet(null, new SecurityInitialHandler(AuthenticationMode.PRO_ACTIVE, IDENTITY_MANAGER, "KEYCLOAK", handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.securityHandler.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdapterDeploymentContext buildDeploymentContext() {
|
||||||
|
if (configResolver != null) {
|
||||||
|
LOG.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolver.getClass());
|
||||||
|
return new AdapterDeploymentContext(configResolver);
|
||||||
|
} else if (adapterConfig != null) {
|
||||||
|
KeycloakDeployment kd = KeycloakDeploymentBuilder.build(adapterConfig);
|
||||||
|
return new AdapterDeploymentContext(kd);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.warning("Adapter is unconfigured, Keycloak will deny every request");
|
||||||
|
return new AdapterDeploymentContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNext(HttpHandler nextHandler) {
|
||||||
|
this.next = nextHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldSkip(String requestPath) {
|
||||||
|
return skipPattern != null && skipPattern.matcher(requestPath).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeycloakConfigResolver getConfigResolver() {
|
||||||
|
return configResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfigResolver(KeycloakConfigResolver configResolver) {
|
||||||
|
this.configResolver = configResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getConfidentialPort() {
|
||||||
|
return confidentialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfidentialPort(int confidentialPort) {
|
||||||
|
this.confidentialPort = confidentialPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdapterConfig getAdapterConfig() {
|
||||||
|
return adapterConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdapterConfig(AdapterConfig adapterConfig) {
|
||||||
|
this.adapterConfig = adapterConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSkipPattern() {
|
||||||
|
return skipPattern.pattern();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSkipPattern(String skipPattern) {
|
||||||
|
this.skipPattern = Pattern.compile(skipPattern, Pattern.DOTALL);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.adapters.osgi.undertow;
|
||||||
|
|
||||||
|
import org.ops4j.pax.web.service.AuthenticatorService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class KeycloakAuthenticatorService implements AuthenticatorService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getAuthenticatorService(String method, Class<T> iface) {
|
||||||
|
if (method == null || iface != io.undertow.servlet.ServletExtension.class) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("KEYCLOAK".equalsIgnoreCase(method)) {
|
||||||
|
return iface.cast(new org.keycloak.adapters.undertow.KeycloakServletExtension());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* 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.osgi.undertow;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.osgi.PaxWebSecurityConstraintMapping;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.ops4j.pax.web.service.WebContainer;
|
||||||
|
import org.osgi.framework.BundleContext;
|
||||||
|
import org.osgi.framework.ServiceReference;
|
||||||
|
import org.osgi.service.http.HttpContext;
|
||||||
|
import org.osgi.util.tracker.ServiceTracker;
|
||||||
|
import org.osgi.util.tracker.ServiceTrackerCustomizer;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration with pax-web, which allows to inject custom security constraint for securing resources by Keycloak.
|
||||||
|
*
|
||||||
|
* <p>It assumes that pax-web {@link WebContainer} is used as implementation of OSGI {@link org.osgi.service.http.HttpService}, which
|
||||||
|
* is true in karaf/fuse environment</p>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class PaxWebIntegrationService {
|
||||||
|
|
||||||
|
protected static final Logger log = Logger.getLogger(PaxWebIntegrationService.class);
|
||||||
|
|
||||||
|
private BundleContext bundleContext;
|
||||||
|
private List<PaxWebSecurityConstraintMapping> constraintMappings;
|
||||||
|
|
||||||
|
private ServiceTracker webContainerTracker;
|
||||||
|
private HttpContext httpContext;
|
||||||
|
|
||||||
|
public BundleContext getBundleContext() {
|
||||||
|
return bundleContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBundleContext(BundleContext bundleContext) {
|
||||||
|
this.bundleContext = bundleContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PaxWebSecurityConstraintMapping> getConstraintMappings() {
|
||||||
|
return constraintMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstraintMappings(List<PaxWebSecurityConstraintMapping> constraintMappings) {
|
||||||
|
this.constraintMappings = constraintMappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ServiceTracker getWebContainerTracker() {
|
||||||
|
return webContainerTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpContext getHttpContext() {
|
||||||
|
return httpContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
ServiceTrackerCustomizer trackerCustomizer = new ServiceTrackerCustomizer() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object addingService(ServiceReference reference) {
|
||||||
|
return addingWebContainerCallback(reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modifiedService(ServiceReference reference, Object service) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removedService(ServiceReference reference, Object service) {
|
||||||
|
removingWebContainerCallback(reference);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
webContainerTracker = new ServiceTracker(bundleContext, WebContainer.class.getName(), trackerCustomizer);
|
||||||
|
webContainerTracker.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
webContainerTracker.remove(webContainerTracker.getServiceReference());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected WebContainer addingWebContainerCallback(ServiceReference webContainerServiceReference) {
|
||||||
|
WebContainer service = (WebContainer) bundleContext.getService(webContainerServiceReference);
|
||||||
|
httpContext = service.createDefaultHttpContext();
|
||||||
|
|
||||||
|
if (constraintMappings == null) {
|
||||||
|
throw new IllegalStateException("constraintMappings was null!");
|
||||||
|
}
|
||||||
|
for (PaxWebSecurityConstraintMapping constraintMapping : constraintMappings) {
|
||||||
|
addConstraintMapping(service, constraintMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
service.registerLoginConfig("KEYCLOAK", "specified-in-keycloak-json", null, null, httpContext);
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addConstraintMapping(WebContainer service, PaxWebSecurityConstraintMapping cm) {
|
||||||
|
log.debug("Adding security constraint name=" + cm.getConstraintName() + ", url=" + cm.getUrl() + ", dataConstraint=" + cm.getDataConstraint() + ", canAuthenticate="
|
||||||
|
+ cm.isAuthentication() + ", roles=" + cm.getRoles());
|
||||||
|
service.registerConstraintMapping(
|
||||||
|
cm.getConstraintName(),
|
||||||
|
cm.getMapping(),
|
||||||
|
cm.getUrl(),
|
||||||
|
cm.getDataConstraint(),
|
||||||
|
cm.isAuthentication(),
|
||||||
|
cm.getRoles(),
|
||||||
|
httpContext
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void removingWebContainerCallback(ServiceReference serviceReference) {
|
||||||
|
WebContainer service = (WebContainer)bundleContext.getService(serviceReference);
|
||||||
|
if (service != null) {
|
||||||
|
service.unregisterLoginConfig(httpContext);
|
||||||
|
service.unregisterConstraintMapping(httpContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
org.keycloak.adapters.osgi.undertow.KeycloakAuthenticatorService
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+702
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-281
@@ -1,281 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.keycloak.adapters.KeycloakDeployment;
|
|
||||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
|
||||||
import org.keycloak.adapters.ServerRequest;
|
|
||||||
import org.keycloak.common.util.Time;
|
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
|
||||||
import org.keycloak.util.JsonSerialization;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
|
||||||
* @version $Revision: 1 $
|
|
||||||
*/
|
|
||||||
public class KeycloakCliSso {
|
|
||||||
|
|
||||||
public void mainCmd(String[] args) throws Exception {
|
|
||||||
if (args.length != 1) {
|
|
||||||
printHelp();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args[0].equalsIgnoreCase("login")) {
|
|
||||||
login();
|
|
||||||
} else if (args[0].equalsIgnoreCase("login-manual")) {
|
|
||||||
loginManual();
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
else if (args[0].equalsIgnoreCase("login-cli")) {
|
|
||||||
loginCli();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
else if (args[0].equalsIgnoreCase("token")) {
|
|
||||||
token();
|
|
||||||
} else if (args[0].equalsIgnoreCase("logout")) {
|
|
||||||
logout();
|
|
||||||
} else if (args[0].equalsIgnoreCase("env")) {
|
|
||||||
System.out.println(System.getenv().toString());
|
|
||||||
} else {
|
|
||||||
printHelp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void printHelp() {
|
|
||||||
System.err.println("Commands:");
|
|
||||||
System.err.println(" login - login with desktop browser if available, otherwise do manual login. Output is access token.");
|
|
||||||
System.err.println(" login-manual - manual login");
|
|
||||||
//System.err.println(" login-cli - attempt Keycloak proprietary cli protocol. Otherwise do normal login");
|
|
||||||
System.err.println(" token - print access token if logged in");
|
|
||||||
System.err.println(" logout - logout.");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AdapterConfig getConfig() {
|
|
||||||
String url = System.getProperty("KEYCLOAK_AUTH_SERVER");
|
|
||||||
if (url == null) {
|
|
||||||
System.err.println("KEYCLOAK_AUTH_SERVER property not set");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
String realm = System.getProperty("KEYCLOAK_REALM");
|
|
||||||
if (realm == null) {
|
|
||||||
System.err.println("KEYCLOAK_REALM property not set");
|
|
||||||
System.exit(1);
|
|
||||||
|
|
||||||
}
|
|
||||||
String client = System.getProperty("KEYCLOAK_CLIENT");
|
|
||||||
if (client == null) {
|
|
||||||
System.err.println("KEYCLOAK_CLIENT property not set");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
String secret = System.getProperty("KEYCLOAK_CLIENT_SECRET");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
AdapterConfig config = new AdapterConfig();
|
|
||||||
config.setAuthServerUrl(url);
|
|
||||||
config.setRealm(realm);
|
|
||||||
config.setResource(client);
|
|
||||||
config.setSslRequired("external");
|
|
||||||
if (secret != null) {
|
|
||||||
Map<String, Object> creds = new HashMap<>();
|
|
||||||
creds.put("secret", secret);
|
|
||||||
config.setCredentials(creds);
|
|
||||||
} else {
|
|
||||||
config.setPublicClient(true);
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean checkToken(boolean outputToken) throws Exception {
|
|
||||||
String token = getTokenResponse();
|
|
||||||
if (token == null) return false;
|
|
||||||
|
|
||||||
|
|
||||||
if (token != null) {
|
|
||||||
Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
|
|
||||||
if (m.find()) {
|
|
||||||
String json = m.group(0);
|
|
||||||
try {
|
|
||||||
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
|
||||||
if (Time.currentTime() < tokenResponse.getExpiresIn()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
AdapterConfig config = getConfig();
|
|
||||||
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
|
|
||||||
installed.refreshToken(tokenResponse.getRefreshToken());
|
|
||||||
processResponse(installed, outputToken);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Error processing existing token");
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getTokenResponse() throws IOException {
|
|
||||||
String token = null;
|
|
||||||
File tokenFile = getTokenFilePath();
|
|
||||||
if (tokenFile.exists()) {
|
|
||||||
FileInputStream fis = new FileInputStream(tokenFile);
|
|
||||||
byte[] data = new byte[(int) tokenFile.length()];
|
|
||||||
fis.read(data);
|
|
||||||
fis.close();
|
|
||||||
token = new String(data, "UTF-8");
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void token() throws Exception {
|
|
||||||
String token = getTokenResponse();
|
|
||||||
if (token == null) {
|
|
||||||
System.err.println("There is no token for client");
|
|
||||||
System.exit(1);
|
|
||||||
} else {
|
|
||||||
Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
|
|
||||||
if (m.find()) {
|
|
||||||
String json = m.group(0);
|
|
||||||
try {
|
|
||||||
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
|
||||||
if (Time.currentTime() < tokenResponse.getExpiresIn()) {
|
|
||||||
System.out.println(tokenResponse.getToken());
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
System.err.println("token in response file is expired");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Failure processing token response file");
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.err.println("Could not find json within token response file");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void login() throws Exception {
|
|
||||||
if (checkToken(true)) return;
|
|
||||||
AdapterConfig config = getConfig();
|
|
||||||
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
|
|
||||||
installed.login();
|
|
||||||
processResponse(installed, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loginCli() throws Exception {
|
|
||||||
if (checkToken(false)) return;
|
|
||||||
AdapterConfig config = getConfig();
|
|
||||||
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
|
|
||||||
if (!installed.loginCommandLine()) installed.login();
|
|
||||||
processResponse(installed, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 File getTokenDirectory() {
|
|
||||||
return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM")).toFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
public File getTokenFilePath() {
|
|
||||||
return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM"), System.getProperty("KEYCLOAK_CLIENT") + ".json").toFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processResponse(KeycloakInstalled installed, boolean outputToken) throws IOException {
|
|
||||||
AccessTokenResponse tokenResponse = installed.getTokenResponse();
|
|
||||||
tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
|
|
||||||
tokenResponse.setIdToken(null);
|
|
||||||
String output = JsonSerialization.writeValueAsString(tokenResponse);
|
|
||||||
getTokenDirectory().mkdirs();
|
|
||||||
FileOutputStream fos = new FileOutputStream(getTokenFilePath());
|
|
||||||
fos.write(output.getBytes("UTF-8"));
|
|
||||||
fos.flush();
|
|
||||||
fos.close();
|
|
||||||
if (outputToken) System.out.println(tokenResponse.getToken());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loginManual() throws Exception {
|
|
||||||
if (checkToken(true)) return;
|
|
||||||
AdapterConfig config = getConfig();
|
|
||||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
|
|
||||||
KeycloakInstalled installed = new KeycloakInstalled(deployment);
|
|
||||||
installed.loginManual();
|
|
||||||
processResponse(installed, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void logout() throws Exception {
|
|
||||||
String token = getTokenResponse();
|
|
||||||
if (token != null) {
|
|
||||||
Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
|
|
||||||
if (m.find()) {
|
|
||||||
String json = m.group(0);
|
|
||||||
try {
|
|
||||||
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
|
|
||||||
if (Time.currentTime() > tokenResponse.getExpiresIn()) {
|
|
||||||
System.err.println("Login is expired");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
AdapterConfig config = getConfig();
|
|
||||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
|
|
||||||
ServerRequest.invokeLogout(deployment, tokenResponse.getRefreshToken());
|
|
||||||
for (File fp : getTokenDirectory().listFiles()) fp.delete();
|
|
||||||
System.out.println("logout complete");
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Failure processing token response file");
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.err.println("Could not find json within token response file");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.err.println("Not logged in");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+223
-164
@@ -39,15 +39,7 @@ import javax.ws.rs.core.Form;
|
|||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.BufferedReader;
|
import java.io.*;
|
||||||
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.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@@ -65,6 +57,7 @@ public class KeycloakInstalled {
|
|||||||
|
|
||||||
public interface HttpResponseWriter {
|
public interface HttpResponseWriter {
|
||||||
void success(PrintWriter pw, KeycloakInstalled ki);
|
void success(PrintWriter pw, KeycloakInstalled ki);
|
||||||
|
|
||||||
void failure(PrintWriter pw, KeycloakInstalled ki);
|
void failure(PrintWriter pw, KeycloakInstalled ki);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,12 +79,12 @@ public class KeycloakInstalled {
|
|||||||
private Locale locale;
|
private Locale locale;
|
||||||
private HttpResponseWriter loginResponseWriter;
|
private HttpResponseWriter loginResponseWriter;
|
||||||
private HttpResponseWriter logoutResponseWriter;
|
private HttpResponseWriter logoutResponseWriter;
|
||||||
|
private ResteasyClient resteasyClient;
|
||||||
Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
|
Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
|
||||||
Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
|
Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
|
||||||
Pattern codePattern = Pattern.compile("code=([^&]+)");
|
Pattern codePattern = Pattern.compile("code=([^&]+)");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public KeycloakInstalled() {
|
public KeycloakInstalled() {
|
||||||
InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON);
|
InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON);
|
||||||
deployment = KeycloakDeploymentBuilder.build(config);
|
deployment = KeycloakDeploymentBuilder.build(config);
|
||||||
@@ -105,70 +98,12 @@ public class KeycloakInstalled {
|
|||||||
this.deployment = deployment;
|
this.deployment = deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpResponseWriter defaultLoginWriter = new HttpResponseWriter() {
|
|
||||||
@Override
|
|
||||||
public void success(PrintWriter pw, KeycloakInstalled ki) {
|
|
||||||
pw.println("HTTP/1.1 200 OK");
|
|
||||||
pw.println("Content-Type: text/html");
|
|
||||||
pw.println();
|
|
||||||
pw.println("<html><h1>Login completed.</h1><div>");
|
|
||||||
pw.println("This browser will remain logged in until you close it, logout, or the session expires.");
|
|
||||||
pw.println("</div></html>");
|
|
||||||
pw.flush();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void failure(PrintWriter pw, KeycloakInstalled ki) {
|
|
||||||
pw.println("HTTP/1.1 200 OK");
|
|
||||||
pw.println("Content-Type: text/html");
|
|
||||||
pw.println();
|
|
||||||
pw.println("<html><h1>Login attempt failed.</h1><div>");
|
|
||||||
pw.println("</div></html>");
|
|
||||||
pw.flush();
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static HttpResponseWriter defaultLogoutWriter = new HttpResponseWriter() {
|
|
||||||
@Override
|
|
||||||
public void success(PrintWriter pw, KeycloakInstalled ki) {
|
|
||||||
pw.println("HTTP/1.1 200 OK");
|
|
||||||
pw.println("Content-Type: text/html");
|
|
||||||
pw.println();
|
|
||||||
pw.println("<html><h1>Logout completed.</h1><div>");
|
|
||||||
pw.println("You may close this browser tab.");
|
|
||||||
pw.println("</div></html>");
|
|
||||||
pw.flush();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void failure(PrintWriter pw, KeycloakInstalled ki) {
|
|
||||||
pw.println("HTTP/1.1 200 OK");
|
|
||||||
pw.println("Content-Type: text/html");
|
|
||||||
pw.println();
|
|
||||||
pw.println("<html><h1>Logout failed.</h1><div>");
|
|
||||||
pw.println("You may close this browser tab.");
|
|
||||||
pw.println("</div></html>");
|
|
||||||
pw.flush();
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public HttpResponseWriter getLoginResponseWriter() {
|
public HttpResponseWriter getLoginResponseWriter() {
|
||||||
if (loginResponseWriter == null) {
|
return null;
|
||||||
return defaultLoginWriter;
|
|
||||||
} else {
|
|
||||||
return loginResponseWriter;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpResponseWriter getLogoutResponseWriter() {
|
public HttpResponseWriter getLogoutResponseWriter() {
|
||||||
if (logoutResponseWriter == null) {
|
return null;
|
||||||
return defaultLogoutWriter;
|
|
||||||
} else {
|
|
||||||
return logoutResponseWriter;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) {
|
public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) {
|
||||||
@@ -179,6 +114,10 @@ public class KeycloakInstalled {
|
|||||||
this.logoutResponseWriter = logoutResponseWriter;
|
this.logoutResponseWriter = logoutResponseWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setResteasyClient(ResteasyClient resteasyClient) {
|
||||||
|
this.resteasyClient = resteasyClient;
|
||||||
|
}
|
||||||
|
|
||||||
public Locale getLocale() {
|
public Locale getLocale() {
|
||||||
return locale;
|
return locale;
|
||||||
}
|
}
|
||||||
@@ -302,6 +241,139 @@ public class KeycloakInstalled {
|
|||||||
status = Status.LOGGED_MANUAL;
|
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 {
|
public boolean loginCommandLine() throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||||
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
|
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
|
||||||
|
|
||||||
@@ -309,7 +381,6 @@ public class KeycloakInstalled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Experimental proprietary WWW-Authentication challenge protocol.
|
* Experimental proprietary WWW-Authentication challenge protocol.
|
||||||
* WWW-Authentication: X-Text-Form-Challenge callback="{url}" param="{param-name}" label="{param-display-label}"
|
* WWW-Authentication: X-Text-Form-Challenge callback="{url}" param="{param-name}" label="{param-display-label}"
|
||||||
@@ -325,28 +396,44 @@ public class KeycloakInstalled {
|
|||||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||||
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
||||||
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
|
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
|
||||||
|
.queryParam("display", "console")
|
||||||
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
|
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
|
||||||
.build().toString();
|
.build().toString();
|
||||||
ResteasyClient client = new ResteasyClientBuilder().disableTrustManager().build();
|
ResteasyClient client = createResteasyClient();
|
||||||
try {
|
try {
|
||||||
|
//System.err.println("initial request");
|
||||||
Response response = client.target(authUrl).request().get();
|
Response response = client.target(authUrl).request().get();
|
||||||
if (response.getStatus() != 401) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
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;
|
||||||
|
} else if (response.getStatus() == 401) {
|
||||||
String authenticationHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
|
String authenticationHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
|
||||||
if (authenticationHeader == null) {
|
if (authenticationHeader == null) {
|
||||||
|
System.err.println("Failure: Invalid protocol. No WWW-Authenticate header");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
//System.err.println("got header: " + authenticationHeader);
|
||||||
if (!authenticationHeader.contains("X-Text-Form-Challenge")) {
|
if (!authenticationHeader.contains("X-Text-Form-Challenge")) {
|
||||||
|
System.err.println("Failure: Invalid WWW-Authenticate header.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (response.getMediaType() != null) {
|
if (response.getMediaType() != null) {
|
||||||
String splash = response.readEntity(String.class);
|
String splash = response.readEntity(String.class);
|
||||||
System.console().writer().println(splash);
|
console().writer().println(splash);
|
||||||
|
} else {
|
||||||
|
response.close();
|
||||||
}
|
}
|
||||||
Matcher m = callbackPattern.matcher(authenticationHeader);
|
Matcher m = callbackPattern.matcher(authenticationHeader);
|
||||||
if (!m.find()) return false;
|
if (!m.find()) {
|
||||||
|
System.err.println("Failure: Invalid WWW-Authenticate header.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
String callback = m.group(1);
|
String callback = m.group(1);
|
||||||
//System.err.println("callback: " + callback);
|
//System.err.println("callback: " + callback);
|
||||||
m = paramPattern.matcher(authenticationHeader);
|
m = paramPattern.matcher(authenticationHeader);
|
||||||
@@ -358,29 +445,67 @@ public class KeycloakInstalled {
|
|||||||
boolean maskInput = mask.equals("true");
|
boolean maskInput = mask.equals("true");
|
||||||
String value = null;
|
String value = null;
|
||||||
if (maskInput) {
|
if (maskInput) {
|
||||||
char[] txt = System.console().readPassword(label);
|
char[] txt = console().readPassword(label);
|
||||||
value = new String(txt);
|
value = new String(txt);
|
||||||
} else {
|
} else {
|
||||||
value = System.console().readLine(label);
|
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));
|
response = client.target(callback).request().post(Entity.form(form));
|
||||||
if (response.getStatus() == 401) continue;
|
} else if (response.getStatus() == 302) {
|
||||||
if (response.getStatus() != 302) return false;
|
int redirectCount = 0;
|
||||||
|
do {
|
||||||
String location = response.getLocation().toString();
|
String location = response.getLocation().toString();
|
||||||
m = codePattern.matcher(location);
|
Matcher m = codePattern.matcher(location);
|
||||||
if (!m.find()) return false;
|
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);
|
String code = m.group(1);
|
||||||
processCode(code, redirectUri);
|
processCode(code, redirectUri);
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw ex;
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
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 {
|
public String getTokenString() throws VerificationException, IOException, ServerRequest.HttpFailure {
|
||||||
return tokenString;
|
return tokenString;
|
||||||
@@ -452,7 +577,6 @@ public class KeycloakInstalled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||||
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
|
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
|
||||||
parseAccessToken(tokenResponse);
|
parseAccessToken(tokenResponse);
|
||||||
@@ -474,86 +598,6 @@ public class KeycloakInstalled {
|
|||||||
return sb.toString();
|
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 {
|
public class CallbackListener extends Thread {
|
||||||
|
|
||||||
@@ -607,11 +651,26 @@ public class KeycloakInstalled {
|
|||||||
|
|
||||||
OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream());
|
OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream());
|
||||||
PrintWriter pw = new PrintWriter(out);
|
PrintWriter pw = new PrintWriter(out);
|
||||||
|
if (writer != null) {
|
||||||
|
System.err.println("Using a writer is deprecated. Please remove its usage. This is now handled by endpoint on server");
|
||||||
|
}
|
||||||
|
|
||||||
if (error == null) {
|
if (error == null) {
|
||||||
|
if (writer != null) {
|
||||||
writer.success(pw, KeycloakInstalled.this);
|
writer.success(pw, KeycloakInstalled.this);
|
||||||
} else {
|
} else {
|
||||||
|
pw.println("HTTP/1.1 302 Found");
|
||||||
|
pw.println("Location: " + deployment.getTokenUrl().replace("/token", "/delegated"));
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (writer != null) {
|
||||||
writer.failure(pw, KeycloakInstalled.this);
|
writer.failure(pw, KeycloakInstalled.this);
|
||||||
|
} else {
|
||||||
|
pw.println("HTTP/1.1 302 Found");
|
||||||
|
pw.println("Location: " + deployment.getTokenUrl().replace("/token", "/delegated?error=true"));
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pw.flush();
|
pw.flush();
|
||||||
socket.close();
|
socket.close();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+18
@@ -27,6 +27,9 @@ import javax.security.cert.X509Certificate;
|
|||||||
import javax.ws.rs.container.ContainerRequestContext;
|
import javax.ws.rs.container.ContainerRequestContext;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.SecurityContext;
|
import javax.ws.rs.core.SecurityContext;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -51,6 +54,8 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
|
|||||||
|
|
||||||
protected class RequestFacade implements OIDCHttpFacade.Request {
|
protected class RequestFacade implements OIDCHttpFacade.Request {
|
||||||
|
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFirstParam(String param) {
|
public String getFirstParam(String param) {
|
||||||
throw new RuntimeException("NOT IMPLEMENTED");
|
throw new RuntimeException("NOT IMPLEMENTED");
|
||||||
@@ -108,6 +113,19 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getInputStream() {
|
public InputStream getInputStream() {
|
||||||
|
return getInputStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream(boolean buffered) {
|
||||||
|
if (inputStream != null) {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffered) {
|
||||||
|
return inputStream = new BufferedInputStream(requestContext.getEntityStream());
|
||||||
|
}
|
||||||
|
|
||||||
return requestContext.getEntityStream();
|
return requestContext.getEntityStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../../pom.xml</relativePath>
|
<relativePath>../../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak Jetty Integration</name>
|
<name>Keycloak Jetty Integration</name>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+70
-7
@@ -18,7 +18,7 @@
|
|||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import * as Keycloak from 'keycloak';
|
import * as Keycloak from './keycloak';
|
||||||
|
|
||||||
export as namespace KeycloakAuthorization;
|
export as namespace KeycloakAuthorization;
|
||||||
|
|
||||||
@@ -35,6 +35,64 @@ declare namespace KeycloakAuthorization {
|
|||||||
then(onGrant: (rpt: string) => void, onDeny: () => void, onError: () => void): void;
|
then(onGrant: (rpt: string) => void, onDeny: () => void, onError: () => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AuthorizationRequest {
|
||||||
|
/**
|
||||||
|
* An array of objects representing the resource and scopes.
|
||||||
|
*/
|
||||||
|
permissions?:ResourcePermission[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A permission ticket obtained from a resource server when using UMA authorization protocol.
|
||||||
|
*/
|
||||||
|
ticket?:string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean value indicating whether the server should create permission requests to the resources
|
||||||
|
* and scopes referenced by a permission ticket. This parameter will only take effect when used together
|
||||||
|
* with the ticket parameter as part of a UMA authorization process.
|
||||||
|
*/
|
||||||
|
submitRequest?:boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines additional information about this authorization request in order to specify how it should be processed
|
||||||
|
* by the server.
|
||||||
|
*/
|
||||||
|
metadata?:AuthorizationRequestMetadata,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines whether or not this authorization request should include the current RPT. If set to true, the RPT will
|
||||||
|
* be sent and permissions in the current RPT will be included in the new RPT. Otherwise, only the permissions referenced in this
|
||||||
|
* authorization request will be granted in the new RPT.
|
||||||
|
*/
|
||||||
|
incrementalAuthorization?:boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthorizationRequestMetadata {
|
||||||
|
/**
|
||||||
|
* A boolean value indicating to the server if resource names should be included in the RPT’s permissions.
|
||||||
|
* If false, only the resource identifier is included.
|
||||||
|
*/
|
||||||
|
responseIncludeResourceName?:any,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An integer N that defines a limit for the amount of permissions an RPT can have. When used together with
|
||||||
|
* rpt parameter, only the last N requested permissions will be kept in the RPT.
|
||||||
|
*/
|
||||||
|
response_permissions_limit?:number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourcePermission {
|
||||||
|
/**
|
||||||
|
* The id or name of a resource.
|
||||||
|
*/
|
||||||
|
id:string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of strings where each value is the name of a scope associated with the resource.
|
||||||
|
*/
|
||||||
|
scopes?:string[]
|
||||||
|
}
|
||||||
|
|
||||||
interface KeycloakAuthorizationInstance {
|
interface KeycloakAuthorizationInstance {
|
||||||
rpt: any;
|
rpt: any;
|
||||||
config: { rpt_endpoint: string };
|
config: { rpt_endpoint: string };
|
||||||
@@ -43,17 +101,22 @@ declare namespace KeycloakAuthorization {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
||||||
* policy enforcer.
|
* policy enforcer using UMA protocol.
|
||||||
*
|
*
|
||||||
* In this case, the resource server will respond with a 401 status code and a WWW-Authenticate header holding the
|
* The authorization request must be provided with a ticket.
|
||||||
* necessary information to ask a Keycloak server for authorization data using both UMA and Entitlement protocol,
|
*
|
||||||
* depending on how the policy enforcer at the resource server was configured.
|
* @param authorizationRequest An AuthorizationRequest instance with a valid permission ticket set.
|
||||||
|
* @returns A promise to set functions to be invoked on grant, deny or error.
|
||||||
*/
|
*/
|
||||||
authorize(wwwAuthenticateHeader: string): KeycloakAuthorizationPromise;
|
authorize(authorizationRequest: AuthorizationRequest): KeycloakAuthorizationPromise;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains all entitlements from a Keycloak server based on a given resourceServerId.
|
* Obtains all entitlements from a Keycloak server based on a given resourceServerId.
|
||||||
|
*
|
||||||
|
* @param resourceServerId The id (client id) of the resource server to obtain permissions from.
|
||||||
|
* @param authorizationRequest An AuthorizationRequest instance.
|
||||||
|
* @returns A promise to set functions to be invoked on grant, deny or error.
|
||||||
*/
|
*/
|
||||||
entitlement(resourceServerId: string, entitlementRequest: {}): KeycloakAuthorizationPromise;
|
entitlement(resourceServerId: string, authorizationRequest?: AuthorizationRequest): KeycloakAuthorizationPromise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,11 +41,9 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
||||||
* policy enforcer.
|
* policy enforcer using UMA protocol.
|
||||||
*
|
*
|
||||||
* In this case, the resource server will respond with a 401 status code and a WWW-Authenticate header holding the
|
* The authorization request must be provided with a ticket.
|
||||||
* necessary information to ask a Keycloak server for authorization data using both UMA and Entitlement protocol,
|
|
||||||
* depending on how the policy enforcer at the resource server was configured.
|
|
||||||
*/
|
*/
|
||||||
this.authorize = function (authorizationRequest) {
|
this.authorize = function (authorizationRequest) {
|
||||||
this.then = function (onGrant, onDeny, onError) {
|
this.then = function (onGrant, onDeny, onError) {
|
||||||
@@ -205,6 +203,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.init(this);
|
this.init(this);
|
||||||
|
|
||||||
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
|
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
|
||||||
|
|||||||
+15
-3
@@ -29,7 +29,7 @@ export = Keycloak;
|
|||||||
declare function Keycloak(config?: string|{}): Keycloak.KeycloakInstance;
|
declare function Keycloak(config?: string|{}): Keycloak.KeycloakInstance;
|
||||||
|
|
||||||
declare namespace Keycloak {
|
declare namespace Keycloak {
|
||||||
type KeycloakAdapterName = 'cordova'|'default';
|
type KeycloakAdapterName = 'cordova'|'default' | any;
|
||||||
type KeycloakOnLoad = 'login-required'|'check-sso';
|
type KeycloakOnLoad = 'login-required'|'check-sso';
|
||||||
type KeycloakResponseMode = 'query'|'fragment';
|
type KeycloakResponseMode = 'query'|'fragment';
|
||||||
type KeycloakResponseType = 'code'|'id_token token'|'code id_token token';
|
type KeycloakResponseType = 'code'|'id_token token'|'code id_token token';
|
||||||
@@ -42,7 +42,11 @@ declare namespace Keycloak {
|
|||||||
useNonce?: boolean;
|
useNonce?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private Undocumented.
|
* Allows to use different adapter:
|
||||||
|
*
|
||||||
|
* - {string} default - using browser api for redirects
|
||||||
|
* - {string} cordova - using cordova plugins
|
||||||
|
* - {function} - allows to provide custom function as adapter.
|
||||||
*/
|
*/
|
||||||
adapter?: KeycloakAdapterName;
|
adapter?: KeycloakAdapterName;
|
||||||
|
|
||||||
@@ -146,9 +150,17 @@ declare namespace Keycloak {
|
|||||||
idpHint?: string;
|
idpHint?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies the desired locale for the UI.
|
* Sets the 'ui_locales' query param in compliance with section 3.1.2.1
|
||||||
|
* of the OIDC 1.0 specification.
|
||||||
*/
|
*/
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the desired Keycloak locale for the UI. This differs from
|
||||||
|
* the locale param in that it tells the Keycloak server to set a cookie and update
|
||||||
|
* the user's profile to a new preferred locale.
|
||||||
|
*/
|
||||||
|
kcLocale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeycloakPromiseCallback<T> = (result: T) => void;
|
type KeycloakPromiseCallback<T> = (result: T) => void;
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
adapter = loadAdapter('cordova');
|
adapter = loadAdapter('cordova');
|
||||||
} else if (initOptions && initOptions.adapter === 'default') {
|
} else if (initOptions && initOptions.adapter === 'default') {
|
||||||
adapter = loadAdapter();
|
adapter = loadAdapter();
|
||||||
|
} else if (initOptions && typeof initOptions.adapter === "object") {
|
||||||
|
adapter = initOptions.adapter;
|
||||||
} else {
|
} else {
|
||||||
if (window.Cordova || window.cordova) {
|
if (window.Cordova || window.cordova) {
|
||||||
adapter = loadAdapter('cordova');
|
adapter = loadAdapter('cordova');
|
||||||
@@ -281,6 +283,10 @@
|
|||||||
url += '&ui_locales=' + encodeURIComponent(options.locale);
|
url += '&ui_locales=' + encodeURIComponent(options.locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options && options.kcLocale) {
|
||||||
|
url += '&kc_locale=' + encodeURIComponent(options.kcLocale);
|
||||||
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,6 +470,10 @@
|
|||||||
} else {
|
} else {
|
||||||
console.warn('[KEYCLOAK] Failed to refresh token');
|
console.warn('[KEYCLOAK] Failed to refresh token');
|
||||||
|
|
||||||
|
if (req.status == 400) {
|
||||||
|
kc.clearToken();
|
||||||
|
}
|
||||||
|
|
||||||
kc.onAuthRefreshError && kc.onAuthRefreshError();
|
kc.onAuthRefreshError && kc.onAuthRefreshError();
|
||||||
for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {
|
for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {
|
||||||
p.setError(true);
|
p.setError(true);
|
||||||
@@ -1177,24 +1187,54 @@
|
|||||||
return window.open(loginUrl, target, options);
|
return window.open(loginUrl, target, options);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var shallowCloneCordovaOptions = function (userOptions) {
|
||||||
|
if (userOptions && userOptions.cordovaOptions) {
|
||||||
|
return Object.keys(userOptions.cordovaOptions).reduce(function (options, optionName) {
|
||||||
|
options[optionName] = userOptions.cordovaOptions[optionName];
|
||||||
|
return options;
|
||||||
|
}, {});
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var formatCordovaOptions = function (cordovaOptions) {
|
||||||
|
return Object.keys(cordovaOptions).reduce(function (options, optionName) {
|
||||||
|
options.push(optionName+"="+cordovaOptions[optionName]);
|
||||||
|
return options;
|
||||||
|
}, []).join(",");
|
||||||
|
};
|
||||||
|
|
||||||
|
var createCordovaOptions = function (userOptions) {
|
||||||
|
var cordovaOptions = shallowCloneCordovaOptions(userOptions);
|
||||||
|
cordovaOptions.location = 'no';
|
||||||
|
if (userOptions && userOptions.prompt == 'none') {
|
||||||
|
cordovaOptions.hidden = 'yes';
|
||||||
|
}
|
||||||
|
return formatCordovaOptions(cordovaOptions);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
login: function(options) {
|
login: function(options) {
|
||||||
var promise = createPromise();
|
var promise = createPromise();
|
||||||
|
|
||||||
var o = 'location=no';
|
var cordovaOptions = createCordovaOptions(options);
|
||||||
if (options && options.prompt == 'none') {
|
|
||||||
o += ',hidden=yes';
|
|
||||||
}
|
|
||||||
|
|
||||||
var loginUrl = kc.createLoginUrl(options);
|
var loginUrl = kc.createLoginUrl(options);
|
||||||
var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', o);
|
var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', cordovaOptions);
|
||||||
var completed = false;
|
var completed = false;
|
||||||
|
|
||||||
|
var closed = false;
|
||||||
|
var closeBrowser = function() {
|
||||||
|
closed = true;
|
||||||
|
ref.close();
|
||||||
|
};
|
||||||
|
|
||||||
ref.addEventListener('loadstart', function(event) {
|
ref.addEventListener('loadstart', function(event) {
|
||||||
if (event.url.indexOf('http://localhost') == 0) {
|
if (event.url.indexOf('http://localhost') == 0) {
|
||||||
var callback = parseCallback(event.url);
|
var callback = parseCallback(event.url);
|
||||||
processCallback(callback, promise);
|
processCallback(callback, promise);
|
||||||
ref.close();
|
closeBrowser();
|
||||||
completed = true;
|
completed = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1204,15 +1244,23 @@
|
|||||||
if (event.url.indexOf('http://localhost') == 0) {
|
if (event.url.indexOf('http://localhost') == 0) {
|
||||||
var callback = parseCallback(event.url);
|
var callback = parseCallback(event.url);
|
||||||
processCallback(callback, promise);
|
processCallback(callback, promise);
|
||||||
ref.close();
|
closeBrowser();
|
||||||
completed = true;
|
completed = true;
|
||||||
} else {
|
} else {
|
||||||
promise.setError();
|
promise.setError();
|
||||||
ref.close();
|
closeBrowser();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ref.addEventListener('exit', function(event) {
|
||||||
|
if (!closed) {
|
||||||
|
promise.setError({
|
||||||
|
reason: "closed_by_user"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return promise.promise;
|
return promise.promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1253,7 +1301,8 @@
|
|||||||
|
|
||||||
register : function() {
|
register : function() {
|
||||||
var registerUrl = kc.createRegisterUrl();
|
var registerUrl = kc.createRegisterUrl();
|
||||||
var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', 'location=no');
|
var cordovaOptions = createCordovaOptions(options);
|
||||||
|
var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', cordovaOptions);
|
||||||
ref.addEventListener('loadstart', function(event) {
|
ref.addEventListener('loadstart', function(event) {
|
||||||
if (event.url.indexOf('http://localhost') == 0) {
|
if (event.url.indexOf('http://localhost') == 0) {
|
||||||
ref.close();
|
ref.close();
|
||||||
|
|||||||
File renamed without changes.
@@ -21,12 +21,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<artifactId>keycloak-cli-sso</artifactId>
|
<artifactId>kcinit</artifactId>
|
||||||
<name>Keycloak CLI SSO Framework</name>
|
<name>Keycloak CLI SSO Framework</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<transformers>
|
<transformers>
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
<mainClass>org.keycloak.adapters.KeycloakCliSsoMain</mainClass>
|
<mainClass>org.keycloak.adapters.KcinitMain</mainClass>
|
||||||
</transformer>
|
</transformer>
|
||||||
</transformers>
|
</transformers>
|
||||||
|
|
||||||
Executable
+26
@@ -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 $@
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
if "%OS%" == "Windows_NT" (
|
||||||
|
set "DIRNAME=%~dp0%"
|
||||||
|
) else (
|
||||||
|
set DIRNAME=.\
|
||||||
|
)
|
||||||
|
java -jar %DIRNAME%\kcinit-${project.version}.jar %*
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.installed.KcinitDriver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class KcinitMain extends KcinitDriver {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
new KcinitMain().mainCmd(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
org.keycloak.adapters.osgi.*
|
org.keycloak.adapters.osgi.*
|
||||||
</keycloak.osgi.export>
|
</keycloak.osgi.export>
|
||||||
<keycloak.osgi.import>
|
<keycloak.osgi.import>
|
||||||
org.ops4j.pax.web.*;version="[3.0,5)",
|
org.ops4j.pax.web.*;version="[3.0,8)",
|
||||||
javax.servlet.*;version="[2.5,4)";resolution:=optional,
|
javax.servlet.*;version="[2.5,4)";resolution:=optional,
|
||||||
org.eclipse.jetty.*;version="[8.1,10)";resolution:=optional,
|
org.eclipse.jetty.*;version="[8.1,10)";resolution:=optional,
|
||||||
org.keycloak.*;version="${project.version}",
|
org.keycloak.*;version="${project.version}",
|
||||||
|
|||||||
+101
-6
@@ -29,11 +29,12 @@ import org.osgi.util.tracker.ServiceTrackerCustomizer;
|
|||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration with pax-web, which allows to inject custom jetty-web.xml configuration from current bundle classpath into {@link WebContainer}
|
* Integration with pax-web in Fuse 6.3, which allows to inject custom jetty-web.xml configuration from current bundle classpath into {@link WebContainer}
|
||||||
* and allows to inject custom security constraint for securing resources by Keycloak.
|
* and allows to inject custom security constraint for securing resources by Keycloak.
|
||||||
*
|
*
|
||||||
* <p>It assumes that pax-web {@link WebContainer} is used as implementation of OSGI {@link org.osgi.service.http.HttpService}, which
|
* <p>It assumes that pax-web {@link WebContainer} is used as implementation of OSGI {@link org.osgi.service.http.HttpService}, which
|
||||||
@@ -47,7 +48,7 @@ public class PaxWebIntegrationService {
|
|||||||
|
|
||||||
private BundleContext bundleContext;
|
private BundleContext bundleContext;
|
||||||
private String jettyWebXmlLocation;
|
private String jettyWebXmlLocation;
|
||||||
private List<ConstraintMapping> constraintMappings; // Using jetty constraint mapping just because of compatibility with other fuse services
|
private List<Object> constraintMappings;
|
||||||
|
|
||||||
private ServiceTracker webContainerTracker;
|
private ServiceTracker webContainerTracker;
|
||||||
private HttpContext httpContext;
|
private HttpContext httpContext;
|
||||||
@@ -68,11 +69,11 @@ public class PaxWebIntegrationService {
|
|||||||
this.jettyWebXmlLocation = jettyWebXmlLocation;
|
this.jettyWebXmlLocation = jettyWebXmlLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ConstraintMapping> getConstraintMappings() {
|
public List<Object> getConstraintMappings() {
|
||||||
return constraintMappings;
|
return constraintMappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setConstraintMappings(List<ConstraintMapping> constraintMappings) {
|
public void setConstraintMappings(List<Object> constraintMappings) {
|
||||||
this.constraintMappings = constraintMappings;
|
this.constraintMappings = constraintMappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +121,25 @@ public class PaxWebIntegrationService {
|
|||||||
if (constraintMappings == null) {
|
if (constraintMappings == null) {
|
||||||
throw new IllegalStateException("constraintMappings was null!");
|
throw new IllegalStateException("constraintMappings was null!");
|
||||||
}
|
}
|
||||||
for (ConstraintMapping constraintMapping : constraintMappings) {
|
List<ConstraintHandler> handlers = new ArrayList<>();
|
||||||
addConstraintMapping(service, constraintMapping);
|
try {
|
||||||
|
handlers.add(new JettyConstraintHandler());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
handlers.add(new PaxWebConstraintHandler());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
for (Object constraintMapping : constraintMappings) {
|
||||||
|
boolean handled = false;
|
||||||
|
for (ConstraintHandler handler : handlers) {
|
||||||
|
handled |= handler.addConstraintMapping(httpContext, service, constraintMapping);
|
||||||
|
}
|
||||||
|
if (!handled) {
|
||||||
|
log.warnv("Unable to add constraint mapping for constraint of type " + constraintMapping.getClass().toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
service.registerLoginConfig("BASIC", "does-not-matter", null, null, httpContext);
|
service.registerLoginConfig("BASIC", "does-not-matter", null, null, httpContext);
|
||||||
@@ -146,6 +164,16 @@ public class PaxWebIntegrationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void addConstraintMapping(WebContainer service, PaxWebSecurityConstraintMapping constraintMapping) {
|
||||||
|
String name = constraintMapping.getConstraintName();
|
||||||
|
if (name == null) {
|
||||||
|
name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getUrl() + ", dataConstraint=" + constraintMapping.getDataConstraint() + ", canAuthenticate="
|
||||||
|
+ constraintMapping.isAuthentication() + ", roles=" + constraintMapping.getRoles());
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getUrl(), constraintMapping.getMapping(), constraintMapping.getDataConstraint(), constraintMapping.isAuthentication(), constraintMapping.getRoles(), httpContext);
|
||||||
|
}
|
||||||
|
|
||||||
protected void addConstraintMapping(WebContainer service, ConstraintMapping constraintMapping) {
|
protected void addConstraintMapping(WebContainer service, ConstraintMapping constraintMapping) {
|
||||||
Constraint constraint = constraintMapping.getConstraint();
|
Constraint constraint = constraintMapping.getConstraint();
|
||||||
String[] roles = constraint.getRoles();
|
String[] roles = constraint.getRoles();
|
||||||
@@ -178,4 +206,71 @@ public class PaxWebIntegrationService {
|
|||||||
service.unregisterConstraintMapping(httpContext);
|
service.unregisterConstraintMapping(httpContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private interface ConstraintHandler {
|
||||||
|
boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PaxWebConstraintHandler implements ConstraintHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm) {
|
||||||
|
if (cm instanceof PaxWebSecurityConstraintMapping) {
|
||||||
|
PaxWebSecurityConstraintMapping constraintMapping = (PaxWebSecurityConstraintMapping) cm;
|
||||||
|
String name = constraintMapping.getConstraintName();
|
||||||
|
if (name == null) {
|
||||||
|
name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getUrl() + ", dataConstraint=" + constraintMapping.getDataConstraint() + ", canAuthenticate="
|
||||||
|
+ constraintMapping.isAuthentication() + ", roles=" + constraintMapping.getRoles());
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getUrl(), constraintMapping.getMapping(), constraintMapping.getDataConstraint(), constraintMapping.isAuthentication(), constraintMapping.getRoles(), httpContext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class JettyConstraintHandler implements ConstraintHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addConstraintMapping(HttpContext httpContext, WebContainer service, Object cm) {
|
||||||
|
if (cm instanceof ConstraintMapping) {
|
||||||
|
ConstraintMapping constraintMapping = (ConstraintMapping) cm;
|
||||||
|
Constraint constraint = constraintMapping.getConstraint();
|
||||||
|
String[] roles = constraint.getRoles();
|
||||||
|
// name property is unavailable on constraint object :/
|
||||||
|
|
||||||
|
String name = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
|
||||||
|
int dataConstraint = constraint.getDataConstraint();
|
||||||
|
String dataConstraintStr;
|
||||||
|
switch (dataConstraint) {
|
||||||
|
case Constraint.DC_UNSET:
|
||||||
|
dataConstraintStr = null;
|
||||||
|
break;
|
||||||
|
case Constraint.DC_NONE:
|
||||||
|
dataConstraintStr = "NONE";
|
||||||
|
break;
|
||||||
|
case Constraint.DC_CONFIDENTIAL:
|
||||||
|
dataConstraintStr = "CONFIDENTIAL";
|
||||||
|
break;
|
||||||
|
case Constraint.DC_INTEGRAL:
|
||||||
|
dataConstraintStr = "INTEGRAL";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warnv("Unknown data constraint: " + dataConstraint);
|
||||||
|
dataConstraintStr = "CONFIDENTIAL";
|
||||||
|
}
|
||||||
|
List<String> rolesList = Arrays.asList(roles);
|
||||||
|
|
||||||
|
log.debug("Adding security constraint name=" + name + ", url=" + constraintMapping.getPathSpec() + ", dataConstraint=" + dataConstraintStr + ", canAuthenticate="
|
||||||
|
+ constraint.getAuthenticate() + ", roles=" + rolesList);
|
||||||
|
service.registerConstraintMapping(name, constraintMapping.getPathSpec(), null, dataConstraintStr, constraint.getAuthenticate(), rolesList, httpContext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* To change this license header, choose License Headers in Project Properties.
|
||||||
|
* To change this template file, choose Tools | Templates
|
||||||
|
* and open the template in the editor.
|
||||||
|
*/
|
||||||
|
package org.keycloak.adapters.osgi;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author hmlnarik
|
||||||
|
*/
|
||||||
|
public class PaxWebSecurityConstraintMapping {
|
||||||
|
|
||||||
|
private String constraintName = "Constraint-" + new SecureRandom().nextInt(Integer.MAX_VALUE);
|
||||||
|
private String mapping;
|
||||||
|
private String url;
|
||||||
|
private String dataConstraint = "NONE";
|
||||||
|
private boolean authentication = true;
|
||||||
|
private List<String> roles = new LinkedList<>();
|
||||||
|
|
||||||
|
public String getConstraintName() {
|
||||||
|
return constraintName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConstraintName(String constraintName) {
|
||||||
|
this.constraintName = constraintName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMapping() {
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMapping(String mapping) {
|
||||||
|
this.mapping = mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDataConstraint() {
|
||||||
|
return dataConstraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDataConstraint(String dataConstraint) {
|
||||||
|
this.dataConstraint = dataConstraint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAuthentication() {
|
||||||
|
return authentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthentication(boolean authentication) {
|
||||||
|
this.authentication = authentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getRoles() {
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoles(List<String> roles) {
|
||||||
|
this.roles = roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../pom.xml</relativePath>
|
<relativePath>../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak OIDC Client Adapter Modules</name>
|
<name>Keycloak OIDC Client Adapter Modules</name>
|
||||||
@@ -34,7 +34,8 @@
|
|||||||
<module>adapter-core</module>
|
<module>adapter-core</module>
|
||||||
<module>as7-eap6</module>
|
<module>as7-eap6</module>
|
||||||
<module>installed</module>
|
<module>installed</module>
|
||||||
<module>cli-sso</module>
|
<module>fuse7</module>
|
||||||
|
<module>kcinit</module>
|
||||||
<module>jaxrs-oauth-client</module>
|
<module>jaxrs-oauth-client</module>
|
||||||
<module>jetty</module>
|
<module>jetty</module>
|
||||||
<module>js</module>
|
<module>js</module>
|
||||||
@@ -42,6 +43,8 @@
|
|||||||
<module>servlet-filter</module>
|
<module>servlet-filter</module>
|
||||||
<module>servlet-oauth-client</module>
|
<module>servlet-oauth-client</module>
|
||||||
<module>spring-boot</module>
|
<module>spring-boot</module>
|
||||||
|
<module>spring-boot2</module>
|
||||||
|
<module>spring-boot-adapter-core</module>
|
||||||
<module>spring-boot-container-bundle</module>
|
<module>spring-boot-container-bundle</module>
|
||||||
<module>spring-security</module>
|
<module>spring-security</module>
|
||||||
<module>tomcat</module>
|
<module>tomcat</module>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+23
-2
@@ -36,6 +36,8 @@ import javax.security.cert.X509Certificate;
|
|||||||
import javax.servlet.http.Cookie;
|
import javax.servlet.http.Cookie;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@@ -265,6 +267,8 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
|||||||
public Request getRequest() {
|
public Request getRequest() {
|
||||||
return new Request() {
|
return new Request() {
|
||||||
|
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFirstParam(String param) {
|
public String getFirstParam(String param) {
|
||||||
return servletRequest.getParameter(param);
|
return servletRequest.getParameter(param);
|
||||||
@@ -314,10 +318,27 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getInputStream() {
|
public InputStream getInputStream() {
|
||||||
|
return getInputStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream(boolean buffered) {
|
||||||
|
if (inputStream != null) {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffered) {
|
||||||
|
try {
|
||||||
|
return inputStream = new BufferedInputStream(servletRequest.getInputStream());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return servletRequest.getInputStream();
|
return servletRequest.getInputStream();
|
||||||
} catch (IOException ioe) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(ioe);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Beta3</version>
|
||||||
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-spring-boot-adapter-core</artifactId>
|
||||||
|
<name>Keycloak Spring Boot Adapter Core</name>
|
||||||
|
<description/>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<spring-boot.version>1.3.0.RELEASE</spring-boot.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>spring-boot-container-bundle</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<optional>true</optional>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-spring-security-adapter</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.undertow</groupId>
|
||||||
|
<artifactId>undertow-servlet</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-server</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-security</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-webapp</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+262
@@ -0,0 +1,262 @@
|
|||||||
|
/*
|
||||||
|
* 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.springboot;
|
||||||
|
|
||||||
|
import io.undertow.servlet.api.DeploymentInfo;
|
||||||
|
import io.undertow.servlet.api.WebResourceCollection;
|
||||||
|
import org.apache.catalina.Context;
|
||||||
|
import org.apache.tomcat.util.descriptor.web.LoginConfig;
|
||||||
|
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
|
||||||
|
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
|
||||||
|
import org.eclipse.jetty.security.ConstraintMapping;
|
||||||
|
import org.eclipse.jetty.security.ConstraintSecurityHandler;
|
||||||
|
import org.eclipse.jetty.server.Server;
|
||||||
|
import org.eclipse.jetty.util.security.Constraint;
|
||||||
|
import org.eclipse.jetty.webapp.WebAppContext;
|
||||||
|
import org.keycloak.adapters.jetty.KeycloakJettyAuthenticator;
|
||||||
|
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
|
||||||
|
import org.keycloak.adapters.undertow.KeycloakServletExtension;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
|
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
|
||||||
|
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
|
||||||
|
import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory;
|
||||||
|
import org.springframework.boot.context.embedded.jetty.JettyServerCustomizer;
|
||||||
|
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
|
||||||
|
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
|
||||||
|
import org.springframework.boot.context.embedded.undertow.UndertowDeploymentInfoCustomizer;
|
||||||
|
import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServletContainerFactory;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keycloak authentication base integration for Spring Boot - base to be extended for particular boot versions.
|
||||||
|
*/
|
||||||
|
public class KeycloakBaseSpringBootConfiguration {
|
||||||
|
|
||||||
|
protected KeycloakSpringBootProperties keycloakProperties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setKeycloakSpringBootProperties(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
this.keycloakProperties = keycloakProperties;
|
||||||
|
KeycloakSpringBootConfigResolver.setAdapterConfig(keycloakProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class KeycloakBaseUndertowDeploymentInfoCustomizer {
|
||||||
|
|
||||||
|
protected final KeycloakSpringBootProperties keycloakProperties;
|
||||||
|
|
||||||
|
public KeycloakBaseUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
this.keycloakProperties = keycloakProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void customize(DeploymentInfo deploymentInfo) {
|
||||||
|
|
||||||
|
io.undertow.servlet.api.LoginConfig loginConfig = new io.undertow.servlet.api.LoginConfig(keycloakProperties.getRealm());
|
||||||
|
loginConfig.addFirstAuthMethod("KEYCLOAK");
|
||||||
|
|
||||||
|
deploymentInfo.setLoginConfig(loginConfig);
|
||||||
|
|
||||||
|
deploymentInfo.addInitParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName());
|
||||||
|
deploymentInfo.addSecurityConstraints(getSecurityConstraints());
|
||||||
|
|
||||||
|
deploymentInfo.addServletExtension(new KeycloakServletExtension());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<io.undertow.servlet.api.SecurityConstraint> getSecurityConstraints() {
|
||||||
|
|
||||||
|
List<io.undertow.servlet.api.SecurityConstraint> undertowSecurityConstraints = new ArrayList<io.undertow.servlet.api.SecurityConstraint>();
|
||||||
|
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
||||||
|
|
||||||
|
io.undertow.servlet.api.SecurityConstraint undertowSecurityConstraint = new io.undertow.servlet.api.SecurityConstraint();
|
||||||
|
undertowSecurityConstraint.addRolesAllowed(constraintDefinition.getAuthRoles());
|
||||||
|
|
||||||
|
for (KeycloakSpringBootProperties.SecurityCollection collectionDefinition : constraintDefinition.getSecurityCollections()) {
|
||||||
|
|
||||||
|
WebResourceCollection webResourceCollection = new WebResourceCollection();
|
||||||
|
webResourceCollection.addHttpMethods(collectionDefinition.getMethods());
|
||||||
|
webResourceCollection.addHttpMethodOmissions(collectionDefinition.getOmittedMethods());
|
||||||
|
webResourceCollection.addUrlPatterns(collectionDefinition.getPatterns());
|
||||||
|
|
||||||
|
undertowSecurityConstraint.addWebResourceCollections(webResourceCollection);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
undertowSecurityConstraints.add(undertowSecurityConstraint);
|
||||||
|
}
|
||||||
|
return undertowSecurityConstraints;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeycloakBaseJettyServerCustomizer {
|
||||||
|
|
||||||
|
protected final KeycloakSpringBootProperties keycloakProperties;
|
||||||
|
|
||||||
|
public KeycloakBaseJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
this.keycloakProperties = keycloakProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void customize(Server server) {
|
||||||
|
|
||||||
|
KeycloakJettyAuthenticator keycloakJettyAuthenticator = new KeycloakJettyAuthenticator();
|
||||||
|
keycloakJettyAuthenticator.setConfigResolver(new KeycloakSpringBootConfigResolver());
|
||||||
|
|
||||||
|
/* see org.eclipse.jetty.webapp.StandardDescriptorProcessor#visitSecurityConstraint for an example
|
||||||
|
on how to map servlet spec to Constraints */
|
||||||
|
|
||||||
|
List<ConstraintMapping> jettyConstraintMappings = new ArrayList<ConstraintMapping>();
|
||||||
|
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
||||||
|
|
||||||
|
for (KeycloakSpringBootProperties.SecurityCollection securityCollectionDefinition : constraintDefinition
|
||||||
|
.getSecurityCollections()) {
|
||||||
|
// securityCollection matches servlet spec's web-resource-collection
|
||||||
|
Constraint jettyConstraint = new Constraint();
|
||||||
|
|
||||||
|
if (constraintDefinition.getAuthRoles().size() > 0) {
|
||||||
|
jettyConstraint.setAuthenticate(true);
|
||||||
|
jettyConstraint.setRoles(constraintDefinition.getAuthRoles().toArray(new String[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
jettyConstraint.setName(securityCollectionDefinition.getName());
|
||||||
|
|
||||||
|
// according to the servlet spec each security-constraint has at least one URL pattern
|
||||||
|
for(String pattern : securityCollectionDefinition.getPatterns()) {
|
||||||
|
|
||||||
|
/* the following code is asymmetric as Jetty's ConstraintMapping accepts only one allowed HTTP method,
|
||||||
|
but multiple omitted methods. Therefore we add one ConstraintMapping for each allowed
|
||||||
|
mapping but only one mapping in the cases of omitted methods or no methods.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (securityCollectionDefinition.getMethods().size() > 0) {
|
||||||
|
// according to the servlet spec we have either methods ...
|
||||||
|
for(String method : securityCollectionDefinition.getMethods()) {
|
||||||
|
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
||||||
|
jettyConstraintMappings.add(jettyConstraintMapping);
|
||||||
|
|
||||||
|
jettyConstraintMapping.setConstraint(jettyConstraint);
|
||||||
|
jettyConstraintMapping.setPathSpec(pattern);
|
||||||
|
jettyConstraintMapping.setMethod(method);
|
||||||
|
}
|
||||||
|
} else if (securityCollectionDefinition.getOmittedMethods().size() > 0){
|
||||||
|
// ... omitted methods ...
|
||||||
|
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
||||||
|
jettyConstraintMappings.add(jettyConstraintMapping);
|
||||||
|
|
||||||
|
jettyConstraintMapping.setConstraint(jettyConstraint);
|
||||||
|
jettyConstraintMapping.setPathSpec(pattern);
|
||||||
|
jettyConstraintMapping.setMethodOmissions(
|
||||||
|
securityCollectionDefinition.getOmittedMethods().toArray(new String[0]));
|
||||||
|
} else {
|
||||||
|
// ... or no methods at all
|
||||||
|
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
||||||
|
jettyConstraintMappings.add(jettyConstraintMapping);
|
||||||
|
|
||||||
|
jettyConstraintMapping.setConstraint(jettyConstraint);
|
||||||
|
jettyConstraintMapping.setPathSpec(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WebAppContext webAppContext = server.getBean(WebAppContext.class);
|
||||||
|
//if not found as registered bean let's try the handler
|
||||||
|
if(webAppContext==null){
|
||||||
|
webAppContext = (WebAppContext) server.getHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
|
||||||
|
securityHandler.setConstraintMappings(jettyConstraintMappings);
|
||||||
|
securityHandler.setAuthenticator(keycloakJettyAuthenticator);
|
||||||
|
|
||||||
|
webAppContext.setSecurityHandler(securityHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeycloakBaseTomcatContextCustomizer {
|
||||||
|
|
||||||
|
protected final KeycloakSpringBootProperties keycloakProperties;
|
||||||
|
|
||||||
|
public KeycloakBaseTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
this.keycloakProperties = keycloakProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void customize(Context context) {
|
||||||
|
LoginConfig loginConfig = new LoginConfig();
|
||||||
|
loginConfig.setAuthMethod("KEYCLOAK");
|
||||||
|
context.setLoginConfig(loginConfig);
|
||||||
|
|
||||||
|
Set<String> authRoles = new HashSet<String>();
|
||||||
|
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
||||||
|
for (String authRole : constraint.getAuthRoles()) {
|
||||||
|
if (!authRoles.contains(authRole)) {
|
||||||
|
context.addSecurityRole(authRole);
|
||||||
|
authRoles.add(authRole);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
||||||
|
SecurityConstraint tomcatConstraint = new SecurityConstraint();
|
||||||
|
|
||||||
|
for (String authRole : constraint.getAuthRoles()) {
|
||||||
|
tomcatConstraint.addAuthRole(authRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (KeycloakSpringBootProperties.SecurityCollection collection : constraint.getSecurityCollections()) {
|
||||||
|
SecurityCollection tomcatSecCollection = new SecurityCollection();
|
||||||
|
|
||||||
|
if (collection.getName() != null) {
|
||||||
|
tomcatSecCollection.setName(collection.getName());
|
||||||
|
}
|
||||||
|
if (collection.getDescription() != null) {
|
||||||
|
tomcatSecCollection.setDescription(collection.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String pattern : collection.getPatterns()) {
|
||||||
|
tomcatSecCollection.addPattern(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String method : collection.getMethods()) {
|
||||||
|
tomcatSecCollection.addMethod(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String method : collection.getOmittedMethods()) {
|
||||||
|
tomcatSecCollection.addOmittedMethod(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
tomcatConstraint.addCollection(tomcatSecCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.addConstraint(tomcatConstraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.addParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,2 @@
|
|||||||
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||||
|
org.keycloak.adapters.springboot.KeycloakAutoConfiguration
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>spring-boot-container-bundle</artifactId>
|
<artifactId>spring-boot-container-bundle</artifactId>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
@@ -37,6 +37,12 @@
|
|||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-spring-boot-adapter-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.logging</groupId>
|
<groupId>org.jboss.logging</groupId>
|
||||||
<artifactId>jboss-logging</artifactId>
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
|||||||
+7
-199
@@ -17,20 +17,7 @@
|
|||||||
|
|
||||||
package org.keycloak.adapters.springboot;
|
package org.keycloak.adapters.springboot;
|
||||||
|
|
||||||
import io.undertow.servlet.api.DeploymentInfo;
|
|
||||||
import io.undertow.servlet.api.WebResourceCollection;
|
|
||||||
import org.apache.catalina.Context;
|
|
||||||
import org.apache.tomcat.util.descriptor.web.LoginConfig;
|
|
||||||
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
|
|
||||||
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
|
|
||||||
import org.eclipse.jetty.security.ConstraintMapping;
|
|
||||||
import org.eclipse.jetty.security.ConstraintSecurityHandler;
|
|
||||||
import org.eclipse.jetty.server.Server;
|
|
||||||
import org.eclipse.jetty.util.security.Constraint;
|
|
||||||
import org.eclipse.jetty.webapp.WebAppContext;
|
|
||||||
import org.keycloak.adapters.jetty.KeycloakJettyAuthenticator;
|
|
||||||
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
|
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
|
||||||
import org.keycloak.adapters.undertow.KeycloakServletExtension;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@@ -47,10 +34,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keycloak authentication integration for Spring Boot
|
* Keycloak authentication integration for Spring Boot
|
||||||
@@ -62,7 +45,7 @@ import java.util.Set;
|
|||||||
@ConditionalOnWebApplication
|
@ConditionalOnWebApplication
|
||||||
@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
|
@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
|
||||||
@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true)
|
@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true)
|
||||||
public class KeycloakAutoConfiguration {
|
public class KeycloakAutoConfiguration extends KeycloakBaseSpringBootConfiguration {
|
||||||
|
|
||||||
private KeycloakSpringBootProperties keycloakProperties;
|
private KeycloakSpringBootProperties keycloakProperties;
|
||||||
|
|
||||||
@@ -117,202 +100,27 @@ public class KeycloakAutoConfiguration {
|
|||||||
return new KeycloakUndertowDeploymentInfoCustomizer(keycloakProperties);
|
return new KeycloakUndertowDeploymentInfoCustomizer(keycloakProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
static class KeycloakUndertowDeploymentInfoCustomizer implements UndertowDeploymentInfoCustomizer {
|
static class KeycloakUndertowDeploymentInfoCustomizer extends KeycloakBaseUndertowDeploymentInfoCustomizer implements UndertowDeploymentInfoCustomizer {
|
||||||
|
|
||||||
private final KeycloakSpringBootProperties keycloakProperties;
|
|
||||||
|
|
||||||
public KeycloakUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
public KeycloakUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
this.keycloakProperties = keycloakProperties;
|
super(keycloakProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void customize(DeploymentInfo deploymentInfo) {
|
|
||||||
|
|
||||||
io.undertow.servlet.api.LoginConfig loginConfig = new io.undertow.servlet.api.LoginConfig(keycloakProperties.getRealm());
|
|
||||||
loginConfig.addFirstAuthMethod("KEYCLOAK");
|
|
||||||
|
|
||||||
deploymentInfo.setLoginConfig(loginConfig);
|
|
||||||
deploymentInfo.addInitParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName());
|
|
||||||
deploymentInfo.addSecurityConstraints(getSecurityConstraints());
|
|
||||||
deploymentInfo.addServletExtension(new KeycloakServletExtension());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<io.undertow.servlet.api.SecurityConstraint> getSecurityConstraints() {
|
static class KeycloakJettyServerCustomizer extends KeycloakBaseJettyServerCustomizer implements JettyServerCustomizer {
|
||||||
|
|
||||||
List<io.undertow.servlet.api.SecurityConstraint> undertowSecurityConstraints = new ArrayList<io.undertow.servlet.api.SecurityConstraint>();
|
|
||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
|
||||||
|
|
||||||
io.undertow.servlet.api.SecurityConstraint undertowSecurityConstraint = new io.undertow.servlet.api.SecurityConstraint();
|
|
||||||
undertowSecurityConstraint.addRolesAllowed(constraintDefinition.getAuthRoles());
|
|
||||||
|
|
||||||
for (KeycloakSpringBootProperties.SecurityCollection collectionDefinition : constraintDefinition.getSecurityCollections()) {
|
|
||||||
|
|
||||||
WebResourceCollection webResourceCollection = new WebResourceCollection();
|
|
||||||
webResourceCollection.addHttpMethods(collectionDefinition.getMethods());
|
|
||||||
webResourceCollection.addHttpMethodOmissions(collectionDefinition.getOmittedMethods());
|
|
||||||
webResourceCollection.addUrlPatterns(collectionDefinition.getPatterns());
|
|
||||||
|
|
||||||
undertowSecurityConstraint.addWebResourceCollections(webResourceCollection);
|
|
||||||
|
|
||||||
}
|
|
||||||
undertowSecurityConstraints.add(undertowSecurityConstraint);
|
|
||||||
}
|
|
||||||
return undertowSecurityConstraints;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class KeycloakJettyServerCustomizer implements JettyServerCustomizer {
|
|
||||||
|
|
||||||
private final KeycloakSpringBootProperties keycloakProperties;
|
|
||||||
|
|
||||||
public KeycloakJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
public KeycloakJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
this.keycloakProperties = keycloakProperties;
|
super(keycloakProperties);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void customize(Server server) {
|
|
||||||
|
|
||||||
KeycloakJettyAuthenticator keycloakJettyAuthenticator = new KeycloakJettyAuthenticator();
|
|
||||||
keycloakJettyAuthenticator.setConfigResolver(new KeycloakSpringBootConfigResolver());
|
|
||||||
|
|
||||||
/* see org.eclipse.jetty.webapp.StandardDescriptorProcessor#visitSecurityConstraint for an example
|
|
||||||
on how to map servlet spec to Constraints */
|
|
||||||
|
|
||||||
List<ConstraintMapping> jettyConstraintMappings = new ArrayList<ConstraintMapping>();
|
|
||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraintDefinition : keycloakProperties.getSecurityConstraints()) {
|
|
||||||
|
|
||||||
for (KeycloakSpringBootProperties.SecurityCollection securityCollectionDefinition : constraintDefinition
|
|
||||||
.getSecurityCollections()) {
|
|
||||||
// securityCollection matches servlet spec's web-resource-collection
|
|
||||||
Constraint jettyConstraint = new Constraint();
|
|
||||||
|
|
||||||
if (constraintDefinition.getAuthRoles().size() > 0) {
|
|
||||||
jettyConstraint.setAuthenticate(true);
|
|
||||||
jettyConstraint.setRoles(constraintDefinition.getAuthRoles().toArray(new String[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
jettyConstraint.setName(securityCollectionDefinition.getName());
|
|
||||||
|
|
||||||
// according to the servlet spec each security-constraint has at least one URL pattern
|
|
||||||
for(String pattern : securityCollectionDefinition.getPatterns()) {
|
|
||||||
|
|
||||||
/* the following code is asymmetric as Jetty's ConstraintMapping accepts only one allowed HTTP method,
|
|
||||||
but multiple omitted methods. Therefore we add one ConstraintMapping for each allowed
|
|
||||||
mapping but only one mapping in the cases of omitted methods or no methods.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (securityCollectionDefinition.getMethods().size() > 0) {
|
|
||||||
// according to the servlet spec we have either methods ...
|
|
||||||
for(String method : securityCollectionDefinition.getMethods()) {
|
|
||||||
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
|
||||||
jettyConstraintMappings.add(jettyConstraintMapping);
|
|
||||||
|
|
||||||
jettyConstraintMapping.setConstraint(jettyConstraint);
|
|
||||||
jettyConstraintMapping.setPathSpec(pattern);
|
|
||||||
jettyConstraintMapping.setMethod(method);
|
|
||||||
}
|
|
||||||
} else if (securityCollectionDefinition.getOmittedMethods().size() > 0){
|
|
||||||
// ... omitted methods ...
|
|
||||||
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
|
||||||
jettyConstraintMappings.add(jettyConstraintMapping);
|
|
||||||
|
|
||||||
jettyConstraintMapping.setConstraint(jettyConstraint);
|
|
||||||
jettyConstraintMapping.setPathSpec(pattern);
|
|
||||||
jettyConstraintMapping.setMethodOmissions(
|
|
||||||
securityCollectionDefinition.getOmittedMethods().toArray(new String[0]));
|
|
||||||
} else {
|
|
||||||
// ... or no methods at all
|
|
||||||
ConstraintMapping jettyConstraintMapping = new ConstraintMapping();
|
|
||||||
jettyConstraintMappings.add(jettyConstraintMapping);
|
|
||||||
|
|
||||||
jettyConstraintMapping.setConstraint(jettyConstraint);
|
|
||||||
jettyConstraintMapping.setPathSpec(pattern);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
static class KeycloakTomcatContextCustomizer extends KeycloakBaseTomcatContextCustomizer implements TomcatContextCustomizer {
|
||||||
}
|
|
||||||
|
|
||||||
WebAppContext webAppContext = server.getBean(WebAppContext.class);
|
|
||||||
//if not found as registered bean let's try the handler
|
|
||||||
if(webAppContext==null){
|
|
||||||
webAppContext = (WebAppContext) server.getHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
|
|
||||||
securityHandler.setConstraintMappings(jettyConstraintMappings);
|
|
||||||
securityHandler.setAuthenticator(keycloakJettyAuthenticator);
|
|
||||||
|
|
||||||
webAppContext.setSecurityHandler(securityHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class KeycloakTomcatContextCustomizer implements TomcatContextCustomizer {
|
|
||||||
|
|
||||||
private final KeycloakSpringBootProperties keycloakProperties;
|
|
||||||
|
|
||||||
public KeycloakTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
public KeycloakTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
this.keycloakProperties = keycloakProperties;
|
super(keycloakProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void customize(Context context) {
|
|
||||||
LoginConfig loginConfig = new LoginConfig();
|
|
||||||
loginConfig.setAuthMethod("KEYCLOAK");
|
|
||||||
context.setLoginConfig(loginConfig);
|
|
||||||
|
|
||||||
Set<String> authRoles = new HashSet<String>();
|
|
||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
|
||||||
for (String authRole : constraint.getAuthRoles()) {
|
|
||||||
if (!authRoles.contains(authRole)) {
|
|
||||||
context.addSecurityRole(authRole);
|
|
||||||
authRoles.add(authRole);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (KeycloakSpringBootProperties.SecurityConstraint constraint : keycloakProperties.getSecurityConstraints()) {
|
|
||||||
SecurityConstraint tomcatConstraint = new SecurityConstraint();
|
|
||||||
|
|
||||||
for (String authRole : constraint.getAuthRoles()) {
|
|
||||||
tomcatConstraint.addAuthRole(authRole);
|
|
||||||
if(authRole.equals("*") || authRole.equals("**")) {
|
|
||||||
// For some reasons embed tomcat don't set the auth constraint on true when wildcard is
|
|
||||||
// used
|
|
||||||
tomcatConstraint.setAuthConstraint(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (KeycloakSpringBootProperties.SecurityCollection collection : constraint.getSecurityCollections()) {
|
|
||||||
SecurityCollection tomcatSecCollection = new SecurityCollection();
|
|
||||||
|
|
||||||
if (collection.getName() != null) {
|
|
||||||
tomcatSecCollection.setName(collection.getName());
|
|
||||||
}
|
|
||||||
if (collection.getDescription() != null) {
|
|
||||||
tomcatSecCollection.setDescription(collection.getDescription());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String pattern : collection.getPatterns()) {
|
|
||||||
tomcatSecCollection.addPattern(pattern);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String method : collection.getMethods()) {
|
|
||||||
tomcatSecCollection.addMethod(method);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String method : collection.getOmittedMethods()) {
|
|
||||||
tomcatSecCollection.addOmittedMethod(method);
|
|
||||||
}
|
|
||||||
|
|
||||||
tomcatConstraint.addCollection(tomcatSecCollection);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.addConstraint(tomcatConstraint);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.addParameter("keycloak.config.resolver", KeycloakSpringBootConfigResolver.class.getName());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Executable
+151
@@ -0,0 +1,151 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>4.0.0.Beta3</version>
|
||||||
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>keycloak-spring-boot-2-adapter</artifactId>
|
||||||
|
<name>Keycloak Spring Boot 2 Integration</name>
|
||||||
|
<description/>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<spring-boot.version>2.0.0.RELEASE</spring-boot.version>
|
||||||
|
<spring.version>5.0.2.RELEASE</spring.version>
|
||||||
|
<mockito.version>1.9.5</mockito.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-spring-boot-adapter-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.logging</groupId>
|
||||||
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>spring-boot-container-bundle</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<optional>true</optional>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-spring-security-adapter</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.9.4</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-annotations</artifactId>
|
||||||
|
<version>2.9.4</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-core</artifactId>
|
||||||
|
<version>5.0.2.RELEASE</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.undertow</groupId>
|
||||||
|
<artifactId>undertow-servlet</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-server</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-security</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-webapp</artifactId>
|
||||||
|
<version>${jetty9.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-test</artifactId>
|
||||||
|
<version>${spring.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-all</artifactId>
|
||||||
|
<version>${mockito.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* 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.springboot;
|
||||||
|
|
||||||
|
import org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||||
|
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
|
||||||
|
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
|
||||||
|
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
|
||||||
|
import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer;
|
||||||
|
import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
|
||||||
|
import org.springframework.boot.web.embedded.undertow.UndertowDeploymentInfoCustomizer;
|
||||||
|
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keycloak authentication integration for Spring Boot 2
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnWebApplication
|
||||||
|
@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
|
||||||
|
@ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true)
|
||||||
|
public class KeycloakAutoConfiguration extends KeycloakBaseSpringBootConfiguration {
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> getKeycloakContainerCustomizer() {
|
||||||
|
return new WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>() {
|
||||||
|
@Override
|
||||||
|
public void customize(ConfigurableServletWebServerFactory configurableServletWebServerFactory) {
|
||||||
|
if(configurableServletWebServerFactory instanceof TomcatServletWebServerFactory){
|
||||||
|
|
||||||
|
TomcatServletWebServerFactory container = (TomcatServletWebServerFactory)configurableServletWebServerFactory;
|
||||||
|
container.addContextValves(new KeycloakAuthenticatorValve());
|
||||||
|
container.addContextCustomizers(tomcatKeycloakContextCustomizer());
|
||||||
|
|
||||||
|
} else if (configurableServletWebServerFactory instanceof UndertowServletWebServerFactory){
|
||||||
|
|
||||||
|
UndertowServletWebServerFactory container = (UndertowServletWebServerFactory)configurableServletWebServerFactory;
|
||||||
|
container.addDeploymentInfoCustomizers(undertowKeycloakContextCustomizer());
|
||||||
|
|
||||||
|
} else if (configurableServletWebServerFactory instanceof JettyServletWebServerFactory){
|
||||||
|
|
||||||
|
JettyServletWebServerFactory container = (JettyServletWebServerFactory)configurableServletWebServerFactory;
|
||||||
|
container.addServerCustomizers(jettyKeycloakServerCustomizer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnClass(name = {"org.eclipse.jetty.webapp.WebAppContext"})
|
||||||
|
public JettyServerCustomizer jettyKeycloakServerCustomizer() {
|
||||||
|
return new KeycloakJettyServerCustomizer(keycloakProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnClass(name = {"org.apache.catalina.startup.Tomcat"})
|
||||||
|
public TomcatContextCustomizer tomcatKeycloakContextCustomizer() {
|
||||||
|
return new KeycloakTomcatContextCustomizer(keycloakProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnClass(name = {"io.undertow.Undertow"})
|
||||||
|
public UndertowDeploymentInfoCustomizer undertowKeycloakContextCustomizer() {
|
||||||
|
return new KeycloakUndertowDeploymentInfoCustomizer(keycloakProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeycloakJettyServerCustomizer extends KeycloakBaseJettyServerCustomizer implements JettyServerCustomizer {
|
||||||
|
|
||||||
|
|
||||||
|
public KeycloakJettyServerCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
super(keycloakProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeycloakTomcatContextCustomizer extends KeycloakBaseTomcatContextCustomizer implements TomcatContextCustomizer {
|
||||||
|
|
||||||
|
public KeycloakTomcatContextCustomizer(KeycloakSpringBootProperties keycloakProperties) {
|
||||||
|
super(keycloakProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeycloakUndertowDeploymentInfoCustomizer extends KeycloakBaseUndertowDeploymentInfoCustomizer implements UndertowDeploymentInfoCustomizer {
|
||||||
|
|
||||||
|
public KeycloakUndertowDeploymentInfoCustomizer(KeycloakSpringBootProperties keycloakProperties){
|
||||||
|
super(keycloakProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package org.keycloak.adapters.springboot.client;
|
||||||
|
|
||||||
|
import org.springframework.boot.web.client.RestTemplateCustomizer;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
public class KeycloakRestTemplateCustomizer implements RestTemplateCustomizer {
|
||||||
|
|
||||||
|
private final KeycloakSecurityContextClientRequestInterceptor keycloakInterceptor;
|
||||||
|
|
||||||
|
public KeycloakRestTemplateCustomizer() {
|
||||||
|
this(new KeycloakSecurityContextClientRequestInterceptor());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected KeycloakRestTemplateCustomizer(
|
||||||
|
KeycloakSecurityContextClientRequestInterceptor keycloakInterceptor
|
||||||
|
) {
|
||||||
|
this.keycloakInterceptor = keycloakInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void customize(RestTemplate restTemplate) {
|
||||||
|
restTemplate.getInterceptors().add(keycloakInterceptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
package org.keycloak.adapters.springboot.client;
|
||||||
|
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.springframework.http.HttpRequest;
|
||||||
|
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||||
|
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||||
|
import org.springframework.http.client.ClientHttpResponse;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.Principal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interceptor for {@link ClientHttpRequestExecution} objects created for server to server secured
|
||||||
|
* communication using OAuth2 bearer tokens issued by Keycloak.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jmcshan1@gmail.com">James McShane</a>
|
||||||
|
* @version $Revision: 1 $
|
||||||
|
*/
|
||||||
|
public class KeycloakSecurityContextClientRequestInterceptor implements ClientHttpRequestInterceptor {
|
||||||
|
|
||||||
|
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link KeycloakSecurityContext} from the Spring {@link ServletRequestAttributes}'s {@link Principal}.
|
||||||
|
*
|
||||||
|
* The principal must support retrieval of the KeycloakSecurityContext, so at this point, only {@link KeycloakPrincipal}
|
||||||
|
* values are supported
|
||||||
|
*
|
||||||
|
* @return the current <code>KeycloakSecurityContext</code>
|
||||||
|
*/
|
||||||
|
protected KeycloakSecurityContext getKeycloakSecurityContext() {
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
Principal principal = attributes.getRequest().getUserPrincipal();
|
||||||
|
if (principal == null) {
|
||||||
|
throw new IllegalStateException("Cannot set authorization header because there is no authenticated principal");
|
||||||
|
}
|
||||||
|
if (!(principal instanceof KeycloakPrincipal)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
String.format(
|
||||||
|
"Cannot set authorization header because the principal type %s does not provide the KeycloakSecurityContext",
|
||||||
|
principal.getClass()));
|
||||||
|
}
|
||||||
|
return ((KeycloakPrincipal) principal).getKeycloakSecurityContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
|
||||||
|
KeycloakSecurityContext context = this.getKeycloakSecurityContext();
|
||||||
|
httpRequest.getHeaders().set(AUTHORIZATION_HEADER, "Bearer " + context.getTokenString());
|
||||||
|
return clientHttpRequestExecution.execute(httpRequest, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||||
|
org.keycloak.adapters.springboot.KeycloakAutoConfiguration
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package org.keycloak.adapters.springboot.client;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class KeycloakRestTemplateCustomizerTest {
|
||||||
|
|
||||||
|
private KeycloakRestTemplateCustomizer customizer;
|
||||||
|
private KeycloakSecurityContextClientRequestInterceptor interceptor =
|
||||||
|
mock(KeycloakSecurityContextClientRequestInterceptor.class);
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
customizer = new KeycloakRestTemplateCustomizer(interceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void interceptorIsAddedToRequest() {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
customizer.customize(restTemplate);
|
||||||
|
assertTrue(restTemplate.getInterceptors().contains(interceptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.adapters.springboot.client;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.mockito.Spy;
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keycloak spring boot client request factory tests.
|
||||||
|
*/
|
||||||
|
public class KeycloakSecurityContextClientRequestInterceptorTest {
|
||||||
|
|
||||||
|
@Spy
|
||||||
|
private KeycloakSecurityContextClientRequestInterceptor factory;
|
||||||
|
|
||||||
|
private MockHttpServletRequest servletRequest;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private KeycloakSecurityContext keycloakSecurityContext;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private KeycloakPrincipal keycloakPrincipal;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this);
|
||||||
|
servletRequest = new MockHttpServletRequest();
|
||||||
|
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(servletRequest));
|
||||||
|
servletRequest.setUserPrincipal(keycloakPrincipal);
|
||||||
|
when(keycloakPrincipal.getKeycloakSecurityContext()).thenReturn(keycloakSecurityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetKeycloakSecurityContext() throws Exception {
|
||||||
|
KeycloakSecurityContext context = factory.getKeycloakSecurityContext();
|
||||||
|
assertNotNull(context);
|
||||||
|
assertEquals(keycloakSecurityContext, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalStateException.class)
|
||||||
|
public void testGetKeycloakSecurityContextInvalidPrincipal() throws Exception {
|
||||||
|
servletRequest.setUserPrincipal(new MarkerPrincipal());
|
||||||
|
factory.getKeycloakSecurityContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalStateException.class)
|
||||||
|
public void testGetKeycloakSecurityContextNullAuthentication() throws Exception {
|
||||||
|
servletRequest.setUserPrincipal(null);
|
||||||
|
factory.getKeycloakSecurityContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MarkerPrincipal implements Principal {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+21
-1
@@ -24,6 +24,8 @@ import org.keycloak.adapters.spi.LogoutError;
|
|||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -40,6 +42,7 @@ import java.util.List;
|
|||||||
class WrappedHttpServletRequest implements Request {
|
class WrappedHttpServletRequest implements Request {
|
||||||
|
|
||||||
private final HttpServletRequest request;
|
private final HttpServletRequest request;
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new request for the given <code>HttpServletRequest</code>
|
* Creates a new request for the given <code>HttpServletRequest</code>
|
||||||
@@ -122,10 +125,27 @@ class WrappedHttpServletRequest implements Request {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getInputStream() {
|
public InputStream getInputStream() {
|
||||||
|
return getInputStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream(boolean buffered) {
|
||||||
|
if (inputStream != null) {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffered) {
|
||||||
|
try {
|
||||||
|
return inputStream = new BufferedInputStream(request.getInputStream());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return request.getInputStream();
|
return request.getInputStream();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("Unable to get request input stream", e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-14
@@ -16,6 +16,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.keycloak.adapters.springsecurity.filter;
|
package org.keycloak.adapters.springsecurity.filter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
import javax.servlet.ServletResponse;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.keycloak.KeycloakPrincipal;
|
import org.keycloak.KeycloakPrincipal;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
@@ -29,14 +38,6 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.web.filter.GenericFilterBean;
|
import org.springframework.web.filter.GenericFilterBean;
|
||||||
|
|
||||||
import javax.servlet.FilterChain;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.ServletRequest;
|
|
||||||
import javax.servlet.ServletResponse;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
@@ -60,8 +61,6 @@ public class KeycloakSecurityContextRequestFilter extends GenericFilterBean impl
|
|||||||
|
|
||||||
if (keycloakSecurityContext instanceof RefreshableKeycloakSecurityContext) {
|
if (keycloakSecurityContext instanceof RefreshableKeycloakSecurityContext) {
|
||||||
RefreshableKeycloakSecurityContext refreshableSecurityContext = (RefreshableKeycloakSecurityContext) keycloakSecurityContext;
|
RefreshableKeycloakSecurityContext refreshableSecurityContext = (RefreshableKeycloakSecurityContext) keycloakSecurityContext;
|
||||||
|
|
||||||
if (refreshableSecurityContext.isActive()) {
|
|
||||||
KeycloakDeployment deployment = resolveDeployment(request, response);
|
KeycloakDeployment deployment = resolveDeployment(request, response);
|
||||||
|
|
||||||
if (deployment.isAlwaysRefreshToken()) {
|
if (deployment.isAlwaysRefreshToken()) {
|
||||||
@@ -71,16 +70,13 @@ public class KeycloakSecurityContextRequestFilter extends GenericFilterBean impl
|
|||||||
clearAuthenticationContext();
|
clearAuthenticationContext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
clearAuthenticationContext();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initFilterBean() throws ServletException {
|
protected void initFilterBean() {
|
||||||
deploymentContext = applicationContext.getBean(AdapterDeploymentContext.class);
|
deploymentContext = applicationContext.getBean(AdapterDeploymentContext.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak Tomcat Integration</name>
|
<name>Keycloak Tomcat Integration</name>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
<artifactId>keycloak-tomcat-integration-pom</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
@@ -30,6 +30,15 @@
|
|||||||
<name>Keycloak Undertow Integration</name>
|
<name>Keycloak Undertow Integration</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<keycloak.osgi.export>
|
||||||
|
org.keycloak.adapters.undertow.*
|
||||||
|
</keycloak.osgi.export>
|
||||||
|
<keycloak.osgi.import>
|
||||||
|
*;resolution:=optional
|
||||||
|
</keycloak.osgi.import>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.logging</groupId>
|
<groupId>org.jboss.logging</groupId>
|
||||||
@@ -105,6 +114,39 @@
|
|||||||
<target>${maven.compiler.target}</target>
|
<target>${maven.compiler.target}</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.felix</groupId>
|
||||||
|
<artifactId>maven-bundle-plugin</artifactId>
|
||||||
|
<extensions>true</extensions>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>bundle-manifest</id>
|
||||||
|
<phase>process-classes</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>manifest</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<instructions>
|
||||||
|
<Bundle-ClassPath>.</Bundle-ClassPath>
|
||||||
|
<Bundle-Name>${project.name}</Bundle-Name>
|
||||||
|
<Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName>
|
||||||
|
<Import-Package>${keycloak.osgi.import}</Import-Package>
|
||||||
|
<Export-Package>${keycloak.osgi.export}</Export-Package>
|
||||||
|
</instructions>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|||||||
+20
-1
@@ -40,6 +40,8 @@ import org.wildfly.security.http.Scope;
|
|||||||
|
|
||||||
import javax.security.auth.callback.CallbackHandler;
|
import javax.security.auth.callback.CallbackHandler;
|
||||||
import javax.security.cert.X509Certificate;
|
import javax.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -158,6 +160,8 @@ class ElytronHttpFacade implements OIDCHttpFacade {
|
|||||||
@Override
|
@Override
|
||||||
public Request getRequest() {
|
public Request getRequest() {
|
||||||
return new Request() {
|
return new Request() {
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMethod() {
|
public String getMethod() {
|
||||||
return request.getRequestMethod();
|
return request.getRequestMethod();
|
||||||
@@ -184,7 +188,7 @@ class ElytronHttpFacade implements OIDCHttpFacade {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFirstParam(String param) {
|
public String getFirstParam(String param) {
|
||||||
throw new RuntimeException("Not implemented.");
|
return request.getFirstParameterValue(param);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -230,6 +234,19 @@ class ElytronHttpFacade implements OIDCHttpFacade {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getInputStream() {
|
public InputStream getInputStream() {
|
||||||
|
return getInputStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream(boolean buffered) {
|
||||||
|
if (inputStream != null) {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffered) {
|
||||||
|
return inputStream = new BufferedInputStream(request.getInputStream());
|
||||||
|
}
|
||||||
|
|
||||||
return request.getInputStream();
|
return request.getInputStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,8 +284,10 @@ class ElytronHttpFacade implements OIDCHttpFacade {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setStatus(final int status) {
|
public void setStatus(final int status) {
|
||||||
|
if (status < 200 || status > 300) {
|
||||||
responseConsumer = responseConsumer.andThen(response -> response.setStatusCode(status));
|
responseConsumer = responseConsumer.andThen(response -> response.setStatusCode(status));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addHeader(final String name, final String value) {
|
public void addHeader(final String name, final String value) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>keycloak-parent</artifactId>
|
<artifactId>keycloak-parent</artifactId>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<version>4.0.0.CR1-SNAPSHOT</version>
|
<version>4.0.0.Beta3</version>
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
<relativePath>../../../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
<name>Keycloak WildFly Integration</name>
|
<name>Keycloak WildFly Integration</name>
|
||||||
|
|||||||
Loaded 100 of 906 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user