mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
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:
parent
37c4588c7d
commit
987ce19b45
@ -18,6 +18,15 @@ When the "Remember Me" option is disabled in the realm settings, all user sessio
|
||||
Users will be required to log in again, and any associated refresh tokens will no longer be usable.
|
||||
User sessions created without selecting "Remember Me" are not affected.
|
||||
|
||||
=== Correct encoding for OpenID Connect client credentials when acting as a broker
|
||||
|
||||
In a scenario where {project_name} acts as a broker and connects via OpenID Connect to another identity provider, it now sends the client credentials via basic authentication in the correct encoding as specified in RFC6749.
|
||||
|
||||
This prevents problems with client IDs or passwords that contain, for example, a colon or a percentage sign.
|
||||
|
||||
To revert to the old behavior, change the client authentication to *Client secret sent as HTTP Basic authentication without URL encoding (deprecated)* (`client_secret_basic_unencoded`).
|
||||
|
||||
|
||||
// ------------------------ Deprecated features ------------------------ //
|
||||
== Deprecated features
|
||||
|
||||
|
||||
@ -1441,7 +1441,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
|
||||
@ -2774,7 +2774,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
|
||||
|
||||
@ -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",
|
||||
];
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user