Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[microservice] Add auth-mode ldap to enable authentication based on LDAP #35

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
*/
package de.muenchen.oss.ezldap.spring;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.LdapTemplate;
Expand All @@ -44,7 +46,8 @@
public class LdapConfiguration {

@Bean
LdapContextSource ldapContextSource(final EzLdapConfigurationProperties props) {
@ConditionalOnMissingBean(name = "ezldapQueryContextSource")
LdapContextSource ezldapQueryContextSource(final EzLdapConfigurationProperties props) {
final LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl(props.getLdap().getUrl());
contextSource.setUserDn(props.getLdap().getUserDn());
Expand All @@ -54,16 +57,17 @@ LdapContextSource ldapContextSource(final EzLdapConfigurationProperties props) {
return contextSource;
}

@Bean
LdapTemplate ldapTemplate(final LdapContextSource ldapContextSource) {
@Bean("ezldapLdapTemplate")
LdapTemplate ezldapLdapTemplate(@Qualifier("ezldapQueryContextSource") final LdapContextSource ldapContextSource) {
return new LdapTemplate(ldapContextSource);
}

@Bean
LdapService ldapService(final LdapTemplate template, final EzLdapConfigurationProperties props) {
LdapService ldapService(@Qualifier("ezldapLdapTemplate") final LdapTemplate template, final EzLdapConfigurationProperties props) {
final LdapBaseUserAttributesMapper ldapBaseUserAttributesMapper = new LdapBaseUserAttributesMapper();
final LdapOuAttributesMapper ldapOuAttributesMapper = new LdapOuAttributesMapper();
final LdapUserAttributesMapper ldapUserAttributesMapper = new LdapUserAttributesMapper(ldapBaseUserAttributesMapper);
final LdapUserAttributesMapper ldapUserAttributesMapper = new LdapUserAttributesMapper(
ldapBaseUserAttributesMapper);
return new LdapService(template, ldapUserAttributesMapper, ldapBaseUserAttributesMapper, ldapOuAttributesMapper,
new DtoMapperImpl(), props.getLdap().getUserSearchBase(), props.getLdap().getOuSearchBase());
}
Expand Down
14 changes: 13 additions & 1 deletion microservice/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
THE SOFTWARE.

-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
Expand Down Expand Up @@ -61,6 +63,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>


<!-- Testing -->
<dependency>
Expand All @@ -73,6 +80,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>


<!-- Logging -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class AppConfigurationProperties {

public enum AuthMode {
NONE,
LDAP,
BASIC;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,33 @@
*/
package de.muenchen.oss.ezldap.config;

import java.util.Collections;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.AuthenticationSource;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;

import de.muenchen.oss.ezldap.config.AppConfigurationProperties.AuthMode;
import de.muenchen.oss.ezldap.security.BasicAuthPassthroughFilter;
import de.muenchen.oss.ezldap.spring.props.EzLdapConfigurationProperties;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -49,32 +63,88 @@
public class SecurityConfiguration {

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, EzLdapConfigurationProperties configProps, AppConfigurationProperties appProps)
throws Exception {
SecurityFilterChain securityFilterChain(HttpSecurity http, EzLdapConfigurationProperties configProps,
AppConfigurationProperties appProps, @Autowired(required = false) AuthenticationManager authenticationManager) throws Exception {
if (AuthMode.NONE.equals(appProps.getAuthMode())) {
log.info("Bootstrapping Spring Security filter chain for auth-mode 'none' ...");
http
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
http.sessionManagement(
sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
} else if (AuthMode.LDAP.equals(appProps.getAuthMode())) {
log.info("Bootstrapping Spring Security filter chain for auth-mode 'ldap' ...");
configureMatchers(http);
http.sessionManagement(
sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.addFilterBefore(new BasicAuthPassthroughFilter(authenticationManager, "/v1/**"), AnonymousAuthenticationFilter.class);
} else {
log.info("Bootstrapping Spring Security filter chain for auth-mode 'basic' ...");
http
.authorizeHttpRequests(authz -> {
authz.requestMatchers("/openapi/v3/**", "/swagger-ui/**").permitAll();
authz.requestMatchers("/actuator/prometheus", "/actuator/info", "/actuator/health/**").permitAll();
authz.anyRequest().authenticated();
});
http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
configureMatchers(http);
http.sessionManagement(
sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.httpBasic(Customizer.withDefaults());
}
return http.build();
}

private void configureMatchers(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authz -> {
authz.requestMatchers("/openapi/v3/**", "/swagger-ui/**").permitAll();
authz.requestMatchers("/actuator/prometheus", "/actuator/info", "/actuator/health/**").permitAll();
authz.anyRequest().authenticated();
});
}

@Bean
@ConditionalOnProperty(name = "app.auth-mode", havingValue = "basic")
InMemoryUserDetailsManager userDetailsService(AppConfigurationProperties appProps) {
UserDetails userDetails = User.withUsername(appProps.getBasicAuth().getUser()).password(appProps.getBasicAuth().getPassword()).roles("USER").build();
UserDetails userDetails = User.withUsername(appProps.getBasicAuth().getUser())
.password(appProps.getBasicAuth().getPassword()).roles("USER").build();
return new InMemoryUserDetailsManager(userDetails);
}

@Bean
@ConditionalOnProperty(name = "app.auth-mode", havingValue = "ldap")
AuthenticationManager authenticationManager() {
ProviderManager providerManager = new ProviderManager(Collections.singletonList(new AuthenticationProvider() {

@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
return new UsernamePasswordAuthenticationToken(token.getPrincipal(), token.getCredentials(), Collections.emptyList());
}
}));
providerManager.setEraseCredentialsAfterAuthentication(false);
return providerManager;
}

@Bean(name = "ezldapQueryContextSource")
@ConditionalOnProperty(name = "app.auth-mode", havingValue = "ldap")
LdapContextSource ezldapQueryContextSource(final EzLdapConfigurationProperties props) {
final LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl(props.getLdap().getUrl());
contextSource.setAuthenticationSource(new AuthenticationSource() {

@Override
public String getPrincipal() {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
return auth.getPrincipal().toString();
}

@Override
public String getCredentials() {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
return auth.getCredentials().toString();
}
});
log.info(
"Initiating LDAP context-source with url='{}' and SpringSecurityAuthenticationSource for Web LDAP authentication credentials passthrough to LDAP queries.",
props.getLdap().getUrl());
return contextSource;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,8 @@ public class SpringdocsSwaggerConfig {

@Bean
GroupedOpenApi publicApi(OpenApiCustomizer globalResponseCodeCustomiser) {
return GroupedOpenApi.builder()
.group("ezLDAP")
.packagesToScan("de.muenchen.oss.ezldap.spring.rest.v1")
.addOpenApiCustomizer(globalResponseCodeCustomiser)
.build();
return GroupedOpenApi.builder().group("ezLDAP").packagesToScan("de.muenchen.oss.ezldap.spring.rest.v1")
.addOpenApiCustomizer(globalResponseCodeCustomiser).build();
}

@Bean
Expand All @@ -73,10 +70,13 @@ OpenApiCustomizer globalResponseCodeCustomiser(AppConfigurationProperties appPro

if (AuthMode.BASIC.equals(appProps.getAuthMode())) {
openApi.addSecurityItem(new SecurityRequirement().addList("basicAuth"))
.components(new Components()
.addSecuritySchemes("basicAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("basic")));
.components(new Components().addSecuritySchemes("basicAuth",
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("basic")));
}
if (AuthMode.LDAP.equals(appProps.getAuthMode())) {
openApi.addSecurityItem(new SecurityRequirement().addList("basicAuthLdap"))
.components(new Components().addSecuritySchemes("basicAuthLdap",
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("basic").description("Authenticate using LDAP")));
}
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* The MIT License
* Copyright © 2024 Landeshauptstadt München | it@M
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.muenchen.oss.ezldap.security;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
* @author michael.prankl
*/
public class BasicAuthPassthroughFilter extends AbstractAuthenticationProcessingFilter {

public BasicAuthPassthroughFilter(AuthenticationManager authenticationManager, String filter) {
super(filter);
this.setAuthenticationManager(authenticationManager);
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {

String header = request.getHeader("Authorization");

if (header != null && header.startsWith("Basic ")) {
String[] tokens = extractAndDecodeHeader(header);
String username = tokens[0];
// Validate username as a valid DN
try {
new LdapName(username);
} catch (InvalidNameException e) {
throw new BadCredentialsException("Invalid username format: " + e.getMessage(), e);
}
String password = tokens[1];

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(authRequest);
} else {
throw new BadCredentialsException("Missing basic auth header");
}
}

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {

SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}

private String[] extractAndDecodeHeader(String header) {
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Token);
} catch (IllegalArgumentException e) {
throw new BadCredentialsException("Failed to decode basic authentication token");
}
String token = new String(decoded, StandardCharsets.UTF_8);
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
return new String[] { token.substring(0, delim), token.substring(delim + 1) };
}

}
Loading
Loading