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.
86 lines
2.8 KiB
Go
86 lines
2.8 KiB
Go
package security
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
|
|
)
|
|
|
|
const parURIPrefix = "urn:ietf:params:oauth:request_uri:"
|
|
|
|
// --------------------------------------------------------------------------
|
|
// RFC 9126 — Pushed authorization requests: POST /oauth/par
|
|
// --------------------------------------------------------------------------
|
|
|
|
func (s *OAuthServer) parHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
writeOAuthError(w, "invalid_request", "cannot parse form", http.StatusBadRequest)
|
|
return
|
|
}
|
|
ac, e := s.requireClient(r)
|
|
if e != nil {
|
|
e.write(w)
|
|
return
|
|
}
|
|
if r.PostForm.Get("request_uri") != "" {
|
|
writeOAuthError(w, "invalid_request", "request_uri must not be pushed", http.StatusBadRequest)
|
|
return
|
|
}
|
|
form := url.Values{}
|
|
for k, v := range r.PostForm {
|
|
switch k {
|
|
case "client_secret", "client_assertion", "client_assertion_type":
|
|
continue
|
|
}
|
|
form[k] = v
|
|
}
|
|
form.Set("client_id", ac.Client.ClientID)
|
|
if _, fail := s.parseAuthz(r.Context(), form, true); fail != nil {
|
|
writeOAuthError(w, fail.code, fail.desc, http.StatusBadRequest)
|
|
return
|
|
}
|
|
id, err := randomOAuthToken()
|
|
gs := s.grants()
|
|
if err != nil || gs == nil {
|
|
writeOAuthError(w, "server_error", "", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
uri := parURIPrefix + id
|
|
if err := gs.SavePushedRequest(r.Context(), lookup.PushedRequest{
|
|
RequestURI: uri, ClientID: ac.Client.ClientID, Params: map[string]string{"q": form.Encode()},
|
|
ExpiresAt: time.Now().Add(s.cfg.PARTTL),
|
|
}); err != nil {
|
|
writeOAuthError(w, "server_error", "", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, map[string]any{"request_uri": uri, "expires_in": int(s.cfg.PARTTL.Seconds())})
|
|
}
|
|
|
|
// resolvePushedRequest consumes the pushed request behind request_uri (single use).
|
|
func (s *OAuthServer) resolvePushedRequest(ctx context.Context, clientID, uri string) (url.Values, *oauthError) {
|
|
gs := s.grants()
|
|
if gs == nil || (!s.cfg.EnablePAR && !s.cfg.RequirePAR) {
|
|
return nil, oerr("request_uri_not_supported", "pushed authorization requests are not enabled", http.StatusBadRequest)
|
|
}
|
|
pr, err := gs.ConsumePushedRequest(ctx, uri)
|
|
if err != nil {
|
|
return nil, oerr("invalid_request_uri", "request_uri is unknown, expired or already used", http.StatusBadRequest)
|
|
}
|
|
if clientID != "" && clientID != pr.ClientID {
|
|
return nil, oerr("invalid_request", "client_id does not match the pushed request", http.StatusBadRequest)
|
|
}
|
|
q, err := url.ParseQuery(pr.Params["q"])
|
|
if err != nil {
|
|
return nil, oerr("invalid_request_uri", "stored request is unreadable", http.StatusBadRequest)
|
|
}
|
|
q.Set("client_id", pr.ClientID)
|
|
return q, nil
|
|
}
|