mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-21 00:50:29 +00:00
109 lines
3.3 KiB
Go
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
|
|
}
|