| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -31,6 +31,12 @@ const cibaGrantType = "urn:openid:params:grant-type:ciba" | |||
| 31 | 31 | // it should assume a default of 5 seconds"). | |
| 32 | 32 | const defaultBackchannelAuthenticationPollInterval = 5 * time.Second | |
| 33 | 33 | ||
| 34 | + // errBackchannelAuthenticationRequestFailed is the shared newError | ||
| 35 | + // description sendBackchannelAuthenticationRequest's own retry sequence | ||
| 36 | + // uses at every call site — the same transport-level failure | ||
| 37 | + // regardless of which attempt hit it. | ||
| 38 | + const errBackchannelAuthenticationRequestFailed = "backchannel authentication request failed" | ||
| 39 | + | ||
| 34 | 40 | // BeginBackchannelAuthenticationRequest is the input to | |
| 35 | 41 | // Client.BeginBackchannelAuthentication. Exactly one of LoginHint, | |
| 36 | 42 | // LoginHintToken or IDTokenHint must be set — validated locally before | |
@@ -263,7 +269,7 @@ func (c *Client) sendBackchannelAuthenticationRequest(ctx context.Context, dpopS | |||
| 263 | 269 | if c.cfg.SenderConstrain == storage.SenderConstrainMTLS { | |
| 264 | 270 | body, status, _, err := c.postForm(ctx, endpointURL.String(), form, nil) | |
| 265 | 271 | if err != nil { | |
| 266 | - return nil, newError(ErrorInternal, "backchannel authentication request failed", err) | ||
| 272 | + return nil, newError(ErrorInternal, errBackchannelAuthenticationRequestFailed, err) | ||
| 267 | 273 | } | |
| 268 | 274 | if status != http.StatusOK { | |
| 269 | 275 | return nil, parErrorFromResponse(body) | |
@@ -272,9 +278,9 @@ func (c *Client) sendBackchannelAuthenticationRequest(ctx context.Context, dpopS | |||
| 272 | 278 | } | |
| 273 | 279 | body, status, header, err := c.postBackchannelAuthenticationRequestWithDPoP(ctx, dpopSigner, endpointURL, form, c.cachedDPoPNonce(ctx, asNonceScope)) | |
| 274 | 280 | if err != nil { | |
| 275 | - return nil, newError(ErrorInternal, "backchannel authentication request failed", err) | ||
| 281 | + return nil, newError(ErrorInternal, errBackchannelAuthenticationRequestFailed, err) | ||
| 276 | 282 | } | |
| 277 | - nextNonce := header.Get("DPoP-Nonce") | ||
| 283 | + nextNonce := header.Get(dpopNonceHeader) | ||
| 278 | 284 | c.cacheDPoPNonce(ctx, asNonceScope, nextNonce) | |
| 279 | 285 | if status == http.StatusOK { | |
| 280 | 286 | return body, nil | |
@@ -288,9 +294,9 @@ func (c *Client) sendBackchannelAuthenticationRequest(ctx context.Context, dpopS | |||
| 288 | 294 | } | |
| 289 | 295 | body, status, header, err = c.postBackchannelAuthenticationRequestWithDPoP(ctx, dpopSigner, endpointURL, retryForm, nextNonce) | |
| 290 | 296 | if err != nil { | |
| 291 | - return nil, newError(ErrorInternal, "backchannel authentication request failed", err) | ||
| 297 | + return nil, newError(ErrorInternal, errBackchannelAuthenticationRequestFailed, err) | ||
| 292 | 298 | } | |
| 293 | - c.cacheDPoPNonce(ctx, asNonceScope, header.Get("DPoP-Nonce")) | ||
| 299 | + c.cacheDPoPNonce(ctx, asNonceScope, header.Get(dpopNonceHeader)) | ||
| 294 | 300 | if status != http.StatusOK { | |
| 295 | 301 | return nil, parErrorFromResponse(body) | |
| 296 | 302 | } | |
@@ -470,15 +476,15 @@ func (c *Client) pollBackchannelAuthenticationOnce(ctx context.Context, dpopSign | |||
| 470 | 476 | if c.cfg.SenderConstrain == storage.SenderConstrainMTLS { | |
| 471 | 477 | body, status, _, err := c.postForm(ctx, tokenURL.String(), form, nil) | |
| 472 | 478 | if err != nil { | |
| 473 | - return nil, 0, newError(ErrorInternal, "token request failed", err) | ||
| 479 | + return nil, 0, newError(ErrorInternal, errTokenRequestFailed, err) | ||
| 474 | 480 | } | |
| 475 | 481 | return body, status, nil | |
| 476 | 482 | } | |
| 477 | 483 | body, status, header, err := c.postTokenRequestWithDPoP(ctx, dpopSigner, tokenURL, form, c.cachedDPoPNonce(ctx, asNonceScope)) | |
| 478 | 484 | if err != nil { | |
| 479 | - return nil, 0, newError(ErrorInternal, "token request failed", err) | ||
| 485 | + return nil, 0, newError(ErrorInternal, errTokenRequestFailed, err) | ||
| 480 | 486 | } | |
| 481 | - nextNonce := header.Get("DPoP-Nonce") | ||
| 487 | + nextNonce := header.Get(dpopNonceHeader) | ||
| 482 | 488 | c.cacheDPoPNonce(ctx, asNonceScope, nextNonce) | |
| 483 | 489 | if status == http.StatusOK || nextNonce == "" || !isDPoPNonceError(body) { | |
| 484 | 490 | return body, status, nil | |
@@ -489,9 +495,9 @@ func (c *Client) pollBackchannelAuthenticationOnce(ctx context.Context, dpopSign | |||
| 489 | 495 | } | |
| 490 | 496 | body, status, header, err = c.postTokenRequestWithDPoP(ctx, dpopSigner, tokenURL, retryForm, nextNonce) | |
| 491 | 497 | if err != nil { | |
| 492 | - return nil, 0, newError(ErrorInternal, "token request failed", err) | ||
| 498 | + return nil, 0, newError(ErrorInternal, errTokenRequestFailed, err) | ||
| 493 | 499 | } | |
| 494 | - c.cacheDPoPNonce(ctx, asNonceScope, header.Get("DPoP-Nonce")) | ||
| 500 | + c.cacheDPoPNonce(ctx, asNonceScope, header.Get(dpopNonceHeader)) | ||
| 495 | 501 | return body, status, nil | |
| 496 | 502 | } | |
| 497 | 503 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -57,6 +57,12 @@ const ( | |||
| 57 | 57 | responseModeJARM = "jarm" | |
| 58 | 58 | ) | |
| 59 | 59 | ||
| 60 | + // errPushedAuthorizationRequestFailed is the shared newError | ||
| 61 | + // description sendPushedAuthorizationRequest's own retry sequence uses | ||
| 62 | + // at every call site — the same transport-level failure regardless of | ||
| 63 | + // which attempt hit it. | ||
| 64 | + const errPushedAuthorizationRequestFailed = "pushed authorization request failed" | ||
| 65 | + | ||
| 60 | 66 | // BeginAuthorization starts a new authorization attempt: it generates | |
| 61 | 67 | // state, nonce and a PKCE verifier, builds and signs a request object | |
| 62 | 68 | // when Config.Profile requires one, authenticates to and calls the | |
@@ -185,7 +191,7 @@ func (c *Client) pushAuthorizationRequestPlain(ctx context.Context, params map[s | |||
| 185 | 191 | } | |
| 186 | 192 | body, status, _, err := c.postForm(ctx, c.cfg.Endpoints.PushedAuthorizationRequest.String(), par.EncodeForm(formParams), nil) | |
| 187 | 193 | if err != nil { | |
| 188 | - return nil, newError(ErrorInternal, "pushed authorization request failed", err) | ||
| 194 | + return nil, newError(ErrorInternal, errPushedAuthorizationRequestFailed, err) | ||
| 189 | 195 | } | |
| 190 | 196 | if status != http.StatusCreated && status != http.StatusOK { | |
| 191 | 197 | return nil, parErrorFromResponse(body) | |
@@ -212,7 +218,7 @@ func (c *Client) pushAuthorizationRequestWithJKT(ctx context.Context, params map | |||
| 212 | 218 | } | |
| 213 | 219 | body, status, _, err := c.postForm(ctx, c.cfg.Endpoints.PushedAuthorizationRequest.String(), par.EncodeForm(formParams), nil) | |
| 214 | 220 | if err != nil { | |
| 215 | - return nil, newError(ErrorInternal, "pushed authorization request failed", err) | ||
| 221 | + return nil, newError(ErrorInternal, errPushedAuthorizationRequestFailed, err) | ||
| 216 | 222 | } | |
| 217 | 223 | if status != http.StatusCreated && status != http.StatusOK { | |
| 218 | 224 | return nil, parErrorFromResponse(body) | |
@@ -252,9 +258,9 @@ func (c *Client) pushAuthorizationRequestWithDPoPProof(ctx context.Context, para | |||
| 252 | 258 | ||
| 253 | 259 | body, status, header, err := c.postParRequestWithDPoP(ctx, dpopSigner, &parURL, form, c.cachedDPoPNonce(ctx, asNonceScope)) | |
| 254 | 260 | if err != nil { | |
| 255 | - return nil, newError(ErrorInternal, "pushed authorization request failed", err) | ||
| 261 | + return nil, newError(ErrorInternal, errPushedAuthorizationRequestFailed, err) | ||
| 256 | 262 | } | |
| 257 | - nextNonce := header.Get("DPoP-Nonce") | ||
| 263 | + nextNonce := header.Get(dpopNonceHeader) | ||
| 258 | 264 | c.cacheDPoPNonce(ctx, asNonceScope, nextNonce) | |
| 259 | 265 | if status == http.StatusCreated || status == http.StatusOK { | |
| 260 | 266 | return body, nil | |
@@ -269,9 +275,9 @@ func (c *Client) pushAuthorizationRequestWithDPoPProof(ctx context.Context, para | |||
| 269 | 275 | } | |
| 270 | 276 | body, status, header, err = c.postParRequestWithDPoP(ctx, dpopSigner, &parURL, retryForm, nextNonce) | |
| 271 | 277 | if err != nil { | |
| 272 | - return nil, newError(ErrorInternal, "pushed authorization request failed", err) | ||
| 278 | + return nil, newError(ErrorInternal, errPushedAuthorizationRequestFailed, err) | ||
| 273 | 279 | } | |
| 274 | - c.cacheDPoPNonce(ctx, asNonceScope, header.Get("DPoP-Nonce")) | ||
| 280 | + c.cacheDPoPNonce(ctx, asNonceScope, header.Get(dpopNonceHeader)) | ||
| 275 | 281 | if status != http.StatusCreated && status != http.StatusOK { | |
| 276 | 282 | return nil, parErrorFromResponse(body) | |
| 277 | 283 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,132 @@ | |||
| 1 | + package client_test | ||
| 2 | + | ||
| 3 | + import ( | ||
| 4 | + "context" | ||
| 5 | + "net/http" | ||
| 6 | + "net/http/httptest" | ||
| 7 | + "testing" | ||
| 8 | + | ||
| 9 | + fapi "github.com/idfoundry/fapigo" | ||
| 10 | + "github.com/idfoundry/fapigo/client" | ||
| 11 | + "github.com/idfoundry/fapigo/storage" | ||
| 12 | + ) | ||
| 13 | + | ||
| 14 | + // TestBeginAuthorizationPropagatesTransportFailureOnPARPlain covers | ||
| 15 | + // pushAuthorizationRequestPlain's (SenderConstrainMTLS) transport-failure | ||
| 16 | + // branch — a connection failure at PAR, as opposed to an HTTP-level | ||
| 17 | + // error response. | ||
| 18 | + func TestBeginAuthorizationPropagatesTransportFailureOnPARPlain(t *testing.T) { | ||
| 19 | + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 20 | + w.WriteHeader(http.StatusOK) | ||
| 21 | + })) | ||
| 22 | + ts.Close() // closed before use: every request now fails to connect | ||
| 23 | + | ||
| 24 | + cfg := validConfig(t) | ||
| 25 | + cfg.SenderConstrain = storage.SenderConstrainMTLS | ||
| 26 | + cfg.Algorithms.DPoP = 0 | ||
| 27 | + parURL, err := fapi.ParseEndpointURL(ts.URL+"/par", fapi.AllowLoopbackHTTP()) | ||
| 28 | + if err != nil { | ||
| 29 | + t.Fatalf("ParseEndpointURL(par): %v", err) | ||
| 30 | + } | ||
| 31 | + cfg.Endpoints.PushedAuthorizationRequest = parURL | ||
| 32 | + | ||
| 33 | + deps := validDependencies(t) | ||
| 34 | + deps.HTTP = ts.Client() | ||
| 35 | + | ||
| 36 | + c, err := client.New(cfg, deps) | ||
| 37 | + if err != nil { | ||
| 38 | + t.Fatalf("client.New: %v", err) | ||
| 39 | + } | ||
| 40 | + if _, err := c.BeginAuthorization(context.Background(), client.BeginAuthorizationRequest{Scope: []string{"openid"}}); err == nil { | ||
| 41 | + t.Fatalf("BeginAuthorization(MTLS PAR transport failure) = nil error, want error") | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + // TestBeginAuthorizationPropagatesTransportFailureOnPARWithJKT covers | ||
| 46 | + // pushAuthorizationRequestWithJKT's transport-failure branch. | ||
| 47 | + func TestBeginAuthorizationPropagatesTransportFailureOnPARWithJKT(t *testing.T) { | ||
| 48 | + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 49 | + w.WriteHeader(http.StatusOK) | ||
| 50 | + })) | ||
| 51 | + ts.Close() // closed before use: every request now fails to connect | ||
| 52 | + | ||
| 53 | + cfg := validConfig(t) | ||
| 54 | + cfg.PARDPoPBinding = client.PARDPoPBindingJKT | ||
| 55 | + parURL, err := fapi.ParseEndpointURL(ts.URL+"/par", fapi.AllowLoopbackHTTP()) | ||
| 56 | + if err != nil { | ||
| 57 | + t.Fatalf("ParseEndpointURL(par): %v", err) | ||
| 58 | + } | ||
| 59 | + cfg.Endpoints.PushedAuthorizationRequest = parURL | ||
| 60 | + | ||
| 61 | + deps := validDependencies(t) | ||
| 62 | + deps.HTTP = ts.Client() | ||
| 63 | + | ||
| 64 | + c, err := client.New(cfg, deps) | ||
| 65 | + if err != nil { | ||
| 66 | + t.Fatalf("client.New: %v", err) | ||
| 67 | + } | ||
| 68 | + if _, err := c.BeginAuthorization(context.Background(), client.BeginAuthorizationRequest{Scope: []string{"openid"}}); err == nil { | ||
| 69 | + t.Fatalf("BeginAuthorization(dpop_jkt PAR transport failure) = nil error, want error") | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + // TestBeginAuthorizationPropagatesTransportFailureOnPARWithDPoPProof | ||
| 74 | + // covers pushAuthorizationRequestWithDPoPProof's initial-attempt | ||
| 75 | + // transport-failure branch (PARDPoPBindingProof, the default). | ||
| 76 | + func TestBeginAuthorizationPropagatesTransportFailureOnPARWithDPoPProof(t *testing.T) { | ||
| 77 | + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 78 | + w.WriteHeader(http.StatusOK) | ||
| 79 | + })) | ||
| 80 | + ts.Close() // closed before use: every request now fails to connect | ||
| 81 | + | ||
| 82 | + cfg := validConfig(t) | ||
| 83 | + parURL, err := fapi.ParseEndpointURL(ts.URL+"/par", fapi.AllowLoopbackHTTP()) | ||
| 84 | + if err != nil { | ||
| 85 | + t.Fatalf("ParseEndpointURL(par): %v", err) | ||
| 86 | + } | ||
| 87 | + cfg.Endpoints.PushedAuthorizationRequest = parURL | ||
| 88 | + | ||
| 89 | + deps := validDependencies(t) | ||
| 90 | + deps.HTTP = ts.Client() | ||
| 91 | + | ||
| 92 | + c, err := client.New(cfg, deps) | ||
| 93 | + if err != nil { | ||
| 94 | + t.Fatalf("client.New: %v", err) | ||
| 95 | + } | ||
| 96 | + if _, err := c.BeginAuthorization(context.Background(), client.BeginAuthorizationRequest{Scope: []string{"openid"}}); err == nil { | ||
| 97 | + t.Fatalf("BeginAuthorization(PAR transport failure) = nil error, want error") | ||
| 98 | + } | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + // TestBeginAuthorizationPropagatesTransportFailureOnPARRetry covers | ||
| 102 | + // pushAuthorizationRequestWithDPoPProof's retry-attempt transport-failure | ||
| 103 | + // branch: the initial PAR call succeeds far enough to receive a | ||
| 104 | + // use_dpop_nonce challenge, but the replay carrying the fresh nonce | ||
| 105 | + // fails at the transport level. | ||
| 106 | + func TestBeginAuthorizationPropagatesTransportFailureOnPARRetry(t *testing.T) { | ||
| 107 | + as := newFakeAS(t, testIssuer, false) | ||
| 108 | + ts := httptest.NewServer(as.handler()) | ||
| 109 | + t.Cleanup(ts.Close) | ||
| 110 | + as.challengeParDPoPNonce = "server-issued-par-nonce" | ||
| 111 | + | ||
| 112 | + cfg := validConfig(t) | ||
| 113 | + parURL, err := fapi.ParseEndpointURL(ts.URL+"/par", fapi.AllowLoopbackHTTP()) | ||
| 114 | + if err != nil { | ||
| 115 | + t.Fatalf("ParseEndpointURL(par): %v", err) | ||
| 116 | + } | ||
| 117 | + cfg.Endpoints.PushedAuthorizationRequest = parURL | ||
| 118 | + | ||
| 119 | + deps := validDependencies(t) | ||
| 120 | + deps.HTTP = &failNthRequestHTTPClient{real: ts.Client(), pathSuffix: "/par", failOn: 2} | ||
| 121 | + | ||
| 122 | + c, err := client.New(cfg, deps) | ||
| 123 | + if err != nil { | ||
| 124 | + t.Fatalf("client.New: %v", err) | ||
| 125 | + } | ||
| 126 | + if _, err := c.BeginAuthorization(context.Background(), client.BeginAuthorizationRequest{Scope: []string{"openid"}}); err == nil { | ||
| 127 | + t.Fatalf("BeginAuthorization(PAR retry transport failure) = nil error, want error") | ||
| 128 | + } | ||
| 129 | + if as.parCallCount != 1 { | ||
| 130 | + t.Fatalf("parCallCount = %d, want 1 (the retry failed at the transport, never reaching the fake AS)", as.parCallCount) | ||
| 131 | + } | ||
| 132 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -255,7 +255,7 @@ func (c *Client) sendTokenRequest(ctx context.Context, dpopSigner crypto.Signer, | |||
| 255 | 255 | if c.cfg.SenderConstrain == storage.SenderConstrainMTLS { | |
| 256 | 256 | body, status, _, err := c.postForm(ctx, tokenURL.String(), form, nil) | |
| 257 | 257 | if err != nil { | |
| 258 | - return nil, newError(ErrorInternal, "token request failed", err) | ||
| 258 | + return nil, newError(ErrorInternal, errTokenRequestFailed, err) | ||
| 259 | 259 | } | |
| 260 | 260 | if status != http.StatusOK { | |
| 261 | 261 | return nil, parErrorFromResponse(body) | |
@@ -264,9 +264,9 @@ func (c *Client) sendTokenRequest(ctx context.Context, dpopSigner crypto.Signer, | |||
| 264 | 264 | } | |
| 265 | 265 | body, status, header, err := c.postTokenRequestWithDPoP(ctx, dpopSigner, tokenURL, form, c.cachedDPoPNonce(ctx, asNonceScope)) | |
| 266 | 266 | if err != nil { | |
| 267 | - return nil, newError(ErrorInternal, "token request failed", err) | ||
| 267 | + return nil, newError(ErrorInternal, errTokenRequestFailed, err) | ||
| 268 | 268 | } | |
| 269 | - nextNonce := header.Get("DPoP-Nonce") | ||
| 269 | + nextNonce := header.Get(dpopNonceHeader) | ||
| 270 | 270 | c.cacheDPoPNonce(ctx, asNonceScope, nextNonce) | |
| 271 | 271 | if status == http.StatusOK { | |
| 272 | 272 | return body, nil | |
@@ -281,9 +281,9 @@ func (c *Client) sendTokenRequest(ctx context.Context, dpopSigner crypto.Signer, | |||
| 281 | 281 | } | |
| 282 | 282 | body, status, header, err = c.postTokenRequestWithDPoP(ctx, dpopSigner, tokenURL, retryForm, nextNonce) | |
| 283 | 283 | if err != nil { | |
| 284 | - return nil, newError(ErrorInternal, "token request failed", err) | ||
| 284 | + return nil, newError(ErrorInternal, errTokenRequestFailed, err) | ||
| 285 | 285 | } | |
| 286 | - c.cacheDPoPNonce(ctx, asNonceScope, header.Get("DPoP-Nonce")) | ||
| 286 | + c.cacheDPoPNonce(ctx, asNonceScope, header.Get(dpopNonceHeader)) | ||
| 287 | 287 | if status != http.StatusOK { | |
| 288 | 288 | return nil, parErrorFromResponse(body) | |
| 289 | 289 | } | |
| Back | FazBrowse Home | New Git URL |
0 commit comments