+
Skip to content

Add option 'Requires short state parameter' to OIDC IDP #41113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: release/26.2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ In the case of JWT signed with private key or Client secret as jwt, it is requir

If the user is unauthenticated in the IDP, the client still receives a `login_required` error. If the user is authentic in the IDP, the client can still receive an `interaction_required` error if {project_name} must display authentication pages that require user interaction. This authentication includes required actions (for example, password change), consent screens, and screens set to display by the `first broker login` flow or `post broker login` flow.

|Requires short state parameter
|This switch needs to be enabled if identity provider does not support long value of the `state` parameter sent in the initial OIDC authentication request (EG. more than 100 characters). In this case, {project_name} will try to make shorter `state` parameter and may omit some client data to be sent in the initial request. This may result in the limited functionality in some very corner case scenarios (EG. in case that IDP redirects to {project_name} with the error in the OIDC authentication response, {project_name} might need to display error page instead of being able to redirect to the client in case that login session is expired).

|Validate Signatures
|Specifies if {project_name} verifies signatures on the external ID Token signed by this IDP. If *ON*, {project_name} must know the public key of the external OIDC IDP. For performance purposes, {project_name} caches the public key of the external OIDC identity provider.

Expand All @@ -82,4 +85,4 @@ If the user is unauthenticated in the IDP, the client still receives a `login_re

You can import all this configuration data by providing a URL or file that points to OpenID Provider Metadata. If you connect to a {project_name} external IDP, you can import the IDP settings from `<root>{kc_realms_path}/{realm-name}/.well-known/openid-configuration`. This link is a JSON document describing metadata about the IDP.

If you want to use https://datatracker.ietf.org/doc/html/rfc7516[Json Web Encryption (JWE)] ID Tokens or UserInfo responses in the provider, the IDP needs to know the public key to use with {project_name}. The provider uses the <<realm_keys, realm keys>> defined for the different encryption algorithms to decrypt the tokens. {project_name} provides a standard xref:con-server-oidc-uri-endpoints_{context}[JWKS endpoint] which the IDP can use for downloading the keys automatically.
If you want to use https://datatracker.ietf.org/doc/html/rfc7516[Json Web Encryption (JWE)] ID Tokens or UserInfo responses in the provider, the IDP needs to know the public key to use with {project_name}. The provider uses the <<realm_keys, realm keys>> defined for the different encryption algorithms to decrypt the tokens. {project_name} provides a standard xref:con-server-oidc-uri-endpoints_{context}[JWKS endpoint] which the IDP can use for downloading the keys automatically.
Original file line number Diff line number Diff line change
Expand Up @@ -2042,6 +2042,7 @@ titleEvents=Events
signServiceProviderMetadata=Sign service provider metadata
updateClientPoliciesError=Could not update client policies\: {{error}}
acceptsPromptNoneHelp=This is used only together with the Identity Provider Authenticator or when kc_idp_hint points to this identity provider. If that client sends a request with prompt\=none and the user is not authenticated, the error is not directly returned to the client; the request with prompt\=none is forwarded to this identity provider.
requiresShortStateParameterHelp=This switch needs to be enabled if identity provider does not support long value of the 'state' parameter sent in the initial OIDC/OAuth2 authentication request (EG. more than 100 characters). In this case, Keycloak will try to make shorter 'state' parameter and may omit some client data to be sent in the initial request. This may result in the limited functionality in some very corner case scenarios (EG. in case that IDP redirects to Keycloak with the error in the OIDC authentication response, Keycloak might need to display error page instead of being able to redirect to the client in case that login session is expired).
roleDetails=Role details
eventTypes.USER_INFO_REQUEST.name=User info request
clientScopeType.none=None
Expand Down Expand Up @@ -2628,6 +2629,7 @@ eventTypes.CLIENT_INITIATED_ACCOUNT_LINKING.description=Client initiated account
annotationsText=Annotations
ldapAttributeName=LDAP attribute name
acceptsPromptNone=Accepts prompt\=none forward from client
requiresShortStateParameter=Requires short state parameter
loginThemeHelp=Select theme for login, OTP, grant, registration and forgot password pages.
AESKeySizeHelp=Size in bytes for the generated AES key. Size 16 is for AES-128, Size 24 for AES-192, and Size 32 for AES-256. WARN\: Bigger keys than 128 are not allowed on some JDK implementations.
client-accesstype.tooltip=Access Type of the client, for which the condition will be applied. Confidential client has enabled client authentication when public client has disabled client authentication. Bearer-only is a deprecated client type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export const ExtendedNonDiscoverySettings = () => {
field="config.acceptsPromptNoneForwardFromClient"
label="acceptsPromptNone"
/>
<SwitchField
field="config.requiresShortStateParameter"
label="requiresShortStateParameter"
/>
<FormGroup
label={t("allowedClockSkew")}
labelIcon={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -749,5 +749,8 @@ public void exchangeExternalComplete(UserSessionModel userSession, BrokeredIdent

}


@Override
public boolean supportsLongStateParameter() {
return !getConfig().isRequiresShortStateParameter();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {

public static final String JWT_X509_HEADERS_ENABLED = "jwtX509HeadersEnabled";

public static final String REQUIRES_SHORT_STATE_PARAMETER = "requiresShortStateParameter";

public OAuth2IdentityProviderConfig(IdentityProviderModel model) {
super(model);
}
Expand Down Expand Up @@ -125,6 +127,14 @@ public String getPrompt() {
return getConfig().get("prompt");
}

public boolean isRequiresShortStateParameter() {
return Boolean.parseBoolean(getConfig().get(REQUIRES_SHORT_STATE_PARAMETER));
}

public void setRequiresShortStateParameter(boolean requiresShortStateParameter) {
getConfig().put(REQUIRES_SHORT_STATE_PARAMETER, String.valueOf(requiresShortStateParameter));
}

public String getForwardParameters() {
return getConfig().get("forwardParameters");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.Constants;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.BrowserTabUtil;
Expand All @@ -41,6 +44,8 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assume.assumeTrue;
import static org.keycloak.testsuite.AssertEvents.DEFAULT_REDIRECT_URI;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_CONS_NAME;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;

/**
Expand Down Expand Up @@ -169,6 +174,87 @@ public void testAuthenticationExpiredWithMoreBrowserTabs_loginExpiredInBothConsu
}
}

// Same like testAuthenticationExpiredWithMoreBrowserTabs_loginExpiredInBothConsumerAndProvider, but OIDC IDP supports only short "state" parameter. Hence client_data cannot be encoded into it
// So this is similar behaviour like KcSamlMultipleTabsBrokerTest.testAuthenticationExpiredWithMoreBrowserTabs_loginExpiredInBothConsumerAndProvider
@Test
public void testAuthenticationExpiredWithMoreBrowserTabs_loginExpiredInBothConsumerAndProvider_requiresShortStateParameter() {
assumeTrue("Since the JS engine in real browser does check the expiration regularly in all tabs, this test only works with HtmlUnit", driver instanceof HtmlUnitDriver);

// Update IDP and set invalid credentials there
IdentityProviderResource idpResource = adminClient.realm(REALM_CONS_NAME).identityProviders().get(IDP_OIDC_ALIAS);
IdentityProviderRepresentation idpRep = idpResource.toRepresentation();
idpRep.getConfig().put(OAuth2IdentityProviderConfig.REQUIRES_SHORT_STATE_PARAMETER, "true");
idpResource.update(idpRep);


try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
// Open login page in tab1 and click "login with IDP"
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
loginPage.clickSocial(bc.getIDPAlias());

// Open login page in tab 2
tabUtil.newTab(oauth.loginForm().build());
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2));
Assert.assertTrue(loginPage.isCurrent("consumer"));
getLogger().infof("URL in tab2: %s", driver.getCurrentUrl());

setTimeOffset(7200000);

// Finish login in tab2
loginPage.clickSocial(bc.getIDPAlias());
Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning.");
logInWithBroker(bc);

waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
Assert.assertTrue("We must be on consumer realm right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), bc.getUserEmail(), "Firstname", "Lastname");
appPage.assertCurrent();
events.clear();

// Login in provider realm will redirect back to consumer with "authentication_expired" error.
// The consumer has also expired authentication session, but it cannot redirect to client due the "clientData" missing from IdentityBrokerState.
// That is the difference from testAuthenticationExpiredWithMoreBrowserTabs_loginExpiredInBothConsumerAndProvider, which is able to redirect back to client
tabUtil.closeTab(1);
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(1));
loginPage.login(bc.getUserLogin(), bc.getUserPassword());

// Event for "already logged-in" in the provider realm
events.expectLogin().error(Errors.ALREADY_LOGGED_IN)
.realm(getProviderRealmId())
.client("brokerapp")
.user((String) null)
.session((String) null)
.removeDetail(Details.CONSENT)
.removeDetail(Details.CODE_ID)
.detail(Details.REDIRECT_URI, Matchers.equalTo(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + bc.consumerRealmName() + "/broker/" + bc.getIDPAlias() + "/endpoint"))
.detail(Details.REDIRECTED_TO_CLIENT, "true")
.detail(Details.RESPONSE_TYPE, OIDCResponseType.CODE)
.detail(Details.RESPONSE_MODE, OIDCResponseMode.QUERY.value())
.assertEvent();

// Event for "already logged-in" in the consumer realm
events.expect(EventType.IDENTITY_PROVIDER_LOGIN).error(Errors.ALREADY_LOGGED_IN)
.realm(getConsumerRealmId())
.client("broker-app")
.user((String) null)
.session((String) null)
.removeDetail(Details.REDIRECT_URI)
.detail(Details.REDIRECTED_TO_CLIENT, "false")
.assertEvent();

// Being on "You are already logged-in" now. No way to redirect to client due "clientData" are null in "state" of OIDC IDP as OIDC IDP requires short state parameter
loginPage.assertCurrent("consumer");
Assert.assertEquals("You are already logged in.", loginPage.getInstruction());
} finally {
// Revert config
idpRep.getConfig().put(OAuth2IdentityProviderConfig.REQUIRES_SHORT_STATE_PARAMETER, "false");
idpResource.update(idpRep);
}
}

@Test
public void testAuthenticationExpiredWithMoreBrowserTabs_loginExpiredInProvider() throws Exception {
assumeTrue("Since the JS engine in real browser does check the expiration regularly in all tabs, this test only works with HtmlUnit", driver instanceof HtmlUnitDriver);
Expand Down Expand Up @@ -222,7 +308,7 @@ public void testAuthenticationExpiredWithMoreBrowserTabs_loginExpiredInProvider(
.detail(Details.RESPONSE_MODE, OIDCResponseMode.QUERY.value())
.assertEvent();

// SAML IDP on "consumer" will retry IDP login on the "provider"
// OIDC IDP on "consumer" will retry IDP login on the "provider"
events.expect(EventType.IDENTITY_PROVIDER_LOGIN)
.realm(getConsumerRealmId())
.client("broker-app")
Expand Down
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载