mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-05 13:01:58 +00:00
feat(security): full OAuth 2.1 / OpenID Connect server and OIDC relying-party client
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.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user