diff --git a/pom.xml b/pom.xml
index fd32156..bc6bdaa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,6 +24,9 @@
0.16
3.11.0
1.0.1
+ 5.10.2
+ 5.12.0
+ 1.6.1
@@ -39,6 +42,33 @@
${auto-service.version}
provided
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+ ${junit-jupiter.version}
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+ ${junit-jupiter.version}
+
+
+ org.mockito
+ mockito-core
+ ${mockito-core.version}
+ test
+
+
+
+ com.github.ua-parser
+ uap-java
+ provided
+ ${uap-java.version}
+
diff --git a/spi/pom.xml b/spi/pom.xml
index f86f8f8..eae96aa 100644
--- a/spi/pom.xml
+++ b/spi/pom.xml
@@ -38,6 +38,7 @@
+ src/test/java
org.apache.maven.plugins
diff --git a/spi/src/main/java/nl/wouterh/keycloak/trusteddevice/authenticator/RegisterTrustedDeviceAuthenticator.java b/spi/src/main/java/nl/wouterh/keycloak/trusteddevice/authenticator/RegisterTrustedDeviceAuthenticator.java
index 365a46b..2d2e66b 100644
--- a/spi/src/main/java/nl/wouterh/keycloak/trusteddevice/authenticator/RegisterTrustedDeviceAuthenticator.java
+++ b/spi/src/main/java/nl/wouterh/keycloak/trusteddevice/authenticator/RegisterTrustedDeviceAuthenticator.java
@@ -2,20 +2,13 @@
import static nl.wouterh.keycloak.trusteddevice.authenticator.RegisterTrustedDeviceAuthenticatorFactory.CONF_DURATION;
-import com.google.common.base.Strings;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map;
-import jakarta.ws.rs.core.MultivaluedMap;
-import jakarta.ws.rs.core.Response;
-import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialModel;
-import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialProvider;
-import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialProviderFactory;
-import nl.wouterh.keycloak.trusteddevice.util.TrustedDeviceToken;
-import nl.wouterh.keycloak.trusteddevice.util.UserAgentParser;
+
import org.apache.commons.codec.binary.Hex;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
@@ -27,6 +20,16 @@
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import com.google.common.base.Strings;
+
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.Response;
+import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialModel;
+import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialProvider;
+import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialProviderFactory;
+import nl.wouterh.keycloak.trusteddevice.util.TrustedDeviceToken;
+import nl.wouterh.keycloak.trusteddevice.util.UserAgentParser;
+
public class RegisterTrustedDeviceAuthenticator implements Authenticator {
private static final SecureRandom secureRandom = new SecureRandom();
@@ -47,9 +50,11 @@ public void authenticate(AuthenticationFlowContext context) {
TrustedDeviceCredentialModel credential = TrustedDeviceToken.getCredentialFromCookie(
context.getSession(), realm, user);
+ // Check if the user already has a trusted device
if (credential != null) {
context.success();
} else {
+ // Otherwise, show the registration form
Response form = context.form()
.setAttribute("trustedDeviceName", UserAgentParser.getDeviceName(session))
.createForm("trusted-device-register.ftl");
@@ -93,7 +98,7 @@ public void action(AuthenticationFlowContext context) {
secureRandom.nextBytes(bytes);
String deviceId = Hex.encodeHexString(bytes);
- // Expire the token in 1 year
+ // Expire the token in configured duration
Long exp = null;
String credentialName = deviceName;
if (duration != null) {
@@ -106,6 +111,7 @@ public void action(AuthenticationFlowContext context) {
TrustedDeviceCredentialModel trustedDeviceCredentialModel = TrustedDeviceCredentialModel.create(
credentialName, deviceId, exp);
+ // Remove all expired credentials
trustedDeviceCredentialProvider.removeExpiredCredentials(realm, user);
// Add the new credential
diff --git a/spi/src/main/java/nl/wouterh/keycloak/trusteddevice/util/TrustedDeviceToken.java b/spi/src/main/java/nl/wouterh/keycloak/trusteddevice/util/TrustedDeviceToken.java
index eb74029..d7ed1b2 100644
--- a/spi/src/main/java/nl/wouterh/keycloak/trusteddevice/util/TrustedDeviceToken.java
+++ b/spi/src/main/java/nl/wouterh/keycloak/trusteddevice/util/TrustedDeviceToken.java
@@ -1,15 +1,5 @@
package nl.wouterh.keycloak.trusteddevice.util;
-import jakarta.ws.rs.core.NewCookie;
-import jakarta.ws.rs.core.UriBuilder;
-import jakarta.ws.rs.core.NewCookie.SameSite;
-import jakarta.ws.rs.core.Cookie;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
-import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialModel;
-import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialProvider;
-import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialProviderFactory;
import org.keycloak.TokenCategory;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.Time;
@@ -19,6 +9,17 @@
import org.keycloak.models.UserModel;
import org.keycloak.representations.JsonWebToken;
+import jakarta.ws.rs.core.Cookie;
+import jakarta.ws.rs.core.NewCookie;
+import jakarta.ws.rs.core.NewCookie.SameSite;
+import jakarta.ws.rs.core.UriBuilder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialModel;
+import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialProvider;
+import nl.wouterh.keycloak.trusteddevice.credential.TrustedDeviceCredentialProviderFactory;
+
@Getter
@Setter
@NoArgsConstructor
@@ -50,7 +51,13 @@ private static void addCookie(KeycloakSession session, RealmModel realm, String
session.getContext().getHttpResponse().setCookieIfAbsent(newCookie);
}
- public static TrustedDeviceToken getCookie(KeycloakSession session) {
+ /**
+ * Retrieves the TrustedDeviceToken from the session cookie.
+ *
+ * @param session The KeycloakSession object.
+ * @return The TrustedDeviceToken object if the cookie exists and is valid, otherwise null.
+ */
+ private static TrustedDeviceToken getCookie(KeycloakSession session) {
Cookie cookie = session.getContext().getRequestHeaders().getCookies().get(COOKIE_NAME);
long time = Time.currentTime();
@@ -66,8 +73,17 @@ public static TrustedDeviceToken getCookie(KeycloakSession session) {
return null;
}
+ /**
+ * Retrieves the trusted device credential from the cookie.
+ *
+ * @param session the Keycloak session
+ * @param realm the realm model
+ * @param user the user model
+ * @return the trusted device credential, or null if not found or invalid
+ */
public static TrustedDeviceCredentialModel getCredentialFromCookie(KeycloakSession session,
RealmModel realm, UserModel user) {
+
TrustedDeviceToken deviceToken = getCookie(session);
TrustedDeviceCredentialProvider trustedDeviceCredentialProvider = (TrustedDeviceCredentialProvider) session
.getProvider(CredentialProvider.class, TrustedDeviceCredentialProviderFactory.PROVIDER_ID);
diff --git a/spi/src/test/java/nl/wouterh/keycloak/trusteddevice/TrustedDeviceTokenTest.java b/spi/src/test/java/nl/wouterh/keycloak/trusteddevice/TrustedDeviceTokenTest.java
new file mode 100644
index 0000000..c1fefa0
--- /dev/null
+++ b/spi/src/test/java/nl/wouterh/keycloak/trusteddevice/TrustedDeviceTokenTest.java
@@ -0,0 +1,28 @@
+package nl.wouterh.keycloak.trusteddevice;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.keycloak.TokenCategory;
+import org.keycloak.common.util.Time;
+
+import nl.wouterh.keycloak.trusteddevice.util.TrustedDeviceToken;
+
+public class TrustedDeviceTokenTest {
+
+ @Test
+ public void testCreateTrustedDeviceToken() {
+ TrustedDeviceToken token = new TrustedDeviceToken("secret-id", "secret", (long) 10000000);
+
+ // Check mappers work correctly
+ assertEquals("secret-id", token.getId());
+ assertEquals("secret", token.getSecret());
+ assertEquals(10000000, token.getExp());
+ assertTrue(token.getIat() >= Time.currentTime(),
+ "iat should be set to current time or later to avoid time drift issues in test");
+
+ assertEquals(TokenCategory.INTERNAL, token.getCategory());
+ }
+
+}
diff --git a/spi/src/test/java/nl/wouterh/keycloak/trusteddevice/util/UserAgentParserTest.java b/spi/src/test/java/nl/wouterh/keycloak/trusteddevice/util/UserAgentParserTest.java
new file mode 100644
index 0000000..57db5d8
--- /dev/null
+++ b/spi/src/test/java/nl/wouterh/keycloak/trusteddevice/util/UserAgentParserTest.java
@@ -0,0 +1,36 @@
+package nl.wouterh.keycloak.trusteddevice.util;
+
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.core.HttpHeaders;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakContext;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class UserAgentParserTest {
+
+ @Test
+ public void testGetDeviceName() {
+
+ // Mock the KeycloakSession
+ KeycloakSession session = mock(KeycloakSession.class);
+ HttpHeaders headers = mock(HttpHeaders.class);
+ KeycloakContext context = mock(KeycloakContext.class);
+
+ // Mock the KeycloakSession to return the mocked HttpHeaders
+ when(session.getContext()).thenReturn(context);
+ when(session.getContext().getRequestHeaders()).thenReturn(headers);
+
+ // Mock the User-Agent header
+ when(headers.getHeaderString(HttpHeaders.USER_AGENT))
+ .thenReturn("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3");
+
+ // Call the method under test
+ String deviceName = UserAgentParser.getDeviceName(session);
+
+ // Verify the result
+ assertEquals("Chrome on Windows", deviceName);
+ }
+}
\ No newline at end of file