feat(cognito): show the client's identity providers on the hosted login page

The hosted login page now offers a "Continue with <provider>" button for each
of the app client's other SupportedIdentityProviders, carrying the same
authorize parameters (state, nonce, PKCE) into the federated flow, as Cognito's
managed login does. A client that lists only external providers gets the
buttons without the password form instead of an error.
This commit is contained in:
Zakaria
2026-10-24 12:14:04 -04:00
parent 2e7ebefca9
commit 174cb600d4
3 changed files with 84 additions and 21 deletions
@@ -23,6 +23,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -171,11 +172,32 @@ class CognitoHostedLoginIntegrationTest {
"AllowedOAuthFlows":["code"],"AllowedOAuthScopes":["openid"],"CallbackURLs":["%s"],
"SupportedIdentityProviders":["CompanyIdp"]}
""".formatted(poolId, CALLBACK)).path("UserPoolClient").path("ClientId").asText();
given().redirects().follow(false)
String federatedLogin = given().redirects().follow(false)
.queryParam("client_id", federatedOnly).queryParam("redirect_uri", CALLBACK).queryParam("response_type", "code")
.queryParam("state", "app-state")
.when().get("/cognito-idp/oauth2/authorize")
.then().statusCode(302).extract().header("Location");
String federatedPage = given().redirects().follow(false).urlEncodingEnabled(false).get(federatedLogin)
.then().statusCode(200).extract().asString();
assertTrue(federatedPage.contains(">Continue with CompanyIdp</a>"), federatedPage);
assertTrue(federatedPage.contains("identity_provider=CompanyIdp"), "the button starts the federated flow");
assertTrue(federatedPage.contains("state=app-state"), "the button keeps the app's authorize parameters");
assertFalse(federatedPage.contains("name=\"password\""), "no password form without COGNITO");
given().redirects().follow(false)
.formParam("client_id", federatedOnly).formParam("redirect_uri", CALLBACK).formParam("response_type", "code")
.formParam("username", email).formParam("password", PASSWORD)
.when().post("/cognito-idp/login")
.then().statusCode(400).body(containsString("COGNITO is not one of its identity providers"));
cognitoJson("UpdateUserPoolClient", """
{"UserPoolId":"%s","ClientId":"%s","ClientName":"hosted-web","AllowedOAuthFlowsUserPoolClient":true,
"AllowedOAuthFlows":["code"],"AllowedOAuthScopes":["openid","email"],
"CallbackURLs":["%s"],"LogoutURLs":["%s"],"SupportedIdentityProviders":["COGNITO","CompanyIdp"],
"ExplicitAuthFlows":["ALLOW_REFRESH_TOKEN_AUTH"]}
""".formatted(poolId, clientId, CALLBACK, SIGNED_OUT));
String both = loginPage().asString();
assertTrue(both.contains("name=\"password\"") && both.contains(">Continue with CompanyIdp</a>"), both);
given().redirects().follow(false)
.queryParam("client_id", clientId).queryParam("redirect_uri", CALLBACK).queryParam("response_type", "code")
.queryParam("code_challenge", "abc").queryParam("code_challenge_method", "plain")