Skip to content
This repository was archived by the owner on Jan 9, 2026. It is now read-only.

Commit b380b0b

Browse files
authored
Update MFA totp factor enroll flow (#48)
* style update * remove qr from factor details * enroll factor ui update * totp endpoint created * diplay qr code modal * sms and totp endpoint updated * cleanup
1 parent a7f99df commit b380b0b

4 files changed

Lines changed: 170 additions & 55 deletions

File tree

java-mfa-example/src/main/java/com/workos/java/examples/MfaApp.java

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.util.HashMap;
1717
import java.util.List;
1818
import java.util.Map;
19+
import com.fasterxml.jackson.databind.JsonNode;
1920

2021

2122
public class MfaApp {
@@ -35,7 +36,8 @@ public MfaApp() {
3536
app.get("/", this::home);
3637
app.get("/clear_session", this::clear_session);
3738
app.get("/factor_detail", this::factor_detail);
38-
app.post("/enroll_factor", this::enroll_factor);
39+
app.post("/enroll_sms_factor", this::enroll_sms_factor);
40+
app.post("/enroll_totp_factor", this::enroll_totp_factor);
3941
app.get("/enroll_factor_details", this::enroll_factor_details);
4042
app.post("/challenge_factor", this::challenge_factor);
4143
app.post("/verify_factor", this::verify_factor);
@@ -168,31 +170,59 @@ public void enroll_factor_details(Context ctx ) {
168170
ctx.render("enroll_factor.jte");
169171
}
170172

171-
public void enroll_factor(Context ctx) {
172-
String phoneNumber = ctx.formParam("phone_number");
173-
String factorType = ctx.formParam("type");
174-
String issuer = ctx.formParam("totp_issuer");
175-
String user = ctx.formParam("totp_user");
173+
public void enroll_totp_factor(Context ctx ) {
174+
JsonNode jsonNode = ctx.bodyAsClass(JsonNode.class);
175+
String issuer = jsonNode.get("issuer").asText();
176+
String user = jsonNode.get("user").asText();
176177
EnrollFactorOptions options;
177178

178-
switch(factorType) {
179-
case "sms":
180-
options = EnrollFactorOptions.builder()
181-
.type("sms")
182-
.phoneNumber(phoneNumber)
183-
.build();
184-
break;
185-
case "totp":
186-
options = EnrollFactorOptions.builder()
187-
.type("totp")
188-
.issuer(issuer)
189-
.user(user)
190-
.build();
191-
break;
192-
default:
193-
options = EnrollFactorOptions.builder()
194-
.build();
179+
options = EnrollFactorOptions.builder()
180+
.type("totp")
181+
.issuer(issuer)
182+
.user(user)
183+
.build();
184+
185+
try {
186+
Factor factor = workos.mfa.enrollFactor(options);
187+
String factorId = factor.id;
188+
189+
if(ctx.sessionAttribute("factorList") != null) {
190+
ArrayList<String> factorIdList = ctx.sessionAttribute("factorIdList");
191+
factorIdList.add(factorId);
192+
ctx.sessionAttribute("factorIdList", factorIdList);
193+
HashMap<String, Factor> factorList = ctx.sessionAttribute("factorList");
194+
factorList.put(factorId, factor);
195+
ctx.sessionAttribute("factorList", factorList);
196+
} else {
197+
ArrayList<String> factorIdList = new ArrayList<>();
198+
factorIdList.add(factorId);
199+
ctx.sessionAttribute("factorIdList", factorIdList);
200+
HashMap<String, Factor> factorList = new HashMap<>();
201+
factorList.put(factorId, factor);
202+
ctx.sessionAttribute("factorList", factorList);
203+
}
204+
HashMap<String, Factor> factorList = ctx.sessionAttribute("factorList");
205+
ArrayList<Object> list = new ArrayList<Object>(factorList.values());
206+
ctx.sessionAttribute("arrayFactorList", list);
207+
ctx.status(200).json(factor);
208+
} catch (Exception e) {
209+
if(e.equals(null)) {
210+
ctx.render("error.jte");
211+
}
212+
Map<String, Object> jteParams = new HashMap<>();
213+
jteParams.put("error", e.getMessage());
214+
ctx.render("error.jte", jteParams);
195215
}
216+
}
217+
218+
public void enroll_sms_factor(Context ctx) {
219+
String phoneNumber = ctx.formParam("phone_number");
220+
EnrollFactorOptions options;
221+
222+
options = EnrollFactorOptions.builder()
223+
.type("sms")
224+
.phoneNumber(phoneNumber)
225+
.build();
196226

197227
try {
198228
Factor factor = workos.mfa.enrollFactor(options);

java-mfa-example/src/main/jte/enroll_factor.jte

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,47 +42,111 @@
4242
<div class="flex space_between ">
4343
<div class="factor_card">
4444
<h2>Enroll SMS Factor</h2>
45-
<form action="/enroll_factor" method="post">
45+
<form action="/enroll_sms_factor" method="post">
4646
<div class="flex flex_column">
4747
<div class="flex">
4848
<input class="text_input" type="text" id="phone_number" name="phone_number"
49-
placeholder="Phone Number" required>
49+
placeholder="Phone Number">
5050
</div>
51-
5251
<div>
5352
<button type="submit" name="type" value="sms"
54-
class="button button-outline button-sm">Enroll New
55-
Factor</button>
53+
class="button button-outline button-sm" id="sms-factor-submit-btn"
54+
disabled>Enroll New Factor
55+
</button>
5656
</div>
5757
</div>
5858
</form>
5959
</div>
6060

6161
<div class="factor_card">
6262
<h2>Enroll TOTP Factor</h2>
63-
<form method="POST" action="/enroll_factor">
63+
<div>
6464
<div class="flex_column">
6565
<div class="flex">
6666
<input class="text_input" type="text" id="totp_issuer" name="totp_issuer"
67-
placeholder="TOTP Issuer" required>
67+
placeholder="TOTP Issuer">
6868
</div>
6969
<div class="flex">
7070
<input class="text_input" type="text" id="totp_user" name="totp_user"
71-
placeholder="User Email" required>
71+
placeholder="User Email">
7272
</div>
7373

7474
<div>
7575
<button type="submit" name="type" value="totp"
76-
class="button button-outline button-sm">Enroll New Factor</button>
76+
class="button button-outline button-sm" id="totp-factor-submit-btn"
77+
disabled>Enroll New Factor
78+
</button>
7779
</div>
7880
</div>
79-
</form>
81+
</div>
8082
</div>
8183
</div>
8284
</div>
83-
8485
</div>
8586
</div>
86-
87+
<div class="overlay" id="overlay" style="display: none;">
88+
<div class="modal" id="modal">
89+
<button id="close-modal-btn">I've scanned a QR code</button>
90+
</div>
91+
</div>
8792
</body>
8893

94+
<script>
95+
const phoneNumber = document.getElementById("phone_number")
96+
const totpIssuer = document.getElementById("totp_issuer")
97+
const totpUser = document.getElementById("totp_user")
98+
const smsSubmitButton = document.getElementById("sms-factor-submit-btn")
99+
const totpSubmitButton = document.getElementById("totp-factor-submit-btn")
100+
const closeModalBtn = document.getElementById("close-modal-btn")
101+
const overlay = document.getElementById("overlay")
102+
const modal = document.getElementById("modal")
103+
104+
phoneNumber.addEventListener("input", validateSmsForm)
105+
totpIssuer.addEventListener("input", validateTotpForm)
106+
totpUser.addEventListener("input", validateTotpForm)
107+
108+
function validateSmsForm() {
109+
if (phoneNumber.value.trim() !== "") {
110+
smsSubmitButton.disabled = false
111+
} else {
112+
smsSubmitButton.disabled = true
113+
}
114+
}
115+
116+
function validateTotpForm() {
117+
if (totpIssuer.value.trim() !== "" && totpUser.value.trim() !== "") {
118+
totpSubmitButton.disabled = false
119+
} else {
120+
totpSubmitButton.disabled = true
121+
}
122+
}
123+
124+
totpSubmitButton.addEventListener("click", function() {
125+
fetch('/enroll_totp_factor', {
126+
method: 'POST',
127+
headers: {
128+
'Content-Type': 'application/json'
129+
},
130+
body: JSON.stringify({
131+
issuer: totpIssuer.value,
132+
user: totpUser.value,
133+
})
134+
})
135+
.then(response => response.json())
136+
.then(response => {
137+
overlay.style.display = "block"
138+
modal.innerHTML = `
139+
<h2>Scan the QR code</h2>
140+
<p class="qr_code_instructions">Use the authenticator app to scan the QR code. After you scan the code, click 'Continue'.</p>
141+
<img class="qr_code" id="qr_code" src="" alt="qr_code">
142+
<a href="/" class="button button-outline">Continue</a>
143+
`
144+
const qrCode = document.getElementById("qr_code");
145+
qrCode.src = response.totp.qr_code
146+
})
147+
})
148+
149+
closeModalBtn.addEventListener("click", function() {
150+
overlay.style.display = "none"
151+
})
152+
</script>

java-mfa-example/src/main/jte/factor_detail.jte

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,6 @@
5656
@endif
5757
<p>Created At: <code>${createdAt}</code></p>
5858
</div>
59-
60-
@if(type.equals("totp"))
61-
<div class="qr_div">
62-
<img class="qr_code" src="${qrCode}" alt="qr_code">
63-
</div>
64-
@endif
6559
</div>
6660
</div>
6761

java-mfa-example/src/resources/main.css

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
body {
22
font-family: Inter, sans-serif;
33
background-color: #f9f9fb;
4-
54
}
65

76
.container_login {
@@ -130,7 +129,6 @@ h1 {
130129
align-items: center;
131130
position: relative;
132131
bottom: 10%;
133-
/* background-color: #f9f9fb; */
134132
}
135133

136134
.logged_in_div_left {
@@ -212,8 +210,7 @@ div.text_box {
212210
background-color: #f9f9fb;
213211
height: 60px;
214212
padding: 15px 30px 15px 30px;
215-
216-
z-index: 1000;
213+
z-index: 999;
217214
}
218215

219216
.logged_in_nav p {
@@ -277,20 +274,50 @@ pre.prettyprint {
277274
margin-bottom: 20px;
278275
}
279276

280-
.qr_div {
281-
align-self: center;
282-
padding-top: 15px;
283-
}
284-
285-
.qr_code {
286-
width: 7vw;
287-
max-width: 100px;
288-
}
289-
290277
.code-input {
291278
width: 75px;
292279
height: 100px;
293280
margin: 0px 5px 30px 5px;
294281
font-size: 60px;
295282
color: darkslategray;
283+
}
284+
285+
.overlay {
286+
position: fixed;
287+
top: 0;
288+
left: 0;
289+
right: 0;
290+
bottom: 0;
291+
background-color: rgba(0,0,0,0.5);
292+
z-index: 999;
293+
}
294+
295+
.modal {
296+
position: absolute;
297+
top: 50%;
298+
left: 50%;
299+
transform: translate(-50%, -50%);
300+
background-color: #fff;
301+
padding: 25px;
302+
border-radius: 10px;
303+
box-shadow: 0 0 10px rgba(0,0,0,0.5);
304+
z-index: 1000;
305+
display: flex;
306+
flex-direction: column;
307+
justify-content: center;
308+
align-items: center;
309+
}
310+
311+
.qr_code_instructions {
312+
width: 300px;
313+
}
314+
315+
.qr_code {
316+
width: 300px;
317+
margin: 25px;
318+
}
319+
320+
button:disabled {
321+
opacity: 0.5;
322+
pointer-events: none;
296323
}

0 commit comments

Comments
 (0)