Files
at-container-registry/pkg/auth/token/claims.go
2026-03-29 13:01:40 -07:00

109 lines
3.3 KiB
Go

// Package token provides JWT claims and token handling for registry authentication.
package token
import (
"time"
"atcr.io/pkg/auth"
"github.com/golang-jwt/jwt/v5"
)
// Auth method constants
const (
AuthMethodOAuth = "oauth"
AuthMethodAppPassword = "app_password"
)
// Claims represents the JWT claims for registry authentication
// This follows the Docker Registry token specification
type Claims struct {
jwt.RegisteredClaims
Access []auth.AccessEntry `json:"access,omitempty"`
AuthMethod string `json:"auth_method,omitempty"` // "oauth" or "app_password"
}
// NewClaims creates a new Claims structure with standard fields
func NewClaims(subject, issuer, audience string, expiration time.Duration, access []auth.AccessEntry, authMethod string) *Claims {
now := time.Now()
return &Claims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: subject, // User's DID
Issuer: issuer, // "atcr.io"
Audience: jwt.ClaimStrings{audience}, // Service name
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(expiration)),
},
Access: access,
AuthMethod: authMethod, // "oauth" or "app_password"
}
}
// ExtractAuthMethod parses a JWT token string and extracts the auth_method claim
// Returns the auth method or empty string if not found or token is invalid
// This does NOT validate the token - it only parses it to extract the claim
func ExtractAuthMethod(tokenString string) string {
// Parse token without validation (we only need the claims, validation is done by distribution library)
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
token, _, err := parser.ParseUnverified(tokenString, &Claims{})
if err != nil {
return "" // Invalid token format
}
claims, ok := token.Claims.(*Claims)
if !ok {
return "" // Wrong claims type
}
return claims.AuthMethod
}
// ExtractAccess parses a JWT token string and extracts the access entries (scopes)
// Returns nil if not found or token is invalid
// This does NOT validate the token - it only parses it to extract the claim
func ExtractAccess(tokenString string) []auth.AccessEntry {
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
token, _, err := parser.ParseUnverified(tokenString, &Claims{})
if err != nil {
return nil
}
claims, ok := token.Claims.(*Claims)
if !ok {
return nil
}
return claims.Access
}
// HasPushScope checks if any access entry contains a "push" action
func HasPushScope(access []auth.AccessEntry) bool {
for _, entry := range access {
for _, action := range entry.Actions {
if action == "push" {
return true
}
}
}
return false
}
// ExtractSubject parses a JWT token string and extracts the Subject claim (the user's DID)
// Returns the subject or empty string if not found or token is invalid
// This does NOT validate the token - it only parses it to extract the claim
func ExtractSubject(tokenString string) string {
// Parse token without validation (we only need the claims, validation is done by distribution library)
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
token, _, err := parser.ParseUnverified(tokenString, &Claims{})
if err != nil {
return "" // Invalid token format
}
claims, ok := token.Claims.(*Claims)
if !ok {
return "" // Wrong claims type
}
return claims.Subject
}