Files

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)
}
}