diff --git a/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__3__token_no_code.snap b/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__3__token_no_code.snap new file mode 100644 index 0000000..53b3600 --- /dev/null +++ b/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__3__token_no_code.snap @@ -0,0 +1,10 @@ +--- +source: src/tests/test_oidc_auth_flow.rs +expression: "(headers, json)" +--- +- access-control-allow-origin: "*" + access-control-expose-headers: "*" + content-length: "75" + content-type: application/json +- error: invalid_request + error_description: "`code` parameter missing." diff --git a/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__4__token_malformed_code.snap b/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__4__token_malformed_code.snap new file mode 100644 index 0000000..01444ff --- /dev/null +++ b/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__4__token_malformed_code.snap @@ -0,0 +1,10 @@ +--- +source: src/tests/test_oidc_auth_flow.rs +expression: "(headers, json)" +--- +- access-control-allow-origin: "*" + access-control-expose-headers: "*" + content-length: "77" + content-type: application/json +- error: invalid_request + error_description: "`code` parameter malformed." diff --git a/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__5__token_no_verifier.snap b/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__5__token_no_verifier.snap new file mode 100644 index 0000000..23a3f3b --- /dev/null +++ b/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__5__token_no_verifier.snap @@ -0,0 +1,10 @@ +--- +source: src/tests/test_oidc_auth_flow.rs +expression: "(headers, json)" +--- +- access-control-allow-origin: "*" + access-control-expose-headers: "*" + content-length: "84" + content-type: application/json +- error: invalid_request + error_description: "`code_verifier` parameter missing." diff --git a/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__6__token_wrong_verifier.snap b/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__6__token_wrong_verifier.snap new file mode 100644 index 0000000..709152b --- /dev/null +++ b/src/tests/snapshots/idcoop__tests__test_oidc_auth_flow__6__token_wrong_verifier.snap @@ -0,0 +1,10 @@ +--- +source: src/tests/test_oidc_auth_flow.rs +expression: "(headers, json)" +--- +- access-control-allow-origin: "*" + access-control-expose-headers: "*" + content-length: "74" + content-type: application/json +- error: invalid_grant + error_description: Code challenge is invalid. diff --git a/src/tests/test_oidc_auth_flow.rs b/src/tests/test_oidc_auth_flow.rs index ce56130..bc0ce3b 100644 --- a/src/tests/test_oidc_auth_flow.rs +++ b/src/tests/test_oidc_auth_flow.rs @@ -49,7 +49,7 @@ async fn test_full_flow() { let pwhash = create_password_hash("secret", &sys.config.password_hashing).unwrap(); let _: () = sys .store - .txn(|mut txn| { + .txn(|txn| { Box::pin(async move { sqlx::query( "INSERT INTO users (user_name, user_id, created_at_utc, password_hash, locked) VALUES ($1, $2, NOW(), $3, $4) RETURNING user_id", @@ -219,3 +219,140 @@ async fn test_userinfo_bad_auth() { assert_eq!(status, 401); assert_yaml_snapshot!("3. wrong auth token", (headers, text)); } + +// Tests TODO: +// /token conflict — including checking that the old access token gets revoked + +/// Tests error conditions in the /token endpoint +#[tokio::test] +async fn test_token_errors() { + let sys = basic_system().await; + + let uuid = Uuid::nil(); + let pwhash = create_password_hash("secret", &sys.config.password_hashing).unwrap(); + let _: () = sys + .store + .txn(|txn| { + Box::pin(async move { + sqlx::query( + "INSERT INTO users (user_name, user_id, created_at_utc, password_hash, locked) VALUES ($1, $2, NOW(), $3, $4) RETURNING user_id", + ).bind("robert").bind(uuid).bind(pwhash).bind(false) + .fetch_one(&mut **txn.txn) + .await.unwrap(); + Ok(()) + }) + }) + .await + .unwrap(); + + let client = TestClient::new(sys.web); + + ///// These requests are on behalf of the user's browser ///// + + const CODE_VERIFIER: &str = "verifying"; + // base64(sha256("verifying")) + const CODE_CHALLENGE: &str = "LeU9Sprdh-i2mzasKGh8-hmbnmzk48l3Siw390dKY3M"; + + // 1. /login request with credentials + let resp = client + .post("/login") + .form(&btreemap! { + "username" => "robert", + "password" => "secret", + "xsrf" => "HL4qRFKUlBqkrPTvAQ6z-w", + }) + .header("Cookie", "__Host-SessionlessXsrf=HL4qRFKUlBqkrPTvAQ6z-w") + // /login is rate-limited by IP source and needs an IP + .header("X-Forwarded-For", "0.0.0.0") + .send() + .await; + let (status, _headers, _text) = + dump_resp_text("1. /login request with credentials", resp).await; + assert_eq!(status, 302); + + let auth_loc = format!("/oidc/auth?scope=openid&client_id=aclient&response_type=code&state=wombat&redirect_uri=http:%2F%2Faclient.example.com%2Fredirect&code_challenge={CODE_CHALLENGE}&code_challenge_method=S256&nonce=noncey"); + + // 2. /auth request with confirmation + let resp = client + .post(&auth_loc) + .header( + "Cookie", + "__Host-LoginSession=HL4qRFKUlBqkrPTvAQ6z-xpYf2uo9sbO68miVnnz7KE", + ) + .form(&btreemap! { + "action" => "accept", + "xsrf" => "0.48qkqIorf3dyk1LgVQwyNT82yDHyqHbXge09Rvfsz8Y", + }) + .send() + .await; + let (status, _headers, _text) = dump_resp_text("2. POST /auth after confirmation", resp).await; + assert_eq!(status, 302); + + ///// At this point, we make requests on behalf of the client ///// + + // 3. /token request with no code + let resp = client + .post("/oidc/token") + .header("Authorization", "Basic YWNsaWVudDpzZWNyZXRB") + .form(&btreemap! { + "code_verifier" => CODE_VERIFIER, + "grant_type" => "authorization_code", + "redirect_uri" => "http://aclient.example.com/redirect", + }) + .send() + .await; + let (status, headers, text) = dump_resp_text("3. /token no code", resp).await; + assert_eq!(status, 400); + let json: BTreeMap = serde_json::from_str(&text).unwrap(); + assert_yaml_snapshot!("3/token_no_code", (headers, json)); + + // 4. /token request with malformed code (not long enough) + let resp = client + .post("/oidc/token") + .header("Authorization", "Basic YWNsaWVudDpzZWNyZXRB") + .form(&btreemap! { + "code" => "aaaa", + "code_verifier" => CODE_VERIFIER, + "grant_type" => "authorization_code", + "redirect_uri" => "http://aclient.example.com/redirect", + }) + .send() + .await; + let (status, headers, text) = dump_resp_text("4. /token malformed code", resp).await; + assert_eq!(status, 400); + let json: BTreeMap = serde_json::from_str(&text).unwrap(); + assert_yaml_snapshot!("4/token_malformed_code", (headers, json)); + + // 5. /token request with no code_verifier + let resp = client + .post("/oidc/token") + .header("Authorization", "Basic YWNsaWVudDpzZWNyZXRB") + .form(&btreemap! { + "code" => "HL4qRFKUlBqkrPTvAQ6z-xpYf2uo9sbO", + "grant_type" => "authorization_code", + "redirect_uri" => "http://aclient.example.com/redirect", + }) + .send() + .await; + let (status, headers, text) = dump_resp_text("5. /token no verifier", resp).await; + assert_eq!(status, 400); + let json: BTreeMap = serde_json::from_str(&text).unwrap(); + assert_yaml_snapshot!("5/token_no_verifier", (headers, json)); + + // 6. /token request with wrong code_verifier + let resp = client + .post("/oidc/token") + .header("Authorization", "Basic YWNsaWVudDpzZWNyZXRB") + .form(&btreemap! { + "code" => "HL4qRFKUlBqkrPTvAQ6z-xpYf2uo9sbO", + "code_verifier" => "i'm wrong", + "grant_type" => "authorization_code", + "redirect_uri" => "http://aclient.example.com/redirect", + }) + .send() + .await; + let (status, headers, text) = dump_resp_text("6. /token wrong verifier", resp).await; + assert_eq!(status, 400); + let json: BTreeMap = serde_json::from_str(&text).unwrap(); + assert_yaml_snapshot!("6/token_wrong_verifier", (headers, json)); +}