Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SignInWithSso #94

Merged
merged 2 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading