main
go 145 lines 4.14 KB
Raw
1 package auth
2
3 import (
4 "errors"
5 "strings"
6 "time"
7
8 "github.com/decred/dcrd/dcrec/secp256k1/v4"
9 jose "github.com/go-jose/go-jose/v4"
10 "github.com/go-jose/go-jose/v4/jwt"
11
12 "github.com/gosuda/portal-tunnel/v2/portal/identity"
13 "github.com/gosuda/portal-tunnel/v2/types"
14 "github.com/gosuda/portal-tunnel/v2/utils"
15 )
16
17 const (
18 leaseAccessTokenAudience = "portal-sdk"
19 leaseTokenAlgorithm = jose.SignatureAlgorithm("ES256K")
20 )
21
22 type LeaseAccessTokenClaims struct {
23 jwt.Claims
24 Identity types.Identity `json:"identity"`
25 }
26
27 type es256kOpaqueSigner struct {
28 authority identity.Authority
29 }
30
31 func (s *es256kOpaqueSigner) Public() *jose.JSONWebKey {
32 return &jose.JSONWebKey{}
33 }
34
35 func (s *es256kOpaqueSigner) Algs() []jose.SignatureAlgorithm {
36 return []jose.SignatureAlgorithm{leaseTokenAlgorithm}
37 }
38
39 func (s *es256kOpaqueSigner) SignPayload(payload []byte, alg jose.SignatureAlgorithm) ([]byte, error) {
40 if alg != leaseTokenAlgorithm {
41 return nil, jose.ErrUnsupportedAlgorithm
42 }
43 if s == nil || s.authority == nil {
44 return nil, errors.New("signing key is required")
45 }
46 signature, err := s.authority.SignSHA256Secp256k1(payload)
47 if err != nil {
48 return nil, err
49 }
50 return signature.Raw64()
51 }
52
53 type es256kOpaqueVerifier struct {
54 publicKey *secp256k1.PublicKey
55 }
56
57 func (v *es256kOpaqueVerifier) VerifyPayload(payload []byte, signature []byte, alg jose.SignatureAlgorithm) error {
58 if alg != leaseTokenAlgorithm {
59 return jose.ErrUnsupportedAlgorithm
60 }
61 if v == nil || v.publicKey == nil {
62 return errors.New("verification key is required")
63 }
64 if err := identity.VerifySHA256Secp256k1Raw64(payload, signature, v.publicKey); err != nil {
65 if errors.Is(err, identity.ErrSecp256k1SignatureInvalid) {
66 return errors.New("token signature is invalid")
67 }
68 return err
69 }
70 return nil
71 }
72
73 func IssueLeaseAccessToken(authority identity.Authority, issuer string, leaseIdentity types.Identity, ttl time.Duration) (string, LeaseAccessTokenClaims, error) {
74 if authority == nil {
75 return "", LeaseAccessTokenClaims{}, errors.New("lease token signing authority is required")
76 }
77 normalizedIdentity, err := identity.NormalizeIdentity(leaseIdentity)
78 if err != nil {
79 return "", LeaseAccessTokenClaims{}, err
80 }
81
82 signer, err := jose.NewSigner(jose.SigningKey{
83 Algorithm: leaseTokenAlgorithm,
84 Key: &es256kOpaqueSigner{
85 authority: authority,
86 },
87 }, (&jose.SignerOptions{}).WithType("JWT"))
88 if err != nil {
89 return "", LeaseAccessTokenClaims{}, err
90 }
91
92 now := time.Now().UTC()
93 expiresAt := now.Add(ttl)
94 claims := LeaseAccessTokenClaims{
95 Claims: jwt.Claims{
96 Issuer: strings.TrimSpace(issuer),
97 Subject: normalizedIdentity.Key(),
98 Audience: jwt.Audience{leaseAccessTokenAudience},
99 ID: utils.RandomID("tok_"),
100 IssuedAt: jwt.NewNumericDate(now),
101 NotBefore: jwt.NewNumericDate(now),
102 Expiry: jwt.NewNumericDate(expiresAt),
103 },
104 Identity: normalizedIdentity,
105 }
106
107 token, err := jwt.Signed(signer).Claims(claims).Serialize()
108 if err != nil {
109 return "", LeaseAccessTokenClaims{}, err
110 }
111 return token, claims, nil
112 }
113
114 func VerifyLeaseAccessToken(token, publicKeyHex, issuer string, now time.Time) (LeaseAccessTokenClaims, error) {
115 publicKey, err := identity.ParseSecp256k1PublicKeyHex(publicKeyHex)
116 if err != nil {
117 return LeaseAccessTokenClaims{}, err
118 }
119
120 parsed, err := jwt.ParseSigned(strings.TrimSpace(token), []jose.SignatureAlgorithm{leaseTokenAlgorithm})
121 if err != nil {
122 return LeaseAccessTokenClaims{}, err
123 }
124
125 var claims LeaseAccessTokenClaims
126 if err := parsed.Claims(&es256kOpaqueVerifier{publicKey: publicKey}, &claims); err != nil {
127 return LeaseAccessTokenClaims{}, err
128 }
129 normalizedClaimsIdentity, err := identity.NormalizeIdentity(claims.Identity)
130 if err != nil {
131 return LeaseAccessTokenClaims{}, err
132 }
133 if normalizedClaimsIdentity.Key() != claims.Subject {
134 return LeaseAccessTokenClaims{}, errors.New("lease access token identity does not match subject")
135 }
136 claims.Identity = normalizedClaimsIdentity
137 if err := claims.ValidateWithLeeway(jwt.Expected{
138 Issuer: strings.TrimSpace(issuer),
139 AnyAudience: jwt.Audience{leaseAccessTokenAudience},
140 Time: now.UTC(),
141 }, 0); err != nil {
142 return LeaseAccessTokenClaims{}, err
143 }
144 return claims, nil
145 }