Files
at-container-registry/pkg/auth/appview_token.go
T

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