Skip to content

Commit c151620

Browse files
committed
Unify login pipeline to accept an AuthenticatedLogin which the login flow can use to get the login, name and groups.
Could not implement a group/claims mapper to WebAPI Roles due to the tight coupling with UserImportJobs and LDAP. Phase2 can incorporate a method of group mapping.
1 parent 8af7932 commit c151620

11 files changed

Lines changed: 987 additions & 77 deletions

articles/LoginPipeline.md

Lines changed: 438 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package org.ohdsi.webapi.security.authc;
2+
3+
import org.springframework.security.core.Authentication;
4+
import org.ohdsi.webapi.security.authc.UserOrigin;
5+
6+
import java.util.*;
7+
8+
/**
9+
* Normalized authentication login object that bridges all authentication methods
10+
* (Database, LDAP, Windows, OIDC) into a common structure for the login pipeline.
11+
*
12+
* This class is used by all authentication handlers to standardize the data passed
13+
* to LoginService.onSuccess(), ensuring consistent user creation, role mapping,
14+
* session establishment, and JWT generation across all authentication types.
15+
*/
16+
public class AuthenticatedLogin {
17+
18+
private final String login;
19+
private final String name;
20+
private final UserOrigin origin;
21+
private final Set<String> roles;
22+
private final Authentication originAuthentication;
23+
private final Map<String, Object> attributes;
24+
25+
private AuthenticatedLogin(Builder builder) {
26+
this.login = Objects.requireNonNull(builder.login, "login cannot be null");
27+
this.name = Objects.requireNonNull(builder.name, "name cannot be null");
28+
if (this.login.isBlank()) {
29+
throw new IllegalArgumentException("login cannot be blank");
30+
}
31+
if (this.name.isBlank()) {
32+
throw new IllegalArgumentException("name cannot be blank");
33+
}
34+
this.origin = Objects.requireNonNull(builder.origin, "origin cannot be null");
35+
this.roles = builder.roles != null ? new HashSet<>(builder.roles) : new HashSet<>();
36+
this.originAuthentication = builder.originAuthentication;
37+
this.attributes = builder.attributes != null ? new HashMap<>(builder.attributes) : new HashMap<>();
38+
}
39+
40+
/**
41+
* Gets the normalized login name (typically lowercase).
42+
*/
43+
public String getLogin() {
44+
return login;
45+
}
46+
47+
/**
48+
* Gets the display name for the user.
49+
*/
50+
public String getName() {
51+
return name;
52+
}
53+
54+
/**
55+
* Gets the origin/source of this authentication.
56+
*/
57+
public UserOrigin getOrigin() {
58+
return origin;
59+
}
60+
61+
/**
62+
* Gets the set of WebAPI role names that should be assigned to this user.
63+
* These roles should already be filtered to only valid WebAPI roles
64+
* and mapped from the authentication source (e.g., LDAP groups, OIDC claims).
65+
*/
66+
public Set<String> getRoles() {
67+
return Collections.unmodifiableSet(roles);
68+
}
69+
70+
/**
71+
* Gets the original Spring Authentication object for debugging/auditing purposes.
72+
* May be null if not provided by the authentication handler.
73+
*/
74+
public Authentication getOriginAuthentication() {
75+
return originAuthentication;
76+
}
77+
78+
/**
79+
* Gets optional auth-type-specific attributes.
80+
* Can be used to pass additional data through the login pipeline.
81+
*/
82+
public Map<String, Object> getAttributes() {
83+
return Collections.unmodifiableMap(attributes);
84+
}
85+
86+
/**
87+
* Gets an attribute by key, or null if not present.
88+
*/
89+
public Object getAttribute(String key) {
90+
return attributes.get(key);
91+
}
92+
93+
/**
94+
* Creates a new builder for constructing AuthenticatedLogin instances.
95+
*/
96+
public static Builder builder() {
97+
return new Builder();
98+
}
99+
100+
/**
101+
* Builder for constructing AuthenticatedLogin instances.
102+
*/
103+
public static class Builder {
104+
private String login;
105+
private String name;
106+
private UserOrigin origin;
107+
private Set<String> roles;
108+
private Authentication originAuthentication;
109+
private Map<String, Object> attributes;
110+
111+
/**
112+
* Sets the login name.
113+
*/
114+
public Builder login(String login) {
115+
this.login = login;
116+
return this;
117+
}
118+
119+
/**
120+
* Sets the display name.
121+
*/
122+
public Builder name(String name) {
123+
this.name = name;
124+
return this;
125+
}
126+
127+
/**
128+
* Sets the authentication origin.
129+
*/
130+
public Builder origin(UserOrigin origin) {
131+
this.origin = origin;
132+
return this;
133+
}
134+
135+
/**
136+
* Sets the roles. If called multiple times, the last value is used.
137+
*/
138+
public Builder roles(Set<String> roles) {
139+
this.roles = roles;
140+
return this;
141+
}
142+
143+
/**
144+
* Adds a single role. Can be called multiple times to build up the role set.
145+
*/
146+
public Builder addRole(String role) {
147+
if (this.roles == null) {
148+
this.roles = new HashSet<>();
149+
}
150+
this.roles.add(role);
151+
return this;
152+
}
153+
154+
/**
155+
* Sets the original Spring Authentication object.
156+
*/
157+
public Builder originAuthentication(Authentication originAuthentication) {
158+
this.originAuthentication = originAuthentication;
159+
return this;
160+
}
161+
162+
/**
163+
* Sets auth-type-specific attributes.
164+
*/
165+
public Builder attributes(Map<String, Object> attributes) {
166+
this.attributes = attributes;
167+
return this;
168+
}
169+
170+
/**
171+
* Adds a single attribute. Can be called multiple times.
172+
*/
173+
public Builder attribute(String key, Object value) {
174+
if (this.attributes == null) {
175+
this.attributes = new HashMap<>();
176+
}
177+
this.attributes.put(key, value);
178+
return this;
179+
}
180+
181+
/**
182+
* Builds the AuthenticatedLogin instance.
183+
*/
184+
public AuthenticatedLogin build() {
185+
return new AuthenticatedLogin(this);
186+
}
187+
}
188+
189+
@Override
190+
public String toString() {
191+
return "AuthenticatedLogin{" +
192+
"login='" + login + '\'' +
193+
", name='" + name + '\'' +
194+
", origin=" + origin +
195+
", roles=" + roles +
196+
", attributes=" + attributes +
197+
'}';
198+
}
199+
}

src/main/java/org/ohdsi/webapi/security/authc/DatabaseAuthConfig.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import org.springframework.http.HttpMethod;
2020
import org.springframework.security.config.Customizer;
2121
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
22-
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
2322
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
2423
import org.springframework.security.crypto.password.PasswordEncoder;
2524
import org.springframework.security.web.SecurityFilterChain;

src/main/java/org/ohdsi/webapi/security/authc/LdapAuthConfig.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import org.springframework.security.authentication.ProviderManager;
1212
import org.springframework.security.config.Customizer;
1313
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
14-
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
1514
import org.springframework.security.core.authority.SimpleGrantedAuthority;
1615
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
1716
import org.springframework.security.ldap.authentication.BindAuthenticator;

src/main/java/org/ohdsi/webapi/security/authc/LoginController.java

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
1414
import org.springframework.security.core.Authentication;
1515
import org.springframework.security.core.AuthenticationException;
16-
import org.springframework.security.core.GrantedAuthority;
1716
import org.springframework.security.access.prepost.PreAuthorize;
1817
import org.springframework.web.bind.annotation.*;
1918

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

84-
public Windows(LoginService loginSvc) {
84+
public Windows(LoginService loginSvc,
85+
org.ohdsi.webapi.security.authc.mapper.WindowsGroupToRoleMapper windowsGroupToRoleMapper) {
8586
this.loginSvc = loginSvc;
87+
this.windowsGroupToRoleMapper = windowsGroupToRoleMapper;
8688
}
8789

8890
@GetMapping("/user/login/windows")
8991
public LoginService.Result login(Authentication authentication) {
90-
return loginSvc.onSuccess(authentication);
92+
// Map Windows groups to WebAPI roles
93+
java.util.Set<String> roles = windowsGroupToRoleMapper.mapGroupsToRoles(
94+
authentication.getAuthorities());
95+
96+
AuthenticatedLogin authenticatedLogin = AuthenticatedLogin.builder()
97+
.login(authentication.getName())
98+
.name(authentication.getName())
99+
.origin(UserOrigin.WINDOWS)
100+
.roles(roles)
101+
.originAuthentication(authentication)
102+
.build();
103+
104+
return loginSvc.onSuccess(authenticatedLogin);
91105
}
92106
}
93107

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

109123
@GetMapping("/user/login/db")
110124
public LoginService.Result login(Authentication authentication) {
111-
return loginSvc.onSuccess(authentication);
125+
AuthenticatedLogin authenticatedLogin = AuthenticatedLogin.builder()
126+
.login(authentication.getName())
127+
.name(authentication.getName())
128+
.origin(UserOrigin.DATABASE)
129+
.roles(java.util.Collections.emptySet())
130+
.originAuthentication(authentication)
131+
.build();
132+
return loginSvc.onSuccess(authenticatedLogin);
112133
}
113134

114135
@PostMapping(value = "/user/login/db", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@@ -122,7 +143,14 @@ public ResponseEntity<?> loginPost(
122143
try {
123144
Authentication auth = dbAuthenticationManager.authenticate(
124145
new UsernamePasswordAuthenticationToken(login, password));
125-
return ResponseEntity.ok(loginSvc.onSuccess(auth));
146+
AuthenticatedLogin authenticatedLogin = AuthenticatedLogin.builder()
147+
.login(auth.getName())
148+
.name(auth.getName())
149+
.origin(UserOrigin.DATABASE)
150+
.roles(java.util.Collections.emptySet())
151+
.originAuthentication(auth)
152+
.build();
153+
return ResponseEntity.ok(loginSvc.onSuccess(authenticatedLogin));
126154
} catch (AuthenticationException e) {
127155
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
128156
.body(new LoginService.Result(null, null, null, "Invalid credentials"));
@@ -138,21 +166,38 @@ public ResponseEntity<?> loginPost(
138166
public static class Ldap {
139167

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

143-
public Ldap(LoginService loginSvc) {
172+
public Ldap(LoginService loginSvc,
173+
org.ohdsi.webapi.security.authc.mapper.LdapGroupToRoleMapper ldapGroupToRoleMapper) {
144174
this.loginSvc = loginSvc;
175+
this.ldapGroupToRoleMapper = ldapGroupToRoleMapper;
145176
}
146177

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

150-
List<String> roles = authentication.getAuthorities().stream()
151-
.map(GrantedAuthority::getAuthority)
181+
List<String> groupNames = authentication.getAuthorities().stream()
182+
.map(org.springframework.security.core.GrantedAuthority::getAuthority)
152183
.toList();
153184

154-
log.info("User {} has roles {}", authentication.getName(), roles);
155-
return loginSvc.onSuccess(authentication);
185+
log.info("User {} has LDAP groups {}", authentication.getName(), groupNames);
186+
187+
// Map LDAP groups to WebAPI roles
188+
java.util.Set<String> roles = ldapGroupToRoleMapper.mapGroupsToRoles(
189+
authentication.getAuthorities(),
190+
org.ohdsi.webapi.security.provisioning.model.LdapProviderType.LDAP);
191+
192+
AuthenticatedLogin authenticatedLogin = AuthenticatedLogin.builder()
193+
.login(authentication.getName())
194+
.name(authentication.getName())
195+
.origin(UserOrigin.LDAP)
196+
.roles(roles)
197+
.originAuthentication(authentication)
198+
.build();
199+
200+
return loginSvc.onSuccess(authenticatedLogin);
156201
}
157202
}
158203

0 commit comments

Comments
 (0)