this repo has no description

support signin with wpds url

Changed files
+135 -111
cmd
client_test
+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
··· 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
··· 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
··· 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
··· 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 - }
-1
oauth_test.go
··· 42 42 } 43 43 44 44 // make sure the server is running 45 - 46 45 req, err := http.NewRequest("GET", serverMetadataUrl, nil) 47 46 if err != nil { 48 47 panic(err)