Fix OIDC IDP broker basic auth encoding

Ensures that the client_id and client_secret are URL-encoded before being Base64-encoded for the Basic Auth header, following RFC 6749. This fixes authentication failures when the client_id contains special characters.

Closes #26374
Closes #43022

Signed-off-by: rpjicond <ronaldopaulino32@hotmail.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Co-authored-by: rpjicond <ronaldopaulino32@hotmail.com>
Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Co-authored-by: cgeorgilakis-grnet <cgeorgilakis@admin.grnet.gr>
This commit is contained in:
Ronaldo Paulino Jiconda 2025-10-20 23:48:24 +02:00 committed by Pedro Igor
parent a321c2c91f
commit 489d10157a
6 changed files with 97 additions and 2 deletions

View File

@ -1436,7 +1436,7 @@ removeMappingConfirm_one=Are you sure you want to remove this role?
oidcSettings=OpenID Connect settings
oAuthSettings=OAuth2 settings
otpPolicyDigitsHelp=How many digits should the OTP have?
clientAuthentications.client_secret_post=Client secret sent as post
clientAuthentications.client_secret_post=Client secret sent in the request body
prompts.select_account=Select account
defaultACRValues=Default ACR Values
minimumACRValue=Minimum ACR Value
@ -2769,7 +2769,8 @@ authenticationFlow=Authentication flow
leaveGroup_other=Leave groups?
deleteClientPolicySuccess=Client policy deleted
mapperTypeCertificateLdapMapper=certificate-ldap-mapper
clientAuthentications.client_secret_basic=Client secret sent as basic auth
clientAuthentications.client_secret_basic=Client secret sent as HTTP Basic authentication
clientAuthentications.client_secret_basic_unencoded=Client secret sent as HTTP Basic authentication without URL encoding (deprecated)
started=Started
filteredByClaimHelp=If true, ID tokens issued by the identity provider must have a specific claim. Otherwise, the user can not authenticate through this broker.
mapperTypeCertificateLdapMapperHelp=Used to map single attribute which contains a certificate from LDAP user to attribute of UserModel in Keycloak database

View File

@ -10,6 +10,7 @@ import { TextField } from "../component/TextField";
const clientAuthentications = [
"client_secret_post",
"client_secret_basic",
"client_secret_basic_unencoded",
"client_secret_jwt",
"private_key_jwt",
];

View File

@ -622,6 +622,11 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
} else {
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
if (getConfig().isBasicAuthentication()) {
String clientSecret = vaultStringSecret.get().orElse(getConfig().getClientSecret());
String header = org.keycloak.util.BasicAuthHelper.RFC6749.createHeader(getConfig().getClientId(), clientSecret);
return tokenRequest.header(HttpHeaders.AUTHORIZATION, header);
}
if (getConfig().isBasicAuthenticationUnencoded()) {
return tokenRequest.authBasic(getConfig().getClientId(), vaultStringSecret.get().orElse(getConfig().getClientSecret()));
}
return tokenRequest

View File

@ -125,6 +125,10 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {
return getClientAuthMethod().equals(OIDCLoginProtocol.CLIENT_SECRET_BASIC);
}
public boolean isBasicAuthenticationUnencoded(){
return getClientAuthMethod().equals(OIDCLoginProtocol.CLIENT_SECRET_BASIC_UNENCODED);
}
public boolean isUiLocales() {
return Boolean.valueOf(getConfig().get("uiLocales"));
}

View File

@ -119,6 +119,12 @@ public class OIDCLoginProtocol implements LoginProtocol {
public static final String PRIVATE_KEY_JWT = "private_key_jwt";
public static final String TLS_CLIENT_AUTH = "tls_client_auth";
/**
* This is just for legacy setups which expect an unencoded, non-RFC6749 compliant client secret send from Keycloak to an IdP.
*/
@Deprecated(since = "26.5")
public static final String CLIENT_SECRET_BASIC_UNENCODED = "client_secret_basic_unencoded";
// https://tools.ietf.org/html/rfc7636#section-4.3
public static final String CODE_CHALLENGE_PARAM = "code_challenge";
public static final String CODE_CHALLENGE_METHOD_PARAM = "code_challenge_method";

View File

@ -0,0 +1,78 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.broker;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import java.util.Map;
import static org.keycloak.broker.oidc.OAuth2IdentityProviderConfig.TOKEN_ENDPOINT_URL;
import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
import static org.keycloak.testsuite.broker.BrokerTestTools.*;
public class KcOidcBrokerColonAliasClientSecretBasicAuthTest extends AbstractBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerColonAliasClientSecretBasicAuthTest.KcOidcBrokerColonAliasConfigurationWithBasicAuthAuthentication();
}
private class KcOidcBrokerColonAliasConfigurationWithBasicAuthAuthentication extends KcOidcBrokerConfiguration {
public final static String CLIENT_ID_COLON = "https://kc-dev.general.gr/staging/realms/general";
@Override
public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) {
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
Map<String, String> config = idp.getConfig();
applyDefaultConfiguration(config, syncMode);
config.put("clientAuthMethod", OIDCLoginProtocol.CLIENT_SECRET_BASIC);
return idp;
}
@Override
protected void applyDefaultConfiguration(final Map<String, String> config, IdentityProviderSyncMode syncMode) {
config.put(IdentityProviderModel.SYNC_MODE, syncMode.toString());
config.put("clientId", CLIENT_ID_COLON);
config.put("clientSecret", CLIENT_SECRET);
config.put("prompt", "login");
config.put("loginHint", "true");
config.put(OIDCIdentityProviderConfig.ISSUER, getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME);
config.put("authorizationUrl", getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/auth");
config.put(TOKEN_ENDPOINT_URL, getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/token");
config.put("logoutUrl", getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/logout");
config.put("userInfoUrl", getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/userinfo");
config.put("defaultScope", "email profile");
config.put("backchannelSupported", "true");
config.put(OIDCIdentityProviderConfig.JWKS_URL,
getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/certs");
config.put(OIDCIdentityProviderConfig.USE_JWKS_URL, "true");
config.put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, "true");
}
@Override
public String getIDPClientIdInProviderRealm() {
return CLIENT_ID_COLON;
}
}
}