Skip to content

Commit 489d101

Browse files
rpjicond1oronyahus1cgeorgilakis-grnet
authored andcommitted
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 <[email protected]> Signed-off-by: Alexander Schwartz <[email protected]> Co-authored-by: rpjicond <[email protected]> Co-authored-by: Alexander Schwartz <[email protected]> Co-authored-by: cgeorgilakis-grnet <[email protected]>
1 parent a321c2c commit 489d101

File tree

6 files changed

+97
-2
lines changed

6 files changed

+97
-2
lines changed

js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties

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

js/apps/admin-ui/src/identity-providers/add/OIDCAuthentication.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TextField } from "../component/TextField";
1010
const clientAuthentications = [
1111
"client_secret_post",
1212
"client_secret_basic",
13+
"client_secret_basic_unencoded",
1314
"client_secret_jwt",
1415
"private_key_jwt",
1516
];

services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,11 @@ public SimpleHttpRequest authenticateTokenRequest(final SimpleHttpRequest tokenR
622622
} else {
623623
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
624624
if (getConfig().isBasicAuthentication()) {
625+
String clientSecret = vaultStringSecret.get().orElse(getConfig().getClientSecret());
626+
String header = org.keycloak.util.BasicAuthHelper.RFC6749.createHeader(getConfig().getClientId(), clientSecret);
627+
return tokenRequest.header(HttpHeaders.AUTHORIZATION, header);
628+
}
629+
if (getConfig().isBasicAuthenticationUnencoded()) {
625630
return tokenRequest.authBasic(getConfig().getClientId(), vaultStringSecret.get().orElse(getConfig().getClientSecret()));
626631
}
627632
return tokenRequest

services/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ public boolean isBasicAuthentication(){
125125
return getClientAuthMethod().equals(OIDCLoginProtocol.CLIENT_SECRET_BASIC);
126126
}
127127

128+
public boolean isBasicAuthenticationUnencoded(){
129+
return getClientAuthMethod().equals(OIDCLoginProtocol.CLIENT_SECRET_BASIC_UNENCODED);
130+
}
131+
128132
public boolean isUiLocales() {
129133
return Boolean.valueOf(getConfig().get("uiLocales"));
130134
}

services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ public class OIDCLoginProtocol implements LoginProtocol {
119119
public static final String PRIVATE_KEY_JWT = "private_key_jwt";
120120
public static final String TLS_CLIENT_AUTH = "tls_client_auth";
121121

122+
/**
123+
* This is just for legacy setups which expect an unencoded, non-RFC6749 compliant client secret send from Keycloak to an IdP.
124+
*/
125+
@Deprecated(since = "26.5")
126+
public static final String CLIENT_SECRET_BASIC_UNENCODED = "client_secret_basic_unencoded";
127+
122128
// https://tools.ietf.org/html/rfc7636#section-4.3
123129
public static final String CODE_CHALLENGE_PARAM = "code_challenge";
124130
public static final String CODE_CHALLENGE_METHOD_PARAM = "code_challenge_method";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2025 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.keycloak.testsuite.broker;
19+
20+
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
21+
import org.keycloak.models.IdentityProviderModel;
22+
import org.keycloak.models.IdentityProviderSyncMode;
23+
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
24+
import org.keycloak.representations.idm.IdentityProviderRepresentation;
25+
26+
import java.util.Map;
27+
28+
import static org.keycloak.broker.oidc.OAuth2IdentityProviderConfig.TOKEN_ENDPOINT_URL;
29+
import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
30+
import static org.keycloak.testsuite.broker.BrokerTestTools.*;
31+
32+
public class KcOidcBrokerColonAliasClientSecretBasicAuthTest extends AbstractBrokerTest {
33+
34+
@Override
35+
protected BrokerConfiguration getBrokerConfiguration() {
36+
return new KcOidcBrokerColonAliasClientSecretBasicAuthTest.KcOidcBrokerColonAliasConfigurationWithBasicAuthAuthentication();
37+
}
38+
39+
private class KcOidcBrokerColonAliasConfigurationWithBasicAuthAuthentication extends KcOidcBrokerConfiguration {
40+
41+
public final static String CLIENT_ID_COLON = "https://kc-dev.general.gr/staging/realms/general";
42+
43+
@Override
44+
public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) {
45+
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
46+
Map<String, String> config = idp.getConfig();
47+
applyDefaultConfiguration(config, syncMode);
48+
config.put("clientAuthMethod", OIDCLoginProtocol.CLIENT_SECRET_BASIC);
49+
return idp;
50+
}
51+
52+
@Override
53+
protected void applyDefaultConfiguration(final Map<String, String> config, IdentityProviderSyncMode syncMode) {
54+
config.put(IdentityProviderModel.SYNC_MODE, syncMode.toString());
55+
config.put("clientId", CLIENT_ID_COLON);
56+
config.put("clientSecret", CLIENT_SECRET);
57+
config.put("prompt", "login");
58+
config.put("loginHint", "true");
59+
config.put(OIDCIdentityProviderConfig.ISSUER, getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME);
60+
config.put("authorizationUrl", getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/auth");
61+
config.put(TOKEN_ENDPOINT_URL, getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/token");
62+
config.put("logoutUrl", getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/logout");
63+
config.put("userInfoUrl", getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/userinfo");
64+
config.put("defaultScope", "email profile");
65+
config.put("backchannelSupported", "true");
66+
config.put(OIDCIdentityProviderConfig.JWKS_URL,
67+
getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/certs");
68+
config.put(OIDCIdentityProviderConfig.USE_JWKS_URL, "true");
69+
config.put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, "true");
70+
}
71+
72+
@Override
73+
public String getIDPClientIdInProviderRealm() {
74+
return CLIENT_ID_COLON;
75+
}
76+
77+
}
78+
}

0 commit comments

Comments
 (0)