-
Notifications
You must be signed in to change notification settings - Fork 4
/
third_party_authentication.go
200 lines (166 loc) · 5.7 KB
/
third_party_authentication.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
package auth
import (
"crypto/ecdsa"
"crypto/elliptic"
"encoding/json"
"fmt"
"math/big"
"strconv"
"strings"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
"github.com/decentraland/auth-go/internal/utils"
"github.com/dgrijalva/jwt-go"
)
const thirdPartyUserIDPattern = "public key derived address: (.*)"
// ThirdPartyStrategy strategy to validate JWT is signed by a trusted third party
type ThirdPartyStrategy struct {
RequestTolerance int64
TrustedKey *ecdsa.PublicKey
}
// AccessTokenPayload represents the information in the JWT payload
type AccessTokenPayload struct {
EphemeralKey string `json:"ephemeral_key"`
Expiration int64 `json:"exp"`
UserID string `json:"user_id"`
Version string `json:"version"`
}
func (a AccessTokenPayload) isValid() bool {
return a.EphemeralKey != "" && a.Expiration > 0 && a.UserID != "" && a.Version != ""
}
// Authenticate check if the JWT is signed by a trusted third party
func (s *ThirdPartyStrategy) Authenticate(r *AuthRequest) (Result, error) {
cred := r.Credentials
output := NewResultOutput()
requiredCredentials := []string{HeaderIdentity, HeaderTimestamp, HeaderAccessToken, HeaderSignature}
if err := utils.ValidateRequiredCredentials(cred, requiredCredentials); err != nil {
return output, MissingCredentialsError{err.Error()}
}
tokens, err := utils.ParseTokensWithRegex(cred[HeaderIdentity], thirdPartyUserIDPattern)
if err != nil {
return output, err
}
if len(tokens) != 1 {
return output, InvalidCredentialError{"unable to extract required information from 'x-identity' header"}
}
ephPbKey := tokens[0]
if err = checkRequestExpiration(cred["x-timestamp"], s.RequestTolerance); err != nil {
return output, err
}
if err = validateRequestSignature(r, ephPbKey); err != nil {
return output, err
}
tkn, err := validateAccessToken(cred[HeaderAccessToken], s.TrustedKey, ephPbKey)
if err != nil {
return output, err
}
output.AddUserID(tkn.UserID)
return output, nil
}
func validateAccessToken(token string, trustedKey *ecdsa.PublicKey, ephKey string) (*AccessTokenPayload, error) {
segments := strings.Split(token, ".")
if len(segments) != 3 {
return nil, InvalidAccessTokenError{"invalid token format", TokenFormatError}
}
seg, err := jwt.DecodeSegment(segments[1])
if err != nil {
return nil, InvalidAccessTokenError{fmt.Sprintf("decoding Access Token error: %s", err.Error()), PayloadFormatError}
}
var payload AccessTokenPayload
err = json.Unmarshal(seg, &payload)
if err != nil || !payload.isValid() {
return nil, InvalidAccessTokenError{"access token payload missing required claims", MissingClaimsError}
}
if !strings.EqualFold(ephKey, payload.EphemeralKey) {
return nil, InvalidAccessTokenError{"access Token ephemeral Key does not match the request key", EphKeyMatchError}
}
if time.Now().Unix() > payload.Expiration {
return nil, InvalidAccessTokenError{"expired token", ExpiredTokenError}
}
if _, err := jwt.Parse(token, getKeyJWT(trustedKey.X, trustedKey.Y)); err != nil {
return nil, InvalidAccessTokenError{fmt.Sprintf("error validating Access Token: %s", err.Error()), InvalidTokenError}
}
return &payload, nil
}
func getKeyJWT(x *big.Int, y *big.Int) func(token *jwt.Token) (interface{}, error) {
return func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok {
return nil, fmt.Errorf("worng signing method")
}
return &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: x,
Y: y,
}, nil
}
}
// Validates that the signature sent in the request was generated for the current request
func validateRequestSignature(r *AuthRequest, pubKey string) error {
cred := r.Credentials
msg, err := r.Hash()
if err != nil {
return err
}
if err = validateSignature(cred["x-signature"], msg, pubKey); err != nil {
return err
}
return nil
}
// Verifies that the given pubkey created signature over message.
func validateSignature(signature string, message []byte, pubKey string) error {
sigBytes, err := hexutil.Decode(utils.FormatHexString(signature))
if err != nil {
return InvalidCredentialError{fmt.Sprintf("unable to decode signature: %s", err.Error())}
}
key, err := hexutil.Decode(utils.FormatHexString(pubKey))
if err != nil {
return InvalidCredentialError{fmt.Sprintf("unable to decode publickey: %s", err.Error())}
}
if !secp256k1.VerifySignature(key, message, sigBytes) {
return InvalidRequestSignatureError{"invalid Signature"}
}
return nil
}
// Verifies request expiration
func checkRequestExpiration(timestamp string, ttl int64) error {
t, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return InvalidCredentialError{"invalid timestamp"}
}
now := time.Now().Unix()
if abs(now-t) > ttl {
return ExpiredRequestError{"request expired"}
}
return nil
}
func abs(v int64) int64 {
if v > 0 {
return v
}
return -v
}
// InvalidAccessTokenError is a validation error in the JWT
type InvalidAccessTokenError struct {
message string
ErrorCode TokenValidationCode
}
func (e InvalidAccessTokenError) Error() string {
return e.message
}
// TokenValidationCode JWT error code
type TokenValidationCode int
const (
// TokenFormatError JWT is malformed
TokenFormatError TokenValidationCode = 0
// PayloadFormatError JWT payload section is invalid
PayloadFormatError TokenValidationCode = 1
// MissingClaimsError JWT payload is missing a required element
MissingClaimsError TokenValidationCode = 2
// EphKeyMatchError JWT ephKey do not match the key used to sign the request
EphKeyMatchError TokenValidationCode = 3
// ExpiredTokenError JWT expired
ExpiredTokenError TokenValidationCode = 4
// InvalidTokenError JWT is invalid
InvalidTokenError TokenValidationCode = 5
)