Step-Up MFA
PropelAuth's Step-Up MFA allows PropelAuth users on the Growth or Growth+ plans to present an additional authentication challenge to a user when they attempt to perform higher-risk actions or access more sensitive resources with your app, even if they have already successfully logged in.
Using Step-Up MFA
You can use the Step-Up MFA APIs to require users to verify their MFA when they perform certain actions in your app. Here is a basic flow of how to use Step-Up MFA:
-
User Requests to Perform an Action: A user requests to perform an action that requires Step-Up MFA, such as accessing sensitive information or performing a high-risk action.
-
Prompt for TOTP Code: Prompt the user to enter the code generated by their TOTP app (such as Google Authenticator or Authy). The UI for this step is built by you.
-
Verify TOTP Code: Once you receive the TOTP code from the user, you can verify it using the Verify TOTP Challenge API. If successful, the API will respond with a Step-Up Grant. This grant is not required for this specific flow, but we'll cover how to use it later.
-
Perform the Action: Once the user has successfully verified their MFA, you can perform the action they requested.
This flow works well for most use cases, but let's say you only want to require the user to verify their MFA once every 10 minutes. We cover that flow, as well as a few others, in the Additional Step-Up MFA Flows section.
Step-Up MFA Example
Let's build a basic example of how to use Step-Up MFA in your app. In this example, we'll build a component that allows users to withdraw funds. But before they're able to do so, we'll require the user to verify their MFA. Let's start by creating an API route in our backend to handle the transfer as well as verify the user's TOTP code using the Verify TOTP Challenge API. In this example we'll be using PropelAuth's FastAPI library to help, but this is doable with any of our backend frameworks.
Creating a Backend Route
Let's create a /api/withdrawfunds
route. We want to protect this route with the auth.require_user dependency so that only logged in users can access it. If a valid access token is provided, it will return a User object which includes the user_id
of the user. If not, the request is rejected with a 401 status code.
@router.post("/api/withdrawfunds")
async def verify_totp(request: Request, current_user: User = Depends(auth.require_user)):
raw_body = await request.body()
body = json.loads(raw_body.decode())
is_code_valid = auth.verify_step_up_totp_challenge(
action_type="WITHDRAW_FUNDS",
user_id=current_user.user_id,
code=body['code'],
grant_type="ONE_TIME_USE",
valid_for_seconds=60
)
if is_code_valid:
## withdraw funds
return "success!"
else:
raise HTTPException(status_code=403, detail="Incorrect code")
Above, we first check if the TOTP code is valid using the Verify TOTP Challenge API. If it is, we'll withdraw the user's funds.
Creating a Frontend Component
On our frontend, let's first build a button that will withdraw money. When the user clicks the button, we'll prompt them to enter the code from their TOTP app.
import React, { useState } from "react";
import { useAuthInfo } from "@propelauth/react";
export default function WithdrawFunds() {
const [isMfaModalVisible, setIsMfaModalVisible] = useState(false);
const [mfaCode, setMfaCode] = useState("");
const [error, setError] = useState("");
const handleClearMfa = () => {
setIsMfaModalVisible(false);
setError("");
setMfaCode("");
};
return (
<div>
<h2>Step-Up MFA Demo</h2>
<button onClick={() => setIsMfaModalVisible(true)}>
Withdraw Funds
</button>
{isMfaModalVisible && (
<div>
<h2>Step Up MFA Required</h2>
<label htmlFor="mfa-code">MFA Code:</label>
<input
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value)}
placeholder="Enter 6-digit code"
/>
{/* TODO: Create handleWithdrawFunds function */}
<button onClick={handleWithdrawFunds}>
Withdraw Funds
</button>
<button onClick={handleClearMfa}>
Cancel
</button>
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
</div>
)}
</div>
);
}
We now have a button that, when clicked, will prompt for a TOTP code! But nothing happens yet when we submit the code, let's do something about that.
Let's now create a request to our backend that will hit the /api/withdrawfunds
route we created earlier. Since we protected our API with require_user
, we need to generate an access token that we'll pass along in the authorization header of the request. We'll be using the @propelauth/react library in this example, but this is also doable with our other frontend and full-stack libraries.
const { tokens } = useAuthInfo();
const handleWithdrawFunds = async () => {
if (!mfaCode) {
setError("Please enter the MFA code.");
return;
}
setError("");
const accessToken = await tokens.getAccessToken();
const response = await fetch('/api/withdrawfunds', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: mfaCode,
// your other fields for this request
}),
});
if (response.ok) {
handleClearMfa()
alert("Your funds have been withdrawn successfully!");
} else {
setError("Invalid MFA code. Please try again.");
}
};
Ok we have everything set up! Let's test this out. If we enter in an incorrect code, we get an error:
But if we enter the correct TOTP code, we get to withdraw funds!
Additional Step-Up MFA Flows
The above example works well when validating the TOTP code at the same time as performing the action, called Just-in-time Step-up MFA. However, what if you want to validate the user's TOTP code and confirm with the user that they want to perform the action before actually performing it? For that use case, we can use the Step-Up Grant returned from Verify TOTP Challenge API in our request to the Verify Step-Up Grant API.
For this case, our flow changes a bit:
-
User Requests to Perform an Action: A user requests to perform an action that requires Step-Up MFA, such as accessing sensitive information or performing a high-risk action.
-
Prompt for TOTP Code: Prompt the user to enter the code generated by their TOTP app (such as Google Authenticator or Authy). The UI for this step is built by you.
-
Verify TOTP Code: Once you receive the TOTP code from the user, you can verify it using the Verify TOTP Challenge API. Make sure to include an
action_type
that corresponds with the action the user is requesting. If successful, the API will respond with a Step-Up Grant. Go ahead and store this somewhere, such as in the state. -
Confirm the user wants to perform the action: Prompt the user again to confirm that they want to perform the action.
-
Perform the Action: Include the Step-Up Grant from step 3 in your request to your backend to perform the action. In your backend, use the Verify Step-Up Grant API to verify the Step-Up Grant (using the same
action_type
from the third step) before performing the action.
In code, this would look something like this:
import React, { useState } from 'react';
import { useAuthInfo } from '@propelauth/react';
function WithdrawFunds() {
const { tokens } = useAuthInfo();
const [ stepUpGrant, setStepUpGrant ] = useState('');
// Check if we have a Step-Up grant for the user.
// If not, verify their TOTP code
if (!stepUpGrant) {
// This component will collect the TOTP code
// It will also post to the backend to get a stepUpGrant, and then set it
return <CollectTotpCode setStepUpGrant={setStepUpGrant} />
} else {
// Include the step up grant in the withdraw funds request
const withdrawFunds = async () => {
await postRequestToWithdrawFunds({
step_up_grant: stepUpGrant,
// ...your other fields for this request
})
setStepUpGrant('')
};
return <Button onClick={() => withdrawFunds()}>Withdraw Funds</Button>
}
}
Time Based Step-Up MFA
There are instances where a user may want to perform multiple actions in a short period of time that require MFA. For example, a user may want to perform multiple high-risk actions in a row, such as accessing a sensitive dashboard that has multiple actions the user would want to take. You can gate their access to the dashboard and then let them perform multiple actions without re-entering MFA each time.
To solve for this, we included two properties in the Verify TOTP Challenge API:
-
grant_type
: This property can be set to eitherONE_TIME_USE
orTIME_BASED
. -
valid_for_seconds
: Allows us to customize how long the Step-Up Grant will be valid for.
Setting grant_type
to TIME_BASED
will allow us to use the Step-Up Grant as many times as we want before it expires. In order to keep re-using it, we should save it in our app's state.
When the user requests to perform an action that requires Step-Up MFA, you can check if the user already has a Step-Up Grant. If they do, you can verify it using the Verify Step-Up Grant API. If the grant is valid, you can let the user proceed with the action. Otherwise, you'll need to prompt the user to verify their MFA again.
Action Type Based Step-Up MFA
Let's say you have some actions that are more sensitive than others. For some actions, such as transferring money, you want the Step-Up Grant to only be valid for 5 minutes while other actions it can be valid for 10 minutes. For this, you can use an Action Type.
You can set the action_type
parameter when using the Verify TOTP Challenge API to specify the action type. This will then return a Step-Up Grant that is valid just for that specific action type.
You can then save the Step-Up Grant in your app's state. The next time your user attempts to do a sensitive action, you can first check if a Step-Up Grant exists for that action type.
When you go to verify the Step-Up Grant using the Verify Step-Up Grant API, you can include that action type in the action_type
parameter. This will check if the grant is valid for that action type as well as if it has not expired.