Merge pull request #1440 from patriot1burke/master

user impersonation
This commit is contained in:
Bill Burke 2015-07-10 21:21:39 -04:00
commit aeab25b7fe
24 changed files with 629 additions and 194 deletions

View file

@ -0,0 +1,45 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayInfo=social.displayInfo; section>
<#if section = "title">
${msg("imperonateTitle",(realm.name!''))}
<#elseif section = "header">
${msg("impersonateTitleHtml",(realm.name!''))}
<#elseif section = "form">
<form id="kc-form-login" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
<#if realmList??>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="realm" class="${properties.kcLabelClass!}">${msg("realmChoice")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<select class="${properties.kcInputClass!}" id="selectRealm" name="realm">
<#list realmList as r>
<option value="${r}">${r}</option>
</#list>
</select>
</div>
</div>
</#if>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input id="username" class="${properties.kcInputClass!}" name="username" value="" type="text" autofocus />
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}"></label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="impersonate" id="kc-impersonate" type="submit" value="${msg("doImpersonate")}"/>
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-cancel" type="submit" value="${msg("doCancel")}"/>
</div>
</div>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -9,6 +9,7 @@ doDecline=Decline
doContinue=Continue
doForgotPassword=Passwort vergessen?
doClickHere=hier klicken
doImpersonate=Impersonate
kerberosNotConfigured=Kerberos Not Configured
kerberosNotConfiguredTitle=Kerberos Not Configured
bypassKerberos=Your browser is not set up for Kerberos login. Please click continue to login in through other means
@ -24,6 +25,10 @@ loginOauthTitle=
loginOauthTitleHtml=Tempor\u00E4rer zugriff auf <strong>{0}</strong> angefordert von <strong>{1}</strong>.
loginTotpTitle=Mobile Authentifizierung Einrichten
loginProfileTitle=Benutzerkonto Informationen aktualisieren
impersonateTitle={0} Impersonate User
impersonateTitleHtml=<strong>{0}</strong> Impersonate User</strong>
unknownUser=Unknown user
realmChoice=Realm
oauthGrantTitle=OAuth gew\u00E4hren
oauthGrantTitleHtml=Tempor\u00E4rer zugriff auf <strong>{0}</strong> angefordert von
errorTitle=Es tut uns leid...

View file

@ -9,6 +9,7 @@ doAccept=Accept
doDecline=Decline
doForgotPassword=Forgot Password?
doClickHere=Click here
doImpersonate=Impersonate
kerberosNotConfigured=Kerberos Not Configured
kerberosNotConfiguredTitle=Kerberos Not Configured
bypassKerberosDetail=Either you are not logged in via Kerberos or your browser is not set up for Kerberos login. Please click continue to login in through other means
@ -17,6 +18,10 @@ registerWithTitle=Register with {0}
registerWithTitleHtml=Register with <strong>{0}</strong>
loginTitle=Log in to {0}
loginTitleHtml=Log in to <strong>{0}</strong>
impersonateTitle={0} Impersonate User
impersonateTitleHtml=<strong>{0}</strong> Impersonate User</strong>
realmChoice=Realm
unknownUser=Unknown user
loginTotpTitle=Mobile Authenticator Setup
loginProfileTitle=Update Account Information
oauthGrantTitle=OAuth Grant
@ -76,7 +81,6 @@ emailInstruction=Enter your username or email address and we will send you instr
copyCodeInstruction=Please copy this code and paste it into your application:
personalInfo=Personal Info:
role_admin=Admin
role_realm-admin=Realm Admin
role_create-realm=Create realm

View file

@ -9,6 +9,7 @@ doDecline=Decline
doContinue=Continue
doForgotPassword=Password Dimenticata?
doClickHere=Clicca qui
doImpersonate=Impersonate
bypassKerberos=Your browser is not set up for Kerberos login. Please click continue to login in through other means
kerberosNotSetUp=Kerberos is not set up. You cannot login.
kerberosNotConfigured=Kerberos Not Configured
@ -22,6 +23,10 @@ loginTitle=Accedi a {0}
loginTitleHtml=Accedi a <strong>{0}</strong>
loginTotpTitle=Configura Autenticazione Mobile
loginProfileTitle=Aggiorna Profilo
impersonateTitle={0} Impersonate User
impersonateTitleHtml=<strong>{0}</strong> Impersonate User</strong>
unknownUser=Unknown user
realmChoice=Realm
oauthGrantTitle=OAuth Grant
oauthGrantTitleHtml=Accesso temporaneo per <strong>{0}</strong> richiesto da
errorTitle=Siamo spiacenti...

View file

@ -9,6 +9,7 @@ doNo=N\u00E3o
doContinue=Continue
doForgotPassword=Esqueceu sua senha?
doClickHere=Clique aqui
doImpersonate=Impersonate
bypassKerberos=Your browser is not set up for Kerberos login. Please click continue to login in through other means
kerberosNotSetUp=Kerberos is not set up. You cannot login.
kerberosNotConfigured=Kerberos Not Configured
@ -20,6 +21,10 @@ registerWithTitle=Registre-se com {0}
registerWithTitleHtml=Registre-se com <strong>{0}</strong>
loginTitle=Entrar em {0}
loginTitleHtml=Entrar em <strong>{0}</strong>
impersonateTitle={0} Impersonate User
impersonateTitleHtml=<strong>{0}</strong> Impersonate User</strong>
unknownUser=Unknown user
realmChoice=Realm
loginTotpTitle=Configura\u00E7\u00E3o do autenticador mobile
loginProfileTitle=Atualiza\u00E7\u00E3o das Informa\u00E7\u00F5es da Conta
oauthGrantTitle=Concess\u00E3o OAuth

View file

@ -1,6 +1,7 @@
package org.keycloak.migration.migrators;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.ImpersonationServiceConstants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
@ -15,7 +16,6 @@ import java.util.List;
public class MigrateTo1_4_0 {
public static final ModelVersion VERSION = new ModelVersion("1.4.0");
public void migrate(KeycloakSession session) {
List<RealmModel> realms = session.realms().getRealms();
for (RealmModel realm : realms) {
@ -23,6 +23,7 @@ public class MigrateTo1_4_0 {
DefaultAuthenticationFlows.addFlows(realm);
DefaultRequiredActions.addActions(realm);
}
ImpersonationServiceConstants.setupImpersonationService(session, realm, session.getContext().getContextPath());
}

View file

@ -8,6 +8,7 @@ public interface Constants {
String ADMIN_CONSOLE_CLIENT_ID = "security-admin-console";
String ACCOUNT_MANAGEMENT_CLIENT_ID = "account";
String IMPERSONATION_SERVICE_CLIENT_ID = "impersonation";
String BROKER_SERVICE_CLIENT_ID = "broker";
String REALM_MANAGEMENT_CLIENT_ID = "realm-management";

View file

@ -0,0 +1,59 @@
package org.keycloak.models;
import org.keycloak.Config;
import org.keycloak.models.utils.KeycloakModelUtils;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ImpersonationServiceConstants {
public static String IMPERSONATION_ALLOWED = "impersonation";
public static void setupMasterRealmRole(RealmProvider model, RealmModel realm) {
RealmModel adminRealm;
RoleModel adminRole;
if (realm.getName().equals(Config.getAdminRealm())) {
adminRealm = realm;
adminRole = realm.getRole(AdminRoles.ADMIN);
} else {
adminRealm = model.getRealmByName(Config.getAdminRealm());
adminRole = adminRealm.getRole(AdminRoles.ADMIN);
}
ClientModel realmAdminApp = adminRealm.getClientByClientId(KeycloakModelUtils.getMasterRealmAdminApplicationClientId(realm));
RoleModel impersonationRole = realmAdminApp.addRole(IMPERSONATION_ALLOWED);
impersonationRole.setDescription("${role_" + IMPERSONATION_ALLOWED + "}");
adminRole.addCompositeRole(impersonationRole);
}
public static void setupRealmRole(RealmModel realm) {
if (realm.getName().equals(Config.getAdminRealm())) { return; } // don't need to do this for master realm
String realmAdminApplicationClientId = Constants.REALM_MANAGEMENT_CLIENT_ID;
ClientModel realmAdminApp = realm.getClientByClientId(realmAdminApplicationClientId);
RoleModel impersonationRole = realmAdminApp.addRole(IMPERSONATION_ALLOWED);
impersonationRole.setDescription("${role_" + IMPERSONATION_ALLOWED + "}");
RoleModel adminRole = realmAdminApp.getRole(AdminRoles.REALM_ADMIN);
adminRole.addCompositeRole(impersonationRole);
}
public static void setupImpersonationService(KeycloakSession session, RealmModel realm, String contextPath) {
ClientModel client = realm.getClientNameMap().get(Constants.IMPERSONATION_SERVICE_CLIENT_ID);
if (client == null) {
client = KeycloakModelUtils.createClient(realm, Constants.IMPERSONATION_SERVICE_CLIENT_ID);
client.setName("${client_" + Constants.IMPERSONATION_SERVICE_CLIENT_ID + "}");
client.setEnabled(true);
client.setFullScopeAllowed(false);
String base = contextPath + "/realms/" + realm.getName() + "/impersonate";
String redirectUri = base + "/*";
client.addRedirectUri(redirectUri);
client.setBaseUrl(base);
setupMasterRealmRole(session.realms(), realm);
setupRealmRole(realm);
}
}
}

View file

@ -10,6 +10,8 @@ import javax.ws.rs.core.UriInfo;
*/
public interface KeycloakContext {
String getContextPath();
UriInfo getUri();
HttpHeaders getRequestHeaders();

View file

@ -131,7 +131,7 @@ public class SamlService {
return ErrorPage.error(session, Messages.INVALID_REQUEST);
}
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
if (authResult == null) {
logger.warn("Unknown saml response.");
event.event(EventType.LOGOUT);
@ -354,7 +354,7 @@ public class SamlService {
}
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
if (authResult != null) {
String logoutBinding = getBindingType();
if ("true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING)))

View file

@ -21,7 +21,7 @@ public class CookieAuthenticator implements Authenticator {
@Override
public void authenticate(AuthenticatorContext context) {
AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(context.getSession(),
context.getRealm(), context.getUriInfo(), context.getConnection(), context.getHttpRequest().getHttpHeaders(), true);
context.getRealm(), true);
if (authResult == null) {
context.attempted();
} else {

View file

@ -117,7 +117,7 @@ public class LogoutEndpoint {
}
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
if (authResult != null) {
userSession = userSession != null ? userSession : authResult.getSession();
if (redirect != null) userSession.setNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI, redirect);

View file

@ -5,6 +5,7 @@ import org.keycloak.ClientConnection;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.RealmModel;
import org.keycloak.services.resources.KeycloakApplication;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriInfo;
@ -20,6 +21,12 @@ public class DefaultKeycloakContext implements KeycloakContext {
private ClientConnection connection;
@Override
public String getContextPath() {
KeycloakApplication app = ResteasyProviderFactory.getContextData(KeycloakApplication.class);
return app.getContextPath();
}
@Override
public UriInfo getUri() {
return ResteasyProviderFactory.getContextData(UriInfo.class);

View file

@ -172,7 +172,7 @@ public class Urls {
return realmBase(baseUri).path("{realm}").build(realmId).toString();
}
private static UriBuilder realmBase(URI baseUri) {
public static UriBuilder realmBase(URI baseUri) {
return UriBuilder.fromUri(baseUri).path(RealmsResource.class);
}

View file

@ -18,12 +18,12 @@ public class AppAuthManager extends AuthenticationManager {
protected static Logger logger = Logger.getLogger(AppAuthManager.class);
@Override
public AuthResult authenticateIdentityCookie(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
AuthResult authResult = super.authenticateIdentityCookie(session, realm, uriInfo, connection, headers);
public AuthResult authenticateIdentityCookie(KeycloakSession session, RealmModel realm) {
AuthResult authResult = super.authenticateIdentityCookie(session, realm);
if (authResult == null) return null;
// refresh the cookies!
createLoginCookie(realm, authResult.getUser(), authResult.getSession(), uriInfo, connection);
if (authResult.getSession().isRememberMe()) createRememberMeCookie(realm, authResult.getUser().getUsername(), uriInfo, connection);
createLoginCookie(realm, authResult.getUser(), authResult.getSession(), session.getContext().getUri(), session.getContext().getConnection());
if (authResult.getSession().isRememberMe()) createRememberMeCookie(realm, authResult.getUser().getUsername(), session.getContext().getUri(), session.getContext().getConnection());
return authResult;
}

View file

@ -359,21 +359,21 @@ public class AuthenticationManager {
CookieHelper.addCookie(cookieName, "", path, null, "Expiring cookie", 0, secureOnly, httpOnly);
}
public AuthResult authenticateIdentityCookie(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
return authenticateIdentityCookie(session, realm, uriInfo, connection, headers, true);
public AuthResult authenticateIdentityCookie(KeycloakSession session, RealmModel realm) {
return authenticateIdentityCookie(session, realm, true);
}
public static AuthResult authenticateIdentityCookie(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers, boolean checkActive) {
Cookie cookie = headers.getCookies().get(KEYCLOAK_IDENTITY_COOKIE);
public static AuthResult authenticateIdentityCookie(KeycloakSession session, RealmModel realm, boolean checkActive) {
Cookie cookie = session.getContext().getRequestHeaders().getCookies().get(KEYCLOAK_IDENTITY_COOKIE);
if (cookie == null || "".equals(cookie.getValue())) {
logger.debugv("Could not find cookie: {0}", KEYCLOAK_IDENTITY_COOKIE);
return null;
}
String tokenString = cookie.getValue();
AuthResult authResult = verifyIdentityToken(session, realm, uriInfo, connection, checkActive, tokenString, headers);
AuthResult authResult = verifyIdentityToken(session, realm, session.getContext().getUri(), session.getContext().getConnection(), checkActive, tokenString, session.getContext().getRequestHeaders());
if (authResult == null) {
expireIdentityCookie(realm, uriInfo, connection);
expireIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection());
return null;
}
authResult.getSession().setLastSessionRefresh(Time.currentTime());
@ -399,9 +399,9 @@ public class AuthenticationManager {
}
}
}
if (userSession.getState() != UserSessionModel.State.LOGGED_IN) userSession.setState(UserSessionModel.State.LOGGED_IN);
// refresh the cookies!
createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection);
if (userSession.getState() != UserSessionModel.State.LOGGED_IN) userSession.setState(UserSessionModel.State.LOGGED_IN);
if (userSession.isRememberMe()) createRememberMeCookie(realm, userSession.getUser().getUsername(), uriInfo, clientConnection);
LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod());
protocol.setRealm(realm)

View file

@ -9,6 +9,7 @@ import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientModel;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.Constants;
import org.keycloak.models.ImpersonationServiceConstants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
@ -88,6 +89,7 @@ public class RealmManager {
setupAccountManagement(realm);
setupBrokerService(realm);
setupAdminConsole(realm);
setupImpersonationService(realm);
setupAuthenticationFlows(realm);
setupRequiredActions(realm);
@ -233,6 +235,10 @@ public class RealmManager {
}
}
public void setupImpersonationService(RealmModel realm) {
ImpersonationServiceConstants.setupImpersonationService(session, realm, contextPath);
}
public void setupBrokerService(RealmModel realm) {
ClientModel client = realm.getClientNameMap().get(Constants.BROKER_SERVICE_CLIENT_ID);
if (client == null) {
@ -261,6 +267,8 @@ public class RealmManager {
setupMasterAdminManagement(realm);
if (!hasRealmAdminManagementClient(rep)) setupRealmAdminManagement(realm);
if (!hasAccountManagementClient(rep)) setupAccountManagement(realm);
if (!hasImpersonationServiceClient(rep)) setupImpersonationService(realm);
if (!hasBrokerClient(rep)) setupBrokerService(realm);
if (!hasAdminConsoleClient(rep)) setupAdminConsole(realm);
@ -297,6 +305,15 @@ public class RealmManager {
}
return false;
}
private boolean hasImpersonationServiceClient(RealmRepresentation rep) {
if (rep.getClients() == null) return false;
for (ClientRepresentation clientRep : rep.getClients()) {
if (clientRep.getClientId().equals(Constants.IMPERSONATION_SERVICE_CLIENT_ID)) {
return true;
}
}
return false;
}
private boolean hasBrokerClient(RealmRepresentation rep) {
if (rep.getClients() == null) return false;
for (ClientRepresentation clientRep : rep.getClients()) {

View file

@ -0,0 +1,246 @@
package org.keycloak.services.resources;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.AbstractOAuthClient;
import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.util.UriUtils;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.Set;
import java.util.UUID;
/**
* Helper class for securing local services. Provides login basics as well as CSRF check basics
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class AbstractSecuredLocalService {
private static final Logger logger = Logger.getLogger(AbstractSecuredLocalService.class);
protected final ClientModel client;
protected RealmModel realm;
@Context
protected UriInfo uriInfo;
@Context
protected HttpHeaders headers;
@Context
protected ClientConnection clientConnection;
protected String stateChecker;
@Context
protected KeycloakSession session;
@Context
protected HttpRequest request;
protected Auth auth;
public AbstractSecuredLocalService(RealmModel realm, ClientModel client) {
this.realm = realm;
this.client = client;
}
@Path("login-redirect")
@GET
public Response loginRedirect(@QueryParam("code") String code,
@QueryParam("state") String state,
@QueryParam("error") String error,
@QueryParam("path") String path,
@QueryParam("referrer") String referrer,
@Context HttpHeaders headers) {
try {
if (error != null) {
logger.debug("error from oauth");
throw new ForbiddenException("error");
}
if (path != null && !getValidPaths().contains(path)) {
throw new BadRequestException("Invalid path");
}
if (!realm.isEnabled()) {
logger.debug("realm not enabled");
throw new ForbiddenException();
}
if (!client.isEnabled()) {
logger.debug("account management app not enabled");
throw new ForbiddenException();
}
if (code == null) {
logger.debug("code not specified");
throw new BadRequestException("code not specified");
}
if (state == null) {
logger.debug("state not specified");
throw new BadRequestException("state not specified");
}
URI uri = getBaseRedirectUri();
URI redirectUri = path != null ? uri.resolve(path) : uri;
if (referrer != null) {
redirectUri = redirectUri.resolve("?referrer=" + referrer);
}
return Response.status(302).location(redirectUri).build();
} finally {
}
}
protected void updateCsrfChecks() {
Cookie cookie = headers.getCookies().get(AccountService.KEYCLOAK_STATE_CHECKER);
if (cookie != null) {
stateChecker = cookie.getValue();
} else {
stateChecker = UUID.randomUUID().toString();
String cookiePath = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
boolean secureOnly = realm.getSslRequired().isRequired(clientConnection);
CookieHelper.addCookie(AccountService.KEYCLOAK_STATE_CHECKER, stateChecker, cookiePath, null, null, -1, secureOnly, true);
}
}
protected abstract Set<String> getValidPaths();
/**
* Check to see if form post has sessionId hidden field and match it against the session id.
*
* @param formData
*/
protected void csrfCheck(final MultivaluedMap<String, String> formData) {
if (!auth.isCookieAuthenticated()) return;
String stateChecker = formData.getFirst("stateChecker");
if (!this.stateChecker.equals(stateChecker)) {
throw new ForbiddenException();
}
}
/**
* Check to see if form post has sessionId hidden field and match it against the session id.
*
*/
protected void csrfCheck(String stateChecker) {
if (!auth.isCookieAuthenticated()) return;
if (auth.getSession() == null) return;
if (!this.stateChecker.equals(stateChecker)) {
throw new ForbiddenException();
}
}
protected abstract URI getBaseRedirectUri();
protected Response login(String path) {
OAuthRedirect oauth = new OAuthRedirect();
String authUrl = OIDCLoginProtocolService.authUrl(uriInfo).build(realm.getName()).toString();
oauth.setAuthUrl(authUrl);
oauth.setClientId(client.getClientId());
UriBuilder uriBuilder = UriBuilder.fromUri(getBaseRedirectUri()).path("login-redirect");
if (path != null) {
uriBuilder.queryParam("path", path);
}
String referrer = uriInfo.getQueryParameters().getFirst("referrer");
if (referrer != null) {
uriBuilder.queryParam("referrer", referrer);
}
String referrerUri = uriInfo.getQueryParameters().getFirst("referrer_uri");
if (referrerUri != null) {
uriBuilder.queryParam("referrer_uri", referrerUri);
}
URI accountUri = uriBuilder.build(realm.getName());
oauth.setStateCookiePath(accountUri.getRawPath());
return oauth.redirect(uriInfo, accountUri.toString());
}
protected Response authenticateBrowser() {
AppAuthManager authManager = new AppAuthManager();
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm);
if (authResult != null) {
auth = new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), true);
} else {
return login(null);
}
// don't allow cors requests
// This is to prevent CSRF attacks.
String requestOrigin = UriUtils.getOrigin(uriInfo.getBaseUri());
String origin = headers.getRequestHeaders().getFirst("Origin");
if (origin != null && !requestOrigin.equals(origin)) {
throw new ForbiddenException();
}
if (!request.getHttpMethod().equals("GET")) {
String referrer = headers.getRequestHeaders().getFirst("Referer");
if (referrer != null && !requestOrigin.equals(UriUtils.getOrigin(referrer))) {
throw new ForbiddenException();
}
}
updateCsrfChecks();
return null;
}
static class OAuthRedirect extends AbstractOAuthClient {
/**
* closes client
*/
public void stop() {
}
public Response redirect(UriInfo uriInfo, String redirectUri) {
String state = getStateCode();
UriBuilder uriBuilder = UriBuilder.fromUri(authUrl)
.queryParam(OAuth2Constants.CLIENT_ID, clientId)
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
.queryParam(OAuth2Constants.STATE, state)
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE);
if (scope != null) {
uriBuilder.queryParam(OAuth2Constants.SCOPE, scope);
}
URI url = uriBuilder.build();
// todo httpOnly!
NewCookie cookie = new NewCookie(getStateCookieName(), state, getStateCookiePath(uriInfo), null, null, -1, isSecure);
logger.debug("NewCookie: " + cookie.toString());
logger.debug("Oauth Redirect to: " + url);
return Response.status(302)
.location(url)
.cookie(cookie).build();
}
private String getStateCookiePath(UriInfo uriInfo) {
if (stateCookiePath != null) return stateCookiePath;
return uriInfo.getBaseUri().getRawPath();
}
}
}

View file

@ -98,7 +98,7 @@ import java.util.UUID;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AccountService {
public class AccountService extends AbstractSecuredLocalService {
private static final Logger logger = Logger.getLogger(AccountService.class);
@ -128,34 +128,13 @@ public class AccountService {
public static final String KEYCLOAK_STATE_CHECKER = "KEYCLOAK_STATE_CHECKER";
private RealmModel realm;
@Context
private HttpRequest request;
@Context
protected HttpHeaders headers;
@Context
private UriInfo uriInfo;
@Context
private ClientConnection clientConnection;
@Context
private KeycloakSession session;
private final AppAuthManager authManager;
private final ClientModel client;
private EventBuilder event;
private AccountProvider account;
private Auth auth;
private EventStoreProvider eventStore;
private String stateChecker;
public AccountService(RealmModel realm, ClientModel client, EventBuilder event) {
this.realm = realm;
this.client = client;
super(realm, client);
this.event = event;
this.authManager = new AppAuthManager();
}
@ -169,18 +148,10 @@ public class AccountService {
if (authResult != null) {
auth = new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false);
} else {
authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers);
authResult = authManager.authenticateIdentityCookie(session, realm);
if (authResult != null) {
auth = new Auth(realm, authResult.getToken(), authResult.getUser(), client, authResult.getSession(), true);
Cookie cookie = headers.getCookies().get(KEYCLOAK_STATE_CHECKER);
if (cookie != null) {
stateChecker = cookie.getValue();
} else {
stateChecker = UUID.randomUUID().toString();
String cookiePath = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
boolean secureOnly = realm.getSslRequired().isRequired(clientConnection);
CookieHelper.addCookie(KEYCLOAK_STATE_CHECKER, stateChecker, cookiePath, null, null, -1, secureOnly, true);
}
updateCsrfChecks();
account.setStateChecker(stateChecker);
}
}
@ -236,10 +207,18 @@ public class AccountService {
return base;
}
public static UriBuilder accountServiceApplicationPage(UriInfo uriInfo) {
return accountServiceBaseUrl(uriInfo).path(AccountService.class, "applicationsPage");
}
public static UriBuilder accountServiceBaseUrl(UriBuilder base) {
return base.path(RealmsResource.class).path(RealmsResource.class, "getAccountService");
}
protected Set<String> getValidPaths() {
return AccountService.VALID_PATHS;
}
private Response forwardToPage(String path, AccountPages page) {
if (auth != null) {
try {
@ -367,33 +346,6 @@ public class AccountService {
return forwardToPage("applications", AccountPages.APPLICATIONS);
}
/**
* Check to see if form post has sessionId hidden field and match it against the session id.
*
* @param formData
*/
protected void csrfCheck(final MultivaluedMap<String, String> formData) {
if (!auth.isCookieAuthenticated()) return;
String stateChecker = formData.getFirst("stateChecker");
if (!this.stateChecker.equals(stateChecker)) {
throw new ForbiddenException();
}
}
/**
* Check to see if form post has sessionId hidden field and match it against the session id.
*
*/
protected void csrfCheck(String stateChecker) {
if (!auth.isCookieAuthenticated()) return;
if (auth.getSession() == null) return;
if (!this.stateChecker.equals(stateChecker)) {
throw new ForbiddenException();
}
}
/**
* Update account information.
*
@ -799,77 +751,9 @@ public class AccountService {
return RealmsResource.accountUrl(base).path(AccountService.class, "loginRedirect");
}
@Path("login-redirect")
@GET
public Response loginRedirect(@QueryParam("code") String code,
@QueryParam("state") String state,
@QueryParam("error") String error,
@QueryParam("path") String path,
@QueryParam("referrer") String referrer,
@Context HttpHeaders headers) {
try {
if (error != null) {
logger.debug("error from oauth");
throw new ForbiddenException("error");
}
if (path != null && !VALID_PATHS.contains(path)) {
throw new BadRequestException("Invalid path");
}
if (!realm.isEnabled()) {
logger.debug("realm not enabled");
throw new ForbiddenException();
}
if (!client.isEnabled()) {
logger.debug("account management app not enabled");
throw new ForbiddenException();
}
if (code == null) {
logger.debug("code not specified");
throw new BadRequestException("code not specified");
}
if (state == null) {
logger.debug("state not specified");
throw new BadRequestException("state not specified");
}
URI accountUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName());
URI redirectUri = path != null ? accountUri.resolve(path) : accountUri;
if (referrer != null) {
redirectUri = redirectUri.resolve("?referrer=" + referrer);
}
return Response.status(302).location(redirectUri).build();
} finally {
}
}
private Response login(String path) {
OAuthRedirect oauth = new OAuthRedirect();
String authUrl = OIDCLoginProtocolService.authUrl(uriInfo).build(realm.getName()).toString();
oauth.setAuthUrl(authUrl);
oauth.setClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
UriBuilder uriBuilder = Urls.accountPageBuilder(uriInfo.getBaseUri()).path(AccountService.class, "loginRedirect");
if (path != null) {
uriBuilder.queryParam("path", path);
}
String referrer = uriInfo.getQueryParameters().getFirst("referrer");
if (referrer != null) {
uriBuilder.queryParam("referrer", referrer);
}
String referrerUri = uriInfo.getQueryParameters().getFirst("referrer_uri");
if (referrerUri != null) {
uriBuilder.queryParam("referrer_uri", referrerUri);
}
URI accountUri = uriBuilder.build(realm.getName());
oauth.setStateCookiePath(accountUri.getRawPath());
return oauth.redirect(uriInfo, accountUri.toString());
@Override
protected URI getBaseRedirectUri() {
return Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName());
}
public static boolean isPasswordSet(UserModel user) {
@ -954,43 +838,4 @@ public class AccountService {
}
}
}
class OAuthRedirect extends AbstractOAuthClient {
/**
* closes client
*/
public void stop() {
}
public Response redirect(UriInfo uriInfo, String redirectUri) {
String state = getStateCode();
UriBuilder uriBuilder = UriBuilder.fromUri(authUrl)
.queryParam(OAuth2Constants.CLIENT_ID, clientId)
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
.queryParam(OAuth2Constants.STATE, state)
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE);
if (scope != null) {
uriBuilder.queryParam(OAuth2Constants.SCOPE, scope);
}
URI url = uriBuilder.build();
// todo httpOnly!
NewCookie cookie = new NewCookie(getStateCookieName(), state, getStateCookiePath(uriInfo), null, null, -1, isSecure);
logger.debug("NewCookie: " + cookie.toString());
logger.debug("Oauth Redirect to: " + url);
return Response.status(302)
.location(url)
.cookie(cookie).build();
}
private String getStateCookiePath(UriInfo uriInfo) {
if (stateCookiePath != null) return stateCookiePath;
return uriInfo.getBaseUri().getRawPath();
}
}
}

View file

@ -0,0 +1,177 @@
package org.keycloak.services.resources;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.Config;
import org.keycloak.events.EventBuilder;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.ImpersonationServiceConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ForbiddenException;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ImpersonationService extends AbstractSecuredLocalService {
public static final String UNKNOWN_USER_MESSAGE = "unknownUser";
private EventBuilder event;
public ImpersonationService(RealmModel realm, ClientModel client, EventBuilder event) {
super(realm, client);
this.event = event;
}
private static Set<String> VALID_PATHS = new HashSet<String>();
static {
}
@Override
protected Set<String> getValidPaths() {
return VALID_PATHS;
}
@Override
protected URI getBaseRedirectUri() {
return Urls.realmBase(uriInfo.getBaseUri()).path(RealmsResource.class, "getImpersonationService").build(realm.getName());
}
@GET
public Response impersonatePage() {
Response challenge = authenticateBrowser();
if (challenge != null) return challenge;
LoginFormsProvider page = page();
return renderPage(page);
}
protected LoginFormsProvider page() {
UserModel user = auth.getUser();
LoginFormsProvider page = session.getProvider(LoginFormsProvider.class)
.setActionUri(getBaseRedirectUri())
.setAttribute("stateChecker", stateChecker);
if (realm.getName().equals(Config.getAdminRealm())) {
List<String> realms = new LinkedList<>();
for (RealmModel possibleRealm : session.realms().getRealms()) {
ClientModel realmAdminApp = realm.getClientByClientId(KeycloakModelUtils.getMasterRealmAdminApplicationClientId(possibleRealm));
RoleModel role = realmAdminApp.getRole(ImpersonationServiceConstants.IMPERSONATION_ALLOWED);
if (user.hasRole(role)) {
realms.add(possibleRealm.getName());
}
}
if (realms.isEmpty()) {
throw new ForbiddenException("not authorized to access impersonation", ErrorPage.error(session, Messages.NO_ACCESS));
}
if (realms.size() > 1 || !realms.get(0).equals(realm.getName())) {
page.setAttribute("realmList", realms);
}
} else {
authorizeCurrentRealm();
} return page;
}
protected Response renderPage(LoginFormsProvider page) {
return page
.createForm("impersonate.ftl", new HashMap<String, Object>());
}
protected void authorizeMaster(String realmName) {
RealmModel possibleRealm = session.realms().getRealmByName(realmName);
if (possibleRealm == null) {
throw new NotFoundException("Could not find realm");
}
ClientModel realmAdminApp = realm.getClientByClientId(KeycloakModelUtils.getMasterRealmAdminApplicationClientId(possibleRealm));
RoleModel role = realmAdminApp.getRole(ImpersonationServiceConstants.IMPERSONATION_ALLOWED);
if (!auth.getUser().hasRole(role)) {
throw new ForbiddenException("not authorized to access impersonation", ErrorPage.error(session, Messages.NO_ACCESS));
}
}
private void authorizeCurrentRealm() {
UserModel user = auth.getUser();
String realmAdminApplicationClientId = Constants.REALM_MANAGEMENT_CLIENT_ID;
ClientModel realmAdminApp = realm.getClientByClientId(realmAdminApplicationClientId);
RoleModel role = realmAdminApp.getRole(ImpersonationServiceConstants.IMPERSONATION_ALLOWED);
if (!user.hasRole(role)) {
throw new ForbiddenException("not authorized to access impersonation", ErrorPage.error(session, Messages.NO_ACCESS));
}
}
@POST
public Response impersonate() {
Response challenge = authenticateBrowser();
if (challenge != null) return challenge;
MultivaluedMap<String, String> formData = request.getDecodedFormParameters();
String realmName = formData.getFirst("realm");
RealmModel chosenRealm = null;
if (realmName == null) {
chosenRealm = realm;
} else{
chosenRealm = session.realms().getRealmByName(realmName);
if (chosenRealm == null) {
throw new NotFoundException("Could not find realm");
}
}
if (realm.getName().equals(Config.getAdminRealm())) {
authorizeMaster(chosenRealm.getName());
} else {
if (realmName == null) authorizeCurrentRealm();
else {
throw new ForbiddenException("not authorized to access impersonation", ErrorPage.error(session, Messages.NO_ACCESS));
}
}
csrfCheck(formData);
if (formData.containsKey("cancel")) {
return renderPage(page());
}
String username = formData.getFirst(AuthenticationManager.FORM_USERNAME);
if (username == null) {
return renderPage(
page().setError(UNKNOWN_USER_MESSAGE)
);
}
UserModel user = session.users().getUserByUsername(username, chosenRealm);
if (user == null) {
user = session.users().getUserByEmail(username, chosenRealm);
}
if (user == null) {
return renderPage(
page().setError(UNKNOWN_USER_MESSAGE)
);
}
// if same realm logout before impersonation
if (chosenRealm.getId().equals(realm.getId())) {
AuthenticationManager.backchannelLogout(session, realm, auth.getSession(), uriInfo, clientConnection, headers, true);
}
UserSessionModel userSession = session.sessions().createUserSession(chosenRealm, user, username, clientConnection.getRemoteAddr(), "impersonate", false, null, null);
AuthenticationManager.createLoginCookie(chosenRealm, userSession.getUser(), userSession, uriInfo, clientConnection);
URI redirect = AccountService.accountServiceApplicationPage(uriInfo).build(chosenRealm.getName());
return Response.status(302).location(redirect).build();
}
}

View file

@ -61,13 +61,14 @@ public class KeycloakApplication extends Application {
public KeycloakApplication(@Context ServletContext context, @Context Dispatcher dispatcher) {
loadConfig();
this.contextPath = context.getContextPath();
this.sessionFactory = createSessionFactory();
dispatcher.getDefaultContextObjects().put(KeycloakApplication.class, this);
this.contextPath = context.getContextPath();
BruteForceProtector protector = new BruteForceProtector(sessionFactory);
dispatcher.getDefaultContextObjects().put(BruteForceProtector.class, protector);
ResteasyProviderFactory.pushContext(BruteForceProtector.class, protector); // for injection
ResteasyProviderFactory.pushContext(KeycloakApplication.class, this); // for injection
protector.start();
context.setAttribute(BruteForceProtector.class.getName(), protector);
context.setAttribute(KeycloakSessionFactory.class.getName(), this.sessionFactory);

View file

@ -4,7 +4,6 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.ClientConnection;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
@ -149,6 +148,22 @@ public class RealmsResource {
return accountService;
}
@Path("{realm}/impersonate")
public ImpersonationService getImpersonationService(final @PathParam("realm") String name) {
RealmModel realm = init(name);
ClientModel client = realm.getClientNameMap().get(Constants.IMPERSONATION_SERVICE_CLIENT_ID);
if (client == null || !client.isEnabled()) {
logger.debug("impersonate service not enabled");
throw new NotFoundException("impersonate service not enabled");
}
EventBuilder event = new EventBuilder(realm, session, clientConnection);
ImpersonationService impersonateService = new ImpersonationService(realm, client, event);
ResteasyProviderFactory.getInstance().injectProperties(impersonateService);
return impersonateService;
}
@Path("{realm}")
public PublicRealmResource getRealmResource(final @PathParam("realm") String name) {
RealmModel realm = init(name);

View file

@ -42,7 +42,7 @@ public class ClientTest extends AbstractClientTest {
@Test
public void getClients() {
assertNames(realm.clients().findAll(), "account", "realm-management", "security-admin-console", "broker");
assertNames(realm.clients().findAll(), "account", "realm-management", "security-admin-console", "broker", "impersonation");
}
private String createClient() {
@ -59,7 +59,7 @@ public class ClientTest extends AbstractClientTest {
String id = createClient();
assertNotNull(realm.clients().get(id));
assertNames(realm.clients().findAll(), "account", "realm-management", "security-admin-console", "broker", "my-app");
assertNames(realm.clients().findAll(), "account", "realm-management", "security-admin-console", "broker", "my-app", "impersonation");
}
@Test

View file

@ -86,7 +86,7 @@ public class ImportTest extends AbstractModelTest {
Assert.assertEquals(0, session.users().getFederatedIdentities(user, realm).size());
List<ClientModel> resources = realm.getClients();
Assert.assertEquals(7, resources.size());
Assert.assertEquals(8, resources.size());
// Test applications imported
ClientModel application = realm.getClientByClientId("Application");
@ -97,7 +97,7 @@ public class ImportTest extends AbstractModelTest {
Assert.assertNotNull(otherApp);
Assert.assertNull(nonExisting);
Map<String, ClientModel> clients = realm.getClientNameMap();
Assert.assertEquals(7, clients.size());
Assert.assertEquals(8, clients.size());
Assert.assertTrue(clients.values().contains(application));
Assert.assertTrue(clients.values().contains(otherApp));
Assert.assertTrue(clients.values().contains(accountApp));