oidc: merge userinfo claims; tick P4-05 in tasks.md
Authelia (and many other IdPs) only put `sub` in the ID token by default, surfacing `preferred_username`/`email`/`groups` from the userinfo endpoint. Fetch userinfo after id_token verification and fold its claims into the parsed claim map; the id_token claims remain authoritative on conflict so the signed assertion still wins. Live sweep against https://auth.dcglab.co.uk verified all four flows: rm-admin → admin JIT, rm-operator → operator JIT (RBAC denies admin pages), rm-viewer → viewer JIT (RBAC denies operator pages), rm-other → no_role_match banner with no row created. Returning rm-admin sign-in resolves to the same row by sub. Screenshots in _diag/p4-05-sweep/.
This commit is contained in:
@@ -101,6 +101,20 @@ func (c *Client) Exchange(ctx context.Context, code, verifier string) (*Claims,
|
||||
if err := idTok.Claims(&raw); err != nil {
|
||||
return nil, "", fmt.Errorf("oidc: claims: %w", err)
|
||||
}
|
||||
// Many IdPs (Authelia among them) only return minimal claims in
|
||||
// the ID token and put profile/email/groups on /userinfo. Fetch
|
||||
// userinfo and merge — id_token claims win on conflict so the
|
||||
// signed assertion remains authoritative.
|
||||
if ui, err := c.provider.UserInfo(ctx, oauth2.StaticTokenSource(tok)); err == nil {
|
||||
var uiClaims map[string]any
|
||||
if err := ui.Claims(&uiClaims); err == nil {
|
||||
for k, v := range uiClaims {
|
||||
if _, present := raw[k]; !present {
|
||||
raw[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseClaims(raw, c.cfg.RoleClaim), rawID, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user