Skip to content

Commit 7f6f0cd

Browse files
committed
Fix #32637: implement CustomHeaderAuthenticationBackend
1 parent 3e4aa46 commit 7f6f0cd

6 files changed

Lines changed: 322 additions & 0 deletions

src/main/java/eu/openanalytics/containerproxy/auth/AuthenticationBackendFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*/
2121
package eu.openanalytics.containerproxy.auth;
2222

23+
import eu.openanalytics.containerproxy.auth.impl.CustomHeaderAuthenticationBackend;
2324
import eu.openanalytics.containerproxy.auth.impl.LDAPAuthenticationBackend;
2425
import eu.openanalytics.containerproxy.auth.impl.NoAuthenticationBackend;
2526
import eu.openanalytics.containerproxy.auth.impl.OpenIDAuthenticationBackend;
@@ -72,6 +73,7 @@ protected IAuthenticationBackend createInstance() {
7273
case LDAPAuthenticationBackend.NAME -> backend = new LDAPAuthenticationBackend();
7374
case OpenIDAuthenticationBackend.NAME -> backend = new OpenIDAuthenticationBackend();
7475
case WebServiceAuthenticationBackend.NAME -> backend = new WebServiceAuthenticationBackend(environment);
76+
case CustomHeaderAuthenticationBackend.NAME -> backend = new CustomHeaderAuthenticationBackend(environment, applicationEventPublisher);
7577
case SAMLAuthenticationBackend.NAME -> {
7678
return samlBackend;
7779
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2025 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.auth.impl;
22+
23+
import eu.openanalytics.containerproxy.auth.IAuthenticationBackend;
24+
import eu.openanalytics.containerproxy.auth.impl.customHeader.CustomHeaderAuthenticationFilter;
25+
import eu.openanalytics.containerproxy.auth.impl.customHeader.CustomHeaderAuthenticationProvider;
26+
import org.springframework.context.ApplicationEventPublisher;
27+
import org.springframework.core.env.Environment;
28+
import org.springframework.security.authentication.ProviderManager;
29+
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
30+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
31+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
32+
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
33+
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
34+
35+
public class CustomHeaderAuthenticationBackend implements IAuthenticationBackend {
36+
37+
public final static String NAME = "customHeader";
38+
39+
private final static String PROP_CUSTOM_AUTH_USERNAME_HEADER_NAME = "proxy.custom-header.username-header-name";
40+
private final static String DEFAULT_USERNAME_HEADER_NAME = "REMOTE_USER";
41+
42+
private final CustomHeaderAuthenticationFilter filter;
43+
44+
public CustomHeaderAuthenticationBackend(Environment environment, ApplicationEventPublisher applicationEventPublisher) {
45+
String usernameHeaderName = environment.getProperty(PROP_CUSTOM_AUTH_USERNAME_HEADER_NAME, DEFAULT_USERNAME_HEADER_NAME);
46+
ProviderManager providerManager = new ProviderManager(new CustomHeaderAuthenticationProvider());
47+
filter = new CustomHeaderAuthenticationFilter(providerManager, applicationEventPublisher, usernameHeaderName);
48+
}
49+
50+
@Override
51+
public String getName() {
52+
return NAME;
53+
}
54+
55+
@Override
56+
public boolean hasAuthorization() {
57+
return true;
58+
}
59+
60+
@Override
61+
public void configureHttpSecurity(HttpSecurity http) throws Exception {
62+
http.formLogin(AbstractHttpConfigurer::disable);
63+
64+
http.addFilterBefore(filter, AnonymousAuthenticationFilter.class)
65+
.exceptionHandling(e -> {
66+
e.authenticationEntryPoint((request, response, authException) -> {
67+
response.sendRedirect(ServletUriComponentsBuilder.fromCurrentContextPath().path("/auth-error").build().toUriString());
68+
});
69+
});
70+
}
71+
72+
@Override
73+
public void configureAuthenticationManagerBuilder(AuthenticationManagerBuilder auth) throws Exception {
74+
// Nothing to do.
75+
}
76+
77+
@Override
78+
public String getLogoutSuccessURL() {
79+
return "/logout-success";
80+
}
81+
82+
83+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2025 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.auth.impl.customHeader;
22+
23+
import org.springframework.security.access.AccessDeniedException;
24+
25+
public class CustomHeaderAuthenticationException extends AccessDeniedException {
26+
27+
public CustomHeaderAuthenticationException(String explanation) {
28+
super(explanation);
29+
}
30+
31+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2025 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.auth.impl.customHeader;
22+
23+
import jakarta.servlet.FilterChain;
24+
import jakarta.servlet.ServletException;
25+
import jakarta.servlet.http.HttpServletRequest;
26+
import jakarta.servlet.http.HttpServletResponse;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
import org.springframework.context.ApplicationEventPublisher;
30+
import org.springframework.security.authentication.AuthenticationManager;
31+
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
32+
import org.springframework.security.core.Authentication;
33+
import org.springframework.security.core.AuthenticationException;
34+
import org.springframework.security.core.context.SecurityContextHolder;
35+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
36+
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
37+
import org.springframework.security.web.util.matcher.OrRequestMatcher;
38+
import org.springframework.security.web.util.matcher.RequestMatcher;
39+
import org.springframework.web.filter.OncePerRequestFilter;
40+
41+
import javax.annotation.Nonnull;
42+
import java.io.IOException;
43+
44+
public class CustomHeaderAuthenticationFilter extends OncePerRequestFilter {
45+
46+
private final Logger logger = LoggerFactory.getLogger(getClass());
47+
private final AuthenticationManager authenticationManager;
48+
private final ApplicationEventPublisher eventPublisher;
49+
50+
// prevent re-login on logout-success page
51+
private static final RequestMatcher REQUEST_MATCHER = new NegatedRequestMatcher(new OrRequestMatcher(
52+
new AntPathRequestMatcher("/logout-success"),
53+
new AntPathRequestMatcher("/webjars/**"),
54+
new AntPathRequestMatcher("/css/**"))
55+
);
56+
57+
private final String usernameHeaderName;
58+
59+
public CustomHeaderAuthenticationFilter(AuthenticationManager authenticationManager, ApplicationEventPublisher eventPublisher, String usernameHeaderName) {
60+
this.authenticationManager = authenticationManager;
61+
this.eventPublisher = eventPublisher;
62+
this.usernameHeaderName = usernameHeaderName;
63+
}
64+
65+
@Override
66+
protected void doFilterInternal(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response, @Nonnull FilterChain chain) throws ServletException, IOException, AuthenticationException {
67+
if (!REQUEST_MATCHER.matches(request)) {
68+
chain.doFilter(request, response);
69+
return;
70+
}
71+
try {
72+
String remoteUser = request.getHeader(usernameHeaderName);
73+
if (remoteUser == null) {
74+
throw new CustomHeaderAuthenticationException(String.format("Missing username header '%s'", usernameHeaderName));
75+
}
76+
77+
Authentication existingAuthentication = SecurityContextHolder.getContext().getAuthentication();
78+
if (existingAuthentication instanceof CustomHeaderAuthenticationToken) {
79+
if (!existingAuthentication.getPrincipal().equals(remoteUser)) {
80+
throw new CustomHeaderAuthenticationException(String.format("Username in header '%s' does not match existing session '%s'", remoteUser, existingAuthentication.getPrincipal()));
81+
} else {
82+
chain.doFilter(request, response);
83+
return;
84+
}
85+
}
86+
87+
Authentication authRequest = new CustomHeaderAuthenticationToken(remoteUser, false);
88+
Authentication authResult = authenticationManager.authenticate(authRequest);
89+
if (authResult == null) {
90+
throw new CustomHeaderAuthenticationException("No authentication");
91+
}
92+
93+
SecurityContextHolder.getContext().setAuthentication(authResult);
94+
eventPublisher.publishEvent(new AuthenticationSuccessEvent(authResult));
95+
} catch (CustomHeaderAuthenticationException e) {
96+
logger.warn("Authentication failed", e);
97+
SecurityContextHolder.clearContext();
98+
}
99+
chain.doFilter(request, response);
100+
}
101+
102+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2025 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.auth.impl.customHeader;
22+
23+
import org.springframework.security.authentication.AuthenticationProvider;
24+
import org.springframework.security.core.Authentication;
25+
import org.springframework.security.core.AuthenticationException;
26+
27+
public class CustomHeaderAuthenticationProvider implements AuthenticationProvider {
28+
29+
@Override
30+
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
31+
CustomHeaderAuthenticationToken authRequest = (CustomHeaderAuthenticationToken) authentication;
32+
33+
if (authRequest.isValid()) {
34+
return new CustomHeaderAuthenticationToken(authRequest.getPrincipal().toString(), true);
35+
}
36+
37+
throw new CustomHeaderAuthenticationException("Invalid username");
38+
}
39+
40+
@Override
41+
public boolean supports(Class<?> authentication) {
42+
return CustomHeaderAuthenticationToken.class.isAssignableFrom(authentication);
43+
}
44+
45+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2025 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.auth.impl.customHeader;
22+
23+
import org.springframework.security.authentication.AbstractAuthenticationToken;
24+
25+
public class CustomHeaderAuthenticationToken extends AbstractAuthenticationToken {
26+
27+
private final String username;
28+
29+
public CustomHeaderAuthenticationToken(String username, boolean isAuthenticated) {
30+
super(null);
31+
this.username = username;
32+
super.setAuthenticated(isAuthenticated);
33+
}
34+
35+
public boolean isValid() {
36+
return username != null && !username.isBlank();
37+
}
38+
39+
@Override
40+
public Object getPrincipal() {
41+
return username;
42+
}
43+
44+
@Override
45+
public Object getCredentials() {
46+
return null;
47+
}
48+
49+
@Override
50+
public String getName() {
51+
return this.username;
52+
}
53+
54+
@Override
55+
public void setAuthenticated(boolean isAuthenticated) {
56+
throw new CustomHeaderAuthenticationException("Cannot change authenticated after initialization!");
57+
}
58+
59+
}

0 commit comments

Comments
 (0)