+42
-33
cmd/client_test/handle_auth.go
+42
-33
cmd/client_test/handle_auth.go
···
4
4
"encoding/json"
5
5
"fmt"
6
6
"net/url"
7
+
"strings"
7
8
"time"
8
9
9
10
"github.com/bluesky-social/indigo/atproto/syntax"
···
15
16
)
16
17
17
18
func (s *TestServer) handleLoginSubmit(e echo.Context) error {
18
-
handle := e.FormValue("handle")
19
-
if handle == "" {
20
-
return e.Redirect(302, "/login?e=handle-empty")
19
+
authInput := e.FormValue("auth-input")
20
+
if authInput == "" {
21
+
return e.Redirect(302, "/login?e=auth-input-empty")
21
22
}
22
23
23
-
_, herr := syntax.ParseHandle(handle)
24
-
_, derr := syntax.ParseDID(handle)
24
+
var service string
25
+
var did string
25
26
26
-
if herr != nil && derr != nil {
27
-
return e.Redirect(302, "/login?e=handle-invalid")
28
-
}
27
+
if strings.HasPrefix("https://", authInput) {
28
+
u, err := url.Parse(authInput)
29
+
if err == nil {
30
+
u.Path = ""
31
+
u.RawQuery = ""
32
+
u.User = nil
33
+
service = u.String()
34
+
}
35
+
} else {
36
+
_, herr := syntax.ParseHandle(authInput)
37
+
_, derr := syntax.ParseDID(authInput)
29
38
30
-
var did string
39
+
if herr != nil && derr != nil {
40
+
return e.Redirect(302, "/login?e=handle-invalid")
41
+
}
42
+
43
+
if derr == nil {
44
+
did = authInput
45
+
} else {
46
+
maybeDid, err := resolveHandle(e.Request().Context(), authInput)
47
+
if err != nil {
48
+
return err
49
+
}
50
+
51
+
did = maybeDid
52
+
}
31
53
32
-
if derr == nil {
33
-
did = handle
34
-
} else {
35
-
maybeDid, err := resolveHandle(e.Request().Context(), handle)
54
+
maybeService, err := resolveService(ctx, did)
36
55
if err != nil {
37
56
return err
38
57
}
39
58
40
-
did = maybeDid
41
-
}
42
-
43
-
service, err := resolveService(ctx, did)
44
-
if err != nil {
45
-
return err
59
+
service = maybeService
46
60
}
47
61
48
62
authserver, err := s.oauthClient.ResolvePDSAuthServer(ctx, service)
···
129
143
}
130
144
131
145
sessState := sess.Values["oauth_state"]
132
-
sessDid := sess.Values["oauth_did"]
133
146
134
-
if resState == "" || resIss == "" || resCode == "" || sessState == "" || sessDid == "" {
147
+
if resState == "" || resIss == "" || resCode == "" {
135
148
return fmt.Errorf("request missing needed parameters")
136
149
}
137
150
···
140
153
}
141
154
142
155
var oauthRequest OauthRequest
143
-
if err := s.db.Raw("SELECT * FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Scan(&oauthRequest).Error; err != nil {
156
+
if err := s.db.Raw("SELECT * FROM oauth_requests WHERE state = ?", sessState).Scan(&oauthRequest).Error; err != nil {
144
157
return err
145
158
}
146
159
147
-
if err := s.db.Exec("DELETE FROM oauth_requests WHERE state = ? AND did = ?", sessState, sessDid).Error; err != nil {
160
+
if err := s.db.Exec("DELETE FROM oauth_requests WHERE state = ?", sessState).Error; err != nil {
148
161
return err
149
162
}
150
163
···
157
170
return err
158
171
}
159
172
160
-
initialTokenResp, err := s.oauthClient.InitialTokenRequest(
161
-
e.Request().Context(),
162
-
resCode,
163
-
resIss,
164
-
oauthRequest.PkceVerifier,
165
-
oauthRequest.DpopAuthserverNonce,
166
-
jwk,
167
-
)
173
+
initialTokenResp, err := s.oauthClient.InitialTokenRequest(e.Request().Context(), resCode, resIss, oauthRequest.PkceVerifier, oauthRequest.DpopAuthserverNonce, jwk)
168
174
if err != nil {
169
175
return err
170
176
}
171
-
172
-
// TODO: resolve if needed
173
177
174
178
if initialTokenResp.Scope != scope {
175
179
return fmt.Errorf("did not receive correct scopes from token request")
180
+
}
181
+
182
+
// if we didn't start with a did, we can get it from the response
183
+
if oauthRequest.Did == "" {
184
+
oauthRequest.Did = initialTokenResp.Sub
176
185
}
177
186
178
187
oauthSession := &OauthSession{
+5
-1
cmd/client_test/html/login.html
+5
-1
cmd/client_test/html/login.html
···
1
1
<!doctype html>
2
2
<html>
3
3
<form action="/login" method="post">
4
-
<input name="handle" id="handle" placeholder="Enter your handle" />
4
+
<input
5
+
name="auth-input"
6
+
id="auth-input"
7
+
placeholder="Enter your handle, did, or pds url"
8
+
/>
5
9
<button type="submit" value="Submit">Submit</button>
6
10
</form>
7
11
</html>
+2
-2
cmd/client_test/user.go
+2
-2
cmd/client_test/user.go
···
52
52
return nil, false, err
53
53
}
54
54
55
-
did, ok := sess.Values["did"]
55
+
did, ok := sess.Values["did"].(string)
56
56
if !ok {
57
57
return nil, false, nil
58
58
}
59
59
60
-
oauthSession, err := s.getOauthSession(e.Request().Context(), did.(string))
60
+
oauthSession, err := s.getOauthSession(e.Request().Context(), did)
61
61
62
62
privateJwk, err := oauth.ParseKeyFromBytes([]byte(oauthSession.DpopPrivateJwk))
63
63
if err != nil {
+19
generic.go
+19
generic.go
···
4
4
"crypto/ecdsa"
5
5
"crypto/elliptic"
6
6
"crypto/rand"
7
+
"crypto/sha256"
8
+
"encoding/base64"
9
+
"encoding/hex"
7
10
"fmt"
8
11
"net/url"
9
12
"time"
···
92
95
func ParseKeyFromBytes(b []byte) (jwk.Key, error) {
93
96
return jwk.ParseKey(b)
94
97
}
98
+
99
+
func generateToken(len int) (string, error) {
100
+
b := make([]byte, len)
101
+
if _, err := rand.Read(b); err != nil {
102
+
return "", err
103
+
}
104
+
105
+
return hex.EncodeToString(b), nil
106
+
}
107
+
108
+
func generateCodeChallenge(pkceVerifier string) string {
109
+
h := sha256.New()
110
+
h.Write([]byte(pkceVerifier))
111
+
hash := h.Sum(nil)
112
+
return base64.RawURLEncoding.EncodeToString(hash)
113
+
}
+67
-74
oauth.go
+67
-74
oauth.go
···
3
3
import (
4
4
"context"
5
5
"crypto/ecdsa"
6
-
"crypto/rand"
7
-
"crypto/sha256"
8
-
"encoding/base64"
9
-
"encoding/hex"
10
6
"encoding/json"
11
7
"fmt"
12
8
"io"
···
29
25
}
30
26
31
27
type ClientArgs struct {
32
-
H *http.Client
28
+
Http *http.Client
33
29
ClientJwk jwk.Key
34
30
ClientId string
35
31
RedirectUri string
···
44
40
return nil, fmt.Errorf("no redirect uri provided")
45
41
}
46
42
47
-
if args.H == nil {
48
-
args.H = &http.Client{
43
+
if args.Http == nil {
44
+
args.Http = &http.Client{
49
45
Timeout: 5 * time.Second,
50
46
}
51
47
}
···
58
54
kid := args.ClientJwk.KeyID()
59
55
60
56
return &Client{
61
-
h: args.H,
57
+
h: args.Http,
62
58
clientKid: kid,
63
59
clientPrivateKey: clientPkey,
64
60
clientId: args.ClientId,
···
122
118
123
119
resp, err := c.h.Do(req)
124
120
if err != nil {
125
-
return nil, fmt.Errorf("error getting response for auth metadata: %w", err)
121
+
return nil, fmt.Errorf("error getting response for authserver metadata: %w", err)
126
122
}
127
123
defer resp.Body.Close()
128
124
129
125
if resp.StatusCode != http.StatusOK {
130
126
io.Copy(io.Discard, resp.Body)
131
-
return nil, fmt.Errorf(
132
-
"received non-200 response from pds. status code was %d",
133
-
resp.StatusCode,
134
-
)
127
+
return nil, fmt.Errorf("received non-200 response from pds. status code was %d", resp.StatusCode)
135
128
}
136
129
137
130
b, err := io.ReadAll(resp.Body)
138
131
if err != nil {
139
-
return nil, fmt.Errorf("could not read body for metadata response: %w", err)
132
+
return nil, fmt.Errorf("could not read body for authserver metadata response: %w", err)
140
133
}
141
134
142
135
var metadata OauthAuthorizationMetadata
143
136
if err := metadata.UnmarshalJSON(b); err != nil {
144
-
return nil, fmt.Errorf("could not unmarshal metadata: %w", err)
137
+
return nil, fmt.Errorf("could not unmarshal authserver metadata: %w", err)
145
138
}
146
139
147
140
if err := metadata.Validate(u); err != nil {
148
-
return nil, fmt.Errorf("could not validate metadata: %w", err)
141
+
return nil, fmt.Errorf("could not validate authserver metadata: %w", err)
149
142
}
150
143
151
144
return &metadata, nil
···
338
331
dpopAuthserverNonce string,
339
332
dpopPrivateJwk jwk.Key,
340
333
) (*TokenResponse, error) {
341
-
authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverIss)
342
-
if err != nil {
343
-
return nil, err
344
-
}
334
+
// we might need to re-run to update dpop nonce
335
+
for range 2 {
336
+
authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverIss)
337
+
if err != nil {
338
+
return nil, err
339
+
}
340
+
341
+
clientAssertion, err := c.ClientAssertionJwt(authserverIss)
342
+
if err != nil {
343
+
return nil, err
344
+
}
345
345
346
-
clientAssertion, err := c.ClientAssertionJwt(authserverIss)
347
-
if err != nil {
348
-
return nil, err
349
-
}
346
+
params := url.Values{
347
+
"client_id": {c.clientId},
348
+
"redirect_uri": {c.redirectUri},
349
+
"grant_type": {"authorization_code"},
350
+
"code": {code},
351
+
"code_verifier": {pkceVerifier},
352
+
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
353
+
"client_assertion": {clientAssertion},
354
+
}
355
+
356
+
dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, dpopAuthserverNonce, dpopPrivateJwk)
357
+
if err != nil {
358
+
return nil, err
359
+
}
360
+
361
+
req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode()))
362
+
if err != nil {
363
+
return nil, err
364
+
}
365
+
366
+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
367
+
req.Header.Set("DPoP", dpopProof)
368
+
369
+
resp, err := c.h.Do(req)
370
+
if err != nil {
371
+
return nil, err
372
+
}
373
+
defer resp.Body.Close()
350
374
351
-
params := url.Values{
352
-
"client_id": {c.clientId},
353
-
"redirect_uri": {c.redirectUri},
354
-
"grant_type": {"authorization_code"},
355
-
"code": {code},
356
-
"code_verifier": {pkceVerifier},
357
-
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
358
-
"client_assertion": {clientAssertion},
359
-
}
375
+
if resp.StatusCode != 200 && resp.StatusCode != 201 {
376
+
var respMap map[string]string
377
+
if err := json.NewDecoder(resp.Body).Decode(&respMap); err != nil {
378
+
return nil, err
379
+
}
360
380
361
-
dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, dpopAuthserverNonce, dpopPrivateJwk)
362
-
if err != nil {
363
-
return nil, err
364
-
}
381
+
if resp.StatusCode == 400 && respMap["error"] == "use_dpop_nonce" {
382
+
dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce")
383
+
continue
384
+
}
365
385
366
-
req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode()))
367
-
if err != nil {
368
-
return nil, err
369
-
}
386
+
return nil, fmt.Errorf("token refresh error: %s", respMap["error"])
387
+
}
370
388
371
-
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
372
-
req.Header.Set("DPoP", dpopProof)
389
+
var tokenResponse TokenResponse
390
+
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
391
+
return nil, err
392
+
}
373
393
374
-
resp, err := c.h.Do(req)
375
-
if err != nil {
376
-
return nil, err
377
-
}
378
-
defer resp.Body.Close()
394
+
// set nonce so the updates are reflected in the response
395
+
tokenResponse.DpopAuthserverNonce = dpopAuthserverNonce
379
396
380
-
var tokenResponse TokenResponse
381
-
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
382
-
return nil, err
397
+
return &tokenResponse, nil
383
398
}
384
399
385
-
tokenResponse.DpopAuthserverNonce = dpopAuthserverNonce
386
-
387
-
return &tokenResponse, nil
400
+
return nil, nil
388
401
}
389
402
390
403
func (c *Client) RefreshTokenRequest(
···
460
473
461
474
return nil, nil
462
475
}
463
-
464
-
func generateToken(len int) (string, error) {
465
-
b := make([]byte, len)
466
-
if _, err := rand.Read(b); err != nil {
467
-
return "", err
468
-
}
469
-
470
-
return hex.EncodeToString(b), nil
471
-
}
472
-
473
-
func generateCodeChallenge(pkceVerifier string) string {
474
-
h := sha256.New()
475
-
h.Write([]byte(pkceVerifier))
476
-
hash := h.Sum(nil)
477
-
return base64.RawURLEncoding.EncodeToString(hash)
478
-
}
479
-
480
-
func parsePrivateJwkFromString(str string) (jwk.Key, error) {
481
-
return jwk.ParseKey([]byte(str))
482
-
}