Skip to content

Commit 79ba9b9

Browse files
wied03hjaretspwitt
authored
Merge feature/ENG-1111/mfa-lambda (#166)
* first lambda type * Merge wied03/ENG-3605/mfa-lambda-refactor (#159) * carriage return * reduce client blast radius * MFA lambda configuration (#160) * ENG-3487: Tenant-scoped IdPs (#158) * update domain for tenantId on IdPs (#153) * IdP search updates * add tenantId to equals and hashCode (#155) * add tenantId to IdP lookup by managed domain response * Merge wied03/ENG-3602/mfa-lambda-invocation (#161) * propagate client changes * propagate client changes 2 * remove service dependency * client code update from Javadoc * Merge wied03/ENG-3603/mfa-retrieve-status-post (#162) * propagate client changes * propagate client changes 2 * remove service dependency * client code update from Javadoc * client generation/new method * better method name * missing client stuff * redo client again * Merge wied03/ENG-3608/mfa-change-password (#165) * add IP address client overload * forgot to update method names * organize app specific params * naming advice * mfa lambda * Change function name and parameters * Change function name and parameters * PR feedback - lambda classses - new package and names * keep value as mfaTrust within lambda * pass raw JWT all the way in * Lambda signature - registration out of context, action and app in * Change context.encodedJWT to context.accessToken * rename token to accessToken on status API --------- Co-authored-by: Jaret Hendrickson <[email protected]> Co-authored-by: Spencer Witt <[email protected]>
1 parent 68e9548 commit 79ba9b9

File tree

12 files changed

+405
-5
lines changed

12 files changed

+405
-5
lines changed

src/main/java/io/fusionauth/client/FusionAuthClient.java

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@
218218
import io.fusionauth.domain.api.twoFactor.TwoFactorStartRequest;
219219
import io.fusionauth.domain.api.twoFactor.TwoFactorStartResponse;
220220
import io.fusionauth.domain.api.twoFactor.TwoFactorStatusResponse;
221+
import io.fusionauth.domain.api.twoFactor.TwoFactorStatusRequest;
221222
import io.fusionauth.domain.api.user.ActionRequest;
222223
import io.fusionauth.domain.api.user.ActionResponse;
223224
import io.fusionauth.domain.api.user.ChangePasswordRequest;
@@ -513,6 +514,26 @@ public ClientResponse<Void, Errors> checkChangePasswordUsingId(String changePass
513514
.go();
514515
}
515516

517+
/**
518+
* Check to see if the user must obtain a Trust Token Id in order to complete a change password request.
519+
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
520+
* your password, you must obtain a Trust Token by completing a Two-Factor Step-Up authentication.
521+
* <p>
522+
* An HTTP status code of 400 with a general error code of [TrustTokenRequired] indicates that a Trust Token is required to make a POST request to this API.
523+
*
524+
* @param changePasswordId The change password Id used to find the user. This value is generated by FusionAuth once the change password workflow has been initiated.
525+
* @param ipAddress (Optional) IP address of the user changing their password. This is used for MFA risk assessment.
526+
* @return The ClientResponse object.
527+
*/
528+
public ClientResponse<Void, Errors> checkChangePasswordUsingIdAndIPAddress(String changePasswordId, String ipAddress) {
529+
return startAnonymous(Void.TYPE, Errors.class)
530+
.uri("/api/user/change-password")
531+
.urlSegment(changePasswordId)
532+
.urlParameter("ipAddress", ipAddress)
533+
.get()
534+
.go();
535+
}
536+
516537
/**
517538
* Check to see if the user must obtain a Trust Token Id in order to complete a change password request.
518539
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
@@ -531,6 +552,26 @@ public ClientResponse<Void, Errors> checkChangePasswordUsingJWT(String encodedJW
531552
.go();
532553
}
533554

555+
/**
556+
* Check to see if the user must obtain a Trust Token Id in order to complete a change password request.
557+
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
558+
* your password, you must obtain a Trust Token by completing a Two-Factor Step-Up authentication.
559+
* <p>
560+
* An HTTP status code of 400 with a general error code of [TrustTokenRequired] indicates that a Trust Token is required to make a POST request to this API.
561+
*
562+
* @param encodedJWT The encoded JWT (access token).
563+
* @param ipAddress (Optional) IP address of the user changing their password. This is used for MFA risk assessment.
564+
* @return The ClientResponse object.
565+
*/
566+
public ClientResponse<Void, Errors> checkChangePasswordUsingJWTAndIPAddress(String encodedJWT, String ipAddress) {
567+
return startAnonymous(Void.TYPE, Errors.class)
568+
.uri("/api/user/change-password")
569+
.authorization("Bearer " + encodedJWT)
570+
.urlParameter("ipAddress", ipAddress)
571+
.get()
572+
.go();
573+
}
574+
534575
/**
535576
* Check to see if the user must obtain a Trust Request Id in order to complete a change password request.
536577
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
@@ -549,6 +590,26 @@ public ClientResponse<Void, Errors> checkChangePasswordUsingLoginId(String login
549590
.go();
550591
}
551592

593+
/**
594+
* Check to see if the user must obtain a Trust Request Id in order to complete a change password request.
595+
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
596+
* your password, you must obtain a Trust Request Id by completing a Two-Factor Step-Up authentication.
597+
* <p>
598+
* An HTTP status code of 400 with a general error code of [TrustTokenRequired] indicates that a Trust Token is required to make a POST request to this API.
599+
*
600+
* @param loginId The loginId (email or username) of the User that you intend to change the password for.
601+
* @param ipAddress (Optional) IP address of the user changing their password. This is used for MFA risk assessment.
602+
* @return The ClientResponse object.
603+
*/
604+
public ClientResponse<Void, Errors> checkChangePasswordUsingLoginIdAndIPAddress(String loginId, String ipAddress) {
605+
return start(Void.TYPE, Errors.class)
606+
.uri("/api/user/change-password")
607+
.urlParameter("loginId", loginId)
608+
.urlParameter("ipAddress", ipAddress)
609+
.get()
610+
.go();
611+
}
612+
552613
/**
553614
* Check to see if the user must obtain a Trust Request Id in order to complete a change password request.
554615
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
@@ -569,6 +630,28 @@ public ClientResponse<Void, Errors> checkChangePasswordUsingLoginIdAndLoginIdTyp
569630
.go();
570631
}
571632

633+
/**
634+
* Check to see if the user must obtain a Trust Request Id in order to complete a change password request.
635+
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
636+
* your password, you must obtain a Trust Request Id by completing a Two-Factor Step-Up authentication.
637+
* <p>
638+
* An HTTP status code of 400 with a general error code of [TrustTokenRequired] indicates that a Trust Token is required to make a POST request to this API.
639+
*
640+
* @param loginId The loginId of the User that you intend to change the password for.
641+
* @param loginIdTypes The identity types that FusionAuth will compare the loginId to.
642+
* @param ipAddress (Optional) IP address of the user changing their password. This is used for MFA risk assessment.
643+
* @return The ClientResponse object.
644+
*/
645+
public ClientResponse<Void, Errors> checkChangePasswordUsingLoginIdAndLoginIdTypesAndIPAddress(String loginId, List<String> loginIdTypes, String ipAddress) {
646+
return start(Void.TYPE, Errors.class)
647+
.uri("/api/user/change-password")
648+
.urlParameter("loginId", loginId)
649+
.urlParameter("loginIdTypes", loginIdTypes)
650+
.urlParameter("ipAddress", ipAddress)
651+
.get()
652+
.go();
653+
}
654+
572655
/**
573656
* Make a Client Credentials grant request to obtain an access token.
574657
*
@@ -4125,6 +4208,24 @@ public ClientResponse<TwoFactorStatusResponse, Errors> retrieveTwoFactorStatus(U
41254208
.go();
41264209
}
41274210

4211+
/**
4212+
* Retrieve a user's two-factor status.
4213+
* <p>
4214+
* This can be used to see if a user will need to complete a two-factor challenge to complete a login,
4215+
* and optionally identify the state of the two-factor trust across various applications. This operation
4216+
* provides more payload options than retrieveTwoFactorStatus.
4217+
*
4218+
* @param request The request object that contains all the information used to check the status.
4219+
* @return The ClientResponse object.
4220+
*/
4221+
public ClientResponse<TwoFactorStatusResponse, Errors> retrieveTwoFactorStatusWithRequest(TwoFactorStatusRequest request) {
4222+
return start(TwoFactorStatusResponse.class, Errors.class)
4223+
.uri("/api/two-factor/status")
4224+
.bodyHandler(new JSONBodyHandler(request, objectMapper()))
4225+
.post()
4226+
.go();
4227+
}
4228+
41284229
/**
41294230
* Retrieves the user for the given Id.
41304231
*

src/main/java/io/fusionauth/domain/Application.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,8 @@ public static class LambdaConfiguration {
454454

455455
public UUID idTokenPopulateId;
456456

457+
public UUID multiFactorRequirementId;
458+
457459
public UUID samlv2PopulateId;
458460

459461
public UUID selfServiceRegistrationValidationId;
@@ -467,6 +469,7 @@ public LambdaConfiguration() {
467469
public LambdaConfiguration(LambdaConfiguration other) {
468470
this.accessTokenPopulateId = other.accessTokenPopulateId;
469471
this.idTokenPopulateId = other.idTokenPopulateId;
472+
this.multiFactorRequirementId = other.multiFactorRequirementId;
470473
this.samlv2PopulateId = other.samlv2PopulateId;
471474
this.selfServiceRegistrationValidationId = other.selfServiceRegistrationValidationId;
472475
this.userinfoPopulateId = other.userinfoPopulateId;
@@ -483,14 +486,15 @@ public boolean equals(Object o) {
483486
LambdaConfiguration that = (LambdaConfiguration) o;
484487
return Objects.equals(accessTokenPopulateId, that.accessTokenPopulateId) &&
485488
Objects.equals(idTokenPopulateId, that.idTokenPopulateId) &&
489+
Objects.equals(multiFactorRequirementId, that.multiFactorRequirementId) &&
486490
Objects.equals(samlv2PopulateId, that.samlv2PopulateId) &&
487491
Objects.equals(selfServiceRegistrationValidationId, that.selfServiceRegistrationValidationId) &&
488492
Objects.equals(userinfoPopulateId, that.userinfoPopulateId);
489493
}
490494

491495
@Override
492496
public int hashCode() {
493-
return Objects.hash(accessTokenPopulateId, idTokenPopulateId, samlv2PopulateId, selfServiceRegistrationValidationId, userinfoPopulateId);
497+
return Objects.hash(accessTokenPopulateId, idTokenPopulateId, multiFactorRequirementId, samlv2PopulateId, selfServiceRegistrationValidationId, userinfoPopulateId);
494498
}
495499

496500
@Override

src/main/java/io/fusionauth/domain/LambdaType.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019-2024, FusionAuth, All Rights Reserved
2+
* Copyright (c) 2019-2025, FusionAuth, All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@
2222
*/
2323
@SuppressWarnings("ALL")
2424
public enum LambdaType {
25+
// This is an ordinal enum, so make sure new values are added to the end
2526
// @formatter:off
2627
JWTPopulate("populate", "" +
2728
//language=JavaScript
@@ -496,7 +497,24 @@ public enum LambdaType {
496497
"\n" +
497498
" console.info('Hello World!');" +
498499
"\n" +
499-
"}\n");
500+
"}\n"),
501+
MFARequirement("checkRequired", "" +
502+
//language=JavaScript
503+
"// Check whether MFA is required, for a particular action, user, and application, in a given context.\n" +
504+
"function checkRequired(result, user, registration, context) {\n" +
505+
" // When writing a lambda we've added a few helpers to make life easier.\n" +
506+
" // console.info('Hello World'); # This will create an EventLog of type Information\n" +
507+
" // console.error('Not good.'); # This will create an EventLog of type Error\n" +
508+
" // console.debug('Step 42 completed.'); # This will create an EventLog of type Debug\n" +
509+
" // \n" +
510+
" // To dump an entire object to the EventLog you can use JSON.stringify, for example: \n" +
511+
" // console.info(JSON.stringify(user)); \n" +
512+
"\n" +
513+
" // Happy coding! Perform your MFA requirement check here.\n" +
514+
"\n" +
515+
" console.info('Hello World!');" +
516+
"\n" +
517+
"}\n");
500518
// @formatter:on
501519

502520
private final String example;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2025, FusionAuth, All Rights Reserved
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing,
11+
* software distributed under the License is distributed on an
12+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific
14+
* language governing permissions and limitations under the License.
15+
*/
16+
package io.fusionauth.domain;
17+
18+
/**
19+
* Communicate various actions/contexts in which multi-factor authentication can be used.
20+
*/
21+
public enum MultiFactorAction {
22+
changePassword,
23+
login,
24+
stepUp
25+
}

src/main/java/io/fusionauth/domain/Tenant.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,12 @@ public JWTConfiguration lookupJWTConfiguration(Application application) {
267267
return jwtConfiguration;
268268
}
269269

270+
/**
271+
* Lookup the login MFA policy
272+
*
273+
* @param application application to examine
274+
* @return policy in effect
275+
*/
270276
@JsonIgnore
271277
public MultiFactorLoginPolicy lookupMultiFactorLoginPolicy(Application application) {
272278
if (application != null && application.multiFactorConfiguration.loginPolicy != null) {

src/main/java/io/fusionauth/domain/TenantLambdaConfiguration.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022-2024, FusionAuth, All Rights Reserved
2+
* Copyright (c) 2022-2025, FusionAuth, All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,8 @@
2727
public class TenantLambdaConfiguration implements Buildable<TenantLambdaConfiguration> {
2828
public UUID loginValidationId;
2929

30+
public UUID multiFactorRequirementId;
31+
3032
public UUID scimEnterpriseUserRequestConverterId;
3133

3234
public UUID scimEnterpriseUserResponseConverterId;
@@ -45,6 +47,7 @@ public TenantLambdaConfiguration() {
4547

4648
public TenantLambdaConfiguration(TenantLambdaConfiguration other) {
4749
this.loginValidationId = other.loginValidationId;
50+
this.multiFactorRequirementId = other.multiFactorRequirementId;
4851
this.scimEnterpriseUserRequestConverterId = other.scimEnterpriseUserRequestConverterId;
4952
this.scimEnterpriseUserResponseConverterId = other.scimEnterpriseUserResponseConverterId;
5053
this.scimGroupRequestConverterId = other.scimGroupRequestConverterId;
@@ -63,6 +66,7 @@ public boolean equals(Object o) {
6366
}
6467
TenantLambdaConfiguration that = (TenantLambdaConfiguration) o;
6568
return Objects.equals(loginValidationId, that.loginValidationId) &&
69+
Objects.equals(multiFactorRequirementId, that.multiFactorRequirementId) &&
6670
Objects.equals(scimEnterpriseUserRequestConverterId, that.scimEnterpriseUserRequestConverterId) &&
6771
Objects.equals(scimEnterpriseUserResponseConverterId, that.scimEnterpriseUserResponseConverterId) &&
6872
Objects.equals(scimGroupRequestConverterId, that.scimGroupRequestConverterId) &&
@@ -74,6 +78,7 @@ public boolean equals(Object o) {
7478
@Override
7579
public int hashCode() {
7680
return Objects.hash(loginValidationId,
81+
multiFactorRequirementId,
7782
scimEnterpriseUserRequestConverterId,
7883
scimEnterpriseUserResponseConverterId,
7984
scimGroupRequestConverterId,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2025, FusionAuth, All Rights Reserved
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing,
11+
* software distributed under the License is distributed on an
12+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific
14+
* language governing permissions and limitations under the License.
15+
*/
16+
package io.fusionauth.domain.api.twoFactor;
17+
18+
import java.util.UUID;
19+
20+
import com.inversoft.json.JacksonConstructor;
21+
import io.fusionauth.domain.MultiFactorAction;
22+
import io.fusionauth.domain.api.BaseEventRequest;
23+
24+
/**
25+
* Check the status of two-factor authentication for a user, with more options than on a GET request.
26+
*/
27+
public class TwoFactorStatusRequest extends BaseEventRequest {
28+
// required
29+
public final UUID userId;
30+
31+
public String accessToken;
32+
33+
public MultiFactorAction action = MultiFactorAction.login;
34+
35+
public UUID applicationId;
36+
37+
public String twoFactorTrustId;
38+
39+
public TwoFactorStatusRequest(UUID userId) {
40+
this.userId = userId;
41+
}
42+
43+
@JacksonConstructor
44+
private TwoFactorStatusRequest() {
45+
// will be overridden by Jackson
46+
this.userId = null;
47+
}
48+
}

0 commit comments

Comments
 (0)