Skip to content

feat(resource,serverresource): DPoP-Nonce and UserInfo response helpers - #520

Merged
osanderson merged 2 commits into
mainfrom
feat/userinfo-dpop-nonce-helpers
Oct 2, 2026
Merged

osanderson merged 2 commits into
mainfrom
feat/userinfo-dpop-nonce-helpers

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Summary

This is DevX follow-up: two pieces every protected endpoint hand-wrote.

authz, err := verifier.Verify(ctx, resource.VerifyRequestFromHTTP(r, userinfoURL))
// ...
body, err := serverresource.UserInfoClaims(ctx, authz, identityClaims)
authz.SetDPoPNonce(w.Header())
  • resource.AuthorizationContext.SetDPoPNonce(h http.Header) sets DPoP-Nonce when Verify issued a next nonce (RFC 9449 §8), so the client's next call carries a fresh one. It replaces the same if authz.NextDPoPNonce != "" block in identity-check and in the conformance AS's UserInfo and accounts handlers.
  • serverresource.UserInfoClaims(ctx, authz, source) builds a UserInfo response's claims (OIDC Core §5.3.2) for an endpoint hosted with the server. It lives in serverresource because it uses server.RequestedUserinfoClaimsKey and server.IdentityClaimsSource with resource.AuthorizationContext, and server and resource don't import each other.
    • Scope check. A token without openid gets a 403 insufficient_scope *resource.Error.
    • Which claims. The requested and approved claim names come from the token. A malformed entry is an error, not an empty request; identity-check silently ignored one.
    • The source is asked for those names only, and not at all when there are none, so it never returns every claim it knows.
    • Filtering. A claim the source returns beyond the requested ones is dropped, whatever the source does. sub is always the token's subject.
  • Call sites. identity-check's UserInfo handler and the conformance AS's userinfoHandler use UserInfoClaims. The conformance AS keeps its own 403 for a token without openid. identity-check's w.Write of the signed JWT gets a #nosec G705: gosec now traces taint through the helper, and the response is an application/jwt JWS, never HTML. This matches the suppression already in conformance-federation-trust-anchor.
  • Docs. RequestedUserinfoClaimsKey's doc and GETTING_STARTED's resource-server section point at both helpers.
  • Additive (feat).

Tests

  • TestUserInfoClaimsReturnsOnlyWhatWasRequested: the source returns email, name, phone and a different sub. The result is email, name and the token's sub; the source was asked for exactly [email name].
  • TestUserInfoClaimsWithNothingRequestedIsSubjectOnly: with no entry, [] or null, the result is sub alone and the source is never called.
  • TestUserInfoClaimsRefuses: a token without openid gives 403 insufficient_scope; a malformed entry is an error; a source error is wrapped (errors.Is).
  • TestSetDPoPNonce: the header is set with a nonce and absent without one.
  • Mutation checks: removing the filtering, the openid check, or the skip-when-none, or reading a malformed entry as none, each fails a test.
  • Other checks:
    • go test ./cmd/... passes, including the conformance AS's UserInfo tests.
    • go test -race ./resource/... ./serverresource/... ./server/... passes.
    • golangci-lint is clean.
    • identity-check's tests and lint pass.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

osanderson and others added 2 commits October 2, 2026 15:39
Two pieces of every protected endpoint were hand-written each time:

- resource.AuthorizationContext.SetDPoPNonce(h) sets the DPoP-Nonce
  header when Verify issued a next nonce (RFC 9449 §8), so the client's
  next call carries a fresh one. It replaces the same three-line check
  in identity-check and the conformance AS's two resource handlers.
- serverresource.UserInfoClaims(ctx, authz, source) builds a UserInfo
  response's claims (OIDC Core §5.3.2) for an endpoint hosted with the
  server:
  - it refuses a token without the openid scope with 403
    insufficient_scope;
  - it reads which claims were requested and approved from the token
    (server.RequestedUserinfoClaimsKey), and a malformed entry is an
    error rather than an empty request;
  - it asks the IdentityClaimsSource for those only, and not at all
    when there are none;
  - it drops anything the source returns beyond them, and sets "sub"
    from the token.

  identity-check and the conformance AS used it, and identity-check had
  been ignoring a malformed entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UserInfo handler's error path now has one call, writeResourceError,
which already answers a non-resource error with a 500.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@osanderson
osanderson force-pushed the feat/userinfo-dpop-nonce-helpers branch from 84d24c2 to 32b39b9 Compare October 2, 2026 07:39
@sonarqubecloud

sonarqubecloud Bot commented Oct 2, 2026

Copy link
Copy Markdown

@osanderson
osanderson merged commit 35c4229 into main Oct 2, 2026
17 checks passed
@osanderson
osanderson deleted the feat/userinfo-dpop-nonce-helpers branch October 2, 2026 07:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant