mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-02 19:41:57 +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.
142 lines
4.0 KiB
Go
142 lines
4.0 KiB
Go
package security
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
|
|
)
|
|
|
|
var builtinScopeDescriptions = map[string]string{
|
|
"openid": "Verify your identity",
|
|
"profile": "Read your username and profile",
|
|
"email": "Read your email address",
|
|
"offline_access": "Stay signed in and refresh access without asking again",
|
|
}
|
|
|
|
func (s *OAuthServer) scopeInfos(scopes []string) []OAuthScopeInfo {
|
|
out := make([]OAuthScopeInfo, 0, len(scopes))
|
|
for _, sc := range scopes {
|
|
d := s.cfg.ScopeDescriptions[sc]
|
|
if d == "" {
|
|
d = builtinScopeDescriptions[sc]
|
|
}
|
|
out = append(out, OAuthScopeInfo{Name: sc, Description: d})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func scopesCovered(have, want []string) bool {
|
|
for _, w := range want {
|
|
if !oauthSliceContains(have, w) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// consentRequired reports whether the user has to approve the request on the consent screen.
|
|
func (s *OAuthServer) consentRequired(ctx context.Context, req *authzRequest, client *OAuthServerClient, userID int) (bool, error) {
|
|
if client.FirstParty || req.ConsentDone {
|
|
return false, nil
|
|
}
|
|
if !s.cfg.RequireConsent && !client.RequireConsent {
|
|
return false, nil
|
|
}
|
|
if req.hasPrompt("consent") {
|
|
return true, nil
|
|
}
|
|
g := s.grants()
|
|
if g == nil {
|
|
return true, nil
|
|
}
|
|
c, err := g.GetConsent(ctx, userID, client.ClientID)
|
|
if errors.Is(err, lookup.ErrNotFound) {
|
|
return true, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return !scopesCovered(c.Scopes, req.Scopes), nil
|
|
}
|
|
|
|
func (s *OAuthServer) renderConsent(w http.ResponseWriter, r *http.Request, req *authzRequest, client *OAuthServerClient, sso *ssoSession) {
|
|
req.Bind = sso.SID
|
|
req.LoginDone = true
|
|
if s.cfg.SSOCookie.Disable {
|
|
req.Sess = sso
|
|
}
|
|
state, err := s.sealRequest(w, r, req)
|
|
if err != nil {
|
|
http.Error(w, "server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
name := client.ClientName
|
|
if name == "" {
|
|
name = client.ClientID
|
|
}
|
|
user := ""
|
|
if a := s.anyAuth(); a != nil {
|
|
if info, err := a.OAuthIntrospectToken(r.Context(), sso.Token); err == nil && info.Active {
|
|
user = info.Username
|
|
if user == "" {
|
|
user = info.Email
|
|
}
|
|
}
|
|
}
|
|
s.renderHTML(w, http.StatusOK, s.tmpl.consent, "consent", OAuthConsentPage{
|
|
Title: "Authorize " + name, Action: "authorize", State: state, ClientName: name,
|
|
ClientURI: safeWebURL(client.ClientURI), LogoURI: safeWebURL(client.LogoURI),
|
|
Scopes: s.scopeInfos(req.Scopes), User: user,
|
|
})
|
|
}
|
|
|
|
// safeWebURL returns u when it is an http(s) URL, so client-supplied links cannot be javascript: URLs.
|
|
func safeWebURL(u string) string {
|
|
if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {
|
|
return u
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// consentSubmit handles the consent form.
|
|
func (s *OAuthServer) consentSubmit(w http.ResponseWriter, r *http.Request, req *authzRequest) {
|
|
sso := s.ssoFromRequest(r)
|
|
if sso == nil && req.Sess != nil {
|
|
if a := s.anyAuth(); a != nil {
|
|
if info, err := a.OAuthIntrospectToken(r.Context(), req.Sess.Token); err == nil && info.Active {
|
|
sso = req.Sess
|
|
}
|
|
}
|
|
}
|
|
if sso == nil || req.Bind == "" || req.Bind != sso.SID {
|
|
s.renderMessage(w, http.StatusBadRequest, "Session expired", "Your session has expired. Please start again from the application.", true)
|
|
return
|
|
}
|
|
if r.PostFormValue("decision") != "allow" {
|
|
s.authzRedirectError(w, r, req, "access_denied", "the user denied the request")
|
|
return
|
|
}
|
|
if g := s.grants(); g != nil && r.PostFormValue("remember") == "1" {
|
|
scopes := req.Scopes
|
|
if old, err := g.GetConsent(r.Context(), sso.UserID, req.ClientID); err == nil {
|
|
for _, sc := range old.Scopes {
|
|
if !oauthSliceContains(scopes, sc) {
|
|
scopes = append(scopes, sc)
|
|
}
|
|
}
|
|
}
|
|
if err := g.SaveConsent(r.Context(), lookup.Consent{
|
|
UserID: sso.UserID, ClientID: req.ClientID, Scopes: scopes, ExpiresAt: time.Now().Add(s.cfg.ConsentTTL),
|
|
}); err != nil {
|
|
s.authzRedirectError(w, r, req, "server_error", "could not store the consent")
|
|
return
|
|
}
|
|
}
|
|
req.ConsentDone = true
|
|
s.issueCode(w, r, req, sso)
|
|
}
|