Skip to content

Commit

Permalink
Implement SignInWithSso (#94)
Browse files Browse the repository at this point in the history
* Port changes for SignInWithSSO from the js library to the csharp one

* Update readme
  • Loading branch information
Rycko1 committed May 31, 2024
1 parent e9c2e50 commit 5acd1b2
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 32 deletions.
63 changes: 56 additions & 7 deletions Gotrue/Api.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ public Api(string url, Dictionary<string, string>? headers = null)

/// <summary>
/// Log in a user using magiclink or a one-time password (OTP).
///
///
/// If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
/// If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
/// If you're using phone sign-ins, only an OTP will be sent. You won't be able to send a magiclink for phone sign-ins.
///
///
/// Be aware that you may get back an error message that will not distinguish
/// between the cases where the account does not exist or, that the account
/// can only be accessed via social login.
///
///
/// Do note that you will need to configure a Whatsapp sender on Twilio
/// if you are using phone sign in with the 'whatsapp' channel. The whatsapp
/// channel is not supported on other providers at this time.
Expand Down Expand Up @@ -160,15 +160,15 @@ public async Task<PasswordlessSignInState> SignInWithOtp(SignInWithPasswordlessE

/// <summary>
/// Log in a user using magiclink or a one-time password (OTP).
///
///
/// If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
/// If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
/// If you're using phone sign-ins, only an OTP will be sent. You won't be able to send a magiclink for phone sign-ins.
///
///
/// Be aware that you may get back an error message that will not distinguish
/// between the cases where the account does not exist or, that the account
/// can only be accessed via social login.
///
///
/// Do note that you will need to configure a Whatsapp sender on Twilio
/// if you are using phone sign in with the 'whatsapp' channel. The whatsapp
/// channel is not supported on other providers at this time.
Expand Down Expand Up @@ -252,6 +252,55 @@ public async Task<PasswordlessSignInState> SignInWithOtp(SignInWithPasswordlessP
return Helpers.MakeRequest<Session>(HttpMethod.Post, $"{Url}/token?grant_type=id_token", body, Headers);
}

private Task<SsoResponse?> SignInWithSsoInternal(Guid? providerId = null, string? domain = null, SignInOptionsWithSsoOptions? options = null)
{
if(providerId != null && domain != null)
throw new GotrueException($"Both providerId and domain were provided to the API, " +
$"you must supply either one or the other but not both providerId={providerId}, domain={domain}");
if(providerId == null && domain == null)
throw new GotrueException($"Both providerId and domain were null " +
$"you must supply either one or the other but not both providerId={providerId}, domain={domain}");

string? codeChallenge = null;
string? codeChallengeMethod = null;
if (options?.FlowType == OAuthFlowType.PKCE)
{
var codeVerifier = Helpers.GenerateNonce();
codeChallenge = Helpers.GeneratePKCENonceVerifier(codeVerifier);
codeChallengeMethod = "s256";
}

var body = new Dictionary<string, object?>
{
{ providerId != null ? "provider_id" : "domain", providerId != null ? providerId.ToString() : domain},
{ "redirect_to", options?.RedirectTo },

// this is important, it will not auto redirect the request and instead return the Uri needed to handle the login
// without this in the body the request will automatically redirect to the providers sign in page
{ "skip_http_redirect", true },

{ "code_challenge", codeChallenge },
{ "code_challenge_method", codeChallengeMethod }
};

if (!string.IsNullOrEmpty(options?.CaptchaToken))
body.Add("gotrue_meta_security", new Dictionary<string, object?> { { "captcha_token", options?.CaptchaToken } });

return Helpers.MakeRequest<SsoResponse>(HttpMethod.Post, $"{Url}/sso", body, Headers);
}

/// <inheritdoc />
public Task<SsoResponse?> SignInWithSso(Guid providerId, SignInOptionsWithSsoOptions? options = null)
{
return SignInWithSsoInternal(providerId: providerId, options: options);
}

/// <inheritdoc />
public Task<SsoResponse?> SignInWithSso(string domain, SignInOptionsWithSsoOptions? options = null)
{
return SignInWithSsoInternal(domain: domain, options: options);
}

/// <summary>
/// Sends a magic login link to an email address.
/// </summary>
Expand Down Expand Up @@ -675,4 +724,4 @@ public Task<BaseResponse> GenerateLink(string jwt, GenerateLinkOptions options)
return Helpers.MakeRequest<Session>(HttpMethod.Post, $"{Url}/token?grant_type=refresh_token", data, Headers.MergeLeft(headers));
}
}
}
}
34 changes: 28 additions & 6 deletions Gotrue/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ public class Client : IGotrueClient<User, Session>
public TokenRefresh? TokenRefresh { get; }

/// <summary>
/// Initializes the GoTrue stateful client.
///
/// Initializes the GoTrue stateful client.
///
/// You will likely want to at least specify a <see>
/// <cref>ClientOptions.Url</cref>
/// </see>
///
///
/// Sessions are not automatically retrieved when this object is created.
///
///
/// If you want to load the session from your persistence store, <see>
/// <cref>GotrueSessionPersistence</cref>
/// </see>.
Expand All @@ -62,7 +62,7 @@ public class Client : IGotrueClient<User, Session>
/// and then refresh it. If your application is listening for session changes, you'll
/// get two SignIn notifications if the persisted session is valid - one for the
/// session loaded from disk, and a second on a successful session refresh.
///
///
/// <remarks></remarks>
/// <example>
/// var client = new Supabase.Gotrue.Client(options);
Expand Down Expand Up @@ -295,6 +295,28 @@ public Task<ProviderAuthState> SignIn(Provider provider, SignInOptions? options
return Task.FromResult(providerUri);
}

/// <inheritdoc />
public Task<SsoResponse?> SignInWithSso(Guid providerId, SignInOptionsWithSsoOptions? options = null)
{
if (!Online)
throw new GotrueException("Only supported when online", Offline);

DestroySession();

return _api.SignInWithSso(providerId, options);
}

/// <inheritdoc />
public Task<SsoResponse?> SignInWithSso(string domain, SignInOptionsWithSsoOptions? options = null)
{
if (!Online)
throw new GotrueException("Only supported when online", Offline);

DestroySession();

return _api.SignInWithSso(domain, options);
}

/// <inheritdoc />
public async Task<Session?> SignInAnonymously(SignInAnonymouslyOptions? options = null)
{
Expand Down Expand Up @@ -711,4 +733,4 @@ public void Shutdown()
NotifyAuthStateChange(AuthState.Shutdown);
}
}
}
}
14 changes: 12 additions & 2 deletions Gotrue/Exceptions/FailureReason.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,15 @@ public enum Reason
/// <summary>
/// An invalid authentication flow has been selected.
/// </summary>
InvalidFlowType
InvalidFlowType,
/// <summary>
/// The SSO domain provided was not registered via the CLI
/// </summary>
SsoDomainNotFound,
/// <summary>
/// The sso provider ID was incorrect or does not exist
/// </summary>
SsoProviderNotFound
}

/// <summary>
Expand Down Expand Up @@ -107,6 +115,8 @@ 400 when gte.Content.Contains("provide") => UserMissingInformation,
401 when gte.Content.Contains("This endpoint requires a Bearer token") => AdminTokenRequired,
403 when gte.Content.Contains("Invalid token") => AdminTokenRequired,
403 when gte.Content.Contains("invalid JWT") => AdminTokenRequired,
404 when gte.Content.Contains("No SSO provider assigned for this domain") => SsoDomainNotFound,
404 when gte.Content.Contains("No such SSO provider") => SsoProviderNotFound,
422 when gte.Content.Contains("User already registered") => UserAlreadyRegistered,
422 when gte.Content.Contains("Phone") && gte.Content.Contains("Email") => UserBadMultiple,
422 when gte.Content.Contains("email") && gte.Content.Contains("password") => UserBadMultiple,
Expand All @@ -118,4 +128,4 @@ 422 when gte.Content.Contains("password") => UserBadPassword,

}
}
}
}
9 changes: 6 additions & 3 deletions Gotrue/Interfaces/IGotrueApi.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Supabase.Core.Interfaces;
using Supabase.Gotrue.Responses;
using static Supabase.Gotrue.Constants;
Expand Down Expand Up @@ -28,6 +29,8 @@ public interface IGotrueApi<TUser, TSession> : IGettableHeaders
Task<PasswordlessSignInState> SignInWithOtp(SignInWithPasswordlessEmailOptions options);
Task<PasswordlessSignInState> SignInWithOtp(SignInWithPasswordlessPhoneOptions options);
Task<TSession?> SignInAnonymously(SignInAnonymouslyOptions? options = null);
Task<SsoResponse?> SignInWithSso(Guid providerId, SignInOptionsWithSsoOptions? options = null);
Task<SsoResponse?> SignInWithSso(string domain, SignInOptionsWithSsoOptions? options = null);
Task<BaseResponse> SignOut(string jwt);
Task<TSession?> SignUpWithEmail(string email, string password, SignUpOptions? options = null);
Task<TSession?> SignUpWithPhone(string phone, string password, SignUpOptions? options = null);
Expand All @@ -43,7 +46,7 @@ public interface IGotrueApi<TUser, TSession> : IGettableHeaders

/// <summary>
/// Links an oauth identity to an existing user.
///
///
/// This method requires the PKCE flow.
/// </summary>
/// <param name="token">User's token</param>
Expand All @@ -61,4 +64,4 @@ public interface IGotrueApi<TUser, TSession> : IGettableHeaders
Task<bool> UnlinkIdentity(string token, UserIdentity userIdentity);
}

}
}
51 changes: 37 additions & 14 deletions Gotrue/Interfaces/IGotrueClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders
{
/// <summary>
/// Indicates if the client should be considered online or offline.
///
///
/// In a server environment, this client would likely always be online.
///
/// On a mobile client, you will want to pair this with a network implementation
Expand All @@ -45,7 +45,7 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders
TSession? CurrentSession { get; }

/// <summary>
/// The currently logged in User. This is a local cache of the current session User.
/// The currently logged in User. This is a local cache of the current session User.
/// To persist modifications to the User you'll want to use other methods.
/// <see cref="Update"/>>
/// </summary>
Expand Down Expand Up @@ -139,7 +139,7 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders
///
/// Most of the interesting configuration for this flow is done in the
/// Supabase/GoTrue admin panel.
///
///
/// </summary>
/// <param name="email"></param>
/// <param name="options"></param>
Expand All @@ -148,7 +148,7 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders

/// <summary>
/// Sets a new session given a user's access token and their refresh token.
///
///
/// 1. Will destroy the current session (if existing)
/// 2. Raise a <see cref="AuthState.SignedOut"/> event.
/// 3. Decode token
Expand Down Expand Up @@ -190,15 +190,15 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders

/// <summary>
/// Log in a user using magiclink or a one-time password (OTP).
///
///
/// If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
/// If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
/// If you're using phone sign-ins, only an OTP will be sent. You won't be able to send a magiclink for phone sign-ins.
///
///
/// Be aware that you may get back an error message that will not distinguish
/// between the cases where the account does not exist or, that the account
/// can only be accessed via social login.
///
///
/// Do note that you will need to configure a Whatsapp sender on Twilio
/// if you are using phone sign in with the 'whatsapp' channel. The whatsapp
/// channel is not supported on other providers at this time.
Expand All @@ -210,15 +210,15 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders

/// <summary>
/// Log in a user using magiclink or a one-time password (OTP).
///
///
/// If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
/// If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
/// If you're using phone sign-ins, only an OTP will be sent. You won't be able to send a magiclink for phone sign-ins.
///
///
/// Be aware that you may get back an error message that will not distinguish
/// between the cases where the account does not exist or, that the account
/// can only be accessed via social login.
///
///
/// Do note that you will need to configure a Whatsapp sender on Twilio
/// if you are using phone sign in with the 'whatsapp' channel. The whatsapp
/// channel is not supported on other providers at this time.
Expand Down Expand Up @@ -269,7 +269,30 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders
/// <param name="options"></param>
/// <returns>A session where the is_anonymous claim in the access token JWT set to true</returns>
Task<TSession?> SignInAnonymously(SignInAnonymouslyOptions? options = null);


/// <summary>
/// Sign in using single sign on (SSO) as supported by supabase
/// To use SSO you need to first set up the providers using the supabase CLI
/// please follow the guide found here: https://supabase.com/docs/guides/auth/enterprise-sso/auth-sso-saml
/// </summary>
/// <param name="providerId">The guid of the provider you wish to use, obtained from running supabase sso list from the CLI</param>
/// <param name="options">The redirect uri and captcha token, if any</param>
/// <returns>The Uri returned from supabase auth that a user can use to sign in to their given SSO provider (okta, microsoft entra, gsuite ect...)</returns>
Task<SsoResponse?> SignInWithSso(Guid providerId, SignInOptionsWithSsoOptions? options = null);

/// <summary>
/// Sign in using single sign on (SSO) as supported by supabase
/// To use SSO you need to first set up the providers using the supabase CLI
/// please follow the guide found here: https://supabase.com/docs/guides/auth/enterprise-sso/auth-sso-saml
/// </summary>
/// <param name="domain">
/// Your organizations email domain to use for sign in, this domain needs to already be registered with supabase by running the CLI commands
/// Example: `google.com`
/// </param>
/// <param name="options">The redirect uri and captcha token, if any</param>
/// <returns>The Uri returned from supabase auth that a user can use to sign in to their given SSO provider (okta, microsoft entra, gsuite ect...)</returns>
Task<SsoResponse?> SignInWithSso(string domain, SignInOptionsWithSsoOptions? options = null);

/// <summary>
/// Logs in an existing user via a third-party provider.
/// </summary>
Expand All @@ -282,7 +305,7 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders
/// </summary>
/// <remarks>
/// Calling this method will log out the current user session (if any).
///
///
/// By default, the user needs to verify their email address before logging in. To turn this off, disable confirm email in your project.
/// Confirm email determines if users need to confirm their email address after signing up.
/// - If Confirm email is enabled, a user is returned but session is null.
Expand Down Expand Up @@ -379,7 +402,7 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders
/// <param name="userIdentity">Identity to be unlinked</param>
/// <returns></returns>
Task<bool> UnlinkIdentity(UserIdentity userIdentity);

/// <summary>
/// Add a listener to get errors that occur outside of a typical Exception flow.
/// In particular, this is used to get errors and messages from the background thread
Expand Down Expand Up @@ -432,4 +455,4 @@ public interface IGotrueClient<TUser, TSession> : IGettableHeaders
/// <returns></returns>
public Task RefreshToken();
}
}
}
16 changes: 16 additions & 0 deletions Gotrue/SignInWithSsoOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Newtonsoft.Json;

namespace Supabase.Gotrue
{
/// <summary>
/// Options used for signing in a user using single sign on (SSO).
/// </summary>
public class SignInOptionsWithSsoOptions : SignInOptions
{
/// <summary>
/// Verification token received when the user completes the captcha on the site.
/// </summary>
[JsonProperty("captchaToken")]
public string? CaptchaToken { get; set; }
}
}
16 changes: 16 additions & 0 deletions Gotrue/SsoResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;

namespace Supabase.Gotrue
{
/// <summary>
/// Single sign on (SSO) response data deserialized from the API {supabaseAuthUrl}/sso
/// </summary>
public class SsoResponse : ProviderAuthState
{
/// <summary>
/// Deserialized response from {supabaseAuthUrl}/sso
/// </summary>
/// <param name="uri">Uri from the response, this will open the SSO providers login page and allow a user to login to their provider</param>
public SsoResponse(Uri uri) : base(uri) { }
}
}
Loading

0 comments on commit 5acd1b2

Please sign in to comment.