refactor adapters complete
This commit is contained in:
parent
63bc2d6dbb
commit
0284d040dc
23 changed files with 656 additions and 708 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -59,7 +59,7 @@ public class KeycloakUndertowAccount implements Account, Serializable {
|
|||
return session.getTokenString();
|
||||
}
|
||||
|
||||
public RefreshableKeycloakSecurityContext getSession() {
|
||||
public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() {
|
||||
return session;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
Loading…
Reference in a new issue