Skip to content

Commit 339f1f7

Browse files
Add ChallengeAndVerify and StatelessClient implementations
1 parent abc30ca commit 339f1f7

14 files changed

+202
-42
lines changed

Gotrue/Api.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,6 @@ public ProviderAuthState GetUriForProvider(Provider provider, SignInOptions? opt
540540
return Helpers.MakeRequest<Session>(HttpMethod.Post, url.ToString(), body, Headers);
541541
}
542542

543-
544543
/// <inheritdoc />
545544
public Task<MfaEnrollResponse?> Enroll(string jwt, MfaEnrollParams mfaEnrollParams)
546545
{
@@ -550,7 +549,7 @@ public ProviderAuthState GetUriForProvider(Provider provider, SignInOptions? opt
550549
{ "factor_type", mfaEnrollParams.FactorType },
551550
{ "issuer", mfaEnrollParams.Issuer }
552551
};
553-
552+
554553
return Helpers.MakeRequest<MfaEnrollResponse>(HttpMethod.Post, $"{Url}/factors", body, CreateAuthedRequestHeaders(jwt));
555554
}
556555

@@ -559,7 +558,7 @@ public ProviderAuthState GetUriForProvider(Provider provider, SignInOptions? opt
559558
{
560559
return Helpers.MakeRequest<MfaChallengeResponse>(HttpMethod.Post, $"{Url}/factors/{mfaChallengeParams.FactorId}/challenge", null, CreateAuthedRequestHeaders(jwt));
561560
}
562-
561+
563562
/// <inheritdoc />
564563
public Task<MfaVerifyResponse?> Verify(string jwt, MfaVerifyParams mfaVerifyParams)
565564
{
@@ -568,16 +567,16 @@ public ProviderAuthState GetUriForProvider(Provider provider, SignInOptions? opt
568567
{ "code", mfaVerifyParams.Code },
569568
{ "challenge_id", mfaVerifyParams.ChallengeId }
570569
};
571-
570+
572571
return Helpers.MakeRequest<MfaVerifyResponse>(HttpMethod.Post, $"{Url}/factors/{mfaVerifyParams.FactorId}/verify", body, CreateAuthedRequestHeaders(jwt));
573572
}
574-
573+
575574
/// <inheritdoc />
576575
public Task<MfaUnenrollResponse?> Unenroll(string jwt, MfaUnenrollParams mfaUnenrollParams)
577576
{
578577
return Helpers.MakeRequest<MfaUnenrollResponse>(HttpMethod.Delete, $"{Url}/factors/{mfaUnenrollParams.FactorId}", null, CreateAuthedRequestHeaders(jwt));
579578
}
580-
579+
581580
/// <inheritdoc />
582581
public async Task<ProviderAuthState> LinkIdentity(string token, Provider provider, SignInOptions options)
583582
{

Gotrue/Client.cs

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class Client : IGotrueClient<User, Session>
3737
/// Object called to persist the session (e.g. filesystem or cookie)
3838
/// </summary>
3939
private IGotruePersistenceListener<Session>? _sessionPersistence;
40-
40+
4141
/// <summary>
4242
/// Get the TokenRefresh object, if it exists
4343
/// </summary>
@@ -762,7 +762,7 @@ public void Shutdown()
762762

763763
if (!Online)
764764
throw new GotrueException("Only supported when online", Offline);
765-
765+
766766
return await _api.Enroll(CurrentSession.AccessToken, mfaEnrollParams);
767767
}
768768

@@ -774,7 +774,7 @@ public void Shutdown()
774774

775775
if (!Online)
776776
throw new GotrueException("Only supported when online", Offline);
777-
777+
778778
return await _api.Challenge(CurrentSession.AccessToken, mfaChallengeParams);
779779
}
780780

@@ -788,10 +788,54 @@ public void Shutdown()
788788
throw new GotrueException("Only supported when online", Offline);
789789

790790
var result = await _api.Verify(CurrentSession.AccessToken, mfaVerifyParams);
791-
791+
792+
if (result == null || string.IsNullOrEmpty(result.AccessToken))
793+
throw new GotrueException("Could not verify MFA.", MfaChallengeUnverified);
794+
795+
var session = new Session
796+
{
797+
AccessToken = result.AccessToken,
798+
RefreshToken = result.RefreshToken,
799+
TokenType = "bearer",
800+
ExpiresIn = result.ExpiresIn,
801+
User = result.User
802+
};
803+
804+
UpdateSession(session);
805+
NotifyAuthStateChange(MfaChallengeVerified);
806+
807+
return session;
808+
}
809+
810+
/// <inheritdoc />
811+
public async Task<Session?> ChallengeAndVerify(MfaChallengeAndVerifyParams mfaChallengeAndVerifyParams)
812+
{
813+
if (CurrentSession == null || string.IsNullOrEmpty(CurrentSession.AccessToken))
814+
throw new GotrueException("Not Logged in.", NoSessionFound);
815+
816+
if (!Online)
817+
throw new GotrueException("Only supported when online", Offline);
818+
819+
var challengeResponse = await _api.Challenge(CurrentSession.AccessToken, new MfaChallengeParams
820+
{
821+
FactorId = mfaChallengeAndVerifyParams.FactorId
822+
});
823+
824+
if (challengeResponse == null)
825+
{
826+
return null;
827+
}
828+
829+
var result = await _api.Verify(CurrentSession.AccessToken, new MfaVerifyParams
830+
{
831+
FactorId = mfaChallengeAndVerifyParams.FactorId,
832+
Code = mfaChallengeAndVerifyParams.Code,
833+
ChallengeId = challengeResponse.Id
834+
});
835+
792836
if (result == null || string.IsNullOrEmpty(result.AccessToken))
793837
throw new GotrueException("Could not verify MFA.", MfaChallengeUnverified);
794-
838+
795839
var session = new Session
796840
{
797841
AccessToken = result.AccessToken,
@@ -800,10 +844,10 @@ public void Shutdown()
800844
ExpiresIn = result.ExpiresIn,
801845
User = result.User
802846
};
803-
847+
804848
UpdateSession(session);
805849
NotifyAuthStateChange(MfaChallengeVerified);
806-
850+
807851
return session;
808852
}
809853

@@ -815,10 +859,10 @@ public void Shutdown()
815859

816860
if (!Online)
817861
throw new GotrueException("Only supported when online", Offline);
818-
862+
819863
return await _api.Unenroll(CurrentSession.AccessToken, mfaUnenrollParams);
820864
}
821-
865+
822866
/// <inheritdoc />
823867
public Task<MfaListFactorsResponse?> ListFactors()
824868
{
@@ -838,14 +882,14 @@ public void Shutdown()
838882
{
839883
if (CurrentSession == null || string.IsNullOrEmpty(CurrentSession.AccessToken))
840884
throw new GotrueException("Not Logged in.", NoSessionFound);
841-
885+
842886
var payload = new JwtSecurityTokenHandler().ReadJwtToken(CurrentSession.AccessToken).Payload;
843887

844888
if (payload == null || payload.ValidTo == DateTime.MinValue)
845889
throw new GotrueException("`accessToken`'s payload was of an unknown structure.", NoSessionFound);
846890

847891
AuthenticatorAssuranceLevel? currentLevel = null;
848-
892+
849893
if (payload.ContainsKey("aal"))
850894
{
851895
currentLevel = Enum.TryParse(payload["aal"].ToString(), out AuthenticatorAssuranceLevel parsedLevel) ? parsedLevel : (AuthenticatorAssuranceLevel?)null;
@@ -867,7 +911,7 @@ public void Shutdown()
867911
NextLevel = nextLevel,
868912
CurrentAuthenticationMethods = currentAuthenticationMethods.ToArray()
869913
};
870-
914+
871915
return Task.FromResult(response);
872916
}
873917
}

Gotrue/Interfaces/IGotrueClient.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders
464464
/// </summary>
465465
/// <returns></returns>
466466
public Task RefreshToken();
467-
467+
468468
#region MFA
469469
/**
470470
* Starts the enrollment process for a new Multi-Factor Authentication (MFA)
@@ -490,6 +490,12 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders
490490
*/
491491
Task<Session?> Verify(MfaVerifyParams mfaVerifyParams);
492492

493+
/**
494+
* Helper method which creates a challenge and immediately uses the given code to verify against it thereafter. The verification code is
495+
* provided by the user by entering a code seen in their authenticator app.
496+
*/
497+
Task<Session?> ChallengeAndVerify(MfaChallengeAndVerifyParams mfaChallengeAndVerifyParams);
498+
493499
/**
494500
* Unenroll removes a MFA factor.
495501
* A user has to have an `aal2` authenticator level in order to unenroll a `verified` factor.

Gotrue/Interfaces/IGotrueStatelessClient.cs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,62 @@ public interface IGotrueStatelessClient<TUser, TSession>
264264
/// <param name="options"></param>
265265
/// <returns></returns>
266266
Task<Settings?> Settings(StatelessClientOptions options);
267-
267+
268+
269+
/// <summary>
270+
/// Starts the enrollment process for a new Multi-Factor Authentication (MFA)
271+
/// factor. This method creates a new `unverified` factor.
272+
/// To verify a factor, present the QR code or secret to the user and ask them to add it to their
273+
/// authenticator app.
274+
/// The user has to enter the code from their authenticator app to verify it.
275+
///
276+
/// Upon verifying a factor, all other sessions are logged out and the current session's authenticator level is promoted to `aal2`.
277+
/// </summary>
278+
/// <param name="jwt"></param>
279+
/// <param name="mfaEnrollParams"></param>
280+
/// <param name="options"></param>
268281
Task<MfaEnrollResponse?> Enroll(string jwt, MfaEnrollParams mfaEnrollParams, StatelessClientOptions options);
282+
283+
/// <summary>
284+
/// Prepares a challenge used to verify that a user has access to a MFA
285+
/// factor.
286+
/// </summary>
287+
/// <param name="jwt"></param>
288+
/// <param name="mfaChallengeParams"></param>
289+
/// <param name="options"></param>
290+
Task<MfaChallengeResponse?> Challenge(string jwt, MfaChallengeParams mfaChallengeParams, StatelessClientOptions options);
291+
292+
/// <summary>
293+
/// Verifies a code against a challenge. The verification code is
294+
/// provided by the user by entering a code seen in their authenticator app. </summary>
295+
/// <param name="jwt"></param>
296+
/// <param name="mfaVerifyParams"></param>
297+
/// <param name="options"></param>
298+
Task<MfaVerifyResponse?> Verify(string jwt, MfaVerifyParams mfaVerifyParams, StatelessClientOptions options);
299+
300+
/// <summary>
301+
/// Helper method which creates a challenge and immediately uses the given code to verify against it thereafter. The verification code is
302+
/// provided by the user by entering a code seen in their authenticator app.
303+
/// </summary>
304+
/// <param name="jwt"></param>
305+
/// <param name="mfaChallengeAndVerifyParams"></param>
306+
/// <param name="options"></param>
307+
Task<MfaVerifyResponse?> ChallengeAndVerify(string jwt, MfaChallengeAndVerifyParams mfaChallengeAndVerifyParams, StatelessClientOptions options);
308+
309+
/// <summary>
310+
/// Unenroll removes a MFA factor.
311+
/// A user has to have an `aal2` authenticator level in order to unenroll a `verified` factor.
312+
/// </summary>
313+
/// <param name="jwt"></param>
314+
/// <param name="mfaUnenrollParams"></param>
315+
/// <param name="options"></param>
316+
Task<MfaUnenrollResponse?> Unenroll(string jwt, MfaUnenrollParams mfaUnenrollParams, StatelessClientOptions options);
317+
318+
/// <summary>
319+
/// Returns the list of MFA factors enabled for this user
320+
/// </summary>
321+
/// <param name="jwt"></param>
322+
/// <param name="options"></param>
323+
Task<MfaListFactorsResponse?> ListFactors(string jwt, StatelessClientOptions options);
269324
}
270325
}

Gotrue/Mfa/Factor.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@ public class Factor
77
{
88
[JsonProperty("id")]
99
public string Id { get; set; }
10-
10+
1111
[JsonProperty("friendly_name")]
1212
public string? FriendlyName { get; set; }
13-
13+
1414
[JsonProperty("factor_type")]
1515
public string FactorType { get; set; }
16-
16+
1717
[JsonProperty("status")]
1818
public string Status { get; set; }
19-
19+
2020
[JsonProperty("created_at")]
2121
public DateTime CreatedAt { get; set; }
22-
22+
2323
[JsonProperty("updated_at")]
2424
public DateTime UpdatedAt { get; set; }
2525
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Supabase.Gotrue.Mfa
2+
{
3+
public class MfaChallengeAndVerifyParams
4+
{
5+
public string FactorId { get; set; }
6+
public string Code { get; set; }
7+
}
8+
}

Gotrue/Mfa/MfaChallengeResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public class MfaChallengeResponse
77
// ID of the newly created challenge.
88
[JsonProperty("id")]
99
public string Id { get; set; }
10-
10+
1111
[JsonProperty("expires_at")]
1212
public long ExpiresAt { get; set; }
1313
}

Gotrue/Mfa/MfaEnrollResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public class MfaEnrollResponse
77
// ID of the factor that was just enrolled (in an unverified state).
88
[JsonProperty("id")]
99
public string Id { get; set; }
10-
10+
1111
// Type of MFA factor. Only `totp` supported for now.
1212
[JsonProperty("type")]
1313
public string Type { get; set; }

Gotrue/Mfa/MfaListFactorsResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public class MfaListFactorsResponse
66
{
77
// All available factors (verified and unverified)
88
public List<Factor> All { get; set; }
9-
9+
1010
// Only verified TOTP factors. (A subset of `all`.)
1111
public List<Factor> Totp { get; set; }
1212
}

Gotrue/Mfa/MfaResponse.cs

Lines changed: 0 additions & 7 deletions
This file was deleted.

Gotrue/Mfa/MfaVerifyParams.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ public class MfaVerifyParams
44
{
55
// ID of the factor being verified. Returned in enroll()
66
public string FactorId { get; set; }
7-
7+
88
// ID of the challenge being verified. Returned in challenge()
99
public string ChallengeId { get; set; }
10-
10+
1111
// Verification code provided by the user
1212
public string Code { get; set; }
1313
}

Gotrue/Mfa/MfaVerifyResponse.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@ public class MfaVerifyResponse
77
// New access token (JWT) after successful verification
88
[JsonProperty("access_token")]
99
public string AccessToken { get; set; }
10-
10+
1111
// Type of token, typically `Bearer`
1212
[JsonProperty("token_type")]
1313
public string TokenType { get; set; }
14-
14+
1515
// Number of seconds in which the access token will expire
1616
[JsonProperty("expires_in")]
1717
public int ExpiresIn { get; set; }
18-
18+
1919
// Refresh token you can use to obtain new access tokens when expired
2020
[JsonProperty("refresh_token")]
2121
public string RefreshToken { get; set; }
22-
22+
2323
// Updated user profile
2424
[JsonProperty("user")]
2525
public User User { get; set; }

0 commit comments

Comments
 (0)