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 (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);
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() + "\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue