forked from pagnihotry/siwago
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtoken.go
219 lines (196 loc) · 7.2 KB
/
token.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
package siwago
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"encoding/json"
"fmt"
"strings"
"time"
)
//struct for JWT Header
type JWTTokenHeader struct {
Alg string `json:"alg"`
Kid string `json:"kid"`
}
//struct for JWT Body
type JWTTokenBody struct {
Iss string `json:"iss"`
Iat int64 `json:"iat"`
Exp int64 `json:"exp"`
Aud string `json:"aud"`
Sub string `json:"sub"`
AtHash string `json:"at_hash"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
IsPrivateEmail bool `json:"is_private_email"`
RealUserStatus int64 `json:"real_user_status"`
AuthTime int64 `json:"auth_time"`
Nonce string `json:"nonce"`
}
// struct for unmarshaling JWT Body: note the different type for EmailVerified
//
// This works around Apple's bizarre API where "email_verified" is allowed to
// be either a string or a bool.
//
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple
type serializedJWTTokenBody struct {
Iss string `json:"iss"`
Iat int64 `json:"iat"`
Exp int64 `json:"exp"`
Aud string `json:"aud"`
Sub string `json:"sub"`
AtHash string `json:"at_hash"`
Email string `json:"email"`
EmailVerified StringyBool `json:"email_verified"`
IsPrivateEmail StringyBool `json:"is_private_email"`
RealUserStatus int64 `json:"real_user_status"`
AuthTime int64 `json:"auth_time"`
Nonce string `json:"nonce"`
}
//struct to hold the decoded idtoken
type SiwaIdToken struct {
Header *JWTTokenHeader
Body *JWTTokenBody
Signature []byte
Valid bool
}
//struct for token returned from apple
type Token struct {
//(Reserved for future use) A token used to access allowed data. Currently, no data set has been defined for access.
AccessToken string `json:"access_token"`
//The type of access token. It will always be bearer.
TokenType string `json:"token_type"`
//The amount of time, in seconds, before the access token expires.
ExpiresIn int64 `json:"expires_in"`
//The refresh token used to regenerate new access tokens. Store this token securely on your server.
RefreshToken string `json:"refresh_token"`
//A JSON Web Token that contains the user’s identity information.
IdToken string `json:"id_token"`
//Set if ErrorResponse is recieved
//A string that describes the reason for the unsuccessful request. The string consists of a single allowed value.
//Possible values: invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope
Error string `json:"error"`
//After the token is fetched from apple, id token is validated
//this field stores the result of the validation check
Valid bool `json:"_"`
//The decoded Id token
//Holds the decoded JWT Header, Body, Signature and result of validity check
DecodedIdToken *SiwaIdToken `json:"_"`
}
func (self Token) String() string {
return fmt.Sprintf("AccessToken: %v, TokenType: %v, ExpiresIn:%v, RefreshToken:%v, IdToken:%v, Error:%v, Valid:%v",
self.AccessToken, self.TokenType, self.ExpiresIn, self.RefreshToken, self.IdToken, self.Error, self.Valid)
}
//function to verify idtoken signature for apple published public key
func verifyAppleRSA256(message string, signature []byte, kid string) error {
//get the public key
rsaPublicKey, err := getApplePublicKeyObject(kid, "RS256")
if err != nil {
return err
}
//if key found, validate
if rsaPublicKey != nil {
bytesToHash := []byte(message)
//get hash
hashed := sha256.Sum256(bytesToHash)
err = rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hashed[:], signature)
if err != nil {
return err
}
}
return nil
}
//validates idToken without nonce check
func ValidateIdToken(aud string, idToken string) (*SiwaIdToken, string) {
return ValidateIdTokenWithNonce(aud, idToken, "")
}
//validates idtoken
//more info: https://developer.apple.com/documentation/signinwithapplerestapi/verifying_a_user
func ValidateIdTokenWithNonce(aud string, idToken string, nonce string) (*SiwaIdToken, string) {
//initialize the token object
var siwaIdToken *SiwaIdToken = &SiwaIdToken{Valid: false}
if idToken == "" {
return siwaIdToken, "empty_token"
}
//split and decode token
parts := strings.Split(idToken, ".")
if len(parts) != 3 {
return siwaIdToken, "invalid_format_missing_parts"
}
jsonHeaderB, err := base64UrlDecode(parts[0])
if err != nil {
return siwaIdToken, "invalid_format_header_base64_decode_failed error:" + err.Error()
}
var jwtHeader JWTTokenHeader
err = json.Unmarshal(jsonHeaderB, &jwtHeader)
if err != nil {
return siwaIdToken, "invalid_format_header_json_decode_failed error:" + err.Error()
}
jsonBodyB, err := base64UrlDecode(parts[1])
if err != nil {
return siwaIdToken, "invalid_format_body_base64_decode_failed error:" + err.Error()
}
// Two-step deserialization to handle the maybe-bool-maybe-string EmailVerified and IsPrivateEmail fields
var parsedJWTBody serializedJWTTokenBody
err = json.Unmarshal(jsonBodyB, &parsedJWTBody)
if err != nil {
return siwaIdToken, "invalid_format_body_json_decode_failed error:" + err.Error()
}
jwtBody := &JWTTokenBody{
Iss: parsedJWTBody.Iss,
Iat: parsedJWTBody.Iat,
Exp: parsedJWTBody.Exp,
Aud: parsedJWTBody.Aud,
Sub: parsedJWTBody.Sub,
AtHash: parsedJWTBody.AtHash,
Email: parsedJWTBody.Email,
EmailVerified: bool(parsedJWTBody.EmailVerified),
IsPrivateEmail: bool(parsedJWTBody.IsPrivateEmail),
RealUserStatus: parsedJWTBody.RealUserStatus,
AuthTime: parsedJWTBody.AuthTime,
Nonce: parsedJWTBody.Nonce,
}
//the basic validation tests pass. Now check if the contents of token are valid
var reason string
var valid bool = true
//Verify the nonce for the authentication
//if idtoken had nonce, the check will fail
if jwtBody.Nonce != "" && jwtBody.Nonce != nonce {
reason = reason + "nonce_check_failed"
valid = false
}
//Verify that the iss field contains https://appleid.apple.com
if jwtBody.Iss != "https://appleid.apple.com" {
reason = reason + " iss_check_failed"
valid = false
}
//Verify that the aud field is the developer’s client_id
if jwtBody.Aud != aud {
reason = reason + " aud_check_failed"
valid = false
}
//Verify that the time is earlier than the exp value of the token
if jwtBody.Exp < time.Now().Unix() {
reason = reason + " expiry_in_past"
valid = false
}
//Verify the JWS E256 signature using the server’s public key
var decodedSignature []byte
decodedSignature, err = base64UrlDecode(parts[2])
if err != nil {
reason = fmt.Sprintf("%s signature_base64_decode_failed [%s] ", reason, err)
valid = false
} else {
if err := verifyAppleRSA256(parts[0]+"."+parts[1], decodedSignature, jwtHeader.Kid); err != nil {
reason = fmt.Sprintf("%s signature_verification_failed [%s] ", reason, err)
valid = false
}
}
//set the values of parsed token into the id token object
siwaIdToken.Header = &jwtHeader
siwaIdToken.Body = jwtBody
siwaIdToken.Valid = valid
siwaIdToken.Signature = decodedSignature
return siwaIdToken, reason
}