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
438 changes: 438 additions & 0 deletions articles/LoginPipeline.md

Large diffs are not rendered by default.

199 changes: 199 additions & 0 deletions src/main/java/org/ohdsi/webapi/security/authc/AuthenticatedLogin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package org.ohdsi.webapi.security.authc;

import org.springframework.security.core.Authentication;
import org.ohdsi.webapi.security.authc.UserOrigin;

import java.util.*;

/**
* Normalized authentication login object that bridges all authentication methods
* (Database, LDAP, Windows, OIDC) into a common structure for the login pipeline.
*
* This class is used by all authentication handlers to standardize the data passed
* to LoginService.onSuccess(), ensuring consistent user creation, role mapping,
* session establishment, and JWT generation across all authentication types.
*/
public class AuthenticatedLogin {

private final String login;
private final String name;
private final UserOrigin origin;
private final Set<String> roles;
private final Authentication originAuthentication;
private final Map<String, Object> attributes;

private AuthenticatedLogin(Builder builder) {
this.login = Objects.requireNonNull(builder.login, "login cannot be null");
this.name = Objects.requireNonNull(builder.name, "name cannot be null");
if (this.login.isBlank()) {
throw new IllegalArgumentException("login cannot be blank");
}
if (this.name.isBlank()) {
throw new IllegalArgumentException("name cannot be blank");
}
this.origin = Objects.requireNonNull(builder.origin, "origin cannot be null");
this.roles = builder.roles != null ? new HashSet<>(builder.roles) : new HashSet<>();
this.originAuthentication = builder.originAuthentication;
this.attributes = builder.attributes != null ? new HashMap<>(builder.attributes) : new HashMap<>();
}

/**
* Gets the normalized login name (typically lowercase).
*/
public String getLogin() {
return login;
}

/**
* Gets the display name for the user.
*/
public String getName() {
return name;
}

/**
* Gets the origin/source of this authentication.
*/
public UserOrigin getOrigin() {
return origin;
}

/**
* Gets the set of WebAPI role names that should be assigned to this user.
* These roles should already be filtered to only valid WebAPI roles
* and mapped from the authentication source (e.g., LDAP groups, OIDC claims).
*/
public Set<String> getRoles() {
return Collections.unmodifiableSet(roles);
}

/**
* Gets the original Spring Authentication object for debugging/auditing purposes.
* May be null if not provided by the authentication handler.
*/
public Authentication getOriginAuthentication() {
return originAuthentication;
}

/**
* Gets optional auth-type-specific attributes.
* Can be used to pass additional data through the login pipeline.
*/
public Map<String, Object> getAttributes() {
return Collections.unmodifiableMap(attributes);
}

/**
* Gets an attribute by key, or null if not present.
*/
public Object getAttribute(String key) {
return attributes.get(key);
}

/**
* Creates a new builder for constructing AuthenticatedLogin instances.
*/
public static Builder builder() {
return new Builder();
}

/**
* Builder for constructing AuthenticatedLogin instances.
*/
public static class Builder {
private String login;
private String name;
private UserOrigin origin;
private Set<String> roles;
private Authentication originAuthentication;
private Map<String, Object> attributes;

/**
* Sets the login name.
*/
public Builder login(String login) {
this.login = login;
return this;
}

/**
* Sets the display name.
*/
public Builder name(String name) {
this.name = name;
return this;
}

/**
* Sets the authentication origin.
*/
public Builder origin(UserOrigin origin) {
this.origin = origin;
return this;
}

/**
* Sets the roles. If called multiple times, the last value is used.
*/
public Builder roles(Set<String> roles) {
this.roles = roles;
return this;
}

/**
* Adds a single role. Can be called multiple times to build up the role set.
*/
public Builder addRole(String role) {
if (this.roles == null) {
this.roles = new HashSet<>();
}
this.roles.add(role);
return this;
}

/**
* Sets the original Spring Authentication object.
*/
public Builder originAuthentication(Authentication originAuthentication) {
this.originAuthentication = originAuthentication;
return this;
}

/**
* Sets auth-type-specific attributes.
*/
public Builder attributes(Map<String, Object> attributes) {
this.attributes = attributes;
return this;
}

/**
* Adds a single attribute. Can be called multiple times.
*/
public Builder attribute(String key, Object value) {
if (this.attributes == null) {
this.attributes = new HashMap<>();
}
this.attributes.put(key, value);
return this;
}

/**
* Builds the AuthenticatedLogin instance.
*/
public AuthenticatedLogin build() {
return new AuthenticatedLogin(this);
}
}

@Override
public String toString() {
return "AuthenticatedLogin{" +
"login='" + login + '\'' +
", name='" + name + '\'' +
", origin=" + origin +
", roles=" + roles +
", attributes=" + attributes +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.authentication.BindAuthenticator;
Expand Down
65 changes: 55 additions & 10 deletions src/main/java/org/ohdsi/webapi/security/authc/LoginController.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

Expand Down Expand Up @@ -80,14 +79,29 @@ public ResponseEntity<LoginService.Result> runAs(
@ConditionalOnProperty(prefix = "security.auth.windows", name = "enabled", havingValue = "true")
public static class Windows {
private final LoginService loginSvc;
private final org.ohdsi.webapi.security.authc.mapper.WindowsGroupToRoleMapper windowsGroupToRoleMapper;

public Windows(LoginService loginSvc) {
public Windows(LoginService loginSvc,
org.ohdsi.webapi.security.authc.mapper.WindowsGroupToRoleMapper windowsGroupToRoleMapper) {
this.loginSvc = loginSvc;
this.windowsGroupToRoleMapper = windowsGroupToRoleMapper;
}

@GetMapping("/user/login/windows")
public LoginService.Result login(Authentication authentication) {
return loginSvc.onSuccess(authentication);
// Map Windows groups to WebAPI roles
java.util.Set<String> roles = windowsGroupToRoleMapper.mapGroupsToRoles(
authentication.getAuthorities());

AuthenticatedLogin authenticatedLogin = AuthenticatedLogin.builder()
.login(authentication.getName())
.name(authentication.getName())
.origin(UserOrigin.WINDOWS)
.roles(roles)
.originAuthentication(authentication)
.build();

return loginSvc.onSuccess(authenticatedLogin);
}
}

Expand All @@ -108,7 +122,14 @@ public Database(LoginService loginSvc,

@GetMapping("/user/login/db")
public LoginService.Result login(Authentication authentication) {
return loginSvc.onSuccess(authentication);
AuthenticatedLogin authenticatedLogin = AuthenticatedLogin.builder()
.login(authentication.getName())
.name(authentication.getName())
.origin(UserOrigin.DATABASE)
.roles(java.util.Collections.emptySet())
.originAuthentication(authentication)
.build();
return loginSvc.onSuccess(authenticatedLogin);
}

@PostMapping(value = "/user/login/db", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
Expand All @@ -122,7 +143,14 @@ public ResponseEntity<?> loginPost(
try {
Authentication auth = dbAuthenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(login, password));
return ResponseEntity.ok(loginSvc.onSuccess(auth));
AuthenticatedLogin authenticatedLogin = AuthenticatedLogin.builder()
.login(auth.getName())
.name(auth.getName())
.origin(UserOrigin.DATABASE)
.roles(java.util.Collections.emptySet())
.originAuthentication(auth)
.build();
return ResponseEntity.ok(loginSvc.onSuccess(authenticatedLogin));
} catch (AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new LoginService.Result(null, null, null, "Invalid credentials"));
Expand All @@ -138,21 +166,38 @@ public ResponseEntity<?> loginPost(
public static class Ldap {

private final LoginService loginSvc;
private final org.ohdsi.webapi.security.authc.mapper.LdapGroupToRoleMapper ldapGroupToRoleMapper;
private static final Logger log = LoggerFactory.getLogger(Ldap.class);

public Ldap(LoginService loginSvc) {
public Ldap(LoginService loginSvc,
org.ohdsi.webapi.security.authc.mapper.LdapGroupToRoleMapper ldapGroupToRoleMapper) {
this.loginSvc = loginSvc;
this.ldapGroupToRoleMapper = ldapGroupToRoleMapper;
}

@GetMapping("/user/login/ldap")
public LoginService.Result login(Authentication authentication) {

List<String> roles = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
List<String> groupNames = authentication.getAuthorities().stream()
.map(org.springframework.security.core.GrantedAuthority::getAuthority)
.toList();

log.info("User {} has roles {}", authentication.getName(), roles);
return loginSvc.onSuccess(authentication);
log.info("User {} has LDAP groups {}", authentication.getName(), groupNames);

// Map LDAP groups to WebAPI roles
java.util.Set<String> roles = ldapGroupToRoleMapper.mapGroupsToRoles(
authentication.getAuthorities(),
org.ohdsi.webapi.security.provisioning.model.LdapProviderType.LDAP);

AuthenticatedLogin authenticatedLogin = AuthenticatedLogin.builder()
.login(authentication.getName())
.name(authentication.getName())
.origin(UserOrigin.LDAP)
.roles(roles)
.originAuthentication(authentication)
.build();

return loginSvc.onSuccess(authenticatedLogin);
}
}

Expand Down
Loading
Loading