Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions server/plugins/oidc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,6 @@
<name>${project.groupId}:${project.artifactId}</name>

<dependencies>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-oidc</artifactId>
</dependency>

<dependency>
<groupId>com.walmartlabs.concord.server</groupId>
<artifactId>concord-server-sdk</artifactId>
Expand Down Expand Up @@ -79,6 +70,11 @@
<artifactId>config</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* *****
* Concord
* -----
* Copyright (C) 2020 Ivan Bodrov
* Copyright (C) 2017 - 2025 Walmart Inc.
* -----
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,59 +20,49 @@
* =====
*/

import org.pac4j.core.config.Config;
import org.pac4j.core.context.JEEContext;
import org.pac4j.core.exception.http.RedirectionAction;
import org.pac4j.core.util.Pac4jConstants;
import org.pac4j.oidc.client.OidcClient;

import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;

// TODO can be implemented as a JAX-RS resource
public class OidcAuthFilter implements Filter {

public static final String URL = "/api/service/oidc/auth";
private static final String SESSION_STATE_KEY = "OIDC_STATE";
private static final String SESSION_REDIRECT_KEY = "OIDC_REDIRECT_URL";

private final PluginConfiguration pluginConfig;
private final Config oidcConfig;
private final OidcClient<?> client;
private final OidcService oidcService;

@Inject
public OidcAuthFilter(PluginConfiguration pluginConfig, @Named("oidc") Config oidcConfig, OidcClient<?> client) {
public OidcAuthFilter(PluginConfiguration pluginConfig, OidcService oidcService) {
this.pluginConfig = pluginConfig;
this.oidcConfig = oidcConfig;
this.client = client;

if (pluginConfig.isEnabled() && !client.isInitialized()) {
client.init();
}
this.oidcService = oidcService;
}

@Override
@SuppressWarnings("unchecked")
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
var req = (HttpServletRequest) request;
var resp = (HttpServletResponse) response;

if (!pluginConfig.isEnabled()) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "OIDC disabled");
return;
}

JEEContext context = new JEEContext(req, resp);

String redirectUrl = req.getParameter("from");
context.getSessionStore().set(context, Pac4jConstants.REQUESTED_URL, redirectUrl);
var redirectUrl = req.getParameter("from");
var state = UUID.randomUUID().toString();
var callbackUrl = pluginConfig.getUrlBase() + OidcCallbackFilter.URL + "?client_name=oidc";

RedirectionAction action = client.getRedirectionActionBuilder()
.getRedirectionAction(context)
.orElseThrow(() -> new IllegalStateException("Can't get a redirection action for the request"));
var session = req.getSession(true);
session.setAttribute(SESSION_STATE_KEY, state);
session.setAttribute(SESSION_REDIRECT_KEY, redirectUrl);

oidcConfig.getHttpActionAdapter().adapt(action, context);
var authUrl = oidcService.buildAuthorizationUrl(callbackUrl, state);
resp.sendRedirect(authUrl);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* *****
* Concord
* -----
* Copyright (C) 2017 - 2020 Walmart Inc.
* Copyright (C) 2017 - 2025 Walmart Inc.
* -----
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,35 +22,32 @@

import com.walmartlabs.concord.server.boot.filters.AuthenticationHandler;
import org.apache.shiro.authc.AuthenticationToken;
import org.pac4j.core.context.JEEContext;
import org.pac4j.core.credentials.TokenCredentials;
import org.pac4j.core.profile.ProfileManager;
import org.pac4j.oidc.config.OidcConfiguration;
import org.pac4j.oidc.credentials.authenticator.UserInfoOidcAuthenticator;
import org.pac4j.oidc.profile.OidcProfile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;

public class OidcAuthenticationHandler implements AuthenticationHandler {

private static final Logger log = LoggerFactory.getLogger(OidcAuthenticationHandler.class);
private static final String FORM_URL_PATTERN = "/forms/.*";

private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String HEADER_PREFIX = "Bearer";
private static final String SESSION_PROFILE_KEY = "OIDC_USER_PROFILE";

private final PluginConfiguration cfg;
private final OidcConfiguration oidcCfg;
private final OidcService oidcService;

@Inject
public OidcAuthenticationHandler(PluginConfiguration cfg, OidcConfiguration oidcCfg) {
public OidcAuthenticationHandler(PluginConfiguration cfg, OidcService oidcService) {
this.cfg = cfg;
this.oidcCfg = oidcCfg;
this.oidcService = oidcService;
}

@Override
Expand All @@ -59,33 +56,32 @@ public AuthenticationToken createToken(ServletRequest request, ServletResponse r
return null;
}

HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
JEEContext context = new JEEContext(req, resp);
var req = (HttpServletRequest) request;

Optional<OidcProfile> profile;

// check the token first
String header = req.getHeader(AUTHORIZATION_HEADER);
if (header != null) {
String[] as = header.split(" ");
if (as.length != 2 || !as[0].equals(HEADER_PREFIX)) {
var header = req.getHeader(AUTHORIZATION_HEADER);
if (header == null) {
var session = req.getSession(false);
if (session == null) {
return null;
}

TokenCredentials credentials = new TokenCredentials(as[1].trim());

UserInfoOidcAuthenticator authenticator = new UserInfoOidcAuthenticator(oidcCfg);
authenticator.validate(credentials, context);
var profile = (UserProfile) session.getAttribute(SESSION_PROFILE_KEY);
return new OidcToken(profile);
}

// we know that UserInfoOidcAuthenticator produces OidcProfile, so we can cast to it here
profile = Optional.ofNullable((OidcProfile) credentials.getUserProfile());
} else {
ProfileManager<OidcProfile> profileManager = new ProfileManager<>(context);
profile = profileManager.get(true);
var as = header.split(" ");
if (as.length != 2 || !as[0].equals(HEADER_PREFIX)) {
return null;
}

return profile.map(OidcToken::new).orElse(null);
var accessToken = as[1].trim();
try {
var profile = oidcService.validateToken(accessToken);
return new OidcToken(profile);
} catch (IOException e) {
log.warn("Token validation failed: {}", e.getMessage());
return null;
}
}

@Override
Expand All @@ -94,8 +90,8 @@ public boolean onAccessDenied(ServletRequest request, ServletResponse response)
return false;
}

HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
var req = (HttpServletRequest) request;
var resp = (HttpServletResponse) response;

if (req.getRequestURI().matches(FORM_URL_PATTERN)) {
resp.sendRedirect(resp.encodeRedirectURL(OidcAuthFilter.URL + "?from=" + req.getRequestURL()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* *****
* Concord
* -----
* Copyright (C) 2020 Ivan Bodrov
* Copyright (C) 2017 - 2025 Walmart Inc.
* -----
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,99 +20,92 @@
* =====
*/

import org.pac4j.core.config.Config;
import org.pac4j.core.context.JEEContext;
import org.pac4j.core.context.session.SessionStore;
import org.pac4j.core.engine.CallbackLogic;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.core.util.Pac4jConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.*;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

public class OidcCallbackFilter implements Filter {

private static final Logger log = LoggerFactory.getLogger(OidcCallbackFilter.class);

public static final String URL = "/api/service/oidc/callback";
private static final String SESSION_STATE_KEY = "OIDC_STATE";
private static final String SESSION_REDIRECT_KEY = "OIDC_REDIRECT_URL";
private static final String SESSION_PROFILE_KEY = "OIDC_USER_PROFILE";

private final PluginConfiguration cfg;
private final Config pac4jConfig;
private final OidcService oidcService;

@Inject
public OidcCallbackFilter(PluginConfiguration cfg,
@Named("oidc") Config pac4jConfig) {

public OidcCallbackFilter(PluginConfiguration cfg, OidcService oidcService) {
this.cfg = cfg;
this.pac4jConfig = pac4jConfig;
this.oidcService = oidcService;
}

@Override
@SuppressWarnings("unchecked")
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
var req = (HttpServletRequest) request;
var resp = (HttpServletResponse) response;

if (!cfg.isEnabled()) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "OIDC disabled");
return;
}

JEEContext context = new JEEContext(req, resp, pac4jConfig.getSessionStore());
var session = req.getSession(false);
if (session == null) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "No session");
return;
}

String postLoginUrl = removeRequestedUrl(context);
var postLoginUrl = (String) session.getAttribute(SESSION_REDIRECT_KEY);
if (postLoginUrl == null || postLoginUrl.trim().isEmpty()) {
postLoginUrl = cfg.getAfterLoginUrl();
}

String error = req.getParameter("error");
var error = req.getParameter("error");
if (error != null) {
String derivedError = "unknown";
var derivedError = "unknown";
if ("access_denied".equals(error)) {
derivedError = "oidc_access_denied";
}
resp.sendRedirect(resp.encodeRedirectURL(cfg.getOnErrorUrl() + "?from=" + postLoginUrl + "&error=" + derivedError));
return;
}

try {
CallbackLogic<?, JEEContext> callback = pac4jConfig.getCallbackLogic();
callback.perform(context, pac4jConfig, pac4jConfig.getHttpActionAdapter(), postLoginUrl, true, false, true, OidcPluginModule.CLIENT_NAME);
} catch (TechnicalException e) {
log.warn("OIDC callback error: {}", e.getMessage());
HttpSession session = req.getSession(false);
if (session != null) {
session.invalidate();
}
var code = req.getParameter("code");
var state = req.getParameter("state");
var expectedState = (String) session.getAttribute(SESSION_STATE_KEY);

if (code == null || state == null || !state.equals(expectedState)) {
log.warn("Invalid callback parameters: code={}, state={}, expectedState={}", code != null, state, expectedState);
session.invalidate();
resp.sendRedirect(resp.encodeRedirectURL(OidcAuthFilter.URL + "?from=" + postLoginUrl));
return;
}
}

@Override
public void init(FilterConfig filterConfig) {
// do nothing
}
try {
var redirectUri = cfg.getUrlBase() + URL + "?client_name=oidc";
var profile = oidcService.exchangeCodeForProfile(code, redirectUri);

@Override
public void destroy() {
// do nothing
}
session.setAttribute(SESSION_PROFILE_KEY, profile);
session.removeAttribute(SESSION_STATE_KEY);
session.removeAttribute(SESSION_REDIRECT_KEY);

resp.sendRedirect(resp.encodeRedirectURL(postLoginUrl));

@SuppressWarnings("unchecked")
private static String removeRequestedUrl(JEEContext context) {
SessionStore<JEEContext> sessionStore = context.getSessionStore();
Object result = sessionStore.get(context, Pac4jConstants.REQUESTED_URL).orElse(null);
sessionStore.set(context, Pac4jConstants.REQUESTED_URL, "");
if (result instanceof String) {
return (String) result;
} catch (Exception e) {
log.warn("OIDC callback error: {}", e.getMessage());
session.invalidate();
resp.sendRedirect(resp.encodeRedirectURL(OidcAuthFilter.URL + "?from=" + postLoginUrl));
}
return null;
}
}
Loading