Skip to content

Commit 96bc1e2

Browse files
authored
Merge pull request #285 from devondragon/fix/284-oauth2-attributes-empty
fix(oauth2): populate DSUserDetails.getAttributes() with provider attributes
2 parents 91822e2 + 7c372cf commit 96bc1e2

8 files changed

Lines changed: 524 additions & 35 deletions

File tree

src/main/java/com/digitalsanctuary/spring/user/service/DSOAuth2UserService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
219219
String registrationId = userRequest.getClientRegistration().getRegistrationId();
220220
log.debug("registrationId: {}", registrationId);
221221
User dbUser = handleOAuthLoginSuccess(registrationId, user);
222-
DSUserDetails userDetails = loginHelperService.userLoginHelper(dbUser);
222+
DSUserDetails userDetails = loginHelperService.userLoginHelper(dbUser, user.getAttributes());
223223
return userDetails;
224224
}
225225

src/main/java/com/digitalsanctuary/spring/user/service/DSOidcUserService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,6 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio
198198
String registrationId = userRequest.getClientRegistration().getRegistrationId();
199199
log.debug("registrationId: {}", registrationId);
200200
User dbUser = handleOidcLoginSuccess(registrationId, user);
201-
return loginHelperService.userLoginHelper(dbUser, user.getUserInfo(), user.getIdToken());
201+
return loginHelperService.userLoginHelper(dbUser, user.getUserInfo(), user.getIdToken(), user.getAttributes());
202202
}
203203
}

src/main/java/com/digitalsanctuary/spring/user/service/DSUserDetails.java

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.ArrayList;
44
import java.util.Collection;
5+
import java.util.Collections;
56
import java.util.HashMap;
67
import java.util.Map;
78
import org.springframework.security.core.GrantedAuthority;
@@ -60,15 +61,27 @@ public class DSUserDetails implements UserDetails, OidcUser {
6061
private OidcIdToken oidcIdToken;
6162

6263
/**
63-
* Instantiates a new DS user details.
64+
* Instantiates a new DS user details with OAuth2 provider attributes.
6465
*
6566
* @param user the user
6667
* @param grantedAuthorities the granted authorities (optional, default = empty list)
68+
* @param attributes the OAuth2 provider attributes (optional, falls back to User entity fields)
6769
*/
68-
public DSUserDetails(User user, Collection<? extends GrantedAuthority> grantedAuthorities) {
70+
public DSUserDetails(User user, Collection<? extends GrantedAuthority> grantedAuthorities, Map<String, Object> attributes) {
6971
this.user = user;
7072
this.grantedAuthorities = grantedAuthorities != null ? grantedAuthorities : new ArrayList<>();
71-
this.attributes = new HashMap<>();
73+
this.attributes = attributes != null ? new HashMap<>(attributes) : buildFallbackAttributes(user);
74+
}
75+
76+
/**
77+
* Instantiates a new DS user details without provider attributes. Attributes will be populated
78+
* from the {@link User} entity fields as a fallback.
79+
*
80+
* @param user the user
81+
* @param grantedAuthorities the granted authorities (optional, default = empty list)
82+
*/
83+
public DSUserDetails(User user, Collection<? extends GrantedAuthority> grantedAuthorities) {
84+
this(user, grantedAuthorities, (Map<String, Object>) null);
7285
}
7386

7487
/**
@@ -77,35 +90,91 @@ public DSUserDetails(User user, Collection<? extends GrantedAuthority> grantedAu
7790
* @param user the user
7891
*/
7992
public DSUserDetails(User user) {
80-
this(user, null);
93+
this(user, null, (Map<String, Object>) null);
8194
}
8295

8396
/**
84-
* Instantiates a new DS user details.
97+
* Instantiates a new DS user details with OIDC tokens and provider attributes.
8598
*
8699
* @param user the user
87100
* @param oidcUserInfo containing claims about the user
88101
* @param oidcIdToken containing claims about the user
89102
* @param grantedAuthorities the granted authorities (optional, default = empty list)
103+
* @param attributes the OAuth2/OIDC provider attributes (optional, falls back to idToken claims or User entity)
90104
*/
91105
@Builder
92-
public DSUserDetails(User user, OidcUserInfo oidcUserInfo, OidcIdToken oidcIdToken, Collection<? extends GrantedAuthority> grantedAuthorities) {
106+
public DSUserDetails(User user, OidcUserInfo oidcUserInfo, OidcIdToken oidcIdToken,
107+
Collection<? extends GrantedAuthority> grantedAuthorities, Map<String, Object> attributes) {
93108
this.user = user;
94109
this.oidcUserInfo = oidcUserInfo;
95110
this.oidcIdToken = oidcIdToken;
96111
this.grantedAuthorities = grantedAuthorities != null ? grantedAuthorities : new ArrayList<>();
112+
if (attributes != null) {
113+
this.attributes = new HashMap<>(attributes);
114+
} else if (oidcIdToken != null) {
115+
this.attributes = new HashMap<>(oidcIdToken.getClaims());
116+
} else {
117+
this.attributes = buildFallbackAttributes(user);
118+
}
97119
}
98120

99121
/**
100-
* Instantiates a new DS user details.
122+
* Instantiates a new DS user details with OIDC tokens. Attributes will be populated from the
123+
* OIDC ID token claims or {@link User} entity as a fallback.
124+
*
125+
* @param user the user
126+
* @param oidcUserInfo containing claims about the user
127+
* @param oidcIdToken containing claims about the user
128+
* @param grantedAuthorities the granted authorities (optional, default = empty list)
129+
*/
130+
public DSUserDetails(User user, OidcUserInfo oidcUserInfo, OidcIdToken oidcIdToken,
131+
Collection<? extends GrantedAuthority> grantedAuthorities) {
132+
this(user, oidcUserInfo, oidcIdToken, grantedAuthorities, null);
133+
}
134+
135+
/**
136+
* Instantiates a new DS user details with OIDC tokens and no authorities.
101137
*
102138
* @param user the user
103139
* @param oidcUserInfo containing claims about the user
104140
* @param oidcIdToken containing claims about the user
105141
*/
106-
@Builder
107142
public DSUserDetails(User user, OidcUserInfo oidcUserInfo, OidcIdToken oidcIdToken) {
108-
this(user, oidcUserInfo, oidcIdToken, null);
143+
this(user, oidcUserInfo, oidcIdToken, null, null);
144+
}
145+
146+
/**
147+
* Builds a fallback attributes map from the {@link User} entity fields. Used when no provider
148+
* attributes are available (e.g., local/password login).
149+
*
150+
* @param user the user entity
151+
* @return a map containing available user fields using standard OAuth2/OIDC claim names
152+
*/
153+
private static Map<String, Object> buildFallbackAttributes(User user) {
154+
Map<String, Object> attrs = new HashMap<>();
155+
if (user.getEmail() != null) {
156+
attrs.put("email", user.getEmail());
157+
}
158+
if (user.getFirstName() != null) {
159+
attrs.put("given_name", user.getFirstName());
160+
}
161+
if (user.getLastName() != null) {
162+
attrs.put("family_name", user.getLastName());
163+
}
164+
StringBuilder name = new StringBuilder();
165+
if (user.getFirstName() != null && !user.getFirstName().trim().isEmpty()) {
166+
name.append(user.getFirstName().trim());
167+
}
168+
if (user.getLastName() != null && !user.getLastName().trim().isEmpty()) {
169+
if (name.length() > 0) {
170+
name.append(' ');
171+
}
172+
name.append(user.getLastName().trim());
173+
}
174+
if (name.length() > 0) {
175+
attrs.put("name", name.toString());
176+
}
177+
return attrs;
109178
}
110179

111180
/**
@@ -189,7 +258,7 @@ public User getUser() {
189258

190259
@Override
191260
public Map<String, Object> getAttributes() {
192-
return attributes;
261+
return Collections.unmodifiableMap(attributes);
193262
}
194263

195264
@Override

src/main/java/com/digitalsanctuary/spring/user/service/LoginHelperService.java

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.Collection;
44
import java.util.Date;
5+
import java.util.Map;
56
import org.springframework.security.core.GrantedAuthority;
67
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
78
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
@@ -34,41 +35,68 @@ public class LoginHelperService {
3435
private final AuthorityService authorityService;
3536

3637
/**
37-
* Helper method to authenticate a user after login. This method is called from the DSUserDetailsService and DSOAuth2UserService classes after a
38-
* user has been successfully authenticated.
38+
* Helper method to authenticate a user after login. This method is called from the DSUserDetailsService after a user has been successfully
39+
* authenticated via local/password login. Attributes are populated from the {@link User} entity as a fallback.
3940
*
4041
* @param dbUser The user to authenticate.
4142
* @return The user details object.
4243
*/
4344
public DSUserDetails userLoginHelper(User dbUser) {
45+
return userLoginHelper(dbUser, (Map<String, Object>) null);
46+
}
47+
48+
/**
49+
* Helper method to authenticate a user after OAuth2 login, preserving the original provider attributes so that
50+
* {@link DSUserDetails#getAttributes()} returns the full set of attributes from the OAuth2 provider.
51+
*
52+
* @param dbUser The user to authenticate.
53+
* @param attributes The OAuth2 provider attributes (may be null for local login fallback).
54+
* @return The user details object with provider attributes set.
55+
*/
56+
public DSUserDetails userLoginHelper(User dbUser, Map<String, Object> attributes) {
4457
// Updating lastActivity date for this login
4558
dbUser.setLastActivityDate(new Date());
4659

4760
// Check if the user account is locked, but should be unlocked now, and unlock it
4861
dbUser = loginAttemptService.checkIfUserShouldBeUnlocked(dbUser);
4962

5063
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromUser(dbUser);
51-
DSUserDetails userDetails = new DSUserDetails(dbUser, authorities);
52-
return userDetails;
64+
return new DSUserDetails(dbUser, authorities, attributes);
5365
}
5466

5567
/**
5668
* Helper method to authenticate an OIDC user after login, attaching the OIDC-specific tokens
57-
* and claims to the principal while keeping {@link DSUserDetails} immutable.
69+
* and claims to the principal while keeping {@link DSUserDetails} immutable. Attributes are
70+
* populated from the OIDC ID token claims as a fallback.
5871
*
5972
* @param dbUser The user to authenticate.
6073
* @param oidcUserInfo The OIDC user info claims.
6174
* @param oidcIdToken The OIDC ID token.
6275
* @return The user details object with OIDC tokens set.
6376
*/
6477
public DSUserDetails userLoginHelper(User dbUser, OidcUserInfo oidcUserInfo, OidcIdToken oidcIdToken) {
78+
return userLoginHelper(dbUser, oidcUserInfo, oidcIdToken, null);
79+
}
80+
81+
/**
82+
* Helper method to authenticate an OIDC user after login, preserving the original provider attributes and
83+
* attaching the OIDC-specific tokens and claims to the principal.
84+
*
85+
* @param dbUser The user to authenticate.
86+
* @param oidcUserInfo The OIDC user info claims.
87+
* @param oidcIdToken The OIDC ID token.
88+
* @param attributes The OIDC provider attributes (may be null to fall back to idToken claims).
89+
* @return The user details object with OIDC tokens and provider attributes set.
90+
*/
91+
public DSUserDetails userLoginHelper(User dbUser, OidcUserInfo oidcUserInfo, OidcIdToken oidcIdToken,
92+
Map<String, Object> attributes) {
6593
// Updating lastActivity date for this login
6694
dbUser.setLastActivityDate(new Date());
6795

6896
// Check if the user account is locked, but should be unlocked now, and unlock it
6997
dbUser = loginAttemptService.checkIfUserShouldBeUnlocked(dbUser);
7098

7199
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromUser(dbUser);
72-
return new DSUserDetails(dbUser, oidcUserInfo, oidcIdToken, authorities);
100+
return new DSUserDetails(dbUser, oidcUserInfo, oidcIdToken, authorities, attributes);
73101
}
74102
}

src/test/java/com/digitalsanctuary/spring/user/service/DSOAuth2UserServiceTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import static org.mockito.ArgumentMatchers.any;
66
import static org.mockito.ArgumentMatchers.anyString;
77
import static org.mockito.ArgumentMatchers.eq;
8+
import org.mockito.ArgumentMatchers;
9+
810
import static org.mockito.Mockito.*;
911

1012
import java.util.Arrays;
@@ -416,7 +418,7 @@ void shouldLoadUserThroughOAuth2RequestFlow() {
416418
when(spyService.defaultOAuth2UserService.loadUser(userRequest)).thenReturn(googleUser);
417419

418420
DSUserDetails mockUserDetails = mock(DSUserDetails.class);
419-
when(loginHelperService.userLoginHelper(any(User.class))).thenReturn(mockUserDetails);
421+
when(loginHelperService.userLoginHelper(any(User.class), ArgumentMatchers.<Map<String, Object>>any())).thenReturn(mockUserDetails);
420422
when(userRepository.findByEmail("loadtest@gmail.com")).thenReturn(null);
421423
when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0));
422424

@@ -425,7 +427,7 @@ void shouldLoadUserThroughOAuth2RequestFlow() {
425427

426428
// Then
427429
assertThat(result).isEqualTo(mockUserDetails);
428-
verify(loginHelperService).userLoginHelper(any(User.class));
430+
verify(loginHelperService).userLoginHelper(any(User.class), ArgumentMatchers.<Map<String, Object>>any());
429431
verify(userRepository).save(any(User.class));
430432
}
431433
}

src/test/java/com/digitalsanctuary/spring/user/service/DSOidcUserServiceTest.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
55
import static org.mockito.ArgumentMatchers.any;
66
import static org.mockito.Mockito.*;
7+
import org.mockito.ArgumentMatchers;
78

89
import java.util.Arrays;
910
import java.util.List;
11+
import java.util.Map;
1012
import org.junit.jupiter.api.BeforeEach;
1113
import org.junit.jupiter.api.DisplayName;
1214
import org.junit.jupiter.api.Nested;
@@ -372,13 +374,14 @@ void shouldLoadUserThroughOidcRequestFlow() {
372374
user.setId(999L);
373375
return user;
374376
});
375-
when(loginHelperService.userLoginHelper(any(User.class), any(OidcUserInfo.class), any(OidcIdToken.class)))
377+
when(loginHelperService.userLoginHelper(any(User.class), any(OidcUserInfo.class), any(OidcIdToken.class), ArgumentMatchers.<Map<String, Object>>any()))
376378
.thenAnswer(invocation -> {
377379
User user = invocation.getArgument(0);
378380
OidcUserInfo oidcUserInfo = invocation.getArgument(1);
379381
OidcIdToken oidcIdToken = invocation.getArgument(2);
382+
Map<String, Object> attributes = invocation.getArgument(3);
380383
return new DSUserDetails(user, oidcUserInfo, oidcIdToken,
381-
List.of(new SimpleGrantedAuthority("ROLE_USER")));
384+
List.of(new SimpleGrantedAuthority("ROLE_USER")), attributes);
382385
});
383386

384387
// When
@@ -391,9 +394,10 @@ void shouldLoadUserThroughOidcRequestFlow() {
391394
assertThat(dsUserDetails.getIdToken()).isNotNull();
392395
assertThat(dsUserDetails.getUserInfo()).isNotNull();
393396
assertThat(dsUserDetails.getAuthorities()).isNotEmpty();
397+
assertThat(dsUserDetails.getAttributes()).isNotEmpty();
394398

395399
verify(userRepository).save(any(User.class));
396-
verify(loginHelperService).userLoginHelper(any(User.class), any(OidcUserInfo.class), any(OidcIdToken.class));
400+
verify(loginHelperService).userLoginHelper(any(User.class), any(OidcUserInfo.class), any(OidcIdToken.class), ArgumentMatchers.<Map<String, Object>>any());
397401
}
398402

399403
@Test

0 commit comments

Comments
 (0)