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