From b957c30782065e4cc421a526c62c101d35c443d4 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Fri, 9 Aug 2024 16:28:01 +0800 Subject: [PATCH] feat: add bindings for Multi-Factor Authentication (Phone) (#932) ## What kind of change does this PR introduce? Adds the bindings for MFA (Phone) Client library. In particular, add - Enroll - now allows for enrollment of Phone Factors - Challenge for Phone - now takes in a channel - List Factors - now supports access of Phone factors ### TODOS: - [x] Remove empty `totp` from response field on enroll response ### Not addressed yet - `challengeAndVerify` will currently return an error on the `verify` step when used with a phone factor. We could introduce an alternate behaviour (e.g. terminating at the `challenge` step gracefully) since we have the `type` of the challenge-associated-factor in the response. This is not addressed in this PR --- src/GoTrueClient.ts | 24 +++++++++++++----- src/lib/types.ts | 59 +++++++++++++++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index 00958fd65..3d94fe562 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -2341,12 +2341,14 @@ export default class GoTrueClient { return { data: null, error: sessionError } } + const body = { + friendly_name: params.friendlyName, + factor_type: params.factorType, + ...(params.factorType === 'phone' ? { phone: params.phone } : { issuer: params.issuer }), + } + const { data, error } = await _request(this.fetch, 'POST', `${this.url}/factors`, { - body: { - friendly_name: params.friendlyName, - factor_type: params.factorType, - issuer: params.issuer, - }, + body, headers: this.headers, jwt: sessionData?.session?.access_token, }) @@ -2355,7 +2357,12 @@ export default class GoTrueClient { return { data: null, error } } - if (data?.totp?.qr_code) { + // TODO: Remove once: https://github.com/supabase/auth/pull/1717 is deployed + if (params.factorType === 'phone') { + delete data.totp + } + + if (params.factorType === 'totp' && data?.totp?.qr_code) { data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}` } @@ -2429,6 +2436,7 @@ export default class GoTrueClient { 'POST', `${this.url}/factors/${params.factorId}/challenge`, { + body: { channel: params.channel }, headers: this.headers, jwt: sessionData?.session?.access_token, } @@ -2483,11 +2491,15 @@ export default class GoTrueClient { const totp = factors.filter( (factor) => factor.factor_type === 'totp' && factor.status === 'verified' ) + const phone = factors.filter( + (factor) => factor.factor_type === 'phone' && factor.status === 'verified' + ) return { data: { all: factors, totp, + phone, }, error: null, } diff --git a/src/lib/types.ts b/src/lib/types.ts index 2129c01b7..95b0ab214 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -302,10 +302,9 @@ export interface Factor { friendly_name?: string /** - * Type of factor. Only `totp` supported with this version but may change in - * future versions. + * Type of factor. `totp` and `phone` supported with this version */ - factor_type: 'totp' | string + factor_type: 'totp' | 'phone' | string /** Factor's status. */ status: 'verified' | 'unverified' @@ -471,10 +470,6 @@ export interface Subscription { unsubscribe: () => void } -export interface UpdatableFactorAttributes { - friendlyName: string -} - export type SignInAnonymouslyCredentials = { options?: { /** @@ -805,14 +800,23 @@ export type GenerateLinkType = | 'email_change_current' | 'email_change_new' -export type MFAEnrollParams = { - /** The type of factor being enrolled. */ - factorType: 'totp' - /** Domain which the user is enrolled with. */ - issuer?: string - /** Human readable name assigned to the factor. */ - friendlyName?: string -} +export type MFAEnrollParams = + | { + /** The type of factor being enrolled. */ + factorType: 'totp' + /** Domain which the user is enrolled with. */ + issuer?: string + /** Human readable name assigned to the factor. */ + friendlyName?: string + } + | { + /** The type of factor being enrolled. */ + factorType: 'phone' + /** Human readable name assigned to the factor. */ + friendlyName?: string + /** Phone number associated with a factor. Number should conform to E.164 format */ + phone: string + } export type MFAUnenrollParams = { /** ID of the factor being unenrolled. */ @@ -833,6 +837,8 @@ export type MFAVerifyParams = { export type MFAChallengeParams = { /** ID of the factor to be challenged. Returned in enroll(). */ factorId: string + /** Messaging channel to use (e.g. whatsapp or sms). Only relevant for phone factors */ + channel?: 'sms' | 'whatsapp' } export type MFAChallengeAndVerifyParams = { @@ -873,7 +879,7 @@ export type AuthMFAEnrollResponse = /** ID of the factor that was just enrolled (in an unverified state). */ id: string - /** Type of MFA factor. Only `totp` supported for now. */ + /** Type of MFA factor.*/ type: 'totp' /** TOTP enrollment information. */ @@ -897,6 +903,22 @@ export type AuthMFAEnrollResponse = } error: null } + | { + data: { + /** ID of the factor that was just enrolled (in an unverified state). */ + id: string + + /** Type of MFA factor. */ + type: 'phone' + + /** Friendly name of the factor, useful for distinguishing between factors **/ + friendly_name?: string + + /** Phone number of the MFA factor in E.164 format. Used to send messages */ + phone: string + } + error: null + } | { data: null error: AuthError @@ -918,6 +940,9 @@ export type AuthMFAChallengeResponse = /** ID of the newly created challenge. */ id: string + /** Factor Type which generated the challenge */ + type: 'totp' | 'phone' + /** Timestamp in UNIX seconds when this challenge will no longer be usable. */ expires_at: number } @@ -933,6 +958,8 @@ export type AuthMFAListFactorsResponse = /** Only verified TOTP factors. (A subset of `all`.) */ totp: Factor[] + /** Only verified Phone factors. (A subset of `all`.) */ + phone: Factor[] } error: null }