Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ba9ce6a
first lambda type
wied03 Nov 21, 2025
cf7f61f
Merge wied03/ENG-3605/mfa-lambda-refactor (#159)
wied03 Nov 25, 2025
9d920af
MFA lambda configuration (#160)
hjaret Dec 3, 2025
68dc46d
ENG-3487: Tenant-scoped IdPs (#158)
spwitt Dec 5, 2025
2ddf867
Merge remote-tracking branch 'origin/develop' into feature/ENG-1111/m…
wied03 Dec 8, 2025
e91a63e
Merge wied03/ENG-3602/mfa-lambda-invocation (#161)
wied03 Dec 8, 2025
a2a681f
Merge wied03/ENG-3603/mfa-retrieve-status-post (#162)
wied03 Dec 9, 2025
1c4944b
missing client stuff
wied03 Dec 9, 2025
b9c3ad2
redo client again
wied03 Dec 9, 2025
d37fb5d
Merge pull request #163 from FusionAuth/wied03/ENG-3606/suspicious_login
wied03 Dec 9, 2025
e5c6a03
Merge wied03/ENG-3608/mfa-change-password (#165)
wied03 Dec 10, 2025
9d4b578
organize app specific params
wied03 Dec 10, 2025
f67e5cc
Merge remote-tracking branch 'origin/develop' into feature/ENG-1111/m…
wied03 Dec 10, 2025
392a2fe
naming advice
wied03 Dec 10, 2025
2c77231
mfa lambda
hjaret Dec 12, 2025
ab4f1d8
Change function name and parameters
wied03 Dec 16, 2025
e0d8d4a
Change function name and parameters
wied03 Dec 16, 2025
8bcc834
PR feedback - lambda classses - new package and names
wied03 Dec 16, 2025
6452942
keep value as mfaTrust within lambda
wied03 Dec 16, 2025
5e78f2a
pass raw JWT all the way in
wied03 Dec 16, 2025
68f1b68
Lambda signature - registration out of context, action and app in
wied03 Dec 17, 2025
56e6ef5
Change context.encodedJWT to context.accessToken
wied03 Dec 17, 2025
67ff90a
rename token to accessToken on status API
wied03 Dec 17, 2025
0fc57b2
Merge remote-tracking branch 'origin/develop' into feature/ENG-1111/m…
wied03 Dec 17, 2025
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
101 changes: 101 additions & 0 deletions src/main/java/io/fusionauth/client/FusionAuthClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
import io.fusionauth.domain.api.twoFactor.TwoFactorStartRequest;
import io.fusionauth.domain.api.twoFactor.TwoFactorStartResponse;
import io.fusionauth.domain.api.twoFactor.TwoFactorStatusResponse;
import io.fusionauth.domain.api.twoFactor.TwoFactorStatusRequest;
import io.fusionauth.domain.api.user.ActionRequest;
import io.fusionauth.domain.api.user.ActionResponse;
import io.fusionauth.domain.api.user.ChangePasswordRequest;
Expand Down Expand Up @@ -513,6 +514,26 @@ public ClientResponse<Void, Errors> checkChangePasswordUsingId(String changePass
.go();
}

/**
* Check to see if the user must obtain a Trust Token Id in order to complete a change password request.
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
* your password, you must obtain a Trust Token by completing a Two-Factor Step-Up authentication.
* <p>
* 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.
*
* @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.
* @param ipAddress (Optional) IP address of the user changing their password. This is used for MFA risk assessment.
* @return The ClientResponse object.
*/
public ClientResponse<Void, Errors> checkChangePasswordUsingIdAndIPAddress(String changePasswordId, String ipAddress) {
return startAnonymous(Void.TYPE, Errors.class)
.uri("/api/user/change-password")
.urlSegment(changePasswordId)
.urlParameter("ipAddress", ipAddress)
.get()
.go();
}

/**
* Check to see if the user must obtain a Trust Token Id in order to complete a change password request.
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
Expand All @@ -531,6 +552,26 @@ public ClientResponse<Void, Errors> checkChangePasswordUsingJWT(String encodedJW
.go();
}

/**
* Check to see if the user must obtain a Trust Token Id in order to complete a change password request.
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
* your password, you must obtain a Trust Token by completing a Two-Factor Step-Up authentication.
* <p>
* 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.
*
* @param encodedJWT The encoded JWT (access token).
* @param ipAddress (Optional) IP address of the user changing their password. This is used for MFA risk assessment.
* @return The ClientResponse object.
*/
public ClientResponse<Void, Errors> checkChangePasswordUsingJWTAndIPAddress(String encodedJWT, String ipAddress) {
return startAnonymous(Void.TYPE, Errors.class)
.uri("/api/user/change-password")
.authorization("Bearer " + encodedJWT)
.urlParameter("ipAddress", ipAddress)
.get()
.go();
}

/**
* Check to see if the user must obtain a Trust Request Id in order to complete a change password request.
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
Expand All @@ -549,6 +590,26 @@ public ClientResponse<Void, Errors> checkChangePasswordUsingLoginId(String login
.go();
}

/**
* Check to see if the user must obtain a Trust Request Id in order to complete a change password request.
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
* your password, you must obtain a Trust Request Id by completing a Two-Factor Step-Up authentication.
* <p>
* 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.
*
* @param loginId The loginId (email or username) of the User that you intend to change the password for.
* @param ipAddress (Optional) IP address of the user changing their password. This is used for MFA risk assessment.
* @return The ClientResponse object.
*/
public ClientResponse<Void, Errors> checkChangePasswordUsingLoginIdAndIPAddress(String loginId, String ipAddress) {
return start(Void.TYPE, Errors.class)
.uri("/api/user/change-password")
.urlParameter("loginId", loginId)
.urlParameter("ipAddress", ipAddress)
.get()
.go();
}

/**
* Check to see if the user must obtain a Trust Request Id in order to complete a change password request.
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
Expand All @@ -569,6 +630,28 @@ public ClientResponse<Void, Errors> checkChangePasswordUsingLoginIdAndLoginIdTyp
.go();
}

/**
* Check to see if the user must obtain a Trust Request Id in order to complete a change password request.
* When a user has enabled Two-Factor authentication, before you are allowed to use the Change Password API to change
* your password, you must obtain a Trust Request Id by completing a Two-Factor Step-Up authentication.
* <p>
* 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.
*
* @param loginId The loginId of the User that you intend to change the password for.
* @param loginIdTypes The identity types that FusionAuth will compare the loginId to.
* @param ipAddress (Optional) IP address of the user changing their password. This is used for MFA risk assessment.
* @return The ClientResponse object.
*/
public ClientResponse<Void, Errors> checkChangePasswordUsingLoginIdAndLoginIdTypesAndIPAddress(String loginId, List<String> loginIdTypes, String ipAddress) {
return start(Void.TYPE, Errors.class)
.uri("/api/user/change-password")
.urlParameter("loginId", loginId)
.urlParameter("loginIdTypes", loginIdTypes)
.urlParameter("ipAddress", ipAddress)
.get()
.go();
}

/**
* Make a Client Credentials grant request to obtain an access token.
*
Expand Down Expand Up @@ -4125,6 +4208,24 @@ public ClientResponse<TwoFactorStatusResponse, Errors> retrieveTwoFactorStatus(U
.go();
}

/**
* Retrieve a user's two-factor status.
* <p>
* This can be used to see if a user will need to complete a two-factor challenge to complete a login,
* and optionally identify the state of the two-factor trust across various applications. This operation
* provides more payload options than retrieveTwoFactorStatus.
*
* @param request The request object that contains all the information used to check the status.
* @return The ClientResponse object.
*/
public ClientResponse<TwoFactorStatusResponse, Errors> retrieveTwoFactorStatusWithRequest(TwoFactorStatusRequest request) {
return start(TwoFactorStatusResponse.class, Errors.class)
.uri("/api/two-factor/status")
.bodyHandler(new JSONBodyHandler(request, objectMapper()))
.post()
.go();
}

/**
* Retrieves the user for the given Id.
*
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/io/fusionauth/domain/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@ public static class LambdaConfiguration {

public UUID idTokenPopulateId;

public UUID multiFactorRequirementId;

public UUID samlv2PopulateId;

public UUID selfServiceRegistrationValidationId;
Expand All @@ -467,6 +469,7 @@ public LambdaConfiguration() {
public LambdaConfiguration(LambdaConfiguration other) {
this.accessTokenPopulateId = other.accessTokenPopulateId;
this.idTokenPopulateId = other.idTokenPopulateId;
this.multiFactorRequirementId = other.multiFactorRequirementId;
this.samlv2PopulateId = other.samlv2PopulateId;
this.selfServiceRegistrationValidationId = other.selfServiceRegistrationValidationId;
this.userinfoPopulateId = other.userinfoPopulateId;
Expand All @@ -483,14 +486,15 @@ public boolean equals(Object o) {
LambdaConfiguration that = (LambdaConfiguration) o;
return Objects.equals(accessTokenPopulateId, that.accessTokenPopulateId) &&
Objects.equals(idTokenPopulateId, that.idTokenPopulateId) &&
Objects.equals(multiFactorRequirementId, that.multiFactorRequirementId) &&
Objects.equals(samlv2PopulateId, that.samlv2PopulateId) &&
Objects.equals(selfServiceRegistrationValidationId, that.selfServiceRegistrationValidationId) &&
Objects.equals(userinfoPopulateId, that.userinfoPopulateId);
}

@Override
public int hashCode() {
return Objects.hash(accessTokenPopulateId, idTokenPopulateId, samlv2PopulateId, selfServiceRegistrationValidationId, userinfoPopulateId);
return Objects.hash(accessTokenPopulateId, idTokenPopulateId, multiFactorRequirementId, samlv2PopulateId, selfServiceRegistrationValidationId, userinfoPopulateId);
}

@Override
Expand Down
22 changes: 20 additions & 2 deletions src/main/java/io/fusionauth/domain/LambdaType.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019-2024, FusionAuth, All Rights Reserved
* Copyright (c) 2019-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,6 +22,7 @@
*/
@SuppressWarnings("ALL")
public enum LambdaType {
// This is an ordinal enum, so make sure new values are added to the end
// @formatter:off
JWTPopulate("populate", "" +
//language=JavaScript
Expand Down Expand Up @@ -496,7 +497,24 @@ public enum LambdaType {
"\n" +
" console.info('Hello World!');" +
"\n" +
"}\n");
"}\n"),
MFARequirement("checkRequired", "" +
//language=JavaScript
"// Check whether MFA is required, for a particular action, user, and application, in a given context.\n" +
"function checkRequired(result, user, registration, context) {\n" +
" // When writing a lambda we've added a few helpers to make life easier.\n" +
" // console.info('Hello World'); # This will create an EventLog of type Information\n" +
" // console.error('Not good.'); # This will create an EventLog of type Error\n" +
" // console.debug('Step 42 completed.'); # This will create an EventLog of type Debug\n" +
" // \n" +
" // To dump an entire object to the EventLog you can use JSON.stringify, for example: \n" +
" // console.info(JSON.stringify(user)); \n" +
"\n" +
" // Happy coding! Perform your MFA requirement check here.\n" +
"\n" +
" console.info('Hello World!');" +
"\n" +
"}\n");
// @formatter:on

private final String example;
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/io/fusionauth/domain/MultiFactorAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
package io.fusionauth.domain;

/**
* Communicate various actions/contexts in which multi-factor authentication can be used.
*/
public enum MultiFactorAction {
changePassword,
login,
stepUp
}
6 changes: 6 additions & 0 deletions src/main/java/io/fusionauth/domain/Tenant.java
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@ public JWTConfiguration lookupJWTConfiguration(Application application) {
return jwtConfiguration;
}

/**
* Lookup the login MFA policy
*
* @param application application to examine
* @return policy in effect
*/
@JsonIgnore
public MultiFactorLoginPolicy lookupMultiFactorLoginPolicy(Application application) {
if (application != null && application.multiFactorConfiguration.loginPolicy != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022-2024, FusionAuth, All Rights Reserved
* Copyright (c) 2022-2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,6 +27,8 @@
public class TenantLambdaConfiguration implements Buildable<TenantLambdaConfiguration> {
public UUID loginValidationId;

public UUID multiFactorRequirementId;

public UUID scimEnterpriseUserRequestConverterId;

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

public TenantLambdaConfiguration(TenantLambdaConfiguration other) {
this.loginValidationId = other.loginValidationId;
this.multiFactorRequirementId = other.multiFactorRequirementId;
this.scimEnterpriseUserRequestConverterId = other.scimEnterpriseUserRequestConverterId;
this.scimEnterpriseUserResponseConverterId = other.scimEnterpriseUserResponseConverterId;
this.scimGroupRequestConverterId = other.scimGroupRequestConverterId;
Expand All @@ -63,6 +66,7 @@ public boolean equals(Object o) {
}
TenantLambdaConfiguration that = (TenantLambdaConfiguration) o;
return Objects.equals(loginValidationId, that.loginValidationId) &&
Objects.equals(multiFactorRequirementId, that.multiFactorRequirementId) &&
Objects.equals(scimEnterpriseUserRequestConverterId, that.scimEnterpriseUserRequestConverterId) &&
Objects.equals(scimEnterpriseUserResponseConverterId, that.scimEnterpriseUserResponseConverterId) &&
Objects.equals(scimGroupRequestConverterId, that.scimGroupRequestConverterId) &&
Expand All @@ -74,6 +78,7 @@ public boolean equals(Object o) {
@Override
public int hashCode() {
return Objects.hash(loginValidationId,
multiFactorRequirementId,
scimEnterpriseUserRequestConverterId,
scimEnterpriseUserResponseConverterId,
scimGroupRequestConverterId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
package io.fusionauth.domain.api.twoFactor;

import java.util.UUID;

import com.inversoft.json.JacksonConstructor;
import io.fusionauth.domain.MultiFactorAction;
import io.fusionauth.domain.api.BaseEventRequest;

/**
* Check the status of two-factor authentication for a user, with more options than on a GET request.
*/
public class TwoFactorStatusRequest extends BaseEventRequest {
// required
public final UUID userId;

public String accessToken;

public MultiFactorAction action = MultiFactorAction.login;

public UUID applicationId;

public String twoFactorTrustId;

public TwoFactorStatusRequest(UUID userId) {
this.userId = userId;
}

@JacksonConstructor
private TwoFactorStatusRequest() {
// will be overridden by Jackson
this.userId = null;
}
}
Loading