Skip to content

Commit 46a44e6

Browse files
committed
feat: JWT/SIWE auth primitives (pkg/auth/jwt)
Self-contained, reusable auth building blocks with no app wiring: - Issuer: mint short-lived RS256 JWTs; publish the public key as JWKS - Validator: verify bearer JWTs (in-process key or remote JWKS) - SIWEVerifier: validate EIP-4361 login messages and recover the signer - RequireAuth: bearer-token HTTP middleware - ParseRSAPrivateKey: load the base64-PEM signing key Folds in the previously-dead auth.JWTValidator (relocated from pkg/auth/jwt.go).
1 parent 83d4116 commit 46a44e6

16 files changed

Lines changed: 1065 additions & 202 deletions

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/lib/pq v1.12.3
1313
github.com/prometheus/client_golang v1.20.5
1414
github.com/shopspring/decimal v1.4.0
15+
github.com/spruceid/siwe-go v0.2.1
1516
github.com/stretchr/testify v1.11.1
1617
github.com/testcontainers/testcontainers-go v0.40.0
1718
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0
@@ -45,8 +46,9 @@ require (
4546
github.com/cpuguy83/dockercfg v0.3.2 // indirect
4647
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
4748
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
49+
github.com/dchest/uniuri v1.2.0 // indirect
4850
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
49-
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
51+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
5052
github.com/distribution/reference v0.6.0 // indirect
5153
github.com/docker/docker v28.5.1+incompatible // indirect
5254
github.com/docker/go-connections v0.6.0 // indirect
@@ -87,6 +89,7 @@ require (
8789
github.com/prometheus/common v0.66.1 // indirect
8890
github.com/prometheus/procfs v0.16.1 // indirect
8991
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
92+
github.com/relvacode/iso8601 v1.1.1-0.20210511065120-b30b151cc433 // indirect
9093
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
9194
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
9295
github.com/sirupsen/logrus v1.9.3 // indirect

go.sum

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
6464
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6565
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
6666
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
67+
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
68+
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
6769
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
6870
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
6971
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
7072
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
71-
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
72-
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
73+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
74+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
7375
github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0=
7476
github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
7577
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
@@ -267,6 +269,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
267269
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
268270
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
269271
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
272+
github.com/relvacode/iso8601 v1.1.1-0.20210511065120-b30b151cc433 h1:mLbKGKe5gDGHE8uJLYMmA/fkp/htaXEMl2Hj0k4xfYE=
273+
github.com/relvacode/iso8601 v1.1.1-0.20210511065120-b30b151cc433/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I=
270274
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
271275
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
272276
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -284,6 +288,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp
284288
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
285289
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
286290
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
291+
github.com/spruceid/siwe-go v0.2.1 h1:BroySys6CyUzeyNppTseEOT/w56xTdOfcmECTI7rnuc=
292+
github.com/spruceid/siwe-go v0.2.1/go.mod h1:MHpHbptGsM3lHth2L8quhZ9ipiwST8zsJH1CjWpeO1k=
287293
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
288294
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
289295
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=

pkg/auth/jwt.go

Lines changed: 0 additions & 199 deletions
This file was deleted.

pkg/auth/jwt/helpers_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package jwt
4+
5+
import (
6+
"crypto/ecdsa"
7+
"crypto/rand"
8+
"crypto/rsa"
9+
"encoding/hex"
10+
"fmt"
11+
"maps"
12+
"testing"
13+
"time"
14+
15+
"github.com/ethereum/go-ethereum/common"
16+
"github.com/ethereum/go-ethereum/crypto"
17+
gojwt "github.com/golang-jwt/jwt/v5"
18+
siwe "github.com/spruceid/siwe-go"
19+
)
20+
21+
const (
22+
testDomain = "localhost"
23+
testURI = "http://localhost"
24+
testChainID = 31337
25+
testIssuer = "canton-middleware"
26+
testAud = "canton-middleware-api"
27+
)
28+
29+
func newTestKey(t *testing.T) *rsa.PrivateKey {
30+
t.Helper()
31+
key, err := rsa.GenerateKey(rand.Reader, 2048)
32+
if err != nil {
33+
t.Fatalf("generate RSA key: %v", err)
34+
}
35+
return key
36+
}
37+
38+
// signSIWE builds an EIP-4361 message for the given nonce (with optional overrides
39+
// such as expirationTime), signs it with a freshly generated EOA key, and returns
40+
// the raw message, its 0x-prefixed signature, and the signer's checksummed address.
41+
func signSIWE(t *testing.T, nonce string, opts map[string]any) (raw, signature string, addr common.Address) {
42+
t.Helper()
43+
44+
priv, err := crypto.GenerateKey()
45+
if err != nil {
46+
t.Fatalf("generate EOA key: %v", err)
47+
}
48+
addr = crypto.PubkeyToAddress(priv.PublicKey)
49+
50+
options := map[string]any{
51+
"chainId": testChainID,
52+
"issuedAt": time.Now().UTC().Format(time.RFC3339),
53+
}
54+
maps.Copy(options, opts)
55+
56+
msg, err := siwe.InitMessage(testDomain, addr.Hex(), testURI, nonce, options)
57+
if err != nil {
58+
t.Fatalf("init SIWE message: %v", err)
59+
}
60+
return msg.String(), signPersonal(t, priv, msg.String()), addr
61+
}
62+
63+
// signPersonal produces an EIP-191 personal_sign signature over message.
64+
func signPersonal(t *testing.T, priv *ecdsa.PrivateKey, message string) string {
65+
t.Helper()
66+
prefixed := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), message)
67+
hash := crypto.Keccak256Hash([]byte(prefixed))
68+
sig, err := crypto.Sign(hash.Bytes(), priv)
69+
if err != nil {
70+
t.Fatalf("sign: %v", err)
71+
}
72+
return "0x" + hex.EncodeToString(sig)
73+
}
74+
75+
// mintRS256 signs claims as an RS256 JWT with the given key and kid header. It lets
76+
// validator tests craft tokens with arbitrary claims without going through Issuer.
77+
func mintRS256(t *testing.T, key *rsa.PrivateKey, kid string, claims gojwt.MapClaims) string {
78+
t.Helper()
79+
token := gojwt.NewWithClaims(gojwt.SigningMethodRS256, claims)
80+
token.Header["kid"] = kid
81+
signed, err := token.SignedString(key)
82+
if err != nil {
83+
t.Fatalf("sign RS256 token: %v", err)
84+
}
85+
return signed
86+
}

0 commit comments

Comments
 (0)