-
Notifications
You must be signed in to change notification settings - Fork 0
/
pkce.go
274 lines (232 loc) · 7.77 KB
/
pkce.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
// Package pkce implements proof key generation defined by RFC 7636 to enable
// generation and validation of code verifiers and code challenges.
//
// "Proof Key for Code Exchange" (PKCE, pronounced "pixy") was created as a
// technique to mitigate against the authorization code interception attack.
//
// For a detailed specification of PKCE (RFC 7636) see [1].
//
// Terminology:
// 1. code verifier
// A cryptographically random string that is used to correlate the
// authorization request to the token request.
//
// 2. code challenge
// A challenge derived from the code verifier that is sent in the
// authorization request, to be verified against later.
//
// 3. code challenge method
// A method that was used to derive code challenge.
//
// 4. Base64url Encoding
// Base64 encoding using the URL- and filename-safe character set
// defined in Section 5 of [RFC4648], with all trailing '='
// characters omitted (as permitted by Section 3.2 of [RFC4648]) and
// without the inclusion of any line breaks, whitespace, or other
// additional characters. (See Appendix A for notes on implementing
// base64url encoding without padding.)
//
// [1] https://datatracker.ietf.org/doc/html/rfc7636
package pkce
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"math/big"
)
// Method specifies the code challenge transformation method that was used to
// derive the code challenge.
type Method string
// String implements Stringer.
func (m Method) String() string {
return string(m)
}
const (
// Plain method specifies that the code challenge has had no transformation
// performed on the code verifier.
//
// code_challenge = code_verifier
//
// The plain transformation is for compatibility with existing
// deployments and for constrained environments that can't use the S256
// transformation.
//
Plain Method = "plain"
// S256 method specifies that the code challenge has been transformed by
// being hashed by SHA-256 then base64url-encoded.
//
// code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
//
// If the client is capable of using "S256", it MUST use "S256", as
// "S256" is Mandatory To Implement (MTI) on the server. Clients are
// permitted to use "plain" only if they cannot support "S256" for some
// technical reason and know via out-of-band configuration that the
// server supports "plain".
S256 Method = "S256"
)
const (
// ABNF for "code_verifier"
// ALPHA = %x41-5A / %x61-7A
alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
// DIGIT = %x30-39
digit = "0123456789"
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
unreserved = alpha + digit + "-._~"
)
const (
// RFC 7636, 4.1
verifierMinLen = 43
verifierMaxLen = 128
)
// New returns a Proof Key
func New(opts ...Option) (key *Key, err error) {
key = &Key{
challengeMethod: S256,
codeVerifierLen: verifierMinLen,
}
for _, opt := range opts {
if err = opt(key); err != nil {
return
}
}
return
}
// GenerateCodeVerifier generates an RFC7636 compliant, cryptographically secure
// code verifier.
func GenerateCodeVerifier(n int) (string, error) {
if err := validateVerifierLen(n); err != nil {
return "", err
}
return string(generateCodeVerifier(n)), nil
}
// GenerateCodeChallenge takes a code verifier and method to generate a code
// challenge.
func GenerateCodeChallenge(method Method, codeVerifier string) (out string, err error) {
in := []byte(codeVerifier)
if err = validateCodeVerifier(in); err != nil {
return
}
return generateCodeChallenge(method, in), nil
}
// VerifyCodeVerifier enables servers to verify the received code verifier.
func VerifyCodeVerifier(method Method, codeVerifier string, codeChallenge string) bool {
// RFC 7636, 4.6.
//
// the server verifies it by calculating the code challenge from the
// received "code_verifier" and comparing it with the previously associated
// "code_challenge", after first transforming it according to the
// "code_challenge_method" method specified by the client.
switch method {
case Plain:
// If the "code_challenge_method" from Section 4.3 was "plain", they are
// compared directly, i.e.:
return codeVerifier == codeChallenge
case S256:
// If the "code_challenge_method" from Section 4.3 was "S256", the
// received "code_verifier" is hashed by SHA-256, base64url-encoded, and
// then compared to the "code_challenge", i.e.:
codeVerifierChallenge, err := GenerateCodeChallenge(method, codeVerifier)
if err != nil {
return false
}
return codeVerifierChallenge == codeChallenge
default:
return false
}
}
// Key provides the proof key for secure code exchange.
type Key struct {
// challengeMethod determines the code challenge transform method to use.
challengeMethod Method
// codeVerifierLen provides the length of the code verifier to generate, if
// a code verifier is not supplied on key generation.
codeVerifierLen int
// codeVerifier provides the code verifier data.
codeVerifier []byte
}
// SetChallengeMethod enables upgrading code challenge generation method.
func (k *Key) SetChallengeMethod(method Method) error {
switch method {
case Plain, S256:
if k.challengeMethod == S256 && method == Plain {
return ErrMethodDowngrade
}
k.challengeMethod = method
default:
return ErrMethodNotSupported
}
return nil
}
// ChallengeMethod returns the configured key's method for generating a code
// challenge.
func (k *Key) ChallengeMethod() Method {
return k.challengeMethod
}
// setCodeVerifierLength sets the length of the code verifier to be generated.
//
// If a code verifier is supplied, this setting will be ignored in favour of
// using the supplied verifier.
func (k *Key) setCodeVerifierLength(n int) error {
if len(k.codeVerifier) > 0 {
// Don't overwrite the set length.
return nil
}
if err := validateVerifierLen(n); err != nil {
return err
}
k.codeVerifierLen = n
return nil
}
// setCodeVerifier enables setting a new code verifier.
func (k *Key) setCodeVerifier(verifier []byte) (err error) {
if err = validateCodeVerifier(verifier); err != nil {
return
}
k.codeVerifier = verifier
k.codeVerifierLen = len(verifier)
return
}
// CodeVerifier returns the code verifier.
func (k *Key) CodeVerifier() string {
return string(k.getCodeVerifier())
}
// getCodeVerifier returns a code verifier. If one has not been set, it will
// generate one based on the configured verifier length.
func (k *Key) getCodeVerifier() []byte {
if len(k.codeVerifier) == 0 {
k.codeVerifier = generateCodeVerifier(k.codeVerifierLen)
}
return k.codeVerifier
}
// CodeChallenge returns the challenge for the configured code verifier.
// Will generate a verifier if nil.
func (k *Key) CodeChallenge() string {
return generateCodeChallenge(k.ChallengeMethod(), k.getCodeVerifier())
}
// VerifyCodeVerifier provides a convenience function, for if you've loaded the
// code verifier into the key. If not, this won't really be useful to use...
func (k *Key) VerifyCodeVerifier(codeVerifier string) bool {
return VerifyCodeVerifier(k.ChallengeMethod(), codeVerifier, k.CodeChallenge())
}
// generateCodeVerifier performs the computations required to generate a
// cryptographically random, specification compliant code verifier.
func generateCodeVerifier(n int) (out []byte) {
unreservedLen := big.NewInt(int64(len(unreserved)))
out = make([]byte, n)
for i := range out {
// ensure we use non-deterministic random ints.
j, _ := rand.Int(rand.Reader, unreservedLen)
out[i] = unreserved[j.Int64()]
}
return out
}
// generateCodeChallenge performs the transform required by the specified
// method.
func generateCodeChallenge(method Method, codeVerifier []byte) (out string) {
if method == Plain {
return string(codeVerifier)
}
s256 := sha256.New()
s256.Write(codeVerifier)
return base64.RawURLEncoding.EncodeToString(s256.Sum(nil))
}