this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

more improvements from review

+70 -76
+51 -51
atproto/auth/oauth/oauth.go
··· 33 33 type ClientConfig struct { 34 34 ClientID string 35 35 CallbackURL string 36 - // set of scope strings; should not include "atproto" 36 + // set of scope strings; must include "atproto" 37 37 Scopes []string 38 38 39 39 UserAgent string ··· 55 55 } 56 56 if config.UserAgent != "" { 57 57 app.Resolver.UserAgent = config.UserAgent 58 - // TODO: some way to wire UserAgent through to identity directory 58 + 59 + // unpack DefaultDirectory nested type and insert UserAgent (and log failure in case default types change) 60 + dirAgent := false 61 + cdir, ok := app.Dir.(*identity.CacheDirectory) 62 + if ok { 63 + bdir, ok := cdir.Inner.(*identity.BaseDirectory) 64 + if ok { 65 + dirAgent = true 66 + bdir.UserAgent = config.UserAgent 67 + } 68 + } 69 + if !dirAgent { 70 + slog.Info("OAuth ClientApp identity directory User-Agent not configured") 71 + } 59 72 } 60 73 return app 61 74 } 62 75 76 + // Creates a basic [ClientConfig] for use as a public (non-confidential) client. To upgrade to a confidential client, use this method and then [ClientConfig.AddClientSecret()]. 77 + // 78 + // The "scopes" array must include "atproto". 63 79 func NewPublicConfig(clientID, callbackURL string, scopes []string) ClientConfig { 64 80 c := ClientConfig{ 65 81 ClientID: clientID, ··· 70 86 return c 71 87 } 72 88 89 + // Creats a basic [ClientConfig] for use with localhost developmnet. Such a client is always public (non-confidential). 90 + // 91 + // The "scopes" array must include "atproto". 73 92 func NewLocalhostConfig(callbackURL string, scopes []string) ClientConfig { 74 93 params := make(url.Values) 75 94 params.Set("redirect_uri", callbackURL) ··· 129 148 130 149 // helper to turn a list of scope strings in to a single space-separated scope string 131 150 func scopeStr(scopes []string) string { 132 - if len(scopes) == 0 { 133 - return "atproto" 134 - } 135 - return "atproto " + strings.Join(scopes, " ") 151 + return strings.Join(scopes, " ") 136 152 } 137 153 138 154 // Returns a ClientMetadata struct with the required fields populated based on this client configuration. Clients may want to populate additional metadata fields on top of this response. ··· 275 291 return token.SignedString(privKey) 276 292 } 277 293 294 + // attempts to read an HTTP response body as JSON, and determine an error reason. always closes the response body 295 + func parseAuthErrorReason(resp *http.Response, reqType string) string { 296 + defer resp.Body.Close() 297 + var errResp map[string]any 298 + if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { 299 + slog.Warn("auth server request failed", "request", reqType, "statusCode", resp.StatusCode, "err", err) 300 + return "unknown" 301 + } 302 + slog.Warn("auth server request failed", "request", reqType, "statusCode", resp.StatusCode, "body", errResp) 303 + return fmt.Sprintf("%s", errResp["error"]) 304 + } 305 + 278 306 // Sends PAR request to auth server 279 307 func (app *ClientApp) SendAuthRequest(ctx context.Context, authMeta *AuthServerMetadata, scope, loginHint string) (*AuthRequestData, error) { 280 308 ··· 351 379 // check for an error condition caused by an out of date DPoP nonce 352 380 // note that the HTTP status code would be 400 Bad Request on token endpoint, not 401 Unauthorized like it would be on Resource Server requests 353 381 if resp.StatusCode == http.StatusBadRequest && dpopServerNonce != "" { 354 - 355 - // parse the error body to confirm the error type 356 - var errResp map[string]any 357 - if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { 358 - slog.Warn("PAR request failed, and could not parse response body", "authServer", parURL, "err", err, "statusCode", resp.StatusCode) 359 - resp.Body.Close() 360 - return nil, fmt.Errorf("token refresh failed: HTTP %d", resp.StatusCode) 361 - } else if errResp["error"] != "use_dpop_nonce" { 362 - slog.Warn("PAR request failed", "authServer", parURL, "body", errResp, "statusCode", resp.StatusCode) 363 - return nil, fmt.Errorf("PAR request failed: %s", errResp["error"]) 382 + // parseAuthErrorReason() always closes resp.Body 383 + reason := parseAuthErrorReason(resp, "PAR") 384 + if reason == "use_dpop_nonce" { 385 + // already updated nonce value above; loop around and try again 386 + continue 364 387 } 365 - 366 - // already updated nonce value above; loop around and try again 367 - // NOTE: having already parsed the body means that the error handling below could fail if we call out of 'for' loop 368 - resp.Body.Close() 369 - continue 388 + return nil, fmt.Errorf("PAR request failed (HTTP %d): %s", resp.StatusCode, reason) 370 389 } 371 390 372 391 // otherwise process result ··· 375 394 376 395 defer resp.Body.Close() 377 396 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { 378 - var errResp map[string]any 379 - if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { 380 - slog.Warn("PAR request failed", "authServer", parURL, "err", err, "statusCode", resp.StatusCode) 381 - } else { 382 - slog.Warn("PAR request failed", "authServer", parURL, "resp", errResp, "statusCode", resp.StatusCode) 383 - } 384 - return nil, fmt.Errorf("auth request (PAR) failed: HTTP %d", resp.StatusCode) 397 + reason := parseAuthErrorReason(resp, "PAR") 398 + return nil, fmt.Errorf("PAR request failed (HTTP %d): %s", resp.StatusCode, reason) 385 399 } 386 400 387 401 var parResp PushedAuthResponse ··· 468 482 // check for an error condition caused by an out of date DPoP nonce 469 483 // note that the HTTP status code would be 400 Bad Request on token endpoint, not 401 Unauthorized like it would be on Resource Server requests 470 484 if resp.StatusCode == http.StatusBadRequest && dpopNonceHdr != "" { 471 - 472 - // parse the error body to confirm the error type 473 - var errResp map[string]any 474 - if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { 475 - slog.Warn("initial token request failed, and could not parse response body", "authServer", authServerMeta.TokenEndpoint, "err", err, "statusCode", resp.StatusCode) 476 - resp.Body.Close() 477 - return nil, fmt.Errorf("initial token request failed: HTTP %d", resp.StatusCode) 478 - } else if errResp["error"] != "use_dpop_nonce" { 479 - slog.Warn("initial token request failed", "authServer", authServerMeta.TokenEndpoint, "body", errResp, "statusCode", resp.StatusCode) 480 - return nil, fmt.Errorf("initial token request failed: %s", errResp["error"]) 485 + // parseAuthErrorReason() always closes resp.Body 486 + reason := parseAuthErrorReason(resp, "initial-token") 487 + if reason == "use_dpop_nonce" { 488 + // already updated nonce value above; loop around and try again 489 + continue 481 490 } 482 - 483 - // already updated nonce value above; loop around and try again 484 - // NOTE: having already parsed the body means that the error handling below could fail if we call out of 'for' loop 485 - resp.Body.Close() 486 - continue 491 + return nil, fmt.Errorf("initial token request failed (HTTP %d): %s", resp.StatusCode, reason) 487 492 } 488 493 489 494 // otherwise process result ··· 492 497 493 498 defer resp.Body.Close() 494 499 if resp.StatusCode != http.StatusOK { 495 - var errResp map[string]any 496 - if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { 497 - slog.Warn("initial token request failed", "authServer", authServerMeta.TokenEndpoint, "err", err, "statusCode", resp.StatusCode) 498 - } else { 499 - slog.Warn("initial token request failed", "authServer", authServerMeta.TokenEndpoint, "resp", errResp, "statusCode", resp.StatusCode) 500 - } 501 - return nil, fmt.Errorf("initial token request failed: HTTP %d", resp.StatusCode) 500 + reason := parseAuthErrorReason(resp, "initial-token") 501 + return nil, fmt.Errorf("initial token request failed (HTTP %d): %s", resp.StatusCode, reason) 502 502 } 503 503 504 504 var tokenResp TokenResponse ··· 562 562 params.Set("client_id", app.Config.ClientID) 563 563 params.Set("request_uri", info.RequestURI) 564 564 565 - // TODO: check that 'authorization_endpoint' is "safe" (?) 565 + // NOTE: AuthorizationEndpoint was already checked to be a clean URL 566 566 redirectURL := fmt.Sprintf("%s?%s", authserverMeta.AuthorizationEndpoint, params.Encode()) 567 567 return redirectURL, nil 568 568 }
+10 -25
atproto/auth/oauth/session.go
··· 123 123 } 124 124 125 125 // check for an error condition caused by an out of date DPoP nonce 126 - // note that the HTTP status code would be 400 Bad Request on token endpoint, not 401 Unauthorized like it would be on Resource Server requests 126 + // note that the HTTP status code is 400 Bad Request on the Auth Server token endpoint, not 401 Unauthorized like it would be on Resource Server requests 127 127 if resp.StatusCode == http.StatusBadRequest && dpopNonceHdr != "" { 128 - 129 - // parse the error body to confirm the error type 130 - var errResp map[string]any 131 - if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { 132 - slog.Warn("token refresh failed, and could not parse response body", "authServer", tokenURL, "err", err, "statusCode", resp.StatusCode) 133 - resp.Body.Close() 134 - return "", fmt.Errorf("token refresh failed: HTTP %d", resp.StatusCode) 135 - } else if errResp["error"] != "use_dpop_nonce" { 136 - slog.Warn("token refresh failed", "authServer", tokenURL, "body", errResp, "statusCode", resp.StatusCode) 137 - resp.Body.Close() 138 - return "", fmt.Errorf("token refresh failed: %s", errResp["error"]) 128 + // parseAuthErrorReason() always closes resp.Body 129 + reason := parseAuthErrorReason(resp, "token-refresh") 130 + if reason == "use_dpop_nonce" { 131 + // already updated nonce value above; loop around and try again 132 + continue 139 133 } 140 - 141 - // already updated nonce value above; loop around and try again 142 - // NOTE: having already parsed the body means that the error handling below could fail if we call out of 'for' loop 143 - resp.Body.Close() 144 - continue 134 + return "", fmt.Errorf("token refresh failed (HTTP %d): %s", resp.StatusCode, reason) 145 135 } 146 136 147 - // otherwise process result 137 + // otherwise process response (success or other error type) 148 138 break 149 139 } 150 140 151 141 defer resp.Body.Close() 152 142 if resp.StatusCode != http.StatusOK { 153 - var errResp map[string]any 154 - if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { 155 - slog.Warn("token refresh failed", "authServer", tokenURL, "err", err, "statusCode", resp.StatusCode) 156 - return "", fmt.Errorf("token refresh failed: HTTP %d", resp.StatusCode) 157 - } 158 - slog.Warn("token refresh failed", "authServer", tokenURL, "body", errResp, "statusCode", resp.StatusCode) 159 - return "", fmt.Errorf("token refresh failed: %s", errResp["error"]) 143 + reason := parseAuthErrorReason(resp, "token-refresh") 144 + return "", fmt.Errorf("token refresh failed (HTTP %d): %s", resp.StatusCode, reason) 160 145 } 161 146 162 147 var tokenResp TokenResponse
+9
atproto/auth/oauth/types.go
··· 218 218 return fmt.Errorf("%w: issuer must match request URL", ErrInvalidAuthServerMetadata) 219 219 } 220 220 221 + // check that authorization endpoint is a valid HTTPS URL with no fragment or query params (we will be appending query params latter) 222 + aeurl, err := url.Parse(m.AuthorizationEndpoint) 223 + if err != nil { 224 + return fmt.Errorf("%w: invalid auth endpoint URL (%s): %w", ErrInvalidAuthServerMetadata, m.AuthorizationEndpoint, err) 225 + } 226 + if aeurl.Scheme != "https" || u.Fragment != "" || u.RawQuery != "" { 227 + return fmt.Errorf("%w: invalid auth endpoint URL: %s", ErrInvalidAuthServerMetadata, m.AuthorizationEndpoint) 228 + } 229 + 221 230 if !slices.Contains(m.ResponseTypesSupported, "code") { 222 231 return fmt.Errorf("%w: response_types_supported must include 'code'", ErrInvalidAuthServerMetadata) 223 232 }