mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-01 19:20:31 +00:00
Authorization server: consent and scopes, OIDC (nonce, auth_time, acr, sid, at_hash, signed userinfo, RP-initiated and back-channel logout), managed refresh tokens with rotation and reuse detection, RFC 9068 JWT access tokens, DPoP, PAR, device grant, token exchange, private_key_jwt, RFC 7591/7592 registration, RFC 9207 iss, signing keyring with rotation. State is DB-backed through a new lookup.OAuthGrantStore (procedure and direct backends, four dialect DDLs, conformance cases). Client side: WithOIDC discovery, PKCE, nonce, id_token validation, OAuth2LogoutURL. PeekRefresh now returns already rotated tokens so RotateRefresh can detect reuse. Docs: OAUTH2_SERVER.md, oauth2_full_example.go, breaking_changes.md step 8.
77 lines
2.8 KiB
Go
77 lines
2.8 KiB
Go
package security
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
tokenTypeAccess = "urn:ietf:params:oauth:token-type:access_token" //nolint:gosec // RFC 8693 URN, not a credential
|
|
tokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt" //nolint:gosec // RFC 8693 URN, not a credential
|
|
)
|
|
|
|
// handleTokenExchange implements RFC 8693 for access tokens issued by this server: the caller
|
|
// trades a subject token for one with a narrower scope and/or another audience. Delegation with an
|
|
// actor_token is not supported.
|
|
func (s *OAuthServer) handleTokenExchange(r *http.Request) (map[string]any, *oauthError) {
|
|
ac, e := s.requireClient(r)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
client := ac.Client
|
|
if ac.Method == "none" {
|
|
return nil, invalidClient("token exchange requires a confidential client", true)
|
|
}
|
|
if !grantAllowed(client, grantTokenExchange) {
|
|
return nil, oerr("unauthorized_client", "client may not use token exchange", http.StatusBadRequest)
|
|
}
|
|
if r.PostFormValue("actor_token") != "" {
|
|
return nil, oerr("invalid_request", "actor_token is not supported", http.StatusBadRequest)
|
|
}
|
|
if t := r.PostFormValue("subject_token_type"); t != "" && t != tokenTypeAccess && t != tokenTypeJWT {
|
|
return nil, oerr("invalid_request", "unsupported subject_token_type", http.StatusBadRequest)
|
|
}
|
|
if t := r.PostFormValue("requested_token_type"); t != "" && t != tokenTypeAccess {
|
|
return nil, oerr("invalid_request", "only access tokens can be issued", http.StatusBadRequest)
|
|
}
|
|
subject := r.PostFormValue("subject_token")
|
|
if subject == "" {
|
|
return nil, oerr("invalid_request", "subject_token required", http.StatusBadRequest)
|
|
}
|
|
ti := s.resolveAccessToken(r.Context(), subject)
|
|
if ti == nil {
|
|
return nil, oerr("invalid_grant", "subject_token is invalid or inactive", http.StatusBadRequest)
|
|
}
|
|
if ti.JKT != "" {
|
|
return nil, oerr("invalid_request", "DPoP-bound tokens cannot be exchanged", http.StatusBadRequest)
|
|
}
|
|
|
|
scopes := ti.Scopes
|
|
if req := strings.Fields(r.PostFormValue("scope")); len(req) > 0 {
|
|
if !scopesCovered(ti.Scopes, req) || (len(client.AllowedScopes) > 0 && !scopesCovered(client.AllowedScopes, req)) {
|
|
return nil, oerr("invalid_scope", "scope exceeds the subject token or the client", http.StatusBadRequest)
|
|
}
|
|
scopes = req
|
|
}
|
|
var resource []string
|
|
for _, v := range append(r.PostForm["audience"], r.PostForm["resource"]...) {
|
|
if v == "" {
|
|
continue
|
|
}
|
|
if u, err := url.Parse(v); r.PostForm["resource"] != nil && (err != nil || !u.IsAbs() || u.Fragment != "") {
|
|
return nil, oerr("invalid_target", "resource must be an absolute URI", http.StatusBadRequest)
|
|
}
|
|
resource = append(resource, v)
|
|
}
|
|
|
|
resp, e := s.mintTokens(r.Context(), &tokenGrant{
|
|
Client: client, UserID: ti.UserID, Scopes: scopes, Resource: resource, SID: ti.SID,
|
|
})
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
resp["issued_token_type"] = tokenTypeAccess
|
|
return resp, nil
|
|
}
|