Skip to content

DSUserDetails.getAttributes() returns empty map for OAuth users — violates OAuth2User contract #284

Description

@devondragon

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:

  1. defaultOAuth2UserService.loadUser(request) → returns OAuth2User with full provider attributes
  2. handleOAuthLoginSuccess(registrationId, oAuth2User) → extracts email/name into a User entity
  3. 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:

  1. DSOAuth2UserService.loadUser() passes the original attributes to loginHelperService
  2. LoginHelperService.userLoginHelper() accepts an optional Map<String, Object> attributes parameter
  3. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions