mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-21 06:20:05 -06:00
Incorrect scheme in the WWW-Authenticate when Authorization: DPoP used
closes #42706 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
@@ -168,6 +168,7 @@ public interface OAuth2Constants {
|
||||
String DPOP_NONCE_HEADER = "DPoP-Nonce";
|
||||
Algorithm DPOP_DEFAULT_ALGORITHM = PS256;
|
||||
String DPOP_JWT_HEADER_TYPE = "dpop+jwt";
|
||||
String ALGS_ATTRIBUTE = "algs";
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,9 @@ public class UserInfoEndpoint {
|
||||
this.realm = session.getContext().getRealm();
|
||||
this.tokenManager = tokenManager;
|
||||
this.appAuthManager = new AppAuthManager();
|
||||
this.error = new OAuth2Error().json(false).realm(realm);
|
||||
this.error = new OAuth2Error().json(false)
|
||||
.session(session)
|
||||
.realm(realm);
|
||||
this.request = session.getContext().getHttpRequest();
|
||||
}
|
||||
|
||||
@@ -119,7 +121,7 @@ public class UserInfoEndpoint {
|
||||
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_JWT})
|
||||
public Response issueUserInfoGet() {
|
||||
setupCors();
|
||||
String accessToken = this.appAuthManager.extractAuthorizationHeaderTokenOrReturnNull(session.getContext().getRequestHeaders());
|
||||
AppAuthManager.AuthHeader accessToken = AppAuthManager.extractAuthorizationHeaderTokenOrReturnNull(session.getContext().getRequestHeaders());
|
||||
authorization(accessToken);
|
||||
return issueUserInfo();
|
||||
}
|
||||
@@ -133,8 +135,8 @@ public class UserInfoEndpoint {
|
||||
|
||||
// Try header first
|
||||
HttpHeaders headers = request.getHttpHeaders();
|
||||
String accessToken = this.appAuthManager.extractAuthorizationHeaderTokenOrReturnNull(headers);
|
||||
authorization(accessToken);
|
||||
AppAuthManager.AuthHeader authHeader = AppAuthManager.extractAuthorizationHeaderTokenOrReturnNull(headers);
|
||||
authorization(authHeader);
|
||||
|
||||
try {
|
||||
|
||||
@@ -144,8 +146,9 @@ public class UserInfoEndpoint {
|
||||
if (jakarta.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED_TYPE.isCompatible(mediaType)) {
|
||||
MultivaluedMap<String, String> formParams = request.getDecodedFormParameters();
|
||||
checkAccessTokenDuplicated(formParams);
|
||||
accessToken = formParams.getFirst(OAuth2Constants.ACCESS_TOKEN);
|
||||
authorization(accessToken);
|
||||
String accessToken = formParams.getFirst(OAuth2Constants.ACCESS_TOKEN);
|
||||
authHeader = accessToken == null ? null : new AppAuthManager.AuthHeader(AppAuthManager.BEARER, accessToken);
|
||||
authorization(authHeader);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// not application/x-www-form-urlencoded, ignore
|
||||
@@ -364,10 +367,11 @@ public class UserInfoEndpoint {
|
||||
error.cors(cors);
|
||||
}
|
||||
|
||||
private void authorization(String accessToken) {
|
||||
if (accessToken != null) {
|
||||
private void authorization(AppAuthManager.AuthHeader authHeader) {
|
||||
if (authHeader != null) {
|
||||
if (tokenForUserInfo.getToken() == null) {
|
||||
tokenForUserInfo.setToken(accessToken);
|
||||
error.authScheme(authHeader.getScheme());
|
||||
tokenForUserInfo.setToken(authHeader.getToken());
|
||||
} else {
|
||||
throw error.cors(cors.allowAllOrigins()).invalidRequest("More than one method used for including an access token");
|
||||
}
|
||||
|
||||
@@ -204,7 +204,8 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
|
||||
* @return The raw bearer token.
|
||||
*/
|
||||
protected String getRawBearerToken(HttpHeaders httpHeaders, AuthenticationChannelResponse response) {
|
||||
return AppAuthManager.extractAuthorizationHeaderTokenOrReturnNull(httpHeaders);
|
||||
AppAuthManager.AuthHeader authHeader = AppAuthManager.extractAuthorizationHeaderTokenOrReturnNull(httpHeaders);
|
||||
return authHeader == null ? null : authHeader.getToken();
|
||||
}
|
||||
|
||||
protected void sendClientNotificationRequest(ClientModel client, CibaConfig cibaConfig, OAuth2DeviceCodeModel deviceModel) {
|
||||
|
||||
@@ -40,7 +40,7 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
public class AppAuthManager extends AuthenticationManager {
|
||||
|
||||
private static final String BEARER = "Bearer";
|
||||
public static final String BEARER = "Bearer";
|
||||
|
||||
private static final Pattern WHITESPACES = Pattern.compile("\\s+");
|
||||
|
||||
@@ -57,9 +57,9 @@ public class AppAuthManager extends AuthenticationManager {
|
||||
/**
|
||||
* Extracts the token string from the given Authorization Bearer header.
|
||||
*
|
||||
* @return the token string or {@literal null}
|
||||
* @return authHeader with the token and scheme or {@literal null}
|
||||
*/
|
||||
private static String extractTokenStringFromAuthHeader(String authHeader) {
|
||||
private static AuthHeader extractTokenStringFromAuthHeader(String authHeader) {
|
||||
|
||||
if (authHeader == null) {
|
||||
return null;
|
||||
@@ -88,16 +88,16 @@ public class AppAuthManager extends AuthenticationManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tokenString;
|
||||
return new AuthHeader(typeString, tokenString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the token string from the Authorization Bearer Header.
|
||||
*
|
||||
* @param headers
|
||||
* @return the token string or {@literal null} if the Authorization header is not of type Bearer, or the token string is missing.
|
||||
* @return the authHeader with the token and scheme or {@literal null} if the Authorization header is not of supported type (EG. Bearer or DPoP), or the token string is missing.
|
||||
*/
|
||||
public static String extractAuthorizationHeaderTokenOrReturnNull(HttpHeaders headers) {
|
||||
public static AuthHeader extractAuthorizationHeaderTokenOrReturnNull(HttpHeaders headers) {
|
||||
// error if including more than one Authorization header
|
||||
List<String> authHeaders = headers.getRequestHeaders().get(HttpHeaders.AUTHORIZATION);
|
||||
if (authHeaders == null || authHeaders.isEmpty()) {
|
||||
@@ -122,11 +122,11 @@ public class AppAuthManager extends AuthenticationManager {
|
||||
if (authHeader == null) {
|
||||
return null;
|
||||
}
|
||||
String tokenString = extractTokenStringFromAuthHeader(authHeader);
|
||||
if (tokenString == null ){
|
||||
AuthHeader parsedHeader = extractTokenStringFromAuthHeader(authHeader);
|
||||
if (parsedHeader == null ){
|
||||
throw new NotAuthorizedException(BEARER);
|
||||
}
|
||||
return tokenString;
|
||||
return parsedHeader.getToken();
|
||||
}
|
||||
|
||||
public static class BearerTokenAuthenticator {
|
||||
@@ -198,4 +198,23 @@ public class AppAuthManager extends AuthenticationManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static class AuthHeader {
|
||||
|
||||
private final String scheme;
|
||||
private final String token;
|
||||
|
||||
public AuthHeader(String scheme, String token) {
|
||||
this.scheme = scheme;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getScheme() {
|
||||
return scheme;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,11 +36,15 @@ import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||
import org.keycloak.services.cors.Cors;
|
||||
import org.keycloak.services.util.DPoPUtil;
|
||||
|
||||
import static jakarta.ws.rs.core.HttpHeaders.WWW_AUTHENTICATE;
|
||||
import static org.keycloak.OAuth2Constants.ALGS_ATTRIBUTE;
|
||||
import static org.keycloak.services.util.DPoPUtil.DPOP_SCHEME;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:dmitryt@backbase.com">Dmitry Telegin</a>
|
||||
@@ -49,7 +53,9 @@ public class OAuth2Error {
|
||||
|
||||
private static final Map<Response.Status, Class<? extends WebApplicationException>> STATUS_MAP = new HashMap<>();
|
||||
|
||||
private KeycloakSession session;
|
||||
private RealmModel realm;
|
||||
private String authScheme;
|
||||
private String error;
|
||||
private String errorDescription;
|
||||
private Optional<Cors> cors = Optional.empty();
|
||||
@@ -65,11 +71,21 @@ public class OAuth2Error {
|
||||
STATUS_MAP.put(Response.Status.INTERNAL_SERVER_ERROR, InternalServerErrorException.class);
|
||||
}
|
||||
|
||||
public OAuth2Error session(KeycloakSession session) {
|
||||
this.session = session;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OAuth2Error realm(RealmModel realm) {
|
||||
this.realm = realm;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OAuth2Error authScheme(String authScheme) {
|
||||
this.authScheme = authScheme;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OAuth2Error error(String error) {
|
||||
|
||||
this.error = error;
|
||||
@@ -130,7 +146,7 @@ public class OAuth2Error {
|
||||
OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);
|
||||
builder.entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE);
|
||||
} else {
|
||||
WWWAuthenticate.BearerChallenge bearer = new WWWAuthenticate.BearerChallenge();
|
||||
WWWAuthenticate.BearerChallenge bearer = DPOP_SCHEME.equals(this.authScheme) ? new WWWAuthenticate.DPoPChallenge(session) : new WWWAuthenticate.BearerChallenge();
|
||||
bearer.setRealm(realm.getName());
|
||||
bearer.setError(error);
|
||||
bearer.setErrorDescription(errorDescription);
|
||||
@@ -301,6 +317,22 @@ public class OAuth2Error {
|
||||
|
||||
}
|
||||
|
||||
public static class DPoPChallenge extends BearerChallenge {
|
||||
|
||||
public DPoPChallenge(KeycloakSession session) {
|
||||
List<String> dpopAlgs = DPoPUtil.getDPoPSupportedAlgorithms(session);
|
||||
dpopAlgs.stream()
|
||||
.reduce((str1, current) -> str1 + " " + current)
|
||||
.ifPresent(algs -> setAttribute(ALGS_ATTRIBUTE, algs));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getScheme() {
|
||||
return DPOP_SCHEME;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.Matchers.emptyOrNullString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
@@ -113,6 +114,8 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.OAuth2Constants.DPOP_HTTP_HEADER;
|
||||
import static org.keycloak.OAuth2Constants.DPOP_JWT_HEADER_TYPE;
|
||||
import static org.keycloak.OAuthErrorException.INVALID_TOKEN;
|
||||
import static org.keycloak.services.util.DPoPUtil.DPOP_SCHEME;
|
||||
import static org.keycloak.services.util.DPoPUtil.DPOP_TOKEN_TYPE;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientAccessTypeConditionConfig;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createDPoPBindEnforcerExecutorConfig;
|
||||
@@ -546,7 +549,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
UserInfoResponse userInfoResponse = oauth.userInfoRequest(response.getAccessToken()).dpop(null).send();
|
||||
assertEquals(401, userInfoResponse.getStatusCode());
|
||||
assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"Token verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate"));
|
||||
testWWWAuthenticateHeaderError(userInfoResponse);
|
||||
|
||||
oauth.doLogout(response.getRefreshToken());
|
||||
} finally {
|
||||
@@ -561,7 +564,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
UserInfoResponse userInfoResponse = oauth.userInfoRequest(response.getAccessToken()).dpop(null).send();
|
||||
assertEquals(401, userInfoResponse.getStatusCode());
|
||||
assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"Token verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate"));
|
||||
testWWWAuthenticateHeaderError(userInfoResponse);
|
||||
|
||||
oauth.doLogout(response.getRefreshToken());
|
||||
}
|
||||
@@ -577,7 +580,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
||||
String dpopProofRsaEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET, oauth.getEndpoints().getToken(), (long) Time.currentTime(), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate(), response.getAccessToken());
|
||||
UserInfoResponse userInfoResponse = oauth.userInfoRequest(response.getAccessToken()).dpop(dpopProofRsaEncoded).send();
|
||||
assertEquals(401, userInfoResponse.getStatusCode());
|
||||
assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"Token verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate"));
|
||||
testWWWAuthenticateHeaderError(userInfoResponse);
|
||||
|
||||
oauth.doLogout(response.getRefreshToken());
|
||||
}
|
||||
@@ -591,7 +594,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
||||
// use the same DPoP proof
|
||||
UserInfoResponse userInfoResponse = oauth.userInfoRequest(response.getAccessToken()).dpop(dpopProof).send();
|
||||
assertEquals(401, userInfoResponse.getStatusCode());
|
||||
assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"Token verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate"));
|
||||
testWWWAuthenticateHeaderError(userInfoResponse);
|
||||
|
||||
oauth.doLogout(response.getRefreshToken());
|
||||
}
|
||||
@@ -608,11 +611,27 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
||||
String dpopProofRsaEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.GET, oauth.getEndpoints().getUserInfo(), (long) Time.currentTime(), Algorithm.PS256, jwsRsaHeader, rsaKeyPair.getPrivate(), response.getAccessToken());
|
||||
UserInfoResponse userInfoResponse = oauth.userInfoRequest(response.getAccessToken()).dpop(dpopProofRsaEncoded).send();
|
||||
assertEquals(401, userInfoResponse.getStatusCode());
|
||||
assertEquals("Bearer realm=\"test\", error=\"invalid_token\", error_description=\"Token verification failed\"", userInfoResponse.getHeaders().get("WWW-Authenticate"));
|
||||
testWWWAuthenticateHeaderError(userInfoResponse);
|
||||
|
||||
oauth.doLogout(response.getRefreshToken());
|
||||
}
|
||||
|
||||
private void testWWWAuthenticateHeaderError(UserInfoResponse userInfoResponse) {
|
||||
String wwwAuthenticate = userInfoResponse.getHeaders().get("WWW-Authenticate");
|
||||
Assert.assertThat(wwwAuthenticate, startsWith(DPOP_SCHEME));
|
||||
String chunks1 = wwwAuthenticate.substring(DPOP_SCHEME.length() + 1);
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (String p : chunks1.split(", ")) {
|
||||
String[] chunks2 = p.split("=");
|
||||
map.put(chunks2[0], chunks2[1]);
|
||||
}
|
||||
|
||||
Assert.assertEquals(map.get(OAuth2Constants.ERROR), "\"" + INVALID_TOKEN + "\"");
|
||||
String algs = map.get(OAuth2Constants.ALGS_ATTRIBUTE);
|
||||
Assert.assertTrue(algs.contains(Algorithm.EdDSA));
|
||||
Assert.assertTrue(algs.contains(Algorithm.RS256));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDPoPBindEnforcerExecutor() throws Exception {
|
||||
setInitialAccessTokenForDynamicClientRegistration();
|
||||
|
||||
Reference in New Issue
Block a user