Skip to content

Commit

Permalink
KEYCLOAK-2339 Adding impersonator details to user session notes and s…
Browse files Browse the repository at this point in the history
…upporting built-in protocol mappers.
  • Loading branch information
Corey McGregor authored and mposolda committed Mar 8, 2019
1 parent 231db05 commit be77fd9
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.keycloak.models;

/**
* Session note metadata for impersonation details stored in user session notes.
*/
public enum ImpersonationSessionNote implements UserSessionNoteDescriptor {
IMPERSONATOR_ID("Impersonator User ID"),
IMPERSONATOR_USERNAME("Impersonator Username");

final String displayName;

ImpersonationSessionNote(String displayName) {
this.displayName = displayName;
}

public String getDisplayName() {
return displayName;
}

public String getTokenClaim() {
return this.toString().toLowerCase().replace('_', '.');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.keycloak.models;

/**
* Describes a user session note for simple and generic {@link ProtocolMapperModel} creation.
*/
public interface UserSessionNoteDescriptor {
/**
* @return A human-readable name for the session note. This should tell the end user what the session note contains
*/
String getDisplayName();

/**
* @return Token claim name/path to store the user session note value in.
*/
String getTokenClaim();
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
import java.util.Map;
import java.util.Set;

import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;

/**
* @author <a href="mailto:[email protected]">Bill Burke</a>
* @version $Revision: 1 $
Expand Down Expand Up @@ -173,6 +176,9 @@ public Map<String, ProtocolMapperModel> getBuiltinMappers() {

model = AllowedWebOriginsProtocolMapper.createClaimMapper(ALLOWED_WEB_ORIGINS);
builtins.put(ALLOWED_WEB_ORIGINS, model);

builtins.put(IMPERSONATOR_ID.getDisplayName(), UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_ID));
builtins.put(IMPERSONATOR_USERNAME.getDisplayName(), UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_USERNAME));
}

private static void createUserAttributeMapper(String name, String attrName, String claimName, String type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
import org.keycloak.authorization.util.Tokens;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ExchangeExternalToken;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.broker.provider.IdentityProviderFactory;
import org.keycloak.broker.provider.IdentityProviderMapper;
import org.keycloak.common.ClientConnection;
Expand All @@ -45,8 +45,8 @@
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext;
Expand Down Expand Up @@ -99,14 +99,17 @@
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import java.security.MessageDigest;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.security.MessageDigest;

import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;

/**
* @author <a href="mailto:[email protected]">Stian Thorgersen</a>
Expand Down Expand Up @@ -755,12 +758,16 @@ public Response tokenExchange() {
}
}

tokenUser = requestedUser;
tokenSession = session.sessions().createUserSession(realm, requestedUser, requestedUser.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
if (tokenUser != null) {
tokenSession.setNote(IMPERSONATOR_ID.toString(), tokenUser.getId());
tokenSession.setNote(IMPERSONATOR_USERNAME.toString(), tokenUser.getUsername());
}

tokenUser = requestedUser;
}

String requestedIssuer = formParams.getFirst(OAuth2Constants.REQUESTED_ISSUER);

if (requestedIssuer == null) {
return exchangeClientToClient(tokenUser, tokenSession);
} else {
Expand Down Expand Up @@ -825,7 +832,6 @@ protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel
}
}


if (targetClient.isConsentRequired()) {
event.detail(Details.REASON, "audience requires consent");
event.error(Errors.CONSENT_DENIED);
Expand Down Expand Up @@ -924,8 +930,6 @@ public Response exchangeExternalToken(String issuer, String subjectToken) {
userSession.setNote(IdentityProvider.FEDERATED_ACCESS_TOKEN, subjectToken);

return exchangeClientToClient(user, userSession);


}

protected UserModel importUserFromExternalIdentity(BrokeredIdentityContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionNoteDescriptor;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
Expand Down Expand Up @@ -101,4 +102,20 @@ public static ProtocolMapperModel createClaimMapper(String name,
mapper.setConfig(config);
return mapper;
}

/**
* For session notes defined using a {@link UserSessionNoteDescriptor} enum
*
* @param userSessionNoteDescriptor User session note descriptor for which to create a protocol mapper model.
*/
public static ProtocolMapperModel createUserSessionNoteMapper(UserSessionNoteDescriptor userSessionNoteDescriptor) {
return UserSessionNoteMapper.createClaimMapper(
userSessionNoteDescriptor.getDisplayName(),
userSessionNoteDescriptor.toString(),
userSessionNoteDescriptor.getTokenClaim(),
"String",
true, true
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.ImpersonationSessionNote;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
Expand Down Expand Up @@ -105,6 +106,9 @@
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;

/**
* Base resource for managing users
*
Expand Down Expand Up @@ -281,6 +285,13 @@ public Map<String, Object> impersonate() {
EventBuilder event = new EventBuilder(realm, session, clientConnection);

UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);

UserModel adminUser = auth.adminAuth().getUser();
String impersonatorId = adminUser.getId();
String impersonator = adminUser.getUsername();
userSession.setNote(IMPERSONATOR_ID.toString(), impersonatorId);
userSession.setNote(IMPERSONATOR_USERNAME.toString(), impersonator);

AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, session.getContext().getUri(), clientConnection);
URI redirect = AccountFormService.accountServiceApplicationPage(session.getContext().getUri()).build(realm.getName());
Map<String, Object> result = new HashMap<>();
Expand All @@ -289,8 +300,8 @@ public Map<String, Object> impersonate() {
event.event(EventType.IMPERSONATE)
.session(userSession)
.user(user)
.detail(Details.IMPERSONATOR_REALM,authenticatedRealm.getName())
.detail(Details.IMPERSONATOR, auth.adminAuth().getUser().getUsername()).success();
.detail(Details.IMPERSONATOR_REALM, authenticatedRealm.getName())
.detail(Details.IMPERSONATOR, impersonator).success();

return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@

package org.keycloak.testsuite.admin;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.Config;
Expand All @@ -35,6 +39,10 @@
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.models.ImpersonationConstants;
import org.keycloak.models.ImpersonationSessionNote;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
Expand All @@ -46,24 +54,24 @@
import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.CredentialBuilder;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.Cookie;

import javax.ws.rs.ClientErrorException;
import javax.ws.rs.client.Client;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.openqa.selenium.Cookie;

import static org.hamcrest.Matchers.containsString;

Expand All @@ -74,6 +82,25 @@
*/
public class ImpersonationTest extends AbstractKeycloakTest {

static class UserSessionNotesHolder {
private Map<String, String> notes = new HashMap<>();

public UserSessionNotesHolder() {
}

public UserSessionNotesHolder(final Map<String, String> notes) {
this.notes = notes;
}

public void setNotes(final Map<String, String> notes) {
this.notes = notes;
}

public Map<String, String> getNotes() {
return notes;
}
}

@Rule
public AssertEvents events = new AssertEvents(this);

Expand All @@ -85,6 +112,12 @@ public class ImpersonationTest extends AbstractKeycloakTest {

private String impersonatedUserId;

@Deployment
public static WebArchive deploy() {
return RunOnServerDeployment.create(ImpersonationTest.class, AbstractKeycloakTest.class, UserResource.class)
.addPackages(true, "org.keycloak.testsuite", "org.keycloak.admin.client", "org.openqa.selenium");
}

@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmBuilder realm = RealmBuilder.create().name("test").testEventListener();
Expand Down Expand Up @@ -234,8 +267,21 @@ private Cookie impersonate(Keycloak adminClient, String admin, String adminRealm
.detail(Details.IMPERSONATOR_REALM, adminRealm)
.client((String) null).assertEvent();

NewCookie cookie = response.getCookies().get(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE);
// Fetch user session notes
final String userId = impersonatedUserId;
final UserSessionNotesHolder notesHolder = testingClient.server("test").fetch(session -> {
final RealmModel realm = session.realms().getRealmByName("test");
final UserModel user = session.users().getUserById(userId, realm);
final UserSessionModel userSession = session.sessions().getUserSessions(realm, user).get(0);
return new UserSessionNotesHolder(userSession.getNotes());
}, UserSessionNotesHolder.class);

// Check impersonation details
final Map<String, String> notes = notesHolder.getNotes();
Assert.assertNotNull(notes.get(ImpersonationSessionNote.IMPERSONATOR_ID.toString()));
Assert.assertEquals(admin, notes.get(ImpersonationSessionNote.IMPERSONATOR_USERNAME.toString()));

NewCookie cookie = response.getCookies().get(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE);
Assert.assertNotNull(cookie);

return new Cookie(cookie.getName(), cookie.getValue(), cookie.getDomain(), cookie.getPath(), cookie.getExpiry(), cookie.isSecure(), cookie.isHttpOnly());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.RealmRepresentation;
Expand All @@ -57,8 +59,12 @@
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Map;

import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;

/**
Expand Down Expand Up @@ -110,6 +116,8 @@ public static void setupRealm(KeycloakSession session) {
clientExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
clientExchanger.setFullScopeAllowed(false);
clientExchanger.addScopeMapping(impersonateRole);
clientExchanger.addProtocolMapper(UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_ID));
clientExchanger.addProtocolMapper(UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_USERNAME));


ClientModel directExchanger = realm.addClient("direct-exchanger");
Expand Down Expand Up @@ -302,8 +310,15 @@ public void testImpersonation() throws Exception {
AccessToken exchangedToken = verifier.parse().getToken();
Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor());
Assert.assertNull(exchangedToken.getAudience());
Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user");
Assert.assertEquals("impersonated-user", exchangedToken.getPreferredUsername());
Assert.assertNull(exchangedToken.getRealmAccess());

Object impersonatorRaw = exchangedToken.getOtherClaims().get("impersonator");
Assert.assertThat(impersonatorRaw, instanceOf(Map.class));
Map impersonatorClaim = (Map) impersonatorRaw;

Assert.assertEquals(token.getSubject(), impersonatorClaim.get("id"));
Assert.assertEquals("user", impersonatorClaim.get("username"));
}

// client-exchanger can impersonate from token "user" to user "impersonated-user" and to "target" client
Expand Down Expand Up @@ -449,7 +464,7 @@ public void testDirectImpersonation() throws Exception {
.param(OAuth2Constants.AUDIENCE, "target")

));
org.junit.Assert.assertEquals(403, response.getStatus());
Assert.assertEquals(403, response.getStatus());
response.close();
}

Expand All @@ -464,7 +479,7 @@ public void testDirectImpersonation() throws Exception {
.param(OAuth2Constants.AUDIENCE, "target")

));
org.junit.Assert.assertTrue(response.getStatus() >= 400);
Assert.assertTrue(response.getStatus() >= 400);
response.close();
}

Expand Down

0 comments on commit be77fd9

Please sign in to comment.