Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
public class DummyAuthenticationSessionModel implements AuthenticationSessionModel {

private Map<String, String> 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() {
Expand Down Expand Up @@ -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
Expand All @@ -176,7 +183,7 @@ public void setAction(String s) {

@Override
public String getProtocol() {
return null;
return "openid-connect";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -46,14 +47,21 @@ public Stream<CredentialModel> getStoredCredentialsStream() {
}

@Override
public Stream<CredentialModel> getStoredCredentialsByTypeStream(String s) {
List<String> generatedRecoveryAuthnCodes = RecoveryAuthnCodesUtils.generateRawCodes();
CredentialModel recoveryAuthnCodesCred = RecoveryAuthnCodesCredentialModel.createFromValues(
generatedRecoveryAuthnCodes,
System.currentTimeMillis(),
null);

return Stream.of(recoveryAuthnCodesCred);
public Stream<CredentialModel> getStoredCredentialsByTypeStream(String type) {
if (RecoveryAuthnCodesCredentialModel.TYPE.equals(type)) {
List<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public void removeRequiredAction(String s) {

@Override
public String getFirstName() {
return null;
return "John";
}

@Override
Expand All @@ -104,7 +104,7 @@ public void setFirstName(String s) {

@Override
public String getLastName() {
return null;
return "Doe";
}

@Override
Expand All @@ -114,7 +114,7 @@ public void setLastName(String s) {

@Override
public String getEmail() {
return null;
return "john.doe@example.com";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> generatedRecoveryAuthnCodes;
private final long generatedAt;

public RecoveryCodesBean() {
this.generatedRecoveryAuthnCodes = RecoveryAuthnCodesUtils.generateRawCodes();
this.generatedAt = Time.currentTimeMillis();
}

// Property name expected by template: generatedRecoveryAuthnCodes
public List<String> getGeneratedRecoveryAuthnCodes() {
return this.generatedRecoveryAuthnCodes;
}

// Also provide the List suffix version for compatibility
public List<String> getGeneratedRecoveryAuthnCodesList() {
return this.generatedRecoveryAuthnCodes;
}

public String getGeneratedRecoveryAuthnCodesAsString() {
return String.join(",", this.generatedRecoveryAuthnCodes);
}

public long getGeneratedAt() {
return generatedAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Page> pageHandler = pages.stream().filter(p -> p.template.equals(page)).findFirst();
Expand Down