Closes #47 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com> Conflicts: core/src/main/java/org/keycloak/util/TokenUtil.java testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java Co-authored-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
cbc4a8c305
commit
0d60e58029
7 changed files with 133 additions and 8 deletions
|
@ -42,6 +42,10 @@ public class TokenUtil {
|
||||||
|
|
||||||
public static final String TOKEN_TYPE_DPOP = "DPoP";
|
public static final String TOKEN_TYPE_DPOP = "DPoP";
|
||||||
|
|
||||||
|
// JWT Access Token types from https://datatracker.ietf.org/doc/html/rfc9068#section-2.1
|
||||||
|
public static final String TOKEN_TYPE_JWT_ACCESS_TOKEN = "at+jwt";
|
||||||
|
public static final String TOKEN_TYPE_JWT_ACCESS_TOKEN_PREFIXED = "application/" + TOKEN_TYPE_JWT_ACCESS_TOKEN;
|
||||||
|
|
||||||
public static final String TOKEN_TYPE_KEYCLOAK_ID = "Serialized-ID";
|
public static final String TOKEN_TYPE_KEYCLOAK_ID = "Serialized-ID";
|
||||||
|
|
||||||
public static final String TOKEN_TYPE_ID = "ID";
|
public static final String TOKEN_TYPE_ID = "ID";
|
||||||
|
|
|
@ -64,6 +64,7 @@ import org.keycloak.services.resources.IdentityBrokerService;
|
||||||
import org.keycloak.services.resources.RealmsResource;
|
import org.keycloak.services.resources.RealmsResource;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
import org.keycloak.util.TokenUtil;
|
||||||
import org.keycloak.vault.VaultStringSecret;
|
import org.keycloak.vault.VaultStringSecret;
|
||||||
|
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
|
@ -79,6 +80,7 @@ import jakarta.ws.rs.core.UriInfo;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@ -97,6 +99,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
public static final String EXCHANGE_PROVIDER = "EXCHANGE_PROVIDER";
|
public static final String EXCHANGE_PROVIDER = "EXCHANGE_PROVIDER";
|
||||||
public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
|
public static final String VALIDATED_ACCESS_TOKEN = "VALIDATED_ACCESS_TOKEN";
|
||||||
private static final String BROKER_NONCE_PARAM = "BROKER_NONCE";
|
private static final String BROKER_NONCE_PARAM = "BROKER_NONCE";
|
||||||
|
private static final List<String> SUPPORTED_TOKEN_TYPES = Arrays.asList(TokenUtil.TOKEN_TYPE_ID, TokenUtil.TOKEN_TYPE_BEARER, TokenUtil.TOKEN_TYPE_JWT_ACCESS_TOKEN, TokenUtil.TOKEN_TYPE_JWT_ACCESS_TOKEN_PREFIXED);
|
||||||
|
|
||||||
public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
||||||
super(session, config);
|
super(session, config);
|
||||||
|
@ -849,7 +852,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
throw new ErrorResponseException(Errors.INVALID_CONFIG, "Invalid server config", Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException(Errors.INVALID_CONFIG, "Invalid server config", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonWebToken parsedToken = null;
|
JsonWebToken parsedToken;
|
||||||
try {
|
try {
|
||||||
parsedToken = validateToken(subjectToken, true);
|
parsedToken = validateToken(subjectToken, true);
|
||||||
} catch (IdentityBrokerException e) {
|
} catch (IdentityBrokerException e) {
|
||||||
|
@ -860,7 +863,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!isTokenTypeSupported(parsedToken)) {
|
||||||
|
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token type not supported", Response.Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
boolean idTokenType = OAuth2Constants.ID_TOKEN_TYPE.equals(subjectTokenType);
|
boolean idTokenType = OAuth2Constants.ID_TOKEN_TYPE.equals(subjectTokenType);
|
||||||
BrokeredIdentityContext context = extractIdentity(null, idTokenType ? null : subjectToken, parsedToken);
|
BrokeredIdentityContext context = extractIdentity(null, idTokenType ? null : subjectToken, parsedToken);
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
|
@ -888,6 +893,10 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static boolean isTokenTypeSupported(JsonWebToken parsedToken) {
|
||||||
|
return SUPPORTED_TOKEN_TYPES.contains(parsedToken.getType());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
|
protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
|
||||||
if (!supportsExternalExchange()) return null;
|
if (!supportsExternalExchange()) return null;
|
||||||
|
|
|
@ -59,7 +59,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
|
|
||||||
private final BlockingQueue<LogoutAction> adminLogoutActions;
|
private final BlockingQueue<LogoutAction> adminLogoutActions;
|
||||||
private final BlockingQueue<LogoutToken> frontChannelLogoutTokens;
|
private final BlockingQueue<LogoutToken> frontChannelLogoutTokens;
|
||||||
private final BlockingQueue<LogoutToken> backChannelLogoutTokens;
|
private final BlockingQueue<String> backChannelLogoutTokens;
|
||||||
private final BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions;
|
private final BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions;
|
||||||
private final BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction;
|
private final BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction;
|
||||||
private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData;
|
private final TestApplicationResourceProviderFactory.OIDCClientData oidcClientData;
|
||||||
|
@ -71,7 +71,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
private final HttpRequest request;
|
private final HttpRequest request;
|
||||||
|
|
||||||
public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue<LogoutAction> adminLogoutActions,
|
public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue<LogoutAction> adminLogoutActions,
|
||||||
BlockingQueue<LogoutToken> backChannelLogoutTokens,
|
BlockingQueue<String> backChannelLogoutTokens,
|
||||||
BlockingQueue<LogoutToken> frontChannelLogoutTokens,
|
BlockingQueue<LogoutToken> frontChannelLogoutTokens,
|
||||||
BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions,
|
BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions,
|
||||||
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction,
|
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction,
|
||||||
|
@ -102,8 +102,8 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
@Path("/admin/backchannelLogout")
|
@Path("/admin/backchannelLogout")
|
||||||
public void backchannelLogout() throws JWSInputException {
|
public void backchannelLogout() {
|
||||||
backChannelLogoutTokens.add(new JWSInput(request.getDecodedFormParameters().getFirst(OAuth2Constants.LOGOUT_TOKEN)).readJsonContent(LogoutToken.class));
|
backChannelLogoutTokens.add(request.getDecodedFormParameters().getFirst(OAuth2Constants.LOGOUT_TOKEN));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@ -139,7 +139,14 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/poll-backchannel-logout")
|
@Path("/poll-backchannel-logout")
|
||||||
public LogoutToken getBackChannelLogoutAction() throws InterruptedException {
|
public LogoutToken getBackChannelLogoutAction() throws InterruptedException, JWSInputException {
|
||||||
|
return new JWSInput(backChannelLogoutTokens.poll(20, TimeUnit.SECONDS)).readJsonContent(LogoutToken.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/poll-backchannel-raw-logout")
|
||||||
|
public String getBackChanneRawlLogoutAction() throws InterruptedException {
|
||||||
return backChannelLogoutTokens.poll(20, TimeUnit.SECONDS);
|
return backChannelLogoutTokens.poll(20, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ import java.util.concurrent.LinkedBlockingDeque;
|
||||||
public class TestApplicationResourceProviderFactory implements RealmResourceProviderFactory {
|
public class TestApplicationResourceProviderFactory implements RealmResourceProviderFactory {
|
||||||
|
|
||||||
private BlockingQueue<LogoutAction> adminLogoutActions = new LinkedBlockingDeque<>();
|
private BlockingQueue<LogoutAction> adminLogoutActions = new LinkedBlockingDeque<>();
|
||||||
private BlockingQueue<LogoutToken> backChannelLogoutTokens = new LinkedBlockingDeque<>();
|
private BlockingQueue<String> backChannelLogoutTokens = new LinkedBlockingDeque<>();
|
||||||
private BlockingQueue<LogoutToken> frontChannelLogoutTokens = new LinkedBlockingDeque<>();
|
private BlockingQueue<LogoutToken> frontChannelLogoutTokens = new LinkedBlockingDeque<>();
|
||||||
private BlockingQueue<PushNotBeforeAction> pushNotBeforeActions = new LinkedBlockingDeque<>();
|
private BlockingQueue<PushNotBeforeAction> pushNotBeforeActions = new LinkedBlockingDeque<>();
|
||||||
private BlockingQueue<TestAvailabilityAction> testAvailabilityActions = new LinkedBlockingDeque<>();
|
private BlockingQueue<TestAvailabilityAction> testAvailabilityActions = new LinkedBlockingDeque<>();
|
||||||
|
|
|
@ -45,6 +45,11 @@ public interface TestApplicationResource {
|
||||||
@Path("/poll-backchannel-logout")
|
@Path("/poll-backchannel-logout")
|
||||||
LogoutToken getBackChannelLogoutToken();
|
LogoutToken getBackChannelLogoutToken();
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/poll-backchannel-raw-logout")
|
||||||
|
String getBackChannelRawLogoutToken();
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/poll-frontchannel-logout")
|
@Path("/poll-frontchannel-logout")
|
||||||
|
|
|
@ -23,12 +23,15 @@ import static org.hamcrest.Matchers.notNullValue;
|
||||||
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
|
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
|
||||||
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim;
|
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import jakarta.ws.rs.client.Client;
|
import jakarta.ws.rs.client.Client;
|
||||||
import jakarta.ws.rs.client.Entity;
|
import jakarta.ws.rs.client.Entity;
|
||||||
import jakarta.ws.rs.client.WebTarget;
|
import jakarta.ws.rs.client.WebTarget;
|
||||||
import jakarta.ws.rs.core.Form;
|
import jakarta.ws.rs.core.Form;
|
||||||
import jakarta.ws.rs.core.HttpHeaders;
|
import jakarta.ws.rs.core.HttpHeaders;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.admin.client.resource.ClientResource;
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
|
@ -45,13 +48,16 @@ import org.keycloak.models.IdentityProviderMapperSyncMode;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
||||||
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
|
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||||
|
import org.keycloak.testsuite.Assert;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeatures;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeatures;
|
||||||
import org.keycloak.testsuite.util.AdminClientUtil;
|
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||||
|
@ -127,6 +133,61 @@ public final class KcOidcBrokerTokenExchangeTest extends AbstractInitializedBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSupportedTokenTypesWhenValidatingSubjectToken() throws Exception {
|
||||||
|
testingClient.server().run(KcOidcBrokerTokenExchangeTest::setupRealm);
|
||||||
|
RealmResource providerRealm = realmsResouce().realm(bc.providerRealmName());
|
||||||
|
ClientsResource clients = providerRealm.clients();
|
||||||
|
ClientRepresentation brokerApp = clients.findByClientId("brokerapp").get(0);
|
||||||
|
brokerApp.setDirectAccessGrantsEnabled(true);
|
||||||
|
ClientResource brokerAppResource = providerRealm.clients().get(brokerApp.getId());
|
||||||
|
brokerAppResource.update(brokerApp);
|
||||||
|
RealmResource consumerRealm = realmsResouce().realm(bc.consumerRealmName());
|
||||||
|
IdentityProviderResource identityProviderResource = consumerRealm.identityProviders().get(bc.getIDPAlias());
|
||||||
|
IdentityProviderRepresentation idpRep = identityProviderResource.toRepresentation();
|
||||||
|
idpRep.getConfig().put("disableUserInfo", "true");
|
||||||
|
identityProviderResource.update(idpRep);
|
||||||
|
getCleanup().addCleanup(() -> {
|
||||||
|
idpRep.getConfig().put("disableUserInfo", "false");
|
||||||
|
identityProviderResource.update(idpRep);
|
||||||
|
});
|
||||||
|
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest(bc.providerRealmName(), bc.getUserLogin(), bc.getUserPassword(), null, brokerApp.getClientId(), brokerApp.getSecret());
|
||||||
|
assertThat(tokenResponse.getIdToken(), notNullValue());
|
||||||
|
String idTokenString = tokenResponse.getIdToken();
|
||||||
|
oauth.realm(bc.providerRealmName());
|
||||||
|
String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString)
|
||||||
|
.postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build();
|
||||||
|
driver.navigate().to(logoutUrl);
|
||||||
|
String logoutToken = testingClient.testApp().getBackChannelRawLogoutToken();
|
||||||
|
Assert.assertNotNull(logoutToken);
|
||||||
|
|
||||||
|
Client httpClient = AdminClientUtil.createResteasyClient();
|
||||||
|
try {
|
||||||
|
WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
|
||||||
|
.path("/realms")
|
||||||
|
.path(bc.consumerRealmName())
|
||||||
|
.path("protocol/openid-connect/token");
|
||||||
|
// test user info validation.
|
||||||
|
try (Response response = exchangeUrl.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(
|
||||||
|
"test-app", "secret"))
|
||||||
|
.post(Entity.form(
|
||||||
|
new Form()
|
||||||
|
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
|
||||||
|
.param(OAuth2Constants.SUBJECT_TOKEN, logoutToken)
|
||||||
|
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
|
||||||
|
.param(OAuth2Constants.SUBJECT_ISSUER, bc.getIDPAlias())
|
||||||
|
.param(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
|
||||||
|
|
||||||
|
))) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Status.BAD_REQUEST.getStatusCode()));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
httpClient.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void setupRealm(KeycloakSession session) {
|
private static void setupRealm(KeycloakSession session) {
|
||||||
RealmModel realm = session.realms().getRealmByName(BrokerTestConstants.REALM_CONS_NAME);
|
RealmModel realm = session.realms().getRealmByName(BrokerTestConstants.REALM_CONS_NAME);
|
||||||
IdentityProviderModel idp = realm.getIdentityProviderByAlias(IDP_OIDC_ALIAS);
|
IdentityProviderModel idp = realm.getIdentityProviderByAlias(IDP_OIDC_ALIAS);
|
||||||
|
@ -149,5 +210,10 @@ public final class KcOidcBrokerTokenExchangeTest extends AbstractInitializedBase
|
||||||
ResourceServer server = management.realmResourceServer();
|
ResourceServer server = management.realmResourceServer();
|
||||||
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(server, clientRep);
|
Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(server, clientRep);
|
||||||
management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
|
management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
|
||||||
|
|
||||||
|
realm = session.realms().getRealmByName(BrokerTestConstants.REALM_PROV_NAME);
|
||||||
|
client = realm.getClientByClientId("brokerapp");
|
||||||
|
client.addRedirectUri(OAuthClient.APP_ROOT + "/auth");
|
||||||
|
client.setAttribute(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, OAuthClient.APP_ROOT + "/admin/backchannelLogout");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,13 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.oauth;
|
package org.keycloak.testsuite.oauth;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.TokenVerifier;
|
import org.keycloak.TokenVerifier;
|
||||||
import org.keycloak.admin.client.resource.ClientResource;
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
|
import org.keycloak.admin.client.resource.ClientsResource;
|
||||||
import org.keycloak.authorization.model.Policy;
|
import org.keycloak.authorization.model.Policy;
|
||||||
import org.keycloak.authorization.model.ResourceServer;
|
import org.keycloak.authorization.model.ResourceServer;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
|
@ -65,6 +67,7 @@ import jakarta.ws.rs.core.Response;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.instanceOf;
|
import static org.hamcrest.Matchers.instanceOf;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
@ -179,6 +182,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
|
||||||
directLegal.setSecret("secret");
|
directLegal.setSecret("secret");
|
||||||
directLegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
directLegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
directLegal.setFullScopeAllowed(false);
|
directLegal.setFullScopeAllowed(false);
|
||||||
|
directLegal.addRedirectUri(OAuthClient.APP_ROOT + "/auth");
|
||||||
|
|
||||||
ClientModel directPublic = realm.addClient("direct-public");
|
ClientModel directPublic = realm.addClient("direct-public");
|
||||||
directPublic.setClientId("direct-public");
|
directPublic.setClientId("direct-public");
|
||||||
|
@ -1017,6 +1021,36 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
|
||||||
testExchange();
|
testExchange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSupportedTokenTypesWhenValidatingSubjectToken() throws Exception {
|
||||||
|
testingClient.server().run(ClientTokenExchangeTest::setupRealm);
|
||||||
|
oauth.realm(TEST);
|
||||||
|
oauth.clientId("direct-legal");
|
||||||
|
oauth.scope(OAuth2Constants.SCOPE_OPENID);
|
||||||
|
ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
|
||||||
|
ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
|
||||||
|
rep.getAttributes().put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, oauth.APP_ROOT + "/admin/backchannelLogout");
|
||||||
|
getCleanup().addCleanup(() -> {
|
||||||
|
rep.getAttributes().put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, "");
|
||||||
|
clients.get(rep.getId()).update(rep);
|
||||||
|
});
|
||||||
|
clients.get(rep.getId()).update(rep);
|
||||||
|
String logoutToken;
|
||||||
|
oauth.clientSessionState("client-session");
|
||||||
|
oauth.doLogin("user", "password");
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret");
|
||||||
|
String idTokenString = tokenResponse.getIdToken();
|
||||||
|
String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString)
|
||||||
|
.postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build();
|
||||||
|
driver.navigate().to(logoutUrl);
|
||||||
|
logoutToken = testingClient.testApp().getBackChannelRawLogoutToken();
|
||||||
|
Assert.assertNotNull(logoutToken);
|
||||||
|
OAuthClient.AccessTokenResponse response = oauth.doTokenExchange(TEST, logoutToken, "target", "direct-legal", "secret");
|
||||||
|
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private static void addDirectExchanger(KeycloakSession session) {
|
private static void addDirectExchanger(KeycloakSession session) {
|
||||||
RealmModel realm = session.realms().getRealmByName(TEST);
|
RealmModel realm = session.realms().getRealmByName(TEST);
|
||||||
RoleModel exampleRole = realm.addRole("example");
|
RoleModel exampleRole = realm.addRole("example");
|
||||||
|
|
Loading…
Reference in a new issue