diff --git a/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummyAuthenticationSessionModel.java b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummyAuthenticationSessionModel.java index 593d0b9..a2b6535 100644 --- a/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummyAuthenticationSessionModel.java +++ b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummyAuthenticationSessionModel.java @@ -13,6 +13,13 @@ public class DummyAuthenticationSessionModel implements AuthenticationSessionModel { private Map authNotes = new HashMap<>(); + private final RealmModel realm; + private final ClientModel client; + + public DummyAuthenticationSessionModel(RealmModel realm) { + this.realm = realm; + this.client = DummyClientModel.create(realm); + } @Override public String getTabId() { @@ -156,12 +163,12 @@ public void setRedirectUri(String s) { @Override public RealmModel getRealm() { - return null; + return realm; } @Override public ClientModel getClient() { - return null; + return client; } @Override @@ -176,7 +183,7 @@ public void setAction(String s) { @Override public String getProtocol() { - return null; + return "openid-connect"; } @Override diff --git a/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummyClientModel.java b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummyClientModel.java new file mode 100644 index 0000000..c9f5426 --- /dev/null +++ b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummyClientModel.java @@ -0,0 +1,76 @@ +package org.keycloak.ext.theme; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +public class DummyClientModel { + + public static ClientModel create(RealmModel realm) { + return (ClientModel) Proxy.newProxyInstance( + DummyClientModel.class.getClassLoader(), + new Class[]{ClientModel.class}, + new ClientInvocationHandler(realm) + ); + } + + private static class ClientInvocationHandler implements InvocationHandler { + + private final RealmModel realm; + + public ClientInvocationHandler(RealmModel realm) { + this.realm = realm; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + Class returnType = method.getReturnType(); + + // Handle specific methods + switch (methodName) { + case "getId": + case "getClientId": + return "dummy-client"; + case "getName": + return "Dummy Client"; + case "getDescription": + return "Dummy client for theme preview"; + case "getRealm": + return realm; + case "getProtocol": + return "openid-connect"; + case "isEnabled": + case "isPublicClient": + case "isStandardFlowEnabled": + return true; + } + + // Handle by return type + if (returnType == boolean.class || returnType == Boolean.class) { + return false; + } else if (returnType == int.class || returnType == Integer.class) { + return 0; + } else if (returnType == String.class) { + return null; + } else if (returnType == Set.class) { + return Collections.emptySet(); + } else if (returnType == Map.class) { + return Collections.emptyMap(); + } else if (returnType == Stream.class) { + return Stream.empty(); + } else if (returnType == void.class) { + return null; + } + + return null; + } + } +} diff --git a/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummySubjectCredentialManager.java b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummySubjectCredentialManager.java index 10f5129..bd94978 100644 --- a/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummySubjectCredentialManager.java +++ b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummySubjectCredentialManager.java @@ -3,6 +3,7 @@ import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialModel; import org.keycloak.models.SubjectCredentialManager; +import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.models.credential.RecoveryAuthnCodesCredentialModel; import org.keycloak.models.utils.RecoveryAuthnCodesUtils; @@ -46,14 +47,21 @@ public Stream getStoredCredentialsStream() { } @Override - public Stream getStoredCredentialsByTypeStream(String s) { - List generatedRecoveryAuthnCodes = RecoveryAuthnCodesUtils.generateRawCodes(); - CredentialModel recoveryAuthnCodesCred = RecoveryAuthnCodesCredentialModel.createFromValues( - generatedRecoveryAuthnCodes, - System.currentTimeMillis(), - null); - - return Stream.of(recoveryAuthnCodesCred); + public Stream getStoredCredentialsByTypeStream(String type) { + if (RecoveryAuthnCodesCredentialModel.TYPE.equals(type)) { + List generatedRecoveryAuthnCodes = RecoveryAuthnCodesUtils.generateRawCodes(); + CredentialModel recoveryAuthnCodesCred = RecoveryAuthnCodesCredentialModel.createFromValues( + generatedRecoveryAuthnCodes, + System.currentTimeMillis(), + null); + return Stream.of(recoveryAuthnCodesCred); + } else if (OTPCredentialModel.TYPE.equals(type)) { + OTPCredentialModel otpCred = OTPCredentialModel.createTOTP("dummy-secret", 6, 30, "HmacSHA1"); + otpCred.setId("dummy-otp-id"); + otpCred.setUserLabel("Authenticator App"); + return Stream.of(otpCred); + } + return Stream.empty(); } @Override diff --git a/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummyUserModel.java b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummyUserModel.java index e7468c5..17c6605 100644 --- a/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummyUserModel.java +++ b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/DummyUserModel.java @@ -94,7 +94,7 @@ public void removeRequiredAction(String s) { @Override public String getFirstName() { - return null; + return "John"; } @Override @@ -104,7 +104,7 @@ public void setFirstName(String s) { @Override public String getLastName() { - return null; + return "Doe"; } @Override @@ -114,7 +114,7 @@ public void setLastName(String s) { @Override public String getEmail() { - return null; + return "john.doe@example.com"; } @Override diff --git a/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/RecoveryCodesBean.java b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/RecoveryCodesBean.java new file mode 100644 index 0000000..47b52d7 --- /dev/null +++ b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/RecoveryCodesBean.java @@ -0,0 +1,39 @@ +package org.keycloak.ext.theme; + +import org.keycloak.common.util.Time; +import org.keycloak.models.utils.RecoveryAuthnCodesUtils; + +import java.util.List; + +/** + * Custom bean for recovery codes that provides the property names + * expected by the login-spa theme template. + */ +public class RecoveryCodesBean { + + private final List generatedRecoveryAuthnCodes; + private final long generatedAt; + + public RecoveryCodesBean() { + this.generatedRecoveryAuthnCodes = RecoveryAuthnCodesUtils.generateRawCodes(); + this.generatedAt = Time.currentTimeMillis(); + } + + // Property name expected by template: generatedRecoveryAuthnCodes + public List getGeneratedRecoveryAuthnCodes() { + return this.generatedRecoveryAuthnCodes; + } + + // Also provide the List suffix version for compatibility + public List getGeneratedRecoveryAuthnCodesList() { + return this.generatedRecoveryAuthnCodes; + } + + public String getGeneratedRecoveryAuthnCodesAsString() { + return String.join(",", this.generatedRecoveryAuthnCodes); + } + + public long getGeneratedAt() { + return generatedAt; + } +} diff --git a/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/ThemePreviewProvider.java b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/ThemePreviewProvider.java index 00a90a1..480cadb 100644 --- a/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/ThemePreviewProvider.java +++ b/kc-ext/theme-preview/src/main/java/org/keycloak/ext/theme/ThemePreviewProvider.java @@ -2,7 +2,10 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.Response; + +import java.net.URI; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -21,10 +24,17 @@ public class ThemePreviewProvider implements RealmResourceProvider { new Page("login-username", "Login - username", LoginFormsProvider::createLoginUsername), new Page("login-otp", "Login - OTP", LoginFormsProvider::createLoginTotp), new Page("login-recovery-codes", "Login - recovery codes", LoginFormsProvider::createLoginRecoveryAuthnCode), - new Page("login-config-recover-codes", "Action - recovery codes", l -> l.createResponse(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES)), + new Page("login-config-recover-codes", "Action - recovery codes", l -> { + l.setAttribute("recoveryAuthnCodesConfigBean", new RecoveryCodesBean()); + return l.createResponse(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES); + }), new Page("login-config-totp", "Action - OTP", l -> l.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP)), new Page("login-update-password", "Action - Update password", l -> l.createResponse(UserModel.RequiredAction.UPDATE_PASSWORD)), - new Page("login-register", "Action - Register", LoginFormsProvider::createRegistration) + new Page("login-register", "Action - Register", l -> { + l.setFormData(new MultivaluedHashMap<>()); + l.setAttribute("email", "john.doe@example.com"); + return l.createRegistration(); + }) ); private KeycloakSession session; @@ -67,8 +77,14 @@ public Response view(@PathParam("template") String page) { LoginFormsProvider loginForms = session.getProvider(LoginFormsProvider.class); - loginForms.setActionUri(session.getContext().getUri().getRequestUri()); - loginForms.setAuthenticationSession(new DummyAuthenticationSessionModel()); + URI actionUri = session.getContext().getUri().getBaseUriBuilder() + .path("realms") + .path(session.getContext().getRealm().getName()) + .path("theme-preview") + .path(page) + .build(); + loginForms.setActionUri(actionUri); + loginForms.setAuthenticationSession(new DummyAuthenticationSessionModel(proxy)); loginForms.setUser(new DummyUserModel()); Optional pageHandler = pages.stream().filter(p -> p.template.equals(page)).findFirst();