Use base64 url decoded for client secret when authenticating with Basic Auth (#12486)
Closes #11908
This commit is contained in:
parent
8ce10df6da
commit
f0988a62b8
6 changed files with 78 additions and 66 deletions
|
@ -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);
|
||||||
|
|
|
@ -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};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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<>();
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue