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 (clientSecret != null) {
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
String authorization = BasicAuthHelper.UrlEncoded.createHeader(clientId, clientSecret);
requestHeaders.put("Authorization", authorization);
} else {
logger.warnf("Client '%s' doesn't have secret available", clientId);

View file

@ -18,46 +18,40 @@
package org.keycloak.util;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Base64Url;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BasicAuthHelper
{
public static String createHeader(String username, String password)
{
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 class BasicAuthHelper {
public static String createHeader(String username, String password) {
return "Basic " + Base64.encodeBytes((username + ':' + password).getBytes(StandardCharsets.UTF_8));
}
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 = header.substring(6);
try {
val = new String(Base64.decode(val.getBytes()));
} catch (IOException e) {
throw new RuntimeException(e);
// https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
// The client identifier is encoded using the
// "application/x-www-form-urlencoded" encoding algorithm per
// Appendix B, and the encoded value is used as the username; the client
// password is encoded using the same algorithm and used as the password;
public static abstract class UrlEncoded {
public static String createHeader(String username, String password) {
return "Basic " + Base64Url.encode((username + ':' + password).getBytes(StandardCharsets.UTF_8));
}
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) {
String[] challenge = BasicAuthHelper.parseHeader(authorizationHeader);
String[] challenge = BasicAuthHelper.UrlEncoded.parseHeader(authorizationHeader);
if (challenge.length < 2) {
if (challenge == null || challenge.length < 2) {
return null;
}
@ -146,4 +146,4 @@ public class BasicAuthAuthenticator extends AbstractUsernameFormAuthenticator im
private String getHeader(AuthenticationFlowContext context) {
return "Basic realm=\"" + context.getRealm().getName() + "\"";
}
}
}

View file

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

View file

@ -17,16 +17,7 @@
package org.keycloak.testsuite.util;
import static org.keycloak.protocol.oidc.OIDCLoginProtocol.LOGIN_HINT_PARAM;
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 com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.http.Header;
@ -64,11 +55,11 @@ import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.Constants;
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.channel.AuthenticationChannelResponse;
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.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
@ -88,8 +79,9 @@ import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
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.UnsupportedEncodingException;
import java.net.URI;
@ -108,9 +100,15 @@ import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.UriBuilder;
import static org.keycloak.protocol.oidc.OIDCLoginProtocol.LOGIN_HINT_PARAM;
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;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -747,7 +745,7 @@ public class OAuthClient {
try (CloseableHttpClient client = httpClient.get()) {
HttpPost post = new HttpPost(getServiceAccountUrl());
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
String authorization = BasicAuthHelper.UrlEncoded.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization);
List<NameValuePair> parameters = new LinkedList<>();
@ -766,7 +764,7 @@ public class OAuthClient {
post.setEntity(formEntity);
return new AccessTokenResponse(client.execute(post));
}
}
}
public AuthenticationRequestAcknowledgement doBackchannelAuthenticationRequest(String clientId, String clientSecret, String userid, String bindingMessage, String acrValues) throws Exception {
@ -1409,10 +1407,10 @@ public class OAuthClient {
public String getRedirectUri() {
return redirectUri;
}
/**
* Application-initiated action.
*
*
* @return The action name.
*/
public String getKcAction() {
@ -1520,7 +1518,7 @@ public class OAuthClient {
.param(OAuth2Constants.CLIENT_ID, clientId)
.param(OAuth2Constants.REDIRECT_URI, redirectUri)
.param(OAuth2Constants.STATE, this.state.getState());
return Entity.form(form);
}
@ -1635,7 +1633,7 @@ public class OAuthClient {
this.initiatingIDP = initiatingIDP;
return this;
}
public OAuthClient kcAction(String kcAction) {
this.kcAction = kcAction;
return this;
@ -2156,7 +2154,7 @@ public class OAuthClient {
String id = "social-" + alias;
return DroneUtils.getCurrentDriver().findElement(By.id(id));
}
private interface StateParamProvider {
String getState();

View file

@ -56,6 +56,8 @@ import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.TokenSignatureUtil;
import org.keycloak.testsuite.util.UserBuilder;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Arrays;
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.assertThat;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -126,6 +125,15 @@ public class ServiceAccountTest extends AbstractKeycloakTest {
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()
.id(KeycloakModelUtils.generateId())
.username("test-user@localhost");
@ -333,13 +341,13 @@ public class ServiceAccountTest extends AbstractKeycloakTest {
representation.setCredentials(Arrays.asList(password));
this.expectedException.expect(Matchers.allOf(Matchers.instanceOf(ClientErrorException.class),
this.expectedException.expect(Matchers.allOf(Matchers.instanceOf(ClientErrorException.class),
Matchers.hasProperty("response", Matchers.hasProperty("status", Matchers.is(400)))));
this.expectedException.reportMissingExceptionWithMessage("Should fail, should not be possible to manage credentials for service accounts");
serviceAccount.update(representation);
}
/**
* See KEYCLOAK-9551
*/
@ -513,4 +521,16 @@ public class ServiceAccountTest extends AbstractKeycloakTest {
HttpResponse logoutResponse = oauth.doLogout(response.getRefreshToken(), "secret1");
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());
}
}