Summary
DSUserDetails implements OAuth2User, but getAttributes() always returns an empty HashMap for OAuth-authenticated users. This silently violates the OAuth2User interface contract, which specifies that getAttributes() should return the attributes from the OAuth2 provider (e.g., email, name, picture, sub).
Any code following the standard Spring Security pattern — oAuth2User.getAttribute("email") — will get null, even though the user's email is available internally via getUsername().
Root Cause
In DSOAuth2UserService.loadUser(), the flow is:
defaultOAuth2UserService.loadUser(request) → returns OAuth2User with full provider attributes
handleOAuthLoginSuccess(registrationId, oAuth2User) → extracts email/name into a User entity
loginHelperService.userLoginHelper(user) → creates DSUserDetails(user, authorities)
Step 3 uses the simple constructor, which initializes attributes as an empty HashMap:
// DSUserDetails(User, Collection<authorities>) constructor
this.attributes = new HashMap<>(); // always empty — provider attributes are lost
The original OAuth2User's attributes from step 1 are never passed through to DSUserDetails.
Additionally, the OIDC constructor DSUserDetails(User, OidcUserInfo, OidcIdToken, Collection) doesn't initialize attributes at all, leaving it as null.
Impact
- Silent failures: Code using the standard
getAttribute("email") pattern gets null with no error or warning
- Forces framework-specific workarounds: Consumers must know to cast to
UserDetails and call getUsername() instead, or cast to DSUserDetails and call getUser().getEmail()
- Breaks interoperability: Third-party Spring Security integrations that inspect
OAuth2User.getAttributes() will see an empty map
Discovered In
MagicMenu project — an MVC interceptor that processes invite codes after OAuth login. Despite 5+ fix attempts, the interceptor silently failed because it used the standard oAuth2User.getAttribute("email") pattern. The fix required knowing to use ((UserDetails) principal).getUsername() instead.
Proposed Fix
Option A (minimal): Populate attributes from the User entity in the simple constructor:
public DSUserDetails(User user, Collection<? extends GrantedAuthority> authorities) {
this.user = user;
this.grantedAuthorities = authorities != null ? authorities : new ArrayList<>();
this.attributes = new HashMap<>();
if (user.getEmail() != null) this.attributes.put("email", user.getEmail());
if (user.getFirstName() != null) this.attributes.put("given_name", user.getFirstName());
if (user.getLastName() != null) this.attributes.put("family_name", user.getLastName());
if (user.getFullName() != null) this.attributes.put("name", user.getFullName());
}
Option B (more complete): Pass the original OAuth2User attributes through the chain:
DSOAuth2UserService.loadUser() passes the original attributes to loginHelperService
LoginHelperService.userLoginHelper() accepts an optional Map<String, Object> attributes parameter
DSUserDetails stores and returns those original provider attributes
Option B preserves all provider-specific attributes (picture URL, locale, email_verified, etc.) and fully satisfies the OAuth2User contract. Option A covers the 90% case with less change.
Additional: OIDC Constructor
The OIDC constructor should also initialize attributes — either from the OidcIdToken claims or as an empty map to avoid NullPointerException:
public DSUserDetails(User user, OidcUserInfo userInfo, OidcIdToken idToken,
Collection<? extends GrantedAuthority> authorities) {
// ... existing code ...
this.attributes = idToken != null ? new HashMap<>(idToken.getClaims()) : new HashMap<>();
}
Environment
- DS Spring User Framework: 4.3.0
- Spring Boot: 4.0.1
- Spring Security: 7.0.3
- OAuth2 Provider: Google (OpenID Connect)
Summary
DSUserDetailsimplementsOAuth2User, butgetAttributes()always returns an emptyHashMapfor OAuth-authenticated users. This silently violates theOAuth2Userinterface contract, which specifies thatgetAttributes()should return the attributes from the OAuth2 provider (e.g.,email,name,picture,sub).Any code following the standard Spring Security pattern —
oAuth2User.getAttribute("email")— will getnull, even though the user's email is available internally viagetUsername().Root Cause
In
DSOAuth2UserService.loadUser(), the flow is:defaultOAuth2UserService.loadUser(request)→ returnsOAuth2Userwith full provider attributeshandleOAuthLoginSuccess(registrationId, oAuth2User)→ extracts email/name into aUserentityloginHelperService.userLoginHelper(user)→ createsDSUserDetails(user, authorities)Step 3 uses the simple constructor, which initializes
attributesas an emptyHashMap:The original
OAuth2User's attributes from step 1 are never passed through toDSUserDetails.Additionally, the OIDC constructor
DSUserDetails(User, OidcUserInfo, OidcIdToken, Collection)doesn't initializeattributesat all, leaving it asnull.Impact
getAttribute("email")pattern getsnullwith no error or warningUserDetailsand callgetUsername()instead, or cast toDSUserDetailsand callgetUser().getEmail()OAuth2User.getAttributes()will see an empty mapDiscovered In
MagicMenu project — an MVC interceptor that processes invite codes after OAuth login. Despite 5+ fix attempts, the interceptor silently failed because it used the standard
oAuth2User.getAttribute("email")pattern. The fix required knowing to use((UserDetails) principal).getUsername()instead.Proposed Fix
Option A (minimal): Populate attributes from the
Userentity in the simple constructor:Option B (more complete): Pass the original
OAuth2Userattributes through the chain:DSOAuth2UserService.loadUser()passes the original attributes tologinHelperServiceLoginHelperService.userLoginHelper()accepts an optionalMap<String, Object> attributesparameterDSUserDetailsstores and returns those original provider attributesOption B preserves all provider-specific attributes (picture URL, locale,
email_verified, etc.) and fully satisfies theOAuth2Usercontract. Option A covers the 90% case with less change.Additional: OIDC Constructor
The OIDC constructor should also initialize
attributes— either from theOidcIdTokenclaims or as an empty map to avoidNullPointerException:Environment