Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e220ae80cc | ||
|
|
a79114f0f5 | ||
|
|
a3675c5d77 | ||
|
|
53ec81f03e | ||
|
|
b069ba27f0 |
No files matched your search
+1
-12
@@ -15,24 +15,13 @@ env:
|
||||
- TESTS=server-group3
|
||||
- TESTS=server-group4
|
||||
- TESTS=old
|
||||
- TESTS=crossdc-server
|
||||
- TESTS=crossdc-adapter
|
||||
- TESTS=crossdc
|
||||
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
install: true
|
||||
|
||||
before_install:
|
||||
- "export PHANTOMJS_VERSION=2.1.1"
|
||||
- "phantomjs --version"
|
||||
- "export PATH=$PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin:$PATH"
|
||||
- "phantomjs --version"
|
||||
- "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then rm -rf $PWD/travis_phantomjs; mkdir -p $PWD/travis_phantomjs; fi"
|
||||
- "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then wget https://github.com/Medium/phantomjs/releases/download/v$PHANTOMJS_VERSION/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -O $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2; fi"
|
||||
- "if [ $(phantomjs --version) != '$PHANTOMJS_VERSION' ]; then tar -xvf $PWD/travis_phantomjs/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 -C $PWD/travis_phantomjs; fi"
|
||||
- "phantomjs --version"
|
||||
|
||||
script:
|
||||
- ./travis-run-tests.sh $TESTS
|
||||
|
||||
|
||||
@@ -32,10 +32,6 @@ To build the distribution run:
|
||||
|
||||
Once completed you will find distribution archives in `distribution`.
|
||||
|
||||
To build only the server run:
|
||||
|
||||
mvn -Pdistribution -pl distribution/server-dist -am -Dmaven.test.skip clean install
|
||||
|
||||
|
||||
Starting Keycloak
|
||||
-----------------
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -36,16 +36,7 @@
|
||||
</keycloak.osgi.export>
|
||||
<keycloak.osgi.import>
|
||||
org.keycloak.*;version="${project.version}",
|
||||
org.apache.http.auth.*;version=${apache.httpcomponents.version},
|
||||
org.apache.http.client.*;version=${apache.httpcomponents.version},
|
||||
org.apache.http.conn.*;version=${apache.httpcomponents.version},
|
||||
org.apache.http.cookie.*;version=${apache.httpcomponents.version},
|
||||
org.apache.http.impl.auth.*;version=${apache.httpcomponents.version},
|
||||
org.apache.http.impl.client.*;version=${apache.httpcomponents.version},
|
||||
org.apache.http.impl.conn.*;version=${apache.httpcomponents.version},
|
||||
org.apache.http.impl.cookie.*;version=${apache.httpcomponents.version},
|
||||
org.apache.http.impl.execchain.*;version=${apache.httpcomponents.version},
|
||||
org.apache.http.*;version=${apache.httpcomponents.httpcore.version},
|
||||
org.apache.http.*;version=${apache.httpcomponents.version},
|
||||
org.apache.karaf.jaas.boot.principal;resolution:=optional,
|
||||
org.apache.karaf.jaas.modules;resolution:=optional,
|
||||
*;resolution:=optional
|
||||
|
||||
-10
@@ -206,16 +206,6 @@ public class AdapterDeploymentContext {
|
||||
public void setBearerOnly(boolean bearerOnly) {
|
||||
delegate.setBearerOnly(bearerOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutodetectBearerOnly() {
|
||||
return delegate.isAutodetectBearerOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
|
||||
delegate.setAutodetectBearerOnly(autodetectBearerOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnableBasicAuth() {
|
||||
|
||||
-8
@@ -102,14 +102,6 @@ public class AuthenticatedActionsHandler {
|
||||
KeycloakSecurityContext securityContext = facade.getSecurityContext();
|
||||
String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN);
|
||||
String exposeHeaders = deployment.getCorsExposedHeaders();
|
||||
|
||||
if (deployment.getPolicyEnforcer() != null) {
|
||||
if (exposeHeaders != null) {
|
||||
exposeHeaders += ",";
|
||||
}
|
||||
exposeHeaders += "WWW-Authenticate";
|
||||
}
|
||||
|
||||
String requestOrigin = UriUtils.getOrigin(facade.getRequest().getURI());
|
||||
log.debugv("Origin: {0} uri: {1}", origin, facade.getRequest().getURI());
|
||||
if (securityContext != null && origin != null && !origin.equals(requestOrigin)) {
|
||||
|
||||
-4
@@ -174,10 +174,6 @@ public class BearerTokenRequestAuthenticator {
|
||||
|
||||
@Override
|
||||
public boolean challenge(HttpFacade facade) {
|
||||
if (deployment.getPolicyEnforcer() != null) {
|
||||
deployment.getPolicyEnforcer().enforce(OIDCHttpFacade.class.cast(facade));
|
||||
return true;
|
||||
}
|
||||
OIDCAuthenticationError error = new OIDCAuthenticationError(reason, description);
|
||||
facade.getRequest().setError(error);
|
||||
facade.getResponse().addHeader("WWW-Authenticate", challenge);
|
||||
|
||||
@@ -57,8 +57,6 @@ import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.http.client.params.ClientPNames;
|
||||
import org.apache.http.client.params.CookiePolicy;
|
||||
|
||||
/**
|
||||
* Abstraction for creating HttpClients. Allows SSL configuration.
|
||||
@@ -291,7 +289,6 @@ public class HttpClientBuilder {
|
||||
cm = new SingleClientConnManager(registry);
|
||||
}
|
||||
BasicHttpParams params = new BasicHttpParams();
|
||||
params.setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
|
||||
|
||||
if (proxyHost != null) {
|
||||
params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxyHost);
|
||||
|
||||
@@ -154,8 +154,6 @@ public class KeycloakDeployment {
|
||||
log.debug("resolveUrls");
|
||||
}
|
||||
|
||||
authServerBaseUrl = authUrlBuilder.build().toString();
|
||||
|
||||
String login = authUrlBuilder.clone().path(ServiceUrlConstants.AUTH_PATH).build(getRealm()).toString();
|
||||
authUrl = KeycloakUriBuilder.fromUri(login);
|
||||
realmInfoUrl = authUrlBuilder.clone().path(ServiceUrlConstants.REALM_INFO_PATH).build(getRealm()).toString();
|
||||
|
||||
+1
-2
@@ -390,8 +390,7 @@ public class OAuthRequestAuthenticator {
|
||||
protected String stripOauthParametersFromRedirect() {
|
||||
KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(facade.getRequest().getURI())
|
||||
.replaceQueryParam(OAuth2Constants.CODE, null)
|
||||
.replaceQueryParam(OAuth2Constants.STATE, null)
|
||||
.replaceQueryParam(OAuth2Constants.SESSION_STATE, null);
|
||||
.replaceQueryParam(OAuth2Constants.STATE, null);
|
||||
return builder.build().toString();
|
||||
}
|
||||
|
||||
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
package org.keycloak.adapters.authentication;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.adapters.AdapterUtils;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
/**
|
||||
* Client authentication based on JWT signed by client secret instead of private key .
|
||||
* See <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">specs</a> for more details.
|
||||
*
|
||||
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||
*/
|
||||
public class JWTClientSecretCredentialsProvider implements ClientCredentialsProvider {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(JWTClientSecretCredentialsProvider.class);
|
||||
|
||||
public static final String PROVIDER_ID = "secret-jwt";
|
||||
|
||||
private SecretKey clientSecret;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(KeycloakDeployment deployment, Object config) {
|
||||
if (config == null || !(config instanceof Map)) {
|
||||
throw new RuntimeException("Configuration of jwt credentials by client secret is missing or incorrect for client '" + deployment.getResourceName() + "'. Check your adapter configuration");
|
||||
}
|
||||
|
||||
Map<String, Object> cfg = (Map<String, Object>) config;
|
||||
String clientSecretString = (String) cfg.get("secret");
|
||||
if (clientSecretString == null) {
|
||||
throw new RuntimeException("Missing parameter secret-jwt in configuration of jwt for client " + deployment.getResourceName());
|
||||
}
|
||||
setClientSecret(clientSecretString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
|
||||
String signedToken = createSignedRequestToken(deployment.getResourceName(), deployment.getRealmInfoUrl());
|
||||
formParams.put(OAuth2Constants.CLIENT_ASSERTION_TYPE, OAuth2Constants.CLIENT_ASSERTION_TYPE_JWT);
|
||||
formParams.put(OAuth2Constants.CLIENT_ASSERTION, signedToken);
|
||||
}
|
||||
|
||||
public void setClientSecret(String clientSecretString) {
|
||||
// Get client secret and validate signature
|
||||
// According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
|
||||
// The HMAC (Hash-based Message Authentication Code) is calculated using the octets of the UTF-8 representation of the client_secret as the shared key.
|
||||
// Use "HmacSHA256" consulting <a href="https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Mac.html">java8 api</a>
|
||||
// because it must be implemented in every java platform.
|
||||
try {
|
||||
clientSecret = new SecretKeySpec(clientSecretString.getBytes("UTF-8"), "HmacSHA256");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException("Failed to create secret key spec due to unsupported encoding.");
|
||||
}
|
||||
}
|
||||
|
||||
public String createSignedRequestToken(String clientId, String realmInfoUrl) {
|
||||
JsonWebToken jwt = createRequestToken(clientId, realmInfoUrl);
|
||||
// JOSE header {"alg":"HS256","typ" : "JWT"} no need "kid" due to using only one registered client secret.
|
||||
// Use "HmacSHA256" consulting <a href="https://docs.oracle.com/javase/jp/8/docs/api/javax/crypto/Mac.html">java8 api</a>.
|
||||
// because it must be implemented in every java platform.
|
||||
return new JWSBuilder().jsonContent(jwt).hmac256(clientSecret);
|
||||
}
|
||||
|
||||
private JsonWebToken createRequestToken(String clientId, String realmInfoUrl) {
|
||||
// According to <a href="http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OIDC's client authentication spec</a>,
|
||||
// JWT claims is the same as one by private_key_jwt
|
||||
|
||||
JsonWebToken reqToken = new JsonWebToken();
|
||||
reqToken.id(AdapterUtils.generateId());
|
||||
reqToken.issuer(clientId);
|
||||
reqToken.subject(clientId);
|
||||
reqToken.audience(realmInfoUrl);
|
||||
|
||||
int now = Time.currentTime();
|
||||
reqToken.issuedAt(now);
|
||||
// the same as in KEYCLOAK-2986, JWTClientCredentialsProvider's timeout field
|
||||
reqToken.expiration(now + 10);
|
||||
reqToken.notBefore(now);
|
||||
return reqToken;
|
||||
}
|
||||
|
||||
}
|
||||
+90
-154
@@ -18,27 +18,23 @@
|
||||
package org.keycloak.adapters.authorization;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.AuthorizationContext;
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
import org.keycloak.adapters.OIDCHttpFacade;
|
||||
import org.keycloak.adapters.spi.HttpFacade;
|
||||
import org.keycloak.adapters.spi.HttpFacade.Request;
|
||||
import org.keycloak.authorization.client.AuthzClient;
|
||||
import org.keycloak.authorization.client.ClientAuthorizationContext;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.AccessToken.Authorization;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.ScopeEnforcementMode;
|
||||
import org.keycloak.representations.idm.authorization.Permission;
|
||||
|
||||
/**
|
||||
@@ -47,89 +43,77 @@ import org.keycloak.representations.idm.authorization.Permission;
|
||||
public abstract class AbstractPolicyEnforcer {
|
||||
|
||||
private static Logger LOGGER = Logger.getLogger(AbstractPolicyEnforcer.class);
|
||||
private static final String HTTP_METHOD_DELETE = "DELETE";
|
||||
|
||||
private final PolicyEnforcerConfig enforcerConfig;
|
||||
private final PolicyEnforcer policyEnforcer;
|
||||
|
||||
protected AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
|
||||
private Map<String, PathConfig> paths;
|
||||
private AuthzClient authzClient;
|
||||
private PathMatcher pathMatcher;
|
||||
|
||||
public AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
|
||||
this.policyEnforcer = policyEnforcer;
|
||||
this.enforcerConfig = policyEnforcer.getEnforcerConfig();
|
||||
this.authzClient = policyEnforcer.getClient();
|
||||
this.pathMatcher = policyEnforcer.getPathMatcher();
|
||||
this.paths = policyEnforcer.getPaths();
|
||||
}
|
||||
|
||||
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
|
||||
EnforcementMode enforcementMode = getEnforcerConfig().getEnforcementMode();
|
||||
EnforcementMode enforcementMode = this.enforcerConfig.getEnforcementMode();
|
||||
|
||||
if (EnforcementMode.DISABLED.equals(enforcementMode)) {
|
||||
return createEmptyAuthorizationContext(true);
|
||||
}
|
||||
|
||||
Request request = httpFacade.getRequest();
|
||||
PathConfig pathConfig = getPathConfig(request);
|
||||
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
||||
|
||||
if (securityContext == null) {
|
||||
if (!isDefaultAccessDeniedUri(request)) {
|
||||
if (pathConfig != null) {
|
||||
challenge(pathConfig, getRequiredScopes(pathConfig, request), httpFacade);
|
||||
} else {
|
||||
if (securityContext != null) {
|
||||
AccessToken accessToken = securityContext.getToken();
|
||||
|
||||
if (accessToken != null) {
|
||||
Request request = httpFacade.getRequest();
|
||||
String path = getPath(request);
|
||||
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
|
||||
|
||||
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
|
||||
|
||||
if (pathConfig == null) {
|
||||
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
||||
return createAuthorizationContext(accessToken, null);
|
||||
}
|
||||
|
||||
LOGGER.debugf("Could not find a configuration for path [%s]", path);
|
||||
|
||||
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
|
||||
return createAuthorizationContext(accessToken, null);
|
||||
}
|
||||
|
||||
handleAccessDenied(httpFacade);
|
||||
|
||||
return createEmptyAuthorizationContext(false);
|
||||
}
|
||||
|
||||
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
|
||||
return createEmptyAuthorizationContext(true);
|
||||
}
|
||||
|
||||
MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
|
||||
|
||||
if (isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
|
||||
try {
|
||||
return createAuthorizationContext(accessToken, pathConfig);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
|
||||
|
||||
if (!challenge(pathConfig, methodConfig, httpFacade)) {
|
||||
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
|
||||
handleAccessDenied(httpFacade);
|
||||
}
|
||||
}
|
||||
return createEmptyAuthorizationContext(false);
|
||||
}
|
||||
|
||||
AccessToken accessToken = securityContext.getToken();
|
||||
|
||||
if (accessToken != null) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
|
||||
}
|
||||
|
||||
if (pathConfig == null) {
|
||||
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
|
||||
return createAuthorizationContext(accessToken, null);
|
||||
}
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debugf("Could not find a configuration for path [%s]", getPath(request));
|
||||
}
|
||||
|
||||
if (isDefaultAccessDeniedUri(request)) {
|
||||
return createAuthorizationContext(accessToken, null);
|
||||
}
|
||||
|
||||
handleAccessDenied(httpFacade);
|
||||
|
||||
return createEmptyAuthorizationContext(false);
|
||||
}
|
||||
|
||||
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
|
||||
return createAuthorizationContext(accessToken, pathConfig);
|
||||
}
|
||||
|
||||
MethodConfig methodConfig = getRequiredScopes(pathConfig, request);
|
||||
|
||||
if (isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
|
||||
try {
|
||||
return createAuthorizationContext(accessToken, pathConfig);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (methodConfig != null && ScopeEnforcementMode.DISABLED.equals(methodConfig.getScopesEnforcementMode())) {
|
||||
return createEmptyAuthorizationContext(true);
|
||||
}
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
|
||||
}
|
||||
|
||||
if (!challenge(pathConfig, methodConfig, httpFacade)) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
|
||||
}
|
||||
handleAccessDenied(httpFacade);
|
||||
}
|
||||
}
|
||||
|
||||
return createEmptyAuthorizationContext(false);
|
||||
@@ -139,22 +123,23 @@ public abstract class AbstractPolicyEnforcer {
|
||||
|
||||
protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
|
||||
Request request = httpFacade.getRequest();
|
||||
PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
|
||||
|
||||
if (isDefaultAccessDeniedUri(request)) {
|
||||
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Authorization authorization = accessToken.getAuthorization();
|
||||
AccessToken.Authorization authorization = accessToken.getAuthorization();
|
||||
|
||||
if (authorization == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Permission> permissions = authorization.getPermissions();
|
||||
boolean hasPermission = false;
|
||||
List<Permission> grantedPermissions = authorization.getPermissions();
|
||||
|
||||
for (Permission permission : grantedPermissions) {
|
||||
if (permission.getResourceId() != null) {
|
||||
for (Permission permission : permissions) {
|
||||
if (permission.getResourceSetId() != null) {
|
||||
if (isResourcePermission(actualPathConfig, permission)) {
|
||||
hasPermission = true;
|
||||
|
||||
@@ -163,14 +148,11 @@ public abstract class AbstractPolicyEnforcer {
|
||||
}
|
||||
|
||||
if (hasResourceScopePermission(methodConfig, permission)) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions);
|
||||
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
|
||||
if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
|
||||
this.paths.remove(actualPathConfig);
|
||||
}
|
||||
if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
|
||||
policyEnforcer.getPathMatcher().removeFromCache(getPath(request));
|
||||
}
|
||||
|
||||
return hasValidClaims(actualPathConfig, permission, httpFacade, authorization);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -185,54 +167,24 @@ public abstract class AbstractPolicyEnforcer {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, grantedPermissions);
|
||||
}
|
||||
LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, permissions);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hasValidClaims(PathConfig actualPathConfig, Permission permission, OIDCHttpFacade httpFacade, Authorization authorization) {
|
||||
Map<String, Set<String>> grantedClaims = permission.getClaims();
|
||||
|
||||
if (grantedClaims != null) {
|
||||
Map<String, List<String>> claims = resolveClaims(actualPathConfig, httpFacade);
|
||||
|
||||
if (claims.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Entry<String, Set<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) {
|
||||
httpFacade.getResponse().sendError(403);
|
||||
}
|
||||
|
||||
protected AuthzClient getAuthzClient() {
|
||||
return policyEnforcer.getClient();
|
||||
}
|
||||
private boolean isDefaultAccessDeniedUri(Request request, PolicyEnforcerConfig enforcerConfig) {
|
||||
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
|
||||
|
||||
protected PolicyEnforcerConfig getEnforcerConfig() {
|
||||
return policyEnforcer.getEnforcerConfig();
|
||||
}
|
||||
|
||||
protected PolicyEnforcer getPolicyEnforcer() {
|
||||
return policyEnforcer;
|
||||
}
|
||||
|
||||
private boolean isDefaultAccessDeniedUri(Request request) {
|
||||
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
|
||||
return accessDeniedPath != null && request.getURI().contains(accessDeniedPath);
|
||||
if (accessDeniedPath != null) {
|
||||
if (request.getURI().contains(accessDeniedPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
|
||||
@@ -260,8 +212,20 @@ public abstract class AbstractPolicyEnforcer {
|
||||
return requiredScopes.isEmpty();
|
||||
}
|
||||
|
||||
protected AuthzClient getAuthzClient() {
|
||||
return this.authzClient;
|
||||
}
|
||||
|
||||
protected PolicyEnforcerConfig getEnforcerConfig() {
|
||||
return enforcerConfig;
|
||||
}
|
||||
|
||||
protected PolicyEnforcer getPolicyEnforcer() {
|
||||
return policyEnforcer;
|
||||
}
|
||||
|
||||
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
|
||||
return new ClientAuthorizationContext(getAuthzClient()) {
|
||||
return new ClientAuthorizationContext(authzClient) {
|
||||
@Override
|
||||
public boolean hasPermission(String resourceName, String scopeName) {
|
||||
return granted;
|
||||
@@ -312,7 +276,7 @@ public abstract class AbstractPolicyEnforcer {
|
||||
}
|
||||
|
||||
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
|
||||
return new ClientAuthorizationContext(accessToken, pathConfig, getAuthzClient());
|
||||
return new ClientAuthorizationContext(accessToken, pathConfig, this.paths, authzClient);
|
||||
}
|
||||
|
||||
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
||||
@@ -328,34 +292,6 @@ public abstract class AbstractPolicyEnforcer {
|
||||
}
|
||||
|
||||
private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
||||
return permission.getResourceId().equals(actualPathConfig.getId());
|
||||
}
|
||||
|
||||
private PathConfig getPathConfig(Request request) {
|
||||
return isDefaultAccessDeniedUri(request) ? null : policyEnforcer.getPathMatcher().matches(getPath(request));
|
||||
}
|
||||
|
||||
protected Map<String, List<String>> resolveClaims(PathConfig pathConfig, OIDCHttpFacade httpFacade) {
|
||||
Map<String, List<String>> claims = getClaims(getEnforcerConfig().getClaimInformationPointConfig(), httpFacade);
|
||||
|
||||
claims.putAll(getClaims(pathConfig.getClaimInformationPointConfig(), httpFacade));
|
||||
|
||||
return claims;
|
||||
}
|
||||
|
||||
private Map<String, List<String>> getClaims(Map<String, Map<String, Object>>claimInformationPointConfig, HttpFacade httpFacade) {
|
||||
Map<String, List<String>> claims = new HashMap<>();
|
||||
|
||||
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;
|
||||
return permission.getResourceSetId().equals(actualPathConfig.getId());
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.representation.PermissionRequest;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
||||
challengeUmaAuthentication(pathConfig, methodConfig, facade);
|
||||
} else {
|
||||
challengeEntitlementAuthentication(facade);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void challengeEntitlementAuthentication(OIDCHttpFacade facade) {
|
||||
HttpFacade.Response response = facade.getResponse();
|
||||
AuthzClient authzClient = getAuthzClient();
|
||||
String clientId = authzClient.getConfiguration().getResource();
|
||||
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/entitlement";
|
||||
response.setStatus(401);
|
||||
response.setHeader("WWW-Authenticate", "KC_ETT realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\"");
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Sending Entitlement challenge");
|
||||
}
|
||||
}
|
||||
|
||||
private void challengeUmaAuthentication(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
|
||||
HttpFacade.Response response = facade.getResponse();
|
||||
AuthzClient authzClient = getAuthzClient();
|
||||
String ticket = getPermissionTicket(pathConfig, methodConfig, authzClient);
|
||||
String clientId = authzClient.getConfiguration().getResource();
|
||||
String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/authorize";
|
||||
response.setStatus(401);
|
||||
response.setHeader("WWW-Authenticate", "UMA realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\",ticket=\"" + ticket + "\"");
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Sending UMA challenge");
|
||||
}
|
||||
}
|
||||
|
||||
private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient) {
|
||||
ProtectionResource protection = authzClient.protection();
|
||||
PermissionResource permission = protection.permission();
|
||||
PermissionRequest permissionRequest = new PermissionRequest();
|
||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
||||
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
|
||||
return permission.forResource(permissionRequest).getTicket();
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
+43
-111
@@ -18,30 +18,26 @@
|
||||
package org.keycloak.adapters.authorization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.OIDCHttpFacade;
|
||||
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
|
||||
import org.keycloak.adapters.spi.HttpFacade;
|
||||
import org.keycloak.authorization.client.AuthorizationDeniedException;
|
||||
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.authorization.client.representation.AuthorizationRequest;
|
||||
import org.keycloak.authorization.client.representation.AuthorizationResponse;
|
||||
import org.keycloak.authorization.client.representation.EntitlementRequest;
|
||||
import org.keycloak.authorization.client.representation.EntitlementResponse;
|
||||
import org.keycloak.authorization.client.representation.PermissionRequest;
|
||||
import org.keycloak.authorization.client.representation.PermissionResponse;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
|
||||
import org.keycloak.representations.idm.authorization.AuthorizationResponse;
|
||||
import org.keycloak.representations.idm.authorization.Permission;
|
||||
import org.keycloak.representations.idm.authorization.PermissionRequest;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
@@ -78,14 +74,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
||||
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
|
||||
|
||||
if (newAuthorization != null) {
|
||||
List<Permission> grantedPermissions = authorization.getPermissions();
|
||||
List<Permission> newPermissions = newAuthorization.getPermissions();
|
||||
|
||||
for (Permission newPermission : newPermissions) {
|
||||
if (!grantedPermissions.contains(newPermission)) {
|
||||
grantedPermissions.add(newPermission);
|
||||
}
|
||||
}
|
||||
authorization.getPermissions().addAll(newAuthorization.getPermissions());
|
||||
}
|
||||
|
||||
original.setAuthorization(authorization);
|
||||
@@ -94,29 +83,8 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
|
||||
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);
|
||||
|
||||
protected boolean challenge(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade facade) {
|
||||
handleAccessDenied(facade);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -134,87 +102,51 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
|
||||
}
|
||||
|
||||
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
|
||||
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
|
||||
String accessTokenString = securityContext.getTokenString();
|
||||
String accessToken = httpFacade.getSecurityContext().getTokenString();
|
||||
AuthzClient authzClient = getAuthzClient();
|
||||
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
|
||||
AccessToken accessToken = securityContext.getToken();
|
||||
AuthorizationRequest authzRequest = new AuthorizationRequest();
|
||||
|
||||
if (isBearerAuthorization(httpFacade) || accessToken.getAuthorization() != null) {
|
||||
authzRequest.addPermission(pathConfig.getId(), methodConfig.getScopes());
|
||||
}
|
||||
if (getEnforcerConfig().getUserManagedAccess() != null) {
|
||||
LOGGER.debug("Obtaining authorization for authenticated user.");
|
||||
PermissionRequest permissionRequest = new PermissionRequest();
|
||||
|
||||
Map<String, List<String>> claims = resolveClaims(pathConfig, httpFacade);
|
||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
||||
permissionRequest.setScopes(new HashSet<>(methodConfig.getScopes()));
|
||||
|
||||
if (!claims.isEmpty()) {
|
||||
authzRequest.setClaimTokenFormat("urn:ietf:params:oauth:token-type:jwt");
|
||||
authzRequest.setClaimToken(Base64.encodeBytes(JsonSerialization.writeValueAsBytes(claims)));
|
||||
}
|
||||
PermissionResponse permissionResponse = authzClient.protection().permission().forResource(permissionRequest);
|
||||
AuthorizationRequest authzRequest = new AuthorizationRequest(permissionResponse.getTicket());
|
||||
AuthorizationResponse authzResponse = authzClient.authorization(accessToken).authorize(authzRequest);
|
||||
|
||||
if (accessToken.getAuthorization() != null) {
|
||||
authzRequest.setRpt(accessTokenString);
|
||||
}
|
||||
if (authzResponse != null) {
|
||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
|
||||
}
|
||||
|
||||
LOGGER.debug("Obtaining authorization for authenticated user.");
|
||||
AuthorizationResponse authzResponse;
|
||||
|
||||
if (isBearerAuthorization(httpFacade)) {
|
||||
authzRequest.setSubjectToken(accessTokenString);
|
||||
authzResponse = getAuthzClient().authorization().authorize(authzRequest);
|
||||
return null;
|
||||
} else {
|
||||
authzResponse = getAuthzClient().authorization(accessTokenString).authorize(authzRequest);
|
||||
}
|
||||
LOGGER.debug("Obtaining entitlements for authenticated user.");
|
||||
AccessToken token = httpFacade.getSecurityContext().getToken();
|
||||
|
||||
if (authzResponse != null) {
|
||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getToken(), deployment);
|
||||
if (token.getAuthorization() == null) {
|
||||
EntitlementResponse authzResponse = authzClient.entitlement(accessToken).getAll(authzClient.getConfiguration().getResource());
|
||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
|
||||
} else {
|
||||
EntitlementRequest request = new EntitlementRequest();
|
||||
PermissionRequest permissionRequest = new PermissionRequest();
|
||||
permissionRequest.setResourceSetId(pathConfig.getId());
|
||||
permissionRequest.setResourceSetName(pathConfig.getName());
|
||||
permissionRequest.setScopes(new HashSet<>(pathConfig.getScopes()));
|
||||
LOGGER.debugf("Sending entitlements request: resource_set_id [%s], resource_set_name [%s], scopes [%s].", permissionRequest.getResourceSetId(), permissionRequest.getResourceSetName(), permissionRequest.getScopes());
|
||||
request.addPermission(permissionRequest);
|
||||
EntitlementResponse authzResponse = authzClient.entitlement(accessToken).get(authzClient.getConfiguration().getResource(), request);
|
||||
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
|
||||
}
|
||||
}
|
||||
} catch (AuthorizationDeniedException ignore) {
|
||||
LOGGER.debug("Authorization denied", ignore);
|
||||
} catch (AuthorizationDeniedException e) {
|
||||
LOGGER.debug("Authorization denied", e);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unexpected error during authorization request.", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getPermissionTicket(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AuthzClient authzClient, OIDCHttpFacade 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 = resolveClaims(pathConfig, httpFacade);
|
||||
|
||||
if (!claims.isEmpty()) {
|
||||
permissionRequest.setClaims(claims);
|
||||
}
|
||||
|
||||
return permission.create(permissionRequest).getTicket();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isBearerAuthorization(OIDCHttpFacade httpFacade) {
|
||||
List<String> authHeaders = httpFacade.getRequest().getHeaders("Authorization");
|
||||
|
||||
if (authHeaders != null) {
|
||||
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 getPolicyEnforcer().getDeployment().isBearerOnly();
|
||||
}
|
||||
}
|
||||
+11
-15
@@ -21,12 +21,9 @@ import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||
|
||||
/**
|
||||
* A simple LRU cache implementation supporting expiration and maximum number of entries.
|
||||
*
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class PathCache {
|
||||
@@ -41,7 +38,15 @@ public class PathCache {
|
||||
private final AtomicBoolean writing = new AtomicBoolean(false);
|
||||
|
||||
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.
|
||||
@@ -57,14 +62,9 @@ public class PathCache {
|
||||
}
|
||||
};
|
||||
this.maxAge = maxAge;
|
||||
this.enabled = maxAge > 0;
|
||||
}
|
||||
|
||||
public void put(String uri, PathConfig newValue) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (parkForWriteAndCheckInterrupt()) {
|
||||
return;
|
||||
@@ -80,10 +80,6 @@ public class PathCache {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean containsKey(String uri) {
|
||||
return cache.containsKey(uri);
|
||||
}
|
||||
|
||||
public PathConfig get(String uri) {
|
||||
if (parkForReadAndCheckInterrupt()) {
|
||||
return null;
|
||||
@@ -155,7 +151,7 @@ public class PathCache {
|
||||
if(maxAge == -1) {
|
||||
expiration = -1;
|
||||
} else {
|
||||
expiration = Time.currentTimeMillis() + maxAge;
|
||||
expiration = System.currentTimeMillis() + maxAge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +164,7 @@ public class PathCache {
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return expiration != -1 ? Time.currentTimeMillis() > expiration : false;
|
||||
return expiration != -1 ? System.currentTimeMillis() > expiration : false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* 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.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.authorization.client.AuthzClient;
|
||||
import org.keycloak.authorization.client.representation.ResourceRepresentation;
|
||||
import org.keycloak.authorization.client.resource.ProtectedResource;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
class PathMatcher {
|
||||
|
||||
private static final char WILDCARD = '*';
|
||||
private final AuthzClient authzClient;
|
||||
// TODO: make this configurable
|
||||
private PathCache cache = new PathCache(100, 30000);
|
||||
|
||||
public PathMatcher(AuthzClient authzClient) {
|
||||
this.authzClient = authzClient;
|
||||
}
|
||||
|
||||
public PathConfig matches(final String targetUri, Map<String, PathConfig> paths) {
|
||||
PathConfig pathConfig = paths.get(targetUri) == null ? cache.get(targetUri) : paths.get(targetUri);
|
||||
|
||||
if (pathConfig != null) {
|
||||
return pathConfig;
|
||||
}
|
||||
|
||||
PathConfig matchingAnyPath = null;
|
||||
PathConfig matchingAnySuffixPath = null;
|
||||
|
||||
for (PathConfig entry : paths.values()) {
|
||||
String expectedUri = entry.getPath();
|
||||
String matchingUri = null;
|
||||
|
||||
if (exactMatch(expectedUri, targetUri, expectedUri)) {
|
||||
matchingUri = expectedUri;
|
||||
}
|
||||
|
||||
if (isTemplate(expectedUri)) {
|
||||
String templateUri = buildUriFromTemplate(expectedUri, targetUri);
|
||||
|
||||
if (templateUri != null) {
|
||||
if (exactMatch(expectedUri, targetUri, templateUri)) {
|
||||
matchingUri = templateUri;
|
||||
entry = resolvePathConfig(entry, targetUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingUri != null) {
|
||||
StringBuilder path = new StringBuilder(expectedUri);
|
||||
int patternIndex = path.indexOf("/" + WILDCARD);
|
||||
|
||||
if (patternIndex != -1) {
|
||||
path.delete(patternIndex, path.length());
|
||||
}
|
||||
|
||||
patternIndex = path.indexOf("{");
|
||||
|
||||
if (patternIndex != -1) {
|
||||
path.delete(patternIndex, path.length());
|
||||
}
|
||||
|
||||
String pathString = path.toString();
|
||||
|
||||
if ("".equals(pathString)) {
|
||||
pathString = "/";
|
||||
}
|
||||
|
||||
if (matchingUri.equals(targetUri) || pathString.equals(targetUri)) {
|
||||
cache.put(targetUri, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
if (WILDCARD == expectedUri.charAt(expectedUri.length() - 1)) {
|
||||
matchingAnyPath = entry;
|
||||
} else {
|
||||
int suffixIndex = expectedUri.indexOf(WILDCARD + ".");
|
||||
|
||||
if (suffixIndex != -1) {
|
||||
String protectedSuffix = expectedUri.substring(suffixIndex + 1);
|
||||
|
||||
if (targetUri.endsWith(protectedSuffix)) {
|
||||
matchingAnySuffixPath = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingAnySuffixPath != null) {
|
||||
cache.put(targetUri, matchingAnySuffixPath);
|
||||
return matchingAnySuffixPath;
|
||||
}
|
||||
|
||||
if (matchingAnyPath != null) {
|
||||
cache.put(targetUri, matchingAnyPath);
|
||||
}
|
||||
|
||||
return matchingAnyPath;
|
||||
}
|
||||
|
||||
private boolean exactMatch(String expectedUri, String targetUri, String value) {
|
||||
if (targetUri.equals(value)) {
|
||||
return value.equals(targetUri);
|
||||
}
|
||||
|
||||
if (endsWithWildcard(expectedUri)) {
|
||||
return targetUri.startsWith(expectedUri.substring(0, expectedUri.length() - 2));
|
||||
}
|
||||
|
||||
String suffix = "/*.";
|
||||
int suffixIndex = expectedUri.indexOf(suffix);
|
||||
|
||||
if (suffixIndex != -1) {
|
||||
return targetUri.endsWith(expectedUri.substring(suffixIndex + suffix.length() - 1));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public String buildUriFromTemplate(String expectedUri, String targetUri) {
|
||||
int patternStartIndex = expectedUri.indexOf("{");
|
||||
|
||||
if (patternStartIndex == -1 || patternStartIndex >= targetUri.length()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expectedUri.split("/").length > targetUri.split("/").length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
char[] expectedUriChars = expectedUri.toCharArray();
|
||||
char[] matchingUri = Arrays.copyOfRange(expectedUriChars, 0, patternStartIndex);
|
||||
int matchingUriLastIndex = matchingUri.length;
|
||||
String targetUriParams = targetUri.substring(patternStartIndex);
|
||||
|
||||
if (Arrays.equals(matchingUri, Arrays.copyOf(targetUri.toCharArray(), matchingUri.length))) {
|
||||
matchingUri = Arrays.copyOf(matchingUri, targetUri.length());
|
||||
int paramIndex = 0;
|
||||
|
||||
for (int i = patternStartIndex; i < expectedUriChars.length; i++) {
|
||||
if (matchingUriLastIndex >= matchingUri.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
char c = expectedUriChars[i];
|
||||
|
||||
if (c == '{' || c == '*') {
|
||||
String[] params = targetUriParams.split("/");
|
||||
|
||||
for (int k = paramIndex; k <= (c == '*' ? params.length : paramIndex); k++) {
|
||||
if (k == params.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
int paramLength = params[k].length();
|
||||
|
||||
if (matchingUriLastIndex + paramLength > matchingUri.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int j = 0; j < paramLength; j++) {
|
||||
matchingUri[matchingUriLastIndex++] = params[k].charAt(j);
|
||||
}
|
||||
|
||||
if (c == '*' && matchingUriLastIndex < matchingUri.length) {
|
||||
matchingUri[matchingUriLastIndex++] = '/';
|
||||
}
|
||||
}
|
||||
|
||||
i = expectedUri.indexOf('}', i);
|
||||
} else {
|
||||
if (c == '/') {
|
||||
paramIndex++;
|
||||
}
|
||||
matchingUri[matchingUriLastIndex++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingUri[matchingUri.length - 1] == '\u0000') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return String.valueOf(matchingUri);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean endsWithWildcard(String expectedUri) {
|
||||
return WILDCARD == expectedUri.charAt(expectedUri.length() - 1);
|
||||
}
|
||||
|
||||
private boolean isTemplate(String uri) {
|
||||
return uri.indexOf("{") != -1;
|
||||
}
|
||||
|
||||
private PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
|
||||
if (originalConfig.hasPattern()) {
|
||||
ProtectedResource resource = this.authzClient.protection().resource();
|
||||
Set<String> search = resource.findByFilter("uri=" + path);
|
||||
|
||||
if (!search.isEmpty()) {
|
||||
// resource does exist on the server, cache it
|
||||
ResourceRepresentation targetResource = resource.findById(search.iterator().next()).getResourceDescription();
|
||||
PathConfig config = PolicyEnforcer.createPathConfig(targetResource);
|
||||
|
||||
config.setScopes(originalConfig.getScopes());
|
||||
config.setMethods(originalConfig.getMethods());
|
||||
config.setParentConfig(originalConfig);
|
||||
config.setEnforcementMode(originalConfig.getEnforcementMode());
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
return originalConfig;
|
||||
}
|
||||
}
|
||||
+89
-181
@@ -17,17 +17,6 @@
|
||||
*/
|
||||
package org.keycloak.adapters.authorization;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.ServiceLoader;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.AuthorizationContext;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
@@ -36,14 +25,22 @@ import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
|
||||
import org.keycloak.authorization.client.AuthzClient;
|
||||
import org.keycloak.authorization.client.ClientAuthenticator;
|
||||
import org.keycloak.authorization.client.Configuration;
|
||||
import org.keycloak.authorization.client.representation.RegistrationResponse;
|
||||
import org.keycloak.authorization.client.representation.ResourceRepresentation;
|
||||
import org.keycloak.authorization.client.representation.ScopeRepresentation;
|
||||
import org.keycloak.authorization.client.resource.ProtectedResource;
|
||||
import org.keycloak.common.util.PathMatcher;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathCacheConfig;
|
||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
|
||||
import org.keycloak.representations.idm.authorization.Permission;
|
||||
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
@@ -55,27 +52,20 @@ public class PolicyEnforcer {
|
||||
private final KeycloakDeployment deployment;
|
||||
private final AuthzClient authzClient;
|
||||
private final PolicyEnforcerConfig enforcerConfig;
|
||||
private final PathConfigMatcher pathMatcher;
|
||||
private final Map<String, PathConfig> paths;
|
||||
private final Map<String, ClaimInformationPointProviderFactory> claimInformationPointProviderFactories = new HashMap<>();
|
||||
private final PathMatcher pathMatcher;
|
||||
|
||||
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
|
||||
this.deployment = deployment;
|
||||
this.enforcerConfig = adapterConfig.getPolicyEnforcerConfig();
|
||||
Configuration configuration = new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient());
|
||||
this.authzClient = AuthzClient.create(configuration, new ClientAuthenticator() {
|
||||
this.authzClient = AuthzClient.create(new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient()), new ClientAuthenticator() {
|
||||
@Override
|
||||
public void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders) {
|
||||
Map<String, String> formparams = new HashMap<>();
|
||||
ClientCredentialsProviderUtils.setClientCredentials(PolicyEnforcer.this.deployment, requestHeaders, formparams);
|
||||
for (Entry<String, String> param : formparams.entrySet()) {
|
||||
requestParams.put(param.getKey(), Arrays.asList(param.getValue()));
|
||||
}
|
||||
public void configureClientCredentials(HashMap<String, String> requestParams, HashMap<String, String> requestHeaders) {
|
||||
ClientCredentialsProviderUtils.setClientCredentials(PolicyEnforcer.this.deployment, requestHeaders, requestParams);
|
||||
}
|
||||
});
|
||||
|
||||
paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
|
||||
pathMatcher = new PathConfigMatcher(paths, enforcerConfig, authzClient);
|
||||
this.pathMatcher = new PathMatcher(this.authzClient);
|
||||
this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Initialization complete. Path configurations:");
|
||||
@@ -83,17 +73,20 @@ public class PolicyEnforcer {
|
||||
LOGGER.debug(pathConfig);
|
||||
}
|
||||
}
|
||||
|
||||
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, ClaimInformationPointProviderFactory.class.getClassLoader()));
|
||||
loadClaimInformationPointProviders(ServiceLoader.load(ClaimInformationPointProviderFactory.class, Thread.currentThread().getContextClassLoader()));
|
||||
}
|
||||
|
||||
public AuthorizationContext enforce(OIDCHttpFacade facade) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debugv("Policy enforcement is enabled. Enforcing policy decisions for path [{0}].", facade.getRequest().getURI());
|
||||
LOGGER.debugv("Policy enforcement is enable. Enforcing policy decisions for path [{0}].", facade.getRequest().getURI());
|
||||
}
|
||||
|
||||
AuthorizationContext context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
|
||||
AuthorizationContext context;
|
||||
|
||||
if (deployment.isBearerOnly()) {
|
||||
context = new BearerTokenPolicyEnforcer(this).authorize(facade);
|
||||
} else {
|
||||
context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
|
||||
}
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", facade.getRequest().getURI(), context.isGranted() ? "GRANTED" : "DENIED");
|
||||
@@ -106,11 +99,11 @@ public class PolicyEnforcer {
|
||||
return context;
|
||||
}
|
||||
|
||||
public PolicyEnforcerConfig getEnforcerConfig() {
|
||||
PolicyEnforcerConfig getEnforcerConfig() {
|
||||
return enforcerConfig;
|
||||
}
|
||||
|
||||
public AuthzClient getClient() {
|
||||
AuthzClient getClient() {
|
||||
return authzClient;
|
||||
}
|
||||
|
||||
@@ -118,30 +111,14 @@ public class PolicyEnforcer {
|
||||
return paths;
|
||||
}
|
||||
|
||||
public PathConfigMatcher getPathMatcher() {
|
||||
return pathMatcher;
|
||||
void addPath(PathConfig pathConfig) {
|
||||
paths.put(pathConfig.getPath(), pathConfig);
|
||||
}
|
||||
|
||||
public KeycloakDeployment getDeployment() {
|
||||
KeycloakDeployment getDeployment() {
|
||||
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) {
|
||||
boolean loadPathsFromServer = true;
|
||||
|
||||
@@ -154,11 +131,7 @@ public class PolicyEnforcer {
|
||||
|
||||
if (loadPathsFromServer) {
|
||||
LOGGER.info("No path provided in configuration.");
|
||||
Map<String, PathConfig> paths = configureAllPathsForResourceServer(protectedResource);
|
||||
|
||||
paths.putAll(configureDefinedPaths(protectedResource, enforcerConfig));
|
||||
|
||||
return paths;
|
||||
return configureAllPathsForResourceServer(protectedResource);
|
||||
} else {
|
||||
LOGGER.info("Paths provided in configuration.");
|
||||
return configureDefinedPaths(protectedResource, enforcerConfig);
|
||||
@@ -166,41 +139,56 @@ public class PolicyEnforcer {
|
||||
}
|
||||
|
||||
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
|
||||
Map<String, PathConfig> paths = Collections.synchronizedMap(new LinkedHashMap<String, PathConfig>());
|
||||
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
|
||||
|
||||
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
|
||||
ResourceRepresentation resource;
|
||||
Set<String> search;
|
||||
String resourceName = pathConfig.getName();
|
||||
String path = pathConfig.getPath();
|
||||
|
||||
if (resourceName != null) {
|
||||
LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path);
|
||||
resource = protectedResource.findByName(resourceName);
|
||||
search = protectedResource.findByFilter("name=" + resourceName);
|
||||
} else {
|
||||
LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
|
||||
List<ResourceRepresentation> resources = protectedResource.findByUri(path);
|
||||
|
||||
if (resources.isEmpty()) {
|
||||
resources = protectedResource.findByMatchingUri(path);
|
||||
}
|
||||
|
||||
if (resources.size() == 1) {
|
||||
resource = resources.get(0);
|
||||
} else if (resources.size() > 1) {
|
||||
throw new RuntimeException("Multiple resources found with the same uri");
|
||||
} else {
|
||||
resource = null;
|
||||
}
|
||||
search = protectedResource.findByFilter("uri=" + path);
|
||||
}
|
||||
|
||||
if (resource != null) {
|
||||
pathConfig.setId(resource.getId());
|
||||
if (search.isEmpty()) {
|
||||
if (enforcerConfig.isCreateResources()) {
|
||||
LOGGER.debugf("Creating resource on server for path [%s].", pathConfig);
|
||||
ResourceRepresentation resource = new ResourceRepresentation();
|
||||
|
||||
resource.setName(resourceName);
|
||||
resource.setType(pathConfig.getType());
|
||||
resource.setUri(path);
|
||||
|
||||
HashSet<ScopeRepresentation> scopes = new HashSet<>();
|
||||
|
||||
for (String scopeName : pathConfig.getScopes()) {
|
||||
ScopeRepresentation scope = new ScopeRepresentation();
|
||||
|
||||
scope.setName(scopeName);
|
||||
|
||||
scopes.add(scope);
|
||||
}
|
||||
|
||||
resource.setScopes(scopes);
|
||||
|
||||
RegistrationResponse registrationResponse = protectedResource.create(resource);
|
||||
|
||||
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(search.iterator().next());
|
||||
}
|
||||
|
||||
PathConfig existingPath = null;
|
||||
|
||||
for (PathConfig current : paths.values()) {
|
||||
if (current.getPath().equals(pathConfig.getPath())) {
|
||||
if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) {
|
||||
existingPath = current;
|
||||
break;
|
||||
}
|
||||
@@ -221,126 +209,46 @@ public class PolicyEnforcer {
|
||||
LOGGER.info("Querying the server for all resources associated with this application.");
|
||||
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
|
||||
|
||||
if (!enforcerConfig.getLazyLoadPaths()) {
|
||||
for (String id : protectedResource.findAll()) {
|
||||
ResourceRepresentation resourceDescription = protectedResource.findById(id);
|
||||
for (String id : protectedResource.findAll()) {
|
||||
RegistrationResponse response = protectedResource.findById(id);
|
||||
ResourceRepresentation resourceDescription = response.getResourceDescription();
|
||||
|
||||
if (resourceDescription.getUris() != null && !resourceDescription.getUris().isEmpty()) {
|
||||
for(PathConfig pathConfig : PathConfig.createPathConfigs(resourceDescription)) {
|
||||
paths.put(pathConfig.getPath(), pathConfig);
|
||||
}
|
||||
}
|
||||
if (resourceDescription.getUri() != null) {
|
||||
PathConfig pathConfig = createPathConfig(resourceDescription);
|
||||
paths.put(pathConfig.getPath(), pathConfig);
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
public class PathConfigMatcher extends PathMatcher<PathConfig> {
|
||||
static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
|
||||
PathConfig pathConfig = new PathConfig();
|
||||
|
||||
private final Map<String, PathConfig> paths;
|
||||
private final PathCache pathCache;
|
||||
private final AuthzClient authzClient;
|
||||
private final PolicyEnforcerConfig enforcerConfig;
|
||||
pathConfig.setId(resourceDescription.getId());
|
||||
pathConfig.setName(resourceDescription.getName());
|
||||
|
||||
public PathConfigMatcher(Map<String, PathConfig> paths, PolicyEnforcerConfig enforcerConfig, AuthzClient authzClient) {
|
||||
this.paths = paths;
|
||||
this.enforcerConfig = enforcerConfig;
|
||||
PathCacheConfig cacheConfig = enforcerConfig.getPathCacheConfig();
|
||||
String uri = resourceDescription.getUri();
|
||||
|
||||
if (cacheConfig == null) {
|
||||
cacheConfig = new PathCacheConfig();
|
||||
}
|
||||
|
||||
pathCache = new PathCache(cacheConfig.getMaxEntries(), cacheConfig.getLifespan());
|
||||
this.authzClient = authzClient;
|
||||
if (uri == null || "".equals(uri.trim())) {
|
||||
throw new RuntimeException("Failed to configure paths. Resource [" + resourceDescription.getName() + "] has an invalid or empty URI [" + uri + "].");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PathConfig matches(String targetUri) {
|
||||
PathConfig pathConfig = pathCache.get(targetUri);
|
||||
pathConfig.setPath(uri);
|
||||
|
||||
if (pathCache.containsKey(targetUri) || pathConfig != null) {
|
||||
return pathConfig;
|
||||
}
|
||||
List<String> scopeNames = new ArrayList<>();
|
||||
|
||||
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()) {
|
||||
Map<String, Map<String, Object>> cipConfig = null;
|
||||
|
||||
if (pathConfig != null) {
|
||||
cipConfig = pathConfig.getClaimInformationPointConfig();
|
||||
}
|
||||
|
||||
pathConfig = PathConfig.createPathConfigs(matchingResources.get(0)).iterator().next();
|
||||
|
||||
if (cipConfig != null) {
|
||||
pathConfig.setClaimInformationPointConfig(cipConfig);
|
||||
}
|
||||
}
|
||||
} catch (Exception cause) {
|
||||
LOGGER.errorf(cause, "Could not lazy load resource with path [" + targetUri + "] from server");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pathCache.put(targetUri, pathConfig);
|
||||
|
||||
return pathConfig;
|
||||
for (ScopeRepresentation scope : resourceDescription.getScopes()) {
|
||||
scopeNames.add(scope.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPath(PathConfig entry) {
|
||||
return entry.getPath();
|
||||
}
|
||||
pathConfig.setScopes(scopeNames);
|
||||
pathConfig.setType(resourceDescription.getType());
|
||||
|
||||
@Override
|
||||
protected Collection<PathConfig> getPaths() {
|
||||
return paths.values();
|
||||
}
|
||||
return pathConfig;
|
||||
}
|
||||
|
||||
@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.createPathConfigs(targetResource).iterator().next();
|
||||
|
||||
config.setScopes(originalConfig.getScopes());
|
||||
config.setMethods(originalConfig.getMethods());
|
||||
config.setParentConfig(originalConfig);
|
||||
config.setEnforcementMode(originalConfig.getEnforcementMode());
|
||||
config.setClaimInformationPointConfig(originalConfig.getClaimInformationPointConfig());
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void removeFromCache(String pathConfig) {
|
||||
pathCache.remove(pathConfig);
|
||||
}
|
||||
};
|
||||
public PathMatcher getPathMatcher() {
|
||||
return pathMatcher;
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,210 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,107 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -16,5 +16,4 @@
|
||||
#
|
||||
|
||||
org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider
|
||||
org.keycloak.adapters.authentication.JWTClientCredentialsProvider
|
||||
org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider
|
||||
org.keycloak.adapters.authentication.JWTClientCredentialsProvider
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# * 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
|
||||
-6
@@ -21,7 +21,6 @@ import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider;
|
||||
import org.keycloak.adapters.authentication.JWTClientCredentialsProvider;
|
||||
import org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider;
|
||||
import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator;
|
||||
import org.keycloak.adapters.rotation.JWKPublicKeyLocator;
|
||||
import org.keycloak.common.enums.RelativeUrlsUsed;
|
||||
@@ -95,9 +94,4 @@ public class KeycloakDeploymentBuilderTest {
|
||||
assertEquals(JWTClientCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadSecretJwtCredentials() throws Exception {
|
||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-secret-jwt.json"));
|
||||
assertEquals(JWTClientSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
|
||||
}
|
||||
}
|
||||
-19
@@ -17,9 +17,7 @@
|
||||
package org.keycloak.adapters;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -48,21 +46,4 @@ public class KeycloakDeploymentTest {
|
||||
|
||||
assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stripDefaultPorts() {
|
||||
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
|
||||
keycloakDeployment.setRealm("test");
|
||||
AdapterConfig config = new AdapterConfig();
|
||||
config.setAuthServerUrl("http://localhost:80/auth");
|
||||
keycloakDeployment.setAuthServerBaseUrl(config);
|
||||
|
||||
assertEquals("http://localhost/auth", keycloakDeployment.getAuthServerBaseUrl());
|
||||
|
||||
config.setAuthServerUrl("https://localhost:443/auth");
|
||||
keycloakDeployment.setAuthServerBaseUrl(config);
|
||||
|
||||
assertEquals("https://localhost/auth", keycloakDeployment.getAuthServerBaseUrl());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"realm": "demo",
|
||||
"resource": "customer-portal",
|
||||
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||
"auth-server-url": "https://localhost:8443/auth",
|
||||
"ssl-required": "external",
|
||||
"credentials": {
|
||||
"secret-jwt": {
|
||||
"secret": "234234-234234-234234"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-as7-integration-pom</artifactId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
-8
@@ -166,13 +166,6 @@ class SharedAttributeDefinitons {
|
||||
.setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true))
|
||||
.build();
|
||||
|
||||
protected static final SimpleAttributeDefinition PROXY_URL =
|
||||
new SimpleAttributeDefinitionBuilder("proxy-url", ModelType.STRING, true)
|
||||
.setXmlName("proxy-url")
|
||||
.setAllowExpression(true)
|
||||
.setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true))
|
||||
.build();
|
||||
|
||||
|
||||
|
||||
protected static final List<SimpleAttributeDefinition> ATTRIBUTES = new ArrayList<SimpleAttributeDefinition>();
|
||||
@@ -199,7 +192,6 @@ class SharedAttributeDefinitons {
|
||||
ATTRIBUTES.add(REGISTER_NODE_PERIOD);
|
||||
ATTRIBUTES.add(TOKEN_STORE);
|
||||
ATTRIBUTES.add(PRINCIPAL_ATTRIBUTE);
|
||||
ATTRIBUTES.add(PROXY_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-2
@@ -46,7 +46,6 @@ keycloak.realm.register-node-at-startup=Cluster setting
|
||||
keycloak.realm.register-node-period=how often to re-register node
|
||||
keycloak.realm.token-store=cookie or session storage for auth session data
|
||||
keycloak.realm.principal-attribute=token attribute to use to set Principal name
|
||||
keycloak.realm.proxy-url=The URL for the HTTP proxy if one is used.
|
||||
|
||||
keycloak.secure-deployment=A deployment secured by Keycloak
|
||||
keycloak.secure-deployment.add=Add a deployment to be secured by Keycloak
|
||||
@@ -83,7 +82,7 @@ keycloak.secure-deployment.principal-attribute=token attribute to use to set Pri
|
||||
keycloak.secure-deployment.turn-off-change-session-id-on-login=The session id is changed by default on a successful login. Change this to true if you want to turn this off
|
||||
keycloak.secure-deployment.token-minimum-time-to-live=The adapter will refresh the token if the current token is expired OR will expire in 'token-minimum-time-to-live' seconds or less
|
||||
keycloak.secure-deployment.min-time-between-jwks-requests=If adapter recognize token signed by unknown public key, it will try to download new public key from keycloak server. However it won't try to download if already tried it in less than 'min-time-between-jwks-requests' seconds
|
||||
keycloak.secure-deployment.proxy-url=The URL for the HTTP proxy if one is used.
|
||||
|
||||
keycloak.secure-deployment.credential=Credential value
|
||||
|
||||
keycloak.credential=Credential
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
<xs:element name="register-node-period" type="xs:integer" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="token-store" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="principal-attribute" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
</xs:all>
|
||||
<xs:attribute name="name" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
@@ -108,7 +107,6 @@
|
||||
<xs:element name="turn-off-change-session-id-on-login" type="xs:boolean" minOccurs="0" maxOccurs="1" />
|
||||
<xs:element name="token-minimum-time-to-live" type="xs:integer" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="min-time-between-jwks-requests" type="xs:integer" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element name="proxy-url" type="xs:string" minOccurs="0" maxOccurs="1"/>
|
||||
</xs:all>
|
||||
<xs:attribute name="name" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<name>Keycloak AS7 / JBoss EAP 6 Integration</name>
|
||||
|
||||
File renamed without changes.
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/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`
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/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
|
||||
|
||||
|
||||
|
||||
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
<?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>3.4.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>keycloak-cli-sso</artifactId>
|
||||
<name>Keycloak CLI SSO Framework</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-installed-adapter</artifactId>
|
||||
</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>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.keycloak.adapters.KeycloakCliSsoMain</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.KeycloakCliSso;
|
||||
import org.keycloak.adapters.installed.KeycloakInstalled;
|
||||
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 KeycloakCliSsoMain extends KeycloakCliSso {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new KeycloakCliSsoMain().mainCmd(args);
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
<?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.2.1.Final</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.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.camel</groupId>
|
||||
<artifactId>camel-core</artifactId>
|
||||
<version>2.21.2</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>
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* 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 org.apache.camel.CamelContext;
|
||||
import org.apache.camel.component.undertow.UndertowComponent;
|
||||
import org.apache.camel.component.undertow.UndertowEndpoint;
|
||||
|
||||
/**
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getComponentName() {
|
||||
return "undertow-keycloak";
|
||||
}
|
||||
}
|
||||
-219
@@ -1,219 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1 +0,0 @@
|
||||
class=org.keycloak.adapters.camel.undertow.UndertowKeycloakComponent
|
||||
@@ -1,131 +0,0 @@
|
||||
<?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.2.1.Final</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.ops4j.pax.web.*;version="[7.1,9)",
|
||||
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
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
-280
@@ -1,280 +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.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 setHttpContext(HttpContext httpContext) {
|
||||
this.httpContext = 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);
|
||||
if (httpContext == null) {
|
||||
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
@@ -1 +0,0 @@
|
||||
org.keycloak.adapters.osgi.jetty94.KeycloakAuthenticatorService
|
||||
@@ -1,45 +0,0 @@
|
||||
<?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.2.1.Final</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>
|
||||
@@ -1,116 +0,0 @@
|
||||
<?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.2.1.Final</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.ops4j.pax.web.*;version="[7.1,9)",
|
||||
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
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1 +0,0 @@
|
||||
org.keycloak.adapters.osgi.tomcat.KeycloakAuthenticatorService
|
||||
@@ -1,153 +0,0 @@
|
||||
<?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.2.1.Final</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="[7.1,9)",
|
||||
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-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
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* 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
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
-142
@@ -1,142 +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.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 setHttpContext(HttpContext httpContext) {
|
||||
this.httpContext = 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);
|
||||
if (httpContext == null) {
|
||||
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
@@ -1 +0,0 @@
|
||||
org.keycloak.adapters.osgi.undertow.KeycloakAuthenticatorService
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -63,15 +63,6 @@
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.spec.javax.ws.rs</groupId>
|
||||
<artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
-702
@@ -1,702 +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.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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("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(" 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() 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);
|
||||
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()) return;
|
||||
AdapterConfig config = getConfig();
|
||||
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
|
||||
installed.login();
|
||||
processResponse(installed);
|
||||
}
|
||||
|
||||
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) 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();
|
||||
System.out.println(tokenResponse.getToken());
|
||||
}
|
||||
|
||||
public void loginManual() throws Exception {
|
||||
if (checkToken()) return;
|
||||
AdapterConfig config = getConfig();
|
||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
|
||||
KeycloakInstalled installed = new KeycloakInstalled(deployment);
|
||||
installed.loginManual();
|
||||
processResponse(installed);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
-305
@@ -17,8 +17,7 @@
|
||||
|
||||
package org.keycloak.adapters.installed;
|
||||
|
||||
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
|
||||
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
|
||||
import org.apache.commons.codec.Charsets;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
@@ -33,13 +32,15 @@ import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.IDToken;
|
||||
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.Form;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.Reader;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
@@ -47,8 +48,6 @@ import java.net.URISyntaxException;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
@@ -57,7 +56,6 @@ public class KeycloakInstalled {
|
||||
|
||||
public interface HttpResponseWriter {
|
||||
void success(PrintWriter pw, KeycloakInstalled ki);
|
||||
|
||||
void failure(PrintWriter pw, KeycloakInstalled ki);
|
||||
}
|
||||
|
||||
@@ -79,10 +77,7 @@ public class KeycloakInstalled {
|
||||
private Locale locale;
|
||||
private HttpResponseWriter loginResponseWriter;
|
||||
private HttpResponseWriter logoutResponseWriter;
|
||||
private ResteasyClient resteasyClient;
|
||||
Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
|
||||
Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
|
||||
Pattern codePattern = Pattern.compile("code=([^&]+)");
|
||||
|
||||
|
||||
|
||||
public KeycloakInstalled() {
|
||||
@@ -98,12 +93,70 @@ public class KeycloakInstalled {
|
||||
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() {
|
||||
return null;
|
||||
if (loginResponseWriter == null) {
|
||||
return defaultLoginWriter;
|
||||
} else {
|
||||
return loginResponseWriter;
|
||||
}
|
||||
}
|
||||
|
||||
public HttpResponseWriter getLogoutResponseWriter() {
|
||||
return null;
|
||||
if (logoutResponseWriter == null) {
|
||||
return defaultLogoutWriter;
|
||||
} else {
|
||||
return logoutResponseWriter;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) {
|
||||
@@ -114,10 +167,6 @@ public class KeycloakInstalled {
|
||||
this.logoutResponseWriter = logoutResponseWriter;
|
||||
}
|
||||
|
||||
public void setResteasyClient(ResteasyClient resteasyClient) {
|
||||
this.resteasyClient = resteasyClient;
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
@@ -241,272 +290,6 @@ public class KeycloakInstalled {
|
||||
status = Status.LOGGED_MANUAL;
|
||||
}
|
||||
|
||||
public static class Console {
|
||||
protected java.io.Console console = System.console();
|
||||
protected PrintWriter writer;
|
||||
protected BufferedReader reader;
|
||||
|
||||
static Console SINGLETON = new Console();
|
||||
|
||||
private Console() {
|
||||
}
|
||||
|
||||
|
||||
public PrintWriter writer() {
|
||||
if (console == null) {
|
||||
if (writer == null) {
|
||||
writer = new PrintWriter(System.err, true);
|
||||
}
|
||||
return writer;
|
||||
}
|
||||
return console.writer();
|
||||
}
|
||||
|
||||
public Reader reader() {
|
||||
if (console == null) {
|
||||
return getReader();
|
||||
}
|
||||
return console.reader();
|
||||
}
|
||||
|
||||
protected BufferedReader getReader() {
|
||||
if (reader != null) return reader;
|
||||
reader = new BufferedReader(new BufferedReader(new InputStreamReader(System.in)));
|
||||
return reader;
|
||||
}
|
||||
|
||||
public Console format(String fmt, Object... args) {
|
||||
if (console == null) {
|
||||
writer().format(fmt, args);
|
||||
return this;
|
||||
}
|
||||
console.format(fmt, args);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Console printf(String format, Object... args) {
|
||||
if (console == null) {
|
||||
writer().printf(format, args);
|
||||
return this;
|
||||
}
|
||||
console.printf(format, args);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String readLine(String fmt, Object... args) {
|
||||
if (console == null) {
|
||||
format(fmt, args);
|
||||
return readLine();
|
||||
}
|
||||
return console.readLine(fmt, args);
|
||||
}
|
||||
|
||||
public boolean confirm(String fmt, Object... args) {
|
||||
String prompt = "";
|
||||
while (!"y".equals(prompt) && !"n".equals(prompt)) {
|
||||
prompt = readLine(fmt, args);
|
||||
}
|
||||
return "y".equals(prompt);
|
||||
|
||||
}
|
||||
|
||||
public String prompt(String fmt, Object... args) {
|
||||
String prompt = "";
|
||||
while (prompt.equals("")) {
|
||||
prompt = readLine(fmt, args).trim();
|
||||
}
|
||||
return prompt;
|
||||
|
||||
}
|
||||
|
||||
public String passwordPrompt(String fmt, Object... args) {
|
||||
String prompt = "";
|
||||
while (prompt.equals("")) {
|
||||
char[] val = readPassword(fmt, args);
|
||||
prompt = new String(val);
|
||||
prompt = prompt.trim();
|
||||
}
|
||||
return prompt;
|
||||
|
||||
}
|
||||
|
||||
public String readLine() {
|
||||
if (console == null) {
|
||||
try {
|
||||
return getReader().readLine();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return console.readLine();
|
||||
}
|
||||
|
||||
public char[] readPassword(String fmt, Object... args) {
|
||||
if (console == null) {
|
||||
return readLine(fmt, args).toCharArray();
|
||||
|
||||
}
|
||||
return console.readPassword(fmt, args);
|
||||
}
|
||||
|
||||
public char[] readPassword() {
|
||||
if (console == null) {
|
||||
return readLine().toCharArray();
|
||||
}
|
||||
return console.readPassword();
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
if (console == null) {
|
||||
System.err.flush();
|
||||
return;
|
||||
}
|
||||
console.flush();
|
||||
}
|
||||
|
||||
public void stderrOutput() {
|
||||
//System.err.println("not using System.console()");
|
||||
console = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Console console() {
|
||||
return Console.SINGLETON;
|
||||
}
|
||||
|
||||
public boolean loginCommandLine() throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
|
||||
|
||||
return loginCommandLine(redirectUri);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Experimental proprietary WWW-Authentication challenge protocol.
|
||||
* WWW-Authentication: X-Text-Form-Challenge callback="{url}" param="{param-name}" label="{param-display-label}"
|
||||
*
|
||||
* @param redirectUri
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @throws ServerRequest.HttpFailure
|
||||
* @throws VerificationException
|
||||
*/
|
||||
public boolean loginCommandLine(String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||
String authUrl = deployment.getAuthUrl().clone()
|
||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
||||
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
|
||||
.queryParam("display", "console")
|
||||
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
|
||||
.build().toString();
|
||||
ResteasyClient client = createResteasyClient();
|
||||
try {
|
||||
//System.err.println("initial request");
|
||||
Response response = client.target(authUrl).request().get();
|
||||
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);
|
||||
if (authenticationHeader == null) {
|
||||
System.err.println("Failure: Invalid protocol. No WWW-Authenticate header");
|
||||
return false;
|
||||
}
|
||||
//System.err.println("got header: " + authenticationHeader);
|
||||
if (!authenticationHeader.contains("X-Text-Form-Challenge")) {
|
||||
System.err.println("Failure: Invalid WWW-Authenticate header.");
|
||||
return false;
|
||||
}
|
||||
if (response.getMediaType() != null) {
|
||||
String splash = response.readEntity(String.class);
|
||||
console().writer().println(splash);
|
||||
} else {
|
||||
response.close();
|
||||
}
|
||||
Matcher m = callbackPattern.matcher(authenticationHeader);
|
||||
if (!m.find()) {
|
||||
System.err.println("Failure: Invalid WWW-Authenticate header.");
|
||||
return false;
|
||||
}
|
||||
String callback = m.group(1);
|
||||
//System.err.println("callback: " + callback);
|
||||
m = paramPattern.matcher(authenticationHeader);
|
||||
Form form = new Form();
|
||||
while (m.find()) {
|
||||
String param = m.group(1);
|
||||
String label = m.group(2);
|
||||
String mask = m.group(3).trim();
|
||||
boolean maskInput = mask.equals("true");
|
||||
String value = null;
|
||||
if (maskInput) {
|
||||
char[] txt = console().readPassword(label);
|
||||
value = new String(txt);
|
||||
} else {
|
||||
value = console().readLine(label);
|
||||
}
|
||||
form.param(param, value);
|
||||
}
|
||||
response.close();
|
||||
client.close();
|
||||
client = createResteasyClient();
|
||||
response = client.target(callback).request().post(Entity.form(form));
|
||||
} else if (response.getStatus() == 302) {
|
||||
int redirectCount = 0;
|
||||
do {
|
||||
String location = response.getLocation().toString();
|
||||
Matcher m = codePattern.matcher(location);
|
||||
if (!m.find()) {
|
||||
response.close();
|
||||
client.close();
|
||||
client = createResteasyClient();
|
||||
response = client.target(location).request().get();
|
||||
} else {
|
||||
response.close();
|
||||
client.close();
|
||||
String code = m.group(1);
|
||||
processCode(code, redirectUri);
|
||||
return true;
|
||||
}
|
||||
if (response.getStatus() == 302 && redirectCount++ > 4) {
|
||||
System.err.println("Too many redirects. Aborting");
|
||||
return false;
|
||||
}
|
||||
} while (response.getStatus() == 302);
|
||||
} else {
|
||||
System.err.println("Unknown response from server: " + response.getStatus());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw ex;
|
||||
} finally {
|
||||
client.close();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected ResteasyClient getResteasyClient() {
|
||||
if (this.resteasyClient == null) {
|
||||
this.resteasyClient = createResteasyClient();
|
||||
}
|
||||
return this.resteasyClient;
|
||||
}
|
||||
|
||||
protected ResteasyClient createResteasyClient() {
|
||||
return new ResteasyClientBuilder()
|
||||
.connectionCheckoutTimeout(1, TimeUnit.HOURS)
|
||||
.connectionTTL(1, TimeUnit.HOURS)
|
||||
.socketTimeout(1, TimeUnit.HOURS)
|
||||
.disableTrustManager().build();
|
||||
}
|
||||
|
||||
|
||||
public String getTokenString() throws VerificationException, IOException, ServerRequest.HttpFailure {
|
||||
return tokenString;
|
||||
}
|
||||
@@ -525,7 +308,7 @@ public class KeycloakInstalled {
|
||||
parseAccessToken(tokenResponse);
|
||||
}
|
||||
|
||||
public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||
public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
|
||||
parseAccessToken(tokenResponse);
|
||||
|
||||
@@ -577,6 +360,7 @@ public class KeycloakInstalled {
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
|
||||
parseAccessToken(tokenResponse);
|
||||
@@ -651,26 +435,11 @@ public class KeycloakInstalled {
|
||||
|
||||
OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream());
|
||||
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 (writer != null) {
|
||||
writer.success(pw, KeycloakInstalled.this);
|
||||
} else {
|
||||
pw.println("HTTP/1.1 302 Found");
|
||||
pw.println("Location: " + deployment.getTokenUrl().replace("/token", "/delegated"));
|
||||
|
||||
}
|
||||
writer.success(pw, KeycloakInstalled.this);
|
||||
} else {
|
||||
if (writer != null) {
|
||||
writer.failure(pw, KeycloakInstalled.this);
|
||||
} else {
|
||||
pw.println("HTTP/1.1 302 Found");
|
||||
pw.println("Location: " + deployment.getTokenUrl().replace("/token", "/delegated?error=true"));
|
||||
|
||||
}
|
||||
writer.failure(pw, KeycloakInstalled.this);
|
||||
}
|
||||
pw.flush();
|
||||
socket.close();
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
-18
@@ -27,9 +27,6 @@ import javax.security.cert.X509Certificate;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.SecurityContext;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
@@ -54,8 +51,6 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
|
||||
|
||||
protected class RequestFacade implements OIDCHttpFacade.Request {
|
||||
|
||||
private InputStream inputStream;
|
||||
|
||||
@Override
|
||||
public String getFirstParam(String param) {
|
||||
throw new RuntimeException("NOT IMPLEMENTED");
|
||||
@@ -113,19 +108,6 @@ public class JaxrsHttpFacade implements OIDCHttpFacade {
|
||||
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<name>Keycloak Jetty Integration</name>
|
||||
@@ -33,7 +33,6 @@
|
||||
<modules>
|
||||
<module>jetty-core</module>
|
||||
<module>jetty8.1</module>
|
||||
<module>jetty9.1</module>
|
||||
<module>jetty9.2</module>
|
||||
<module>jetty9.3</module>
|
||||
</modules>
|
||||
@@ -47,6 +46,7 @@
|
||||
</property>
|
||||
</activation>
|
||||
<modules>
|
||||
<module>jetty9.1</module>
|
||||
<module>jetty9.4</module>
|
||||
</modules>
|
||||
</profile>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
+10
-73
@@ -18,7 +18,7 @@
|
||||
* 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.
|
||||
*/
|
||||
import * as Keycloak from './keycloak';
|
||||
import * as Keycloak from 'keycloak';
|
||||
|
||||
export as namespace KeycloakAuthorization;
|
||||
|
||||
@@ -35,64 +35,6 @@ declare namespace KeycloakAuthorization {
|
||||
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 {
|
||||
rpt: any;
|
||||
config: { rpt_endpoint: string };
|
||||
@@ -100,23 +42,18 @@ declare namespace KeycloakAuthorization {
|
||||
init(): void;
|
||||
|
||||
/**
|
||||
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
||||
* policy enforcer using UMA protocol.
|
||||
*
|
||||
* The authorization request must be provided with a ticket.
|
||||
*
|
||||
* @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(authorizationRequest: AuthorizationRequest): KeycloakAuthorizationPromise;
|
||||
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
||||
* policy enforcer.
|
||||
*
|
||||
* In this case, the resource server will respond with a 401 status code and a WWW-Authenticate header holding the
|
||||
* 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.
|
||||
*/
|
||||
authorize(wwwAuthenticateHeader: string): KeycloakAuthorizationPromise;
|
||||
|
||||
/**
|
||||
* 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, authorizationRequest?: AuthorizationRequest): KeycloakAuthorizationPromise;
|
||||
entitlement(resourceServerId: string, entitlementRequest: {}): KeycloakAuthorizationPromise;
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,14 @@
|
||||
|
||||
(function( window, undefined ) {
|
||||
|
||||
var KeycloakAuthorization = function (keycloak, options) {
|
||||
var KeycloakAuthorization = function (keycloak) {
|
||||
var _instance = this;
|
||||
this.rpt = null;
|
||||
|
||||
this.init = function () {
|
||||
var request = new XMLHttpRequest();
|
||||
|
||||
request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/.well-known/uma2-configuration');
|
||||
request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/.well-known/uma-configuration');
|
||||
request.onreadystatechange = function () {
|
||||
if (request.readyState == 4) {
|
||||
if (request.status == 200) {
|
||||
@@ -41,65 +41,74 @@
|
||||
|
||||
/**
|
||||
* This method enables client applications to better integrate with resource servers protected by a Keycloak
|
||||
* policy enforcer using UMA protocol.
|
||||
* policy enforcer.
|
||||
*
|
||||
* The authorization request must be provided with a ticket.
|
||||
* In this case, the resource server will respond with a 401 status code and a WWW-Authenticate header holding the
|
||||
* 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 (wwwAuthenticateHeader) {
|
||||
this.then = function (onGrant, onDeny, onError) {
|
||||
if (authorizationRequest && authorizationRequest.ticket) {
|
||||
var request = new XMLHttpRequest();
|
||||
if (wwwAuthenticateHeader.indexOf('UMA') != -1) {
|
||||
var params = wwwAuthenticateHeader.split(',');
|
||||
|
||||
request.open('POST', _instance.config.token_endpoint, true);
|
||||
request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
|
||||
for (i = 0; i < params.length; i++) {
|
||||
var param = params[i].split('=');
|
||||
|
||||
request.onreadystatechange = function () {
|
||||
if (request.readyState == 4) {
|
||||
var status = request.status;
|
||||
if (param[0] == 'ticket') {
|
||||
var request = new XMLHttpRequest();
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
var rpt = JSON.parse(request.responseText).access_token;
|
||||
_instance.rpt = rpt;
|
||||
onGrant(rpt);
|
||||
} else if (status == 403) {
|
||||
if (onDeny) {
|
||||
onDeny();
|
||||
} else {
|
||||
console.error('Authorization request was denied by the server.');
|
||||
request.open('POST', _instance.config.rpt_endpoint, true);
|
||||
request.setRequestHeader('Content-Type', 'application/json')
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token)
|
||||
|
||||
request.onreadystatechange = function () {
|
||||
if (request.readyState == 4) {
|
||||
var status = request.status;
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
var rpt = JSON.parse(request.responseText).rpt;
|
||||
_instance.rpt = rpt;
|
||||
onGrant(rpt);
|
||||
} else if (status == 403) {
|
||||
if (onDeny) {
|
||||
onDeny();
|
||||
} else {
|
||||
console.error('Authorization request was denied by the server.');
|
||||
}
|
||||
} else {
|
||||
if (onError) {
|
||||
onError();
|
||||
} else {
|
||||
console.error('Could not obtain authorization data from server.');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (onError) {
|
||||
onError();
|
||||
} else {
|
||||
console.error('Could not obtain authorization data from server.');
|
||||
};
|
||||
|
||||
var ticket = param[1].substring(1, param[1].length - 1).trim();
|
||||
|
||||
request.send(JSON.stringify(
|
||||
{
|
||||
ticket: ticket,
|
||||
rpt: _instance.rpt
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
var params = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=" + keycloak.clientId + "&ticket=" + authorizationRequest.ticket;
|
||||
|
||||
if (authorizationRequest.submitRequest != undefined) {
|
||||
params += "&submit_request=" + authorizationRequest.submitRequest;
|
||||
}
|
||||
} else if (wwwAuthenticateHeader.indexOf('KC_ETT') != -1) {
|
||||
var params = wwwAuthenticateHeader.substring('KC_ETT'.length).trim().split(',');
|
||||
var clientId = null;
|
||||
|
||||
var metadata = authorizationRequest.metadata;
|
||||
for (i = 0; i < params.length; i++) {
|
||||
var param = params[i].split('=');
|
||||
|
||||
if (metadata) {
|
||||
if (metadata.responseIncludeResourceName) {
|
||||
params += "&response_include_resource_name=" + metadata.responseIncludeResourceName;
|
||||
}
|
||||
if (metadata.responsePermissionsLimit) {
|
||||
params += "&response_permissions_limit=" + metadata.responsePermissionsLimit;
|
||||
if (param[0] == 'realm') {
|
||||
clientId = param[1].substring(1, param[1].length - 1).trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (_instance.rpt && (authorizationRequest.incrementalAuthorization == undefined || authorizationRequest.incrementalAuthorization)) {
|
||||
params += "&rpt=" + _instance.rpt;
|
||||
}
|
||||
|
||||
request.send(params);
|
||||
_instance.entitlement(clientId).then(onGrant, onDeny, onError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,22 +116,20 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtains all entitlements from a Keycloak Server based on a given resourceServerId.
|
||||
* Obtains all entitlements from a Keycloak Server based on a give resourceServerId.
|
||||
*/
|
||||
this.entitlement = function (resourceServerId, authorizationRequest) {
|
||||
this.entitlement = function (resourceSeververId, entitlementRequest ) {
|
||||
this.then = function (onGrant, onDeny, onError) {
|
||||
var request = new XMLHttpRequest();
|
||||
|
||||
request.open('POST', _instance.config.token_endpoint, true);
|
||||
request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token);
|
||||
|
||||
|
||||
request.onreadystatechange = function () {
|
||||
if (request.readyState == 4) {
|
||||
var status = request.status;
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
var rpt = JSON.parse(request.responseText).access_token;
|
||||
var rpt = JSON.parse(request.responseText).rpt;
|
||||
_instance.rpt = rpt;
|
||||
onGrant(rpt);
|
||||
} else if (status == 403) {
|
||||
@@ -141,70 +148,25 @@
|
||||
}
|
||||
};
|
||||
|
||||
if (!authorizationRequest) {
|
||||
authorizationRequest = {};
|
||||
var erJson = null
|
||||
|
||||
if(entitlementRequest) {
|
||||
request.open('POST', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/authz/entitlement/' + resourceSeververId, true);
|
||||
request.setRequestHeader("Content-type", "application/json");
|
||||
erJson = JSON.stringify(entitlementRequest)
|
||||
} else {
|
||||
request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/authz/entitlement/' + resourceSeververId, true);
|
||||
}
|
||||
|
||||
var params = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=" + keycloak.clientId;
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token)
|
||||
request.send(erJson);
|
||||
|
||||
if (authorizationRequest.claimToken) {
|
||||
params += "&claim_token=" + authorizationRequest.claimToken;
|
||||
|
||||
if (authorizationRequest.claimTokenFormat) {
|
||||
params += "&claim_token_format=" + authorizationRequest.claimTokenFormat;
|
||||
}
|
||||
}
|
||||
|
||||
params += "&audience=" + resourceServerId;
|
||||
|
||||
var permissions = authorizationRequest.permissions;
|
||||
|
||||
if (!permissions) {
|
||||
permissions = [];
|
||||
}
|
||||
|
||||
for (i = 0; i < permissions.length; i++) {
|
||||
var resource = permissions[i];
|
||||
var permission = resource.id;
|
||||
|
||||
if (resource.scopes && resource.scopes.length > 0) {
|
||||
permission += "#";
|
||||
for (j = 0; j < resource.scopes.length; j++) {
|
||||
var scope = resource.scopes[j];
|
||||
if (permission.indexOf('#') != permission.length - 1) {
|
||||
permission += ",";
|
||||
}
|
||||
permission += scope;
|
||||
}
|
||||
}
|
||||
|
||||
params += "&permission=" + permission;
|
||||
}
|
||||
|
||||
var metadata = authorizationRequest.metadata;
|
||||
|
||||
if (metadata) {
|
||||
if (metadata.responseIncludeResourceName) {
|
||||
params += "&response_include_resource_name=" + metadata.responseIncludeResourceName;
|
||||
}
|
||||
if (metadata.responsePermissionsLimit) {
|
||||
params += "&response_permissions_limit=" + metadata.responsePermissionsLimit;
|
||||
}
|
||||
}
|
||||
|
||||
if (_instance.rpt) {
|
||||
params += "&rpt=" + _instance.rpt;
|
||||
}
|
||||
|
||||
request.send(params);
|
||||
};
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
this.init(this);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
|
||||
|
||||
+478
-509
@@ -1,509 +1,478 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright 2017 Brett Epps <https://github.com/eppsilon>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* 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.
|
||||
*/
|
||||
export as namespace Keycloak;
|
||||
|
||||
export = Keycloak;
|
||||
|
||||
/**
|
||||
* Creates a new Keycloak client instance.
|
||||
* @param config Path to a JSON config file or a plain config object.
|
||||
*/
|
||||
declare function Keycloak(config?: string|{}): Keycloak.KeycloakInstance;
|
||||
|
||||
declare namespace Keycloak {
|
||||
type KeycloakAdapterName = 'cordova' | 'cordova-native' |'default' | any;
|
||||
type KeycloakOnLoad = 'login-required'|'check-sso';
|
||||
type KeycloakResponseMode = 'query'|'fragment';
|
||||
type KeycloakResponseType = 'code'|'id_token token'|'code id_token token';
|
||||
type KeycloakFlow = 'standard'|'implicit'|'hybrid';
|
||||
|
||||
interface KeycloakInitOptions {
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
useNonce?: boolean;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Specifies an action to do on load.
|
||||
*/
|
||||
onLoad?: KeycloakOnLoad;
|
||||
|
||||
/**
|
||||
* Set an initial value for the token.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* Set an initial value for the refresh token.
|
||||
*/
|
||||
refreshToken?: string;
|
||||
|
||||
/**
|
||||
* Set an initial value for the id token (only together with `token` or
|
||||
* `refreshToken`).
|
||||
*/
|
||||
idToken?: string;
|
||||
|
||||
/**
|
||||
* Set an initial value for skew between local time and Keycloak server in
|
||||
* seconds (only together with `token` or `refreshToken`).
|
||||
*/
|
||||
timeSkew?: number;
|
||||
|
||||
/**
|
||||
* Set to enable/disable monitoring login state.
|
||||
* @default true
|
||||
*/
|
||||
checkLoginIframe?: boolean;
|
||||
|
||||
/**
|
||||
* Set the interval to check login state (in seconds).
|
||||
* @default 5
|
||||
*/
|
||||
checkLoginIframeInterval?: number;
|
||||
|
||||
/**
|
||||
* Set the OpenID Connect response mode to send to Keycloak upon login.
|
||||
* @default fragment After successful authentication Keycloak will redirect
|
||||
* to JavaScript application with OpenID Connect parameters
|
||||
* added in URL fragment. This is generally safer and
|
||||
* recommended over query.
|
||||
*/
|
||||
responseMode?: KeycloakResponseMode;
|
||||
|
||||
/**
|
||||
* Specifies a default uri to redirect to after login or logout.
|
||||
* This is currently supported for adapter 'cordova-native' and 'default'
|
||||
*/
|
||||
redirectUri?: string;
|
||||
|
||||
/**
|
||||
* Set the OpenID Connect flow.
|
||||
* @default standard
|
||||
*/
|
||||
flow?: KeycloakFlow;
|
||||
}
|
||||
|
||||
interface KeycloakLoginOptions {
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
scope?: string;
|
||||
|
||||
/**
|
||||
* Specifies the uri to redirect to after login.
|
||||
*/
|
||||
redirectUri?: string;
|
||||
|
||||
/**
|
||||
* By default the login screen is displayed if the user is not logged into
|
||||
* Keycloak. To only authenticate to the application if the user is already
|
||||
* logged in and not display the login page if the user is not logged in, set
|
||||
* this option to `'none'`. To always require re-authentication and ignore
|
||||
* SSO, set this option to `'login'`.
|
||||
*/
|
||||
prompt?: 'none'|'login';
|
||||
|
||||
/**
|
||||
* If value is `'register'` then user is redirected to registration page,
|
||||
* otherwise to login page.
|
||||
*/
|
||||
action?: 'register';
|
||||
|
||||
/**
|
||||
* Used just if user is already authenticated. Specifies maximum time since
|
||||
* the authentication of user happened. If user is already authenticated for
|
||||
* longer time than `'maxAge'`, the SSO is ignored and he will need to
|
||||
* authenticate again.
|
||||
*/
|
||||
maxAge?: number;
|
||||
|
||||
/**
|
||||
* Used to pre-fill the username/email field on the login form.
|
||||
*/
|
||||
loginHint?: string;
|
||||
|
||||
/**
|
||||
* Used to tell Keycloak which IDP the user wants to authenticate with.
|
||||
*/
|
||||
idpHint?: string;
|
||||
|
||||
/**
|
||||
* Sets the 'ui_locales' query param in compliance with section 3.1.2.1
|
||||
* of the OIDC 1.0 specification.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Specifies arguments that are passed to the Cordova in-app-browser (if applicable).
|
||||
* Options 'hidden' and 'location' are not affected by these arguments.
|
||||
* All available options are defined at https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-inappbrowser/.
|
||||
* Example of use: { zoom: "no", hardwareback: "yes" }
|
||||
*/
|
||||
cordovaOptions?: { [optionName: string]: string };
|
||||
}
|
||||
|
||||
type KeycloakPromiseCallback<T> = (result: T) => void;
|
||||
|
||||
interface KeycloakPromise<TSuccess, TError> {
|
||||
/**
|
||||
* Function to call if the promised action succeeds.
|
||||
*/
|
||||
success(callback: KeycloakPromiseCallback<TSuccess>): KeycloakPromise<TSuccess, TError>;
|
||||
|
||||
/**
|
||||
* Function to call if the promised action throws an error.
|
||||
*/
|
||||
error(callback: KeycloakPromiseCallback<TError>): KeycloakPromise<TSuccess, TError>;
|
||||
}
|
||||
|
||||
interface KeycloakError {
|
||||
error: string;
|
||||
error_description: string;
|
||||
}
|
||||
|
||||
interface KeycloakAdapter {
|
||||
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||
logout(options?: any): KeycloakPromise<void, void>;
|
||||
register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||
accountManagement(): KeycloakPromise<void, void>;
|
||||
redirectUri(options: { redirectUri: string; }, encodeHash: boolean): string;
|
||||
}
|
||||
|
||||
interface KeycloakProfile {
|
||||
id?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
enabled?: boolean;
|
||||
emailVerified?: boolean;
|
||||
totp?: boolean;
|
||||
createdTimestamp?: number;
|
||||
}
|
||||
|
||||
// export interface KeycloakUserInfo {}
|
||||
|
||||
/**
|
||||
* A client for the Keycloak authentication server.
|
||||
* @see {@link https://keycloak.gitbooks.io/securing-client-applications-guide/content/topics/oidc/javascript-adapter.html|Keycloak JS adapter documentation}
|
||||
*/
|
||||
interface KeycloakInstance {
|
||||
/**
|
||||
* Is true if the user is authenticated, false otherwise.
|
||||
*/
|
||||
authenticated?: boolean;
|
||||
|
||||
/**
|
||||
* The user id.
|
||||
*/
|
||||
subject?: string;
|
||||
|
||||
/**
|
||||
* Response mode passed in init (default value is `'fragment'`).
|
||||
*/
|
||||
responseMode?: KeycloakResponseMode;
|
||||
|
||||
/**
|
||||
* Response type sent to Keycloak with login requests. This is determined
|
||||
* based on the flow value used during initialization, but can be overridden
|
||||
* by setting this value.
|
||||
*/
|
||||
responseType?: KeycloakResponseType;
|
||||
|
||||
/**
|
||||
* Flow passed in init.
|
||||
*/
|
||||
flow?: KeycloakFlow;
|
||||
|
||||
/**
|
||||
* The realm roles associated with the token.
|
||||
*/
|
||||
realmAccess?: { roles: string[] };
|
||||
|
||||
/**
|
||||
* The resource roles associated with the token.
|
||||
*/
|
||||
resourceAccess?: string[];
|
||||
|
||||
/**
|
||||
* The base64 encoded token that can be sent in the Authorization header in
|
||||
* requests to services.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The parsed token as a JavaScript object.
|
||||
*/
|
||||
tokenParsed?: {
|
||||
exp?: number;
|
||||
iat?: number;
|
||||
nonce?: string;
|
||||
sub?: string;
|
||||
session_state?: string;
|
||||
realm_access?: { roles: string[] };
|
||||
resource_access?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The base64 encoded refresh token that can be used to retrieve a new token.
|
||||
*/
|
||||
refreshToken?: string;
|
||||
|
||||
/**
|
||||
* The parsed refresh token as a JavaScript object.
|
||||
*/
|
||||
refreshTokenParsed?: { nonce?: string };
|
||||
|
||||
/**
|
||||
* The base64 encoded ID token.
|
||||
*/
|
||||
idToken?: string;
|
||||
|
||||
/**
|
||||
* The parsed id token as a JavaScript object.
|
||||
*/
|
||||
idTokenParsed?: { nonce?: string };
|
||||
|
||||
/**
|
||||
* The estimated time difference between the browser time and the Keycloak
|
||||
* server in seconds. This value is just an estimation, but is accurate
|
||||
* enough when determining if a token is expired or not.
|
||||
*/
|
||||
timeSkew?: number;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
loginRequired?: boolean;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
authServerUrl?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
realm?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
clientId?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
clientSecret?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
redirectUri?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
sessionId?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
profile?: KeycloakProfile;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
userInfo?: {}; // KeycloakUserInfo;
|
||||
|
||||
/**
|
||||
* Called when the adapter is initialized.
|
||||
*/
|
||||
onReady?(authenticated?: boolean): void;
|
||||
|
||||
/**
|
||||
* Called when a user is successfully authenticated.
|
||||
*/
|
||||
onAuthSuccess?(): void;
|
||||
|
||||
/**
|
||||
* Called if there was an error during authentication.
|
||||
*/
|
||||
onAuthError?(errorData: KeycloakError): void;
|
||||
|
||||
/**
|
||||
* Called when the token is refreshed.
|
||||
*/
|
||||
onAuthRefreshSuccess?(): void;
|
||||
|
||||
/**
|
||||
* Called if there was an error while trying to refresh the token.
|
||||
*/
|
||||
onAuthRefreshError?(): void;
|
||||
|
||||
/**
|
||||
* Called if the user is logged out (will only be called if the session
|
||||
* status iframe is enabled, or in Cordova mode).
|
||||
*/
|
||||
onAuthLogout?(): void;
|
||||
|
||||
/**
|
||||
* Called when the access token is expired. If a refresh token is available
|
||||
* the token can be refreshed with Keycloak#updateToken, or in cases where
|
||||
* it's not (ie. with implicit flow) you can redirect to login screen to
|
||||
* obtain a new access token.
|
||||
*/
|
||||
onTokenExpired?(): void;
|
||||
|
||||
/**
|
||||
* Called to initialize the adapter.
|
||||
* @param initOptions Initialization options.
|
||||
* @returns A promise to set functions to be invoked on success or error.
|
||||
*/
|
||||
init(initOptions: KeycloakInitOptions): KeycloakPromise<boolean, KeycloakError>;
|
||||
|
||||
/**
|
||||
* Redirects to login form.
|
||||
* @param options Login options.
|
||||
*/
|
||||
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||
|
||||
/**
|
||||
* Redirects to logout.
|
||||
* @param options Logout options.
|
||||
* @param options.redirectUri Specifies the uri to redirect to after logout.
|
||||
*/
|
||||
logout(options?: any): KeycloakPromise<void, void>;
|
||||
|
||||
/**
|
||||
* Redirects to registration form.
|
||||
* @param options Supports same options as Keycloak#login but `action` is
|
||||
* set to `'register'`.
|
||||
*/
|
||||
register(options?: any): KeycloakPromise<void, void>;
|
||||
|
||||
/**
|
||||
* Redirects to the Account Management Console.
|
||||
*/
|
||||
accountManagement(): KeycloakPromise<void, void>;
|
||||
|
||||
/**
|
||||
* Returns the URL to login form.
|
||||
* @param options Supports same options as Keycloak#login.
|
||||
*/
|
||||
createLoginUrl(options?: KeycloakLoginOptions): string;
|
||||
|
||||
/**
|
||||
* Returns the URL to logout the user.
|
||||
* @param options Logout options.
|
||||
* @param options.redirectUri Specifies the uri to redirect to after logout.
|
||||
*/
|
||||
createLogoutUrl(options?: any): string;
|
||||
|
||||
/**
|
||||
* Returns the URL to registration page.
|
||||
* @param options Supports same options as Keycloak#createLoginUrl but
|
||||
* `action` is set to `'register'`.
|
||||
*/
|
||||
createRegisterUrl(options?: KeycloakLoginOptions): string;
|
||||
|
||||
/**
|
||||
* Returns the URL to the Account Management Console.
|
||||
*/
|
||||
createAccountUrl(): string;
|
||||
|
||||
/**
|
||||
* Returns true if the token has less than `minValidity` seconds left before
|
||||
* it expires.
|
||||
* @param minValidity If not specified, `0` is used.
|
||||
*/
|
||||
isTokenExpired(minValidity?: number): boolean;
|
||||
|
||||
/**
|
||||
* If the token expires within `minValidity` seconds, the token is refreshed.
|
||||
* If the session status iframe is enabled, the session status is also
|
||||
* checked.
|
||||
* @returns A promise to set functions that can be invoked if the token is
|
||||
* still valid, or if the token is no longer valid.
|
||||
* @example
|
||||
* ```js
|
||||
* keycloak.updateToken(5).success(function(refreshed) {
|
||||
* if (refreshed) {
|
||||
* alert('Token was successfully refreshed');
|
||||
* } else {
|
||||
* alert('Token is still valid');
|
||||
* }
|
||||
* }).error(function() {
|
||||
* alert('Failed to refresh the token, or the session has expired');
|
||||
* });
|
||||
*/
|
||||
updateToken(minValidity: number): KeycloakPromise<boolean, boolean>;
|
||||
|
||||
/**
|
||||
* Clears authentication state, including tokens. This can be useful if
|
||||
* the application has detected the session was expired, for example if
|
||||
* updating token fails. Invoking this results in Keycloak#onAuthLogout
|
||||
* callback listener being invoked.
|
||||
*/
|
||||
clearToken(): void;
|
||||
|
||||
/**
|
||||
* Returns true if the token has the given realm role.
|
||||
* @param role A realm role name.
|
||||
*/
|
||||
hasRealmRole(role: string): boolean;
|
||||
|
||||
/**
|
||||
* Returns true if the token has the given role for the resource.
|
||||
* @param role A role name.
|
||||
* @param resource If not specified, `clientId` is used.
|
||||
*/
|
||||
hasResourceRole(role: string, resource?: string): boolean;
|
||||
|
||||
/**
|
||||
* Loads the user's profile.
|
||||
* @returns A promise to set functions to be invoked on success or error.
|
||||
*/
|
||||
loadUserProfile(): KeycloakPromise<KeycloakProfile, void>;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
loadUserInfo(): KeycloakPromise<{}, void>;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright 2017 Brett Epps <https://github.com/eppsilon>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* 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.
|
||||
*/
|
||||
export as namespace Keycloak;
|
||||
|
||||
export = Keycloak;
|
||||
|
||||
/**
|
||||
* Creates a new Keycloak client instance.
|
||||
* @param config Path to a JSON config file or a plain config object.
|
||||
*/
|
||||
declare function Keycloak(config?: string|{}): Keycloak.KeycloakInstance;
|
||||
|
||||
declare namespace Keycloak {
|
||||
type KeycloakAdapterName = 'cordova'|'default';
|
||||
type KeycloakOnLoad = 'login-required'|'check-sso';
|
||||
type KeycloakResponseMode = 'query'|'fragment';
|
||||
type KeycloakResponseType = 'code'|'id_token token'|'code id_token token';
|
||||
type KeycloakFlow = 'standard'|'implicit'|'hybrid';
|
||||
|
||||
interface KeycloakInitOptions {
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
adapter?: KeycloakAdapterName;
|
||||
|
||||
/**
|
||||
* Specifies an action to do on load.
|
||||
*/
|
||||
onLoad?: KeycloakOnLoad;
|
||||
|
||||
/**
|
||||
* Set an initial value for the token.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* Set an initial value for the refresh token.
|
||||
*/
|
||||
refreshToken?: string;
|
||||
|
||||
/**
|
||||
* Set an initial value for the id token (only together with `token` or
|
||||
* `refreshToken`).
|
||||
*/
|
||||
idToken?: string;
|
||||
|
||||
/**
|
||||
* Set an initial value for skew between local time and Keycloak server in
|
||||
* seconds (only together with `token` or `refreshToken`).
|
||||
*/
|
||||
timeSkew?: number;
|
||||
|
||||
/**
|
||||
* Set to enable/disable monitoring login state.
|
||||
* @default true
|
||||
*/
|
||||
checkLoginIframe?: boolean;
|
||||
|
||||
/**
|
||||
* Set the interval to check login state (in seconds).
|
||||
* @default 5
|
||||
*/
|
||||
checkLoginIframeInterval?: boolean;
|
||||
|
||||
/**
|
||||
* Set the OpenID Connect response mode to send to Keycloak upon login.
|
||||
* @default fragment After successful authentication Keycloak will redirect
|
||||
* to JavaScript application with OpenID Connect parameters
|
||||
* added in URL fragment. This is generally safer and
|
||||
* recommended over query.
|
||||
*/
|
||||
responseMode?: KeycloakResponseMode;
|
||||
|
||||
/**
|
||||
* Set the OpenID Connect flow.
|
||||
* @default standard
|
||||
*/
|
||||
flow?: KeycloakFlow;
|
||||
}
|
||||
|
||||
interface KeycloakLoginOptions {
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
scope?: string;
|
||||
|
||||
/**
|
||||
* Specifies the uri to redirect to after login.
|
||||
*/
|
||||
redirectUri?: string;
|
||||
|
||||
/**
|
||||
* By default the login screen is displayed if the user is not logged into
|
||||
* Keycloak. To only authenticate to the application if the user is already
|
||||
* logged in and not display the login page if the user is not logged in, set
|
||||
* this option to `'none'`. To always require re-authentication and ignore
|
||||
* SSO, set this option to `'login'`.
|
||||
*/
|
||||
prompt?: 'none'|'login';
|
||||
|
||||
/**
|
||||
* If value is `'register'` then user is redirected to registration page,
|
||||
* otherwise to login page.
|
||||
*/
|
||||
action?: 'register';
|
||||
|
||||
/**
|
||||
* Used just if user is already authenticated. Specifies maximum time since
|
||||
* the authentication of user happened. If user is already authenticated for
|
||||
* longer time than `'maxAge'`, the SSO is ignored and he will need to
|
||||
* authenticate again.
|
||||
*/
|
||||
maxAge?: number;
|
||||
|
||||
/**
|
||||
* Used to pre-fill the username/email field on the login form.
|
||||
*/
|
||||
loginHint?: string;
|
||||
|
||||
/**
|
||||
* Used to tell Keycloak which IDP the user wants to authenticate with.
|
||||
*/
|
||||
idpHint?: string;
|
||||
|
||||
/**
|
||||
* Specifies the desired locale for the UI.
|
||||
*/
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
type KeycloakPromiseCallback<T> = (result: T) => void;
|
||||
|
||||
interface KeycloakPromise<TSuccess, TError> {
|
||||
/**
|
||||
* Function to call if the promised action succeeds.
|
||||
*/
|
||||
success(callback: KeycloakPromiseCallback<TSuccess>): KeycloakPromise<TSuccess, TError>;
|
||||
|
||||
/**
|
||||
* Function to call if the promised action throws an error.
|
||||
*/
|
||||
error(callback: KeycloakPromiseCallback<TError>): KeycloakPromise<TSuccess, TError>;
|
||||
}
|
||||
|
||||
interface KeycloakError {
|
||||
error: string;
|
||||
error_description: string;
|
||||
}
|
||||
|
||||
interface KeycloakAdapter {
|
||||
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||
logout(options?: any): KeycloakPromise<void, void>;
|
||||
register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||
accountManagement(): KeycloakPromise<void, void>;
|
||||
redirectUri(options: { redirectUri: string; }, encodeHash: boolean): string;
|
||||
}
|
||||
|
||||
interface KeycloakProfile {
|
||||
id?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
enabled?: boolean;
|
||||
emailVerified?: boolean;
|
||||
totp?: boolean;
|
||||
createdTimestamp?: number;
|
||||
}
|
||||
|
||||
// export interface KeycloakUserInfo {}
|
||||
|
||||
/**
|
||||
* A client for the Keycloak authentication server.
|
||||
* @see {@link https://keycloak.gitbooks.io/securing-client-applications-guide/content/topics/oidc/javascript-adapter.html|Keycloak JS adapter documentation}
|
||||
*/
|
||||
interface KeycloakInstance {
|
||||
/**
|
||||
* Is true if the user is authenticated, false otherwise.
|
||||
*/
|
||||
authenticated?: boolean;
|
||||
|
||||
/**
|
||||
* The user id.
|
||||
*/
|
||||
subject?: string;
|
||||
|
||||
/**
|
||||
* Response mode passed in init (default value is `'fragment'`).
|
||||
*/
|
||||
responseMode?: KeycloakResponseMode;
|
||||
|
||||
/**
|
||||
* Response type sent to Keycloak with login requests. This is determined
|
||||
* based on the flow value used during initialization, but can be overridden
|
||||
* by setting this value.
|
||||
*/
|
||||
responseType?: KeycloakResponseType;
|
||||
|
||||
/**
|
||||
* Flow passed in init.
|
||||
*/
|
||||
flow?: KeycloakFlow;
|
||||
|
||||
/**
|
||||
* The realm roles associated with the token.
|
||||
*/
|
||||
realmAccess?: { roles: string[] };
|
||||
|
||||
/**
|
||||
* The resource roles associated with the token.
|
||||
*/
|
||||
resourceAccess?: string[];
|
||||
|
||||
/**
|
||||
* The base64 encoded token that can be sent in the Authorization header in
|
||||
* requests to services.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The parsed token as a JavaScript object.
|
||||
*/
|
||||
tokenParsed?: {
|
||||
exp?: number;
|
||||
iat?: number;
|
||||
nonce?: string;
|
||||
sub?: string;
|
||||
session_state?: string;
|
||||
realm_access?: { roles: string[] };
|
||||
resource_access?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The base64 encoded refresh token that can be used to retrieve a new token.
|
||||
*/
|
||||
refreshToken?: string;
|
||||
|
||||
/**
|
||||
* The parsed refresh token as a JavaScript object.
|
||||
*/
|
||||
refreshTokenParsed?: { nonce?: string };
|
||||
|
||||
/**
|
||||
* The base64 encoded ID token.
|
||||
*/
|
||||
idToken?: string;
|
||||
|
||||
/**
|
||||
* The parsed id token as a JavaScript object.
|
||||
*/
|
||||
idTokenParsed?: { nonce?: string };
|
||||
|
||||
/**
|
||||
* The estimated time difference between the browser time and the Keycloak
|
||||
* server in seconds. This value is just an estimation, but is accurate
|
||||
* enough when determining if a token is expired or not.
|
||||
*/
|
||||
timeSkew?: number;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
loginRequired?: boolean;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
authServerUrl?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
realm?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
clientId?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
clientSecret?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
redirectUri?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
sessionId?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
profile?: KeycloakProfile;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
userInfo?: {}; // KeycloakUserInfo;
|
||||
|
||||
/**
|
||||
* Called when the adapter is initialized.
|
||||
*/
|
||||
onReady?(authenticated?: boolean): void;
|
||||
|
||||
/**
|
||||
* Called when a user is successfully authenticated.
|
||||
*/
|
||||
onAuthSuccess?(): void;
|
||||
|
||||
/**
|
||||
* Called if there was an error during authentication.
|
||||
*/
|
||||
onAuthError?(errorData: KeycloakError): void;
|
||||
|
||||
/**
|
||||
* Called when the token is refreshed.
|
||||
*/
|
||||
onAuthRefreshSuccess?(): void;
|
||||
|
||||
/**
|
||||
* Called if there was an error while trying to refresh the token.
|
||||
*/
|
||||
onAuthRefreshError?(): void;
|
||||
|
||||
/**
|
||||
* Called if the user is logged out (will only be called if the session
|
||||
* status iframe is enabled, or in Cordova mode).
|
||||
*/
|
||||
onAuthLogout?(): void;
|
||||
|
||||
/**
|
||||
* Called when the access token is expired. If a refresh token is available
|
||||
* the token can be refreshed with Keycloak#updateToken, or in cases where
|
||||
* it's not (ie. with implicit flow) you can redirect to login screen to
|
||||
* obtain a new access token.
|
||||
*/
|
||||
onTokenExpired?(): void;
|
||||
|
||||
/**
|
||||
* Called to initialize the adapter.
|
||||
* @param initOptions Initialization options.
|
||||
* @returns A promise to set functions to be invoked on success or error.
|
||||
*/
|
||||
init(initOptions: KeycloakInitOptions): KeycloakPromise<boolean, KeycloakError>;
|
||||
|
||||
/**
|
||||
* Redirects to login form.
|
||||
* @param options Login options.
|
||||
*/
|
||||
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
|
||||
|
||||
/**
|
||||
* Redirects to logout.
|
||||
* @param options Logout options.
|
||||
* @param options.redirectUri Specifies the uri to redirect to after logout.
|
||||
*/
|
||||
logout(options?: any): KeycloakPromise<void, void>;
|
||||
|
||||
/**
|
||||
* Redirects to registration form.
|
||||
* @param options Supports same options as Keycloak#login but `action` is
|
||||
* set to `'register'`.
|
||||
*/
|
||||
register(options?: any): KeycloakPromise<void, void>;
|
||||
|
||||
/**
|
||||
* Redirects to the Account Management Console.
|
||||
*/
|
||||
accountManagement(): KeycloakPromise<void, void>;
|
||||
|
||||
/**
|
||||
* Returns the URL to login form.
|
||||
* @param options Supports same options as Keycloak#login.
|
||||
*/
|
||||
createLoginUrl(options?: KeycloakLoginOptions): string;
|
||||
|
||||
/**
|
||||
* Returns the URL to logout the user.
|
||||
* @param options Logout options.
|
||||
* @param options.redirectUri Specifies the uri to redirect to after logout.
|
||||
*/
|
||||
createLogoutUrl(options?: any): string;
|
||||
|
||||
/**
|
||||
* Returns the URL to registration page.
|
||||
* @param options Supports same options as Keycloak#createLoginUrl but
|
||||
* `action` is set to `'register'`.
|
||||
*/
|
||||
createRegisterUrl(options?: KeycloakLoginOptions): string;
|
||||
|
||||
/**
|
||||
* Returns the URL to the Account Management Console.
|
||||
*/
|
||||
createAccountUrl(): string;
|
||||
|
||||
/**
|
||||
* Returns true if the token has less than `minValidity` seconds left before
|
||||
* it expires.
|
||||
* @param minValidity If not specified, `0` is used.
|
||||
*/
|
||||
isTokenExpired(minValidity?: number): boolean;
|
||||
|
||||
/**
|
||||
* If the token expires within `minValidity` seconds, the token is refreshed.
|
||||
* If the session status iframe is enabled, the session status is also
|
||||
* checked.
|
||||
* @returns A promise to set functions that can be invoked if the token is
|
||||
* still valid, or if the token is no longer valid.
|
||||
* @example
|
||||
* ```js
|
||||
* keycloak.updateToken(5).success(function(refreshed) {
|
||||
* if (refreshed) {
|
||||
* alert('Token was successfully refreshed');
|
||||
* } else {
|
||||
* alert('Token is still valid');
|
||||
* }
|
||||
* }).error(function() {
|
||||
* alert('Failed to refresh the token, or the session has expired');
|
||||
* });
|
||||
*/
|
||||
updateToken(minValidity: number): KeycloakPromise<boolean, boolean>;
|
||||
|
||||
/**
|
||||
* Clears authentication state, including tokens. This can be useful if
|
||||
* the application has detected the session was expired, for example if
|
||||
* updating token fails. Invoking this results in Keycloak#onAuthLogout
|
||||
* callback listener being invoked.
|
||||
*/
|
||||
clearToken(): void;
|
||||
|
||||
/**
|
||||
* Returns true if the token has the given realm role.
|
||||
* @param role A realm role name.
|
||||
*/
|
||||
hasRealmRole(role: string): boolean;
|
||||
|
||||
/**
|
||||
* Returns true if the token has the given role for the resource.
|
||||
* @param role A role name.
|
||||
* @param resource If not specified, `clientId` is used.
|
||||
*/
|
||||
hasResourceRole(role: string, resource?: string): boolean;
|
||||
|
||||
/**
|
||||
* Loads the user's profile.
|
||||
* @returns A promise to set functions to be invoked on success or error.
|
||||
*/
|
||||
loadUserProfile(): KeycloakPromise<KeycloakProfile, void>;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
loadUserInfo(): KeycloakPromise<{}, void>;
|
||||
}
|
||||
}
|
||||
@@ -40,18 +40,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
var useNonce = true;
|
||||
|
||||
kc.init = function (initOptions) {
|
||||
kc.authenticated = false;
|
||||
|
||||
callbackStorage = createCallbackStorage();
|
||||
var adapters = ['default', 'cordova', 'cordova-native'];
|
||||
|
||||
if (initOptions && adapters.indexOf(initOptions.adapter) > -1) {
|
||||
adapter = loadAdapter(initOptions.adapter);
|
||||
} else if (initOptions && typeof initOptions.adapter === "object") {
|
||||
adapter = initOptions.adapter;
|
||||
if (initOptions && initOptions.adapter === 'cordova') {
|
||||
adapter = loadAdapter('cordova');
|
||||
} else if (initOptions && initOptions.adapter === 'default') {
|
||||
adapter = loadAdapter();
|
||||
} else {
|
||||
if (window.Cordova || window.cordova) {
|
||||
adapter = loadAdapter('cordova');
|
||||
@@ -61,10 +58,6 @@
|
||||
}
|
||||
|
||||
if (initOptions) {
|
||||
if (typeof initOptions.useNonce !== 'undefined') {
|
||||
useNonce = initOptions.useNonce;
|
||||
}
|
||||
|
||||
if (typeof initOptions.checkLoginIframe !== 'undefined') {
|
||||
loginIframe.enable = initOptions.checkLoginIframe;
|
||||
}
|
||||
@@ -105,10 +98,6 @@
|
||||
if (initOptions.timeSkew != null) {
|
||||
kc.timeSkew = initOptions.timeSkew;
|
||||
}
|
||||
|
||||
if(initOptions.redirectUri) {
|
||||
kc.redirectUri = initOptions.redirectUri;
|
||||
}
|
||||
}
|
||||
|
||||
if (!kc.responseMode) {
|
||||
@@ -170,15 +159,10 @@
|
||||
var callback = parseCallback(window.location.href);
|
||||
|
||||
if (callback) {
|
||||
setupCheckLoginIframe();
|
||||
window.history.replaceState({}, null, callback.newUrl);
|
||||
}
|
||||
|
||||
if (callback && callback.valid) {
|
||||
return setupCheckLoginIframe().success(function() {
|
||||
processCallback(callback, initPromise);
|
||||
}).error(function (e) {
|
||||
initPromise.setError();
|
||||
});
|
||||
processCallback(callback, initPromise);
|
||||
return;
|
||||
} else if (initOptions) {
|
||||
if (initOptions.token && initOptions.refreshToken) {
|
||||
setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken);
|
||||
@@ -246,34 +230,22 @@
|
||||
|
||||
callbackStorage.add(callbackState);
|
||||
|
||||
var baseUrl;
|
||||
var action = 'auth';
|
||||
if (options && options.action == 'register') {
|
||||
baseUrl = kc.endpoints.register();
|
||||
} else {
|
||||
baseUrl = kc.endpoints.authorize();
|
||||
action = 'registrations';
|
||||
}
|
||||
|
||||
var scope;
|
||||
if (options && options.scope) {
|
||||
if (options.scope.indexOf("openid") != -1) {
|
||||
scope = options.scope;
|
||||
} else {
|
||||
scope = "openid " + options.scope;
|
||||
}
|
||||
} else {
|
||||
scope = "openid";
|
||||
}
|
||||
var scope = (options && options.scope) ? "openid " + options.scope : "openid";
|
||||
|
||||
var url = baseUrl
|
||||
var url = getRealmUrl()
|
||||
+ '/protocol/openid-connect/' + action
|
||||
+ '?client_id=' + encodeURIComponent(kc.clientId)
|
||||
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
|
||||
+ '&state=' + encodeURIComponent(state)
|
||||
+ '&nonce=' + encodeURIComponent(nonce)
|
||||
+ '&response_mode=' + encodeURIComponent(kc.responseMode)
|
||||
+ '&response_type=' + encodeURIComponent(kc.responseType)
|
||||
+ '&scope=' + encodeURIComponent(scope);
|
||||
if (useNonce) {
|
||||
url = url + '&nonce=' + encodeURIComponent(nonce);
|
||||
}
|
||||
|
||||
if (options && options.prompt) {
|
||||
url += '&prompt=' + encodeURIComponent(options.prompt);
|
||||
@@ -294,10 +266,6 @@
|
||||
if (options && options.locale) {
|
||||
url += '&ui_locales=' + encodeURIComponent(options.locale);
|
||||
}
|
||||
|
||||
if (options && options.kcLocale) {
|
||||
url += '&kc_locale=' + encodeURIComponent(options.kcLocale);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
@@ -307,7 +275,8 @@
|
||||
}
|
||||
|
||||
kc.createLogoutUrl = function(options) {
|
||||
var url = kc.endpoints.logout()
|
||||
var url = getRealmUrl()
|
||||
+ '/protocol/openid-connect/logout'
|
||||
+ '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false));
|
||||
|
||||
return url;
|
||||
@@ -326,14 +295,11 @@
|
||||
}
|
||||
|
||||
kc.createAccountUrl = function(options) {
|
||||
var realm = getRealmUrl();
|
||||
var url = undefined;
|
||||
if (typeof realm !== 'undefined') {
|
||||
url = realm
|
||||
var url = getRealmUrl()
|
||||
+ '/account'
|
||||
+ '?referrer=' + encodeURIComponent(kc.clientId)
|
||||
+ '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options));
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -381,7 +347,7 @@
|
||||
}
|
||||
|
||||
kc.loadUserInfo = function() {
|
||||
var url = kc.endpoints.userinfo();
|
||||
var url = getRealmUrl() + '/protocol/openid-connect/userinfo';
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('GET', url, true);
|
||||
req.setRequestHeader('Accept', 'application/json');
|
||||
@@ -446,7 +412,7 @@
|
||||
promise.setSuccess(false);
|
||||
} else {
|
||||
var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken;
|
||||
var url = kc.endpoints.token();
|
||||
var url = getRealmUrl() + '/protocol/openid-connect/token';
|
||||
|
||||
refreshQueue.push(promise);
|
||||
|
||||
@@ -482,10 +448,6 @@
|
||||
} else {
|
||||
console.warn('[KEYCLOAK] Failed to refresh token');
|
||||
|
||||
if (req.status == 400) {
|
||||
kc.clearToken();
|
||||
}
|
||||
|
||||
kc.onAuthRefreshError && kc.onAuthRefreshError();
|
||||
for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) {
|
||||
p.setError(true);
|
||||
@@ -524,14 +486,10 @@
|
||||
}
|
||||
|
||||
function getRealmUrl() {
|
||||
if (typeof kc.authServerUrl !== 'undefined') {
|
||||
if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') {
|
||||
return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm);
|
||||
} else {
|
||||
return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm);
|
||||
}
|
||||
if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') {
|
||||
return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm);
|
||||
} else {
|
||||
return undefined;
|
||||
return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,7 +523,7 @@
|
||||
|
||||
if ((kc.flow != 'implicit') && code) {
|
||||
var params = 'code=' + code + '&grant_type=authorization_code';
|
||||
var url = kc.endpoints.token();
|
||||
var url = getRealmUrl() + '/protocol/openid-connect/token';
|
||||
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('POST', url, true);
|
||||
@@ -602,9 +560,9 @@
|
||||
|
||||
setToken(accessToken, refreshToken, idToken, timeLocal);
|
||||
|
||||
if (useNonce && ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) ||
|
||||
if ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) ||
|
||||
(kc.refreshTokenParsed && kc.refreshTokenParsed.nonce != oauth.storedNonce) ||
|
||||
(kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce))) {
|
||||
(kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce)) {
|
||||
|
||||
console.info('[KEYCLOAK] Invalid nonce, clearing token');
|
||||
kc.clearToken();
|
||||
@@ -629,65 +587,6 @@
|
||||
configUrl = config;
|
||||
}
|
||||
|
||||
function setupOidcEndoints(oidcConfiguration) {
|
||||
if (! oidcConfiguration) {
|
||||
kc.endpoints = {
|
||||
authorize: function() {
|
||||
return getRealmUrl() + '/protocol/openid-connect/auth';
|
||||
},
|
||||
token: function() {
|
||||
return getRealmUrl() + '/protocol/openid-connect/token';
|
||||
},
|
||||
logout: function() {
|
||||
return getRealmUrl() + '/protocol/openid-connect/logout';
|
||||
},
|
||||
checkSessionIframe: function() {
|
||||
var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';
|
||||
if (kc.iframeVersion) {
|
||||
src = src + '?version=' + kc.iframeVersion;
|
||||
}
|
||||
return src;
|
||||
},
|
||||
register: function() {
|
||||
return getRealmUrl() + '/protocol/openid-connect/registrations';
|
||||
},
|
||||
userinfo: function() {
|
||||
return getRealmUrl() + '/protocol/openid-connect/userinfo';
|
||||
}
|
||||
};
|
||||
} else {
|
||||
kc.endpoints = {
|
||||
authorize: function() {
|
||||
return oidcConfiguration.authorization_endpoint;
|
||||
},
|
||||
token: function() {
|
||||
return oidcConfiguration.token_endpoint;
|
||||
},
|
||||
logout: function() {
|
||||
if (!oidcConfiguration.end_session_endpoint) {
|
||||
throw "Not supported by the OIDC server";
|
||||
}
|
||||
return oidcConfiguration.end_session_endpoint;
|
||||
},
|
||||
checkSessionIframe: function() {
|
||||
if (!oidcConfiguration.check_session_iframe) {
|
||||
throw "Not supported by the OIDC server";
|
||||
}
|
||||
return oidcConfiguration.check_session_iframe;
|
||||
},
|
||||
register: function() {
|
||||
throw 'Redirection to "Register user" page not supported in standard OIDC mode';
|
||||
},
|
||||
userinfo: function() {
|
||||
if (!oidcConfiguration.userinfo_endpoint) {
|
||||
throw "Not supported by the OIDC server";
|
||||
}
|
||||
return oidcConfiguration.userinfo_endpoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (configUrl) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('GET', configUrl, true);
|
||||
@@ -702,7 +601,7 @@
|
||||
kc.realm = config['realm'];
|
||||
kc.clientId = config['resource'];
|
||||
kc.clientSecret = (config['credentials'] || {})['secret'];
|
||||
setupOidcEndoints(null);
|
||||
|
||||
promise.setSuccess();
|
||||
} else {
|
||||
promise.setError();
|
||||
@@ -712,62 +611,30 @@
|
||||
|
||||
req.send();
|
||||
} else {
|
||||
if (!config['url']) {
|
||||
var scripts = document.getElementsByTagName('script');
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
if (scripts[i].src.match(/.*keycloak\.js/)) {
|
||||
config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.realm) {
|
||||
throw 'realm missing';
|
||||
}
|
||||
|
||||
if (!config.clientId) {
|
||||
throw 'clientId missing';
|
||||
}
|
||||
|
||||
kc.authServerUrl = config.url;
|
||||
kc.realm = config.realm;
|
||||
kc.clientId = config.clientId;
|
||||
kc.clientSecret = (config.credentials || {}).secret;
|
||||
|
||||
var oidcProvider = config['oidcProvider'];
|
||||
if (!oidcProvider) {
|
||||
if (!config['url']) {
|
||||
var scripts = document.getElementsByTagName('script');
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
if (scripts[i].src.match(/.*keycloak\.js/)) {
|
||||
config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!config.realm) {
|
||||
throw 'realm missing';
|
||||
}
|
||||
|
||||
kc.authServerUrl = config.url;
|
||||
kc.realm = config.realm;
|
||||
setupOidcEndoints(null);
|
||||
promise.setSuccess();
|
||||
} else {
|
||||
if (typeof oidcProvider === 'string') {
|
||||
var oidcProviderConfigUrl;
|
||||
if (oidcProvider.charAt(oidcProvider.length - 1) == '/') {
|
||||
oidcProviderConfigUrl = oidcProvider + '.well-known/openid-configuration';
|
||||
} else {
|
||||
oidcProviderConfigUrl = oidcProvider + '/.well-known/openid-configuration';
|
||||
}
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('GET', oidcProviderConfigUrl, true);
|
||||
req.setRequestHeader('Accept', 'application/json');
|
||||
|
||||
req.onreadystatechange = function () {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 || fileLoaded(req)) {
|
||||
var oidcProviderConfig = JSON.parse(req.responseText);
|
||||
setupOidcEndoints(oidcProviderConfig);
|
||||
promise.setSuccess();
|
||||
} else {
|
||||
promise.setError();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
req.send();
|
||||
} else {
|
||||
setupOidcEndoints(oidcProvider);
|
||||
promise.setSuccess();
|
||||
}
|
||||
}
|
||||
promise.setSuccess();
|
||||
}
|
||||
|
||||
return promise.promise;
|
||||
@@ -886,137 +753,23 @@
|
||||
}
|
||||
|
||||
function parseCallback(url) {
|
||||
var oauth = parseCallbackUrl(url);
|
||||
if (!oauth) {
|
||||
return;
|
||||
}
|
||||
|
||||
var oauth = new CallbackParser(url, kc.responseMode).parseUri();
|
||||
var oauthState = callbackStorage.get(oauth.state);
|
||||
|
||||
if (oauthState) {
|
||||
oauth.valid = true;
|
||||
if (oauthState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token)) {
|
||||
oauth.redirectUri = oauthState.redirectUri;
|
||||
oauth.storedNonce = oauthState.nonce;
|
||||
oauth.prompt = oauthState.prompt;
|
||||
}
|
||||
|
||||
return oauth;
|
||||
}
|
||||
|
||||
function parseCallbackUrl(url) {
|
||||
var supportedParams;
|
||||
switch (kc.flow) {
|
||||
case 'standard':
|
||||
supportedParams = ['code', 'state', 'session_state'];
|
||||
break;
|
||||
case 'implicit':
|
||||
supportedParams = ['access_token', 'id_token', 'state', 'session_state'];
|
||||
break;
|
||||
case 'hybrid':
|
||||
supportedParams = ['access_token', 'id_token', 'code', 'state', 'session_state'];
|
||||
break;
|
||||
}
|
||||
|
||||
supportedParams.push('error');
|
||||
supportedParams.push('error_description');
|
||||
supportedParams.push('error_uri');
|
||||
|
||||
var queryIndex = url.indexOf('?');
|
||||
var fragmentIndex = url.indexOf('#');
|
||||
|
||||
var newUrl;
|
||||
var parsed;
|
||||
|
||||
if (kc.responseMode === 'query' && queryIndex !== -1) {
|
||||
newUrl = url.substring(0, queryIndex);
|
||||
parsed = parseCallbackParams(url.substring(queryIndex + 1, fragmentIndex !== -1 ? fragmentIndex : url.length), supportedParams);
|
||||
if (parsed.paramsString !== '') {
|
||||
newUrl += '?' + parsed.paramsString;
|
||||
if (oauth.fragment) {
|
||||
oauth.newUrl += '#' + oauth.fragment;
|
||||
}
|
||||
if (fragmentIndex !== -1) {
|
||||
newUrl += url.substring(fragmentIndex);
|
||||
}
|
||||
} else if (kc.responseMode === 'fragment' && fragmentIndex !== -1) {
|
||||
newUrl = url.substring(0, fragmentIndex);
|
||||
parsed = parseCallbackParams(url.substring(fragmentIndex + 1), supportedParams);
|
||||
if (parsed.paramsString !== '') {
|
||||
newUrl += '#' + parsed.paramsString;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed && parsed.oauthParams) {
|
||||
if (kc.flow === 'standard' || kc.flow === 'hybrid') {
|
||||
if ((parsed.oauthParams.code || parsed.oauthParams.error) && parsed.oauthParams.state) {
|
||||
parsed.oauthParams.newUrl = newUrl;
|
||||
return parsed.oauthParams;
|
||||
}
|
||||
} else if (kc.flow === 'implicit') {
|
||||
if ((parsed.oauthParams.access_token || parsed.oauthParams.error) && parsed.oauthParams.state) {
|
||||
parsed.oauthParams.newUrl = newUrl;
|
||||
return parsed.oauthParams;
|
||||
}
|
||||
}
|
||||
return oauth;
|
||||
}
|
||||
}
|
||||
|
||||
function parseCallbackParams(paramsString, supportedParams) {
|
||||
var p = paramsString.split('&');
|
||||
var result = {
|
||||
paramsString: '',
|
||||
oauthParams: {}
|
||||
}
|
||||
for (var i = 0; i < p.length; i++) {
|
||||
var t = p[i].split('=');
|
||||
if (supportedParams.indexOf(t[0]) !== -1) {
|
||||
result.oauthParams[t[0]] = t[1];
|
||||
} else {
|
||||
if (result.paramsString !== '') {
|
||||
result.paramsString += '&';
|
||||
}
|
||||
result.paramsString += p[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function createPromise() {
|
||||
if (typeof Promise === "function") {
|
||||
return createNativePromise();
|
||||
} else {
|
||||
return createLegacyPromise();
|
||||
}
|
||||
}
|
||||
|
||||
function createNativePromise() {
|
||||
// Need to create a native Promise which also preserves the
|
||||
// interface of the custom promise type previously used by the API
|
||||
var p = {
|
||||
setSuccess: function(result) {
|
||||
p.success = true;
|
||||
p.resolve(result);
|
||||
},
|
||||
|
||||
setError: function(result) {
|
||||
p.success = false;
|
||||
p.reject(result);
|
||||
}
|
||||
};
|
||||
p.promise = new Promise(function(resolve, reject) {
|
||||
p.resolve = resolve;
|
||||
p.reject = reject;
|
||||
});
|
||||
p.promise.success = function(callback) {
|
||||
p.promise.then(callback);
|
||||
return p.promise;
|
||||
}
|
||||
p.promise.error = function(callback) {
|
||||
p.promise.catch(callback);
|
||||
return p.promise;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
function createLegacyPromise() {
|
||||
var p = {
|
||||
setSuccess: function(result) {
|
||||
p.success = true;
|
||||
@@ -1073,18 +826,22 @@
|
||||
loginIframe.iframe = iframe;
|
||||
|
||||
iframe.onload = function() {
|
||||
var authUrl = kc.endpoints.authorize();
|
||||
if (authUrl.charAt(0) === '/') {
|
||||
var realmUrl = getRealmUrl();
|
||||
if (realmUrl.charAt(0) === '/') {
|
||||
loginIframe.iframeOrigin = getOrigin();
|
||||
} else {
|
||||
loginIframe.iframeOrigin = authUrl.substring(0, authUrl.indexOf('/', 8));
|
||||
loginIframe.iframeOrigin = realmUrl.substring(0, realmUrl.indexOf('/', 8));
|
||||
}
|
||||
promise.setSuccess();
|
||||
|
||||
setTimeout(check, loginIframe.interval * 1000);
|
||||
}
|
||||
|
||||
var src = kc.endpoints.checkSessionIframe();
|
||||
var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html';
|
||||
if (kc.iframeVersion) {
|
||||
src = src + '?version=' + kc.iframeVersion;
|
||||
}
|
||||
|
||||
iframe.setAttribute('src', src );
|
||||
iframe.setAttribute('title', 'keycloak-session-iframe' );
|
||||
iframe.style.display = 'none';
|
||||
@@ -1164,12 +921,7 @@
|
||||
},
|
||||
|
||||
accountManagement : function() {
|
||||
var accountUrl = kc.createAccountUrl();
|
||||
if (typeof accountUrl !== 'undefined') {
|
||||
window.location.href = accountUrl;
|
||||
} else {
|
||||
throw "Not supported by the OIDC server";
|
||||
}
|
||||
window.location.href = kc.createAccountUrl();
|
||||
return createPromise().promise;
|
||||
},
|
||||
|
||||
@@ -1183,7 +935,12 @@
|
||||
} else if (kc.redirectUri) {
|
||||
return kc.redirectUri;
|
||||
} else {
|
||||
return location.href;
|
||||
var redirectUri = location.href;
|
||||
if (location.hash && encodeHash) {
|
||||
redirectUri = redirectUri.substring(0, location.href.indexOf('#'));
|
||||
redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'redirect_fragment=' + encodeURIComponent(location.hash.substring(1));
|
||||
}
|
||||
return redirectUri;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1199,54 +956,24 @@
|
||||
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 {
|
||||
login: function(options) {
|
||||
var promise = createPromise();
|
||||
|
||||
var cordovaOptions = createCordovaOptions(options);
|
||||
var o = 'location=no';
|
||||
if (options && options.prompt == 'none') {
|
||||
o += ',hidden=yes';
|
||||
}
|
||||
|
||||
var loginUrl = kc.createLoginUrl(options);
|
||||
var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', cordovaOptions);
|
||||
var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', o);
|
||||
var completed = false;
|
||||
|
||||
var closed = false;
|
||||
var closeBrowser = function() {
|
||||
closed = true;
|
||||
ref.close();
|
||||
};
|
||||
|
||||
ref.addEventListener('loadstart', function(event) {
|
||||
if (event.url.indexOf('http://localhost') == 0) {
|
||||
var callback = parseCallback(event.url);
|
||||
processCallback(callback, promise);
|
||||
closeBrowser();
|
||||
ref.close();
|
||||
completed = true;
|
||||
}
|
||||
});
|
||||
@@ -1256,29 +983,21 @@
|
||||
if (event.url.indexOf('http://localhost') == 0) {
|
||||
var callback = parseCallback(event.url);
|
||||
processCallback(callback, promise);
|
||||
closeBrowser();
|
||||
ref.close();
|
||||
completed = true;
|
||||
} else {
|
||||
promise.setError();
|
||||
closeBrowser();
|
||||
ref.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ref.addEventListener('exit', function(event) {
|
||||
if (!closed) {
|
||||
promise.setError({
|
||||
reason: "closed_by_user"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return promise.promise;
|
||||
},
|
||||
|
||||
logout: function(options) {
|
||||
var promise = createPromise();
|
||||
|
||||
|
||||
var logoutUrl = kc.createLogoutUrl(options);
|
||||
var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes');
|
||||
|
||||
@@ -1313,8 +1032,7 @@
|
||||
|
||||
register : function() {
|
||||
var registerUrl = kc.createRegisterUrl();
|
||||
var cordovaOptions = createCordovaOptions(options);
|
||||
var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', cordovaOptions);
|
||||
var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', 'location=no');
|
||||
ref.addEventListener('loadstart', function(event) {
|
||||
if (event.url.indexOf('http://localhost') == 0) {
|
||||
ref.close();
|
||||
@@ -1324,16 +1042,12 @@
|
||||
|
||||
accountManagement : function() {
|
||||
var accountUrl = kc.createAccountUrl();
|
||||
if (typeof accountUrl !== 'undefined') {
|
||||
var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no');
|
||||
ref.addEventListener('loadstart', function(event) {
|
||||
if (event.url.indexOf('http://localhost') == 0) {
|
||||
ref.close();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw "Not supported by the OIDC server";
|
||||
}
|
||||
var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no');
|
||||
ref.addEventListener('loadstart', function(event) {
|
||||
if (event.url.indexOf('http://localhost') == 0) {
|
||||
ref.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
redirectUri: function(options) {
|
||||
@@ -1342,75 +1056,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (type == 'cordova-native') {
|
||||
loginIframe.enable = false;
|
||||
|
||||
return {
|
||||
login: function(options) {
|
||||
var promise = createPromise();
|
||||
var loginUrl = kc.createLoginUrl(options);
|
||||
|
||||
universalLinks.subscribe('keycloak', function(event) {
|
||||
universalLinks.unsubscribe('keycloak');
|
||||
window.cordova.plugins.browsertab.close();
|
||||
var oauth = parseCallback(event.url);
|
||||
processCallback(oauth, promise);
|
||||
});
|
||||
|
||||
window.cordova.plugins.browsertab.openUrl(loginUrl);
|
||||
return promise.promise;
|
||||
},
|
||||
|
||||
logout: function(options) {
|
||||
var promise = createPromise();
|
||||
var logoutUrl = kc.createLogoutUrl(options);
|
||||
|
||||
universalLinks.subscribe('keycloak', function(event) {
|
||||
universalLinks.unsubscribe('keycloak');
|
||||
window.cordova.plugins.browsertab.close();
|
||||
kc.clearToken();
|
||||
promise.setSuccess();
|
||||
});
|
||||
|
||||
window.cordova.plugins.browsertab.openUrl(logoutUrl);
|
||||
return promise.promise;
|
||||
},
|
||||
|
||||
register : function(options) {
|
||||
var promise = createPromise();
|
||||
var registerUrl = kc.createRegisterUrl(options);
|
||||
universalLinks.subscribe('keycloak' , function(event) {
|
||||
universalLinks.unsubscribe('keycloak');
|
||||
window.cordova.plugins.browsertab.close();
|
||||
var oauth = parseCallback(event.url);
|
||||
processCallback(oauth, promise);
|
||||
});
|
||||
window.cordova.plugins.browsertab.openUrl(registerUrl);
|
||||
return promise.promise;
|
||||
|
||||
},
|
||||
|
||||
accountManagement : function() {
|
||||
var accountUrl = kc.createAccountUrl();
|
||||
if (typeof accountUrl !== 'undefined') {
|
||||
window.cordova.plugins.browsertab.openUrl(accountUrl);
|
||||
} else {
|
||||
throw "Not supported by the OIDC server";
|
||||
}
|
||||
},
|
||||
|
||||
redirectUri: function(options) {
|
||||
if (options && options.redirectUri) {
|
||||
return options.redirectUri;
|
||||
} else if (kc.redirectUri) {
|
||||
return kc.redirectUri;
|
||||
} else {
|
||||
return "http://localhost";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw 'invalid adapter type: ' + type;
|
||||
}
|
||||
|
||||
@@ -1532,6 +1177,99 @@
|
||||
|
||||
return new CookieStorage();
|
||||
}
|
||||
|
||||
var CallbackParser = function(uriToParse, responseMode) {
|
||||
if (!(this instanceof CallbackParser)) {
|
||||
return new CallbackParser(uriToParse, responseMode);
|
||||
}
|
||||
var parser = this;
|
||||
|
||||
var initialParse = function() {
|
||||
var baseUri = null;
|
||||
var queryString = null;
|
||||
var fragmentString = null;
|
||||
|
||||
var questionMarkIndex = uriToParse.indexOf("?");
|
||||
var fragmentIndex = uriToParse.indexOf("#", questionMarkIndex + 1);
|
||||
if (questionMarkIndex == -1 && fragmentIndex == -1) {
|
||||
baseUri = uriToParse;
|
||||
} else if (questionMarkIndex != -1) {
|
||||
baseUri = uriToParse.substring(0, questionMarkIndex);
|
||||
queryString = uriToParse.substring(questionMarkIndex + 1);
|
||||
if (fragmentIndex != -1) {
|
||||
fragmentIndex = queryString.indexOf("#");
|
||||
fragmentString = queryString.substring(fragmentIndex + 1);
|
||||
queryString = queryString.substring(0, fragmentIndex);
|
||||
}
|
||||
} else {
|
||||
baseUri = uriToParse.substring(0, fragmentIndex);
|
||||
fragmentString = uriToParse.substring(fragmentIndex + 1);
|
||||
}
|
||||
|
||||
return { baseUri: baseUri, queryString: queryString, fragmentString: fragmentString };
|
||||
}
|
||||
|
||||
var parseParams = function(paramString) {
|
||||
var result = {};
|
||||
var params = paramString.split('&');
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
var p = params[i].split('=');
|
||||
var paramName = decodeURIComponent(p[0]);
|
||||
var paramValue = decodeURIComponent(p[1]);
|
||||
result[paramName] = paramValue;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var handleQueryParam = function(paramName, paramValue, oauth) {
|
||||
var supportedOAuthParams = [ 'code', 'state', 'error', 'error_description' ];
|
||||
|
||||
for (var i = 0 ; i< supportedOAuthParams.length ; i++) {
|
||||
if (paramName === supportedOAuthParams[i]) {
|
||||
oauth[paramName] = paramValue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
parser.parseUri = function() {
|
||||
var parsedUri = initialParse();
|
||||
|
||||
var queryParams = {};
|
||||
if (parsedUri.queryString) {
|
||||
queryParams = parseParams(parsedUri.queryString);
|
||||
}
|
||||
|
||||
var oauth = { newUrl: parsedUri.baseUri };
|
||||
for (var param in queryParams) {
|
||||
switch (param) {
|
||||
case 'redirect_fragment':
|
||||
oauth.fragment = queryParams[param];
|
||||
break;
|
||||
default:
|
||||
if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) {
|
||||
oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + encodeURIComponent(queryParams[param]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (responseMode === 'fragment') {
|
||||
var fragmentParams = {};
|
||||
if (parsedUri.fragmentString) {
|
||||
fragmentParams = parseParams(parsedUri.fragmentString);
|
||||
}
|
||||
for (var param in fragmentParams) {
|
||||
oauth[param] = fragmentParams[param];
|
||||
}
|
||||
}
|
||||
|
||||
return oauth;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
|
||||
|
||||
@@ -79,10 +79,6 @@
|
||||
|
||||
function receiveMessage(event)
|
||||
{
|
||||
if (typeof event.data !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
var origin = event.origin;
|
||||
var data = event.data.split(' ');
|
||||
if (data.length != 2) {
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<?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.2.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>kcinit</artifactId>
|
||||
<name>Keycloak CLI SSO Framework</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-installed-adapter</artifactId>
|
||||
</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>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.keycloak.adapters.KcinitMain</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/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 $@
|
||||
@@ -1,8 +0,0 @@
|
||||
@echo off
|
||||
|
||||
if "%OS%" == "Windows_NT" (
|
||||
set "DIRNAME=%~dp0%"
|
||||
) else (
|
||||
set DIRNAME=.\
|
||||
)
|
||||
java -jar %DIRNAME%\kcinit-${project.version}.jar %*
|
||||
@@ -1,30 +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;
|
||||
|
||||
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>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -36,7 +36,7 @@
|
||||
org.keycloak.adapters.osgi.*
|
||||
</keycloak.osgi.export>
|
||||
<keycloak.osgi.import>
|
||||
org.ops4j.pax.web.*;version="[3.0,8)",
|
||||
org.ops4j.pax.web.*;version="[3.0,5)",
|
||||
javax.servlet.*;version="[2.5,4)";resolution:=optional,
|
||||
org.eclipse.jetty.*;version="[8.1,10)";resolution:=optional,
|
||||
org.keycloak.*;version="${project.version}",
|
||||
@@ -77,6 +77,11 @@
|
||||
<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-api</artifactId>
|
||||
@@ -94,12 +99,6 @@
|
||||
<version>${cxf.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
-89
@@ -1,89 +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.osgi;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.OIDCHttpFacade;
|
||||
|
||||
/**
|
||||
* This {@link KeycloakConfigResolver} tries to resolve most specific configuration for given URI path. If not found,
|
||||
* <em>parent</em> path is checked up to top-level path.
|
||||
*/
|
||||
public class HierarchicalPathBasedKeycloakConfigResolver extends PathBasedKeycloakConfigResolver {
|
||||
|
||||
protected static final Logger log = Logger.getLogger(HierarchicalPathBasedKeycloakConfigResolver.class);
|
||||
|
||||
public HierarchicalPathBasedKeycloakConfigResolver() {
|
||||
prepopulateCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
|
||||
// we cached all available deployments initially and now we'll try to check them from
|
||||
// most specific to most general
|
||||
URI uri = URI.create(request.getURI());
|
||||
String path = uri.getPath();
|
||||
if (path != null) {
|
||||
while (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
String[] segments = path.split("/");
|
||||
List<String> paths = collectPaths(segments);
|
||||
for (String pathFragment: paths) {
|
||||
KeycloakDeployment cachedDeployment = super.getCachedDeployment(pathFragment);
|
||||
if (cachedDeployment != null) {
|
||||
return cachedDeployment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Can't find Keycloak configuration related to URI path " + uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>For segments like "a, b, c, d", returns:<ul>
|
||||
* <li>"a-b-c-d"</li>
|
||||
* <li>"a-b-c"</li>
|
||||
* <li>"a-b"</li>
|
||||
* <li>"a"</li>
|
||||
* <li>""</li>
|
||||
* </ul></p>
|
||||
* @param segments
|
||||
* @return
|
||||
*/
|
||||
private List<String> collectPaths(String[] segments) {
|
||||
List<String> result = new ArrayList<>(segments.length + 1);
|
||||
for (int idx = segments.length; idx >= 0; idx--) {
|
||||
StringBuilder sb = null;
|
||||
for (int i = 0; i < idx; i++) {
|
||||
if (sb == null) {
|
||||
sb = new StringBuilder();
|
||||
}
|
||||
sb.append("-").append(segments[i]);
|
||||
}
|
||||
result.add(sb == null ? "" : sb.toString().substring(1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
+22
-134
@@ -16,168 +16,56 @@
|
||||
*/
|
||||
package org.keycloak.adapters.osgi;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.adapters.KeycloakConfigResolver;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||
import org.keycloak.adapters.OIDCHttpFacade;
|
||||
import org.keycloak.adapters.spi.HttpFacade;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
|
||||
|
||||
protected static final Logger log = Logger.getLogger(PathBasedKeycloakConfigResolver.class);
|
||||
|
||||
private final Map<String, KeycloakDeployment> cache = new ConcurrentHashMap<String, KeycloakDeployment>();
|
||||
|
||||
private File keycloakConfigLocation = null;
|
||||
|
||||
public PathBasedKeycloakConfigResolver() {
|
||||
String location = null;
|
||||
String keycloakConfig = (String) System.getProperties().get("keycloak.config");
|
||||
if (keycloakConfig != null && !"".equals(keycloakConfig.trim())) {
|
||||
location = keycloakConfig;
|
||||
} else {
|
||||
String karafEtc = (String) System.getProperties().get("karaf.etc");
|
||||
if (karafEtc != null && !"".equals(karafEtc.trim())) {
|
||||
location = karafEtc;
|
||||
}
|
||||
}
|
||||
if (location != null) {
|
||||
File loc = new File(location);
|
||||
if (loc.isDirectory()) {
|
||||
keycloakConfigLocation = loc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakDeployment resolve(OIDCHttpFacade.Request request) {
|
||||
String webContext = getDeploymentKeyForURI(request);
|
||||
String path = request.getURI();
|
||||
String[] urlTokens = path.split("/");
|
||||
if (urlTokens.length < 4) {
|
||||
throw new IllegalStateException("Not able to determine the web-context to load the correspondent keycloak.json file");
|
||||
}
|
||||
|
||||
return getOrCreateDeployment(webContext);
|
||||
}
|
||||
String webContext = urlTokens[3];
|
||||
|
||||
/**
|
||||
* {@code pathFragment} is a key for {@link KeycloakDeployment deployments}. The key is used to construct
|
||||
* a path relative to {@code keycloak.config} or {@code karaf.etc} system properties.
|
||||
* For given key, {@code <key>-keycloak.json} file is checked.
|
||||
* @param pathFragment
|
||||
* @return
|
||||
*/
|
||||
protected synchronized KeycloakDeployment getOrCreateDeployment(String pathFragment) {
|
||||
KeycloakDeployment deployment = getCachedDeployment(pathFragment);
|
||||
KeycloakDeployment deployment = cache.get(webContext);
|
||||
if (null == deployment) {
|
||||
// not found on the simple cache, try to load it from the file system
|
||||
if (keycloakConfigLocation == null) {
|
||||
throw new IllegalStateException("Neither \"keycloak.config\" nor \"karaf.etc\" java properties are set." +
|
||||
" Please set one of them.");
|
||||
String keycloakConfig = (String) System.getProperties().get("keycloak.config");
|
||||
if(keycloakConfig == null || "".equals(keycloakConfig.trim())){
|
||||
String karafEtc = (String) System.getProperties().get("karaf.etc");
|
||||
if(karafEtc == null || "".equals(karafEtc.trim())){
|
||||
throw new IllegalStateException("Neither \"keycloak.config\" nor \"karaf.etc\" java properties are set. Please set one of them.");
|
||||
}
|
||||
keycloakConfig = karafEtc;
|
||||
}
|
||||
|
||||
File configuration = new File(keycloakConfigLocation, pathFragment + ("".equals(pathFragment) ? "" : "-")
|
||||
+ "keycloak.json");
|
||||
if (!cacheConfiguration(pathFragment, configuration)) {
|
||||
throw new IllegalStateException("Not able to read the file " + configuration);
|
||||
String absolutePath = keycloakConfig + File.separator + webContext + "-keycloak.json";
|
||||
InputStream is = null;
|
||||
try {
|
||||
is = new FileInputStream(absolutePath);
|
||||
} catch (FileNotFoundException e){
|
||||
throw new IllegalStateException("Not able to find the file " + absolutePath);
|
||||
}
|
||||
deployment = KeycloakDeploymentBuilder.build(is);
|
||||
cache.put(webContext, deployment);
|
||||
}
|
||||
|
||||
return deployment;
|
||||
}
|
||||
|
||||
protected synchronized KeycloakDeployment getCachedDeployment(String pathFragment) {
|
||||
return cache.get(pathFragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* If there's a need, we can pre populate the cache of deployments.
|
||||
*/
|
||||
protected void prepopulateCache() {
|
||||
if (keycloakConfigLocation == null || !keycloakConfigLocation.isDirectory()) {
|
||||
log.warn("Can't cache Keycloak configurations. No configuration storage is accessible." +
|
||||
" Please set either \"keycloak.config\" or \"karaf.etc\" system properties");
|
||||
return;
|
||||
}
|
||||
|
||||
File[] configs = keycloakConfigLocation.listFiles(new FileFilter() {
|
||||
@Override
|
||||
public boolean accept(File pathname) {
|
||||
return pathname.isFile() && pathname.getName().endsWith("keycloak.json");
|
||||
}
|
||||
});
|
||||
if (configs != null) {
|
||||
for (File config: configs) {
|
||||
String pathFragment = null;
|
||||
if ("keycloak.json".equals(config.getName())) {
|
||||
pathFragment = "";
|
||||
} else if (config.getName().endsWith("-keycloak.json")) {
|
||||
pathFragment = config.getName()
|
||||
.substring(0, config.getName().length() - "-keycloak.json".length());
|
||||
}
|
||||
cacheConfiguration(pathFragment, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean cacheConfiguration(String key, File config) {
|
||||
try {
|
||||
InputStream is = new FileInputStream(config);
|
||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(is);
|
||||
cache.put(key, deployment);
|
||||
return true;
|
||||
} catch (FileNotFoundException | RuntimeException e) {
|
||||
log.warn("Can't cache " + config + ": " + e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a context path from given {@link HttpFacade.Request}. For default context, first path segment
|
||||
* is returned.
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
private String getDeploymentKeyForURI(HttpFacade.Request request) {
|
||||
String uri = request.getURI();
|
||||
String relativePath = request.getRelativePath();
|
||||
String webContext = null;
|
||||
if (relativePath == null || !uri.contains(relativePath)) {
|
||||
String[] urlTokens = uri.split("/");
|
||||
if (urlTokens.length < 4) {
|
||||
throw new IllegalStateException("Not able to determine the web-context to load the correspondent keycloak.json file");
|
||||
}
|
||||
|
||||
webContext = urlTokens[3];
|
||||
} else {
|
||||
URI parsedURI = URI.create(uri);
|
||||
String path = parsedURI.getPath();
|
||||
if (path.contains(relativePath)) {
|
||||
path = path.substring(0, path.indexOf(relativePath));
|
||||
}
|
||||
while (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
webContext = path;
|
||||
if ("".equals(webContext)) {
|
||||
path = relativePath;
|
||||
while (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
if (path.contains("/")) {
|
||||
path = path.substring(0, path.indexOf("/"));
|
||||
}
|
||||
webContext = path;
|
||||
}
|
||||
}
|
||||
|
||||
return webContext;
|
||||
}
|
||||
|
||||
}
|
||||
+7
-102
@@ -29,12 +29,11 @@ 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 in Fuse 6.3, which allows to inject custom jetty-web.xml configuration from current bundle classpath into {@link WebContainer}
|
||||
* 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
|
||||
@@ -48,7 +47,7 @@ public class PaxWebIntegrationService {
|
||||
|
||||
private BundleContext bundleContext;
|
||||
private String jettyWebXmlLocation;
|
||||
private List<Object> constraintMappings;
|
||||
private List<ConstraintMapping> constraintMappings; // Using jetty constraint mapping just because of compatibility with other fuse services
|
||||
|
||||
private ServiceTracker webContainerTracker;
|
||||
private HttpContext httpContext;
|
||||
@@ -69,11 +68,11 @@ public class PaxWebIntegrationService {
|
||||
this.jettyWebXmlLocation = jettyWebXmlLocation;
|
||||
}
|
||||
|
||||
public List<Object> getConstraintMappings() {
|
||||
public List<ConstraintMapping> getConstraintMappings() {
|
||||
return constraintMappings;
|
||||
}
|
||||
|
||||
public void setConstraintMappings(List<Object> constraintMappings) {
|
||||
public void setConstraintMappings(List<ConstraintMapping> constraintMappings) {
|
||||
this.constraintMappings = constraintMappings;
|
||||
}
|
||||
|
||||
@@ -121,25 +120,8 @@ public class PaxWebIntegrationService {
|
||||
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());
|
||||
}
|
||||
for (ConstraintMapping constraintMapping : constraintMappings) {
|
||||
addConstraintMapping(service, constraintMapping);
|
||||
}
|
||||
|
||||
service.registerLoginConfig("BASIC", "does-not-matter", null, null, httpContext);
|
||||
@@ -164,16 +146,6 @@ 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) {
|
||||
Constraint constraint = constraintMapping.getConstraint();
|
||||
String[] roles = constraint.getRoles();
|
||||
@@ -206,71 +178,4 @@ public class PaxWebIntegrationService {
|
||||
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
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
-162
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.OIDCHttpFacade;
|
||||
import org.keycloak.adapters.spi.AuthenticationError;
|
||||
import org.keycloak.adapters.spi.HttpFacade;
|
||||
import org.keycloak.adapters.spi.LogoutError;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class HierarchicalPathBasedKeycloakConfigResolverTest {
|
||||
|
||||
@Test
|
||||
public void genericAndSpecificConfigurations() throws Exception {
|
||||
HierarchicalPathBasedKeycloakConfigResolver resolver = new HierarchicalPathBasedKeycloakConfigResolver();
|
||||
populate(resolver, true);
|
||||
|
||||
assertThat(resolver.resolve(new MockRequest("http://localhost/a/b/c/d/e?a=b")).getRealm(), equalTo("a-b-c-d-e"));
|
||||
assertThat(resolver.resolve(new MockRequest("http://localhost/a/b/c/d/x?a=b")).getRealm(), equalTo("a-b-c-d"));
|
||||
assertThat(resolver.resolve(new MockRequest("http://localhost/a/b/c/x/x?a=b")).getRealm(), equalTo("a-b-c"));
|
||||
assertThat(resolver.resolve(new MockRequest("http://localhost/a/b/x/x/x?a=b")).getRealm(), equalTo("a-b"));
|
||||
assertThat(resolver.resolve(new MockRequest("http://localhost/a/x/x/x/x?a=b")).getRealm(), equalTo("a"));
|
||||
assertThat(resolver.resolve(new MockRequest("http://localhost/x/x/x/x/x?a=b")).getRealm(), equalTo(""));
|
||||
|
||||
populate(resolver, false);
|
||||
try {
|
||||
resolver.resolve(new MockRequest("http://localhost/x/x/x/x/x?a=b"));
|
||||
fail("Expected java.lang.IllegalStateException: Can't find Keycloak configuration ...");
|
||||
} catch (IllegalStateException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private PathBasedKeycloakConfigResolver populate(PathBasedKeycloakConfigResolver resolver, boolean fallback)
|
||||
throws Exception {
|
||||
Field f = PathBasedKeycloakConfigResolver.class.getDeclaredField("cache");
|
||||
f.setAccessible(true);
|
||||
Map<String, KeycloakDeployment> cache = (Map<String, KeycloakDeployment>) f.get(resolver);
|
||||
cache.clear();
|
||||
cache.put("a-b-c-d-e", newKeycloakDeployment("a-b-c-d-e"));
|
||||
cache.put("a-b-c-d", newKeycloakDeployment("a-b-c-d"));
|
||||
cache.put("a-b-c", newKeycloakDeployment("a-b-c"));
|
||||
cache.put("a-b", newKeycloakDeployment("a-b"));
|
||||
cache.put("a", newKeycloakDeployment("a"));
|
||||
if (fallback) {
|
||||
cache.put("", newKeycloakDeployment(""));
|
||||
}
|
||||
|
||||
return resolver;
|
||||
}
|
||||
|
||||
private KeycloakDeployment newKeycloakDeployment(String realm) {
|
||||
KeycloakDeployment deployment = new KeycloakDeployment();
|
||||
deployment.setRealm(realm);
|
||||
|
||||
return deployment;
|
||||
}
|
||||
|
||||
private class MockRequest implements OIDCHttpFacade.Request {
|
||||
|
||||
private String uri;
|
||||
|
||||
public MockRequest(String uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getURI() {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRelativePath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFirstParam(String param) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQueryParamValue(String param) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpFacade.Cookie getCookie(String cookieName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getHeaders(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream(boolean buffered) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRemoteAddr() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setError(AuthenticationError error) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setError(LogoutError error) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.OIDCHttpFacade;
|
||||
import org.keycloak.adapters.spi.AuthenticationError;
|
||||
import org.keycloak.adapters.spi.HttpFacade;
|
||||
import org.keycloak.adapters.spi.LogoutError;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
public class PathBasedKeycloakConfigResolverTest {
|
||||
|
||||
@Test
|
||||
public void relativeURIsAndContexts() throws Exception {
|
||||
PathBasedKeycloakConfigResolver resolver = new PathBasedKeycloakConfigResolver();
|
||||
|
||||
assertNotNull(populate(resolver, "test")
|
||||
.resolve(new MockRequest("http://localhost/test/a/b/c?d=e", "/a/b/c")));
|
||||
|
||||
assertNotNull(populate(resolver, "test")
|
||||
.resolve(new MockRequest("http://localhost/test/a/b/c?d=e", "/a/b")));
|
||||
|
||||
// means default context and actually we use first segment
|
||||
assertNotNull(populate(resolver, "test")
|
||||
.resolve(new MockRequest("http://localhost/test/a/b/c?d=e", "/test/a/b/c")));
|
||||
|
||||
assertNotNull(populate(resolver, "test/a")
|
||||
.resolve(new MockRequest("http://localhost/test/a/b/c?d=e", "/b/c")));
|
||||
|
||||
assertNotNull(populate(resolver, "")
|
||||
.resolve(new MockRequest("http://localhost/", "/")));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private PathBasedKeycloakConfigResolver populate(PathBasedKeycloakConfigResolver resolver, String context)
|
||||
throws Exception {
|
||||
Field f = PathBasedKeycloakConfigResolver.class.getDeclaredField("cache");
|
||||
f.setAccessible(true);
|
||||
Map<String, KeycloakDeployment> cache = (Map<String, KeycloakDeployment>) f.get(resolver);
|
||||
cache.clear();
|
||||
cache.put(context, new KeycloakDeployment());
|
||||
|
||||
return resolver;
|
||||
}
|
||||
|
||||
private class MockRequest implements OIDCHttpFacade.Request {
|
||||
|
||||
private String uri;
|
||||
private String relativePath;
|
||||
|
||||
public MockRequest(String uri, String relativePath) {
|
||||
this.uri = uri;
|
||||
this.relativePath = relativePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getURI() {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRelativePath() {
|
||||
return this.relativePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFirstParam(String param) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQueryParamValue(String param) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpFacade.Cookie getCookie(String cookieName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getHeaders(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream(boolean buffered) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRemoteAddr() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setError(AuthenticationError error) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setError(LogoutError error) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<name>Keycloak OIDC Client Adapter Modules</name>
|
||||
@@ -34,8 +34,7 @@
|
||||
<module>adapter-core</module>
|
||||
<module>as7-eap6</module>
|
||||
<module>installed</module>
|
||||
<module>fuse7</module>
|
||||
<module>kcinit</module>
|
||||
<module>cli-sso</module>
|
||||
<module>jaxrs-oauth-client</module>
|
||||
<module>jetty</module>
|
||||
<module>js</module>
|
||||
@@ -43,8 +42,6 @@
|
||||
<module>servlet-filter</module>
|
||||
<module>servlet-oauth-client</module>
|
||||
<module>spring-boot</module>
|
||||
<module>spring-boot2</module>
|
||||
<module>spring-boot-adapter-core</module>
|
||||
<module>spring-boot-container-bundle</module>
|
||||
<module>spring-security</module>
|
||||
<module>tomcat</module>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
+2
-23
@@ -36,8 +36,6 @@ import javax.security.cert.X509Certificate;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
@@ -267,8 +265,6 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
||||
public Request getRequest() {
|
||||
return new Request() {
|
||||
|
||||
private InputStream inputStream;
|
||||
|
||||
@Override
|
||||
public String getFirstParam(String param) {
|
||||
return servletRequest.getParameter(param);
|
||||
@@ -318,27 +314,10 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
||||
|
||||
@Override
|
||||
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 {
|
||||
return servletRequest.getInputStream();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<?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.2.1.Final</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>
|
||||
-265
@@ -1,265 +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.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);
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.keycloak.adapters.springboot.KeycloakAutoConfiguration
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>keycloak-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>4.2.1.Final</version>
|
||||
<version>3.4.1.Final</version>
|
||||
<relativePath>../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-boot-container-bundle</artifactId>
|
||||
|
||||
Loaded 100 of 5578 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user