mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
DPoP: User Info Endpoint authorization type mismatch
closes #36476 Signed-off-by: Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>
This commit is contained in:
parent
1d9c0f373a
commit
be818502ad
@ -80,6 +80,7 @@ import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author pedroigor
|
||||
@ -99,6 +100,8 @@ public class UserInfoEndpoint {
|
||||
private Cors cors;
|
||||
private TokenForUserInfo tokenForUserInfo = new TokenForUserInfo();
|
||||
|
||||
private static final Pattern WHITESPACES = Pattern.compile("\\s+");
|
||||
|
||||
public UserInfoEndpoint(KeycloakSession session, org.keycloak.protocol.oidc.TokenManager tokenManager) {
|
||||
this.session = session;
|
||||
this.clientConnection = session.getContext().getConnection();
|
||||
@ -257,6 +260,16 @@ public class UserInfoEndpoint {
|
||||
}
|
||||
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.DPOP)) {
|
||||
String authHeader = request.getHttpHeaders().getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||
String[] split = WHITESPACES.split(authHeader.trim());
|
||||
String bearerPart = split[0];
|
||||
if (!bearerPart.equalsIgnoreCase(TokenUtil.TOKEN_TYPE_DPOP) && DPoPUtil.DPOP_TOKEN_TYPE.equals(token.getType())) {
|
||||
String errorMessage = "The access token type is DPoP but Authorization Header is not DPoP";
|
||||
event.detail(Details.REASON, errorMessage);
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw error.invalidToken(errorMessage);
|
||||
}
|
||||
|
||||
if (OIDCAdvancedConfigWrapper.fromClientModel(clientModel).isUseDPoP() || DPoPUtil.DPOP_TOKEN_TYPE.equals(token.getType())) {
|
||||
try {
|
||||
DPoP dPoP = new DPoPUtil.Validator(session).request(request).uriInfo(session.getContext().getUri()).validate();
|
||||
@ -268,6 +281,7 @@ public class UserInfoEndpoint {
|
||||
throw error.invalidToken(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Existence of authenticatedClientSession for our client already handled before
|
||||
|
||||
@ -19,6 +19,7 @@ package org.keycloak.services.managers;
|
||||
import jakarta.ws.rs.NotAuthorizedException;
|
||||
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.ObjectUtil;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
@ -28,6 +29,7 @@ import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
@ -67,8 +69,15 @@ public class AppAuthManager extends AuthenticationManager {
|
||||
}
|
||||
|
||||
String bearerPart = split[0];
|
||||
if (!bearerPart.equalsIgnoreCase(BEARER) && !bearerPart.equalsIgnoreCase(TokenUtil.TOKEN_TYPE_DPOP)){
|
||||
return null;
|
||||
|
||||
if (!Profile.isFeatureEnabled(Profile.Feature.DPOP)) {
|
||||
if (bearerPart.equalsIgnoreCase(TokenUtil.TOKEN_TYPE_DPOP)){
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
if (!bearerPart.equalsIgnoreCase(BEARER) && !bearerPart.equalsIgnoreCase(TokenUtil.TOKEN_TYPE_DPOP)){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String tokenString = split[1];
|
||||
@ -86,6 +95,14 @@ public class AppAuthManager extends AuthenticationManager {
|
||||
* @return the token string or {@literal null} if the Authorization header is not of type Bearer, or the token string is missing.
|
||||
*/
|
||||
public static String extractAuthorizationHeaderTokenOrReturnNull(HttpHeaders headers) {
|
||||
// error if including more than one Authorization header
|
||||
List<String> authHeaders = headers.getRequestHeaders().get(HttpHeaders.AUTHORIZATION);
|
||||
if (authHeaders == null || authHeaders.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (authHeaders.size() != 1) {
|
||||
throw new NotAuthorizedException(BEARER);
|
||||
}
|
||||
String authHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
|
||||
return extractTokenStringFromAuthHeader(authHeader);
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
@ -88,9 +89,11 @@ import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.UserInfoClientUtil;
|
||||
import org.keycloak.testsuite.util.UserManager;
|
||||
import org.keycloak.testsuite.util.oauth.UserInfoResponse;
|
||||
import org.keycloak.util.BasicAuthHelper;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
import org.keycloak.utils.MediaType;
|
||||
import org.openqa.selenium.By;
|
||||
|
||||
import jakarta.ws.rs.client.Client;
|
||||
@ -136,6 +139,7 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
private HttpGet get;
|
||||
|
||||
@Override
|
||||
public void beforeAbstractKeycloakTest() throws Exception {
|
||||
@ -430,6 +434,23 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
||||
RealmManager.realm(adminClient.realm("test")).accessCodeLifeSpan(60);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bearerAccessTokenAsDPoPOnUserInfoEndpoint() throws IOException {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||
loginEvent.getSessionId();
|
||||
AccessTokenResponse response = oauth.doAccessTokenRequest(oauth.parseLoginResponse().getCode());
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
|
||||
get = new HttpGet(oauth.getEndpoints().getUserInfo());
|
||||
get.addHeader("Accept", MediaType.APPLICATION_JSON);
|
||||
get.addHeader(HttpHeaders.AUTHORIZATION, "DPoP" + " " + response.getAccessToken());
|
||||
|
||||
UserInfoResponse userInfoResponse = new UserInfoResponse(oauth.httpClient().get().execute(get));
|
||||
|
||||
assertEquals(401, userInfoResponse.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void accessTokenCodeUsed() throws IOException {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
@ -19,8 +19,10 @@ package org.keycloak.testsuite.oauth;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.HttpMethod;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@ -71,6 +73,7 @@ import org.keycloak.testsuite.util.ServerURLs;
|
||||
import org.keycloak.util.JWKSUtils;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
@ -85,7 +88,6 @@ import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.emptyOrNullString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@ -122,6 +124,8 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
||||
private String jktEc;
|
||||
private ClientRegistration reg;
|
||||
|
||||
private HttpGet get;
|
||||
|
||||
@Before
|
||||
public void beforeDPoPTest() throws Exception {
|
||||
rsaKeyPair = KeyUtils.generateRsaKeyPair(2048);
|
||||
@ -149,6 +153,42 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDuplicatedAuthorizationHeaderOnUserInfo() throws Exception {
|
||||
KeyPair rsaKeyPair = KeyUtils.generateRsaKeyPair(2048);
|
||||
AccessTokenResponse response = getDPoPBindAccessToken(rsaKeyPair);
|
||||
|
||||
get = new HttpGet(oauth.getEndpoints().getUserInfo());
|
||||
get.addHeader("Accept", MediaType.APPLICATION_JSON);
|
||||
String authorization = "DPoP" + " " + response.getAccessToken();
|
||||
get.addHeader(HttpHeaders.AUTHORIZATION, authorization);
|
||||
get.addHeader(HttpHeaders.AUTHORIZATION, authorization);
|
||||
|
||||
UserInfoResponse userInfoResponse = new UserInfoResponse(oauth.httpClient().get().execute(get));
|
||||
|
||||
assertEquals(401, userInfoResponse.getStatusCode());
|
||||
assertEquals("HTTP 401 Unauthorized", userInfoResponse.getError());
|
||||
|
||||
oauth.doLogout(response.getRefreshToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDPoPAccessTokenButBearerAuthorizationHeader() throws Exception {
|
||||
KeyPair rsaKeyPair = KeyUtils.generateRsaKeyPair(2048);
|
||||
AccessTokenResponse response = getDPoPBindAccessToken(rsaKeyPair);
|
||||
|
||||
get = new HttpGet(oauth.getEndpoints().getUserInfo());
|
||||
get.addHeader("Accept", MediaType.APPLICATION_JSON);
|
||||
String authorization = "Bearer" + " " + response.getAccessToken();
|
||||
get.addHeader(HttpHeaders.AUTHORIZATION, authorization);
|
||||
|
||||
UserInfoResponse userInfoResponse = new UserInfoResponse(oauth.httpClient().get().execute(get));
|
||||
assertEquals(401, userInfoResponse.getStatusCode());
|
||||
|
||||
oauth.doLogout(response.getRefreshToken());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDPoPByPublicClientWithDpopJkt() throws Exception {
|
||||
// use pre-computed EC key
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user