mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-26 13:50:48 +00:00
Enable use of kc_idp_hint in Pushed Authorization Requests.
The client can select which Identity Provider to use for user authentication by including an Identity Provider alias in a "kc_idp_hint" parameter in a Pushed Authorization Request. Closes #47229 Signed-off-by: Laurids Møller Jepsen <laurids.jepsen@cryptomathic.com>
This commit is contained in:
committed by
Marek Posolda
parent
d15b258880
commit
3e3191d60c
@@ -47,6 +47,6 @@ public interface AdapterConstants {
|
||||
// Cookie used on adapter side to store token info. Used only when tokenStore is 'COOKIE'
|
||||
public static final String KEYCLOAK_ADAPTER_STATE_COOKIE = "KEYCLOAK_ADAPTER_STATE";
|
||||
|
||||
// Request parameter used to specify the identifier of the identity provider that should be used to authenticate an user
|
||||
// Request parameter used to specify the identifier of the identity provider that should be used to authenticate a user
|
||||
String KC_IDP_HINT = "kc_idp_hint";
|
||||
}
|
||||
|
||||
+7
-4
@@ -46,10 +46,13 @@ public class IdentityProviderAuthenticator implements Authenticator {
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticationFlowContext context) {
|
||||
if (context.getUriInfo().getQueryParameters().containsKey(AdapterConstants.KC_IDP_HINT)) {
|
||||
String providerId = context.getUriInfo().getQueryParameters().getFirst(AdapterConstants.KC_IDP_HINT);
|
||||
if (providerId == null || providerId.equals("")) {
|
||||
LOG.tracef("Skipping: kc_idp_hint query parameter is empty");
|
||||
String providerId = context.getUriInfo().getQueryParameters().containsKey(AdapterConstants.KC_IDP_HINT)
|
||||
? context.getUriInfo().getQueryParameters().getFirst(AdapterConstants.KC_IDP_HINT)
|
||||
: context.getAuthenticationSession().getClientNote(AdapterConstants.KC_IDP_HINT);
|
||||
|
||||
if (providerId != null) {
|
||||
if (providerId.equals("")) {
|
||||
LOG.tracef("Skipping: %s parameter is empty", AdapterConstants.KC_IDP_HINT);
|
||||
context.attempted();
|
||||
} else {
|
||||
LOG.tracef("Redirecting: %s set to %s", AdapterConstants.KC_IDP_HINT, providerId);
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
@@ -83,6 +84,11 @@ public class ParRequest extends AbstractHttpPostRequest<ParRequest, ParResponse>
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParRequest idpHint(String idpHint) {
|
||||
parameter(AdapterConstants.KC_IDP_HINT, idpHint);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initRequest() {
|
||||
parameter(OAuth2Constants.RESPONSE_TYPE, client.config().getResponseType());
|
||||
|
||||
+90
@@ -59,11 +59,13 @@ import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPoliciesBuilder;
|
||||
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder;
|
||||
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfileBuilder;
|
||||
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfilesBuilder;
|
||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
import org.keycloak.testsuite.util.RoleBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AbstractHttpResponse;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
|
||||
import org.keycloak.testsuite.util.oauth.OAuthClient;
|
||||
import org.keycloak.testsuite.util.oauth.ParRequest;
|
||||
import org.keycloak.testsuite.util.oauth.ParResponse;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
@@ -99,6 +101,8 @@ public class ParTest extends AbstractClientPoliciesTest {
|
||||
private static final String VALID_CORS_URL = "http://localtest.me:8180";
|
||||
private static final String INVALID_CORS_URL = "http://invalid.localtest.me:8180";
|
||||
|
||||
private static final String IDENTITY_PROVIDER_ALIAS = "test-idp";
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
|
||||
@@ -133,6 +137,8 @@ public class ParTest extends AbstractClientPoliciesTest {
|
||||
realm.getClients().add(ClientBuilder.create().redirectUris(VALID_CORS_URL + "/realms/master/app")
|
||||
.addWebOrigin(VALID_CORS_URL).clientId("test-app2").publicClient().directAccessGrants().build());
|
||||
|
||||
realm.addIdentityProvider(IdentityProviderBuilder.create().alias(IDENTITY_PROVIDER_ALIAS).providerId("oidc").build());
|
||||
|
||||
testRealms.add(realm);
|
||||
}
|
||||
|
||||
@@ -1225,6 +1231,90 @@ public class ParTest extends AbstractClientPoliciesTest {
|
||||
assertEquals("Exception thrown intentionally", response.getErrorDescription());
|
||||
}
|
||||
|
||||
// Successful redirection to identity provider when including kc_idp_hint in the PAR request.
|
||||
@Test
|
||||
public void testSuccessfulIdpRedirectUsingIdpHintInParRequest() throws Exception {
|
||||
try {
|
||||
// setup PAR realm settings
|
||||
int requestUriLifespan = 45;
|
||||
setParRealmSettings(requestUriLifespan);
|
||||
|
||||
// create client dynamically
|
||||
String clientId = createClientDynamically(generateSuffixedName(CLIENT_NAME), (OIDCClientRepresentation clientRep) -> {
|
||||
clientRep.setRequirePushedAuthorizationRequests(Boolean.TRUE);
|
||||
clientRep.setRedirectUris(new ArrayList<String>(Arrays.asList(CLIENT_REDIRECT_URI)));
|
||||
});
|
||||
OIDCClientRepresentation oidcCRep = getClientDynamically(clientId);
|
||||
String clientSecret = oidcCRep.getClientSecret();
|
||||
assertEquals(Boolean.TRUE, oidcCRep.getRequirePushedAuthorizationRequests());
|
||||
assertTrue(oidcCRep.getRedirectUris().contains(CLIENT_REDIRECT_URI));
|
||||
assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, oidcCRep.getTokenEndpointAuthMethod());
|
||||
|
||||
// Pushed Authorization Request
|
||||
oauth.client(clientId, clientSecret);
|
||||
oauth.redirectUri(CLIENT_REDIRECT_URI);
|
||||
ParRequest parRequest = oauth.pushedAuthorizationRequest()
|
||||
.idpHint(IDENTITY_PROVIDER_ALIAS);
|
||||
ParResponse pResp = parRequest.send();
|
||||
assertEquals(201, pResp.getStatusCode());
|
||||
String requestUri = pResp.getRequestUri();
|
||||
assertEquals(requestUriLifespan, pResp.getExpiresIn());
|
||||
|
||||
// Authorization Request with request_uri of PAR
|
||||
// remove parameters as query strings of uri
|
||||
oauth.redirectUri(null);
|
||||
oauth.scope(null);
|
||||
oauth.responseType(null);
|
||||
String state = "testSuccessfulIdpRedirectUsingIdpHintInParRequest";
|
||||
oauth.loginForm().requestUri(requestUri).state(state).open();
|
||||
assertThat(driver.getCurrentUrl(), startsWith(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + oauth.getRealm() + "/broker/%s/login".formatted(IDENTITY_PROVIDER_ALIAS)));
|
||||
} finally {
|
||||
restoreParRealmSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// No identity provider redirection when including an invalid alias as kc_idp_hint in the PAR request.
|
||||
@Test
|
||||
public void testNoIdpRedirectUsingInvalidIdpHintInParRequest() throws Exception {
|
||||
try {
|
||||
// setup PAR realm settings
|
||||
int requestUriLifespan = 45;
|
||||
setParRealmSettings(requestUriLifespan);
|
||||
|
||||
// create client dynamically
|
||||
String clientId = createClientDynamically(generateSuffixedName(CLIENT_NAME), (OIDCClientRepresentation clientRep) -> {
|
||||
clientRep.setRequirePushedAuthorizationRequests(Boolean.TRUE);
|
||||
clientRep.setRedirectUris(new ArrayList<String>(Arrays.asList(CLIENT_REDIRECT_URI)));
|
||||
});
|
||||
OIDCClientRepresentation oidcCRep = getClientDynamically(clientId);
|
||||
String clientSecret = oidcCRep.getClientSecret();
|
||||
assertEquals(Boolean.TRUE, oidcCRep.getRequirePushedAuthorizationRequests());
|
||||
assertTrue(oidcCRep.getRedirectUris().contains(CLIENT_REDIRECT_URI));
|
||||
assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, oidcCRep.getTokenEndpointAuthMethod());
|
||||
|
||||
// Pushed Authorization Request
|
||||
oauth.client(clientId, clientSecret);
|
||||
oauth.redirectUri(CLIENT_REDIRECT_URI);
|
||||
ParRequest parRequest = oauth.pushedAuthorizationRequest()
|
||||
.idpHint("invalid-idp-alias");
|
||||
ParResponse pResp = parRequest.send();
|
||||
assertEquals(201, pResp.getStatusCode());
|
||||
String requestUri = pResp.getRequestUri();
|
||||
assertEquals(requestUriLifespan, pResp.getExpiresIn());
|
||||
|
||||
// Authorization Request with request_uri of PAR
|
||||
// remove parameters as query strings of uri
|
||||
oauth.redirectUri(null);
|
||||
oauth.scope(null);
|
||||
oauth.responseType(null);
|
||||
String state = "testNoIdpRedirectUsingInvalidIdpHintInParRequest";
|
||||
oauth.loginForm().requestUri(requestUri).state(state).open();
|
||||
assertThat(driver.getCurrentUrl(), startsWith(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + oauth.getRealm() + "/login-actions/authenticate"));
|
||||
} finally {
|
||||
restoreParRealmSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private void doNormalAuthzProcess(String requestUri, String redirectUrl, String clientId, String clientSecret) {
|
||||
// Authorization Request with request_uri of PAR
|
||||
// remove parameters as query strings of uri
|
||||
|
||||
Reference in New Issue
Block a user