diff --git a/adapters/oidc/pom.xml b/adapters/oidc/pom.xml index 91a99a54c0..ef9e79edcb 100755 --- a/adapters/oidc/pom.xml +++ b/adapters/oidc/pom.xml @@ -45,5 +45,6 @@ tomcat undertow wildfly + wildfly-elytron diff --git a/adapters/oidc/wildfly-elytron/pom.xml b/adapters/oidc/wildfly-elytron/pom.xml new file mode 100755 index 0000000000..5aefb70fc3 --- /dev/null +++ b/adapters/oidc/wildfly-elytron/pom.xml @@ -0,0 +1,99 @@ + + + + + + keycloak-parent + org.keycloak + 3.1.0.CR1-SNAPSHOT + ../../../pom.xml + + 4.0.0 + + keycloak-wildfly-elytron-oidc-adapter + Keycloak Wildfly Elytron OIDC Adapter + + + + 1.8 + 1.8 + + + + + org.wildfly.common + wildfly-common + 1.2.0.Beta1 + + + org.wildfly.security + wildfly-elytron + provided + + + org.wildfly.security.elytron-web + undertow-server + provided + + + org.jboss.logging + jboss-logging + ${jboss.logging.version} + provided + + + org.keycloak + keycloak-core + + + org.keycloak + keycloak-adapter-spi + + + org.keycloak + keycloak-adapter-core + + + org.apache.httpcomponents + httpclient + + + org.bouncycastle + bcprov-jdk15on + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + provided + + + \ No newline at end of file diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronAccount.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronAccount.java new file mode 100644 index 0000000000..c8db009779 --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronAccount.java @@ -0,0 +1,103 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.elytron; + +import org.jboss.logging.Logger; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.OidcKeycloakAccount; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.wildfly.security.auth.server.SecurityIdentity; + +import javax.security.auth.callback.CallbackHandler; +import java.security.Principal; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Pedro Igor + */ +public class ElytronAccount implements OidcKeycloakAccount { + + protected static Logger log = Logger.getLogger(ElytronAccount.class); + + private final KeycloakPrincipal principal; + + public ElytronAccount(KeycloakPrincipal principal) { + this.principal = principal; + } + + @Override + public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() { + return principal.getKeycloakSecurityContext(); + } + + @Override + public Principal getPrincipal() { + return principal; + } + + @Override + public Set getRoles() { + Set roles = new HashSet<>(); + + return roles; + } + + void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) { + principal.getKeycloakSecurityContext().setCurrentRequestInfo(deployment, tokenStore); + } + + public boolean checkActive() { + RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext(); + + if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) { + log.debug("session is active"); + return true; + } + + log.debug("session not active"); + + return false; + } + + boolean tryRefresh(CallbackHandler callbackHandler) { + log.debug("Trying to refresh"); + + RefreshableKeycloakSecurityContext securityContext = getKeycloakSecurityContext(); + + if (securityContext == null) { + log.debug("No security context. Aborting refresh."); + } + + if (securityContext.refreshExpiredToken(false)) { + SecurityIdentity securityIdentity = SecurityIdentityUtil.authorize(callbackHandler, principal); + + if (securityIdentity != null) { + log.debug("refresh succeeded"); + return true; + } + + return false; + } + + return checkActive(); + } +} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronCookieTokenStore.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronCookieTokenStore.java new file mode 100644 index 0000000000..eda7d17231 --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronCookieTokenStore.java @@ -0,0 +1,164 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.elytron; + +import java.security.Principal; + +import org.jboss.logging.Logger; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.CookieTokenStore; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.OidcKeycloakAccount; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; +import org.wildfly.security.http.HttpScope; +import org.wildfly.security.http.Scope; + +import javax.security.auth.callback.CallbackHandler; + +/** + * @author Pedro Igor + */ +public class ElytronCookieTokenStore implements ElytronTokeStore { + + protected static Logger log = Logger.getLogger(ElytronCookieTokenStore.class); + + private final ElytronHttpFacade httpFacade; + private final CallbackHandler callbackHandler; + + public ElytronCookieTokenStore(ElytronHttpFacade httpFacade, CallbackHandler callbackHandler) { + this.httpFacade = httpFacade; + this.callbackHandler = callbackHandler; + } + + @Override + public void checkCurrentToken() { + KeycloakDeployment deployment = httpFacade.getDeployment(); + KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, httpFacade, this); + + if (principal == null) { + return; + } + + RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); + + if (securityContext.isActive() && !securityContext.getDeployment().isAlwaysRefreshToken()) return; + + // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will + // not be updated + boolean success = securityContext.refreshExpiredToken(false); + if (success && securityContext.isActive()) return; + + saveAccountInfo(new ElytronAccount(principal)); + } + + @Override + public boolean isCached(RequestAuthenticator authenticator) { + KeycloakDeployment deployment = httpFacade.getDeployment(); + KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(deployment, httpFacade, this); + if (principal == null) { + log.debug("Account was not in cookie or was invalid, returning null"); + return false; + } + ElytronAccount account = new ElytronAccount(principal); + + if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) { + log.debug("Account in session belongs to a different realm than for this request."); + return false; + } + + boolean active = account.checkActive(); + + if (!active) { + active = account.tryRefresh(this.callbackHandler); + } + + if (active) { + log.debug("Cached account found"); + restoreRequest(); + httpFacade.authenticationComplete(account, true); + return true; + } else { + log.debug("Account was not active, removing cookie and returning false"); + CookieTokenStore.removeCookie(httpFacade); + return false; + } + } + + @Override + public void saveAccountInfo(OidcKeycloakAccount account) { + RefreshableKeycloakSecurityContext secContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext(); + CookieTokenStore.setTokenCookie(this.httpFacade.getDeployment(), this.httpFacade, secContext); + HttpScope exchange = this.httpFacade.getScope(Scope.EXCHANGE); + + exchange.registerForNotification(httpServerScopes -> logout()); + + exchange.setAttachment(ElytronAccount.class.getName(), account); + exchange.setAttachment(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); + + restoreRequest(); + } + + @Override + public void logout() { + logout(false); + } + + @Override + public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { + CookieTokenStore.setTokenCookie(this.httpFacade.getDeployment(), httpFacade, securityContext); + } + + @Override + public void saveRequest() { + + } + + @Override + public boolean restoreRequest() { + return false; + } + + @Override + public void logout(boolean glo) { + KeycloakPrincipal principal = CookieTokenStore.getPrincipalFromCookie(this.httpFacade.getDeployment(), this.httpFacade, this); + + if (principal == null) { + return; + } + + CookieTokenStore.removeCookie(this.httpFacade); + + if (glo) { + KeycloakSecurityContext ksc = (KeycloakSecurityContext) principal.getKeycloakSecurityContext(); + + if (ksc == null) { + return; + } + + KeycloakDeployment deployment = httpFacade.getDeployment(); + + if (!deployment.isBearerOnly() && ksc != null && ksc instanceof RefreshableKeycloakSecurityContext) { + ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); + } + } + } +} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java new file mode 100644 index 0000000000..bc2e9039ca --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java @@ -0,0 +1,394 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.elytron; + +import org.bouncycastle.asn1.cmp.Challenge; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.OIDCHttpFacade; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.spi.AuthChallenge; +import org.keycloak.adapters.spi.AuthenticationError; +import org.keycloak.adapters.spi.LogoutError; +import org.keycloak.enums.TokenStore; +import org.wildfly.security.auth.server.SecurityIdentity; +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpScope; +import org.wildfly.security.http.HttpServerCookie; +import org.wildfly.security.http.HttpServerMechanismsResponder; +import org.wildfly.security.http.HttpServerRequest; +import org.wildfly.security.http.HttpServerResponse; +import org.wildfly.security.http.Scope; + +import javax.security.auth.callback.CallbackHandler; +import javax.security.cert.X509Certificate; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +/** + * @author Pedro Igor + */ +class ElytronHttpFacade implements OIDCHttpFacade { + + private final HttpServerRequest request; + private final CallbackHandler callbackHandler; + private final AdapterTokenStore tokenStore; + private final AdapterDeploymentContext deploymentContext; + private Consumer responseConsumer; + private ElytronAccount account; + private SecurityIdentity securityIdentity; + private boolean restored; + + public ElytronHttpFacade(HttpServerRequest request, AdapterDeploymentContext deploymentContext, CallbackHandler handler) { + this.request = request; + this.deploymentContext = deploymentContext; + this.callbackHandler = handler; + this.tokenStore = createTokenStore(); + this.responseConsumer = response -> {}; + } + + void authenticationComplete(ElytronAccount account, boolean storeToken) { + this.securityIdentity = SecurityIdentityUtil.authorize(this.callbackHandler, account.getPrincipal()); + + if (securityIdentity != null) { + this.account = account; + RefreshableKeycloakSecurityContext keycloakSecurityContext = account.getKeycloakSecurityContext(); + account.setCurrentRequestInfo(keycloakSecurityContext.getDeployment(), this.tokenStore); + if (storeToken) { + this.tokenStore.saveAccountInfo(account); + } + } + } + + void authenticationComplete() { + if (securityIdentity != null) { + this.request.authenticationComplete(response -> { + if (!restored) { + responseConsumer.accept(response); + } + }, () -> ((ElytronTokeStore) tokenStore).logout(true)); + } + } + + void authenticationFailed() { + this.request.authenticationFailed("Authentication Failed", response -> responseConsumer.accept(response)); + } + + void noAuthenticationInProgress() { + this.request.noAuthenticationInProgress(); + } + + void noAuthenticationInProgress(AuthChallenge challenge) { + if (challenge != null) { + challenge.challenge(this); + } + this.request.noAuthenticationInProgress(response -> responseConsumer.accept(response)); + } + + void authenticationInProgress() { + this.request.authenticationInProgress(response -> responseConsumer.accept(response)); + } + + HttpScope getScope(Scope scope) { + return request.getScope(scope); + } + + HttpScope getScope(Scope scope, String id) { + return request.getScope(scope, id); + } + + Collection getScopeIds(Scope scope) { + return request.getScopeIds(scope); + } + + AdapterTokenStore getTokenStore() { + return this.tokenStore; + } + + KeycloakDeployment getDeployment() { + return deploymentContext.resolveDeployment(this); + } + + private AdapterTokenStore createTokenStore() { + KeycloakDeployment deployment = getDeployment(); + + if (TokenStore.SESSION.equals(deployment.getTokenStore())) { + return new ElytronSessionTokenStore(this, this.callbackHandler); + } else { + return new ElytronCookieTokenStore(this, this.callbackHandler); + } + } + + @Override + public Request getRequest() { + return new Request() { + @Override + public String getMethod() { + return request.getRequestMethod(); + } + + @Override + public String getURI() { + try { + return URLDecoder.decode(request.getRequestURI().toString(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Failed to decode request URI", e); + } + } + + @Override + public String getRelativePath() { + return request.getRequestPath(); + } + + @Override + public boolean isSecure() { + return request.getRequestURI().getScheme().equals("https"); + } + + @Override + public String getFirstParam(String param) { + throw new RuntimeException("Not implemented."); + } + + @Override + public String getQueryParamValue(String param) { + URI requestURI = request.getRequestURI(); + String query = requestURI.getQuery(); + if (query != null) { + String[] parameters = query.split("&"); + for (String parameter : parameters) { + String[] keyValue = parameter.split("="); + if (keyValue[0].equals(param)) { + return keyValue[1]; + } + } + } + return null; + } + + @Override + public Cookie getCookie(final String cookieName) { + List cookies = request.getCookies(); + + if (cookies != null) { + for (HttpServerCookie cookie : cookies) { + if (cookie.getName().equals(cookieName)) { + return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath()); + } + } + } + + return null; + } + + @Override + public String getHeader(String name) { + return request.getFirstRequestHeaderValue(name); + } + + @Override + public List getHeaders(String name) { + return request.getRequestHeaderValues(name); + } + + @Override + public InputStream getInputStream() { + return request.getInputStream(); + } + + @Override + public String getRemoteAddr() { + InetSocketAddress sourceAddress = request.getSourceAddress(); + if (sourceAddress == null) { + return ""; + } + InetAddress address = sourceAddress.getAddress(); + if (address == null) { + // this is unresolved, so we just return the host name not exactly spec, but if the name should be + // resolved then a PeerNameResolvingHandler should be used and this is probably better than just + // returning null + return sourceAddress.getHostString(); + } + return address.getHostAddress(); + } + + @Override + public void setError(AuthenticationError error) { + request.getScope(Scope.EXCHANGE).setAttachment(AuthenticationError.class.getName(), error); + } + + @Override + public void setError(LogoutError error) { + request.getScope(Scope.EXCHANGE).setAttachment(LogoutError.class.getName(), error); + } + }; + } + + @Override + public Response getResponse() { + return new Response() { + @Override + public void setStatus(final int status) { + responseConsumer = responseConsumer.andThen(response -> response.setStatusCode(status)); + } + + @Override + public void addHeader(final String name, final String value) { + responseConsumer = responseConsumer.andThen(response -> response.addResponseHeader(name, value)); + } + + @Override + public void setHeader(String name, String value) { + addHeader(name, value); + } + + @Override + public void resetCookie(final String name, final String path) { + responseConsumer = responseConsumer.andThen(response -> setCookie(name, "", path, null, 0, false, false, response)); + } + + @Override + public void setCookie(final String name, final String value, final String path, final String domain, final int maxAge, final boolean secure, final boolean httpOnly) { + responseConsumer = responseConsumer.andThen(response -> setCookie(name, value, path, domain, maxAge, secure, httpOnly, response)); + } + + private void setCookie(final String name, final String value, final String path, final String domain, final int maxAge, final boolean secure, final boolean httpOnly, HttpServerResponse response) { + response.setResponseCookie(new HttpServerCookie() { + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public String getDomain() { + return domain; + } + + @Override + public int getMaxAge() { + return maxAge; + } + + @Override + public String getPath() { + return path; + } + + @Override + public boolean isSecure() { + return secure; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public boolean isHttpOnly() { + return httpOnly; + } + }); + } + + @Override + public OutputStream getOutputStream() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + responseConsumer = responseConsumer.andThen(new Consumer() { + @Override + public void accept(HttpServerResponse httpServerResponse) { + try { + httpServerResponse.getOutputStream().write(stream.toByteArray()); + } catch (IOException e) { + throw new RuntimeException("Failed to write to response output stream", e); + } + } + }); + return stream; + } + + @Override + public void sendError(int code) { + setStatus(code); + } + + @Override + public void sendError(final int code, final String message) { + responseConsumer = responseConsumer.andThen(response -> { + response.setStatusCode(code); + response.addResponseHeader("Content-Type", "text/html"); + try { + response.getOutputStream().write(message.getBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public void end() { + + } + }; + } + + @Override + public X509Certificate[] getCertificateChain() { + return new X509Certificate[0]; + } + + @Override + public KeycloakSecurityContext getSecurityContext() { + if (account == null) { + return null; + } + return this.account.getKeycloakSecurityContext(); + } + + public boolean restoreRequest() { + restored = this.request.resumeRequest(); + return restored; + } + + public void suspendRequest() { + responseConsumer = responseConsumer.andThen(httpServerResponse -> request.suspendRequest()); + } + + public boolean isAuthorized() { + return this.securityIdentity != null; + } +} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronRequestAuthenticator.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronRequestAuthenticator.java new file mode 100644 index 0000000000..643a71660d --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronRequestAuthenticator.java @@ -0,0 +1,86 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.elytron; + +import org.keycloak.KeycloakPrincipal; +import org.keycloak.adapters.BearerTokenRequestAuthenticator; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.OAuthRequestAuthenticator; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; +import org.keycloak.adapters.spi.AuthOutcome; +import org.wildfly.security.http.HttpScope; +import org.wildfly.security.http.Scope; + +import javax.security.auth.callback.CallbackHandler; + +/** + * @author Pedro Igor + */ +public class ElytronRequestAuthenticator extends RequestAuthenticator { + + public ElytronRequestAuthenticator(CallbackHandler callbackHandler, ElytronHttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort) { + super(facade, deployment, facade.getTokenStore(), sslRedirectPort); + } + + @Override + public AuthOutcome authenticate() { + AuthOutcome authenticate = super.authenticate(); + + if (AuthOutcome.AUTHENTICATED.equals(authenticate)) { + if (!getElytronHttpFacade().isAuthorized()) { + return AuthOutcome.FAILED; + } + } + + return authenticate; + } + + @Override + protected OAuthRequestAuthenticator createOAuthAuthenticator() { + return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore); + } + + @Override + protected void completeOAuthAuthentication(final KeycloakPrincipal principal) { + getElytronHttpFacade().authenticationComplete(new ElytronAccount(principal), true); + } + + @Override + protected void completeBearerAuthentication(KeycloakPrincipal principal, String method) { + getElytronHttpFacade().authenticationComplete(new ElytronAccount(principal), false); + } + + @Override + protected String changeHttpSessionId(boolean create) { + HttpScope session = getElytronHttpFacade().getScope(Scope.SESSION); + + if (create) { + if (!session.exists()) { + session.create(); + } + } + + return session != null ? session.getID() : null; + } + + private ElytronHttpFacade getElytronHttpFacade() { + return (ElytronHttpFacade) facade; + } +} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronSessionTokenStore.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronSessionTokenStore.java new file mode 100644 index 0000000000..385a8a6d43 --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronSessionTokenStore.java @@ -0,0 +1,202 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.elytron; + +import java.util.function.Consumer; + +import javax.security.auth.callback.CallbackHandler; + +import org.jboss.logging.Logger; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.AdapterUtils; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.OidcKeycloakAccount; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; +import org.wildfly.security.http.HttpScope; +import org.wildfly.security.http.HttpScopeNotification; +import org.wildfly.security.http.Scope; + +/** + * @author Pedro Igor + */ +public class ElytronSessionTokenStore implements ElytronTokeStore { + + private static Logger log = Logger.getLogger(ElytronSessionTokenStore.class); + + private final ElytronHttpFacade httpFacade; + private final CallbackHandler callbackHandler; + + public ElytronSessionTokenStore(ElytronHttpFacade httpFacade, CallbackHandler callbackHandler) { + this.httpFacade = httpFacade; + this.callbackHandler = callbackHandler; + } + + @Override + public void checkCurrentToken() { + HttpScope session = httpFacade.getScope(Scope.SESSION); + if (!session.exists()) return; + RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) session.getAttachment(KeycloakSecurityContext.class.getName()); + if (securityContext == null) return; + + // just in case session got serialized + if (securityContext.getDeployment() == null) securityContext.setCurrentRequestInfo(httpFacade.getDeployment(), this); + + if (securityContext.isActive() && !securityContext.getDeployment().isAlwaysRefreshToken()) return; + + // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will + // not be updated + boolean success = securityContext.refreshExpiredToken(false); + if (success && securityContext.isActive()) return; + + // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session + session.setAttachment(KeycloakSecurityContext.class.getName(), null); + session.invalidate(); + } + + @Override + public boolean isCached(RequestAuthenticator authenticator) { + HttpScope session = this.httpFacade.getScope(Scope.SESSION); + + if (session == null) { + log.debug("session was null, returning null"); + return false; + } + + ElytronAccount account; + + try { + account = (ElytronAccount) session.getAttachment(ElytronAccount.class.getName()); + } catch (IllegalStateException e) { + log.debug("session was invalidated. Return false."); + return false; + } + if (account == null) { + log.debug("Account was not in session, returning null"); + return false; + } + + KeycloakDeployment deployment = httpFacade.getDeployment(); + + if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) { + log.debug("Account in session belongs to a different realm than for this request."); + return false; + } + + boolean active = account.checkActive(); + + if (!active) { + active = account.tryRefresh(this.callbackHandler); + } + + if (active) { + log.debug("Cached account found"); + restoreRequest(); + httpFacade.authenticationComplete(account, true); + return true; + } else { + log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session"); + try { + session.setAttachment(KeycloakSecurityContext.class.getName(), null); + session.setAttachment(ElytronAccount.class.getName(), null); + session.invalidate(); + } catch (Exception e) { + log.debug("Failed to invalidate session, might already be invalidated"); + } + return false; + } + } + + @Override + public void saveAccountInfo(OidcKeycloakAccount account) { + HttpScope session = this.httpFacade.getScope(Scope.SESSION); + + if (!session.exists()) { + session.create(); + } + + session.setAttachment(ElytronAccount.class.getName(), account); + session.setAttachment(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); + + session.registerForNotification(httpScopeNotification -> { + if (!httpScopeNotification.isOfType(HttpScopeNotification.SessionNotificationType.UNDEPLOY)) { + logout(); + } + }); + + HttpScope scope = this.httpFacade.getScope(Scope.EXCHANGE); + + scope.setAttachment(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext()); + } + + @Override + public void logout() { + logout(false); + } + + @Override + public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { + KeycloakPrincipal principal = new KeycloakPrincipal(AdapterUtils.getPrincipalName(this.httpFacade.getDeployment(), securityContext.getToken()), securityContext); + saveAccountInfo(new ElytronAccount(principal)); + } + + @Override + public void saveRequest() { + this.httpFacade.suspendRequest(); + } + + @Override + public boolean restoreRequest() { + return this.httpFacade.restoreRequest(); + } + + @Override + public void logout(boolean glo) { + HttpScope session = this.httpFacade.getScope(Scope.SESSION); + + if (!session.exists()) { + return; + } + + try { + if (glo) { + KeycloakSecurityContext ksc = (KeycloakSecurityContext) session.getAttachment(KeycloakSecurityContext.class.getName()); + + if (ksc == null) { + return; + } + + KeycloakDeployment deployment = httpFacade.getDeployment(); + + if (!deployment.isBearerOnly() && ksc != null && ksc instanceof RefreshableKeycloakSecurityContext) { + ((RefreshableKeycloakSecurityContext) ksc).logout(deployment); + } + } + + session.setAttachment(KeycloakSecurityContext.class.getName(), null); + session.setAttachment(ElytronAccount.class.getName(), null); + session.invalidate(); + } catch (IllegalStateException ise) { + // Session may be already logged-out in case that app has adminUrl + log.debugf("Session %s logged-out already", session.getID()); + } + } +} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronTokeStore.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronTokeStore.java new file mode 100644 index 0000000000..dc1486eb85 --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronTokeStore.java @@ -0,0 +1,26 @@ +/* + * 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.elytron; + +import org.keycloak.adapters.AdapterTokenStore; + +/** + * @author Pedro Igor + */ +public interface ElytronTokeStore extends AdapterTokenStore { + void logout(boolean glo); +} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakConfigurationServletListener.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakConfigurationServletListener.java new file mode 100644 index 0000000000..ad8e9d5215 --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakConfigurationServletListener.java @@ -0,0 +1,109 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.elytron; + +import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.KeycloakConfigResolver; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.KeycloakDeploymentBuilder; +import org.keycloak.constants.AdapterConstants; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +/** + *

A {@link ServletContextListener} that parses the keycloak adapter configuration and set the same configuration + * as a {@link ServletContext} attribute in order to provide to {@link KeycloakHttpServerAuthenticationMechanism} a way + * to obtain the configuration when processing requests. + * + *

This listener should be automatically registered to a deployment using the subsystem. + * + * @author Pedro Igor + */ +public class KeycloakConfigurationServletListener implements ServletContextListener { + @Override + public void contextInitialized(ServletContextEvent sce) { + ServletContext servletContext = sce.getServletContext(); + String configResolverClass = servletContext.getInitParameter("keycloak.config.resolver"); + KeycloakConfigResolver configResolver; + AdapterDeploymentContext deploymentContext; + + if (configResolverClass != null) { + try { + configResolver = (KeycloakConfigResolver) servletContext.getClassLoader().loadClass(configResolverClass).newInstance(); + deploymentContext = new AdapterDeploymentContext(configResolver); + } catch (Exception ex) { + deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); + } + } else { + InputStream is = getConfigInputStream(servletContext); + + KeycloakDeployment deployment; + + if (is == null) { + deployment = new KeycloakDeployment(); + } else { + deployment = KeycloakDeploymentBuilder.build(is); + } + + deploymentContext = new AdapterDeploymentContext(deployment); + } + + servletContext.setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + + } + + private InputStream getConfigInputStream(ServletContext servletContext) { + InputStream is = getJSONFromServletContext(servletContext); + + if (is == null) { + String path = servletContext.getInitParameter("keycloak.config.file"); + + if (path == null) { + is = servletContext.getResourceAsStream("/WEB-INF/keycloak.json"); + } else { + try { + is = new FileInputStream(path); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + } + return is; + } + + private InputStream getJSONFromServletContext(ServletContext servletContext) { + String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME); + + if (json == null) { + return null; + } + + return new ByteArrayInputStream(json.getBytes()); + } +} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java new file mode 100644 index 0000000000..3fcf9bf484 --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanism.java @@ -0,0 +1,168 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.elytron; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import javax.security.auth.callback.CallbackHandler; + +import org.jboss.logging.Logger; +import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.AuthenticatedActionsHandler; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.NodesRegistrationManagement; +import org.keycloak.adapters.PreAuthActionsHandler; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; +import org.keycloak.adapters.spi.AuthChallenge; +import org.keycloak.adapters.spi.AuthOutcome; +import org.keycloak.adapters.spi.UserSessionManagement; +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpScope; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerRequest; +import org.wildfly.security.http.Scope; + +/** + * @author Pedro Igor + */ +class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticationMechanism { + + static Logger LOGGER = Logger.getLogger(KeycloakHttpServerAuthenticationMechanismFactory.class); + static final String NAME = "KEYCLOAK"; + + private final Map properties; + private final CallbackHandler callbackHandler; + private final AdapterDeploymentContext deploymentContext; + + public KeycloakHttpServerAuthenticationMechanism(Map properties, CallbackHandler callbackHandler, AdapterDeploymentContext deploymentContext) { + this.properties = properties; + this.callbackHandler = callbackHandler; + this.deploymentContext = deploymentContext; + } + + @Override + public String getMechanismName() { + return NAME; + } + + @Override + public void evaluateRequest(HttpServerRequest request) throws HttpAuthenticationException { + LOGGER.debugf("Evaluating request for path [%s]", request.getRequestURI()); + AdapterDeploymentContext deploymentContext = getDeploymentContext(request); + + if (deploymentContext == null) { + LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI()); + request.noAuthenticationInProgress(); + return; + } + + ElytronHttpFacade httpFacade = new ElytronHttpFacade(request, deploymentContext, callbackHandler); + KeycloakDeployment deployment = httpFacade.getDeployment(); + + if (!deployment.isConfigured()) { + request.noAuthenticationInProgress(); + return; + } + + RequestAuthenticator authenticator = createRequestAuthenticator(request, httpFacade, deployment); + + httpFacade.getTokenStore().checkCurrentToken(); + + if (preActions(httpFacade, deploymentContext)) { + LOGGER.debugf("Pre-actions has aborted the evaluation of [%s]", request.getRequestURI()); + httpFacade.authenticationInProgress(); + return; + } + + AuthOutcome outcome = authenticator.authenticate(); + + if (AuthOutcome.AUTHENTICATED.equals(outcome)) { + if (new AuthenticatedActionsHandler(deployment, httpFacade).handledRequest()) { + httpFacade.authenticationInProgress(); + } else { + httpFacade.authenticationComplete(); + } + return; + } + + AuthChallenge challenge = authenticator.getChallenge(); + + if (challenge != null) { + httpFacade.noAuthenticationInProgress(challenge); + return; + } + + if (AuthOutcome.FAILED.equals(outcome)) { + httpFacade.getResponse().setStatus(403); + httpFacade.authenticationFailed(); + return; + } + + httpFacade.noAuthenticationInProgress(); + } + + private ElytronRequestAuthenticator createRequestAuthenticator(HttpServerRequest request, ElytronHttpFacade httpFacade, KeycloakDeployment deployment) { + return new ElytronRequestAuthenticator(this.callbackHandler, httpFacade, deployment, getConfidentialPort(request)); + } + + private AdapterDeploymentContext getDeploymentContext(HttpServerRequest request) { + if (this.deploymentContext == null) { + return (AdapterDeploymentContext) request.getScope(Scope.APPLICATION).getAttachment(AdapterDeploymentContext.class.getName()); + } + + return this.deploymentContext; + } + + private boolean preActions(ElytronHttpFacade httpFacade, AdapterDeploymentContext deploymentContext) { + NodesRegistrationManagement nodesRegistrationManagement = new NodesRegistrationManagement(); + + nodesRegistrationManagement.tryRegister(httpFacade.getDeployment()); + + PreAuthActionsHandler preActions = new PreAuthActionsHandler(new UserSessionManagement() { + @Override + public void logoutAll() { + Collection sessions = httpFacade.getScopeIds(Scope.SESSION); + logoutHttpSessions(new ArrayList<>(sessions)); + } + + @Override + public void logoutHttpSessions(List ids) { + for (String id : ids) { + HttpScope session = httpFacade.getScope(Scope.SESSION, id); + + if (session != null) { + session.invalidate(); + } + } + + } + }, deploymentContext, httpFacade); + + return preActions.handleRequest(); + } + + // TODO: obtain confidential port from Elytron + private int getConfidentialPort(HttpServerRequest request) { + return 8443; + } +} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java new file mode 100644 index 0000000000..eb6b333310 --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java @@ -0,0 +1,67 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.elytron; + +import org.keycloak.adapters.AdapterDeploymentContext; +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; + +import javax.security.auth.callback.CallbackHandler; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Pedro Igor + */ +public class KeycloakHttpServerAuthenticationMechanismFactory implements HttpServerAuthenticationMechanismFactory { + + private final AdapterDeploymentContext deploymentContext; + + /** + *

Creates a new instance. + * + *

A default constructor is necessary in order to allow this factory to be loaded via {@link java.util.ServiceLoader}. + */ + public KeycloakHttpServerAuthenticationMechanismFactory() { + this(null); + } + + public KeycloakHttpServerAuthenticationMechanismFactory(AdapterDeploymentContext deploymentContext) { + this.deploymentContext = deploymentContext; + } + + @Override + public String[] getMechanismNames(Map properties) { + return new String[] {KeycloakHttpServerAuthenticationMechanism.NAME}; + } + + @Override + public HttpServerAuthenticationMechanism createAuthenticationMechanism(String mechanismName, Map properties, CallbackHandler callbackHandler) throws HttpAuthenticationException { + Map mechanismProperties = new HashMap(); + + mechanismProperties.putAll(properties); + + if (KeycloakHttpServerAuthenticationMechanism.NAME.equals(mechanismName)) { + return new KeycloakHttpServerAuthenticationMechanism(properties, callbackHandler, this.deploymentContext); + } + + return null; + } +} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakSecurityRealm.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakSecurityRealm.java new file mode 100644 index 0000000000..6042ec82d1 --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/KeycloakSecurityRealm.java @@ -0,0 +1,103 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.adapters.elytron; + +import java.security.Principal; +import java.util.Set; + +import org.keycloak.KeycloakPrincipal; +import org.keycloak.adapters.AdapterUtils; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.wildfly.security.auth.SupportLevel; +import org.wildfly.security.auth.server.RealmIdentity; +import org.wildfly.security.auth.server.RealmUnavailableException; +import org.wildfly.security.auth.server.SecurityRealm; +import org.wildfly.security.authz.Attributes; +import org.wildfly.security.authz.AuthorizationIdentity; +import org.wildfly.security.authz.MapAttributes; +import org.wildfly.security.authz.RoleDecoder; +import org.wildfly.security.credential.Credential; +import org.wildfly.security.evidence.Evidence; + +/** + * @author Pedro Igor + */ +public class KeycloakSecurityRealm implements SecurityRealm { + + @Override + public RealmIdentity getRealmIdentity(Principal principal) throws RealmUnavailableException { + if (principal instanceof KeycloakPrincipal) { + return createRealmIdentity((KeycloakPrincipal) principal); + } + return RealmIdentity.NON_EXISTENT; + } + + private RealmIdentity createRealmIdentity(KeycloakPrincipal principal) { + return new RealmIdentity() { + @Override + public Principal getRealmIdentityPrincipal() { + return principal; + } + + @Override + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName) throws RealmUnavailableException { + return SupportLevel.UNSUPPORTED; + } + + @Override + public C getCredential(Class credentialType) throws RealmUnavailableException { + return null; + } + + @Override + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) throws RealmUnavailableException { + return SupportLevel.SUPPORTED; + } + + @Override + public boolean verifyEvidence(Evidence evidence) throws RealmUnavailableException { + return principal != null; + } + + @Override + public boolean exists() throws RealmUnavailableException { + return principal != null; + } + + @Override + public AuthorizationIdentity getAuthorizationIdentity() throws RealmUnavailableException { + RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) principal.getKeycloakSecurityContext(); + Attributes attributes = new MapAttributes(); + Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); + + attributes.addAll(RoleDecoder.KEY_ROLES, roles); + + return AuthorizationIdentity.basicIdentity(attributes); + } + }; + } + + @Override + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName) throws RealmUnavailableException { + return SupportLevel.UNSUPPORTED; + } + + @Override + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) throws RealmUnavailableException { + return SupportLevel.POSSIBLY_SUPPORTED; + } +} diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/SecurityIdentityUtil.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/SecurityIdentityUtil.java new file mode 100644 index 0000000000..28f6eb9094 --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/SecurityIdentityUtil.java @@ -0,0 +1,82 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.elytron; + +import java.io.IOException; +import java.security.Principal; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.AuthorizeCallback; + +import org.keycloak.KeycloakPrincipal; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; +import org.wildfly.security.auth.callback.EvidenceVerifyCallback; +import org.wildfly.security.auth.callback.SecurityIdentityCallback; +import org.wildfly.security.auth.server.SecurityIdentity; +import org.wildfly.security.evidence.Evidence; +import org.wildfly.security.http.HttpAuthenticationException; + +/** + * @author Pedro Igor + */ +final class SecurityIdentityUtil { + + static final SecurityIdentity authorize(CallbackHandler callbackHandler, Principal principal) { + try { + EvidenceVerifyCallback evidenceVerifyCallback = new EvidenceVerifyCallback(new Evidence() { + @Override + public Principal getPrincipal() { + return principal; + } + }); + + callbackHandler.handle(new Callback[]{evidenceVerifyCallback}); + + if (evidenceVerifyCallback.isVerified()) { + AuthorizeCallback authorizeCallback = new AuthorizeCallback(null, null); + + try { + callbackHandler.handle(new Callback[] {authorizeCallback}); + + authorizeCallback.isAuthorized(); + } catch (Exception e) { + throw new HttpAuthenticationException(e); + } + + SecurityIdentityCallback securityIdentityCallback = new SecurityIdentityCallback(); + + callbackHandler.handle(new Callback[]{AuthenticationCompleteCallback.SUCCEEDED, securityIdentityCallback}); + + SecurityIdentity securityIdentity = securityIdentityCallback.getSecurityIdentity(); + + return securityIdentity; + } + } catch (UnsupportedCallbackException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return null; + } + +} diff --git a/adapters/oidc/wildfly-elytron/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory b/adapters/oidc/wildfly-elytron/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory new file mode 100644 index 0000000000..96a0441f32 --- /dev/null +++ b/adapters/oidc/wildfly-elytron/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory @@ -0,0 +1,19 @@ +# +# JBoss, Home of Professional Open Source. +# Copyright 2016 Red Hat, Inc., and individual 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.elytron.KeycloakHttpServerAuthenticationMechanismFactory \ No newline at end of file diff --git a/adapters/oidc/wildfly/wildfly-adapter/pom.xml b/adapters/oidc/wildfly/wildfly-adapter/pom.xml old mode 100755 new mode 100644 diff --git a/adapters/oidc/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/SecurityInfoHelper.java b/adapters/oidc/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/SecurityInfoHelper.java old mode 100755 new mode 100644 diff --git a/adapters/oidc/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyAuthenticationMechanism.java b/adapters/oidc/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyAuthenticationMechanism.java old mode 100755 new mode 100644 diff --git a/adapters/oidc/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyKeycloakServletExtension.java b/adapters/oidc/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyKeycloakServletExtension.java old mode 100755 new mode 100644 diff --git a/adapters/oidc/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyRequestAuthenticator.java b/adapters/oidc/wildfly/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyRequestAuthenticator.java old mode 100755 new mode 100644 diff --git a/adapters/oidc/wildfly/wildfly-adapter/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension b/adapters/oidc/wildfly/wildfly-adapter/src/main/resources/META-INF/services/io.undertow.servlet.ServletExtension old mode 100755 new mode 100644 diff --git a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java index f9a1e77c29..280c3fe459 100755 --- a/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java +++ b/adapters/saml/core-public/src/main/java/org/keycloak/adapters/saml/SamlPrincipal.java @@ -24,6 +24,7 @@ import java.io.Serializable; import java.security.Principal; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -31,6 +32,9 @@ import java.util.Set; * @version $Revision: 1 $ */ public class SamlPrincipal implements Serializable, Principal { + + public static final String DEFAULT_ROLE_ATTRIBUTE_NAME = "Roles"; + private MultivaluedHashMap attributes = new MultivaluedHashMap<>(); private MultivaluedHashMap friendlyAttributes = new MultivaluedHashMap<>(); private String name; @@ -98,6 +102,15 @@ public class SamlPrincipal implements Serializable, Principal { } + /** + * Convenience function that gets the attributes associated with this principal + * + * @return attributes associated with this principal + */ + public Map> getAttributes() { + return Collections.unmodifiableMap(attributes); + } + /** * Convenience function that gets Attribute value by attribute friendly name * diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java index cb9b4d9fd7..550eeeb616 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java @@ -17,6 +17,8 @@ package org.keycloak.adapters.saml.profile; +import static org.keycloak.adapters.saml.SamlPrincipal.DEFAULT_ROLE_ATTRIBUTE_NAME; + import org.jboss.logging.Logger; import org.keycloak.adapters.saml.AbstractInitiateLogin; import org.keycloak.adapters.saml.OnSessionCreated; @@ -422,6 +424,11 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic } } } + + // roles should also be there as regular attributes + // this mainly required for elytron and its ABAC nature + attributes.put(DEFAULT_ROLE_ATTRIBUTE_NAME, new ArrayList<>(roles)); + if (deployment.getPrincipalNamePolicy() == SamlDeployment.PrincipalNamePolicy.FROM_ATTRIBUTE) { if (deployment.getPrincipalAttributeName() != null) { String attribute = attributes.getFirst(deployment.getPrincipalAttributeName()); diff --git a/adapters/saml/pom.xml b/adapters/saml/pom.xml index 614646d37a..7ca4c17a05 100755 --- a/adapters/saml/pom.xml +++ b/adapters/saml/pom.xml @@ -39,5 +39,6 @@ wildfly as7-eap6 servlet-filter + wildfly-elytron diff --git a/adapters/saml/wildfly-elytron/pom.xml b/adapters/saml/wildfly-elytron/pom.xml new file mode 100755 index 0000000000..51af38019b --- /dev/null +++ b/adapters/saml/wildfly-elytron/pom.xml @@ -0,0 +1,102 @@ + + + + + + keycloak-parent + org.keycloak + 3.1.0.CR1-SNAPSHOT + ../../../pom.xml + + 4.0.0 + + keycloak-saml-wildfly-elytron-adapter + Keycloak WildFly Elytron SAML Adapter + + + + 1.8 + 1.8 + + + + + org.keycloak + keycloak-adapter-core + provided + + + org.keycloak + keycloak-saml-core + provided + + + org.keycloak + keycloak-adapter-spi + provided + + + org.keycloak + keycloak-common + provided + + + org.keycloak + keycloak-saml-adapter-api-public + provided + + + org.keycloak + keycloak-saml-adapter-core + provided + + + org.jboss.logging + jboss-logging + provided + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + provided + + + org.wildfly.security + wildfly-elytron + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java new file mode 100644 index 0000000000..88e96f8bd3 --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronHttpFacade.java @@ -0,0 +1,377 @@ +/* + * 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.saml.elytron; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.cert.X509Certificate; + +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlDeploymentContext; +import org.keycloak.adapters.saml.SamlSession; +import org.keycloak.adapters.saml.SamlSessionStore; +import org.keycloak.adapters.spi.AuthChallenge; +import org.keycloak.adapters.spi.AuthenticationError; +import org.keycloak.adapters.spi.HttpFacade; +import org.keycloak.adapters.spi.LogoutError; +import org.keycloak.adapters.spi.SessionIdMapper; +import org.wildfly.security.auth.callback.AnonymousAuthorizationCallback; +import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; +import org.wildfly.security.auth.callback.SecurityIdentityCallback; +import org.wildfly.security.auth.server.SecurityIdentity; +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpScope; +import org.wildfly.security.http.HttpServerCookie; +import org.wildfly.security.http.HttpServerMechanismsResponder; +import org.wildfly.security.http.HttpServerRequest; +import org.wildfly.security.http.HttpServerResponse; +import org.wildfly.security.http.Scope; + +/** + * @author Pedro Igor + */ +class ElytronHttpFacade implements HttpFacade { + + private final HttpServerRequest request; + private final CallbackHandler callbackHandler; + private final SamlDeploymentContext deploymentContext; + private final SamlSessionStore sessionStore; + private Consumer responseConsumer; + private SecurityIdentity securityIdentity; + private boolean restored; + private SamlSession samlSession; + + public ElytronHttpFacade(HttpServerRequest request, SessionIdMapper idMapper, SamlDeploymentContext deploymentContext, CallbackHandler handler) { + this.request = request; + this.deploymentContext = deploymentContext; + this.callbackHandler = handler; + this.responseConsumer = response -> {}; + this.sessionStore = createTokenStore(idMapper); + } + + private SamlSessionStore createTokenStore(SessionIdMapper idMapper) { + return new ElytronSamlSessionStore(this, idMapper, getDeployment()); + } + + void authenticationComplete(SamlSession samlSession) { + this.samlSession = samlSession; + } + + void authenticationComplete() { + this.securityIdentity = SecurityIdentityUtil.authorize(this.callbackHandler, samlSession.getPrincipal()); + this.request.authenticationComplete(response -> { + if (!restored) { + responseConsumer.accept(response); + } + }, () -> ((ElytronTokeStore) sessionStore).logout(true)); + } + + void authenticationCompleteAnonymous() { + try { + AnonymousAuthorizationCallback anonymousAuthorizationCallback = new AnonymousAuthorizationCallback(null); + + callbackHandler.handle(new Callback[]{anonymousAuthorizationCallback}); + + if (anonymousAuthorizationCallback.isAuthorized()) { + callbackHandler.handle(new Callback[]{AuthenticationCompleteCallback.SUCCEEDED, new SecurityIdentityCallback()}); + } + + request.authenticationComplete(response -> response.forward(getRequest().getRelativePath())); + } catch (Exception e) { + throw new RuntimeException("Unexpected error processing callbacks during logout.", e); + } + } + + void authenticationFailed() { + this.request.authenticationFailed("Authentication Failed", response -> responseConsumer.accept(response)); + } + + void noAuthenticationInProgress(AuthChallenge challenge) { + if (challenge != null) { + challenge.challenge(this); + } + this.request.noAuthenticationInProgress(response -> responseConsumer.accept(response)); + } + + void authenticationInProgress() { + this.request.authenticationInProgress(response -> responseConsumer.accept(response)); + } + + HttpScope getScope(Scope scope) { + return request.getScope(scope); + } + + HttpScope getScope(Scope scope, String id) { + return request.getScope(scope, id); + } + + Collection getScopeIds(Scope scope) { + return request.getScopeIds(scope); + } + + SamlDeployment getDeployment() { + return deploymentContext.resolveDeployment(this); + } + + @Override + public Request getRequest() { + return new Request() { + @Override + public String getMethod() { + return request.getRequestMethod(); + } + + @Override + public String getURI() { + try { + return URLDecoder.decode(request.getRequestURI().toString(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Failed to decode request URI", e); + } + } + + @Override + public String getRelativePath() { + return request.getRequestPath(); + } + + @Override + public boolean isSecure() { + return request.getRequestURI().getScheme().equals("https"); + } + + @Override + public String getFirstParam(String param) { + return request.getFirstParameterValue(param); + } + + @Override + public String getQueryParamValue(String param) { + return request.getFirstParameterValue(param); + } + + @Override + public Cookie getCookie(final String cookieName) { + List cookies = request.getCookies(); + + if (cookies != null) { + for (HttpServerCookie cookie : cookies) { + if (cookie.getName().equals(cookieName)) { + return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath()); + } + } + } + + return null; + } + + @Override + public String getHeader(String name) { + return request.getFirstRequestHeaderValue(name); + } + + @Override + public List getHeaders(String name) { + return request.getRequestHeaderValues(name); + } + + @Override + public InputStream getInputStream() { + return request.getInputStream(); + } + + @Override + public String getRemoteAddr() { + InetSocketAddress sourceAddress = request.getSourceAddress(); + if (sourceAddress == null) { + return ""; + } + InetAddress address = sourceAddress.getAddress(); + if (address == null) { + // this is unresolved, so we just return the host name not exactly spec, but if the name should be + // resolved then a PeerNameResolvingHandler should be used and this is probably better than just + // returning null + return sourceAddress.getHostString(); + } + return address.getHostAddress(); + } + + @Override + public void setError(AuthenticationError error) { + request.getScope(Scope.EXCHANGE).setAttachment(AuthenticationError.class.getName(), error); + } + + @Override + public void setError(LogoutError error) { + request.getScope(Scope.EXCHANGE).setAttachment(LogoutError.class.getName(), error); + } + }; + } + + @Override + public Response getResponse() { + return new Response() { + @Override + public void setStatus(final int status) { + responseConsumer = responseConsumer.andThen(response -> response.setStatusCode(status)); + } + + @Override + public void addHeader(final String name, final String value) { + responseConsumer = responseConsumer.andThen(response -> response.addResponseHeader(name, value)); + } + + @Override + public void setHeader(String name, String value) { + addHeader(name, value); + } + + @Override + public void resetCookie(final String name, final String path) { + responseConsumer = responseConsumer.andThen(response -> setCookie(name, "", path, null, 0, false, false, response)); + } + + @Override + public void setCookie(final String name, final String value, final String path, final String domain, final int maxAge, final boolean secure, final boolean httpOnly) { + responseConsumer = responseConsumer.andThen(response -> setCookie(name, value, path, domain, maxAge, secure, httpOnly, response)); + } + + private void setCookie(final String name, final String value, final String path, final String domain, final int maxAge, final boolean secure, final boolean httpOnly, HttpServerResponse response) { + response.setResponseCookie(new HttpServerCookie() { + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public String getDomain() { + return domain; + } + + @Override + public int getMaxAge() { + return maxAge; + } + + @Override + public String getPath() { + return path; + } + + @Override + public boolean isSecure() { + return secure; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public boolean isHttpOnly() { + return httpOnly; + } + }); + } + + @Override + public OutputStream getOutputStream() { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + responseConsumer = responseConsumer.andThen(new Consumer() { + @Override + public void accept(HttpServerResponse httpServerResponse) { + try { + httpServerResponse.getOutputStream().write(stream.toByteArray()); + } catch (IOException e) { + throw new RuntimeException("Failed to write to response output stream", e); + } + } + }); + return stream; + } + + @Override + public void sendError(int code) { + setStatus(code); + } + + @Override + public void sendError(final int code, final String message) { + responseConsumer = responseConsumer.andThen(response -> { + response.setStatusCode(code); + response.addResponseHeader("Content-Type", "text/html"); + try { + response.getOutputStream().write(message.getBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public void end() { + + } + }; + } + + @Override + public X509Certificate[] getCertificateChain() { + return new X509Certificate[0]; + } + + public boolean restoreRequest() { + restored = this.request.resumeRequest(); + return restored; + } + + public void suspendRequest() { + responseConsumer = responseConsumer.andThen(httpServerResponse -> request.suspendRequest()); + } + + public boolean isAuthorized() { + return this.securityIdentity != null; + } + + public URI getURI() { + return request.getRequestURI(); + } + + public SamlSessionStore getSessionStore() { + return sessionStore; + } +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlAuthenticator.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlAuthenticator.java new file mode 100644 index 0000000000..29975edc9c --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlAuthenticator.java @@ -0,0 +1,51 @@ +/* + * 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.saml.elytron; + +import javax.security.auth.callback.CallbackHandler; + +import org.keycloak.adapters.saml.SamlAuthenticator; +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlSession; +import org.keycloak.adapters.saml.SamlSessionStore; +import org.keycloak.adapters.saml.profile.SamlAuthenticationHandler; +import org.keycloak.adapters.saml.profile.webbrowsersso.BrowserHandler; +import org.keycloak.adapters.spi.HttpFacade; + +/** + * @author Pedro Igor + */ +public class ElytronSamlAuthenticator extends SamlAuthenticator { + private final CallbackHandler callbackHandler; + private final ElytronHttpFacade facade; + + public ElytronSamlAuthenticator(ElytronHttpFacade facade, SamlDeployment samlDeployment, CallbackHandler callbackHandler) { + super(facade, samlDeployment, facade.getSessionStore()); + this.callbackHandler = callbackHandler; + this.facade = facade; + } + + @Override + protected void completeAuthentication(SamlSession samlSession) { + facade.authenticationComplete(samlSession); + } + + @Override + protected SamlAuthenticationHandler createBrowserHandler(HttpFacade facade, SamlDeployment deployment, SamlSessionStore sessionStore) { + return new BrowserHandler(facade, deployment, sessionStore); + } +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlEndpoint.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlEndpoint.java new file mode 100644 index 0000000000..17997e5273 --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlEndpoint.java @@ -0,0 +1,39 @@ +/* + * 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.saml.elytron; + +import org.keycloak.adapters.saml.SamlAuthenticator; +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlSession; + +/** + * @author Pedro Igor + */ +public class ElytronSamlEndpoint extends SamlAuthenticator { + + private final ElytronHttpFacade facade; + + public ElytronSamlEndpoint(ElytronHttpFacade facade, SamlDeployment samlDeployment) { + super(facade, samlDeployment, facade.getSessionStore()); + this.facade = facade; + } + + @Override + protected void completeAuthentication(SamlSession samlSession) { + facade.authenticationComplete(samlSession); + } +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java new file mode 100644 index 0000000000..2ce62928df --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronSamlSessionStore.java @@ -0,0 +1,226 @@ +/* + * 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.saml.elytron; + +import java.net.URI; +import java.security.Principal; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.jboss.logging.Logger; +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlSession; +import org.keycloak.adapters.saml.SamlSessionStore; +import org.keycloak.adapters.saml.SamlUtil; +import org.keycloak.adapters.spi.SessionIdMapper; +import org.keycloak.common.util.KeycloakUriBuilder; +import org.wildfly.security.http.HttpScope; +import org.wildfly.security.http.Scope; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ElytronSamlSessionStore implements SamlSessionStore, ElytronTokeStore { + protected static Logger log = Logger.getLogger(SamlSessionStore.class); + public static final String SAML_REDIRECT_URI = "SAML_REDIRECT_URI"; + + private final SessionIdMapper idMapper; + protected final SamlDeployment deployment; + private final ElytronHttpFacade exchange; + + + public ElytronSamlSessionStore(ElytronHttpFacade exchange, SessionIdMapper idMapper, SamlDeployment deployment) { + this.exchange = exchange; + this.idMapper = idMapper; + this.deployment = deployment; + } + + @Override + public void setCurrentAction(CurrentAction action) { + if (action == CurrentAction.NONE && !exchange.getScope(Scope.SESSION).exists()) return; + exchange.getScope(Scope.SESSION).setAttachment(CURRENT_ACTION, action); + } + + @Override + public boolean isLoggingIn() { + HttpScope session = exchange.getScope(Scope.SESSION); + if (!session.exists()) return false; + CurrentAction action = (CurrentAction) session.getAttachment(CURRENT_ACTION); + return action == CurrentAction.LOGGING_IN; + } + + @Override + public boolean isLoggingOut() { + HttpScope session = exchange.getScope(Scope.SESSION); + if (!session.exists()) return false; + CurrentAction action = (CurrentAction) session.getAttachment(CURRENT_ACTION); + return action == CurrentAction.LOGGING_OUT; + } + + @Override + public void logoutAccount() { + HttpScope session = getSession(false); + if (session.exists()) { + SamlSession samlSession = (SamlSession)session.getAttachment(SamlSession.class.getName()); + if (samlSession != null) { + if (samlSession.getSessionIndex() != null) { + idMapper.removeSession(session.getID()); + } + session.setAttachment(SamlSession.class.getName(), null); + } + session.setAttachment(SAML_REDIRECT_URI, null); + } + } + + @Override + public void logoutByPrincipal(String principal) { + Set sessions = idMapper.getUserSessions(principal); + if (sessions != null) { + List ids = new LinkedList<>(); + ids.addAll(sessions); + logoutSessionIds(ids); + for (String id : ids) { + idMapper.removeSession(id); + } + } + + } + + @Override + public void logoutBySsoId(List ssoIds) { + if (ssoIds == null) return; + List sessionIds = new LinkedList<>(); + for (String id : ssoIds) { + String sessionId = idMapper.getSessionFromSSO(id); + if (sessionId != null) { + sessionIds.add(sessionId); + idMapper.removeSession(sessionId); + } + + } + logoutSessionIds(sessionIds); + } + + protected void logoutSessionIds(List sessionIds) { + sessionIds.forEach(id -> { + HttpScope scope = exchange.getScope(Scope.SESSION, id); + + if (scope.exists()) { + scope.invalidate(); + } + }); + } + + @Override + public boolean isLoggedIn() { + HttpScope session = getSession(false); + if (!session.exists()) { + log.debug("session was null, returning null"); + return false; + } + final SamlSession samlSession = (SamlSession)session.getAttachment(SamlSession.class.getName()); + if (samlSession == null) { + log.debug("SamlSession was not in session, returning null"); + return false; + } + + exchange.authenticationComplete(samlSession); + restoreRequest(); + return true; + } + + @Override + public void saveAccount(SamlSession account) { + HttpScope session = getSession(true); + session.setAttachment(SamlSession.class.getName(), account); + String sessionId = changeSessionId(session); + idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId); + + } + + protected String changeSessionId(HttpScope session) { + if (!deployment.turnOffChangeSessionIdOnLogin()) return session.getID(); + else return session.getID(); + } + + @Override + public SamlSession getAccount() { + HttpScope session = getSession(true); + return (SamlSession)session.getAttachment(SamlSession.class.getName()); + } + + @Override + public String getRedirectUri() { + HttpScope session = exchange.getScope(Scope.SESSION); + String redirect = (String) session.getAttachment(SAML_REDIRECT_URI); + if (redirect == null) { + URI uri = exchange.getURI(); + String path = uri.getPath(); + String relativePath = exchange.getRequest().getRelativePath(); + String contextPath = path.substring(0, path.indexOf(relativePath)); + + if (!contextPath.isEmpty()) { + contextPath = contextPath + "/"; + } + + String baseUri = KeycloakUriBuilder.fromUri(path).replacePath(contextPath).build().toString(); + return SamlUtil.getRedirectTo(exchange, contextPath, baseUri); + } + return redirect; + } + + @Override + public void saveRequest() { + exchange.suspendRequest(); + HttpScope scope = exchange.getScope(Scope.SESSION); + + if (!scope.exists()) { + scope.create(); + } + + KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(exchange.getURI()).replaceQuery(exchange.getURI().getQuery()); + String uri = uriBuilder.build().toString(); + + scope.setAttachment(SAML_REDIRECT_URI, uri); + } + + @Override + public boolean restoreRequest() { + return exchange.restoreRequest(); + } + + protected HttpScope getSession(boolean create) { + HttpScope scope = exchange.getScope(Scope.SESSION); + + if (!scope.exists() && create) { + scope.create(); + } + + return scope; + } + + @Override + public void logout(boolean glo) { + logoutAccount(); + } +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronTokeStore.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronTokeStore.java new file mode 100644 index 0000000000..a658464d3a --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/ElytronTokeStore.java @@ -0,0 +1,24 @@ +/* + * 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.saml.elytron; + +/** + * @author Pedro Igor + */ +public interface ElytronTokeStore { + void logout(boolean glo); +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java new file mode 100644 index 0000000000..94ae5920d3 --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakConfigurationServletListener.java @@ -0,0 +1,121 @@ +/* + * 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.saml.elytron; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.jboss.logging.Logger; +import org.keycloak.adapters.saml.AdapterConstants; +import org.keycloak.adapters.saml.DefaultSamlDeployment; +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlDeploymentContext; +import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; +import org.keycloak.adapters.saml.config.parsers.ResourceLoader; +import org.keycloak.saml.common.exceptions.ParsingException; + +/** + *

A {@link ServletContextListener} that parses the keycloak adapter configuration and set the same configuration + * as a {@link ServletContext} attribute in order to provide to {@link KeycloakHttpServerAuthenticationMechanism} a way + * to obtain the configuration when processing requests. + * + *

This listener should be automatically registered to a deployment using the subsystem. + * + * @author Pedro Igor + */ +public class KeycloakConfigurationServletListener implements ServletContextListener { + + protected static Logger log = Logger.getLogger(KeycloakConfigurationServletListener.class); + + @Override + public void contextInitialized(ServletContextEvent sce) { + ServletContext servletContext = sce.getServletContext(); + String configResolverClass = servletContext.getInitParameter("keycloak.config.resolver"); + SamlDeploymentContext deploymentContext = null; + if (configResolverClass != null) { + try { + throw new RuntimeException("Not implemented yet"); + //configResolver = (SamlConfigResolver) deploymentInfo.getClassLoader().loadClass(configResolverClass).newInstance(); + //deploymentContext = new AdapterDeploymentContext(configResolver); + //log.info("Using " + configResolverClass + " to resolve Keycloak configuration on a per-request basis."); + } catch (Exception ex) { + log.warn("The specified resolver " + configResolverClass + " could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: " + ex.getMessage()); + //deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); + } + } else { + InputStream is = getConfigInputStream(servletContext); + final SamlDeployment deployment; + if (is == null) { + log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests."); + deployment = new DefaultSamlDeployment(); + } else { + try { + ResourceLoader loader = new ResourceLoader() { + @Override + public InputStream getResourceAsStream(String resource) { + return servletContext.getResourceAsStream(resource); + } + }; + deployment = new DeploymentBuilder().build(is, loader); + } catch (ParsingException e) { + throw new RuntimeException(e); + } + } + deploymentContext = new SamlDeploymentContext(deployment); + servletContext.setAttribute(SamlDeploymentContext.class.getName(), deploymentContext); + log.debug("Keycloak is using a per-deployment configuration."); + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + + } + + private static InputStream getConfigInputStream(ServletContext context) { + InputStream is = getXMLFromServletContext(context); + if (is == null) { + String path = context.getInitParameter("keycloak.config.file"); + if (path == null) { + log.debug("using /WEB-INF/keycloak-saml.xml"); + is = context.getResourceAsStream("/WEB-INF/keycloak-saml.xml"); + } else { + try { + is = new FileInputStream(path); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + } + return is; + } + + private static InputStream getXMLFromServletContext(ServletContext servletContext) { + String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME); + if (json == null) { + return null; + } + return new ByteArrayInputStream(json.getBytes()); + } +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java new file mode 100644 index 0000000000..9fce501d93 --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanism.java @@ -0,0 +1,155 @@ +/* + * 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.saml.elytron; + +import java.net.URI; +import java.util.Map; + +import javax.security.auth.callback.CallbackHandler; + +import org.jboss.logging.Logger; +import org.keycloak.adapters.saml.SamlAuthenticator; +import org.keycloak.adapters.saml.SamlDeployment; +import org.keycloak.adapters.saml.SamlDeploymentContext; +import org.keycloak.adapters.spi.AuthChallenge; +import org.keycloak.adapters.spi.AuthOutcome; +import org.keycloak.adapters.spi.SessionIdMapper; +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerRequest; +import org.wildfly.security.http.Scope; + +/** + * @author Pedro Igor + */ +class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticationMechanism { + + static Logger LOGGER = Logger.getLogger(KeycloakHttpServerAuthenticationMechanismFactory.class); + static final String NAME = "KEYCLOAK-SAML"; + + private final Map properties; + private final CallbackHandler callbackHandler; + private final SamlDeploymentContext deploymentContext; + private final SessionIdMapper idMapper; + + public KeycloakHttpServerAuthenticationMechanism(Map properties, CallbackHandler callbackHandler, SamlDeploymentContext deploymentContext, SessionIdMapper idMapper) { + this.properties = properties; + this.callbackHandler = callbackHandler; + this.deploymentContext = deploymentContext; + this.idMapper = idMapper; + } + + @Override + public String getMechanismName() { + return NAME; + } + + @Override + public void evaluateRequest(HttpServerRequest request) throws HttpAuthenticationException { + LOGGER.debugf("Evaluating request for path [%s]", request.getRequestURI()); + SamlDeploymentContext deploymentContext = getDeploymentContext(request); + + if (deploymentContext == null) { + LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI()); + request.noAuthenticationInProgress(); + return; + } + + ElytronHttpFacade httpFacade = new ElytronHttpFacade(request, idMapper, deploymentContext, callbackHandler); + SamlDeployment deployment = httpFacade.getDeployment(); + + if (!deployment.isConfigured()) { + request.noAuthenticationInProgress(); + return; + } + + if (httpFacade.getRequest().getRelativePath().contains(deployment.getLogoutPage())) { + LOGGER.debugf("Ignoring request for [%s] and logout page [%s].", request.getRequestURI(), deployment.getLogoutPage()); + httpFacade.authenticationCompleteAnonymous(); + return; + } + + SamlAuthenticator authenticator; + + if (httpFacade.getRequest().getRelativePath().endsWith("/saml")) { + authenticator = new ElytronSamlEndpoint(httpFacade, deployment); + } else { + authenticator = new ElytronSamlAuthenticator(httpFacade, deployment, callbackHandler); + + } + + AuthOutcome outcome = authenticator.authenticate(); + + if (outcome == AuthOutcome.AUTHENTICATED) { + httpFacade.authenticationComplete(); + return; + } + + if (outcome == AuthOutcome.NOT_AUTHENTICATED) { + httpFacade.noAuthenticationInProgress(null); + return; + } + + if (outcome == AuthOutcome.LOGGED_OUT) { + if (deployment.getLogoutPage() != null) { + redirectLogout(deployment, httpFacade); + } + httpFacade.authenticationInProgress(); + return; + } + + AuthChallenge challenge = authenticator.getChallenge(); + + if (challenge != null) { + httpFacade.noAuthenticationInProgress(challenge); + return; + } + + if (outcome == AuthOutcome.FAILED) { + httpFacade.authenticationFailed(); + return; + } + + httpFacade.authenticationInProgress(); + } + + private SamlDeploymentContext getDeploymentContext(HttpServerRequest request) { + if (this.deploymentContext == null) { + return (SamlDeploymentContext) request.getScope(Scope.APPLICATION).getAttachment(SamlDeploymentContext.class.getName()); + } + + return this.deploymentContext; + } + + protected void redirectLogout(SamlDeployment deployment, ElytronHttpFacade exchange) { + String page = deployment.getLogoutPage(); + sendRedirect(exchange, page); + exchange.getResponse().setStatus(302); + } + + static void sendRedirect(final ElytronHttpFacade exchange, final String location) { + // TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better + // handle this. + URI uri = exchange.getURI(); + String path = uri.getPath(); + String relativePath = exchange.getRequest().getRelativePath(); + String contextPath = path.substring(0, path.indexOf(relativePath)); + String loc = exchange.getURI().getScheme() + "://" + exchange.getURI().getHost() + ":" + exchange.getURI().getPort() + contextPath + location; + exchange.getResponse().setHeader("Location", loc); + } +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java new file mode 100644 index 0000000000..c1b69a4435 --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakHttpServerAuthenticationMechanismFactory.java @@ -0,0 +1,70 @@ +/* + * 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.saml.elytron; + +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.callback.CallbackHandler; + +import org.keycloak.adapters.saml.SamlDeploymentContext; +import org.keycloak.adapters.spi.InMemorySessionIdMapper; +import org.keycloak.adapters.spi.SessionIdMapper; +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; + +/** + * @author Pedro Igor + */ +public class KeycloakHttpServerAuthenticationMechanismFactory implements HttpServerAuthenticationMechanismFactory { + + private SessionIdMapper idMapper = new InMemorySessionIdMapper(); + private final SamlDeploymentContext deploymentContext; + + /** + *

Creates a new instance. + * + *

A default constructor is necessary in order to allow this factory to be loaded via {@link java.util.ServiceLoader}. + */ + public KeycloakHttpServerAuthenticationMechanismFactory() { + this(null); + } + + public KeycloakHttpServerAuthenticationMechanismFactory(SamlDeploymentContext deploymentContext) { + this.deploymentContext = deploymentContext; + } + + @Override + public String[] getMechanismNames(Map properties) { + return new String[] {KeycloakHttpServerAuthenticationMechanism.NAME}; + } + + @Override + public HttpServerAuthenticationMechanism createAuthenticationMechanism(String mechanismName, Map properties, CallbackHandler callbackHandler) throws HttpAuthenticationException { + Map mechanismProperties = new HashMap(); + + mechanismProperties.putAll(properties); + + if (KeycloakHttpServerAuthenticationMechanism.NAME.equals(mechanismName)) { + return new KeycloakHttpServerAuthenticationMechanism(properties, callbackHandler, this.deploymentContext, idMapper); + } + + return null; + } +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakSecurityRealm.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakSecurityRealm.java new file mode 100644 index 0000000000..3207835360 --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/KeycloakSecurityRealm.java @@ -0,0 +1,109 @@ +/* + * 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.saml.elytron; + +import java.security.Principal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.keycloak.adapters.saml.SamlPrincipal; +import org.wildfly.security.auth.SupportLevel; +import org.wildfly.security.auth.server.RealmIdentity; +import org.wildfly.security.auth.server.RealmUnavailableException; +import org.wildfly.security.auth.server.SecurityRealm; +import org.wildfly.security.authz.AuthorizationIdentity; +import org.wildfly.security.authz.MapAttributes; +import org.wildfly.security.credential.Credential; +import org.wildfly.security.evidence.BearerTokenEvidence; +import org.wildfly.security.evidence.Evidence; + +/** + * @author Pedro Igor + */ +public class KeycloakSecurityRealm implements SecurityRealm { + + @Override + public RealmIdentity getRealmIdentity(Principal principal) throws RealmUnavailableException { + if (principal instanceof SamlPrincipal) { + return createRealmIdentity((SamlPrincipal) principal); + } + return RealmIdentity.NON_EXISTENT; + } + + private RealmIdentity createRealmIdentity(SamlPrincipal principal) { + return new RealmIdentity() { + @Override + public Principal getRealmIdentityPrincipal() { + return principal; + } + + @Override + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName) throws RealmUnavailableException { + return SupportLevel.UNSUPPORTED; + } + + @Override + public C getCredential(Class credentialType) throws RealmUnavailableException { + return null; + } + + @Override + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) throws RealmUnavailableException { + if (isBearerTokenEvidence(evidenceType)) { + return SupportLevel.SUPPORTED; + } + + return SupportLevel.UNSUPPORTED; + } + + @Override + public boolean verifyEvidence(Evidence evidence) throws RealmUnavailableException { + return principal != null; + } + + @Override + public boolean exists() throws RealmUnavailableException { + return principal != null; + } + + @Override + public AuthorizationIdentity getAuthorizationIdentity() throws RealmUnavailableException { + Map> attributes = new HashMap<>(principal.getAttributes()); + return AuthorizationIdentity.basicIdentity(new MapAttributes(attributes)); + } + }; + } + + @Override + public SupportLevel getCredentialAcquireSupport(Class credentialType, String algorithmName) throws RealmUnavailableException { + return SupportLevel.UNSUPPORTED; + } + + @Override + public SupportLevel getEvidenceVerifySupport(Class evidenceType, String algorithmName) throws RealmUnavailableException { + if (isBearerTokenEvidence(evidenceType)) { + return SupportLevel.POSSIBLY_SUPPORTED; + } + + return SupportLevel.UNSUPPORTED; + } + + private boolean isBearerTokenEvidence(Class evidenceType) { + return evidenceType != null && evidenceType.equals(BearerTokenEvidence.class); + } +} diff --git a/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/SecurityIdentityUtil.java b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/SecurityIdentityUtil.java new file mode 100644 index 0000000000..ce45db651b --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/java/org/keycloak/adapters/saml/elytron/SecurityIdentityUtil.java @@ -0,0 +1,80 @@ +/* + * 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.saml.elytron; + +import java.io.IOException; +import java.security.Principal; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.AuthorizeCallback; + +import org.keycloak.adapters.saml.SamlPrincipal; +import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; +import org.wildfly.security.auth.callback.EvidenceVerifyCallback; +import org.wildfly.security.auth.callback.SecurityIdentityCallback; +import org.wildfly.security.auth.server.SecurityIdentity; +import org.wildfly.security.evidence.Evidence; +import org.wildfly.security.http.HttpAuthenticationException; + +/** + * @author Pedro Igor + */ +final class SecurityIdentityUtil { + + static final SecurityIdentity authorize(CallbackHandler callbackHandler, SamlPrincipal principal) { + try { + EvidenceVerifyCallback evidenceVerifyCallback = new EvidenceVerifyCallback(new Evidence() { + @Override + public Principal getPrincipal() { + return principal; + } + }); + + callbackHandler.handle(new Callback[]{evidenceVerifyCallback}); + + if (evidenceVerifyCallback.isVerified()) { + AuthorizeCallback authorizeCallback = new AuthorizeCallback(null, null); + + try { + callbackHandler.handle(new Callback[] {authorizeCallback}); + } catch (Exception e) { + throw new HttpAuthenticationException(e); + } + + if (authorizeCallback.isAuthorized()) { + SecurityIdentityCallback securityIdentityCallback = new SecurityIdentityCallback(); + + callbackHandler.handle(new Callback[]{AuthenticationCompleteCallback.SUCCEEDED, securityIdentityCallback}); + + SecurityIdentity securityIdentity = securityIdentityCallback.getSecurityIdentity(); + + return securityIdentity; + } + } + } catch (UnsupportedCallbackException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return null; + } + +} diff --git a/adapters/saml/wildfly-elytron/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory b/adapters/saml/wildfly-elytron/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory new file mode 100644 index 0000000000..a41c127bd9 --- /dev/null +++ b/adapters/saml/wildfly-elytron/src/main/resources/META-INF/services/org.wildfly.security.http.HttpServerAuthenticationMechanismFactory @@ -0,0 +1,19 @@ +# +# JBoss, Home of Professional Open Source. +# Copyright 2016 Red Hat, Inc., and individual 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.saml.elytron.KeycloakHttpServerAuthenticationMechanismFactory \ No newline at end of file diff --git a/distribution/adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml b/distribution/adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml index ece320bd02..527750ff23 100755 --- a/distribution/adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml +++ b/distribution/adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml @@ -35,6 +35,7 @@ org/keycloak/keycloak-jboss-adapter-core/** org/keycloak/keycloak-undertow-adapter/** org/keycloak/keycloak-wildfly-adapter/** + org/keycloak/keycloak-wildfly-elytron-oidc-adapter/** org/keycloak/keycloak-wildfly-subsystem/** org/keycloak/keycloak-adapter-subsystem/** org/keycloak/keycloak-servlet-oauth-client/** diff --git a/distribution/adapters/wildfly-adapter/wildfly-modules/build.xml b/distribution/adapters/wildfly-adapter/wildfly-modules/build.xml index a534b4f9a9..8e608a59ee 100755 --- a/distribution/adapters/wildfly-adapter/wildfly-modules/build.xml +++ b/distribution/adapters/wildfly-adapter/wildfly-modules/build.xml @@ -77,6 +77,10 @@ + + + + diff --git a/distribution/adapters/wildfly-adapter/wildfly-modules/pom.xml b/distribution/adapters/wildfly-adapter/wildfly-modules/pom.xml index b5a3d5ee53..00eef5800a 100755 --- a/distribution/adapters/wildfly-adapter/wildfly-modules/pom.xml +++ b/distribution/adapters/wildfly-adapter/wildfly-modules/pom.xml @@ -66,6 +66,10 @@ org.keycloak keycloak-wildfly-adapter + + org.keycloak + keycloak-wildfly-elytron-oidc-adapter + org.keycloak keycloak-wildfly-subsystem diff --git a/distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-wildfly-elytron-oidc-adapter/main/module.xml b/distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-wildfly-elytron-oidc-adapter/main/module.xml new file mode 100755 index 0000000000..1ca98391a2 --- /dev/null +++ b/distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-wildfly-elytron-oidc-adapter/main/module.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml index b91b2dc594..ff57870dc9 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml @@ -37,6 +37,7 @@ org/keycloak/keycloak-jboss-adapter-core/** org/keycloak/keycloak-saml-undertow-adapter/** org/keycloak/keycloak-saml-wildfly-adapter/** + org/keycloak/keycloak-saml-wildfly-elytron-adapter/** org/keycloak/keycloak-saml-wildfly-subsystem/** org/keycloak/keycloak-saml-adapter-subsystem/** diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/build.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/build.xml index abbe9d3b65..885ed811a4 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/build.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/build.xml @@ -76,6 +76,10 @@ + + + + diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml index 06f0dedc8a..026d8696f1 100755 --- a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/pom.xml @@ -70,6 +70,10 @@ org.keycloak keycloak-saml-wildfly-adapter + + org.keycloak + keycloak-saml-wildfly-elytron-adapter + org.keycloak keycloak-saml-wildfly-subsystem diff --git a/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-elytron-adapter/main/module.xml b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-elytron-adapter/main/module.xml new file mode 100755 index 0000000000..393eac929c --- /dev/null +++ b/distribution/saml-adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-saml-wildfly-elytron-adapter/main/module.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/elytron/standalone-elytron.xml b/elytron/standalone-elytron.xml new file mode 100644 index 0000000000..4237b02418 --- /dev/null +++ b/elytron/standalone-elytron.xml @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + h2 + + sa + sa + + + + + org.h2.jdbcx.JdbcDataSource + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${jboss.bind.address:127.0.0.1} + + + + + + + + + + + + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVOcTeth98Fi9T/9GMK9q7w6Wvft1Xc+aMFTru5vcsqh0NL1hYRuSdqxTK0lpbaTfDLF+bh1QP+1ZArjLEshNoddsc39Lf4xDh9smh1xOp/GcFQSDmSz9dQ8FmQUagnNtwIWSXVphGyK5yOznqIzrV/TNHuGvUA5MsPNkm99LrQlODLYr6hsE/kPoKMybi8z/tYkLJXtXS8ZM5O/2rOrPNcqvw58Fb1pJ0OXO59zK96qw/eqRnPPbi3N0FRQLKCG51DpQu6xe8zKHwEtUXDGdtgSceA6jKynmAG/dWrBEARczgAPbUlEIq3HByrtB1DHR0cZKUYVj5PwkGEg6IgXhwIDAQAB + http://localhost:8180/auth + none + + + wildfly + wildfly-cli + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/elytron/standalone-oauth2-sasl-with-elytron-only.xml b/elytron/standalone-oauth2-sasl-with-elytron-only.xml new file mode 100644 index 0000000000..dde9d8b29c --- /dev/null +++ b/elytron/standalone-oauth2-sasl-with-elytron-only.xml @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + h2 + + sa + sa + + + + + org.h2.jdbcx.JdbcDataSource + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${jboss.bind.address:127.0.0.1} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/elytron/wildfly-config.xml b/elytron/wildfly-config.xml new file mode 100644 index 0000000000..7fcf544323 --- /dev/null +++ b/elytron/wildfly-config.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index e545c8e17b..deb92abef2 100755 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,8 @@ 7.0.0.Beta 7.2.0.Final 10.0.0.Final + 1.1.0.Beta32 + 1.0.0.Beta14 0.66.12 4.5 @@ -621,6 +623,16 @@ wildfly-web-common ${wildfly.version} + + org.wildfly.security + wildfly-elytron + ${version.org.wildfly.security.wildfly-elytron} + + + org.wildfly.security.elytron-web + undertow-server + ${version.org.wildfly.security.elytron-web.undertow-server} + org.infinispan infinispan-core @@ -921,6 +933,16 @@ keycloak-wildfly-adapter ${project.version} + + org.keycloak + keycloak-wildfly-elytron-oidc-adapter + ${project.version} + + + org.keycloak + keycloak-saml-wildfly-elytron-adapter + ${project.version} + org.keycloak keycloak-wildfly-adduser diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/common/configure-elytron.xsl b/testsuite/integration-arquillian/servers/app-server/jboss/common/configure-elytron.xsl new file mode 100644 index 0000000000..96edcfecf6 --- /dev/null +++ b/testsuite/integration-arquillian/servers/app-server/jboss/common/configure-elytron.xsl @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml b/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml index 658fb39123..90a3952f13 100644 --- a/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml +++ b/testsuite/integration-arquillian/servers/app-server/jboss/pom.xml @@ -444,6 +444,45 @@ eap6-fuse + + + app-server-wildfly-elytron + + false + + + ${elytron.wildfly.version} + + + + + org.codehaus.mojo + xml-maven-plugin + + + configure-adapter-debug-log + process-test-resources + + transform + + + + +

${app.server.jboss.home}/standalone/configuration + + standalone.xml + + ${common.resources}/configure-elytron.xsl + ${app.server.jboss.home}/standalone/configuration + + + + + + + + +