Skip to content
LogoLogo

How to set up MFA

This guide shows you how to add an authenticator app to an account, and how to sign in with it. Picked supports TOTP authenticator apps only, such as Google Authenticator. Every MFA mutation needs authentication.

Who must use MFA

Picked staff, and every member of an organization that requires MFA, must sign in with an authenticator app. For these users, most operations return the error code MFA_REQUIRED until the session reaches AAL2. The operations that a session at AAL1 can use are in Authenticator assurance levels. The user.mfa field tells you where a user stands:

query {
  user {
    mfa { required reason enrolled satisfied }
  }
}
FieldMeaning
requiredThe user must hold an AAL2 session to use the fields that need it.
reasonSTAFF or ORGANIZATION. Empty when required is false.
enrolledThe user holds a verified authenticator.
satisfiedThe session that made the request meets the requirement.

1. Add an authenticator

mutation {
  mfaFactorEnroll(input: { friendlyName: "Work phone" }) {
    factor { id factorType status }
    totp { qrCode secret uri }
  }
}

A new factor has the status UNVERIFIED. Show qrCode (SVG markup) for the user to scan, or secret for the user to enter into the app. The API returns it one time only.

Before it adds the factor, the mutation deletes the caller's factors that are not verified. But it keeps the two newest of these factors if they are less than 60 seconds old. A different setup can still use them, for example in a different browser tab. The mutation also deletes a factor that is not verified and has the friendlyName that you give. Therefore, a setup that did not finish does not stop a new setup.

2. Open a challenge

mutation {
  mfaChallengeCreate(input: { factorId: "factor-id" }) {
    challengeId
    expiresAt
  }
}

3. Verify the code

mutation {
  mfaChallengeVerify(input: {
    factorId: "factor-id"
    challengeId: "challenge-id"
    code: "123456"
  }) {
    session { accessToken refreshToken aal }
  }
}

The session moves to AAL2, and the factor status changes to VERIFIED. Use the returned tokens from this point. A wrong code returns MFA_CHALLENGE_FAILED.

Sign in with an authenticator

After sessionCreate, the session is at AAL1. When session.user.mfa.enrolled is true, do steps 2 and 3 against one of the user's verified factors. The query user { mfaFactors { id status } } lists them. When required is true and enrolled is false, do steps 1 to 3.

Remove an authenticator

mutation {
  mfaFactorUnenroll(input: { factorId: "factor-id" }) {
    success
  }
}

To remove a verified factor, the session must be at AAL2. An AAL1 session receives MFA_REQUIRED.

A user who must use MFA always keeps at least one authenticator. A request to remove their last one fails with VALIDATION_ERROR. To replace an authenticator, add the new one first, then remove the old one. A user with the users/reset_mfa scope clears the last one with userMfaReset.