Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save thomasdarimont/25f801c624f3f457fa772fadd19b1e15 to your computer and use it in GitHub Desktop.
Save thomasdarimont/25f801c624f3f457fa772fadd19b1e15 to your computer and use it in GitHub Desktop.
Keycloak RequiredAction for recording user information on login
# manual installation via jboss-cli
# idm login recording action
module add --name=de.tdlabs.idm.keycloak.idm-keycloak-ext-login-action \
--resources=/tmp/idm-keycloak-ext-login-action.jar \
--dependencies=org.keycloak.keycloak-common,org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.jboss.logging
{
"providers": [
"classpath:${jboss.home.dir}/providers/*",
"module:de.tdlabs.idm.keycloak.idm-keycloak-ext-login-action"
],
"admin": {
"realm": "master"
},
"eventsStore": {
"provider": "jpa",
"jpa": {
"exclude-events": [ "REFRESH_TOKEN" ]
}
},
"realm": {
"provider": "jpa"
},
"user": {
"provider": "jpa"
},
"userCache": {
"default" : {
"enabled": true
}
},
"userSessionPersister": {
"provider": "jpa"
},
"timer": {
"provider": "basic"
},
"theme": {
"staticMaxAge": 2592000,
"cacheTemplates": true,
"cacheThemes": true,
"folder": {
"dir": "${jboss.home.dir}/themes"
}
},
"scheduled": {
"interval": 900
},
"connectionsHttpClient": {
"default": {}
},
"connectionsJpa": {
"default": {
"dataSource": "java:jboss/datasources/KeycloakDS",
"databaseSchema": "update"
}
},
"realmCache": {
"provider": "default",
"default" : {
"enabled": true
}
},
"connectionsInfinispan": {
"provider": "default",
"default": {
"cacheContainer" : "java:comp/env/infinispan/Keycloak"
}
}
}
package de.tdlabs.idm.keycloak.ext.authentication;
import static java.time.LocalDateTime.now;
import static java.util.Arrays.asList;
import java.util.List;
import org.jboss.logging.Logger;
import org.keycloak.Config.Scope;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserModel;
public class LoginStatsRecordingRequiredActionProvider implements RequiredActionProvider, RequiredActionFactory {
private static final Logger LOG = Logger.getLogger(LoginStatsRecordingRequiredActionProvider.class);
private static final String PROVIDER_ID = "login_stats_action";
private static final String RECORD_LOGIN_STATISTICS_ACTION = "Record Login Statistics Action";
private static final String LOGIN_LOGIN_COUNT = "login.login-count";
private static final String LOGIN_FIRST_LOGIN_DATE = "login.first-login-date";
private static final String LOGIN_RECENT_LOGIN_DATE = "login.recent-login-date";
private static final String ONE = "1";
private static final LoginStatsRecordingRequiredActionProvider INSTANCE = new LoginStatsRecordingRequiredActionProvider();
@Override
public void close() {
// NOOP
}
@Override
public void evaluateTriggers(RequiredActionContext context) {
UserModel user = context.getUser();
try {
recordFirstLogin(user);
} catch (Exception ex) {
LOG.warnv(ex,"Couldn't record first login <{0}>", this);
}
try {
recordRecentLogin(user);
} catch (Exception ex) {
LOG.warnv(ex, "Couldn't record recent login <{0}>", this);
}
try {
recordLoginCount(user);
} catch (Exception ex) {
LOG.warnv(ex, "Couldn't record login count <{0}>", this);
}
}
private void recordLoginCount(UserModel user) {
List<String> list = user.getAttribute(LOGIN_LOGIN_COUNT);
if (list == null || list.isEmpty()) {
list = asList(ONE);
} else {
list = asList(String.valueOf(Long.parseLong(list.get(0)) + 1));
}
user.setAttribute(LOGIN_LOGIN_COUNT, list);
}
private void recordRecentLogin(UserModel user) {
user.setAttribute(LOGIN_RECENT_LOGIN_DATE, asList(now().toString()));
}
private void recordFirstLogin(UserModel user) {
List<String> list = user.getAttribute(LOGIN_FIRST_LOGIN_DATE);
if (list == null || list.isEmpty()) {
user.setAttribute(LOGIN_FIRST_LOGIN_DATE, asList(now().toString()));
}
}
@Override
public void requiredActionChallenge(RequiredActionContext context) {
// NOOP
}
@Override
public void processAction(RequiredActionContext context) {
context.success();
}
@Override
public RequiredActionProvider create(KeycloakSession session) {
return INSTANCE;
}
@Override
public void init(Scope config) {
LOG.infov("Creating IdM Keycloak extension <{0}>", this);
// NOOP
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// NOOP
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getDisplayText() {
return RECORD_LOGIN_STATISTICS_ACTION;
}
}
<?xml version="1.0" ?>
<module xmlns="urn:jboss:module:1.1" name="de.eurodata.idm.keycloak.idm-keycloak-ext-login-action">
<!-- copy to $KEYCLOAK_HOME/modules/de/tdlabs/idm/keycloak/idm-keycloak-ext-login-action/main/ together with the jar -->
<!-- add module reference in provider section of keycloak-server.json under $KEYCLOAK_HOME/standalone/configuration -->
<resources>
<resource-root path="idm-keycloak-ext-login-action-1.0.0.BUILD-SNAPSHOT.jar"/>
</resources>
<dependencies>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-server-spi"/>
<module name="org.jboss.logging"/>
</dependencies>
</module>
# place this under src/main/resources/META-INF/services
de.tdlabs.idm.keycloak.ext.authentication.LoginStatsRecordingRequiredActionProvider
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.tdlabs.idm</groupId>
<artifactId>idm-keycloak-ext-login-action</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<properties>
<keycloak.version>1.9.4.Final</keycloak.version>
<lombok.version>1.16.8</lombok.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>ISO-8859-1</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@srisaibabupv
Copy link

Hi Thomas,

Thanks for sharing this post.
Am using Keycloak server 3.4.3.Final version
But unfortunately am unable to configure custom provider using above code changes. Whenever i start my keycloak server it fails with error stating unable to find LoginStatsRecordingRequiredActionProvider provider class.

Can you please let me know what could be the issue have place .jar and module.xml under modules folder

Regards,
Sri Sai

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment