skip to content

Explain the difference between OAuth2User and OidcUser, and the role of OAuth2UserService.

level: middleimportance: must knowfreq 55%

answer

  1. OidcUser extends OAuth2User (adds id_token)
  2. DefaultOAuth2UserService vs OidcUserService
  3. userService(...) / oidcUserService(...) hooks
  4. GrantedAuthoritiesMapper for roles
  5. getName() driven by userNameAttributeName

basics

~20 s

OAuth2User is the authenticated principal for plain OAuth2 logins, exposing the provider's user attributes and granted authorities. OidcUser extends it for OIDC logins, adding access to the id_token and standard OIDC claims. OAuth2UserService loads that principal after the token exchange.

solid answer

~40 s

After the authorization-code exchange, Spring calls an `OAuth2UserService<OAuth2UserRequest, OAuth2User>` to load the end-user's identity. For plain OAuth2 (no `openid` scope), `DefaultOAuth2UserService` calls the userinfo endpoint with the access token and returns an `OAuth2User` — a principal exposing `getAttributes()` (the raw provider claims), `getAuthorities()`, and `getName()` (driven by the configured `userNameAttributeName`). For OIDC (scope includes `openid`), `OidcUserService` runs instead and returns an `OidcUser`, which *extends* `OAuth2User` and additionally exposes the parsed `OidcIdToken`, `OidcUserInfo`, and typed standard claims (`getEmail()`, `getSubject()`, etc.). The principal ends up inside an `OAuth2AuthenticationToken`. You typically customize the user service to map provider roles/claims into `GrantedAuthority`s or to enrich the principal with local account data — either by overriding it or wrapping the default and returning a custom `OAuth2User`/`OidcUser`.

code

java · 18 lines
java
// Custom OIDC user service that maps a 'groups' claim to ROLE_* authorities
@Bean
OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
    OidcUserService delegate = new OidcUserService();
    return userRequest -> {
        OidcUser user = delegate.loadUser(userRequest); // does id_token + userinfo work
        Set<GrantedAuthority> authorities = new HashSet<>(user.getAuthorities());
        Object groups = user.getClaims().get("groups");
        if (groups instanceof Collection<?> gs) {
            gs.forEach(g -> authorities.add(new SimpleGrantedAuthority("ROLE_" + g)));
        }
        // Keep it an OidcUser so id_token stays accessible
        return new DefaultOidcUser(authorities, user.getIdToken(), user.getUserInfo());
    };
}

// Wire it in:
// http.oauth2Login(o -> o.userInfoEndpoint(u -> u.oidcUserService(oidcUserService())));

go deeper

for a junior

Know OAuth2User is the logged-in principal and OidcUser is its richer OIDC version with id_token.

for a middle

Explain the two user services, the userService/oidcUserService hooks, and default authorities.

for a senior

Choose between GrantedAuthoritiesMapper and a custom user service, and correctly delegate to the default when overriding.

for a principal

Design claim-to-authority mapping, local account provisioning/JIT, and reject-unknown-user policies across heterogeneous providers.

**Where these fit.** Once Spring has exchanged the authorization code for tokens, it must materialize *who the user is* as a Spring Security principal. That job belongs to an `OAuth2UserService`. Its output becomes the `principal` of the `OAuth2AuthenticationToken` stored in the `SecurityContext`. **OAuth2User (plain OAuth2).** The interface for the principal of a non-OIDC login. Key methods: - `getName()` — the principal name, taken from the attribute named by `userNameAttributeName` in the provider's `UserInfoEndpoint` config (e.g. `id` for GitHub, `sub` for OIDC). Getting this wrong makes `getName()` return null. - `getAttributes()` — a `Map<String,Object>` of raw claims from the userinfo endpoint. - `getAuthorities()` — the `GrantedAuthority`s; by default Spring grants `OAUTH2_USER` plus `SCOPE_*` for each scope. **OidcUser (OIDC).** `OidcUser extends OAuth2User`. It's returned when the login is OIDC (the `openid` scope was requested). Extra capabilities: - `getIdToken()` → `OidcIdToken` — the validated id_token and its claims. - `getUserInfo()` → `OidcUserInfo` — claims from the userinfo endpoint, if called. - `getClaims()` and typed accessors like `getSubject()`, `getEmail()`, `getFullName()`. Default authorities for OIDC include `OIDC_USER` plus `SCOPE_*`. The default concrete type is `DefaultOidcUser`. **OAuth2UserService — the two implementations.** - `DefaultOAuth2UserService` (`OAuth2UserRequest → OAuth2User`): used for non-OIDC. It GETs the userinfo endpoint using the access token and builds a `DefaultOAuth2User`. - `OidcUserService` (`OidcUserRequest → OidcUser`): used for OIDC. It internally delegates to a `DefaultOAuth2UserService` for userinfo (when a userinfo endpoint exists and scopes warrant it), then combines that with the id_token to build a `DefaultOidcUser`. Spring picks the right one automatically based on whether the login is OIDC. **Customizing.** The most common extension is mapping provider claims/roles to authorities. Two approaches: 1. **GrantedAuthoritiesMapper** — a lighter hook (`oauth2Login().userInfoEndpoint().userAuthoritiesMapper(...)`) that only re-maps authorities without replacing the whole loading logic. Great for turning an id_token `roles`/`groups` claim into `ROLE_*`. 2. **Custom OAuth2UserService / OidcUserService** — override `loadUser`, usually by calling the default and then wrapping/replacing the returned user (e.g. to add authorities from a local DB, provision a local account, or reject unknown users). Register via `oauth2Login().userInfoEndpoint().userService(...)` (plain) or `.oidcUserService(...)` (OIDC). **Accessing the principal in controllers.** ```java @GetMapping("/me") public String me(@AuthenticationPrincipal OidcUser user) { return user.getEmail(); } ``` or inject `OAuth2AuthenticationToken` / read from `SecurityContextHolder`. **Gotchas.** - Provider claim shapes differ wildly (GitHub returns `id` as a number, no `email` unless you call a separate endpoint). Don't assume OIDC-standard claims for non-OIDC providers. - If you replace the user service, remember to still delegate to the default so userinfo/id_token processing (and its validation) still happens. - Authorities you *don't* map are simply the defaults (`SCOPE_*`, `OIDC_USER`) — many teams wrongly expect provider roles to appear automatically. - `userNameAttributeName` must point at a claim that actually exists, or `getName()` breaks (and `@AuthenticationPrincipal` name-based logic misbehaves).

  • When would you use a GrantedAuthoritiesMapper instead of a custom OAuth2UserService?
    Use a GrantedAuthoritiesMapper when you only need to transform authorities (e.g. map a roles/groups claim to ROLE_*) and don't need to change how the user is loaded or enrich it from a database. It's a smaller, focused hook that leaves the default userinfo/id_token loading intact.
  • How do you access the logged-in user in a controller?
    Use @AuthenticationPrincipal OidcUser (or OAuth2User) as a method parameter, or inject OAuth2AuthenticationToken / read SecurityContextHolder.getContext().getAuthentication().getPrincipal(). For OIDC you can then call getIdToken(), getEmail(), getClaims(), etc.

saying these in an interview costs you the question

  • Saying OidcUser and OAuth2User are unrelated types (OidcUser extends OAuth2User)
  • Expecting provider roles to become ROLE_* authorities automatically without a mapper/custom service
  • Replacing the user service without delegating to the default, skipping id_token/userinfo processing
  • Assuming every provider returns OIDC-standard claims like email/sub

context