KEYCLOAK-6718 Auth Flow does not Check Client Protocol

This commit is contained in:
Martin Kanis 2018-09-24 13:14:01 +02:00 committed by Marek Posolda
parent c3fc9e9815
commit efe6a38648
18 changed files with 386 additions and 24 deletions

View file

@ -201,6 +201,16 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
throw new ErrorPageException(session, authenticationSession, Response.Status.FORBIDDEN, Messages.BEARER_ONLY); throw new ErrorPageException(session, authenticationSession, Response.Status.FORBIDDEN, Messages.BEARER_ONLY);
} }
String protocol = client.getProtocol();
if (protocol == null) {
logger.warnf("Client '%s' doesn't have protocol set. Fallback to openid-connect. Please fix client configuration", clientId);
protocol = OIDCLoginProtocol.LOGIN_PROTOCOL;
}
if (!protocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, "Wrong client protocol.");
}
session.getContext().setClient(client); session.getContext().setClient(client);
} }

View file

@ -218,7 +218,7 @@ public class LogoutEndpoint {
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient(); ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient();
if (client.isBearerOnly()) { if (client.isBearerOnly()) {
throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST); throw new ErrorResponseException(Errors.INVALID_CLIENT, "Bearer-only not allowed", Response.Status.BAD_REQUEST);
} }
return client; return client;

View file

@ -41,6 +41,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
@ -149,6 +150,11 @@ public class UserInfoEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client not found", Response.Status.BAD_REQUEST); throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client not found", Response.Status.BAD_REQUEST);
} }
if (!clientModel.getProtocol().equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException(Errors.INVALID_CLIENT, "Wrong client protocol.", Response.Status.BAD_REQUEST);
}
session.getContext().setClient(clientModel); session.getContext().setClient(clientModel);
event.client(clientModel); event.client(clientModel);

View file

@ -17,10 +17,12 @@
package org.keycloak.protocol.oidc.utils; package org.keycloak.protocol.oidc.utils;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.ClientAuthenticator; import org.keycloak.authentication.ClientAuthenticator;
import org.keycloak.authentication.ClientAuthenticatorFactory; import org.keycloak.authentication.ClientAuthenticatorFactory;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -40,6 +42,8 @@ import java.util.Map;
*/ */
public class AuthorizeClientUtil { public class AuthorizeClientUtil {
private static final Logger logger = Logger.getLogger(AuthorizeClientUtil.class);
public static ClientAuthResult authorizeClient(KeycloakSession session, EventBuilder event) { public static ClientAuthResult authorizeClient(KeycloakSession session, EventBuilder event) {
AuthenticationProcessor processor = getAuthenticationProcessor(session, event); AuthenticationProcessor processor = getAuthenticationProcessor(session, event);
@ -50,7 +54,18 @@ public class AuthorizeClientUtil {
ClientModel client = processor.getClient(); ClientModel client = processor.getClient();
if (client == null) { if (client == null) {
throw new ErrorResponseException("invalid_client", "Client authentication ended, but client is null", Response.Status.BAD_REQUEST); throw new ErrorResponseException(Errors.INVALID_CLIENT, "Client authentication ended, but client is null", Response.Status.BAD_REQUEST);
}
String protocol = client.getProtocol();
if (protocol == null) {
logger.warnf("Client '%s' doesn't have protocol set. Fallback to openid-connect. Please fix client configuration", client.getClientId());
protocol = OIDCLoginProtocol.LOGIN_PROTOCOL;
}
if (!protocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException(Errors.INVALID_CLIENT, "Wrong client protocol.", Response.Status.BAD_REQUEST);
} }
session.getContext().setClient(client); session.getContext().setClient(client);

View file

@ -175,6 +175,13 @@ public class SamlService extends AuthorizationEndpointBase {
event.error(Errors.CLIENT_NOT_FOUND); event.error(Errors.CLIENT_NOT_FOUND);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.CLIENT_NOT_FOUND); return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.CLIENT_NOT_FOUND);
} }
if (!isClientProtocolCorrect(client)) {
event.event(EventType.LOGOUT);
event.error(Errors.INVALID_CLIENT);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
}
session.getContext().setClient(client); session.getContext().setClient(client);
logger.debug("logout response"); logger.debug("logout response");
Response response = authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers); Response response = authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers);
@ -225,6 +232,11 @@ public class SamlService extends AuthorizationEndpointBase {
event.error(Errors.NOT_ALLOWED); event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.STANDARD_FLOW_DISABLED); return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.STANDARD_FLOW_DISABLED);
} }
if (!isClientProtocolCorrect(client)) {
event.event(EventType.LOGIN);
event.error(Errors.INVALID_CLIENT);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
}
session.getContext().setClient(client); session.getContext().setClient(client);
@ -607,6 +619,14 @@ public class SamlService extends AuthorizationEndpointBase {
key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, false)); key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, false));
} }
private boolean isClientProtocolCorrect(ClientModel clientModel) {
if (SamlProtocol.LOGIN_PROTOCOL.equals(clientModel.getProtocol())) {
return true;
}
return false;
}
@GET @GET
@Path("clients/{client}") @Path("clients/{client}")
@Produces(MediaType.TEXT_HTML_UTF_8) @Produces(MediaType.TEXT_HTML_UTF_8)
@ -631,6 +651,10 @@ public class SamlService extends AuthorizationEndpointBase {
event.error(Errors.CLIENT_DISABLED); event.error(Errors.CLIENT_DISABLED);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.CLIENT_DISABLED); return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.CLIENT_DISABLED);
} }
if (!isClientProtocolCorrect(client)) {
event.error(Errors.INVALID_CLIENT);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
}
if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) == null) { if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) == null) {
logger.error("SAML assertion consumer url not set up"); logger.error("SAML assertion consumer url not set up");
event.error(Errors.INVALID_REDIRECT_URI); event.error(Errors.INVALID_REDIRECT_URI);

View file

@ -100,10 +100,9 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
} }
} }
public ClientRepresentation get(String clientId) { public ClientRepresentation get(ClientModel client) {
event.event(EventType.CLIENT_INFO); event.event(EventType.CLIENT_INFO);
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
auth.requireView(client); auth.requireView(client);
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client, session); ClientRepresentation rep = ModelToRepresentation.toRepresentation(client, session);

View file

@ -36,15 +36,13 @@ import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyException; import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyException;
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager; import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.clientregistration.policy.RegistrationAuth;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -62,11 +60,13 @@ public class ClientRegistrationAuth {
private ClientInitialAccessModel initialAccessModel; private ClientInitialAccessModel initialAccessModel;
private String kid; private String kid;
private String token; private String token;
private String endpoint;
public ClientRegistrationAuth(KeycloakSession session, ClientRegistrationProvider provider, EventBuilder event) { public ClientRegistrationAuth(KeycloakSession session, ClientRegistrationProvider provider, EventBuilder event, String endpoint) {
this.session = session; this.session = session;
this.provider = provider; this.provider = provider;
this.event = event; this.event = event;
this.endpoint = endpoint;
} }
private void init() { private void init() {
@ -129,6 +129,8 @@ public class ClientRegistrationAuth {
RegistrationAuth registrationAuth = RegistrationAuth.ANONYMOUS; RegistrationAuth registrationAuth = RegistrationAuth.ANONYMOUS;
if (isBearerToken()) { if (isBearerToken()) {
checkClientProtocol();
if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.CREATE_CLIENT)) { if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.CREATE_CLIENT)) {
registrationAuth = RegistrationAuth.AUTHENTICATED; registrationAuth = RegistrationAuth.AUTHENTICATED;
} else { } else {
@ -162,6 +164,8 @@ public class ClientRegistrationAuth {
init(); init();
if (isBearerToken()) { if (isBearerToken()) {
checkClientProtocol();
if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.VIEW_CLIENTS)) { if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.VIEW_CLIENTS)) {
if (client == null) { if (client == null) {
throw notFound(); throw notFound();
@ -224,10 +228,26 @@ public class ClientRegistrationAuth {
} }
} }
private void checkClientProtocol() {
ClientModel client = session.getContext().getRealm().getClientByClientId(jwt.getIssuedFor());
checkClientProtocol(client);
}
private void checkClientProtocol(ClientModel client) {
if (endpoint.equals("openid-connect") || endpoint.equals("saml2-entity-descriptor")) {
if (client != null && !endpoint.contains(client.getProtocol())) {
throw new ErrorResponseException(Errors.INVALID_CLIENT, "Wrong client protocol.", Response.Status.BAD_REQUEST);
}
}
}
private RegistrationAuth requireUpdateAuth(ClientModel client) { private RegistrationAuth requireUpdateAuth(ClientModel client) {
init(); init();
if (isBearerToken()) { if (isBearerToken()) {
checkClientProtocol();
if (hasRole(AdminRoles.MANAGE_CLIENTS)) { if (hasRole(AdminRoles.MANAGE_CLIENTS)) {
if (client == null) { if (client == null) {
throw notFound(); throw notFound();
@ -344,6 +364,8 @@ public class ClientRegistrationAuth {
throw unauthorized("Different client authenticated"); throw unauthorized("Different client authenticated");
} }
checkClientProtocol(authClient);
return true; return true;
} }

View file

@ -52,7 +52,7 @@ public class ClientRegistrationService {
} }
provider.setEvent(event); provider.setEvent(event);
provider.setAuth(new ClientRegistrationAuth(session, provider, event)); provider.setAuth(new ClientRegistrationAuth(session, provider, event, providerId));
return provider; return provider;
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.services.clientregistration; package org.keycloak.services.clientregistration;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
@ -55,8 +56,9 @@ public class DefaultClientRegistrationProvider extends AbstractClientRegistratio
@Path("{clientId}") @Path("{clientId}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response getDefault(@PathParam("clientId") String clientId) { public Response getDefault(@PathParam("clientId") String clientId) {
ClientRepresentation client = get(clientId); ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
return Response.ok(client).build(); ClientRepresentation clientRepresentation = get(client);
return Response.ok(clientRepresentation).build();
} }
@PUT @PUT

View file

@ -100,8 +100,11 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
@Path("{clientId}") @Path("{clientId}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public Response getOIDC(@PathParam("clientId") String clientId) { public Response getOIDC(@PathParam("clientId") String clientId) {
ClientRepresentation client = get(clientId); ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
OIDCClientRepresentation clientOIDC = DescriptionConverter.toExternalResponse(session, client, session.getContext().getUri().getRequestUri());
ClientRepresentation clientRepresentation = get(client);
OIDCClientRepresentation clientOIDC = DescriptionConverter.toExternalResponse(session, clientRepresentation, session.getContext().getUri().getRequestUri());
return Response.ok(clientOIDC).build(); return Response.ok(clientOIDC).build();
} }
@ -175,5 +178,4 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
} }
rep.setProtocolMappers(mappings); rep.setProtocolMappers(mappings);
} }
} }

View file

@ -24,6 +24,7 @@ import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator; import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants; import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
@ -32,6 +33,7 @@ import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.broker.provider.IdentityProviderMapper; import org.keycloak.broker.provider.IdentityProviderMapper;
import org.keycloak.broker.provider.util.IdentityBrokerState; import org.keycloak.broker.provider.util.IdentityBrokerState;
import org.keycloak.broker.saml.SAMLEndpoint; import org.keycloak.broker.saml.SAMLEndpoint;
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Base64Url;
@ -70,6 +72,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorPageException; import org.keycloak.services.ErrorPageException;
import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
@ -1240,7 +1243,6 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
} }
} }
private static class ParsedCodeContext { private static class ParsedCodeContext {
private ClientSessionCode<AuthenticationSessionModel> clientSessionCode; private ClientSessionCode<AuthenticationSessionModel> clientSessionCode;
private Response response; private Response response;

View file

@ -21,10 +21,12 @@ package org.keycloak.testsuite.client;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.client.registration.Auth; import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException; import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.common.util.CollectionUtil; import org.keycloak.common.util.CollectionUtil;
import org.keycloak.events.Errors;
import org.keycloak.jose.jws.Algorithm; import org.keycloak.jose.jws.Algorithm;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -33,19 +35,19 @@ import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation; import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.util.KeycloakModelUtils;
import java.util.*; import java.util.*;
import javax.ws.rs.core.Response;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -58,8 +60,14 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
@Override @Override
public void addTestRealms(List<RealmRepresentation> testRealms) { public void addTestRealms(List<RealmRepresentation> testRealms) {
super.addTestRealms(testRealms); super.addTestRealms(testRealms);
testRealms.get(0).setPrivateKey(PRIVATE_KEY); RealmRepresentation testRealm = testRealms.get(0);
testRealms.get(0).setPublicKey(PUBLIC_KEY); testRealm.setPrivateKey(PRIVATE_KEY);
testRealm.setPublicKey(PUBLIC_KEY);
ClientRepresentation samlApp = KeycloakModelUtils.createClient(testRealm, "saml-client");
samlApp.setSecret("secret");
samlApp.setServiceAccountsEnabled(true);
samlApp.setDirectAccessGrantsEnabled(true);
} }
@Before @Before
@ -103,6 +111,19 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
} }
} }
private void assertGetFail(String clientId, int expectedStatusCode, String expectedErrorContains) {
try {
reg.oidc().get(clientId);
Assert.fail("Not expected to successfully get client");
} catch (ClientRegistrationException expected) {
HttpErrorException httpEx = (HttpErrorException) expected.getCause();
Assert.assertEquals(expectedStatusCode, httpEx.getStatusLine().getStatusCode());
if (expectedErrorContains != null) {
assertTrue("Error response doesn't contain expected text", httpEx.getErrorResponse().contains(expectedErrorContains));
}
}
}
// KEYCLOAK-3421 // KEYCLOAK-3421
@Test @Test
public void createClientWithUriFragment() { public void createClientWithUriFragment() {
@ -262,6 +283,50 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
} }
@Test
public void testOIDCEndpointCreateWithSamlClient() throws Exception {
ClientsResource clientsResource = adminClient.realm(TEST).clients();
ClientRepresentation samlClient = clientsResource.findByClientId("saml-client").get(0);
String samlClientServiceId = clientsResource.get(samlClient.getId()).getServiceAccountUser().getId();
String realmManagementId = clientsResource.findByClientId("realm-management").get(0).getId();
RoleRepresentation role = clientsResource.get(realmManagementId).roles().get("create-client").toRepresentation();
adminClient.realm(TEST).users().get(samlClientServiceId).roles().clientLevel(realmManagementId).add(Arrays.asList(role));
String accessToken = oauth.clientId("saml-client").doClientCredentialsGrantAccessTokenRequest("secret").getAccessToken();
reg.auth(Auth.token(accessToken));
// change client to saml
samlClient.setProtocol("saml");
clientsResource.get(samlClient.getId()).update(samlClient);
OIDCClientRepresentation client = createRep();
assertCreateFail(client, 400, Errors.INVALID_CLIENT);
// revert client
samlClient.setProtocol("openid-connect");
clientsResource.get(samlClient.getId()).update(samlClient);
}
@Test
public void testOIDCEndpointGetWithSamlClient() {
ClientsResource clientsResource = adminClient.realm(TEST).clients();
ClientRepresentation samlClient = clientsResource.findByClientId("saml-client").get(0);
reg.auth(Auth.client("saml-client", "secret"));
// change client to saml
samlClient.setProtocol("saml");
clientsResource.get(samlClient.getId()).update(samlClient);
assertGetFail(samlClient.getClientId(), 400, Errors.INVALID_CLIENT);
// revert client
samlClient.setProtocol("openid-connect");
clientsResource.get(samlClient.getId()).update(samlClient);
}
private ClientRepresentation getKeycloakClient(String clientId) { private ClientRepresentation getKeycloakClient(String clientId) {
return ApiUtil.findClientByClientId(adminClient.realms().realm(REALM_NAME), clientId).toRepresentation(); return ApiUtil.findClientByClientId(adminClient.realms().realm(REALM_NAME), clientId).toRepresentation();
} }

View file

@ -20,22 +20,44 @@ package org.keycloak.testsuite.client;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.client.registration.Auth; import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException; import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.events.Errors;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation; import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.util.KeycloakModelUtils;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest { public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest {
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
super.addTestRealms(testRealms);
RealmRepresentation testRealm = testRealms.get(0);
ClientRepresentation samlApp = KeycloakModelUtils.createClient(testRealm, "oidc-client");
samlApp.setSecret("secret");
samlApp.setServiceAccountsEnabled(true);
samlApp.setDirectAccessGrantsEnabled(true);
}
@Before @Before
public void before() throws Exception { public void before() throws Exception {
super.before(); super.before();
@ -61,4 +83,34 @@ public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest {
assertThat(response.getAttributes().get("saml_single_logout_service_url_redirect"), is("https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp")); assertThat(response.getAttributes().get("saml_single_logout_service_url_redirect"), is("https://LoadBalancer-9.siroe.com:3443/federation/SPSloRedirect/metaAlias/sp"));
} }
@Test
public void testSAMLEndpointCreateWithOIDCClient() throws Exception {
ClientsResource clientsResource = adminClient.realm(TEST).clients();
ClientRepresentation oidcClient = clientsResource.findByClientId("oidc-client").get(0);
String oidcClientServiceId = clientsResource.get(oidcClient.getId()).getServiceAccountUser().getId();
String realmManagementId = clientsResource.findByClientId("realm-management").get(0).getId();
RoleRepresentation role = clientsResource.get(realmManagementId).roles().get("create-client").toRepresentation();
adminClient.realm(TEST).users().get(oidcClientServiceId).roles().clientLevel(realmManagementId).add(Arrays.asList(role));
String accessToken = oauth.clientId("oidc-client").doClientCredentialsGrantAccessTokenRequest("secret").getAccessToken();
reg.auth(Auth.token(accessToken));
String entityDescriptor = IOUtils.toString(getClass().getResourceAsStream("/clientreg-test/saml-entity-descriptor.xml"));
assertCreateFail(entityDescriptor, 400, Errors.INVALID_CLIENT);
}
private void assertCreateFail(String entityDescriptor, int expectedStatusCode, String expectedErrorContains) {
try {
reg.saml().create(entityDescriptor);
Assert.fail("Not expected to successfully register client");
} catch (ClientRegistrationException expected) {
HttpErrorException httpEx = (HttpErrorException) expected.getCause();
Assert.assertEquals(expectedStatusCode, httpEx.getStatusLine().getStatusCode());
if (expectedErrorContains != null) {
assertTrue("Error response doesn't contain expected text", httpEx.getErrorResponse().contains(expectedErrorContains));
}
}
}
} }

View file

@ -23,6 +23,7 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Errors;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
@ -68,6 +69,11 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
ClientRepresentation pubApp = KeycloakModelUtils.createClient(testRealm, "public-cli"); ClientRepresentation pubApp = KeycloakModelUtils.createClient(testRealm, "public-cli");
pubApp.setPublicClient(Boolean.TRUE); pubApp.setPublicClient(Boolean.TRUE);
ClientRepresentation samlApp = KeycloakModelUtils.createClient(testRealm, "saml-client");
samlApp.setSecret("secret2");
samlApp.setServiceAccountsEnabled(Boolean.TRUE);
samlApp.setProtocol("saml");
UserRepresentation user = new UserRepresentation(); UserRepresentation user = new UserRepresentation();
user.setUsername("no-permissions"); user.setUsername("no-permissions");
CredentialRepresentation credential = new CredentialRepresentation(); CredentialRepresentation credential = new CredentialRepresentation();
@ -350,4 +356,21 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
assertNull(rep.getClientId()); assertNull(rep.getClientId());
assertNull(rep.getSubject()); assertNull(rep.getSubject());
} }
/**
* Test covers the same scenario from different endpoints like TokenEndpoint and LogoutEndpoint.
*/
@Test
public void testIntrospectWithSamlClient() throws Exception {
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
events.expectLogin().assertEvent();
AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password");
String tokenResponse = oauth.introspectAccessTokenWithClientCredential("saml-client", "secret2", accessTokenResponse.getAccessToken());
TokenMetadataRepresentation rep = JsonSerialization.readValue(tokenResponse, TokenMetadataRepresentation.class);
assertEquals(Errors.INVALID_CLIENT, rep.getOtherClaims().get("error"));
assertNull(rep.getSubject());
}
} }

View file

@ -29,7 +29,7 @@ import org.keycloak.events.EventType;
import org.keycloak.jose.jws.Algorithm; import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
@ -86,11 +86,15 @@ public class UserInfoTest extends AbstractKeycloakTest {
@Override @Override
public void addTestRealms(List<RealmRepresentation> testRealms) { public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
RealmBuilder realm = RealmBuilder.edit(realmRepresentation).testEventListener(); RealmBuilder realm = RealmBuilder.edit(realmRepresentation).testEventListener();
testRealms.add(realm.build()); RealmRepresentation testRealm = realm.build();
testRealms.add(testRealm);
ClientRepresentation samlApp = KeycloakModelUtils.createClient(testRealm, "saml-client");
samlApp.setSecret("secret");
samlApp.setServiceAccountsEnabled(true);
samlApp.setDirectAccessGrantsEnabled(true);
} }
@Test @Test
@ -351,6 +355,35 @@ public class UserInfoTest extends AbstractKeycloakTest {
} }
} }
@Test
public void testUserInfoRequestWithSamlClient() throws Exception {
// obtain an access token
String accessToken = oauth.doGrantAccessTokenRequest("test", "test-user@localhost", "password", null, "saml-client", "secret").getAccessToken();
// change client's protocol
ClientRepresentation samlClient = adminClient.realm("test").clients().findByClientId("saml-client").get(0);
samlClient.setProtocol("saml");
adminClient.realm("test").clients().get(samlClient.getId()).update(samlClient);
Client client = ClientBuilder.newClient();
try {
events.clear();
Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessToken);
response.close();
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
events.expect(EventType.USER_INFO_REQUEST)
.error(Errors.INVALID_CLIENT)
.client((String) null)
.user(Matchers.nullValue(String.class))
.session(Matchers.nullValue(String.class))
.detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN)
.assertEvent();
} finally {
client.close();
}
}
private AccessTokenResponse executeGrantAccessTokenRequest(Client client) { private AccessTokenResponse executeGrantAccessTokenRequest(Client client) {
return executeGrantAccessTokenRequest(client, false); return executeGrantAccessTokenRequest(client, false);
} }

View file

@ -16,13 +16,19 @@
*/ */
package org.keycloak.testsuite.saml; package org.keycloak.testsuite.saml;
import org.apache.http.client.HttpResponseException;
import org.junit.Assert;
import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.UsersResource; import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.Matchers; import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.SamlClient.Binding; import org.keycloak.testsuite.util.SamlClient.Binding;
import org.keycloak.testsuite.util.SamlClientBuilder; import org.keycloak.testsuite.util.SamlClientBuilder;
@ -31,10 +37,16 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.junit.Test; import org.junit.Test;
import javax.ws.rs.core.Response;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.util.Matchers.bodyHC;
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
/** /**
* *
@ -107,4 +119,21 @@ public class IdpInitiatedLoginTest extends AbstractSamlTest {
} }
@Test
public void testIdpInitiatedLoginWithOIDCClient() {
ClientRepresentation clientRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
adminClient.realm(REALM_NAME).clients().get(clientRep.getId()).update(ClientBuilder.edit(clientRep)
.protocol(OIDCLoginProtocol.LOGIN_PROTOCOL).build());
new SamlClientBuilder()
.idpInitiatedLogin(getAuthServerSamlEndpoint(REALM_NAME), "sales-post").build()
.execute(r -> {
Assert.assertThat(r, statusCodeIsHC(Response.Status.BAD_REQUEST));
Assert.assertThat(r, bodyHC(containsString("Wrong client protocol.")));
});
adminClient.realm(REALM_NAME).clients().get(clientRep.getId()).update(ClientBuilder.edit(clientRep)
.protocol(SamlProtocol.LOGIN_PROTOCOL).build());
}
} }

View file

@ -0,0 +1,70 @@
package org.keycloak.testsuite.saml;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.SamlClient;
import org.w3c.dom.Document;
import java.io.IOException;
import java.net.URI;
/**
*
* @author mkanis
*/
public class SamlClientTest extends AbstractSamlTest {
@Test
public void testLoginWithOIDCClient() throws ParsingException, ConfigurationException, ProcessingException, IOException {
ClientRepresentation salesRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
adminClient.realm(REALM_NAME).clients().get(salesRep.getId()).update(ClientBuilder.edit(salesRep)
.protocol(OIDCLoginProtocol.LOGIN_PROTOCOL).build());
AuthnRequestType loginRep = createLoginRequestDocument(SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, REALM_NAME);
Document samlRequest = SAML2Request.convert(loginRep);
SamlClient.RedirectStrategyWithSwitchableFollowRedirect strategy = new SamlClient.RedirectStrategyWithSwitchableFollowRedirect();
URI samlEndpoint = getAuthServerSamlEndpoint(REALM_NAME);
try (CloseableHttpClient client = HttpClientBuilder.create().setRedirectStrategy(strategy).build()) {
HttpUriRequest post = SamlClient.Binding.POST.createSamlUnsignedRequest(samlEndpoint, null, samlRequest);
CloseableHttpResponse response = sendPost(post, client);
Assert.assertEquals(response.getStatusLine().getStatusCode(), 400);
String s = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
Assert.assertThat(s, Matchers.containsString("Wrong client protocol."));
response.close();
}
adminClient.realm(REALM_NAME).clients().get(salesRep.getId()).update(ClientBuilder.edit(salesRep)
.protocol(SamlProtocol.LOGIN_PROTOCOL).build());
}
private CloseableHttpResponse sendPost(HttpUriRequest post, final CloseableHttpClient client) {
CloseableHttpResponse response;
try {
HttpClientContext context = HttpClientContext.create();
response = client.execute(post, context);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return response;
}
}

View file

@ -23,6 +23,9 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import java.util.Arrays;
import java.util.List;
import static org.keycloak.models.utils.KeycloakModelUtils.getDefaultClientAuthenticatorType; import static org.keycloak.models.utils.KeycloakModelUtils.getDefaultClientAuthenticatorType;
/** /**
@ -37,7 +40,12 @@ public class KeycloakModelUtils {
ClientRepresentation app = new ClientRepresentation(); ClientRepresentation app = new ClientRepresentation();
app.setName(name); app.setName(name);
app.setClientId(name); app.setClientId(name);
realm.getClients().add(app); List<ClientRepresentation> clients = realm.getClients();
if (clients != null) {
clients.add(app);
} else {
realm.setClients(Arrays.asList(app));
}
app.setClientAuthenticatorType(getDefaultClientAuthenticatorType()); app.setClientAuthenticatorType(getDefaultClientAuthenticatorType());
generateSecret(app); generateSecret(app);
app.setFullScopeAllowed(true); app.setFullScopeAllowed(true);