-
-
Notifications
You must be signed in to change notification settings - Fork 22
/
auth.go
129 lines (115 loc) · 3.15 KB
/
auth.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package asc
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net/http"
"time"
"github.com/dgrijalva/jwt-go/v4"
)
// AuthTransport is an http.RoundTripper implementation that stores the JWT created.
// If the token expires, the Rotate function should be called to update the stored token.
type AuthTransport struct {
Transport http.RoundTripper
jwtGenerator jwtGenerator
}
type jwtGenerator interface {
Token() (string, error)
IsValid() bool
}
type standardJWTGenerator struct {
keyID string
issuerID string
expireDuration time.Duration
privateKey *ecdsa.PrivateKey
token string
}
// NewTokenConfig returns a new AuthTransport instance that customizes the Authentication header of the request during transport.
// It can be customized further by supplying a custom http.RoundTripper instance to the Transport field.
func NewTokenConfig(keyID string, issuerID string, expireDuration time.Duration, privateKey []byte) (*AuthTransport, error) {
key, err := parsePrivateKey(privateKey)
if err != nil {
return nil, err
}
gen := &standardJWTGenerator{
keyID: keyID,
issuerID: issuerID,
privateKey: key,
expireDuration: expireDuration,
}
_, err = gen.Token()
return &AuthTransport{
jwtGenerator: gen,
}, err
}
func parsePrivateKey(blob []byte) (*ecdsa.PrivateKey, error) {
block, _ := pem.Decode(blob)
if block == nil {
return nil, errors.New("no PEM blob found")
}
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
if key, ok := parsedKey.(*ecdsa.PrivateKey); ok {
return key, nil
}
return nil, errors.New("key could not be parsed as a valid ecdsa.PrivateKey")
}
// RoundTrip implements the http.RoundTripper interface to set the Authorization header.
func (t AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
token, err := t.jwtGenerator.Token()
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return t.transport().RoundTrip(req)
}
// Client returns a new http.Client instance for use with asc.Client.
func (t *AuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}
func (t *AuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}
func (g *standardJWTGenerator) Token() (string, error) {
if g.IsValid() {
return g.token, nil
}
t := jwt.NewWithClaims(jwt.SigningMethodES256, g.claims())
t.Header["kid"] = g.keyID
token, err := t.SignedString(g.privateKey)
if err != nil {
return "", err
}
g.token = token
return token, nil
}
func (g *standardJWTGenerator) IsValid() bool {
if g.token == "" {
return false
}
parsed, err := jwt.Parse(
g.token,
jwt.KnownKeyfunc(jwt.SigningMethodES256, g.privateKey),
jwt.WithAudience("appstoreconnect-v1"),
jwt.WithIssuer(g.issuerID),
)
if err != nil {
return false
}
return parsed.Valid
}
func (g *standardJWTGenerator) claims() jwt.Claims {
expiry := time.Now().Add(g.expireDuration)
return jwt.StandardClaims{
Audience: jwt.ClaimStrings{"appstoreconnect-v1"},
Issuer: g.issuerID,
ExpiresAt: jwt.At(expiry),
}
}