[KEYCLOAK-7450] - Match subject when validating id_token returned from external OP
This commit is contained in:
parent
52b67f6172
commit
b60b85ab65
6 changed files with 94 additions and 3 deletions
|
@ -365,6 +365,10 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
|
|
||||||
try {
|
try {
|
||||||
BrokeredIdentityContext identity = extractIdentity(tokenResponse, accessToken, idToken);
|
BrokeredIdentityContext identity = extractIdentity(tokenResponse, accessToken, idToken);
|
||||||
|
|
||||||
|
if (!identity.getId().equals(idToken.getSubject())) {
|
||||||
|
throw new IdentityBrokerException("Mismatch between the subject in the id_token and the subject from the user_info endpoint");
|
||||||
|
}
|
||||||
|
|
||||||
identity.getContextData().put(BROKER_NONCE_PARAM, idToken.getOtherClaims().get(OIDCLoginProtocol.NONCE_PARAM));
|
identity.getContextData().put(BROKER_NONCE_PARAM, idToken.getOtherClaims().get(OIDCLoginProtocol.NONCE_PARAM));
|
||||||
|
|
||||||
|
|
|
@ -203,11 +203,17 @@ public class UserInfoEndpoint {
|
||||||
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionScopeParameter(clientSession, session);
|
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionScopeParameter(clientSession, session);
|
||||||
|
|
||||||
AccessToken userInfo = new AccessToken();
|
AccessToken userInfo = new AccessToken();
|
||||||
|
|
||||||
|
userInfo.subject(userModel.getId());
|
||||||
|
|
||||||
tokenManager.transformUserInfoAccessToken(session, userInfo, userSession, clientSessionCtx);
|
tokenManager.transformUserInfoAccessToken(session, userInfo, userSession, clientSessionCtx);
|
||||||
|
|
||||||
Map<String, Object> claims = new HashMap<>();
|
Map<String, Object> claims = new HashMap<>();
|
||||||
claims.put("sub", userModel.getId());
|
|
||||||
claims.putAll(userInfo.getOtherClaims());
|
claims.putAll(userInfo.getOtherClaims());
|
||||||
|
// we always set the subject to the correct value and ignore any mapper (not directly related to subject mapping such as
|
||||||
|
// pseudo-subjects). the endpoint should always return a valid subject identifier.
|
||||||
|
// any attempt to customize the value of this field should be done through a different claim
|
||||||
|
claims.put("sub", userInfo.getSubject());
|
||||||
|
|
||||||
Response.ResponseBuilder responseBuilder;
|
Response.ResponseBuilder responseBuilder;
|
||||||
OIDCAdvancedConfigWrapper cfg = OIDCAdvancedConfigWrapper.fromClientModel(clientModel);
|
OIDCAdvancedConfigWrapper cfg = OIDCAdvancedConfigWrapper.fromClientModel(clientModel);
|
||||||
|
|
|
@ -90,7 +90,7 @@ public abstract class AbstractPairwiseSubMapper extends AbstractOIDCProtocolMapp
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setUserInfoTokenSubject(IDToken token, String pairwiseSub) {
|
protected void setUserInfoTokenSubject(IDToken token, String pairwiseSub) {
|
||||||
token.getOtherClaims().put("sub", pairwiseSub);
|
token.setSubject(pairwiseSub);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -50,6 +50,10 @@ public class UserInfoClientUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UserInfo testSuccessfulUserInfoResponse(Response response, String expectedUsername, String expectedEmail) {
|
public static UserInfo testSuccessfulUserInfoResponse(Response response, String expectedUsername, String expectedEmail) {
|
||||||
|
return testSuccessfulUserInfoResponse(response, null, expectedUsername, expectedEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserInfo testSuccessfulUserInfoResponse(Response response, String userId, String expectedUsername, String expectedEmail) {
|
||||||
Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
|
Assert.assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
|
||||||
Assert.assertEquals(response.getHeaderString(HttpHeaders.CONTENT_TYPE), MediaType.APPLICATION_JSON);
|
Assert.assertEquals(response.getHeaderString(HttpHeaders.CONTENT_TYPE), MediaType.APPLICATION_JSON);
|
||||||
|
|
||||||
|
@ -59,6 +63,9 @@ public class UserInfoClientUtil {
|
||||||
|
|
||||||
Assert.assertNotNull(userInfo);
|
Assert.assertNotNull(userInfo);
|
||||||
Assert.assertNotNull(userInfo.getSubject());
|
Assert.assertNotNull(userInfo.getSubject());
|
||||||
|
if (userId != null) {
|
||||||
|
Assert.assertEquals(userId, userInfo.getSubject());
|
||||||
|
}
|
||||||
Assert.assertEquals(expectedEmail, userInfo.getEmail());
|
Assert.assertEquals(expectedEmail, userInfo.getEmail());
|
||||||
Assert.assertEquals(expectedUsername, userInfo.getPreferredUsername());
|
Assert.assertEquals(expectedUsername, userInfo.getPreferredUsername());
|
||||||
return userInfo;
|
return userInfo;
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
package org.keycloak.testsuite.broker;
|
||||||
|
|
||||||
|
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||||
|
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
|
import org.keycloak.representations.IDToken;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||||
|
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||||
|
import org.keycloak.testsuite.util.ClientBuilder;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
|
||||||
|
public class KcOidcBrokerSubMatchIntrospectionest extends AbstractBrokerTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BrokerConfiguration getBrokerConfiguration() {
|
||||||
|
return new KcOidcBrokerConfiguration() {
|
||||||
|
@Override
|
||||||
|
public List<ClientRepresentation> createConsumerClients(SuiteContext suiteContext) {
|
||||||
|
List<ClientRepresentation> clients = new ArrayList<>(super.createConsumerClients(suiteContext));
|
||||||
|
|
||||||
|
clients.add(ClientBuilder.create().clientId("consumer-client")
|
||||||
|
.publicClient()
|
||||||
|
.redirectUris("http://localhost:8180/auth/realms/master/app/auth/*", "https://localhost:8543/auth/realms/master/app/auth/*")
|
||||||
|
.publicClient().build());
|
||||||
|
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ClientRepresentation> createProviderClients(SuiteContext suiteContext) {
|
||||||
|
List<ClientRepresentation> clients = super.createProviderClients(suiteContext);
|
||||||
|
List<ProtocolMapperRepresentation> mappers = new ArrayList<>();
|
||||||
|
|
||||||
|
mappers.add(createHardcodedClaim("sub-override", "sub", "overriden", "String", true, true));
|
||||||
|
|
||||||
|
clients.get(0).setProtocolMappers(mappers);
|
||||||
|
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void testLogInAsUserInIDP() {
|
||||||
|
driver.navigate().to(getAccountUrl(bc.consumerRealmName()));
|
||||||
|
|
||||||
|
oauth.realm(bc.consumerRealmName());
|
||||||
|
oauth.clientId("consumer-client");
|
||||||
|
|
||||||
|
log.debug("Clicking social " + bc.getIDPAlias());
|
||||||
|
loginPage.clickSocial(bc.getIDPAlias());
|
||||||
|
waitForPage(driver, "log in to", true);
|
||||||
|
|
||||||
|
log.debug("Logging in");
|
||||||
|
loginPage.login(bc.getUserLogin(), bc.getUserPassword());
|
||||||
|
errorPage.assertCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Override
|
||||||
|
public void loginWithExistingUser() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.idm.RoleRepresentation;
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.testsuite.util.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;
|
||||||
|
@ -594,7 +595,8 @@ public class UserInfoTest extends AbstractKeycloakTest {
|
||||||
.detail(Details.SIGNATURE_REQUIRED, "false")
|
.detail(Details.SIGNATURE_REQUIRED, "false")
|
||||||
.client(expectedClientId)
|
.client(expectedClientId)
|
||||||
.assertEvent();
|
.assertEvent();
|
||||||
UserInfoClientUtil.testSuccessfulUserInfoResponse(response, "test-user@localhost", "test-user@localhost");
|
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost").get(0);
|
||||||
|
UserInfoClientUtil.testSuccessfulUserInfoResponse(response, user.getId(), "test-user@localhost", "test-user@localhost");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testSuccessSignedResponse(Algorithm sigAlg) throws Exception {
|
private void testSuccessSignedResponse(Algorithm sigAlg) throws Exception {
|
||||||
|
|
Loading…
Reference in a new issue