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
22 changes: 22 additions & 0 deletions ol-keycloak/oltheme/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<keycloak.version>26.3.0</keycloak.version>
</properties>
<name>OL theme</name>

<dependencies>
<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-server-spi-private</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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package edu.mit.ol.keycloak.providers;

import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.jboss.logging.Logger;

import jakarta.ws.rs.core.Response;
import java.util.Objects;
import java.util.HashMap;
import java.util.Map;
import java.security.SecureRandom;

public class CustomVerifyEmailRequiredAction implements RequiredActionProvider {

private static final Logger logger = Logger.getLogger(CustomVerifyEmailRequiredAction.class);
public static final String PROVIDER_ID = "CUSTOM_VERIFY_EMAIL";

@Override
public void evaluateTriggers(RequiredActionContext context) {
if (!context.getUser().isEmailVerified()) {
context.getUser().addRequiredAction(PROVIDER_ID);
}
}

@Override
public void requiredActionChallenge(RequiredActionContext context) {
// Only send code if one doesn't already exist
if (context.getUser().getFirstAttribute("email_verification_code") == null) {
sendVerificationCode(context);
}

Response challenge = context.form()
.setAttribute("user", context.getUser())
.createForm("verify-email.ftl");
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Template name 'verify-email.ftl' doesn't match the actual template file 'login-verify-email.ftl'. This will cause template resolution to fail.

Suggested change
.createForm("verify-email.ftl");
.createForm("login-verify-email.ftl");

Copilot uses AI. Check for mistakes.

context.challenge(challenge);
}

private enum VerificationCodeStatus {
VALID, INVALID, EXPIRED
}

private VerificationCodeStatus checkVerificationCode(RequiredActionContext context, String code) {
String storedCode = context.getUser().getFirstAttribute("email_verification_code");
String timestampStr = context.getUser().getFirstAttribute("email_code_timestamp");

if (storedCode == null || timestampStr == null) {
return VerificationCodeStatus.INVALID;
}

try {
long timestamp = Long.parseLong(timestampStr);
long now = System.currentTimeMillis();
if (now - timestamp > 15 * 60 * 1000) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The code expiration time is a magic number. It's better to define it as a constant to improve readability and maintainability.

private static final long CODE_EXPIRATION_MS = 15 * 60 * 1000L; // 15 minutes

if (now - timestamp > CODE_EXPIRATION_MS) {

Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expiration time (15 minutes) is hardcoded as a magic number. Consider extracting this to a configurable constant for better maintainability.

Suggested change
if (now - timestamp > 15 * 60 * 1000) {
if (now - timestamp > EMAIL_VERIFICATION_EXPIRATION_MS) {

Copilot uses AI. Check for mistakes.

return VerificationCodeStatus.EXPIRED;
}
} catch (NumberFormatException e) {
return VerificationCodeStatus.INVALID;
}

return Objects.equals(code, storedCode) ? VerificationCodeStatus.VALID : VerificationCodeStatus.INVALID;
}

@Override
public void processAction(RequiredActionContext context) {
String code = context.getHttpRequest().getDecodedFormParameters().getFirst("email_code");
String resend = context.getHttpRequest().getDecodedFormParameters().getFirst("resend");

if ("true".equals(resend)) {
sendVerificationCode(context);
requiredActionChallenge(context);
return;
}

VerificationCodeStatus status = checkVerificationCode(context, code);

if (status == VerificationCodeStatus.VALID) {
context.getUser().setEmailVerified(true);
context.getUser().removeAttribute("email_verification_code");
context.getUser().removeAttribute("email_code_timestamp");
context.success();
} else if (status == VerificationCodeStatus.EXPIRED) {
context.getUser().removeAttribute("email_verification_code");
context.getUser().removeAttribute("email_code_timestamp");
sendVerificationCode(context);
Response challenge = context.form()
.setError("Your verification code has expired. A new code has been sent to your email.")
.setAttribute("user", context.getUser())
.createForm("verify-email.ftl");
context.challenge(challenge);
} else {
Response challenge = context.form()
.setError("Invalid verification code. Please check your email and try again.")
.setAttribute("user", context.getUser())
.createForm("verify-email.ftl");
Comment on lines +94 to +100
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Template name 'verify-email.ftl' doesn't match the actual template file 'login-verify-email.ftl'. This will cause template resolution to fail.

Suggested change
.createForm("verify-email.ftl");
context.challenge(challenge);
} else {
Response challenge = context.form()
.setError("Invalid verification code. Please check your email and try again.")
.setAttribute("user", context.getUser())
.createForm("verify-email.ftl");
.createForm("login-verify-email.ftl");
context.challenge(challenge);
} else {
Response challenge = context.form()
.setError("Invalid verification code. Please check your email and try again.")
.setAttribute("user", context.getUser())
.createForm("login-verify-email.ftl");

Copilot uses AI. Check for mistakes.

Comment on lines +94 to +100
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Template name 'verify-email.ftl' doesn't match the actual template file 'login-verify-email.ftl'. This will cause template resolution to fail.

Suggested change
.createForm("verify-email.ftl");
context.challenge(challenge);
} else {
Response challenge = context.form()
.setError("Invalid verification code. Please check your email and try again.")
.setAttribute("user", context.getUser())
.createForm("verify-email.ftl");
.createForm("login-verify-email.ftl");
context.challenge(challenge);
} else {
Response challenge = context.form()
.setError("Invalid verification code. Please check your email and try again.")
.setAttribute("user", context.getUser())
.createForm("login-verify-email.ftl");

Copilot uses AI. Check for mistakes.

context.challenge(challenge);
}
}

private void sendVerificationCode(RequiredActionContext context) {
String code = generateNumericCode(6); // 6-digit numeric code

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The code length is a magic number. It's better to define it as a constant to improve readability and maintainability.

private static final int CODE_LENGTH = 6;
String code = generateNumericCode(CODE_LENGTH); // 6-digit numeric code

long timestamp = System.currentTimeMillis();

context.getUser().setSingleAttribute("email_verification_code", code);
context.getUser().setSingleAttribute("email_code_timestamp", String.valueOf(timestamp));

try {
EmailTemplateProvider emailProvider = context.getSession().getProvider(EmailTemplateProvider.class);
Map<String, Object> attributes = new HashMap<>();
attributes.put("code", code);
emailProvider.setRealm(context.getRealm())
.setUser(context.getUser())
.send("emailVerificationSubject", "email-verification.ftl", attributes);
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The email template reference uses 'email-verification.ftl' but the actual file is 'email-verification.mjml'. This will cause template resolution to fail.

Suggested change
.send("emailVerificationSubject", "email-verification.ftl", attributes);
.send("emailVerificationSubject", "email-verification.mjml", attributes);

Copilot uses AI. Check for mistakes.

} catch (EmailException e) {
logger.error("Failed to send verification email", e);
}
}

private String generateNumericCode(int length) {
StringBuilder sb = new StringBuilder(length);
SecureRandom random = new SecureRandom();
for (int i = 0; i < length; i++) {
sb.append(random.nextInt(10));
}
return sb.toString();
}

@Override
public void close() {
}

public static class Factory implements RequiredActionFactory {

@Override
public RequiredActionProvider create(KeycloakSession session) {
return new CustomVerifyEmailRequiredAction();
}

@Override
public String getDisplayText() {
return "Custom Email Verification with Code";
}

@Override
public String getId() {
return PROVIDER_ID;
}

@Override
public void init(org.keycloak.Config.Scope config) {
// No initialization needed
}

@Override
public void postInit(org.keycloak.models.KeycloakSessionFactory factory) {
// No post-initialization needed
}

@Override
public void close() {
// No resources to clean up
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
edu.mit.ol.keycloak.providers.CustomVerifyEmailRequiredAction$Factory
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<mj-body background-color="#F3F4F8" css-class="email-body">
<mj-wrapper background-color="#FFFFFF" border-radius="8px" padding="0">
<mj-include path="./includes/header.mjml" />

<mj-section background-color="#FFFFFF" padding="0">
<mj-column>
<mj-text>
Expand All @@ -12,6 +11,7 @@
Thank you for creating an account with ${realmName}. Please complete
the account verification process by clicking this link:</p>
</mj-text>

<mj-button href="${link}" border-radius="4px" background-color="#A31F34" align="left" font-weight="bold"
font-size="14px" color="#FFFFFF" padding="0 25px">
Verify Your Email
Expand All @@ -21,7 +21,6 @@
</mj-text>
</mj-column>
</mj-section>

<mj-include path="./includes/footer.mjml" />
</mj-wrapper>
</mj-body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,50 @@
<p class="instruction">
${msg("emailVerifyInstruction1", user.email)}
</p>

<!-- Verification code form -->
<form id="kc-verify-email-code-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="email_code" class="${properties.kcLabelClass!}">Verification Code</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="email_code" name="email_code" class="${properties.kcInputClass!}"
placeholder="Enter 6-digit code" maxlength="6" autofocus autocomplete="off"
style="text-align: center; font-size: 18px; letter-spacing: 2px;" />
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!}"
name="submit" type="submit" value="Verify Email"/>
</div>
</div>
</form>

<!-- Resend code form -->
<div style="margin-top: 15px; text-align: center;">
<form action="${url.loginAction}" method="post" style="display: inline;">
<input type="hidden" name="resend" value="true"/>
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!}">
Resend Code
</button>
</form>
</div>

<#elseif section = "info">
<div id="kc-info" class="${properties.kcSignUpClass!}">
<div id="kc-info-wrapper" class="${properties.kcInfoAreaWrapperClass!}">
<p class="pf-v5-u-my-md">${msg("emailVerifyInstruction2")}</p>
<p class="pf-v5-u-my-md">A 6-digit verification code has been sent to your email address.</p>
<hr />
<p class="pf-v5-u-my-md">${msg("emailVerifyInstruction3")}</p>
<p class="pf-v5-u-my-md">Enter the code from your email to verify your account.</p>
<p class="pf-v5-u-my-md">
<b>${msg("emailVerifyInstruction4Bold")}</b>
<b>Code not working?</b>
${msg("emailVerifyInstruction4")}
</p>
<p class="pf-v5-u-my-md">
<b>Didn't receive the code?</b>
Check your spam folder or click "Resend Code" above.
<a href="#">${msg("emailVerifySupportLinkTitle")}</a>.
</p>
Comment on lines +43 to 54

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This template contains several hardcoded, user-facing strings (e.g., "A 6-digit verification code...", "Code not working?", etc.). For better maintainability and to support internationalization (i18n), it's a best practice in Keycloak theming to move these strings to a message properties file (e.g., theme/ol/login/messages/messages_en.properties) and reference them with the msg() function, like ${msg("verifyEmail.infoText1")}.

            <p class="pf-v5-u-my-md">${msg("verifyEmailInstruction2")}</p>
            <hr />
            <p class="pf-v5-u-my-md">${msg("verifyEmailInstruction3")}</p>
            <p class="pf-v5-u-my-md">
                <b>${msg("emailVerifyInstruction4Bold")}</b>
                ${msg("emailVerifyInstruction4")}
            </p>
            <p class="pf-v5-u-my-md">
                <b>${msg("emailVerifyInstruction5Bold")}</b>
                ${msg("emailVerifyInstruction5")}
                <a href="#">${msg("emailVerifySupportLinkTitle")}</a>.
            </p>

</div>
Expand Down