Add basic auth compliant to RFC 6749 (#14179)

Closes #14179
This commit is contained in:
Lex Cao 2022-09-07 16:09:30 +08:00 committed by GitHub
parent fc93ab1d54
commit 1f197aa96b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 116 additions and 23 deletions

View file

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

View file

@ -18,40 +18,81 @@
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.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* The default implementation is compliant with <a href="https://datatracker.ietf.org/doc/html/rfc2617">RFC 2617</a>
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BasicAuthHelper {
public static String createHeader(String username, String password) {
return "Basic " + Base64.encodeBytes((username + ':' + password).getBytes(StandardCharsets.UTF_8));
try {
return "Basic " + Base64.encodeBytes((username + ':' + password).getBytes(StandardCharsets.UTF_8), Base64.DO_BREAK_LINES);
} catch (IOException e) {
return null;
}
}
// 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[] 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;
try {
val = new String(Base64.decode(header.substring(6), Base64.DO_BREAK_LINES));
} catch (IOException e) {
return null;
}
int separatorIndex = val.indexOf(":");
if (separatorIndex == -1) return null;
String username = val.substring(0, separatorIndex);
String password = val.substring(separatorIndex + 1);
return new String[]{ username, password };
}
/**
* compliant with <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1">RFC 6749</a>
*/
public static abstract class RFC6749 {
public static String createHeader(String username, String password) {
return "Basic " + Base64Url.encode((username + ':' + password).getBytes(StandardCharsets.UTF_8));
try {
return BasicAuthHelper.createHeader(
URLEncoder.encode(username, "UTF-8"),
URLEncoder.encode(password, "UTF-8")
);
} catch (UnsupportedEncodingException e) {
return null;
}
}
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 };
String[] val = BasicAuthHelper.parseHeader(header);
if (null == val) {
return null;
}
try {
return new String[]{
URLDecoder.decode(val[0], "UTF-8"),
URLDecoder.decode(val[1], "UTF-8")
};
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
}

View file

@ -0,0 +1,52 @@
package org.keycloak.util;
import org.junit.Test;
import static org.junit.Assert.*;
public class BasicAuthHelperTest {
@Test
public void createHeader() {
String username = "Aladdin";
String password = "open sesame";
String actual = BasicAuthHelper.createHeader(username, password);
String expect = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==";
assertEquals(expect, actual);
}
@Test
public void parseHeader() {
String username = "Aladdin";
String password = "open sesame";
String header = BasicAuthHelper.createHeader(username, password);
String[] actual = BasicAuthHelper.parseHeader(header);
assertArrayEquals(new String[] {username, password}, actual);
}
@Test
public void rfc6749_createHeader() {
String username = "user";
String password = "secret/with=special?character";
String actual = BasicAuthHelper.RFC6749.createHeader(username, password);
String expect = "Basic dXNlcjpzZWNyZXQlMkZ3aXRoJTNEc3BlY2lhbCUzRmNoYXJhY3Rlcg==";
assertEquals(expect, actual);
}
@Test
public void rfc6749_parseHeader() {
String username = "user";
String password = "secret/with=special?character";
String header = BasicAuthHelper.createHeader(username, password);
String[] actual = BasicAuthHelper.parseHeader(header);
assertArrayEquals(new String[] {username, password}, actual);
}
}

View file

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

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.UrlEncoded.parseHeader(authorizationHeader);
String[] usernameSecret = BasicAuthHelper.RFC6749.parseHeader(authorizationHeader);
if (usernameSecret != null) {
client_id = usernameSecret[0];
clientSecret = usernameSecret[1];

View file

@ -745,7 +745,7 @@ public class OAuthClient {
try (CloseableHttpClient client = httpClient.get()) {
HttpPost post = new HttpPost(getServiceAccountUrl());
String authorization = BasicAuthHelper.UrlEncoded.createHeader(clientId, clientSecret);
String authorization = BasicAuthHelper.RFC6749.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization);
List<NameValuePair> parameters = new LinkedList<>();