Files

137 lines
3.2 KiB
Go

package jwt
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/hanzo-fi/go-libs/v5/pkg/authn/oidc"
)
type JWTAuth struct {
keySets map[string]oidc.KeySet // issuer -> keySet
// this flag toggles a very basic scope check that assumes 'service:read' or 'service:write' as the only scopes
// for scope checks that give the user more control over which endpoints use which scopes see additional_checks.go
checkScopes bool
service string
additionalChecks []AdditionalCheck
}
func NewJWTAuth(
keySets map[string]oidc.KeySet,
service string,
checkScopes bool,
additionalChecks []AdditionalCheck,
) *JWTAuth {
return &JWTAuth{
keySets: keySets,
checkScopes: checkScopes,
service: service,
additionalChecks: additionalChecks,
}
}
func (ja *JWTAuth) authenticate(r *http.Request) (ControlPlaneAgent, error) {
claims, err := ClaimsFromRequest(r, ja.keySets)
if err != nil {
return nil, err
}
// DefaultControlPlaneAgent provides access to claims that are expected to be present when authenticating via the Control Plane
// in the case of another issuer (eg. Stack authentication) some of these claims may not be present
agt := NewDefaultControlPlaneAgent(*claims)
for _, check := range ja.additionalChecks {
err := check(r, claims)
if err != nil {
return agt, err
}
}
if !ja.checkScopes {
return agt, nil
}
valid, err := checkScopes(ja.service, r.Method, claims.Scopes)
if err != nil || !valid {
return agt, fmt.Errorf("scopes not valid: %w", err)
}
return agt, nil
}
func (ja *JWTAuth) AuthenticateOnControlPlane(r *http.Request) (ControlPlaneAgent, error) {
return ja.authenticate(r)
}
// Authenticate validates the JWT in the request and returns the user, if valid.
func (ja *JWTAuth) Authenticate(_ http.ResponseWriter, r *http.Request) (bool, error) {
_, err := ja.authenticate(r)
if err != nil {
return false, err
}
return true, nil
}
var (
ErrNoAuthorizationHeader = errors.New("no authorization header")
ErrMalformedHeader = errors.New("malformed authorization header")
)
func ClaimsFromRequest(r *http.Request, keySets map[string]oidc.KeySet) (*oidc.AccessTokenClaims, error) {
authHeader := r.Header.Get("authorization")
if authHeader == "" {
return nil, ErrNoAuthorizationHeader
}
authParts := strings.Fields(authHeader)
if len(authParts) != 2 || !strings.EqualFold(authParts[0], "Bearer") {
return nil, ErrMalformedHeader
}
token := authParts[1]
claims := &oidc.AccessTokenClaims{}
decrypted, err := oidc.DecryptToken(token)
if err != nil {
return nil, err
}
payload, err := oidc.ParseToken(decrypted, claims)
if err != nil {
return nil, err
}
keySet, ok := keySets[claims.Issuer]
if !ok {
issuers := make([]string, 0, len(keySets))
for iss := range keySets {
issuers = append(issuers, iss)
}
return claims, fmt.Errorf(
"%w: got: %s, trusted: %v",
oidc.ErrIssuerInvalid,
claims.Issuer,
issuers,
)
}
if _, err = oidc.CheckSignature(
r.Context(),
decrypted,
payload,
[]string{}, // Default to RS256
keySet,
); err != nil {
return claims, err
}
if err := oidc.CheckExpiration(claims, 0); err != nil {
return claims, err
}
if err := oidc.CheckNotBefore(claims, 0); err != nil {
return claims, err
}
return claims, nil
}