KEYCLOAK-5350

This commit is contained in:
Bill Burke 2017-12-06 16:00:23 -05:00
parent 3a7fd9e732
commit 64f8d7ce25
10 changed files with 533 additions and 19 deletions

View file

@ -26,6 +26,7 @@ import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
@ -839,12 +840,22 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.setBrokerSessionId(userSession.getBrokerSessionId());
entity.setBrokerUserId(userSession.getBrokerUserId());
entity.setIpAddress(userSession.getIpAddress());
entity.setLoginUsername(userSession.getLoginUsername());
entity.setNotes(userSession.getNotes() == null ? new ConcurrentHashMap<>() : userSession.getNotes());
entity.setAuthenticatedClientSessions(new AuthenticatedClientSessionStore());
entity.setRememberMe(userSession.isRememberMe());
entity.setState(userSession.getState());
entity.setUser(userSession.getUser().getId());
if (userSession instanceof OfflineUserSessionModel) {
// this is a hack so that UserModel doesn't have to be available when offline token is imported.
// see related JIRA - KEYCLOAK-5350 and corresponding test
OfflineUserSessionModel oline = (OfflineUserSessionModel)userSession;
entity.setUser(oline.getUserId());
// NOTE: Hack
// We skip calling entity.setLoginUsername(userSession.getLoginUsername())
} else {
entity.setLoginUsername(userSession.getLoginUsername());
entity.setUser(userSession.getUser().getId());
}
entity.setStarted(userSession.getStarted());
entity.setLastSessionRefresh(userSession.getLastSessionRefresh());

View file

@ -176,7 +176,13 @@ public class UserSessionAdapter implements UserSessionModel {
@Override
public String getLoginUsername() {
return entity.getLoginUsername();
if (entity.getLoginUsername() == null) {
// this is a hack so that UserModel doesn't have to be available when offline token is imported.
// see related JIRA - KEYCLOAK-5350 and corresponding test
return getUser().getUsername();
} else {
return entity.getLoginUsername();
}
}
public String getIpAddress() {

View file

@ -17,6 +17,7 @@
package org.keycloak.models.jpa.session;
import org.jboss.logging.Logger;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
@ -42,6 +43,7 @@ import java.util.Map;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JpaUserSessionPersisterProvider implements UserSessionPersisterProvider {
private static final Logger logger = Logger.getLogger(JpaUserSessionPersisterProvider.class);
private final KeycloakSession session;
private final EntityManager em;
@ -205,15 +207,19 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
List<String> userSessionIds = new ArrayList<>();
for (PersistentUserSessionEntity entity : results) {
RealmModel realm = session.realms().getRealm(entity.getRealmId());
UserModel user = session.users().getUserById(entity.getUserId(), realm);
// Case when user was deleted in the meantime
if (user == null) {
onUserRemoved(realm, entity.getUserId());
return loadUserSessions(firstResult, maxResults, offline);
try {
UserModel user = session.users().getUserById(entity.getUserId(), realm);
// Case when user was deleted in the meantime
if (user == null) {
onUserRemoved(realm, entity.getUserId());
return loadUserSessions(firstResult, maxResults, offline);
}
} catch (Exception e) {
logger.debugv(e,"Failed to load user with id {0}", entity.getUserId());
}
result.add(toAdapter(realm, user, entity));
result.add(toAdapter(realm, entity));
userSessionIds.add(entity.getUserSessionId());
}
@ -247,14 +253,14 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
return result;
}
private PersistentUserSessionAdapter toAdapter(RealmModel realm, UserModel user, PersistentUserSessionEntity entity) {
private PersistentUserSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionEntity entity) {
PersistentUserSessionModel model = new PersistentUserSessionModel();
model.setUserSessionId(entity.getUserSessionId());
model.setLastSessionRefresh(entity.getLastSessionRefresh());
model.setData(entity.getData());
Map<String, AuthenticatedClientSessionModel> clientSessions = new HashMap<>();
return new PersistentUserSessionAdapter(model, realm, user, clientSessions);
return new PersistentUserSessionAdapter(session, model, realm, entity.getUserId(), clientSessions);
}
private PersistentAuthenticatedClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) {
@ -263,7 +269,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
PersistentClientSessionModel model = new PersistentClientSessionModel();
model.setClientId(entity.getClientId());
model.setUserSessionId(userSession.getId());
model.setUserId(userSession.getUser().getId());
model.setUserId(userSession.getUserId());
model.setTimestamp(entity.getTimestamp());
model.setData(entity.getData());
return new PersistentAuthenticatedClientSessionAdapter(model, realm, client, userSession);

View file

@ -0,0 +1,27 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models;
/**
* Hacked extension to UserSessionModel so that user id can be obtain directly so
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface OfflineUserSessionModel extends UserSessionModel {
public String getUserId();
}

View file

@ -19,7 +19,9 @@ package org.keycloak.models.session;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
@ -33,11 +35,14 @@ import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class PersistentUserSessionAdapter implements UserSessionModel {
public class PersistentUserSessionAdapter implements OfflineUserSessionModel {
private final PersistentUserSessionModel model;
private final UserModel user;
private UserModel user;
private String userId;
private String username;
private final RealmModel realm;
private KeycloakSession session;
private final Map<String, AuthenticatedClientSessionModel> authenticatedClientSessions;
private PersistentUserSessionData data;
@ -60,14 +65,16 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
this.model.setLastSessionRefresh(other.getLastSessionRefresh());
this.user = other.getUser();
this.userId = this.user.getId();
this.realm = other.getRealm();
this.authenticatedClientSessions = other.getAuthenticatedClientSessions();
}
public PersistentUserSessionAdapter(PersistentUserSessionModel model, RealmModel realm, UserModel user, Map<String, AuthenticatedClientSessionModel> clientSessions) {
public PersistentUserSessionAdapter(KeycloakSession session, PersistentUserSessionModel model, RealmModel realm, String userId, Map<String, AuthenticatedClientSessionModel> clientSessions) {
this.session = session;
this.model = model;
this.realm = realm;
this.user = user;
this.userId = userId;
this.authenticatedClientSessions = clientSessions;
}
@ -113,9 +120,17 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
@Override
public UserModel getUser() {
if (user == null) {
user = session.users().getUserById(userId, realm);
}
return user;
}
@Override
public String getUserId() {
return userId;
}
@Override
public RealmModel getRealm() {
return realm;
@ -123,7 +138,7 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
@Override
public String getLoginUsername() {
return user.getUsername();
return getUser().getUsername();
}
@Override

View file

@ -349,6 +349,13 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
throw new AssertionError("No type received within timeout");
}
}
public Event event() {
try {
return events.poll(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new AssertionError("No type received within timeout");
}
}
public Event assertEvent(Event actual) {
if (expected.getError() != null && !expected.getType().toString().endsWith("_ERROR")) {

View file

@ -0,0 +1,209 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.federation.storage;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputUpdater;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.user.ImportedUserValidation;
import org.keycloak.storage.user.UserLookupProvider;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class FailableHardcodedStorageProvider implements UserStorageProvider, UserLookupProvider, ImportedUserValidation, CredentialInputUpdater, CredentialInputValidator {
public static String username = "billb";
public static String password = "password";
public static String email = "billb@nowhere.com";
public static String first = "Bill";
public static String last = "Burke";
public static MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
public static boolean fail;
protected ComponentModel model;
protected KeycloakSession session;
protected boolean componentFail;
public FailableHardcodedStorageProvider(ComponentModel model, KeycloakSession session) {
this.model = model;
this.session = session;
componentFail = model.getConfig().getFirst("fail") != null && model.getConfig().getFirst("fail").equalsIgnoreCase("true");
}
@Override
public boolean supportsCredentialType(String credentialType) {
if (fail || componentFail) throw new RuntimeException("FORCED FAILURE");
return CredentialModel.PASSWORD.equals(credentialType);
}
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (fail || componentFail) throw new RuntimeException("FORCED FAILURE");
if (!(input instanceof UserCredentialModel)) return false;
if (!user.getUsername().equals(username)) throw new RuntimeException("UNKNOWN USER!");
if (input.getType().equals(UserCredentialModel.PASSWORD)) {
password = ((UserCredentialModel)input).getValue();
return true;
} else {
return false;
}
}
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
if (fail || componentFail) throw new RuntimeException("FORCED FAILURE");
}
@Override
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
if (fail || componentFail) throw new RuntimeException("FORCED FAILURE");
return Collections.EMPTY_SET;
}
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
if (fail || componentFail) throw new RuntimeException("FORCED FAILURE");
return CredentialModel.PASSWORD.equals(credentialType);
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (fail || componentFail) throw new RuntimeException("FORCED FAILURE");
if (!(input instanceof UserCredentialModel)) return false;
if (!user.getUsername().equals("billb")) throw new RuntimeException("UNKNOWN USER!");
if (input.getType().equals(UserCredentialModel.PASSWORD)) {
return password != null && password.equals( ((UserCredentialModel)input).getValue());
} else {
return false;
}
}
private static class Delegate extends UserModelDelegate {
public Delegate(UserModel delegate) {
super(delegate);
}
@Override
public void setUsername(String name) {
super.setUsername(name);
name = name;
}
@Override
public void setSingleAttribute(String name, String value) {
super.setSingleAttribute(name, value);
attributes.putSingle(name, value);
}
@Override
public void setAttribute(String name, List<String> values) {
super.setAttribute(name, values);
attributes.put(name, values);
}
@Override
public void removeAttribute(String name) {
super.removeAttribute(name);
attributes.remove(name);
}
@Override
public void setFirstName(String firstName) {
super.setFirstName(firstName);
first = firstName;
}
@Override
public void setLastName(String lastName) {
super.setLastName(lastName);
last = lastName;
}
@Override
public void setEmail(String em) {
super.setEmail(em);
email = em;
}
}
@Override
public UserModel validate(RealmModel realm, UserModel user) {
if (fail || componentFail) throw new RuntimeException("FORCED FAILURE");
return new Delegate(user);
}
@Override
public UserModel getUserById(String id, RealmModel realm) {
if (fail || componentFail) throw new RuntimeException("FORCED FAILURE");
throw new RuntimeException("THIS IMPORTS SHOULD NEVER BE CALLED");
}
@Override
public UserModel getUserByUsername(String uname, RealmModel realm) {
if (fail || componentFail) throw new RuntimeException("FORCED FAILURE");
if (!username.equals(uname)) return null;
UserModel local = session.userLocalStorage().getUserByUsername(uname, realm);
if (local != null && !model.getId().equals(local.getFederationLink())) {
throw new RuntimeException("local storage has wrong federation link");
}
if (local != null) return new Delegate(local);
local = session.userLocalStorage().addUser(realm, uname);
local.setEnabled(true);
local.setFirstName(first);
local.setLastName(last);
local.setEmail(email);
local.setFederationLink(model.getId());
for (String key : attributes.keySet()) {
List<String> values = attributes.get(key);
if (values == null) continue;
local.setAttribute(key, values);
}
return new Delegate(local);
}
@Override
public UserModel getUserByEmail(String email, RealmModel realm) {
if (fail || componentFail) throw new RuntimeException("FORCED FAILURE");
return null;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.federation.storage;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.storage.UserStorageProviderFactory;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class FailableHardcodedStorageProviderFactory implements UserStorageProviderFactory<FailableHardcodedStorageProvider> {
public static final String PROVIDER_ID = "failable-hardcoded-storage";
@Override
public FailableHardcodedStorageProvider create(KeycloakSession session, ComponentModel model) {
return new FailableHardcodedStorageProvider(model, session);
}
@Override
public String getId() {
return PROVIDER_ID;
}
static List<ProviderConfigProperty> OPTIONS = new LinkedList<>();
static {
ProviderConfigProperty prop = new ProviderConfigProperty("fail", "fail", "If on, provider will throw exception", ProviderConfigProperty.BOOLEAN_TYPE, "false");
OPTIONS.add(prop);
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return OPTIONS;
}
}

View file

@ -0,0 +1,177 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.federation.storage;
import org.junit.After;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialAuthentication;
import org.keycloak.credential.UserCredentialStoreManager;
import org.keycloak.events.Details;
import org.keycloak.events.Event;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.CachedUserModel;
import org.keycloak.models.cache.infinispan.UserAdapter;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.Constants;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UserStorageFailureTest {
public static ComponentModel memoryProvider = null;
@ClassRule
public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
UserStorageProviderModel model = new UserStorageProviderModel();
model.setName("failure");
model.setPriority(0);
model.setProviderId(FailableHardcodedStorageProviderFactory.PROVIDER_ID);
model.setParentId(appRealm.getId());
memoryProvider = appRealm.addComponentModel(model);
ClientModel offlineClient = appRealm.addClient("offline-client");
offlineClient.setEnabled(true);
offlineClient.setDirectAccessGrantsEnabled(true);
offlineClient.setSecret("secret");
HashSet<String> redirects = new HashSet<>();
redirects.add(Constants.AUTH_SERVER_ROOT + "/offline-client");
offlineClient.setRedirectUris(redirects);
offlineClient.setServiceAccountsEnabled(true);
offlineClient.setFullScopeAllowed(true);
UserModel serviceAccount = manager.getSession().users().addUser(appRealm, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + offlineClient.getClientId());
serviceAccount.setEnabled(true);
RoleModel role = appRealm.getRole("offline_access");
Assert.assertNotNull(role);
serviceAccount.grantRole(role);
serviceAccount.setServiceAccountClientLink(offlineClient.getClientId());
}
});
@Rule
public WebRule webRule = new WebRule(this);
@WebResource
protected OAuthClient oauth;
@WebResource
protected WebDriver driver;
@WebResource
protected AppPage appPage;
@WebResource
protected LoginPage loginPage;
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
// this is a hack so that UserModel doesn't have to be available when offline token is imported.
// see related JIRA - KEYCLOAK-5350 and corresponding test
/**
* KEYCLOAK-5350
*/
@Test
public void testKeycloak5350() {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
oauth.redirectUri(Constants.AUTH_SERVER_ROOT + "/offline-client");
oauth.doLogin("billb", "password");
Event loginEvent = events.expectLogin()
.client("offline-client")
.detail(Details.REDIRECT_URI, Constants.AUTH_SERVER_ROOT + "/offline-client")
.event();
final String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret");
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
String offlineTokenString = tokenResponse.getRefreshToken();
RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
events.clear();
FailableHardcodedStorageProvider.fail = true;
// restart server to make sure we can still boot if user storage is down
keycloakRule.restartServer();
// test that once user storage provider is available again we can still access the token.
FailableHardcodedStorageProvider.fail = false;
tokenResponse = oauth.doRefreshTokenRequest(offlineTokenString, "secret");
Assert.assertNotNull(tokenResponse.getAccessToken());
token = oauth.verifyToken(tokenResponse.getAccessToken());
offlineTokenString = tokenResponse.getRefreshToken();
offlineToken = oauth.verifyRefreshToken(offlineTokenString);
events.clear();
}
@After
public void resetTimeoffset() {
Time.setOffset(0);
}
//@Test
public void testIDE() throws Exception {
Thread.sleep(100000000);
}
}

View file

@ -1,3 +1,4 @@
org.keycloak.testsuite.federation.sync.SyncDummyUserFederationProviderFactory
org.keycloak.testsuite.federation.storage.UserPropertyFileStorageFactory
org.keycloak.testsuite.federation.storage.UserMapStorageFactory
org.keycloak.testsuite.federation.storage.UserMapStorageFactory
org.keycloak.testsuite.federation.storage.FailableHardcodedStorageProviderFactory