forked from pagnihotry/siwago
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsiwaconfig.go
350 lines (298 loc) · 10.4 KB
/
siwaconfig.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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
package siwago
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"net/url"
"strings"
"time"
)
//aud The audience registered claim key, the value of which identifies the recipient the JWT is intended for.
//Since this token is meant for Apple, use https://appleid.apple.com.
//https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens
const AUD = "https://appleid.apple.com"
const APPLE_AUTH_URL = "https://appleid.apple.com/auth/token"
const AUTHORIZATION_CODE = "code"
const REFRESH_TOKEN = "refresh_token"
//struct for JWT Header
type JWTHeader struct {
Alg string `json:"alg"`
Kid string `json:"kid"`
}
//struct for JWT Body
type JWTBody struct {
Iss string `json:"iss"`
Iat int64 `json:"iat"`
Exp int64 `json:"exp"`
Aud string `json:"aud"`
Sub string `json:"sub"`
}
//struct holding various values needed to generate tokens.
//this should only needed to be initialized once and then can be kept in memory
type SiwaConfig struct {
KeyId string //key Id from Certificates, Identifiers & Profiles on developers.apple.com
TokenDelta time.Duration //duration for which you would want the generated client_secret jwt token to be valid. Can not be more than 15777000 (6 months in seconds) from the Current Unix Time on the server.
TeamId string //Team Id that is configured with Key, can also ne found in Certificates, Identifiers & Profiles on developers.apple.com
BundleId string //bundleId for product com.companyname.product
PemFileContents []byte //contents of the p8 file
Nonce string //nonce is set while making the request to generate authorization_code. If you dont use it, keep it an empty string
ClientSecret string //client secret if already generated
}
//helper function to get SiwaConfig object
func GetObject(keyId string, teamId string, bundleId string, d time.Duration, nonce string) *SiwaConfig {
return &SiwaConfig{KeyId: keyId, TokenDelta: d, TeamId: teamId, BundleId: bundleId, Nonce: nonce}
}
//construct a config when you're already generated the secret elsewhere
func GetObjectWithSecret(bundleId, nonce, clientSecret string) *SiwaConfig {
return &SiwaConfig{BundleId: bundleId, Nonce: nonce, ClientSecret: clientSecret}
}
//function to validate the object
func (self *SiwaConfig) ValidateObject() (bool, error) {
return self.ValidateForSecretGeneration()
}
func (self *SiwaConfig) ValidateForSecretGeneration() (bool, error) {
var errorString string = ""
if self.KeyId == "" {
errorString = errorString + " KeyId not set"
}
if self.TokenDelta == 0 {
errorString = errorString + " token exipry set to 0 seconds"
}
if self.TeamId == "" {
errorString = errorString + " TeamId not set"
}
if self.BundleId == "" {
errorString = errorString + " BundleId not set"
}
if len(self.PemFileContents) == 0 {
errorString = errorString + " PemFile not set. Use SetSecretP8File or SetSecretP8String or SetSecretP8Bytes to set it"
}
if errorString != "" {
return false, errors.New(errorString)
}
return true, nil
}
func (self *SiwaConfig) ValidateForTokenExchange() (bool, error) {
//we should either have a ClientSecret set or be fully valid to generate one
if self.ClientSecret == "" {
if ok, err := self.ValidateForSecretGeneration(); !ok {
return false, err
}
}
if self.BundleId == "" {
return false, errors.New("BundleId not set")
}
return true, nil
}
//helper function to set secrets value by filename
//the function expects full path to the p8 file generated
//in the keys and certificates section of developer account
//it should look like this:
//-----BEGIN PRIVATE KEY-----
//jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve
//jkfweshjdjkhjsbjvguybje/vuewkvbbhjdjbdhbjhbvjhbvjhbvbjvbvjvagcve
//jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve
//jkfweshj
//-----END PRIVATE KEY-----
func (self *SiwaConfig) SetSecretP8File(p8Filename string) error {
var content []byte
var err error
content, err = ioutil.ReadFile(p8Filename)
if err != nil {
return err
}
self.PemFileContents = content
return nil
}
//helper function to set secret file contents as a string
//this needs to be pem encoded PKCS8 private key
//same format as the p8 file downloaded from apple
//-----BEGIN PRIVATE KEY-----
//jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve
//jkfweshjdjkhjsbjvguybje/vuewkvbbhjdjbdhbjhbvjhbvjhbvbjvbvjvagcve
//jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve
//jkfweshj
//-----END PRIVATE KEY-----
func (self *SiwaConfig) SetSecretP8String(p8Contents string) {
self.PemFileContents = []byte(p8Contents)
}
//helper function to set secret file contents as bytes
//this needs to be pem encoded PKCS8 private key
//same format as the p8 file downloaded from apple
//-----BEGIN PRIVATE KEY-----
//jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve
//jkfweshjdjkhjsbjvguybje/vuewkvbbhjdjbdhbjhbvjhbvjhbvbjvbvjvagcve
//jkfweshjdjkhjsbjvguybjebvuewkvbbhj+jbdhbjhbvjhbvjhbvbjvbvjvagcve
//jkfweshj
//-----END PRIVATE KEY-----
func (self *SiwaConfig) SetSecretP8Bytes(p8Contents []byte) {
self.PemFileContents = p8Contents
}
//function to get encoded jwt header
func (self *SiwaConfig) GetEncodedJwtHeader(keyId string) (string, error) {
var jwtHeader JWTHeader
var err error
var jwtHeaderJsonB []byte
var jwtHeaderBase64Url string
jwtHeader.Alg = "ES256"
jwtHeader.Kid = keyId
jwtHeaderJsonB, err = json.Marshal(jwtHeader)
if err != nil {
return "", err
}
jwtHeaderBase64Url = base64UrlEncode(jwtHeaderJsonB)
return jwtHeaderBase64Url, nil
}
//function to get encoded jwt body
func (self *SiwaConfig) GetEncodedJwtBody(bundleId string, teamId string, d time.Duration) (string, error) {
var jwtBody JWTBody
var err error
var jwtBodyJsonB []byte
var jwtBodyBase64Url string
jwtBody.Iss = teamId
jwtBody.Iat = time.Now().Unix()
jwtBody.Exp = time.Now().Add(d).Unix()
jwtBody.Aud = AUD
jwtBody.Sub = bundleId
jwtBodyJsonB, err = json.Marshal(jwtBody)
if err != nil {
return "", err
}
jwtBodyBase64Url = base64UrlEncode(jwtBodyJsonB)
return jwtBodyBase64Url, nil
}
//get the client_secret
func (self *SiwaConfig) GetClientSecret() (string, error) {
var err error
var encodedHeader, encodedBody, data, ecdsaHash, clientSecret string
var hash [32]byte
var privKey *ecdsa.PrivateKey
var r, s *big.Int
var hashBytes []byte
if _, err = self.ValidateForSecretGeneration(); err != nil {
return "", err
}
//get encoded heaader
encodedHeader, err = self.GetEncodedJwtHeader(self.KeyId)
if err != nil {
return "", errors.New("Error while encoding JWT header. " + err.Error())
}
//get encoded body
encodedBody, err = self.GetEncodedJwtBody(self.BundleId, self.TeamId, self.TokenDelta)
if err != nil {
return "", errors.New("Error while encoding JWT body. " + err.Error())
}
data = encodedHeader + "." + encodedBody
//compute sha256
hash = sha256.Sum256([]byte(data))
//get the private key object
privKey, err = getPrivKey(self.PemFileContents)
if err != nil {
return "", errors.New("Error while generating private key, check P8 File. " + err.Error())
}
//sign using the private key
r, s, err = ecdsa.Sign(rand.Reader, privKey, hash[:])
if err != nil {
return "", errors.New("Error while signing. " + err.Error())
}
//join r and s
hashBytes = append(r.Bytes(), s.Bytes()...)
//base64urlencode the bytes
ecdsaHash = base64UrlEncode(hashBytes)
//secret is <base64url(jsonHeader)>"."<base64url(jsonBody)>"."<signed hash>
clientSecret = data + "." + ecdsaHash
return clientSecret, nil
}
//function to exchange authorization code for id token, access token, refresh token, etc.
func (self *SiwaConfig) ExchangeAuthCode(code string, redirectUri string) (*Token, error) {
return self.validateWithApple(code, AUTHORIZATION_CODE, redirectUri)
}
//function to exchange refresh token for access token
func (self *SiwaConfig) ExchangeRefreshToken(code string, redirectUri string) (*Token, error) {
return self.validateWithApple(code, REFRESH_TOKEN, redirectUri)
}
//put together the data to make a request to apple
//and return the generated token as an object
func (self *SiwaConfig) validateWithApple(code string, codeType string, redirectUri string) (*Token, error) {
if codeType != AUTHORIZATION_CODE && codeType != REFRESH_TOKEN {
return nil, errors.New(fmt.Sprintf("codeType can be %v or %v. %v recieved", AUTHORIZATION_CODE, REFRESH_TOKEN, codeType))
}
var err error
var clientSecret string
var form url.Values
var c http.Client
var req *http.Request
var resp *http.Response
var bodyContents []byte
var tok Token
var reason string
var siwaIdToken *SiwaIdToken
//check if siwa object is valid, all required values have been set
if _, err = self.ValidateForTokenExchange(); err != nil {
return nil, err
}
//gather form values for post
clientSecret = self.ClientSecret
if clientSecret == "" {
clientSecret, err = self.GetClientSecret()
if err != nil {
return nil, errors.New("Error while generating client_secret. " + err.Error())
}
}
form = url.Values{}
form.Add("client_id", self.BundleId)
form.Add("client_secret", clientSecret)
form.Add(codeType, code)
switch codeType {
case AUTHORIZATION_CODE:
form.Add("grant_type", "authorization_code")
case REFRESH_TOKEN:
form.Add("grant_type", "refresh_token")
}
form.Add("redirect_uri", redirectUri)
//initiate the http request
c = http.Client{Timeout: 5 * time.Second}
req, err = http.NewRequest("POST", APPLE_AUTH_URL, strings.NewReader(form.Encode()))
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err = c.Do(req)
if err != nil {
return nil, err
}
//read response
bodyContents, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
//extract into an object
err = json.Unmarshal(bodyContents, &tok)
if err != nil {
return nil, err
}
//check if there was an error with the request
if tok.Error != "" {
return &tok, errors.New(tok.Error)
}
//validate id token only for authorization code
if tok.IdToken != "" || codeType == AUTHORIZATION_CODE {
siwaIdToken, reason = ValidateIdTokenWithNonce(self.BundleId, tok.IdToken, self.Nonce)
tok.DecodedIdToken = siwaIdToken
//token validity is same as siwa id token validity
tok.Valid = siwaIdToken.Valid
if !tok.Valid {
//if invalid, add message as an error
return &tok, errors.New(reason)
}
} else {
tok.Valid = true
}
return &tok, nil
}