Test conflicting /token redemptions, bringing us up to 66% coverage

Signed-off-by: Olivier 'reivilibre <olivier@librepush.net>
This commit is contained in:
Olivier 'reivilibre' 2024-07-07 12:59:48 +01:00
parent 8fe54e7507
commit f0cd8fa767
3 changed files with 125 additions and 12 deletions

View File

@ -0,0 +1,9 @@
---
source: src/tests/test_oidc_auth_flow.rs
expression: "(headers, text)"
---
- access-control-allow-origin: "*"
access-control-expose-headers: "*"
content-length: "124"
content-type: application/json
- "{\"error\":\"invalid_grant\",\"error_description\":\"Auth code has been redeemed multiple times! This could mean something nasty.\"}"

View File

@ -1,9 +0,0 @@
---
source: src/tests/test_oidc_auth_flow.rs
expression: "(headers, text)"
---
- content-length: "238"
content-type: text/html; charset=utf-8
set-cookie: __Host-SessionlessXsrf=HL4qRFKUlBqkrPTvAQ6z-w; HttpOnly; SameSite=Strict; Secure; Path=/; Max-Age=43200000
x-frame-options: DENY
- "<form method='POST'>UN<input type='text' name='username'> PW<input type='password' name='password'> <input type='hidden' name='xsrf' value='HL4qRFKUlBqkrPTvAQ6z-w'><button type='submit'>click here to login</button> (temporary form)</form>"

View File

@ -220,9 +220,6 @@ async fn test_userinfo_bad_auth() {
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() {
@ -357,3 +354,119 @@ async fn test_token_errors() {
let json: BTreeMap<String, serde_json::Value> = serde_json::from_str(&text).unwrap();
assert_yaml_snapshot!("6/token_wrong_verifier", (headers, json));
}
/// Tests double-requesting a /token (auth code conflict)
#[tokio::test]
async fn test_token_conflict() {
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);
eprintln!("{:?}", _text);
///// At this point, we make requests on behalf of the client /////
// 3. /token request (successful)
let resp = client
.post("/oidc/token")
.header("Authorization", "Basic YWNsaWVudDpzZWNyZXRB")
.form(&btreemap! {
"code" => "LRtIBH5rO3O7hwWaF_UkuFJy0v2xqtGQ",
"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. POST /token", resp).await;
assert_eq!(status, 200);
let json: BTreeMap<String, serde_json::Value> = serde_json::from_str(&text).unwrap();
let access_token = json
.get("access_token")
.unwrap()
.as_str()
.unwrap()
.to_owned();
// 4. /token request (conflicting)
let resp = client
.post("/oidc/token")
.header("Authorization", "Basic YWNsaWVudDpzZWNyZXRB")
.form(&btreemap! {
"code" => "LRtIBH5rO3O7hwWaF_UkuFJy0v2xqtGQ",
"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. POST /token (conflict)", resp).await;
assert_eq!(status, 400);
assert_yaml_snapshot!("4/token_conflict", (headers, text));
// 5. /userinfo (using the access token that should now have expired)
let resp = client
.get("/oidc/userinfo")
.header("Authorization", format!("Bearer {access_token}"))
.send()
.await;
let (status, _headers, _text) = dump_resp_text("7. /userinfo", resp).await;
assert_eq!(status, 401);
}