Hi @Taylorsuk,
You are right 2FA system and auth challenge feature are missing on parse server.
I’ve worked a lot to improve the Auth Adapter interface to introduce 2FA and help developers to write their own AuthAdapters easily (validation, registration, challenge and many more)
The PR is fully ready here and should be shipped on Parse v5: https://github.com/parse-community/parse-server/pull/7079
I already use this PR on production for my company for over 1 year to:
- Support 2FA login (email + password + SMS OTP)
- Biometric authentification via Webauthn
- Fully custom auth pattern (a username + a field 1 + a field 2 + SMS OTP)
It works well !
Currently the feature wait a PR from @davimacedo (https://github.com/parse-community/parse-server/pull/7079#issuecomment-990338449) to add some warnings.
Here the example of the new Auth Adapter Interface applied to Webauthn: https://github.com/parse-community/parse-server/pull/7079/files#diff-51ec90d71b4547d4642872376195afc01ee0a8ddc62d3647cfdbc4bfacb79d8d
Here an example of an SMS OTP Auth Adapter (code is not complete):
export const OTPAuth = {
policy: 'additional',
async challenge(
challengeData: boolean,
authData: OtpAuthDataInput,
options: any,
req: any,
user?: Parse.User,
) {
if (!user || !user.get('phone')) throw new Error('User not found')
const otp = new OTP(user.id, user.get('phone'))
await SMSAdapter.send(
user.get('phone'),
// Dependency injection strategy
getOTPMessage(user.get('type'))(await otp.generate()),
)
},
async validateSetUp(
authData: CreateUpdateOtpAuthDataInput,
options: any,
req: AuthReq,
) {
return checkAuthorization(req)
},
async validateUpdate(
authData: CreateUpdateOtpAuthDataInput,
options: any,
req: AuthReq,
) {
return checkAuthorization(req)
},
async validateLogin(
authData: OtpAuthDataInput,
options: any,
req: AuthReq,
user?: Parse.User,
) {
try {
// As an additional auth system user should be already identified
// with another default auth system
if (!user?.id) return throwError()
if (!user.get('phone')) return throwError()
const otp = new OTP(user.id, user.get('phone'))
await otp.check(authData.code)
return { doNotSave: true }
} catch (e: any) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw new Parse.Error(403, e.message)
}
},
}
@Taylorsuk all the magic happen with the policy: 'additional'
.
The architecture is pretty simple.
Here the client example of the 2FA (using GraphQL API).
First you need to challenge
with authData
or username/passowrd
to obtain the SMS OTP. The challenge
function will only success if provided authData
or username/password
are correct.
mutation loginChallenge {
challenge(input: { challengeData: { otp: true }, username: "[email protected]", password: "aPassword"}) {
clientMutationId
}
}
Then the use will receive the OTP on his phone
In your client app you will juste need to call now login like with additional authData
mutation login {
logIn(input: { usnername: "[email protected]", password: "aPassword", authData: { otp : "123765"} }) {
viewer {
sessionToken
user {
id
}
}
}
}