go-tell/internal/security/token.go

74 lines
2.2 KiB
Go
Raw Permalink Normal View History

2026-09-19 16:45:24 +02:00
// Package security decides whether a request carries the identity asserted by
// the upstream single-sign-on proxy. The CMS never signs users in itself: the
// proxy authenticates the owner and forwards a header on every accepted
// request. Requests without that header are anonymous.
package security
import (
"crypto/subtle"
"net/http"
"strings"
)
// ITokenVerifier reports whether a request was authenticated by the SSO proxy.
type ITokenVerifier interface {
Authenticated(*http.Request) bool
HeaderName() string
}
// SSOTokenVerifier trusts the header injected by the identity-aware proxy. An
// empty Header disables header lookups; Cookie optionally names a cookie that
// carries the same value. When Token is non-empty the forwarded value must
// match it exactly, otherwise the presence of any non-empty value is accepted.
type SSOTokenVerifier struct {
Header string
Token string
Cookie string
}
// NewSSOTokenVerifier builds a verifier for the given header, optional expected
// token, and optional cookie fallback.
func NewSSOTokenVerifier(header, token, cookie string) SSOTokenVerifier {
return SSOTokenVerifier{Header: header, Token: token, Cookie: cookie}
}
// HeaderName returns the proxy header carrying the SSO token.
func (v SSOTokenVerifier) HeaderName() string { return v.Header }
// Authenticated reports whether the request carries an accepted SSO token.
func (v SSOTokenVerifier) Authenticated(r *http.Request) bool {
return v.accepted(v.presented(r))
}
func (v SSOTokenVerifier) presented(r *http.Request) string {
if v.Header != "" {
if value := trimBearer(r.Header.Get(v.Header)); value != "" {
return value
}
}
if v.Cookie != "" {
if c, err := r.Cookie(v.Cookie); err == nil {
return trimBearer(c.Value)
}
}
return ""
}
func (v SSOTokenVerifier) accepted(presented string) bool {
if presented == "" {
return false
}
if v.Token == "" {
return true
}
return subtle.ConstantTimeCompare([]byte(presented), []byte(v.Token)) == 1
}
func trimBearer(value string) string {
value = strings.TrimSpace(value)
if len(value) >= len("bearer ") && strings.EqualFold(value[:len("bearer ")], "bearer ") {
return strings.TrimSpace(value[len("bearer "):])
}
return value
}