refactor adapters complete

This commit is contained in:
Bill Burke 2014-03-10 18:52:57 -04:00
parent 63bc2d6dbb
commit 0284d040dc
23 changed files with 656 additions and 708 deletions

View file

@ -0,0 +1,108 @@
package org.keycloak.adapters;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.representations.AccessToken;
import java.io.IOException;
import java.util.Set;
/**
* Pre-installed actions that must be authenticated
*
* Actions include:
*
* CORS Origin Check and Response headers
* k_query_bearer_token: Get bearer token from server for Javascripts CORS requests
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AuthenticatedActionsHandler {
private static final Logger log = Logger.getLogger(AuthenticatedActionsHandler.class);
protected KeycloakDeployment deployment;
protected HttpFacade facade;
public AuthenticatedActionsHandler(KeycloakDeployment deployment, HttpFacade facade) {
this.deployment = deployment;
this.facade = facade;
}
public boolean handledRequest() {
log.debugv("AuthenticatedActionsValve.invoke {0}", facade.getRequest().getURI());
if (corsRequest()) return true;
String requestUri = facade.getRequest().getURI();
if (requestUri.endsWith(AdapterConstants.K_QUERY_BEARER_TOKEN)) {
queryBearerToken();
return true;
}
return false;
}
protected void queryBearerToken() {
log.debugv("queryBearerToken {0}",facade.getRequest().getURI());
if (abortTokenResponse()) return;
facade.getResponse().setStatus(200);
facade.getResponse().setHeader("Content-Type", "text/plain");
try {
facade.getResponse().getOutputStream().write(facade.getSecurityContext().getTokenString().getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
facade.getResponse().end();
}
protected boolean abortTokenResponse() {
if (facade.getSecurityContext() == null) {
log.debugv("Not logged in, sending back 401: {0}",facade.getRequest().getURI());
facade.getResponse().setStatus(401);
facade.getResponse().end();
return true;
}
if (!deployment.isExposeToken()) {
facade.getResponse().setStatus(200);
facade.getResponse().end();
return true;
}
// Don't allow a CORS request if we're not validating CORS requests.
if (!deployment.isCors() && facade.getRequest().getHeader(CorsHeaders.ORIGIN) != null) {
facade.getResponse().setStatus(200);
facade.getResponse().end();
return true;
}
return false;
}
protected boolean corsRequest() {
if (!deployment.isCors()) return false;
KeycloakSecurityContext securityContext = facade.getSecurityContext();
String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN);
log.debugv("Origin: {0} uri: {1}", origin, facade.getRequest().getURI());
if (securityContext != null && origin != null) {
AccessToken token = securityContext.getToken();
Set<String> allowedOrigins = token.getAllowedOrigins();
if (log.isDebugEnabled()) {
for (String a : allowedOrigins) log.debug(" " + a);
}
if (allowedOrigins == null || (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin))) {
if (allowedOrigins == null) {
log.debugv("allowedOrigins was null in token");
}
if (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin)) {
log.debugv("allowedOrigins did not contain origin");
}
facade.getResponse().setStatus(403);
facade.getResponse().end();
return true;
}
log.debugv("returning origin: {0}", origin);
facade.getResponse().setStatus(200);
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
} else {
log.debugv("cors validation not needed as we're not a secure session or origin header was null: {0}", facade.getRequest().getURI());
}
return false;
}
}

View file

@ -0,0 +1,16 @@
package org.keycloak.adapters;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface CorsHeaders {
String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
String ORIGIN = "Origin";
String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
}

View file

@ -1,6 +1,10 @@
package org.keycloak.adapters;
import org.keycloak.KeycloakSecurityContext;
import javax.security.cert.X509Certificate;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
/**
@ -48,6 +52,8 @@ public interface HttpFacade {
}
interface Request {
String getMethod();
/**
* Full request URI with query params
*
@ -64,7 +70,9 @@ public interface HttpFacade {
String getQueryParamValue(String param);
Cookie getCookie(String cookieName);
String getHeader(String name);
List<String> getHeaders(String name);
InputStream getInputStream();
}
interface Response {
@ -73,6 +81,8 @@ public interface HttpFacade {
void setHeader(String name, String value);
void resetCookie(String name, String path);
void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly);
OutputStream getOutputStream();
void sendError(int code, String message);
/**
* If the response is finished, end it.
@ -81,6 +91,7 @@ public interface HttpFacade {
void end();
}
KeycloakSecurityContext getSecurityContext();
Request getRequest();
Response getResponse();
X509Certificate[] getCertificateChain();

View file

@ -0,0 +1,227 @@
package org.keycloak.adapters;
import org.jboss.logging.Logger;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.adapters.action.AdminAction;
import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.representations.adapters.action.SessionStats;
import org.keycloak.representations.adapters.action.SessionStatsAction;
import org.keycloak.representations.adapters.action.UserStats;
import org.keycloak.representations.adapters.action.UserStatsAction;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.StreamUtil;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class PreAuthActionsHandler {
private static final Logger log = Logger.getLogger(PreAuthActionsHandler.class);
protected UserSessionManagement userSessionManagement;
protected KeycloakDeployment deployment;
protected HttpFacade facade;
public PreAuthActionsHandler(UserSessionManagement userSessionManagement, KeycloakDeployment deployment, HttpFacade facade) {
this.userSessionManagement = userSessionManagement;
this.deployment = deployment;
this.facade = facade;
}
public boolean handleRequest() {
String requestUri = facade.getRequest().getURI();
log.debugv("adminRequest {0}", requestUri);
if (preflightCors()) {
return true;
}
if (requestUri.endsWith(AdapterConstants.K_LOGOUT)) {
handleLogout();
return true;
} else if (requestUri.endsWith(AdapterConstants.K_PUSH_NOT_BEFORE)) {
handlePushNotBefore();
return true;
} else if (requestUri.endsWith(AdapterConstants.K_GET_SESSION_STATS)) {
handleGetSessionStats();
return true;
}else if (requestUri.endsWith(AdapterConstants.K_GET_USER_STATS)) {
handleGetUserStats();
return true;
}
return false;
}
public boolean preflightCors() {
log.debugv("checkCorsPreflight {0}", facade.getRequest().getURI());
if (!facade.getRequest().getMethod().equalsIgnoreCase("OPTIONS")) {
return false;
}
if (facade.getRequest().getHeader(CorsHeaders.ORIGIN) == null) {
log.debug("checkCorsPreflight: no origin header");
return false;
}
log.debug("Preflight request returning");
facade.getResponse().setStatus(200);
String origin = facade.getRequest().getHeader(CorsHeaders.ORIGIN);
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
String requestMethods = facade.getRequest().getHeader(CorsHeaders.ACCESS_CONTROL_REQUEST_METHOD);
if (requestMethods != null) {
if (deployment.getCorsAllowedMethods() != null) {
requestMethods = deployment.getCorsAllowedMethods();
}
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethods);
}
String allowHeaders = facade.getRequest().getHeader(CorsHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
if (allowHeaders != null) {
if (deployment.getCorsAllowedHeaders() != null) {
allowHeaders = deployment.getCorsAllowedHeaders();
}
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_ALLOW_HEADERS, allowHeaders);
}
if (deployment.getCorsMaxAge() > -1) {
facade.getResponse().setHeader(CorsHeaders.ACCESS_CONTROL_MAX_AGE, Integer.toString(deployment.getCorsMaxAge()));
}
return true;
}
protected void handleLogout() {
log.info("K_LOGOUT sent");
try {
JWSInput token = verifyAdminRequest();
if (token == null) {
return;
}
LogoutAction action = JsonSerialization.readValue(token.getContent(), LogoutAction.class);
if (!validateAction(action)) return;
String user = action.getUser();
if (user != null) {
log.info("logout of session for: " + user);
userSessionManagement.logout(user);
} else {
log.info("logout of all sessions");
userSessionManagement.logoutAll();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected void handlePushNotBefore() {
log.info("K_PUSH_NOT_BEFORE sent");
try {
JWSInput token = verifyAdminRequest();
if (token == null) {
return;
}
PushNotBeforeAction action = JsonSerialization.readValue(token.getContent(), PushNotBeforeAction.class);
if (!validateAction(action)) return;
deployment.setNotBefore(action.getNotBefore());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected JWSInput verifyAdminRequest() throws Exception {
String token = StreamUtil.readString(facade.getRequest().getInputStream());
if (token == null) {
log.warn("admin request failed, no token");
facade.getResponse().sendError(403, "no token");
return null;
}
JWSInput input = new JWSInput(token);
boolean verified = false;
try {
verified = RSAProvider.verify(input, deployment.getRealmKey());
} catch (Exception ignore) {
}
if (!verified) {
log.warn("admin request failed, unable to verify token");
facade.getResponse().sendError(403, "no token");
return null;
}
return input;
}
protected boolean validateAction(AdminAction action) {
if (!action.validate()) {
log.warn("admin request failed, not validated" + action.getAction());
facade.getResponse().sendError(400, "Not validated");
return false;
}
if (action.isExpired()) {
log.warn("admin request failed, expired token");
facade.getResponse().sendError(400, "Expired token");
return false;
}
if (!deployment.getResourceName().equals(action.getResource())) {
log.warn("Resource name does not match");
facade.getResponse().sendError(400, "Resource name does not match");
return false;
}
return true;
}
protected void handleGetSessionStats() {
log.info("K_GET_SESSION_STATS sent");
try {
JWSInput token = verifyAdminRequest();
if (token == null) return;
SessionStatsAction action = JsonSerialization.readValue(token.getContent(), SessionStatsAction.class);
if (!validateAction(action)) return;
SessionStats stats = new SessionStats();
stats.setActiveSessions(userSessionManagement.getActiveSessions());
stats.setActiveUsers(userSessionManagement.getActiveUsers().size());
if (action.isListUsers() && userSessionManagement.getActiveSessions() > 0) {
Map<String, UserStats> list = new HashMap<String, UserStats>();
for (String user : userSessionManagement.getActiveUsers()) {
list.put(user, getUserStats(user));
}
stats.setUsers(list);
}
facade.getResponse().setStatus(200);
facade.getResponse().setHeader("Content-Type", "application/json");
JsonSerialization.writeValueToStream(facade.getResponse().getOutputStream(), stats);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected void handleGetUserStats() {
log.info("K_GET_USER_STATS sent");
try {
JWSInput token = verifyAdminRequest();
if (token == null) return;
UserStatsAction action = JsonSerialization.readValue(token.getContent(), UserStatsAction.class);
if (!validateAction(action)) return;
String user = action.getUser();
UserStats stats = getUserStats(user);
facade.getResponse().setStatus(200);
facade.getResponse().setHeader("Content-Type", "application/json");
JsonSerialization.writeValueToStream(facade.getResponse().getOutputStream(), stats);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected UserStats getUserStats(String user) {
UserStats stats = new UserStats();
Long loginTime = userSessionManagement.getUserLoginTime(user);
if (loginTime != null) {
stats.setLoggedIn(true);
stats.setWhenLoggedIn(loginTime);
} else {
stats.setLoggedIn(false);
}
return stats;
}
}

View file

@ -0,0 +1,19 @@
package org.keycloak.adapters;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface UserSessionManagement {
int getActiveSessions();
Long getUserLoginTime(String username);
Set<String> getActiveUsers();
void logoutAll();
void logout(String user);
}

View file

@ -9,6 +9,7 @@ import org.apache.catalina.valves.ValveBase;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.AuthenticatedActionsHandler;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.AdapterConfig;
@ -46,81 +47,10 @@ public class AuthenticatedActionsValve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
log.debugv("AuthenticatedActionsValve.invoke {0}", request.getRequestURI());
KeycloakSecurityContext session = getSkeletonKeySession(request);
if (corsRequest(request, response, session)) return;
String requestUri = request.getRequestURI();
if (requestUri.endsWith(AdapterConstants.K_QUERY_BEARER_TOKEN)) {
queryBearerToken(request, response, session);
AuthenticatedActionsHandler handler = new AuthenticatedActionsHandler(deployment, new CatalinaHttpFacade(request, response));
if (handler.handledRequest()) {
return;
}
getNext().invoke(request, response);
}
public KeycloakSecurityContext getSkeletonKeySession(Request request) {
KeycloakSecurityContext skSession = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
if (skSession != null) return skSession;
Session session = request.getSessionInternal();
if (session != null) {
return (KeycloakSecurityContext) session.getNote(KeycloakSecurityContext.class.getName());
}
return null;
}
protected void queryBearerToken(Request request, Response response, KeycloakSecurityContext session) throws IOException, ServletException {
log.debugv("queryBearerToken {0}", request.getRequestURI());
if (abortTokenResponse(request, response, session)) return;
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/plain");
response.getOutputStream().write(session.getTokenString().getBytes());
response.getOutputStream().flush();
}
protected boolean abortTokenResponse(Request request, Response response, KeycloakSecurityContext session) throws IOException {
if (session == null) {
log.debugv("session was null, sending back 401: {0}", request.getRequestURI());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return true;
}
if (!deployment.isExposeToken()) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
if (!deployment.isCors() && request.getHeader("Origin") != null) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
return false;
}
protected boolean corsRequest(Request request, Response response, KeycloakSecurityContext session) throws IOException {
if (!deployment.isCors()) return false;
log.debugv("CORS enabled + request.getRequestURI()");
String origin = request.getHeader("Origin");
log.debugv("Origin: {0} uri: {1}", origin, request.getRequestURI());
if (session != null && origin != null) {
AccessToken token = session.getToken();
Set<String> allowedOrigins = token.getAllowedOrigins();
if (log.isDebugEnabled()) {
for (String a : allowedOrigins) log.debug(" " + a);
}
if (allowedOrigins == null || (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin))) {
if (allowedOrigins == null) {
log.debugv("allowedOrigins was null in token");
}
if (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin)) {
log.debugv("allowedOrigins did not contain origin");
}
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return true;
}
log.debugv("returning origin: {0}", origin);
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
} else {
log.debugv("letting through. This is an unathenticated session or origin header was null: {0}", request.getRequestURI());
}
return false;
}
}

View file

@ -2,10 +2,14 @@ package org.keycloak.adapters.as7;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.HttpFacade;
import javax.security.cert.X509Certificate;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
@ -64,6 +68,25 @@ public class CatalinaHttpFacade implements HttpFacade {
}
return list;
}
@Override
public InputStream getInputStream() {
try {
return request.getInputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public String getMethod() {
return request.getMethod();
}
@Override
public String getHeader(String name) {
return request.getHeader(name);
}
}
protected class ResponseFacade implements Response {
@ -100,6 +123,24 @@ public class CatalinaHttpFacade implements HttpFacade {
response.addCookie(cookie);
}
@Override
public OutputStream getOutputStream() {
try {
return response.getOutputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void sendError(int code, String message) {
try {
response.sendError(code, message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void end() {
ended = true;
@ -125,6 +166,11 @@ public class CatalinaHttpFacade implements HttpFacade {
return responseFacade;
}
@Override
public KeycloakSecurityContext getSecurityContext() {
return (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
}
@Override
public X509Certificate[] getCertificateChain() {
throw new IllegalStateException("Not supported yet");

View file

@ -23,11 +23,11 @@ import java.util.Set;
*/
public class CatalinaRequestAuthenticator extends RequestAuthenticator {
protected KeycloakAuthenticatorValve valve;
protected UserSessionManagement userSessionManagement;
protected CatalinaUserSessionManagement userSessionManagement;
protected Request request;
public CatalinaRequestAuthenticator(KeycloakDeployment deployment,
KeycloakAuthenticatorValve valve, UserSessionManagement userSessionManagement,
KeycloakAuthenticatorValve valve, CatalinaUserSessionManagement userSessionManagement,
CatalinaHttpFacade facade,
Request request) {
super(facade, deployment, request.getConnector().getRedirectPort());

View file

@ -5,9 +5,9 @@ import org.apache.catalina.SessionEvent;
import org.apache.catalina.SessionListener;
import org.apache.catalina.realm.GenericPrincipal;
import org.jboss.logging.Logger;
import org.keycloak.adapters.UserSessionManagement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -20,8 +20,8 @@ import java.util.concurrent.ConcurrentHashMap;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UserSessionManagement implements SessionListener {
private static final Logger log = Logger.getLogger(UserSessionManagement.class);
public class CatalinaUserSessionManagement implements SessionListener, UserSessionManagement {
private static final Logger log = Logger.getLogger(CatalinaUserSessionManagement.class);
protected ConcurrentHashMap<String, UserSessions> userSessionMap = new ConcurrentHashMap<String, UserSessions>();
public static class UserSessions {
@ -38,6 +38,7 @@ public class UserSessionManagement implements SessionListener {
}
}
@Override
public int getActiveSessions() {
int active = 0;
synchronized (userSessionMap) {
@ -54,12 +55,14 @@ public class UserSessionManagement implements SessionListener {
* @param username
* @return null if user not logged in
*/
@Override
public Long getUserLoginTime(String username) {
UserSessions sessions = userSessionMap.get(username);
if (sessions == null) return null;
return sessions.getLoggedIn();
}
@Override
public Set<String> getActiveUsers() {
HashSet<String> set = new HashSet<String>();
set.addAll(userSessionMap.keySet());
@ -79,12 +82,14 @@ public class UserSessionManagement implements SessionListener {
session.addSessionListener(this);
}
@Override
public void logoutAll() {
List<String> users = new ArrayList<String>();
users.addAll(userSessionMap.keySet());
for (String user : users) logout(user);
}
@Override
public void logout(String user) {
log.debug("logoutUser: " + user);
UserSessions sessions = null;

View file

@ -5,24 +5,20 @@ import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Session;
import org.apache.catalina.authenticator.Constants;
import org.apache.catalina.authenticator.FormAuthenticator;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.realm.GenericPrincipal;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.action.AdminAction;
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.representations.adapters.action.SessionStats;
@ -35,7 +31,6 @@ import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.StreamUtil;
import javax.security.auth.login.LoginException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@ -46,9 +41,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Web deployment whose security is managed by a remote OAuth Skeleton Key authentication server
@ -61,7 +54,7 @@ import java.util.Set;
*/
public class KeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener {
private static final Logger log = Logger.getLogger(KeycloakAuthenticatorValve.class);
protected UserSessionManagement userSessionManagement = new UserSessionManagement();
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
protected KeycloakDeployment deployment;
@ -115,37 +108,8 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
try {
if (deployment.isCors() && new CorsPreflightChecker(deployment).checkCorsPreflight(request, response)) {
return;
}
String requestURI = request.getDecodedRequestURI();
if (requestURI.endsWith(AdapterConstants.K_LOGOUT)) {
JWSInput input = verifyAdminRequest(request, response);
if (input == null) {
return; // we failed to verify the request
}
remoteLogout(input, response);
return;
} else if (requestURI.endsWith(AdapterConstants.K_PUSH_NOT_BEFORE)) {
JWSInput input = verifyAdminRequest(request, response);
if (input == null) {
return; // we failed to verify the request
}
pushNotBefore(input, response);
return;
} else if (requestURI.endsWith(AdapterConstants.K_GET_SESSION_STATS)) {
JWSInput input = verifyAdminRequest(request, response);
if (input == null) {
return; // we failed to verify the request
}
getSessionStats(input, response);
return;
} else if (requestURI.endsWith(AdapterConstants.K_GET_USER_STATS)) {
JWSInput input = verifyAdminRequest(request, response);
if (input == null) {
return; // we failed to verify the request
}
getUserStats(input, response);
PreAuthActionsHandler handler = new PreAuthActionsHandler(userSessionManagement, deployment, new CatalinaHttpFacade(request, response));
if (handler.handleRequest()) {
return;
}
checkKeycloakSession(request);
@ -172,131 +136,6 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
return false;
}
protected JWSInput verifyAdminRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
String token = StreamUtil.readString(request.getInputStream());
if (token == null) {
log.warn("admin request failed, no token");
response.sendError(403, "no token");
return null;
}
JWSInput input = new JWSInput(token);
boolean verified = false;
try {
verified = RSAProvider.verify(input, deployment.getRealmKey());
} catch (Exception ignore) {
}
if (!verified) {
log.warn("admin request failed, unable to verify token");
response.sendError(403, "verification failed");
return null;
}
return input;
}
protected boolean validateAction(HttpServletResponse response, AdminAction action) throws IOException {
if (!action.validate()) {
log.warn("admin request failed, not validated" + action.getAction());
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Not validated");
return false;
}
if (action.isExpired()) {
log.warn("admin request failed, expired token");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Expired token");
return false;
}
if (!deployment.getResourceName().equals(action.getResource())) {
log.warn("Resource name does not match");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Resource name does not match");
return false;
}
return true;
}
protected void pushNotBefore(JWSInput token, HttpServletResponse response) throws IOException {
log.info("->> pushNotBefore: ");
PushNotBeforeAction action = JsonSerialization.readValue(token.getContent(), PushNotBeforeAction.class);
if (!validateAction(response, action)) {
return;
}
deployment.setNotBefore(action.getNotBefore());
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
protected UserStats getUserStats(String user) {
UserStats stats = new UserStats();
Long loginTime = userSessionManagement.getUserLoginTime(user);
if (loginTime != null) {
stats.setLoggedIn(true);
stats.setWhenLoggedIn(loginTime);
} else {
stats.setLoggedIn(false);
}
return stats;
}
protected void getSessionStats(JWSInput token, HttpServletResponse response) throws IOException {
log.info("->> getSessionStats: ");
SessionStatsAction action = JsonSerialization.readValue(token.getContent(), SessionStatsAction.class);
if (!validateAction(response, action)) {
return;
}
SessionStats stats = new SessionStats();
stats.setActiveSessions(userSessionManagement.getActiveSessions());
stats.setActiveUsers(userSessionManagement.getActiveUsers().size());
if (action.isListUsers() && userSessionManagement.getActiveSessions() > 0) {
Map<String, UserStats> list = new HashMap<String, UserStats>();
for (String user : userSessionManagement.getActiveUsers()) {
list.put(user, getUserStats(user));
}
stats.setUsers(list);
}
response.setStatus(200);
response.setContentType("application/json");
JsonSerialization.writeValueToStream(response.getOutputStream(), stats);
}
protected void getUserStats(JWSInput token, HttpServletResponse response) throws IOException {
log.info("->> getUserStats: ");
UserStatsAction action = JsonSerialization.readValue(token.getContent(), UserStatsAction.class);
if (!validateAction(response, action)) {
return;
}
String user = action.getUser();
UserStats stats = getUserStats(user);
response.setStatus(200);
response.setContentType("application/json");
JsonSerialization.writeValueToStream(response.getOutputStream(), stats);
}
protected void remoteLogout(JWSInput token, HttpServletResponse response) throws IOException {
try {
log.debug("->> remoteLogout: ");
LogoutAction action = JsonSerialization.readValue(token.getContent(), LogoutAction.class);
if (!validateAction(response, action)) {
return;
}
String user = action.getUser();
if (user != null) {
log.debug("logout of session for: " + user);
userSessionManagement.logout(user);
} else {
log.debug("logout of all sessions");
userSessionManagement.logoutAll();
}
} catch (Exception e) {
log.warn("failed to logout", e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to logout");
}
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
/**
* Checks that access token is still valid. Will attempt refresh of token if it is not.
*

View file

@ -1,132 +0,0 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.Headers;
import io.undertow.util.StatusCodes;
import org.jboss.logging.Logger;
import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.AdapterConfig;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Set;
/**
* Pre-installed actions that must be authenticated
*
* Actions include:
*
* CORS Origin Check and Response headers
* k_query_bearer_token: Get bearer token from server for Javascripts CORS requests
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AuthenticatedActionsHandler implements HttpHandler {
private static final Logger log = Logger.getLogger(AuthenticatedActionsHandler.class);
protected KeycloakDeployment deployment;
protected HttpHandler next;
public static class Wrapper implements HandlerWrapper {
protected KeycloakDeployment deployment;
public Wrapper(KeycloakDeployment deployment) {
this.deployment = deployment;
}
@Override
public HttpHandler wrap(HttpHandler handler) {
return new AuthenticatedActionsHandler(deployment, handler);
}
}
protected AuthenticatedActionsHandler(KeycloakDeployment deployment, HttpHandler next) {
this.deployment = deployment;
this.next = next;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
log.debugv("AuthenticatedActionsValve.invoke {0}", exchange.getRequestURI());
KeycloakUndertowAccount account = getAccount(exchange);
if (corsRequest(exchange, account)) return;
String requestUri = exchange.getRequestURI();
if (requestUri.endsWith(AdapterConstants.K_QUERY_BEARER_TOKEN)) {
queryBearerToken(exchange, account);
return;
}
next.handleRequest(exchange);
}
public KeycloakUndertowAccount getAccount(HttpServerExchange exchange) {
return (KeycloakUndertowAccount)exchange.getSecurityContext().getAuthenticatedAccount();
}
protected void queryBearerToken(HttpServerExchange exchange, KeycloakUndertowAccount account) throws IOException, ServletException {
log.debugv("queryBearerToken {0}",exchange.getRequestURI());
if (abortTokenResponse(exchange, account)) return;
exchange.setResponseCode(StatusCodes.OK);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
exchange.getResponseSender().send(account.getEncodedAccessToken());
exchange.endExchange();
}
protected boolean abortTokenResponse(HttpServerExchange exchange, KeycloakUndertowAccount account) throws IOException {
if (account == null) {
log.debugv("Not logged in, sending back 401: {0}",exchange.getRequestURI());
exchange.setResponseCode(StatusCodes.UNAUTHORIZED);
exchange.endExchange();
return true;
}
if (!deployment.isExposeToken()) {
exchange.setResponseCode(StatusCodes.OK);
exchange.endExchange();
return true;
}
// Don't allow a CORS request if we're not validating CORS requests.
if (!deployment.isCors() && exchange.getRequestHeaders().getFirst(Headers.ORIGIN) != null) {
exchange.setResponseCode(StatusCodes.OK);
exchange.endExchange();
return true;
}
return false;
}
protected boolean corsRequest(HttpServerExchange exchange, KeycloakUndertowAccount account) throws IOException {
if (!deployment.isCors()) return false;
log.debugv("CORS enabled + request.getRequestURI()");
String origin = exchange.getRequestHeaders().getFirst("Origin");
log.debugv("Origin: {0} uri: {1}", origin, exchange.getRequestURI());
if (account != null && origin != null) {
AccessToken token = account.getAccessToken();
Set<String> allowedOrigins = token.getAllowedOrigins();
if (log.isDebugEnabled()) {
for (String a : allowedOrigins) log.debug(" " + a);
}
if (allowedOrigins == null || (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin))) {
if (allowedOrigins == null) {
log.debugv("allowedOrigins was null in token");
}
if (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin)) {
log.debugv("allowedOrigins did not contain origin");
}
exchange.setResponseCode(StatusCodes.FORBIDDEN);
exchange.endExchange();
return true;
}
log.debugv("returning origin: {0}", origin);
exchange.setResponseCode(StatusCodes.OK);
exchange.getResponseHeaders().put(PreflightCorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
exchange.getResponseHeaders().put(PreflightCorsHandler.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
} else {
log.debugv("cors validation not needed as we're not a secure session or origin header was null: {0}", exchange.getRequestURI());
}
return false;
}
}

View file

@ -14,6 +14,7 @@ import io.undertow.servlet.api.ServletSessionConfig;
import java.io.ByteArrayInputStream;
import org.jboss.logging.Logger;
import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.AuthenticatedActionsHandler;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
@ -63,16 +64,14 @@ public class KeycloakServletExtension implements ServletExtension {
}
if (is == null) throw new RuntimeException("Unable to find realm config in /WEB-INF/keycloak.json or in keycloak subsystem.");
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(is);
PreflightCorsHandler.Wrapper preflight = new PreflightCorsHandler.Wrapper(deployment);
UserSessionManagement userSessionManagement = new UserSessionManagement(deployment);
UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement(deployment);
final ServletKeycloakAuthMech mech = new ServletKeycloakAuthMech(deployment, userSessionManagement, deploymentInfo.getConfidentialPortManager());
AuthenticatedActionsHandler.Wrapper actions = new AuthenticatedActionsHandler.Wrapper(deployment);
UndertowAuthenticatedActionsHandler.Wrapper actions = new UndertowAuthenticatedActionsHandler.Wrapper(deployment);
// setup handlers
deploymentInfo.addInitialHandlerChainWrapper(preflight); // cors preflight
deploymentInfo.addOuterHandlerChainWrapper(new ServletAdminActionsHandler.Wrapper(deployment, userSessionManagement));
deploymentInfo.addOuterHandlerChainWrapper(new ServletPreAuthActionsHandler.Wrapper(deployment, userSessionManagement));
deploymentInfo.addAuthenticationMechanism("KEYCLOAK", new AuthenticationMechanismFactory() {
@Override
public AuthenticationMechanism create(String s, FormParserFactory formParserFactory, Map<String, String> stringStringMap) {

View file

@ -59,7 +59,7 @@ public class KeycloakUndertowAccount implements Account, Serializable {
return session.getTokenString();
}
public RefreshableKeycloakSecurityContext getSession() {
public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() {
return session;
}

View file

@ -1,82 +0,0 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;
import io.undertow.util.StatusCodes;
import org.jboss.logging.Logger;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.representations.adapters.config.AdapterConfig;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class PreflightCorsHandler implements HttpHandler {
private static final Logger log = Logger.getLogger(PreflightCorsHandler.class);
protected KeycloakDeployment deployment;
protected HttpHandler next;
public static final HttpString ACCESS_CONTROL_ALLOW_ORIGIN = new HttpString("Access-Control-Allow-Origin");
public static final HttpString ACCESS_CONTROL_ALLOW_CREDENTIALS = new HttpString("Access-Control-Allow-Credentials");
public static final HttpString ACCESS_CONTROL_ALLOW_METHODS = new HttpString("Access-Control-Allow-Methods");
public static final HttpString ACCESS_CONTROL_ALLOW_HEADERS = new HttpString("Access-Control-Allow-Headers");
public static final HttpString ACCESS_CONTROL_MAX_AGE = new HttpString("Access-Control-Max-Age");
public static class Wrapper implements HandlerWrapper {
protected KeycloakDeployment deployment;
public Wrapper(KeycloakDeployment deployment) {
this.deployment = deployment;
}
@Override
public HttpHandler wrap(HttpHandler handler) {
return new PreflightCorsHandler(deployment, handler);
}
}
protected PreflightCorsHandler(KeycloakDeployment deployment, HttpHandler next) {
this.deployment = deployment;
this.next = next;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
log.debugv("checkCorsPreflight {0}", exchange.getRequestURI());
if (!exchange.getRequestMethod().toString().equalsIgnoreCase("OPTIONS")) {
log.debug("checkCorsPreflight: not options ");
next.handleRequest(exchange);
return;
}
if (exchange.getRequestHeaders().getFirst("Origin") == null) {
log.debug("checkCorsPreflight: no origin header");
next.handleRequest(exchange);
return;
}
log.debug("Preflight request returning");
exchange.setResponseCode(StatusCodes.OK);
String origin = exchange.getRequestHeaders().getFirst("Origin");
exchange.getResponseHeaders().put(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
exchange.getResponseHeaders().put(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
String requestMethods = exchange.getRequestHeaders().getFirst("Access-Control-Request-Method");
if (requestMethods != null) {
if (deployment.getCorsAllowedMethods() != null) {
requestMethods = deployment.getCorsAllowedMethods();
}
exchange.getResponseHeaders().put(ACCESS_CONTROL_ALLOW_METHODS, requestMethods);
}
String allowHeaders = exchange.getRequestHeaders().getFirst("Access-Control-Request-Headers");
if (allowHeaders != null) {
if (deployment.getCorsAllowedHeaders() != null) {
allowHeaders = deployment.getCorsAllowedHeaders();
}
exchange.getResponseHeaders().put(ACCESS_CONTROL_ALLOW_HEADERS, allowHeaders);
}
if (deployment.getCorsMaxAge() > -1) {
exchange.getResponseHeaders().put(ACCESS_CONTROL_MAX_AGE, Integer.toString(deployment.getCorsMaxAge()));
}
exchange.endExchange();
}
}

View file

@ -1,198 +0,0 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.SessionManager;
import io.undertow.servlet.handlers.ServletRequestContext;
import io.undertow.util.StatusCodes;
import org.jboss.logging.Logger;
import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.adapters.action.AdminAction;
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.representations.adapters.action.SessionStats;
import org.keycloak.representations.adapters.action.SessionStatsAction;
import org.keycloak.representations.adapters.action.UserStats;
import org.keycloak.representations.adapters.action.UserStatsAction;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.StreamUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ServletAdminActionsHandler implements HttpHandler {
private static final Logger log = Logger.getLogger(ServletAdminActionsHandler.class);
protected HttpHandler next;
protected UserSessionManagement userSessionManagement;
protected KeycloakDeployment deployment;
public static class Wrapper implements HandlerWrapper {
protected KeycloakDeployment deployment;
protected UserSessionManagement userSessionManagement;
public Wrapper(KeycloakDeployment deployment, UserSessionManagement userSessionManagement) {
this.deployment = deployment;
this.userSessionManagement = userSessionManagement;
}
@Override
public HttpHandler wrap(HttpHandler handler) {
return new ServletAdminActionsHandler(deployment, userSessionManagement, handler);
}
}
protected ServletAdminActionsHandler(KeycloakDeployment deployment,
UserSessionManagement userSessionManagement,
HttpHandler next) {
this.next = next;
this.deployment = deployment;
this.userSessionManagement = userSessionManagement;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
log.debugv("adminActions {0}", exchange.getRequestURI());
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpServletRequest request = (HttpServletRequest) servletRequestContext.getServletRequest();
HttpServletResponse response = (HttpServletResponse) servletRequestContext.getServletResponse();
SessionManager manager = servletRequestContext.getDeployment().getSessionManager();
String requestUri = exchange.getRequestURI();
if (requestUri.endsWith(AdapterConstants.K_LOGOUT)) {
log.info("K_LOGOUT sent");
JWSInput token = verifyAdminRequest(request, response);
if (token == null) return;
userSessionManagement.remoteLogout(token, manager, response);
return;
} else if (requestUri.endsWith(AdapterConstants.K_PUSH_NOT_BEFORE)) {
handlePushNotBefore(request, response);
return;
} else if (requestUri.endsWith(AdapterConstants.K_GET_SESSION_STATS)) {
handleGetSessionStats(request, response);
return;
}else if (requestUri.endsWith(AdapterConstants.K_GET_USER_STATS)) {
handleGetUserStats(request, response);
return;
} else {
next.handleRequest(exchange);
return;
}
}
protected void handlePushNotBefore(HttpServletRequest request, HttpServletResponse response) throws Exception {
log.info("K_PUSH_NOT_BEFORE sent");
JWSInput token = verifyAdminRequest(request, response);
if (token == null) {
return;
}
PushNotBeforeAction action = JsonSerialization.readValue(token.getContent(), PushNotBeforeAction.class);
if (!validateAction(response, action)) return;
deployment.setNotBefore(action.getNotBefore());
return;
}
protected JWSInput verifyAdminRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
String token = StreamUtil.readString(request.getInputStream());
if (token == null) {
log.warn("admin request failed, no token");
response.sendError(StatusCodes.FORBIDDEN, "no token");
return null;
}
JWSInput input = new JWSInput(token);
boolean verified = false;
try {
verified = RSAProvider.verify(input, deployment.getRealmKey());
} catch (Exception ignore) {
}
if (!verified) {
log.warn("admin request failed, unable to verify token");
response.sendError(StatusCodes.FORBIDDEN, "verification failed");
return null;
}
return input;
}
protected boolean validateAction(HttpServletResponse response, AdminAction action) throws IOException {
if (!action.validate()) {
log.warn("admin request failed, not validated" + action.getAction());
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Not validated");
return false;
}
if (action.isExpired()) {
log.warn("admin request failed, expired token");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Expired token");
return false;
}
if (!deployment.getResourceName().equals(action.getResource())) {
log.warn("Resource name does not match");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Resource name does not match");
return false;
}
return true;
}
protected void handleGetSessionStats(HttpServletRequest request, HttpServletResponse response) throws Exception {
log.info("K_GET_SESSION_STATS sent");
JWSInput token = verifyAdminRequest(request, response);
if (token == null) return;
SessionStatsAction action = JsonSerialization.readValue(token.getContent(), SessionStatsAction.class);
if (!validateAction(response, action)) return;
SessionStats stats = new SessionStats();
stats.setActiveSessions(userSessionManagement.getActiveSessions());
stats.setActiveUsers(userSessionManagement.getActiveUsers().size());
if (action.isListUsers() && userSessionManagement.getActiveSessions() > 0) {
Map<String, UserStats> list = new HashMap<String, UserStats>();
for (String user : userSessionManagement.getActiveUsers()) {
list.put(user, getUserStats(user));
}
stats.setUsers(list);
}
response.setStatus(200);
response.setContentType("application/json");
JsonSerialization.writeValueToStream(response.getOutputStream(), stats);
return;
}
protected void handleGetUserStats(HttpServletRequest request, HttpServletResponse response) throws Exception {
log.info("K_GET_USER_STATS sent");
JWSInput token = verifyAdminRequest(request, response);
if (token == null) return;
UserStatsAction action = JsonSerialization.readValue(token.getContent(), UserStatsAction.class);
if (!validateAction(response, action)) return;
String user = action.getUser();
UserStats stats = getUserStats(user);
response.setStatus(200);
response.setContentType("application/json");
JsonSerialization.writeValueToStream(response.getOutputStream(), stats);
return;
}
protected UserStats getUserStats(String user) {
UserStats stats = new UserStats();
Long loginTime = userSessionManagement.getUserLoginTime(user);
if (loginTime != null) {
stats.setLoggedIn(true);
stats.setWhenLoggedIn(loginTime);
} else {
stats.setLoggedIn(false);
}
return stats;
}
}

View file

@ -17,10 +17,10 @@ public class ServletKeycloakAuthMech implements AuthenticationMechanism {
public static final AttachmentKey<AuthChallenge> KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class);
protected KeycloakDeployment deployment;
protected UserSessionManagement userSessionManagement;
protected UndertowUserSessionManagement userSessionManagement;
protected ConfidentialPortManager portManager;
public ServletKeycloakAuthMech(KeycloakDeployment deployment, UserSessionManagement userSessionManagement, ConfidentialPortManager portManager) {
public ServletKeycloakAuthMech(KeycloakDeployment deployment, UndertowUserSessionManagement userSessionManagement, ConfidentialPortManager portManager) {
this.deployment = deployment;
this.userSessionManagement = userSessionManagement;
this.portManager = portManager;

View file

@ -0,0 +1,54 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.handlers.ServletRequestContext;
import org.jboss.logging.Logger;
import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.KeycloakDeployment;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ServletPreAuthActionsHandler implements HttpHandler {
private static final Logger log = Logger.getLogger(ServletPreAuthActionsHandler.class);
protected HttpHandler next;
protected UndertowUserSessionManagement userSessionManagement;
protected KeycloakDeployment deployment;
public static class Wrapper implements HandlerWrapper {
protected KeycloakDeployment deployment;
protected UndertowUserSessionManagement userSessionManagement;
public Wrapper(KeycloakDeployment deployment, UndertowUserSessionManagement userSessionManagement) {
this.deployment = deployment;
this.userSessionManagement = userSessionManagement;
}
@Override
public HttpHandler wrap(HttpHandler handler) {
return new ServletPreAuthActionsHandler(deployment, userSessionManagement, handler);
}
}
protected ServletPreAuthActionsHandler(KeycloakDeployment deployment,
UndertowUserSessionManagement userSessionManagement,
HttpHandler next) {
this.next = next;
this.deployment = deployment;
this.userSessionManagement = userSessionManagement;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
SessionManagementBridge bridge = new SessionManagementBridge(userSessionManagement, servletRequestContext.getDeployment().getSessionManager());
PreAuthActionsHandler handler = new PreAuthActionsHandler(bridge, deployment, new UndertowHttpFacade(exchange));
if (handler.handleRequest()) return;
next.handleRequest(exchange);
}
}

View file

@ -16,11 +16,11 @@ import javax.servlet.http.HttpSession;
*/
public class ServletRequestAuthenticator extends UndertowRequestAuthenticator {
protected UserSessionManagement userSessionManagement;
protected UndertowUserSessionManagement userSessionManagement;
public ServletRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
SecurityContext securityContext, HttpServerExchange exchange,
UserSessionManagement userSessionManagement) {
UndertowUserSessionManagement userSessionManagement) {
super(facade, deployment, sslRedirectPort, securityContext, exchange);
this.userSessionManagement = userSessionManagement;
}
@ -52,16 +52,16 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator {
@Override
protected void propagateKeycloakContext(KeycloakUndertowAccount account) {
super.propagateKeycloakContext(account);
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
req.setAttribute(KeycloakSecurityContext.class.getName(), account.getSession());
req.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
}
@Override
protected void login(KeycloakUndertowAccount account) {
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
req.setAttribute(KeycloakSecurityContext.class.getName(), account.getSession());
HttpSession session = req.getSession(true);
session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
userSessionManagement.login(servletRequestContext.getDeployment().getSessionManager(), session, account.getPrincipal().getName());

View file

@ -0,0 +1,47 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.SessionManager;
import org.keycloak.adapters.UserSessionManagement;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SessionManagementBridge implements UserSessionManagement {
protected UndertowUserSessionManagement userSessionManagement;
protected SessionManager sessionManager;
public SessionManagementBridge(UndertowUserSessionManagement userSessionManagement, SessionManager sessionManager) {
this.userSessionManagement = userSessionManagement;
this.sessionManager = sessionManager;
}
@Override
public int getActiveSessions() {
return userSessionManagement.getActiveSessions();
}
@Override
public Long getUserLoginTime(String username) {
return userSessionManagement.getUserLoginTime(username);
}
@Override
public Set<String> getActiveUsers() {
return userSessionManagement.getActiveUsers();
}
@Override
public void logoutAll() {
userSessionManagement.logoutAll(sessionManager);
}
@Override
public void logout(String user) {
userSessionManagement.logout(sessionManager, user);
}
}

View file

@ -0,0 +1,46 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import org.jboss.logging.Logger;
import org.keycloak.adapters.AuthenticatedActionsHandler;
import org.keycloak.adapters.KeycloakDeployment;
/**
* Bridge for authenticated Keycloak adapter actions
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UndertowAuthenticatedActionsHandler implements HttpHandler {
private static final Logger log = Logger.getLogger(UndertowAuthenticatedActionsHandler.class);
protected KeycloakDeployment deployment;
protected HttpHandler next;
public static class Wrapper implements HandlerWrapper {
protected KeycloakDeployment deployment;
public Wrapper(KeycloakDeployment deployment) {
this.deployment = deployment;
}
@Override
public HttpHandler wrap(HttpHandler handler) {
return new UndertowAuthenticatedActionsHandler(deployment, handler);
}
}
protected UndertowAuthenticatedActionsHandler(KeycloakDeployment deployment, HttpHandler next) {
this.deployment = deployment;
this.next = next;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
AuthenticatedActionsHandler handler = new AuthenticatedActionsHandler(deployment, new UndertowHttpFacade(exchange));
if (handler.handledRequest()) return;
next.handleRequest(exchange);
}
}

View file

@ -3,11 +3,18 @@ package org.keycloak.adapters.undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.Cookie;
import io.undertow.server.handlers.CookieImpl;
import io.undertow.util.AttachmentKey;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.util.KeycloakUriBuilder;
import javax.security.cert.X509Certificate;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Deque;
import java.util.List;
import java.util.Map;
@ -17,6 +24,8 @@ import java.util.Map;
* @version $Revision: 1 $
*/
public class UndertowHttpFacade implements HttpFacade {
public static final AttachmentKey<KeycloakSecurityContext> KEYCLOAK_SECURITY_CONTEXT_KEY = AttachmentKey.create(KeycloakSecurityContext.class);
protected HttpServerExchange exchange;
protected RequestFacade requestFacade = new RequestFacade();
protected ResponseFacade responseFacade = new ResponseFacade();
@ -57,6 +66,23 @@ public class UndertowHttpFacade implements HttpFacade {
public List<String> getHeaders(String name) {
return exchange.getRequestHeaders().get(name);
}
@Override
public String getMethod() {
return exchange.getRequestMethod().toString();
}
@Override
public String getHeader(String name) {
return exchange.getRequestHeaders().getFirst(name);
}
@Override
public InputStream getInputStream() {
return exchange.getInputStream();
}
}
protected class ResponseFacade implements Response {
@ -94,6 +120,24 @@ public class UndertowHttpFacade implements HttpFacade {
exchange.setResponseCookie(cookie);
}
@Override
public OutputStream getOutputStream() {
return exchange.getOutputStream();
}
@Override
public void sendError(int code, String message) {
exchange.setResponseCode(code);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html");
try {
exchange.getOutputStream().write(message.getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
exchange.endExchange();
}
@Override
public void end() {
exchange.endExchange();
@ -114,6 +158,11 @@ public class UndertowHttpFacade implements HttpFacade {
return responseFacade;
}
@Override
public KeycloakSecurityContext getSecurityContext() {
return exchange.getAttachment(KEYCLOAK_SECURITY_CONTEXT_KEY);
}
@Override
public X509Certificate[] getCertificateChain() {
X509Certificate[] chain = new X509Certificate[0];

View file

@ -25,6 +25,7 @@ public class UndertowRequestAuthenticator extends RequestAuthenticator {
}
protected void propagateKeycloakContext(KeycloakUndertowAccount account) {
exchange.putAttachment(UndertowHttpFacade.KEYCLOAK_SECURITY_CONTEXT_KEY, account.getKeycloakSecurityContext());
}
@Override
@ -41,6 +42,7 @@ public class UndertowRequestAuthenticator extends RequestAuthenticator {
protected void completeOAuthAuthentication(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext session) {
KeycloakUndertowAccount account = new KeycloakUndertowAccount(principal, session, deployment);
securityContext.authenticationComplete(account, "KEYCLOAK", false);
propagateKeycloakContext(account);
login(account);
}

View file

@ -28,14 +28,14 @@ import java.util.concurrent.ConcurrentHashMap;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UserSessionManagement implements SessionListener {
private static final Logger log = Logger.getLogger(UserSessionManagement.class);
public class UndertowUserSessionManagement implements SessionListener {
private static final Logger log = Logger.getLogger(UndertowUserSessionManagement.class);
private static final String AUTH_SESSION_NAME = CachedAuthenticatedSessionHandler.class.getName() + ".AuthenticatedSession";
protected ConcurrentHashMap<String, UserSessions> userSessionMap = new ConcurrentHashMap<String, UserSessions>();
protected KeycloakDeployment deployment;
public UserSessionManagement(KeycloakDeployment deployment) {
public UndertowUserSessionManagement(KeycloakDeployment deployment) {
this.deployment = deployment;
}
@ -80,36 +80,6 @@ public class UserSessionManagement implements SessionListener {
return set;
}
public void remoteLogout(JWSInput token, SessionManager manager, HttpServletResponse response) throws IOException {
try {
log.info("->> remoteLogout: ");
LogoutAction action = JsonSerialization.readValue(token.getContent(), LogoutAction.class);
if (action.isExpired()) {
log.warn("admin request failed, expired token");
response.sendError(StatusCodes.BAD_REQUEST, "Expired token");
return;
}
if (!deployment.getResourceName().equals(action.getResource())) {
log.warn("Resource name does not match");
response.sendError(StatusCodes.BAD_REQUEST, "Resource name does not match");
return;
}
String user = action.getUser();
if (user != null) {
log.info("logout of session for: " + user);
logout(manager, user);
} else {
log.info("logout of all sessions");
logoutAll(manager);
}
} catch (Exception e) {
log.warn("failed to logout", e);
response.sendError(StatusCodes.INTERNAL_SERVER_ERROR, "Failed to logout");
}
response.setStatus(StatusCodes.NO_CONTENT);
}
public void login(SessionManager manager, HttpSession session, String username) {
String sessionId = session.getId();
addAuthenticatedSession(username, sessionId);
@ -144,14 +114,6 @@ public class UserSessionManagement implements SessionListener {
for (String user : users) logout(manager, user);
}
public void logoutAllBut(SessionManager manager, String but) {
List<String> users = new ArrayList<String>();
users.addAll(userSessionMap.keySet());
for (String user : users) {
if (!but.equals(user)) logout(manager, user);
}
}
public void logout(SessionManager manager, String user) {
log.info("logoutUser: " + user);
UserSessions sessions = null;