Open jezzsantos opened 1 month ago
Considering:
Questions:
/auth
API, and that returns something other than the access_token
?APIS for authentication (with password): (based on Auth0 flows)
We could support:
We could support OOB pretty easily.
Step1: Auth - we call POST /passwords/auth
with provider=credentials
, but (if MFA is enabled) we get a response like this:
HTTP 401? "Multifactor authentication required"
{
"mfa_token": "...mfatoken..."
}
The client now needs to call: GET /passwords/mfa/authenticators?mfaToken=
, that includes the mfatoken from previous response, and gets a response like this:
HTTP 200 - Success
{
"authenticators": [
{
"id" : "someid",
"type": "RecoveryCodes",
},
{
"id" : "someotherid",
"type": "OOB",
"channel" : "SMS"
},
{
"id" : "someotherid",
"type": "OTP",
"channel" : "authenticator"
},
]
}
Given this response has an authenticator called OOB/SMS
,
Challenge:
Now call POST /passwords/mfa/challenge
and include: type=oob_sms&authenticatorId=somotherid&mfaToken=mfatoken
This should send the SMS code to the user.
This should respond with:
{
"challenge_type": "oob",
"token": "asdae35fdt5...", //calculated one-time challenge value
}
Now we collect the SMS code (send to their phone) in the UI, and POST is back to the server to authenticate:
This would then call: POST /passwords/mfa/auth
with type=oob&mfaToken=mfatoken&code=smscode&oobToken=token
On the server, the details are compared.
and this response should return the authentication response as usual.
Given this response has an authenticator called OTP/Authenticator
,
No challenge call here.
Now we collect the Authenticator code (they can view in the authenticator app) in the UI, and POST is back to the server to authenticate:
This would then call: POST /passwords/mfa/auth
with type=otp&mfaToken=mfatoken&code=authenticatorcode
On the server, the details are compared.
and this response should return the authentication response as usual.
Given this response has an authenticator called RecoveryCodes
,
No challenge call here.
Now we collect the recovery codes in the UI, and POST is back to the server to authenticate:
This would then call: POST /passwords/mfa/auth
with type=recoverycode&mfaToken=mfatoken&code=recoverycode
On the server, the details are compared.
and this response should return the authentication response as usual.
When we authenticate via POST /password/auth
normally we would return various 4XX errors to indicate a failed authentication. e.g. 401, 405, 409 etc.
In the case of 401, we should be returning a WWW-Authenticate header, to indicate how to authenticate. In the case, we succeed we would return a 201 - Created, and this kind of data:
{
"tokens": {
"accessToken": {
"expiresOn": "2024-10-26T02:56:13.7981713Z",
"type": "accessToken",
"value": "eyJhb....."
},
"refreshToken": {
"expiresOn": "2024-11-09T02:41:13.7981734Z",
"type": "refreshToken",
"value": "o8WXGqlNQtrZZuPeROzdyPpkk3KkWGnknm2E9Z0Np7s"
},
"userId": "user_qyhjHnJnURdMbZkCjAiw"
}
}
Now, when authenticating and MFA is enabled for this user, we need to tell the client to follow an MFA flow onwards.
Strictly speaking, we have completed a partial authentication process (by providing their credentials) but we need more (i.e. the relevant 2FA codes (i.e. "Something They Have" - authenticator app, SMS, etc).
The question is do we return a 4xx error with something in response to indicate MFA flow, or do we return a 202 - Accepted
with something in the response to indicate partial success?
Option 1: 202 - Accepted
"mfa_token": "Fe26...Ha"
Option 2: 403 - Forbidden
{
"type": "https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.4",
"title": "mfa_required",
"status": 403,
"detail": "Requires another factor of authentication",
"instance": "https://localhost:5001/passwords/auth",
"extensions": {
"mfa_token": "Fe26...Ha"
}
}
In Auth0, they return a 403 - Forbidden
like this {error:“mfa_required”,error_description:“Multifactor authentication required”}
Note: MFA only applies to accounts that are not SSO.
Plan of attack:
IsMfaEnabled
property on the PasswordCredentials
aggregate.IsMfaEnabled
. The idea is that we can somehow EXTEND and do this by default, for all users (perhaps it is a setting for all users).IsMfaEnabled == true
If the first step is to enroll in MFA of one type or another, and MFA is mandatory for new users, then where do we enforce the enrollment step? Since user registration is a two-step process, where does this process fit in? (If MFA is not mandatory) we can enroll at any time later, but if mandatory, we have to force the process up front, somewhere before they login)
Options:
Another question is, which authenticators do we force them to setup? is that determined in the code? I guess we can offer them a choice, if we have more than one hardcoded?
According to Auth0, they seem to enforce the enrollment during the authentication process: https://auth0.com/docs/secure/multi-factor-authentication/authenticate-using-ropg-flow-with-mfa (which is option 3)
The enrollment process (and the challenge process) would both end up issuing tokens.
Which implies that the mfatoken
is only issued as a "partial" result when trying to authenticate (with mandatory MFA, but not being enrolled already).
Or you can obtain it when you are already authenticated already (when opting into MFA enrollment).
The flow would look like this: (derived from Auth0 ROPG flows)
mfatoken
We need to support some simple 2FA scenarios like: