-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
Is your feature request related to a problem?
I have a use-case where I want the MFA to be required and requested more frequently for apps where sensitive data can be manipulated.
But in other apps I want it to be required, but not asked as many times.
The MFA has the option for Last validation threshold which gives the impression of doing this.
However the MFA cookie also stores the stage it was used for. So it will not work when you have a second flow and/or stage.
Describe the solution you'd like
I would like to be able to use multiple MFA stages and share the MFA threshold.
A way that I though about fixing it is as follows:
In the authenticator_validate stage. Create a MFA cookie with:
- auth device
- issued at
- Possible still an expiration, but not the same as the stage's limit.
Then the stage can compare the issued at date against its own set threshold. That way the same token can be reused across stages. It would also need to check if the device used is a device allowed for this stage.
See the set_valid_mfa_cookie
authentik/authentik/stages/authenticator_validate/stage.py
Lines 395 to 420 in 291b35c
| def set_valid_mfa_cookie(self, device: Device) -> HttpResponse: | |
| """Set an MFA cookie to allow users to skip MFA validation in this context (browser) | |
| The cookie is JWT which is signed with a hash of the secret key and the UID of the stage""" | |
| stage: AuthenticatorValidateStage = self.executor.current_stage | |
| delta = timedelta_from_string(stage.last_auth_threshold) | |
| if delta.total_seconds() < 1: | |
| self.logger.info("Not setting MFA cookie since threshold is not set.") | |
| return self.executor.stage_ok() | |
| expiry = datetime.now() + delta | |
| cookie_payload = { | |
| "device": device.pk, | |
| "stage": stage.pk.hex, | |
| "exp": expiry.timestamp(), | |
| } | |
| response = self.executor.stage_ok() | |
| cookie = encode(cookie_payload, self.cookie_jwt_key) | |
| response.set_cookie( | |
| COOKIE_NAME_MFA, | |
| cookie, | |
| expires=expiry, | |
| path=settings.SESSION_COOKIE_PATH, | |
| domain=settings.SESSION_COOKIE_DOMAIN, | |
| samesite=settings.SESSION_COOKIE_SAMESITE, | |
| ) | |
| return response |
And check_mfa_cookie
authentik/authentik/stages/authenticator_validate/stage.py
Lines 368 to 393 in 291b35c
| def check_mfa_cookie(self, allowed_devices: list[Device]): | |
| """Check if an MFA cookie has been set, whether it's valid and applies | |
| to the current stage and device. | |
| The list of devices passed to this function must only contain devices for the | |
| correct user and with an allowed class""" | |
| if COOKIE_NAME_MFA not in self.request.COOKIES: | |
| return | |
| stage: AuthenticatorValidateStage = self.executor.current_stage | |
| threshold = timedelta_from_string(stage.last_auth_threshold) | |
| latest_allowed = datetime.now() + threshold | |
| try: | |
| payload = decode(self.request.COOKIES[COOKIE_NAME_MFA], self.cookie_jwt_key, ["HS256"]) | |
| if payload["stage"] != stage.pk.hex: | |
| self.logger.warning("Invalid stage PK") | |
| return | |
| if datetime.fromtimestamp(payload["exp"]) > latest_allowed: | |
| self.logger.warning("Expired MFA cookie") | |
| return | |
| if not any(device.pk == payload["device"] for device in allowed_devices): | |
| self.logger.warning("Invalid device PK") | |
| return | |
| self.logger.info("MFA has been used within threshold") | |
| raise FlowSkipStageException() | |
| except (PyJWTError, ValueError, TypeError) as exc: | |
| self.logger.info("Invalid mfa cookie for device", exc=exc) |
This would also require a different method of deriving the jwt key
authentik/authentik/stages/authenticator_validate/stage.py
Lines 361 to 366 in 291b35c
| @property | |
| def cookie_jwt_key(self) -> str: | |
| """Signing key for MFA Cookie for this stage""" | |
| return sha256( | |
| f"{get_unique_identifier()}:{self.executor.current_stage.pk.hex}".encode("ascii") | |
| ).hexdigest() |
I would be willing to create a PR for this if you want.
Describe alternatives that you've considered
I considered not using requiring MFA for my non critical apps and then using a single mfa stages for the apps that require it. But this isn't really desired.
Additional context
Possible related issues that I could find:
Metadata
Metadata
Assignees
Labels
Type
Projects
Status