···8585 scopes := []string{"transition:generic"}
8686 bind := ":8080"
87878888- // TODO: localhost dev mode if hostname is empty
8988 var config oauth.ClientConfig
9089 hostname := cctx.String("hostname")
9190 if hostname == "" {
···247246248247 did := s.currentSessionDID(r)
249248 if did == nil {
250250- // TODO: suppowed to set a WWW header; and could redirect?
249249+ // TODO: supposed to set a WWW header; and could redirect?
251250 http.Error(w, "not authenticated", http.StatusUnauthorized)
252251 return
253252 }
···303302304303 did := s.currentSessionDID(r)
305304 if did == nil {
306306- // TODO: suppowed to set a WWW header; and could redirect?
305305+ // TODO: supposed to set a WWW header; and could redirect?
307306 http.Error(w, "not authenticated", http.StatusUnauthorized)
308307 return
309308 }
+43-23
atproto/auth/oauth/oauth.go
···185185 }
186186 }
187187188188- // XXX: refactor this in to ClientAuthStore layer?
188188+ // TODO: refactor this in to ClientAuthStore layer?
189189 priv, err := crypto.ParsePrivateMultibase(sd.DpopPrivateKeyMultibase)
190190 if err != nil {
191191 return nil, err
···277277278278// Sends PAR request to auth server
279279func (app *ClientApp) SendAuthRequest(ctx context.Context, authMeta *AuthServerMetadata, scope, loginHint string) (*AuthRequestData, error) {
280280- // TODO: pass as argument?
281281- httpClient := http.DefaultClient
282280283281 parURL := authMeta.PushedAuthorizationRequestEndpoint
284282 state := randomNonce()
···287285 // generate PKCE code challenge for use in PAR request
288286 codeChallenge := S256CodeChallenge(pkceVerifier)
289287290290- slog.Info("preparing PAR", "client_id", app.Config.ClientID, "callback_url", app.Config.CallbackURL)
288288+ slog.Debug("preparing PAR", "client_id", app.Config.ClientID, "callback_url", app.Config.CallbackURL)
291289 body := PushedAuthRequest{
292290 ClientID: app.Config.ClientID,
293291 State: state,
···317315 }
318316 bodyBytes := []byte(vals.Encode())
319317318318+ // when starting a new session, we don't know the DPoP nonce yet
320319 dpopServerNonce := ""
321320322321 // create new key for the session
···325324 return nil, err
326325 }
327326328328- slog.Info("sending auth request", "scope", scope, "state", state, "redirectURI", app.Config.CallbackURL)
327327+ slog.Debug("sending auth request", "scope", scope, "state", state, "redirectURI", app.Config.CallbackURL)
329328330329 var resp *http.Response
331330 for range 2 {
···341340 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
342341 req.Header.Set("DPoP", dpopJWT)
343342344344- resp, err = httpClient.Do(req)
343343+ resp, err = app.Client.Do(req)
345344 if err != nil {
346345 return nil, err
347346 }
348347349349- // check if a nonce was provided
348348+ // update DPoP Nonce
350349 dpopServerNonce = resp.Header.Get("DPoP-Nonce")
351351- if resp.StatusCode == 400 && dpopServerNonce != "" {
352352- // TODO: also check that body is JSON with an 'error' string field value of 'use_dpop_nonce'
350350+351351+ // check for an error condition caused by an out of date DPoP nonce
352352+ // 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
353353+ if resp.StatusCode == http.StatusBadRequest && dpopServerNonce != "" {
354354+355355+ // parse the error body to confirm the error type
353356 var errResp map[string]any
354357 if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
355355- slog.Warn("PAR request failed", "authServer", parURL, "err", err, "statusCode", resp.StatusCode)
356356- } else {
357357- slog.Warn("PAR request failed", "authServer", parURL, "resp", errResp, "statusCode", resp.StatusCode)
358358+ slog.Warn("PAR request failed, and could not parse response body", "authServer", parURL, "err", err, "statusCode", resp.StatusCode)
359359+ resp.Body.Close()
360360+ return nil, fmt.Errorf("token refresh failed: HTTP %d", resp.StatusCode)
361361+ } else if errResp["error"] != "use_dpop_nonce" {
362362+ slog.Warn("PAR request failed", "authServer", parURL, "body", errResp, "statusCode", resp.StatusCode)
363363+ return nil, fmt.Errorf("PAR request failed: %s", errResp["error"])
358364 }
359365360360- // loop around try again
366366+ // already updated nonce value above; loop around and try again
367367+ // NOTE: having already parsed the body means that the error handling below could fail if we call out of 'for' loop
361368 resp.Body.Close()
362369 continue
363370 }
371371+364372 // otherwise process result
365373 break
366374 }
367375368376 defer resp.Body.Close()
369369- if resp.StatusCode != 200 && resp.StatusCode != 201 {
377377+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
370378 var errResp map[string]any
371379 if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
372380 slog.Warn("PAR request failed", "authServer", parURL, "err", err, "statusCode", resp.StatusCode)
···452460 }
453461454462 // check if a nonce was provided
455455- dpopServerNonce = resp.Header.Get("DPoP-Nonce")
456456- if resp.StatusCode == 400 && dpopServerNonce != "" {
457457- // TODO: also check that body is JSON with an 'error' string field value of 'use_dpop_nonce'
463463+ dpopNonceHdr := resp.Header.Get("DPoP-Nonce")
464464+ if dpopNonceHdr != "" && dpopNonceHdr != dpopServerNonce {
465465+ dpopServerNonce = dpopNonceHdr
466466+ }
467467+468468+ // check for an error condition caused by an out of date DPoP nonce
469469+ // 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
470470+ if resp.StatusCode == http.StatusBadRequest && dpopNonceHdr != "" {
471471+472472+ // parse the error body to confirm the error type
458473 var errResp map[string]any
459474 if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
460460- slog.Warn("initial token request failed", "authServer", authServerMeta.TokenEndpoint, "err", err, "statusCode", resp.StatusCode)
461461- } else {
462462- slog.Warn("initial token request failed", "authServer", authServerMeta.TokenEndpoint, "resp", errResp, "statusCode", resp.StatusCode)
475475+ slog.Warn("initial token request failed, and could not parse response body", "authServer", authServerMeta.TokenEndpoint, "err", err, "statusCode", resp.StatusCode)
476476+ resp.Body.Close()
477477+ return nil, fmt.Errorf("initial token request failed: HTTP %d", resp.StatusCode)
478478+ } else if errResp["error"] != "use_dpop_nonce" {
479479+ slog.Warn("initial token request failed", "authServer", authServerMeta.TokenEndpoint, "body", errResp, "statusCode", resp.StatusCode)
480480+ return nil, fmt.Errorf("initial token request failed: %s", errResp["error"])
463481 }
464482465465- // loop around try again
483483+ // already updated nonce value above; loop around and try again
484484+ // NOTE: having already parsed the body means that the error handling below could fail if we call out of 'for' loop
466485 resp.Body.Close()
467486 continue
468487 }
488488+469489 // otherwise process result
470490 break
471491 }
472492473493 defer resp.Body.Close()
474474- if resp.StatusCode != 200 {
494494+ if resp.StatusCode != http.StatusOK {
475495 var errResp map[string]any
476496 if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
477497 slog.Warn("initial token request failed", "authServer", authServerMeta.TokenEndpoint, "err", err, "statusCode", resp.StatusCode)
···513533514534 // TODO: logger on ClientApp?
515535 logger := slog.Default().With("did", ident.DID, "handle", ident.Handle, "host", host)
516516- logger.Info("resolving to auth server metadata")
536536+ logger.Debug("resolving to auth server metadata")
517537 authserverURL, err = app.Resolver.ResolveAuthServerURL(ctx, host)
518538 if err != nil {
519539 return "", fmt.Errorf("resolving auth server: %w", err)
+2-1
atproto/auth/oauth/util.go
···66 "math/rand"
77)
8899-// TODO: longer
99+// this generates pseudo-unique nonces to prevent token (JWT) replay. these do not need to be cryptographically resilient
1010func randomNonce() string {
1111+ // TODO: make this longer?
1112 buf := make([]byte, 16)
1213 rand.Read(buf)
1314 return base64.RawURLEncoding.EncodeToString(buf)