Compare commits

..
Author SHA1 Message Date
keycloak-bot f45ef2ccc8 Set version to 3.3.0.CR1 2017-08-28 13:52:06 +00:00
6192 changed files with 63280 additions and 1516390 deletions

No files matched your search

+1 -3
View File
@@ -52,6 +52,4 @@ target
# nodejs #
##########
# KEYCLOAK-5391: We will re-exclude node_modules when node_modules handling is worked out.
# For now, we keep our js libraries checked into GitHub, so we don't ignore.
#node_modules
node_modules
-11
View File
@@ -15,23 +15,12 @@ env:
- TESTS=server-group3
- TESTS=server-group4
- TESTS=old
- 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
Regular → Executable
+1 -5
View File
@@ -38,7 +38,7 @@ Starting Keycloak
To start Keycloak during development first build as specified above, then run:
mvn -f testsuite/utils/pom.xml exec:java -Pkeycloak-server
mvn -f testsuite/integration/pom.xml exec:java -Pkeycloak-server
To start Keycloak from the server distribution first build the distribution it as specified above, then run:
@@ -49,10 +49,6 @@ To start Keycloak from the server distribution first build the distribution it a
To stop the server press `Ctrl + C`.
Reporting security vulnerabilities
----------------------------------
If you've found a security vulnerability, please look at the [instructions on how to properly report it](http://www.keycloak.org/security.html)
Help and Documentation
----------------------
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -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() {
@@ -287,16 +277,6 @@ public class AdapterDeploymentContext {
delegate.setSslRequired(sslRequired);
}
@Override
public int getConfidentialPort() {
return delegate.getConfidentialPort();
}
@Override
public void setConfidentialPort(int confidentialPort) {
delegate.setConfidentialPort(confidentialPort);
}
@Override
public TokenStore getTokenStore() {
return delegate.getTokenStore();
@@ -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)) {
@@ -71,10 +71,9 @@ public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticat
AccessTokenResponse atr=null;
try {
String userpw=new String(Base64.decode(tokenString));
int seperatorIndex = userpw.indexOf(":");
String user = userpw.substring(0, seperatorIndex);
String pw = userpw.substring(seperatorIndex + 1);
atr = getToken(user, pw);
String[] parts=userpw.split(":");
atr = getToken(parts[0], parts[1]);
tokenString = atr.getToken();
} catch (Exception e) {
log.debug("Failed to obtain token", e);
@@ -83,8 +82,8 @@ public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticat
}
return authenticateToken(exchange, atr.getToken());
}
}
private AccessTokenResponse getToken(String username, String password) throws Exception {
AccessTokenResponse tokenResponse=null;
HttpClient client = deployment.getClient();
@@ -23,8 +23,6 @@ import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.representations.AccessToken;
import javax.security.cert.X509Certificate;
@@ -85,16 +83,6 @@ public class BearerTokenRequestAuthenticator {
}
protected AuthOutcome authenticateToken(HttpFacade exchange, String tokenString) {
log.debug("Verifying access_token");
if (log.isTraceEnabled()) {
try {
JWSInput jwsInput = new JWSInput(tokenString);
String wireString = jwsInput.getWireString();
log.tracef("\taccess_token: %s", wireString.substring(0, wireString.lastIndexOf(".")) + ".signature");
} catch (JWSInputException e) {
log.errorf(e, "Failed to parse access_token: %s", tokenString);
}
}
try {
token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment);
} catch (VerificationException e) {
@@ -136,7 +124,6 @@ public class BearerTokenRequestAuthenticator {
}
surrogate = chain[0].getSubjectDN().getName();
}
log.debug("successful authorized");
return AuthOutcome.AUTHENTICATED;
}
@@ -174,19 +161,10 @@ 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);
if(deployment.isDelegateBearerErrorResponseSending()){
facade.getResponse().setStatus(401);
}
else {
facade.getResponse().sendError(401);
}
facade.getResponse().sendError(401);
return true;
}
};
@@ -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);
@@ -68,7 +68,6 @@ public class KeycloakDeployment {
protected String scope;
protected SslRequired sslRequired = SslRequired.ALL;
protected int confidentialPort = -1;
protected TokenStore tokenStore = TokenStore.SESSION;
protected String stateCookieName = "OAuth_Token_Request_State";
protected boolean useResourceRoleMappings;
@@ -95,8 +94,6 @@ public class KeycloakDeployment {
protected Map<String, String> redirectRewriteRules;
protected boolean delegateBearerErrorResponseSending = false;
public KeycloakDeployment() {
}
@@ -154,8 +151,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();
@@ -280,14 +275,6 @@ public class KeycloakDeployment {
this.sslRequired = sslRequired;
}
public int getConfidentialPort() {
return confidentialPort;
}
public void setConfidentialPort(int confidentialPort) {
this.confidentialPort = confidentialPort;
}
public TokenStore getTokenStore() {
return tokenStore;
}
@@ -469,12 +456,6 @@ public class KeycloakDeployment {
public void setRewriteRedirectRules(Map<String, String> redirectRewriteRules) {
this.redirectRewriteRules = redirectRewriteRules;
}
public boolean isDelegateBearerErrorResponseSending() {
return delegateBearerErrorResponseSending;
}
public void setDelegateBearerErrorResponseSending(boolean delegateBearerErrorResponseSending) {
this.delegateBearerErrorResponseSending = delegateBearerErrorResponseSending;
}
}
@@ -78,11 +78,6 @@ public class KeycloakDeploymentBuilder {
} else {
deployment.setSslRequired(SslRequired.EXTERNAL);
}
if (adapterConfig.getConfidentialPort() != -1) {
deployment.setConfidentialPort(adapterConfig.getConfidentialPort());
}
if (adapterConfig.getTokenStore() != null) {
deployment.setTokenStore(TokenStore.valueOf(adapterConfig.getTokenStore().toUpperCase()));
} else {
@@ -66,7 +66,7 @@ public class OAuthRequestAuthenticator {
this.reqAuthenticator = requestAuthenticator;
this.facade = facade;
this.deployment = deployment;
this.sslRedirectPort = deployment.getConfidentialPort() != -1 ? deployment.getConfidentialPort() : sslRedirectPort;
this.sslRedirectPort = sslRedirectPort;
this.tokenStore = tokenStore;
}
@@ -170,9 +170,6 @@ public class OAuthRequestAuthenticator {
String maxAge = getQueryParamValue(OAuth2Constants.MAX_AGE);
url = UriUtils.stripQueryParam(url, OAuth2Constants.MAX_AGE);
String uiLocales = getQueryParamValue(OAuth2Constants.UI_LOCALES_PARAM);
url = UriUtils.stripQueryParam(url, OAuth2Constants.UI_LOCALES_PARAM);
KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
@@ -191,9 +188,6 @@ public class OAuthRequestAuthenticator {
if (maxAge != null && maxAge.length() > 0) {
redirectUriBuilder.queryParam(OAuth2Constants.MAX_AGE, maxAge);
}
if (uiLocales != null && uiLocales.length() > 0) {
redirectUriBuilder.queryParam(OAuth2Constants.UI_LOCALES_PARAM, uiLocales);
}
scope = TokenUtil.attachOIDCScope(scope);
redirectUriBuilder.queryParam(OAuth2Constants.SCOPE, scope);
@@ -337,7 +331,7 @@ public class OAuthRequestAuthenticator {
} catch (ServerRequest.HttpFailure failure) {
log.error("failed to turn code into token");
log.error("status from server: " + failure.getStatus());
if (failure.getError() != null) {
if (failure.getStatus() == 400 && failure.getError() != null) {
log.error(" " + failure.getError());
}
return challenge(403, OIDCAuthenticationError.Reason.CODE_TO_TOKEN_FAILURE, null);
@@ -350,14 +344,6 @@ public class OAuthRequestAuthenticator {
tokenString = tokenResponse.getToken();
refreshToken = tokenResponse.getRefreshToken();
idTokenString = tokenResponse.getIdToken();
log.debug("Verifying tokens");
if (log.isTraceEnabled()) {
logToken("\taccess_token", tokenString);
logToken("\tid_token", idTokenString);
logToken("\trefresh_token", refreshToken);
}
try {
token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment);
if (idTokenString != null) {
@@ -390,8 +376,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();
}
@@ -413,13 +398,4 @@ public class OAuthRequestAuthenticator {
return originalUri;
}
private void logToken(String name, String token) {
try {
JWSInput jwsInput = new JWSInput(token);
String wireString = jwsInput.getWireString();
log.tracef("\t%s: %s", name, wireString.substring(0, wireString.lastIndexOf(".")) + ".signature");
} catch (JWSInputException e) {
log.errorf(e, "Failed to parse %s: %s", name, token);
}
}
}
@@ -32,7 +32,7 @@ import java.util.Map;
*/
public class ClientIdAndSecretCredentialsProvider implements ClientCredentialsProvider {
private static Logger logger = Logger.getLogger(ClientIdAndSecretCredentialsProvider.class);
private static Logger logger = Logger.getLogger(ClientCredentialsProviderUtils.class);
public static final String PROVIDER_ID = CredentialRepresentation.SECRET;
@@ -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;
}
}
@@ -18,27 +18,22 @@
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,110 +42,103 @@ 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 (pathConfig != null) {
challenge(pathConfig, getRequiredScopes(pathConfig, request), httpFacade);
}
return createEmptyAuthorizationContext(false);
}
if (securityContext != null) {
AccessToken accessToken = securityContext.getToken();
AccessToken accessToken = securityContext.getToken();
if (accessToken != null) {
Request request = httpFacade.getRequest();
String path = getPath(request);
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
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 (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 (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Could not find a configuration for path [%s]", getPath(request));
if (EnforcementMode.DISABLED.equals(pathConfig.getEnforcementMode())) {
return createEmptyAuthorizationContext(true);
}
if (isDefaultAccessDeniedUri(request)) {
return createAuthorizationContext(accessToken, null);
Set<String> requiredScopes = getRequiredScopes(pathConfig, request);
if (isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
try {
return createAuthorizationContext(accessToken, pathConfig);
} catch (Exception e) {
throw new RuntimeException("Error processing path [" + pathConfig.getPath() + "].", e);
}
}
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()) {
if (!challenge(pathConfig, requiredScopes, httpFacade)) {
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
handleAccessDenied(httpFacade);
}
handleAccessDenied(httpFacade);
}
}
return createEmptyAuthorizationContext(false);
}
protected abstract boolean challenge(PathConfig pathConfig, MethodConfig methodConfig, OIDCHttpFacade facade);
protected abstract boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade);
protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
protected boolean isAuthorized(PathConfig actualPathConfig, Set<String> requiredScopes, 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;
@@ -158,19 +146,16 @@ public abstract class AbstractPolicyEnforcer {
continue;
}
if (hasResourceScopePermission(methodConfig, permission)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions);
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
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 {
if (hasResourceScopePermission(methodConfig, permission)) {
if (hasResourceScopePermission(requiredScopes, permission, actualPathConfig)) {
hasPermission = true;
return true;
}
@@ -181,83 +166,45 @@ 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]. No 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);
}
private boolean isDefaultAccessDeniedUri(Request request, PolicyEnforcerConfig enforcerConfig) {
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
if (accessDeniedPath != null) {
if (request.getURI().contains(accessDeniedPath)) {
return true;
}
}
return false;
}
private boolean hasResourceScopePermission(Set<String> requiredScopes, Permission permission, PathConfig actualPathConfig) {
Set<String> allowedScopes = permission.getScopes();
return (allowedScopes.containsAll(requiredScopes) || allowedScopes.isEmpty());
}
protected AuthzClient getAuthzClient() {
return policyEnforcer.getClient();
return this.authzClient;
}
protected PolicyEnforcerConfig getEnforcerConfig() {
return policyEnforcer.getEnforcerConfig();
return enforcerConfig;
}
protected PolicyEnforcer getPolicyEnforcer() {
return policyEnforcer;
}
private boolean isDefaultAccessDeniedUri(Request request) {
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
return accessDeniedPath != null && request.getURI().contains(accessDeniedPath);
}
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
List<String> requiredScopes = methodConfig.getScopes();
Set<String> allowedScopes = permission.getScopes();
if (allowedScopes.isEmpty()) {
return true;
}
PolicyEnforcerConfig.ScopeEnforcementMode enforcementMode = methodConfig.getScopesEnforcementMode();
if (PolicyEnforcerConfig.ScopeEnforcementMode.ALL.equals(enforcementMode)) {
return allowedScopes.containsAll(requiredScopes);
}
if (PolicyEnforcerConfig.ScopeEnforcementMode.ANY.equals(enforcementMode)) {
for (String requiredScope : requiredScopes) {
if (allowedScopes.contains(requiredScope)) {
return true;
}
}
}
return requiredScopes.isEmpty();
}
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
return new ClientAuthorizationContext(getAuthzClient()) {
return new ClientAuthorizationContext(authzClient) {
@Override
public boolean hasPermission(String resourceName, String scopeName) {
return granted;
@@ -289,26 +236,24 @@ public abstract class AbstractPolicyEnforcer {
return request.getRelativePath();
}
private MethodConfig getRequiredScopes(PathConfig pathConfig, Request request) {
private Set<String> getRequiredScopes(PathConfig pathConfig, Request request) {
Set<String> requiredScopes = new HashSet<>();
requiredScopes.addAll(pathConfig.getScopes());
String method = request.getMethod();
for (MethodConfig methodConfig : pathConfig.getMethods()) {
for (PolicyEnforcerConfig.MethodConfig methodConfig : pathConfig.getMethods()) {
if (methodConfig.getMethod().equals(method)) {
return methodConfig;
requiredScopes.addAll(methodConfig.getScopes());
}
}
MethodConfig methodConfig = new MethodConfig();
methodConfig.setMethod(request.getMethod());
methodConfig.setScopes(pathConfig.getScopes());
methodConfig.setScopesEnforcementMode(PolicyEnforcerConfig.ScopeEnforcementMode.ANY);
return methodConfig;
return requiredScopes;
}
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) {
@@ -324,34 +269,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());
}
}
@@ -0,0 +1,85 @@
/*
* 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.Set;
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.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, Set<String> requiredScopes, OIDCHttpFacade facade) {
if (getEnforcerConfig().getUserManagedAccess() != null) {
challengeUmaAuthentication(pathConfig, requiredScopes, 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, Set<String> requiredScopes, OIDCHttpFacade facade) {
HttpFacade.Response response = facade.getResponse();
AuthzClient authzClient = getAuthzClient();
String ticket = getPermissionTicket(pathConfig, requiredScopes, 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, Set<String> requiredScopes, AuthzClient authzClient) {
ProtectionResource protection = authzClient.protection();
PermissionResource permission = protection.permission();
PermissionRequest permissionRequest = new PermissionRequest();
permissionRequest.setResourceSetId(pathConfig.getId());
permissionRequest.setScopes(requiredScopes);
return permission.forResource(permissionRequest).getTicket();
}
}
@@ -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);
}
@@ -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);
}
@@ -18,30 +18,25 @@
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>
@@ -55,14 +50,14 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
}
@Override
protected boolean isAuthorized(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
protected boolean isAuthorized(PathConfig pathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
AccessToken original = accessToken;
if (super.isAuthorized(pathConfig, methodConfig, accessToken, httpFacade)) {
if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
return true;
}
accessToken = requestAuthorizationToken(pathConfig, methodConfig, httpFacade);
accessToken = requestAuthorizationToken(pathConfig, requiredScopes, httpFacade);
if (accessToken == null) {
return false;
@@ -78,56 +73,22 @@ 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);
return super.isAuthorized(pathConfig, methodConfig, accessToken, httpFacade);
return super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade);
}
@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, Set<String> requiredScopes, OIDCHttpFacade facade) {
handleAccessDenied(facade);
return true;
}
@Override
protected void handleAccessDenied(OIDCHttpFacade facade) {
KeycloakSecurityContext securityContext = facade.getSecurityContext();
if (securityContext == null) {
return;
}
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
HttpFacade.Response response = facade.getResponse();
@@ -139,89 +100,52 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
}
}
private AccessToken requestAuthorizationToken(PathConfig pathConfig, PolicyEnforcerConfig.MethodConfig methodConfig, OIDCHttpFacade httpFacade) {
if (getEnforcerConfig().getUserManagedAccess() != null) {
return null;
}
private AccessToken requestAuthorizationToken(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade httpFacade) {
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(requiredScopes);
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 || authHeaders.size() == 0) {
return false;
}
for (String authHeader : authHeaders) {
String[] split = authHeader.trim().split("\\s+");
if (split == null || split.length != 2) continue;
if (!split[0].equalsIgnoreCase("Bearer")) continue;
return true;
}
return false;
}
}
@@ -24,8 +24,6 @@ import java.util.concurrent.locks.LockSupport;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
/**
* A simple LRU cache implementation supporting expiration and maximum number of entries.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PathCache {
@@ -40,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.
@@ -56,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;
@@ -79,10 +80,6 @@ public class PathCache {
}
}
public boolean containsKey(String uri) {
return cache.containsKey(uri);
}
public PathConfig get(String uri) {
if (parkForReadAndCheckInterrupt()) {
return null;
@@ -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;
}
}
@@ -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,125 +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.getUri() != null) {
PathConfig pathConfig = PathConfig.createPathConfig(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.createPathConfig(matchingResources.get(0));
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.createPathConfig(targetResource);
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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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 + "]"));
}
}
@@ -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);
}
@@ -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("]", "");
}
}
@@ -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();
}
}
@@ -37,12 +37,8 @@ import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Principal;
import java.util.HashSet;
import java.util.Map;
@@ -57,7 +53,7 @@ public abstract class AbstractKeycloakLoginModule implements LoginModule {
public static final String KEYCLOAK_CONFIG_FILE_OPTION = "keycloak-config-file";
public static final String ROLE_PRINCIPAL_CLASS_OPTION = "role-principal-class";
public static final String PROFILE_RESOURCE = "profile:";
protected Subject subject;
protected CallbackHandler callbackHandler;
protected Auth auth;
@@ -88,27 +84,14 @@ public abstract class AbstractKeycloakLoginModule implements LoginModule {
protected KeycloakDeployment resolveDeployment(String keycloakConfigFile) {
try {
InputStream is = null;
if (keycloakConfigFile.startsWith(PROFILE_RESOURCE)) {
try {
is = new URL(keycloakConfigFile).openStream();
} catch (MalformedURLException mfue) {
throw new RuntimeException(mfue);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
} else {
is = FindFile.findFile(keycloakConfigFile);
}
InputStream is = FindFile.findFile(keycloakConfigFile);
KeycloakDeployment kd = KeycloakDeploymentBuilder.build(is);
return kd;
} catch (RuntimeException e) {
getLogger().debug("Unable to find or parse file " + keycloakConfigFile + " due to " + e.getMessage(), e);
throw e;
}
}
@Override
public boolean login() throws LoginException {
@@ -81,7 +81,7 @@ public class JWKPublicKeyLocator implements PublicKeyLocator {
private PublicKey lookupCachedKey(int publicKeyCacheTtl, int currentTime, String kid) {
if (lastRequestTime + publicKeyCacheTtl > currentTime && kid != null) {
if (lastRequestTime + publicKeyCacheTtl > currentTime) {
return currentKeys.get(kid);
} else {
return null;
@@ -16,5 +16,4 @@
#
org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider
org.keycloak.adapters.authentication.JWTClientCredentialsProvider
org.keycloak.adapters.authentication.JWTClientSecretCredentialsProvider
org.keycloak.adapters.authentication.JWTClientCredentialsProvider
@@ -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
@@ -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());
}
}
@@ -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.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-as7-integration-pom</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -1,13 +0,0 @@
package org.keycloak.adapters.jbossweb;
import org.apache.catalina.Container;
import org.apache.catalina.Valve;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.tomcat.AbstractAuthenticatedActionsValve;
public class AuthenticatedActionsValve extends AbstractAuthenticatedActionsValve {
public AuthenticatedActionsValve(AdapterDeploymentContext deploymentContext, Valve next, Container container) {
super(deploymentContext, next, container);
}
}
@@ -17,15 +17,11 @@
package org.keycloak.adapters.jbossweb;
import org.apache.catalina.Container;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.deploy.LoginConfig;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.tomcat.AbstractAuthenticatedActionsValve;
import org.keycloak.adapters.tomcat.AbstractKeycloakAuthenticatorValve;
import org.keycloak.adapters.tomcat.GenericPrincipalFactory;
@@ -60,6 +56,7 @@ public class KeycloakAuthenticatorValve extends AbstractKeycloakAuthenticatorVal
super.start();
}
public void logout(Request request) {
logoutInternal(request);
}
@@ -68,9 +65,4 @@ public class KeycloakAuthenticatorValve extends AbstractKeycloakAuthenticatorVal
protected GenericPrincipalFactory createPrincipalFactory() {
return new JBossWebPrincipalFactory();
}
@Override
protected AbstractAuthenticatedActionsValve createAuthenticatedActionsValve(AdapterDeploymentContext deploymentContext, Valve next, Container container) {
return new AuthenticatedActionsValve(deploymentContext, next, container);
}
}
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-as7-integration-pom</artifactId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../pom.xml</relativePath>
</parent>
+1 -1
View File
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<name>Keycloak AS7 / JBoss EAP 6 Integration</name>
File renamed without changes.
+10
View File
@@ -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`
+9
View File
@@ -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
+84
View File
@@ -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.3.0.CR1</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);
}
}
-144
View File
@@ -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.0.0.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.0</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-core</artifactId>
<version>2.21.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<id>bundle-manifest</id>
<phase>process-classes</phase>
<goals>
<goal>manifest</goal>
</goals>
</execution>
</executions>
<configuration>
<instructions>
<Bundle-Name>${project.name}</Bundle-Name>
<Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName>
<Import-Package>${keycloak.osgi.import}</Import-Package>
<Export-Package>${keycloak.osgi.export}</Export-Package>
<Export-Service>org.apache.camel.spi.ComponentResolver;component=undertow-keycloak</Export-Service>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,187 +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 java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.apache.camel.CamelContext;
import org.apache.camel.Consumer;
import org.apache.camel.Processor;
import org.apache.camel.component.undertow.RestUndertowHttpBinding;
import org.apache.camel.component.undertow.UndertowComponent;
import org.apache.camel.component.undertow.UndertowEndpoint;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.HostUtils;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.URISupport;
/**
*
* @author hmlnarik
*/
public class UndertowKeycloakComponent extends UndertowComponent {
public UndertowKeycloakComponent() {
}
public UndertowKeycloakComponent(CamelContext context) {
super(context);
}
@Override
protected UndertowEndpoint createEndpointInstance(URI endpointUri, UndertowComponent component) throws URISyntaxException {
return new UndertowKeycloakEndpoint(endpointUri.toString(), component);
}
// TODO: uncomment line below after backport of https://issues.apache.org/jira/browse/CAMEL-12514 into fuse
// @Override
protected String getComponentName() {
return "undertow-keycloak";
}
// TODO: remove all below this line after backport of https://issues.apache.org/jira/browse/CAMEL-12514 into fuse
@Override
public Consumer createConsumer(CamelContext camelContext, Processor processor, String verb, String basePath, String uriTemplate,
String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters) throws Exception {
return doCreateConsumer(camelContext, processor, verb, basePath, uriTemplate, consumes, produces, configuration, parameters, false);
}
@Override
public Consumer createApiConsumer(CamelContext camelContext, Processor processor, String contextPath,
RestConfiguration configuration, Map<String, Object> parameters) throws Exception {
// reuse the createConsumer method we already have. The api need to use GET and match on uri prefix
return doCreateConsumer(camelContext, processor, "GET", contextPath, null, null, null, configuration, parameters, true);
}
Consumer doCreateConsumer(CamelContext camelContext, Processor processor, String verb, String basePath, String uriTemplate,
String consumes, String produces, RestConfiguration configuration, Map<String, Object> parameters, boolean api) throws Exception {
String path = basePath;
if (uriTemplate != null) {
// make sure to avoid double slashes
if (uriTemplate.startsWith("/")) {
path = path + uriTemplate;
} else {
path = path + "/" + uriTemplate;
}
}
path = FileUtil.stripLeadingSeparator(path);
String scheme = "http";
String host = "";
int port = 0;
RestConfiguration config = configuration;
if (config == null) {
config = camelContext.getRestConfiguration(getComponentName(), true);
}
if (config.getScheme() != null) {
scheme = config.getScheme();
}
if (config.getHost() != null) {
host = config.getHost();
}
int num = config.getPort();
if (num > 0) {
port = num;
}
// prefix path with context-path if configured in rest-dsl configuration
String contextPath = config.getContextPath();
if (ObjectHelper.isNotEmpty(contextPath)) {
contextPath = FileUtil.stripTrailingSeparator(contextPath);
contextPath = FileUtil.stripLeadingSeparator(contextPath);
if (ObjectHelper.isNotEmpty(contextPath)) {
path = contextPath + "/" + path;
}
}
// if no explicit hostname set then resolve the hostname
if (ObjectHelper.isEmpty(host)) {
if (config.getRestHostNameResolver() == RestConfiguration.RestHostNameResolver.allLocalIp) {
host = "0.0.0.0";
} else if (config.getRestHostNameResolver() == RestConfiguration.RestHostNameResolver.localHostName) {
host = HostUtils.getLocalHostName();
} else if (config.getRestHostNameResolver() == RestConfiguration.RestHostNameResolver.localIp) {
host = HostUtils.getLocalIp();
}
}
Map<String, Object> map = new HashMap<String, Object>();
// build query string, and append any endpoint configuration properties
if (config.getComponent() == null || config.getComponent().equals(getComponentName())) {
// setup endpoint options
if (config.getEndpointProperties() != null && !config.getEndpointProperties().isEmpty()) {
map.putAll(config.getEndpointProperties());
}
}
boolean explicitOptions = true;
// must use upper case for restrict
String restrict = verb.toUpperCase(Locale.US);
// allow OPTIONS in rest-dsl to allow clients to call the API and have responses with ALLOW headers
if (!restrict.contains("OPTIONS")) {
restrict += ",OPTIONS";
// this is not an explicit OPTIONS path in the rest-dsl
explicitOptions = false;
}
boolean cors = config.isEnableCORS();
if (cors) {
// allow HTTP Options as we want to handle CORS in rest-dsl
map.put("optionsEnabled", "true");
} else if (explicitOptions) {
// the rest-dsl is using OPTIONS
map.put("optionsEnabled", "true");
}
String query = URISupport.createQueryString(map);
String url;
if (api) {
url = getComponentName() + ":%s://%s:%s/%s?matchOnUriPrefix=true&httpMethodRestrict=%s";
} else {
url = getComponentName() + ":%s://%s:%s/%s?matchOnUriPrefix=false&httpMethodRestrict=%s";
}
// get the endpoint
url = String.format(url, scheme, host, port, path, restrict);
if (!query.isEmpty()) {
url = url + "&" + query;
}
UndertowEndpoint endpoint = camelContext.getEndpoint(url, UndertowEndpoint.class);
setProperties(camelContext, endpoint, parameters);
if (!map.containsKey("undertowHttpBinding")) {
// use the rest binding, if not using a custom http binding
endpoint.setUndertowHttpBinding(new RestUndertowHttpBinding());
}
// configure consumer properties
Consumer consumer = endpoint.createConsumer(processor);
if (config.getConsumerProperties() != null && !config.getConsumerProperties().isEmpty()) {
setProperties(camelContext, consumer, config.getConsumerProperties());
}
return consumer;
}
}
@@ -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();
}
}
@@ -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 +0,0 @@
class=org.keycloak.adapters.camel.undertow.UndertowKeycloakComponent
-130
View File
@@ -1,130 +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.0.0.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.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>
@@ -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;
}
}
@@ -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 +0,0 @@
org.keycloak.adapters.osgi.jetty94.KeycloakAuthenticatorService
-45
View File
@@ -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.0.0.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>
-115
View File
@@ -1,115 +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.0.0.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.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>
@@ -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 +0,0 @@
org.keycloak.adapters.osgi.tomcat.KeycloakAuthenticatorService
-158
View File
@@ -1,158 +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.0.0.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="[3.0,8)",
javax.servlet.*;version="[2.5,4)";resolution:=optional,
org.apache.cxf.transport.http;resolution:=optional;version="[3,4)",
org.apache.cxf.transport.http_undertow;resolution:=optional;version="[3,4)",
org.apache.cxf.transport.servlet;resolution:=optional;version="[3,4)",
io.undertow.*,
*;resolution:=optional
</keycloak.osgi.import>
<keycloak.osgi.fragment>org.ops4j.pax.web.pax-web-undertow</keycloak.osgi.fragment>
</properties>
<dependencies>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.enterprise</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.ops4j.pax.web</groupId>
<artifactId>pax-web-runtime</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.ops4j.pax.web</groupId>
<artifactId>pax-web-spi</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.ops4j.pax.web</groupId>
<artifactId>pax-web-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
<version>${cxf.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http-undertow</artifactId>
<version>${cxf.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-servlet</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-undertow-adapter</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-osgi-adapter</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<!-- Adding OSGI metadata to the JAR without changing the packaging type. -->
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<id>bundle-manifest</id>
<phase>process-classes</phase>
<goals>
<goal>manifest</goal>
</goals>
</execution>
</executions>
<configuration>
<instructions>
<Bundle-Name>${project.name}</Bundle-Name>
<Bundle-SymbolicName>${project.groupId}.${project.artifactId}</Bundle-SymbolicName>
<Import-Package>${keycloak.osgi.import}</Import-Package>
<Export-Package>${keycloak.osgi.export}</Export-Package>
<Fragment-Host>${keycloak.osgi.fragment}</Fragment-Host>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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;
}
}
@@ -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 +0,0 @@
org.keycloak.adapters.osgi.undertow.KeycloakAuthenticatorService
+1 -10
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</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>
@@ -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();
}
}
}
@@ -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);
}
}
}
@@ -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();
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -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();
}
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
+2 -2
View File
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</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>
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
+10 -73
View File
@@ -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 -503
View File
@@ -1,503 +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'|'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;
/**
* 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>;
}
}
+172 -368
View File
@@ -40,8 +40,6 @@
}
}
var useNonce = true;
kc.init = function (initOptions) {
kc.authenticated = false;
@@ -51,10 +49,8 @@
adapter = loadAdapter('cordova');
} else if (initOptions && initOptions.adapter === 'default') {
adapter = loadAdapter();
} else if (initOptions && typeof initOptions.adapter === "object") {
adapter = initOptions.adapter;
} else {
if (window.Cordova || window.cordova) {
if (window.Cordova) {
adapter = loadAdapter('cordova');
} else {
adapter = loadAdapter();
@@ -62,10 +58,6 @@
}
if (initOptions) {
if (typeof initOptions.useNonce !== 'undefined') {
useNonce = initOptions.useNonce;
}
if (typeof initOptions.checkLoginIframe !== 'undefined') {
loginIframe.enable = initOptions.checkLoginIframe;
}
@@ -167,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);
@@ -243,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);
@@ -291,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;
}
@@ -304,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;
@@ -323,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;
}
@@ -378,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');
@@ -443,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);
@@ -479,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);
@@ -521,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);
}
}
@@ -562,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);
@@ -599,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();
@@ -626,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);
@@ -699,7 +601,7 @@
kc.realm = config['realm'];
kc.clientId = config['resource'];
kc.clientSecret = (config['credentials'] || {})['secret'];
setupOidcEndoints(null);
promise.setSuccess();
} else {
promise.setError();
@@ -709,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;
@@ -883,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;
@@ -1070,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';
@@ -1161,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;
},
@@ -1180,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;
}
}
};
@@ -1188,62 +948,26 @@
if (type == 'cordova') {
loginIframe.enable = false;
var cordovaOpenWindowWrapper = function(loginUrl, target, options) {
if (window.cordova && window.cordova.InAppBrowser) {
// Use inappbrowser for IOS and Android if available
return window.cordova.InAppBrowser.open(loginUrl, target, options);
} else {
return window.open(loginUrl, target, options);
}
};
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 = window.open(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;
}
});
@@ -1253,31 +977,23 @@
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');
var ref = window.open(logoutUrl, '_blank', 'location=no,hidden=yes');
var error;
@@ -1310,8 +1026,7 @@
register : function() {
var registerUrl = kc.createRegisterUrl();
var cordovaOptions = createCordovaOptions(options);
var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', cordovaOptions);
var ref = window.open(registerUrl, '_blank', 'location=no');
ref.addEventListener('loadstart', function(event) {
if (event.url.indexOf('http://localhost') == 0) {
ref.close();
@@ -1321,16 +1036,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 = window.open(accountUrl, '_blank', 'location=no');
ref.addEventListener('loadstart', function(event) {
if (event.url.indexOf('http://localhost') == 0) {
ref.close();
}
});
},
redirectUri: function(options) {
@@ -1460,6 +1171,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" ) {
@@ -36,7 +36,7 @@
req.onreadystatechange = function () {
if (req.readyState === 4) {
if (req.status === 204 || req.status === 1223) {
if (req.status === 204) {
init = {
clientId: clientId,
origin: origin
@@ -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) {
-84
View File
@@ -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.0.0.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>
-26
View File
@@ -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);
}
}
+2 -8
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</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}",
@@ -99,12 +99,6 @@
<version>${cxf.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -25,7 +25,6 @@ import java.io.File;
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;
@@ -35,36 +34,14 @@ public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
@Override
public KeycloakDeployment resolve(OIDCHttpFacade.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();
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;
}
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");
}
String webContext = urlTokens[3];
KeycloakDeployment deployment = cache.get(webContext);
if (null == deployment) {
// not found on the simple cache, try to load it from the file system
@@ -77,8 +54,7 @@ public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
keycloakConfig = karafEtc;
}
String absolutePath = keycloakConfig + File.separator + webContext + ("".equals(webContext) ? "" : "-")
+ "keycloak.json";
String absolutePath = keycloakConfig + File.separator + webContext + "-keycloak.json";
InputStream is = null;
try {
is = new FileInputStream(absolutePath);
@@ -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;
}
}
}
}
@@ -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;
}
}
@@ -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) {
}
}
}
+2 -5
View File
@@ -20,7 +20,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</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>
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>4.0.0.Final</version>
<version>3.3.0.CR1</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -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.0.0.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>
Loaded 100 of 6192 files, more files were not shown because too many files have changed in this diff. Show more