Use base64 url decoded for client secret when authenticating with Basic Auth (#12486)

Closes #11908
This commit is contained in:
Lex Cao 2022-07-16 15:38:41 +08:00 committed by GitHub
parent 8ce10df6da
commit f0988a62b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 78 additions and 66 deletions

View file

@ -54,7 +54,7 @@ public class ClientIdAndSecretCredentialsProvider implements ClientCredentialsPr
if (!deployment.isPublicClient()) { if (!deployment.isPublicClient()) {
if (clientSecret != null) { if (clientSecret != null) {
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret); String authorization = BasicAuthHelper.UrlEncoded.createHeader(clientId, clientSecret);
requestHeaders.put("Authorization", authorization); requestHeaders.put("Authorization", authorization);
} else { } else {
logger.warnf("Client '%s' doesn't have secret available", clientId); logger.warnf("Client '%s' doesn't have secret available", clientId);

View file

@ -18,46 +18,40 @@
package org.keycloak.util; package org.keycloak.util;
import org.keycloak.common.util.Base64; import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Base64Url;
import java.io.IOException; import java.nio.charset.StandardCharsets;
import java.io.UnsupportedEncodingException;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class BasicAuthHelper public class BasicAuthHelper {
{ public static String createHeader(String username, String password) {
public static String createHeader(String username, String password) return "Basic " + Base64.encodeBytes((username + ':' + password).getBytes(StandardCharsets.UTF_8));
{
StringBuffer buf = new StringBuffer(username);
buf.append(':').append(password);
try
{
return "Basic " + Base64.encodeBytes(buf.toString().getBytes("UTF-8"));
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException(e);
}
} }
public static String[] parseHeader(String header) // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
{ // The client identifier is encoded using the
if (header.length() < 6) return null; // "application/x-www-form-urlencoded" encoding algorithm per
String type = header.substring(0, 5); // Appendix B, and the encoded value is used as the username; the client
type = type.toLowerCase(); // password is encoded using the same algorithm and used as the password;
if (!type.equalsIgnoreCase("Basic")) return null; public static abstract class UrlEncoded {
String val = header.substring(6); public static String createHeader(String username, String password) {
try { return "Basic " + Base64Url.encode((username + ':' + password).getBytes(StandardCharsets.UTF_8));
val = new String(Base64.decode(val.getBytes())); }
} catch (IOException e) {
throw new RuntimeException(e); public static String[] parseHeader(String header) {
if (header.length() < 6) return null;
String type = header.substring(0, 5);
type = type.toLowerCase();
if (!type.equalsIgnoreCase("Basic")) return null;
String val = new String(Base64Url.decode(header.substring(6)));
int seperatorIndex = val.indexOf(":");
if (seperatorIndex == -1) return null;
String user = val.substring(0, seperatorIndex);
String pw = val.substring(seperatorIndex + 1);
return new String[]{ user, pw };
} }
int seperatorIndex = val.indexOf(":");
if(seperatorIndex == -1) return null;
String user = val.substring(0, seperatorIndex);
String pw = val.substring(seperatorIndex + 1);
return new String[]{user,pw};
} }
} }

View file

@ -100,9 +100,9 @@ public class BasicAuthAuthenticator extends AbstractUsernameFormAuthenticator im
} }
protected String[] getChallenge(String authorizationHeader) { protected String[] getChallenge(String authorizationHeader) {
String[] challenge = BasicAuthHelper.parseHeader(authorizationHeader); String[] challenge = BasicAuthHelper.UrlEncoded.parseHeader(authorizationHeader);
if (challenge.length < 2) { if (challenge == null || challenge.length < 2) {
return null; return null;
} }

View file

@ -64,7 +64,7 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
MultivaluedMap<String, String> formData = hasFormData ? context.getHttpRequest().getDecodedFormParameters() : null; MultivaluedMap<String, String> formData = hasFormData ? context.getHttpRequest().getDecodedFormParameters() : null;
if (authorizationHeader != null) { if (authorizationHeader != null) {
String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader); String[] usernameSecret = BasicAuthHelper.UrlEncoded.parseHeader(authorizationHeader);
if (usernameSecret != null) { if (usernameSecret != null) {
client_id = usernameSecret[0]; client_id = usernameSecret[0];
clientSecret = usernameSecret[1]; clientSecret = usernameSecret[1];

View file

@ -17,16 +17,7 @@
package org.keycloak.testsuite.util; package org.keycloak.testsuite.util;
import static org.keycloak.protocol.oidc.OIDCLoginProtocol.LOGIN_HINT_PARAM; import com.google.common.base.Charsets;
import static org.keycloak.protocol.oidc.grants.ciba.CibaGrantType.AUTH_REQ_ID;
import static org.keycloak.protocol.oidc.grants.ciba.CibaGrantType.BINDING_MESSAGE;
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.oauth2DeviceAuthUrl;
import static org.keycloak.testsuite.admin.Users.getPasswordOf;
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
import static org.keycloak.testsuite.util.ServerURLs.removeDefaultPorts;
import static org.keycloak.testsuite.util.UIUtils.clickLink;
import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.http.Header; import org.apache.http.Header;
@ -64,11 +55,11 @@ import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType; import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType;
import org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse; import org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse;
import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint; import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -88,8 +79,9 @@ import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
import com.google.common.base.Charsets; import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
@ -108,9 +100,15 @@ import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import javax.ws.rs.client.Entity; import static org.keycloak.protocol.oidc.OIDCLoginProtocol.LOGIN_HINT_PARAM;
import javax.ws.rs.core.Form; import static org.keycloak.protocol.oidc.grants.ciba.CibaGrantType.AUTH_REQ_ID;
import javax.ws.rs.core.UriBuilder; import static org.keycloak.protocol.oidc.grants.ciba.CibaGrantType.BINDING_MESSAGE;
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.oauth2DeviceAuthUrl;
import static org.keycloak.testsuite.admin.Users.getPasswordOf;
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
import static org.keycloak.testsuite.util.ServerURLs.removeDefaultPorts;
import static org.keycloak.testsuite.util.UIUtils.clickLink;
import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -747,7 +745,7 @@ public class OAuthClient {
try (CloseableHttpClient client = httpClient.get()) { try (CloseableHttpClient client = httpClient.get()) {
HttpPost post = new HttpPost(getServiceAccountUrl()); HttpPost post = new HttpPost(getServiceAccountUrl());
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret); String authorization = BasicAuthHelper.UrlEncoded.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization); post.setHeader("Authorization", authorization);
List<NameValuePair> parameters = new LinkedList<>(); List<NameValuePair> parameters = new LinkedList<>();

View file

@ -56,6 +56,8 @@ import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.TokenSignatureUtil; import org.keycloak.testsuite.util.TokenSignatureUtil;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -68,9 +70,6 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
@ -126,6 +125,15 @@ public class ServiceAccountTest extends AbstractKeycloakTest {
realm.client(disabledApp); realm.client(disabledApp);
ClientRepresentation secretsWithSpecialCharacterClient = ClientBuilder.create()
.id(KeycloakModelUtils.generateId())
.clientId("service-account-cl-special-secrets")
.secret("secret/with=special?character")
.serviceAccountsEnabled(true)
.build();
realm.client(secretsWithSpecialCharacterClient);
UserBuilder defaultUser = UserBuilder.create() UserBuilder defaultUser = UserBuilder.create()
.id(KeycloakModelUtils.generateId()) .id(KeycloakModelUtils.generateId())
.username("test-user@localhost"); .username("test-user@localhost");
@ -513,4 +521,16 @@ public class ServiceAccountTest extends AbstractKeycloakTest {
HttpResponse logoutResponse = oauth.doLogout(response.getRefreshToken(), "secret1"); HttpResponse logoutResponse = oauth.doLogout(response.getRefreshToken(), "secret1");
assertEquals(204, logoutResponse.getStatusLine().getStatusCode()); assertEquals(204, logoutResponse.getStatusLine().getStatusCode());
} }
/**
* See KEYCLOAK-18704
*/
@Test
public void clientCredentialsAuthSuccessWithUrlEncodedSpecialCharactersSecret() throws Exception {
oauth.clientId("service-account-cl-special-secrets");
OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret/with=special?character");
assertEquals(200, response.getStatusCode());
}
} }