-
-
Notifications
You must be signed in to change notification settings - Fork 22
/
auth.go
177 lines (140 loc) · 4.24 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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/**
Copyright (C) 2020 Aaron Sky.
This file is part of asc-go, a package for working with Apple's
App Store Connect API.
asc-go is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
asc-go is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with asc-go. If not, see <http://www.gnu.org/licenses/>.
*/
package asc
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net/http"
"time"
"github.com/dgrijalva/jwt-go/v4"
)
// ErrMissingPEM happens when the bytes cannot be decoded as a PEM block.
var ErrMissingPEM = errors.New("no PEM blob found")
// ErrInvalidPrivateKey happens when a key cannot be parsed as a ECDSA PKCS8 private key.
var ErrInvalidPrivateKey = errors.New("key could not be parsed as a valid ecdsa.PrivateKey")
// 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{
Transport: newTransport(),
jwtGenerator: gen,
}, err
}
func parsePrivateKey(blob []byte) (*ecdsa.PrivateKey, error) {
block, _ := pem.Decode(blob)
if block == nil {
return nil, ErrMissingPEM
}
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
if key, ok := parsedKey.(*ecdsa.PrivateKey); ok {
return key, nil
}
return nil, ErrInvalidPrivateKey
}
// 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 {
t.Transport = newTransport()
}
return t.Transport
}
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),
}
}
func newTransport() http.RoundTripper {
return &http.Transport{
IdleConnTimeout: defaultTimeout,
}
}