mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 20:27:16 +00:00
152 lines
4.3 KiB
Go
152 lines
4.3 KiB
Go
package auth
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/bluesky-social/indigo/atproto/atcrypto"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// TestCreateAppviewServiceToken_Claims locks the claim shape so we can detect
|
|
// regressions when swapping the underlying signer.
|
|
func TestCreateAppviewServiceToken_Claims(t *testing.T) {
|
|
priv, err := atcrypto.GeneratePrivateKeyP256()
|
|
if err != nil {
|
|
t.Fatalf("generate key: %v", err)
|
|
}
|
|
|
|
const (
|
|
appviewDID = "did:web:atcr.io"
|
|
holdDID = "did:web:hold01.atcr.io"
|
|
userDID = "did:plc:pddp4xt5lgnv2qsegbzzs4xg"
|
|
)
|
|
|
|
before := time.Now()
|
|
tokenStr, err := CreateAppviewServiceToken(priv, appviewDID, holdDID, userDID)
|
|
if err != nil {
|
|
t.Fatalf("CreateAppviewServiceToken: %v", err)
|
|
}
|
|
after := time.Now()
|
|
|
|
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
|
parsed, _, err := parser.ParseUnverified(tokenStr, jwt.MapClaims{})
|
|
if err != nil {
|
|
t.Fatalf("parse token: %v", err)
|
|
}
|
|
|
|
if parsed.Method.Alg() != "ES256" {
|
|
t.Errorf("alg = %q, want ES256", parsed.Method.Alg())
|
|
}
|
|
|
|
claims, ok := parsed.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
t.Fatalf("claims type %T", parsed.Claims)
|
|
}
|
|
|
|
if got, _ := claims["iss"].(string); got != appviewDID {
|
|
t.Errorf("iss = %q, want %q", got, appviewDID)
|
|
}
|
|
aud := claims["aud"]
|
|
switch a := aud.(type) {
|
|
case string:
|
|
if a != holdDID {
|
|
t.Errorf("aud = %q, want %q", a, holdDID)
|
|
}
|
|
case []any:
|
|
if len(a) != 1 || a[0] != holdDID {
|
|
t.Errorf("aud = %v, want [%q]", a, holdDID)
|
|
}
|
|
default:
|
|
t.Errorf("aud unexpected type %T = %v", aud, aud)
|
|
}
|
|
|
|
exp, err := claims.GetExpirationTime()
|
|
if err != nil || exp == nil {
|
|
t.Fatalf("exp missing: %v", err)
|
|
}
|
|
delta := exp.Sub(before)
|
|
if delta < 55*time.Second || delta > 65*time.Second {
|
|
t.Errorf("exp delta from sign time = %v, want ~60s", delta)
|
|
}
|
|
|
|
iat, err := claims.GetIssuedAt()
|
|
if err != nil || iat == nil {
|
|
t.Fatalf("iat missing: %v", err)
|
|
}
|
|
if iat.Before(before.Add(-1*time.Second)) || iat.After(after.Add(1*time.Second)) {
|
|
t.Errorf("iat = %v, outside sign window [%v, %v]", iat.Time, before, after)
|
|
}
|
|
|
|
if got, _ := claims["sub"].(string); got != userDID {
|
|
t.Errorf("sub = %q, want %q", got, userDID)
|
|
}
|
|
if got, _ := claims["lxm"].(string); got != AppviewLxm.String() {
|
|
t.Errorf("lxm = %q, want %q", got, AppviewLxm.String())
|
|
}
|
|
if got, _ := claims["jti"].(string); got == "" {
|
|
t.Error("jti claim missing — required for replay protection")
|
|
}
|
|
}
|
|
|
|
// TestCreateAppviewServiceToken_K256UsesES256K — when given a secp256k1 key,
|
|
// the token must be signed with ES256K so K-256-only appviews (legacy ATProto
|
|
// PDS keys) can still authenticate to hold.
|
|
func TestCreateAppviewServiceToken_K256UsesES256K(t *testing.T) {
|
|
priv, err := atcrypto.GeneratePrivateKeyK256()
|
|
if err != nil {
|
|
t.Fatalf("generate K-256: %v", err)
|
|
}
|
|
tok, err := CreateAppviewServiceToken(priv, "did:web:atcr.io", "did:web:hold01.atcr.io", "did:plc:pddp4xt5lgnv2qsegbzzs4xg")
|
|
if err != nil {
|
|
t.Fatalf("sign: %v", err)
|
|
}
|
|
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
|
parsed, _, err := parser.ParseUnverified(tok, jwt.MapClaims{})
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if parsed.Method.Alg() != "ES256K" {
|
|
t.Errorf("alg = %q, want ES256K", parsed.Method.Alg())
|
|
}
|
|
}
|
|
|
|
// TestCreateAppviewServiceToken_HasJTI — after the swap to indigo's
|
|
// SignServiceAuth, every token carries a non-empty `jti` (random nonce) so
|
|
// hold can reject replays. Two consecutive tokens get distinct jtis.
|
|
func TestCreateAppviewServiceToken_HasJTI(t *testing.T) {
|
|
priv, err := atcrypto.GeneratePrivateKeyP256()
|
|
if err != nil {
|
|
t.Fatalf("generate key: %v", err)
|
|
}
|
|
const (
|
|
appviewDID = "did:web:atcr.io"
|
|
holdDID = "did:web:hold01.atcr.io"
|
|
userDID = "did:plc:pddp4xt5lgnv2qsegbzzs4xg"
|
|
)
|
|
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
|
|
|
jti := func(t *testing.T) string {
|
|
t.Helper()
|
|
s, err := CreateAppviewServiceToken(priv, appviewDID, holdDID, userDID)
|
|
if err != nil {
|
|
t.Fatalf("sign: %v", err)
|
|
}
|
|
tok, _, err := parser.ParseUnverified(s, jwt.MapClaims{})
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
claims, _ := tok.Claims.(jwt.MapClaims)
|
|
val, _ := claims["jti"].(string)
|
|
return val
|
|
}
|
|
a := jti(t)
|
|
if a == "" {
|
|
t.Fatal("jti claim missing or empty — needed for replay prevention")
|
|
}
|
|
b := jti(t)
|
|
if a == b {
|
|
t.Errorf("two tokens share jti %q; expected distinct nonces", a)
|
|
}
|
|
}
|