authenticator example updated
This commit is contained in:
parent
a1bcd0651d
commit
ff1326fe35
24 changed files with 175 additions and 136 deletions
|
@ -43,7 +43,7 @@
|
||||||
<xsl:copy>
|
<xsl:copy>
|
||||||
<xsl:apply-templates select="node()[name(.)='datasource']"/>
|
<xsl:apply-templates select="node()[name(.)='datasource']"/>
|
||||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" use-java-context="true">
|
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" use-java-context="true">
|
||||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
|
<xa-datasource-property name="URL">jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</xa-datasource-property>
|
||||||
<driver>h2</driver>
|
<driver>h2</driver>
|
||||||
<security>
|
<security>
|
||||||
<user-name>sa</user-name>
|
<user-name>sa</user-name>
|
||||||
|
|
|
@ -1,31 +1,23 @@
|
||||||
Example Custom Authenticator
|
Example Custom Authenticator
|
||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
This is an example of defining a custom Authenticator and Required action. This example is explained in the user documentation
|
1. First, Keycloak must be running.
|
||||||
of Keycloak. To deploy, build this directory then take the jar and copy it to providers directory. Alternatively you can deploy as a module by running:
|
|
||||||
|
|
||||||
KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.secret-question --resources=target/authenticator-required-action-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-services,org.jboss.resteasy.resteasy-jaxrs,javax.ws.rs.api"
|
2. Execute the follow. This will build the example and deploy it
|
||||||
|
|
||||||
Then registering the provider by editing `standalone/configuration/standalone.xml` and adding the module to the providers element:
|
$ mvn clean install wildfly:deploy
|
||||||
|
|
||||||
<providers>
|
3. Copy the secret-question.ftl and secret-question-config.ftl files to the themes/base/login directory.
|
||||||
...
|
|
||||||
<provider>module:org.keycloak.examples.secret-question</provider>
|
|
||||||
</providers>
|
|
||||||
|
|
||||||
You then have to copy the secret-question.ftl and secret-question-config.ftl files to the themes/base/login directory.
|
4. Login to admin console. Hit browser refresh if you are already logged in so that the new providers show up.
|
||||||
|
|
||||||
After you do all this, you then have to reboot keycloak. When reboot is complete, you will need to log into
|
5. Go to the Authentication menu item and go to the Flow tab, you will be able to view the currently
|
||||||
the admin console to create a new flow with your new authenticator.
|
defined flows. You cannot modify an built in flows, so, to add the Authenticator you
|
||||||
|
have to copy an existing flow or create your own. Copy the "Browser" flow.
|
||||||
|
|
||||||
If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently
|
6. In your copy, click the "Actions" menu item and "Add Execution". Pick Secret Question
|
||||||
defined flows. You cannot modify an built in flows, so, to add the Authenticator you
|
|
||||||
have to copy an existing flow or create your own.
|
|
||||||
|
|
||||||
Next you have to register your required action.
|
7. Next you have to register the required action that you created. Click on the Required Actions tab in the Authenticaiton menu.
|
||||||
Click on the Required Actions tab. Click on the Register button and choose your new Required Action.
|
Click on the Register button and choose your new Required Action.
|
||||||
Your new required action should now be displayed and enabled in the required actions list.
|
Your new required action should now be displayed and enabled in the required actions list.
|
||||||
|
|
||||||
I'm hoping the UI is intuitive enough so that you
|
|
||||||
can figure out for yourself how to create a flow and add the Authenticator and Required Action. We're looking to add a screencast
|
|
||||||
to show this in action.
|
|
||||||
|
|
|
@ -64,6 +64,13 @@
|
||||||
<target>1.8</target>
|
<target>1.8</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.wildfly.plugins</groupId>
|
||||||
|
<artifactId>wildfly-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<skip>false</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -17,18 +17,20 @@
|
||||||
|
|
||||||
package org.keycloak.examples.authenticator;
|
package org.keycloak.examples.authenticator;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.spi.HttpResponse;
|
||||||
|
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
import org.keycloak.authentication.AuthenticationFlowError;
|
import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
|
import org.keycloak.common.util.ServerCookie;
|
||||||
import org.keycloak.models.AuthenticatorConfigModel;
|
import org.keycloak.models.AuthenticatorConfigModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserCredentialModel;
|
import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserCredentialValueModel;
|
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.services.util.CookieHelper;
|
|
||||||
|
|
||||||
import javax.ws.rs.core.Cookie;
|
import javax.ws.rs.core.Cookie;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -87,13 +89,22 @@ public class SecretQuestionAuthenticator implements Authenticator {
|
||||||
|
|
||||||
}
|
}
|
||||||
URI uri = context.getUriInfo().getBaseUriBuilder().path("realms").path(context.getRealm().getName()).build();
|
URI uri = context.getUriInfo().getBaseUriBuilder().path("realms").path(context.getRealm().getName()).build();
|
||||||
CookieHelper.addCookie("SECRET_QUESTION_ANSWERED", "true",
|
addCookie("SECRET_QUESTION_ANSWERED", "true",
|
||||||
uri.getRawPath(),
|
uri.getRawPath(),
|
||||||
null, null,
|
null, null,
|
||||||
maxCookieAge,
|
maxCookieAge,
|
||||||
false, true);
|
false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) {
|
||||||
|
HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class);
|
||||||
|
StringBuffer cookieBuf = new StringBuffer();
|
||||||
|
ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure, httpOnly);
|
||||||
|
String cookie = cookieBuf.toString();
|
||||||
|
response.getOutputHeaders().add(HttpHeaders.SET_COOKIE, cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
protected boolean validateAnswer(AuthenticationFlowContext context) {
|
protected boolean validateAnswer(AuthenticationFlowContext context) {
|
||||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
String secret = formData.getFirst("secret_answer");
|
String secret = formData.getFirst("secret_answer");
|
||||||
|
|
|
@ -110,7 +110,7 @@ public class SecretQuestionCredentialProvider implements CredentialProvider, Cre
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCache(RealmModel realm, CachedUserModel user) {
|
public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) {
|
||||||
List<CredentialModel> creds = session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION);
|
List<CredentialModel> creds = session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION);
|
||||||
if (!creds.isEmpty()) user.getCachedWith().put(CACHE_KEY, creds.get(0));
|
if (!creds.isEmpty()) user.getCachedWith().put(CACHE_KEY, creds.get(0));
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,16 +18,21 @@ package org.keycloak.examples.storage.user;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.component.ComponentModel;
|
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.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.models.UserCredentialModel;
|
import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.cache.CachedUserModel;
|
||||||
|
import org.keycloak.models.cache.OnUserCache;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.storage.StorageId;
|
import org.keycloak.storage.StorageId;
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.storage.user.UserCredentialValidatorProvider;
|
|
||||||
import org.keycloak.storage.user.UserLookupProvider;
|
import org.keycloak.storage.user.UserLookupProvider;
|
||||||
import org.keycloak.storage.user.UserQueryProvider;
|
import org.keycloak.storage.user.UserQueryProvider;
|
||||||
import org.keycloak.storage.user.UserRegistrationProvider;
|
import org.keycloak.storage.user.UserRegistrationProvider;
|
||||||
|
@ -52,9 +57,13 @@ import java.util.Map;
|
||||||
public class EjbExampleUserStorageProvider implements UserStorageProvider,
|
public class EjbExampleUserStorageProvider implements UserStorageProvider,
|
||||||
UserLookupProvider,
|
UserLookupProvider,
|
||||||
UserRegistrationProvider,
|
UserRegistrationProvider,
|
||||||
UserCredentialValidatorProvider,
|
UserQueryProvider,
|
||||||
UserQueryProvider {
|
CredentialInputUpdater,
|
||||||
|
CredentialInputValidator,
|
||||||
|
OnUserCache
|
||||||
|
{
|
||||||
private static final Logger logger = Logger.getLogger(EjbExampleUserStorageProvider.class);
|
private static final Logger logger = Logger.getLogger(EjbExampleUserStorageProvider.class);
|
||||||
|
public static final String PASSWORD_CACHE_KEY = UserAdapter.class.getName() + ".password";
|
||||||
|
|
||||||
@PersistenceContext
|
@PersistenceContext
|
||||||
protected EntityManager em;
|
protected EntityManager em;
|
||||||
|
@ -150,18 +159,69 @@ public class EjbExampleUserStorageProvider implements UserStorageProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List<UserCredentialModel> input) {
|
public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) {
|
||||||
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
|
String password = ((UserAdapter)delegate).getPassword();
|
||||||
// If we override getCredentialsDirectly/updateCredentialsDirectly
|
if (password != null) {
|
||||||
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
|
user.getCachedWith().put(PASSWORD_CACHE_KEY, password);
|
||||||
// If you don't like this workaround, you can query the database every time to validate the password
|
|
||||||
for (UserCredentialModel cred : input) {
|
|
||||||
if (!UserCredentialModel.PASSWORD.equals(cred.getType())) return false;
|
|
||||||
if (!cred.getValue().equals(user.getFirstAttribute("password"))) return false;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsCredentialType(String credentialType) {
|
||||||
|
return CredentialModel.PASSWORD.equals(credentialType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
|
||||||
|
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
|
||||||
|
UserCredentialModel cred = (UserCredentialModel)input;
|
||||||
|
UserAdapter adapter = getUserAdapter(user);
|
||||||
|
adapter.setPassword(cred.getValue());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserAdapter getUserAdapter(UserModel user) {
|
||||||
|
UserAdapter adapter = null;
|
||||||
|
if (user instanceof CachedUserModel) {
|
||||||
|
adapter = (UserAdapter)((CachedUserModel)user).getDelegateForUpdate();
|
||||||
|
} else {
|
||||||
|
adapter = (UserAdapter)user;
|
||||||
|
}
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
|
||||||
|
if (!supportsCredentialType(credentialType)) return;
|
||||||
|
|
||||||
|
getUserAdapter(user).setPassword(null);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
|
||||||
|
return supportsCredentialType(credentialType) && getPassword(user) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
|
||||||
|
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
|
||||||
|
UserCredentialModel cred = (UserCredentialModel)input;
|
||||||
|
String password = getPassword(user);
|
||||||
|
return password != null && password.equals(cred.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword(UserModel user) {
|
||||||
|
String password = null;
|
||||||
|
if (user instanceof CachedUserModel) {
|
||||||
|
password = (String)((CachedUserModel)user).getCachedWith().get(PASSWORD_CACHE_KEY);
|
||||||
|
} else if (user instanceof UserAdapter) {
|
||||||
|
password = ((UserAdapter)user).getPassword();
|
||||||
|
}
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getUsersCount(RealmModel realm) {
|
public int getUsersCount(RealmModel realm) {
|
||||||
Object count = em.createNamedQuery("getUserCount")
|
Object count = em.createNamedQuery("getUserCount")
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.examples.storage.user;
|
package org.keycloak.examples.storage.user;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -32,6 +33,7 @@ import java.util.List;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class EjbExampleUserStorageProviderFactory implements UserStorageProviderFactory<EjbExampleUserStorageProvider> {
|
public class EjbExampleUserStorageProviderFactory implements UserStorageProviderFactory<EjbExampleUserStorageProvider> {
|
||||||
|
private static final Logger logger = Logger.getLogger(EjbExampleUserStorageProviderFactory.class);
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -56,4 +58,10 @@ public class EjbExampleUserStorageProviderFactory implements UserStorageProvider
|
||||||
public String getHelpText() {
|
public String getHelpText() {
|
||||||
return "JPA Example User Storage Provider";
|
return "JPA Example User Storage Provider";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
logger.info("<<<<<< Closing factory");
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ import java.util.Map;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class UserAdapter extends AbstractUserAdapterFederatedStorage {
|
public class UserAdapter extends AbstractUserAdapterFederatedStorage {
|
||||||
private static final Logger logger = Logger.getLogger(EjbExampleUserStorageProvider.class);
|
private static final Logger logger = Logger.getLogger(UserAdapter.class);
|
||||||
protected UserEntity entity;
|
protected UserEntity entity;
|
||||||
protected String keycloakId;
|
protected String keycloakId;
|
||||||
|
|
||||||
|
@ -44,6 +44,14 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
|
||||||
keycloakId = StorageId.keycloakId(model, entity.getId());
|
keycloakId = StorageId.keycloakId(model, entity.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return entity.getPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
entity.setPassword(password);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
return entity.getUsername();
|
return entity.getUsername();
|
||||||
|
@ -74,13 +82,6 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
|
||||||
public void setSingleAttribute(String name, String value) {
|
public void setSingleAttribute(String name, String value) {
|
||||||
if (name.equals("phone")) {
|
if (name.equals("phone")) {
|
||||||
entity.setPhone(value);
|
entity.setPhone(value);
|
||||||
} else if (name.equals("password")) {
|
|
||||||
// ignore
|
|
||||||
|
|
||||||
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
|
|
||||||
// If we override getCredentialsDirectly/updateCredentialsDirectly
|
|
||||||
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
|
|
||||||
// If you don't like this workaround, you can query the database every time to validate the password
|
|
||||||
} else {
|
} else {
|
||||||
super.setSingleAttribute(name, value);
|
super.setSingleAttribute(name, value);
|
||||||
}
|
}
|
||||||
|
@ -90,13 +91,6 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
|
||||||
public void removeAttribute(String name) {
|
public void removeAttribute(String name) {
|
||||||
if (name.equals("phone")) {
|
if (name.equals("phone")) {
|
||||||
entity.setPhone(null);
|
entity.setPhone(null);
|
||||||
} else if (name.equals("password")) {
|
|
||||||
// ignore
|
|
||||||
|
|
||||||
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
|
|
||||||
// If we override getCredentialsDirectly/updateCredentialsDirectly
|
|
||||||
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
|
|
||||||
// If you don't like this workaround, you can query the database every time to validate the password
|
|
||||||
} else {
|
} else {
|
||||||
super.removeAttribute(name);
|
super.removeAttribute(name);
|
||||||
}
|
}
|
||||||
|
@ -106,13 +100,6 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
|
||||||
public void setAttribute(String name, List<String> values) {
|
public void setAttribute(String name, List<String> values) {
|
||||||
if (name.equals("phone")) {
|
if (name.equals("phone")) {
|
||||||
entity.setPhone(values.get(0));
|
entity.setPhone(values.get(0));
|
||||||
} else if (name.equals("password")) {
|
|
||||||
// ignore
|
|
||||||
|
|
||||||
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
|
|
||||||
// If we override getCredentialsDirectly/updateCredentialsDirectly
|
|
||||||
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
|
|
||||||
// If you don't like this workaround, you can query the database every time to validate the password
|
|
||||||
} else {
|
} else {
|
||||||
super.setAttribute(name, values);
|
super.setAttribute(name, values);
|
||||||
}
|
}
|
||||||
|
@ -122,12 +109,6 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
|
||||||
public String getFirstAttribute(String name) {
|
public String getFirstAttribute(String name) {
|
||||||
if (name.equals("phone")) {
|
if (name.equals("phone")) {
|
||||||
return entity.getPhone();
|
return entity.getPhone();
|
||||||
} else if (name.equals("password")) {
|
|
||||||
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
|
|
||||||
// If we override getCredentialsDirectly/updateCredentialsDirectly
|
|
||||||
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
|
|
||||||
// If you don't like this workaround, you can query the database every time to validate the password
|
|
||||||
return entity.getPassword();
|
|
||||||
} else {
|
} else {
|
||||||
return super.getFirstAttribute(name);
|
return super.getFirstAttribute(name);
|
||||||
}
|
}
|
||||||
|
@ -139,12 +120,6 @@ public class UserAdapter extends AbstractUserAdapterFederatedStorage {
|
||||||
MultivaluedHashMap<String, String> all = new MultivaluedHashMap<>();
|
MultivaluedHashMap<String, String> all = new MultivaluedHashMap<>();
|
||||||
all.putAll(attrs);
|
all.putAll(attrs);
|
||||||
all.add("phone", entity.getPhone());
|
all.add("phone", entity.getPhone());
|
||||||
|
|
||||||
// having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons...
|
|
||||||
// If we override getCredentialsDirectly/updateCredentialsDirectly
|
|
||||||
// then the realm passsword policy will/may try and overwrite the plain text password with a hash.
|
|
||||||
// If you don't like this workaround, you can query the database every time to validate the password
|
|
||||||
all.add("password", entity.getPassword());
|
|
||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
xsi:schemaLocation="
|
xsi:schemaLocation="
|
||||||
http://java.sun.com/xml/ns/persistence
|
http://java.sun.com/xml/ns/persistence
|
||||||
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
|
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
|
||||||
<persistence-unit name="user-storage-jpa-example">
|
<persistence-unit name="user-storage-jpa-example" transaction-type="JTA">
|
||||||
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
|
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
|
||||||
|
|
||||||
<class>org.keycloak.examples.storage.user.UserEntity</class>
|
<class>org.keycloak.examples.storage.user.UserEntity</class>
|
||||||
|
|
|
@ -60,12 +60,14 @@ public class UserAdapter implements CachedUserModel {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void getDelegateForUpdate() {
|
@Override
|
||||||
|
public UserModel getDelegateForUpdate() {
|
||||||
if (updated == null) {
|
if (updated == null) {
|
||||||
userProviderCache.registerUserInvalidation(realm, cached);
|
userProviderCache.registerUserInvalidation(realm, cached);
|
||||||
updated = userProviderCache.getDelegate().getUserById(getId(), realm);
|
updated = userProviderCache.getDelegate().getUserById(getId(), realm);
|
||||||
if (updated == null) throw new IllegalStateException("Not found in database");
|
if (updated == null) throw new IllegalStateException("Not found in database");
|
||||||
}
|
}
|
||||||
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -168,21 +168,22 @@ public class UserCacheSession implements UserCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
CachedUser cached = cache.get(id, CachedUser.class);
|
CachedUser cached = cache.get(id, CachedUser.class);
|
||||||
|
UserModel delegate = null;
|
||||||
boolean wasCached = cached != null;
|
boolean wasCached = cached != null;
|
||||||
if (cached == null) {
|
if (cached == null) {
|
||||||
logger.trace("not cached");
|
logger.trace("not cached");
|
||||||
Long loaded = cache.getCurrentRevision(id);
|
Long loaded = cache.getCurrentRevision(id);
|
||||||
UserModel model = getDelegate().getUserById(id, realm);
|
delegate = getDelegate().getUserById(id, realm);
|
||||||
if (model == null) {
|
if (delegate == null) {
|
||||||
logger.trace("delegate returning null");
|
logger.trace("delegate returning null");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
cached = new CachedUser(loaded, realm, model);
|
cached = new CachedUser(loaded, realm, delegate);
|
||||||
cache.addRevisioned(cached, startupRevision);
|
cache.addRevisioned(cached, startupRevision);
|
||||||
}
|
}
|
||||||
logger.trace("returning new cache adapter");
|
logger.trace("returning new cache adapter");
|
||||||
UserAdapter adapter = new UserAdapter(cached, this, session, realm);
|
UserAdapter adapter = new UserAdapter(cached, this, session, realm);
|
||||||
if (!wasCached) onCache(realm, adapter);
|
if (!wasCached) onCache(realm, adapter, delegate);
|
||||||
managedUsers.put(id, adapter);
|
managedUsers.put(id, adapter);
|
||||||
return adapter;
|
return adapter;
|
||||||
}
|
}
|
||||||
|
@ -251,24 +252,24 @@ public class UserCacheSession implements UserCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected UserAdapter getUserAdapter(RealmModel realm, String userId, Long loaded, UserModel model) {
|
protected UserAdapter getUserAdapter(RealmModel realm, String userId, Long loaded, UserModel delegate) {
|
||||||
CachedUser cached = cache.get(userId, CachedUser.class);
|
CachedUser cached = cache.get(userId, CachedUser.class);
|
||||||
boolean wasCached = cached != null;
|
boolean wasCached = cached != null;
|
||||||
if (cached == null) {
|
if (cached == null) {
|
||||||
cached = new CachedUser(loaded, realm, model);
|
cached = new CachedUser(loaded, realm, delegate);
|
||||||
cache.addRevisioned(cached, startupRevision);
|
cache.addRevisioned(cached, startupRevision);
|
||||||
}
|
}
|
||||||
UserAdapter adapter = new UserAdapter(cached, this, session, realm);
|
UserAdapter adapter = new UserAdapter(cached, this, session, realm);
|
||||||
if (!wasCached) {
|
if (!wasCached) {
|
||||||
onCache(realm, adapter);
|
onCache(realm, adapter, delegate);
|
||||||
}
|
}
|
||||||
return adapter;
|
return adapter;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onCache(RealmModel realm, UserAdapter adapter) {
|
private void onCache(RealmModel realm, UserAdapter adapter, UserModel delegate) {
|
||||||
((OnUserCache)getDelegate()).onCache(realm, adapter);
|
((OnUserCache)getDelegate()).onCache(realm, adapter, delegate);
|
||||||
((OnUserCache)session.userCredentialManager()).onCache(realm, adapter);
|
((OnUserCache)session.userCredentialManager()).onCache(realm, adapter, delegate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -18,9 +18,7 @@
|
||||||
package org.keycloak.models;
|
package org.keycloak.models;
|
||||||
|
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.credential.CredentialInput;
|
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
import org.keycloak.storage.user.UserCredentialValidatorProvider;
|
|
||||||
import org.keycloak.storage.user.UserLookupProvider;
|
import org.keycloak.storage.user.UserLookupProvider;
|
||||||
import org.keycloak.storage.user.UserQueryProvider;
|
import org.keycloak.storage.user.UserQueryProvider;
|
||||||
import org.keycloak.storage.user.UserRegistrationProvider;
|
import org.keycloak.storage.user.UserRegistrationProvider;
|
||||||
|
|
|
@ -21,10 +21,24 @@ import org.keycloak.models.UserModel;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Cached users will implement this interface
|
||||||
|
*
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public interface CachedUserModel extends UserModel {
|
public interface CachedUserModel extends UserModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates the cache for this user and returns a delegate that represents the actual data provider
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
UserModel getDelegateForUpdate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the cache for this user
|
||||||
|
*
|
||||||
|
*/
|
||||||
void invalidate();
|
void invalidate();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -17,11 +17,12 @@
|
||||||
package org.keycloak.models.cache;
|
package org.keycloak.models.cache;
|
||||||
|
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public interface OnUserCache {
|
public interface OnUserCache {
|
||||||
void onCache(RealmModel realm, CachedUserModel user);
|
void onCache(RealmModel realm, CachedUserModel user, UserModel delegate);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.storage.user;
|
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.UserCredentialModel;
|
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
|
||||||
* @version $Revision: 1 $
|
|
||||||
*/
|
|
||||||
public interface UserCredentialValidatorProvider {
|
|
||||||
boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List<UserCredentialModel> input);
|
|
||||||
}
|
|
|
@ -53,7 +53,7 @@ public class OTPCredentialProvider implements CredentialProvider, CredentialInpu
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCache(RealmModel realm, CachedUserModel user) {
|
public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) {
|
||||||
List<CredentialModel> creds = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP);
|
List<CredentialModel> creds = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP);
|
||||||
user.getCachedWith().put(OTPCredentialProvider.class.getName() + "." + CredentialModel.TOTP, creds);
|
user.getCachedWith().put(OTPCredentialProvider.class.getName() + "." + CredentialModel.TOTP, creds);
|
||||||
|
|
||||||
|
|
|
@ -206,7 +206,7 @@ public class PasswordCredentialProvider implements CredentialProvider, Credentia
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCache(RealmModel realm, CachedUserModel user) {
|
public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) {
|
||||||
List<CredentialModel> passwords = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD);
|
List<CredentialModel> passwords = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD);
|
||||||
if (passwords != null) {
|
if (passwords != null) {
|
||||||
user.getCachedWith().put(PASSWORD_CACHE_KEY, passwords);
|
user.getCachedWith().put(PASSWORD_CACHE_KEY, passwords);
|
||||||
|
|
|
@ -254,10 +254,10 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCache(RealmModel realm, CachedUserModel user) {
|
public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) {
|
||||||
List<OnUserCache> credentialProviders = getCredentialProviders(realm, OnUserCache.class);
|
List<OnUserCache> credentialProviders = getCredentialProviders(realm, OnUserCache.class);
|
||||||
for (OnUserCache validator : credentialProviders) {
|
for (OnUserCache validator : credentialProviders) {
|
||||||
validator.onCache(realm, user);
|
validator.onCache(realm, user, delegate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,6 +136,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void undeploy(ProviderManager pm) {
|
public void undeploy(ProviderManager pm) {
|
||||||
|
logger.debug("undeploy");
|
||||||
// we make a copy to avoid concurrent access exceptions
|
// we make a copy to avoid concurrent access exceptions
|
||||||
Map<Class<? extends Provider>, Map<String, ProviderFactory>> copy = getFactoriesCopy();
|
Map<Class<? extends Provider>, Map<String, ProviderFactory>> copy = getFactoriesCopy();
|
||||||
MultivaluedHashMap<Class<? extends Provider>, ProviderFactory> factories = pm.getLoadedFactories();
|
MultivaluedHashMap<Class<? extends Provider>, ProviderFactory> factories = pm.getLoadedFactories();
|
||||||
|
@ -144,6 +145,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
|
||||||
Map<String, ProviderFactory> registered = copy.get(entry.getKey());
|
Map<String, ProviderFactory> registered = copy.get(entry.getKey());
|
||||||
for (ProviderFactory factory : entry.getValue()) {
|
for (ProviderFactory factory : entry.getValue()) {
|
||||||
undeployed.add(factory);
|
undeployed.add(factory);
|
||||||
|
logger.debugv("undeploying {0} of id {1}", factory.getClass().getName(), factory.getId());
|
||||||
if (registered != null) {
|
if (registered != null) {
|
||||||
registered.remove(factory.getId());
|
registered.remove(factory.getId());
|
||||||
}
|
}
|
||||||
|
|
|
@ -570,6 +570,10 @@ public class AuthenticationManager {
|
||||||
Set<String> requiredActions) {
|
Set<String> requiredActions) {
|
||||||
for (String action : requiredActions) {
|
for (String action : requiredActions) {
|
||||||
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
|
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
|
||||||
|
if (model == null) {
|
||||||
|
logger.warnv("Could not find configuration for Required Action {0}, did you forget to register it?", action);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!model.isEnabled()) {
|
if (!model.isEnabled()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -566,15 +566,15 @@ public class UserStorageManager implements UserProvider, OnUserCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCache(RealmModel realm, CachedUserModel user) {
|
public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) {
|
||||||
if (StorageId.isLocalStorage(user)) {
|
if (StorageId.isLocalStorage(user)) {
|
||||||
if (session.userLocalStorage() instanceof OnUserCache) {
|
if (session.userLocalStorage() instanceof OnUserCache) {
|
||||||
((OnUserCache)session.userLocalStorage()).onCache(realm, user);
|
((OnUserCache)session.userLocalStorage()).onCache(realm, user, delegate);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Object provider = getStorageProvider(session, realm, StorageId.resolveProviderId(user));
|
Object provider = getStorageProvider(session, realm, StorageId.resolveProviderId(user));
|
||||||
if (provider != null && provider instanceof OnUserCache) {
|
if (provider != null && provider instanceof OnUserCache) {
|
||||||
((OnUserCache)provider).onCache(realm, user);
|
((OnUserCache)provider).onCache(realm, user, delegate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,12 +26,10 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.models.UserCredentialModel;
|
import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserCredentialValueModel;
|
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.storage.StorageId;
|
import org.keycloak.storage.StorageId;
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
|
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
|
||||||
import org.keycloak.storage.user.UserCredentialValidatorProvider;
|
|
||||||
import org.keycloak.storage.user.UserLookupProvider;
|
import org.keycloak.storage.user.UserLookupProvider;
|
||||||
import org.keycloak.storage.user.UserRegistrationProvider;
|
import org.keycloak.storage.user.UserRegistrationProvider;
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,6 @@ import org.keycloak.storage.StorageId;
|
||||||
import org.keycloak.storage.UserStorageProvider;
|
import org.keycloak.storage.UserStorageProvider;
|
||||||
import org.keycloak.storage.adapter.AbstractUserAdapter;
|
import org.keycloak.storage.adapter.AbstractUserAdapter;
|
||||||
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
|
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
|
||||||
import org.keycloak.storage.user.UserCredentialValidatorProvider;
|
|
||||||
import org.keycloak.storage.user.UserLookupProvider;
|
import org.keycloak.storage.user.UserLookupProvider;
|
||||||
import org.keycloak.storage.user.UserQueryProvider;
|
import org.keycloak.storage.user.UserQueryProvider;
|
||||||
|
|
||||||
|
|
|
@ -21,22 +21,22 @@
|
||||||
<extension-module>org.jboss.as.connector</extension-module>
|
<extension-module>org.jboss.as.connector</extension-module>
|
||||||
<subsystem xmlns="urn:jboss:domain:datasources:4.0">
|
<subsystem xmlns="urn:jboss:domain:datasources:4.0">
|
||||||
<datasources>
|
<datasources>
|
||||||
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
|
<xa-datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
|
||||||
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
|
<xa-datasource-property name="URL">jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</xa-datasource-property>
|
||||||
<driver>h2</driver>
|
<driver>h2</driver>
|
||||||
<security>
|
<security>
|
||||||
<user-name>sa</user-name>
|
<user-name>sa</user-name>
|
||||||
<password>sa</password>
|
<password>sa</password>
|
||||||
</security>
|
</security>
|
||||||
</datasource>
|
</xa-datasource>
|
||||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
|
<xa-datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
|
||||||
<?KEYCLOAK_DS_CONNECTION_URL?>
|
<xa-datasource-property name="URL"><?KEYCLOAK_DS_CONNECTION_URL?></xa-datasource-property>
|
||||||
<driver>h2</driver>
|
<driver>h2</driver>
|
||||||
<security>
|
<security>
|
||||||
<user-name>sa</user-name>
|
<user-name>sa</user-name>
|
||||||
<password>sa</password>
|
<password>sa</password>
|
||||||
</security>
|
</security>
|
||||||
</datasource>
|
</xa-datasource>
|
||||||
<drivers>
|
<drivers>
|
||||||
<driver name="h2" module="com.h2database.h2">
|
<driver name="h2" module="com.h2database.h2">
|
||||||
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
|
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
|
||||||
|
@ -46,12 +46,12 @@
|
||||||
</subsystem>
|
</subsystem>
|
||||||
<supplement name="default">
|
<supplement name="default">
|
||||||
<replacement placeholder="KEYCLOAK_DS_CONNECTION_URL">
|
<replacement placeholder="KEYCLOAK_DS_CONNECTION_URL">
|
||||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
|
jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE
|
||||||
</replacement>
|
</replacement>
|
||||||
</supplement>
|
</supplement>
|
||||||
<supplement name="domain">
|
<supplement name="domain">
|
||||||
<replacement placeholder="KEYCLOAK_DS_CONNECTION_URL">
|
<replacement placeholder="KEYCLOAK_DS_CONNECTION_URL">
|
||||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/../../shared-database/keycloak;AUTO_SERVER=TRUE</connection-url>
|
jdbc:h2:${jboss.server.data.dir}/../../shared-database/keycloak;AUTO_SERVER=TRUE
|
||||||
</replacement>
|
</replacement>
|
||||||
</supplement>
|
</supplement>
|
||||||
</config>
|
</config>
|
||||||
|
|
Loading…
Reference in a new issue