mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-30 20:57:01 +00:00
92 lines
2.8 KiB
Go
92 lines
2.8 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/bluesky-social/indigo/atproto/atcrypto"
|
|
indigoauth "github.com/bluesky-social/indigo/atproto/auth"
|
|
"github.com/bluesky-social/indigo/atproto/syntax"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
func randomNonce() string {
|
|
buf := make([]byte, 16)
|
|
_, _ = rand.Read(buf)
|
|
return base64.RawURLEncoding.EncodeToString(buf)
|
|
}
|
|
|
|
// AppviewLxm is the lexicon method this token binds to. Hold doesn't enforce
|
|
// lxm today (we just trust the signature), but appview sends it for forward
|
|
// compatibility and so the JWT carries an unambiguous purpose.
|
|
//
|
|
// Kept as a typed NSID rather than a string so callers can't mistype it.
|
|
var AppviewLxm = syntax.NSID("io.atcr.hold.updateCrewTier")
|
|
|
|
// appviewServiceClaims extends RegisteredClaims with the `lxm` claim that
|
|
// indigo's SignServiceAuth uses. We define our own struct because we need to
|
|
// keep Subject (hold's verifier reads it as the acting user DID).
|
|
type appviewServiceClaims struct {
|
|
jwt.RegisteredClaims
|
|
LexMethod string `json:"lxm,omitempty"`
|
|
}
|
|
|
|
// CreateAppviewServiceToken creates a short-lived JWT for appview→hold
|
|
// communication, signed with the appview's private key.
|
|
//
|
|
// Supports both P-256 (ES256) and K-256/secp256k1 (ES256K) keys — the
|
|
// algorithm is selected from the concrete key type.
|
|
//
|
|
// Claims:
|
|
// - iss: appview DID
|
|
// - aud: hold DID
|
|
// - sub: user DID being acted upon
|
|
// - exp: now + 60s
|
|
// - iat: now
|
|
// - jti: random 16-byte nonce (replay prevention)
|
|
// - lxm: lexicon method this token authorizes
|
|
//
|
|
// Signing uses indigo's atproto JWT signing method so the atcrypto key is
|
|
// used natively (no PKCS8 round-trip).
|
|
func CreateAppviewServiceToken(privateKey atcrypto.PrivateKey, appviewDID, holdDID, userDID string) (string, error) {
|
|
var alg string
|
|
switch privateKey.(type) {
|
|
case *atcrypto.PrivateKeyP256:
|
|
alg = "ES256"
|
|
case *atcrypto.PrivateKeyK256:
|
|
alg = "ES256K"
|
|
default:
|
|
return "", fmt.Errorf("unsupported appview key type %T (expected P-256 or K-256)", privateKey)
|
|
}
|
|
|
|
now := time.Now()
|
|
claims := appviewServiceClaims{
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
Issuer: appviewDID,
|
|
Audience: jwt.ClaimStrings{holdDID},
|
|
Subject: userDID,
|
|
ExpiresAt: jwt.NewNumericDate(now.Add(60 * time.Second)),
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
ID: randomNonce(),
|
|
},
|
|
LexMethod: AppviewLxm.String(),
|
|
}
|
|
|
|
sm := jwt.GetSigningMethod(alg)
|
|
if sm == nil {
|
|
return "", fmt.Errorf("%s signing method not registered", alg)
|
|
}
|
|
token := jwt.NewWithClaims(sm, claims)
|
|
signed, err := token.SignedString(privateKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to sign token: %w", err)
|
|
}
|
|
return signed, nil
|
|
}
|
|
|
|
// keep indigoauth referenced so its init() runs and overrides ES256/ES256K
|
|
// signing methods to accept atcrypto.PrivateKey directly.
|
|
var _ = indigoauth.SignServiceAuth
|