diff --git a/.github/workflows/entra-live-check.yml b/.github/workflows/entra-live-check.yml index 9baf011..de7d4f4 100644 --- a/.github/workflows/entra-live-check.yml +++ b/.github/workflows/entra-live-check.yml @@ -49,6 +49,10 @@ jobs: env: ENTRA_TENANT_ID: ${{ secrets.ENTRA_TENANT_ID }} ENTRA_CLIENT_ID: ${{ secrets.ENTRA_CLIENT_ID }} + # Optional: only set when the app registration is a CONFIDENTIAL client (i.e. "Allow + # public client flows" is off). Absent, the check sends no secret, which is what a public + # client requires. + ENTRA_CLIENT_SECRET: ${{ secrets.ENTRA_CLIENT_SECRET }} ENTRA_TEST_USERNAME: ${{ secrets.ENTRA_TEST_USERNAME }} ENTRA_TEST_PASSWORD: ${{ secrets.ENTRA_TEST_PASSWORD }} run: cargo run --example entra_live_check diff --git a/auth-oidc/examples/entra_live_check.rs b/auth-oidc/examples/entra_live_check.rs index d2e15a9..9adc91a 100644 --- a/auth-oidc/examples/entra_live_check.rs +++ b/auth-oidc/examples/entra_live_check.rs @@ -39,15 +39,26 @@ fn main() { let token_endpoint = format!("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"); let http = reqwest::blocking::Client::new(); + // A PUBLIC client sends no secret; a CONFIDENTIAL one must. Which of the two an app + // registration is depends on its "Allow public client flows" setting, and flipping that setting + // in the portal changes what this request has to look like — Entra rejects the mismatch with + // AADSTS7000218 rather than anything that names the setting. Sending the secret only when one + // is provisioned lets the same code cover both, so the check does not have to be edited every + // time the tenant is reconfigured. + let client_secret = env_or_skip("ENTRA_CLIENT_SECRET"); + let mut form = vec![ + ("grant_type", "password"), + ("client_id", client_id.as_str()), + ("username", username.as_str()), + ("password", password.as_str()), + ("scope", "openid profile email"), + ]; + if let Some(secret) = client_secret.as_deref() { + form.push(("client_secret", secret)); + } let resp = http .post(&token_endpoint) - .form(&[ - ("grant_type", "password"), - ("client_id", &client_id), - ("username", &username), - ("password", &password), - ("scope", "openid profile email"), - ]) + .form(&form) .send() .expect("real POST to Entra's real token endpoint"); @@ -61,7 +72,11 @@ fn main() { status.is_success(), "Entra rejected the real password grant ({status}): {body}. If this is \ AADSTS50055 (password expired), sign in interactively once as the test user to set a \ - permanent password, then re-provision the ENTRA_TEST_PASSWORD secret." + permanent password, then re-provision the ENTRA_TEST_PASSWORD secret. If it is \ + AADSTS7000218 ('the request body must contain client_assertion or client_secret'), the \ + app registration is being treated as a CONFIDENTIAL client: either re-enable 'Allow \ + public client flows' on it, or provision an ENTRA_CLIENT_SECRET repo secret, which this \ + check will then send." ); let id_token = body["id_token"] .as_str()