Skip to content

Authorization with Gotrue

Will Iverson edited this page Sep 1, 2023 · 6 revisions

Session Persistence

Persisting, Retrieving, and Destroying Sessions.

This Gotrue client is written to be agnostic when it comes to session persistence, retrieval, and destruction. ClientOptions exposes properties that allow these to be specified.

In the event these are specified and the AutoRefreshToken option is set, as the Client Initializes, it will also attempt to retrieve, set, and refresh an existing session.

MAUI/Xamarin Examples

Phantom-KNA made an example of using Auth with Xamarin Forms/Maui.

Alternatively, using Xamarin.Essentials in Xamarin.Forms, this might look like:

// This is a method you add your application launch/setup
async void Initialize() {

    // Specify the methods you'd like to use as persistence callbacks
    var persistence = new GotrueSessionPersistence(SaveSession, LoadSession, DestroySession);
    var client = new Client(
            Url = GOTRUE_URL,
            new ClientOptions { 
                AllowUnconfirmedUserSessions = true, 
                SessionPersistence = persistence });
                
    // Specify a debug callback to listen to problems with the background token refresh thread
    client.AddDebugListener(LogDebug);
    
    // Specify a call back to listen to changes in the user state (logged in, out, etc)
    client.AddStateChangedListener(AuthStateListener);

    // Load the session from persistence
    client.LoadSession();
    // Loads the session using SessionRetriever and sets state internally.
    await client.RetrieveSessionAsync();
}

// Add callback methods for above
// Here's a quick example of using this to save session data to the user's cache folder
// You'll want to add methods for loading the file and deleting when the user logs out 
internal bool SaveSession(Session session)
{
    var cacheFileName = ".gotrue.cache";
    
    try
    {
        var cacheDir = FileSystem.CacheDirectory;
        var path = Path.Join(cacheDir, cacheFileName);
        var str = JsonConvert.SerializeObject(session);

        using (StreamWriter file = new StreamWriter(path))
        {
            file.Write(str);
            file.Dispose();
            return Task.FromResult(true);
        };
    }
    catch (Exception err)
    {
        Debug.WriteLine("Unable to write cache file.");
        throw err;
    }
}

You can find other sample implementations in the Unity and Desktop Clients documentation.

Updated Refresh Token Handling

The Supabase client supports setting a maximum wait time before refreshing the token. This is useful for scenarios where you want to refresh the token before it expires, but not too often.

By default, GoTrue servers are typically set to expire the token after an hour, and the refresh thread will refresh the token when ~20% of that time is left.

However, you can set the expiration time to be much longer on the server (up to a week). In this scenario, you may want to refresh the token more often than once every 5 days or so, but not every hour.

There is now a new option MaximumRefreshWaitTime which allows you to specify the maximum amount in time that the refresh thread will wait before refreshing the token. This defaults to 4 hours. This means that if you have your server set to a one hour token expiration, nothing changes, but if you extend the server refresh to (for example) a week, as long as the user launches the app at least once a week, they will never have to re-authenticate.

3rd Party OAuth

OAuth is a standard for allowing users to sign in to your application using an account from another provider. This is great, because it allows Supabase to support a wide range of social media providers. Unfortunately, most documentation for OAuth involves a purely web-based flow, which doesn't work well for native applications.

The workaround for this challenge is to use some kind of deep-linking mechanism. All of the major OS providers include a mechanism for allowing an native application to declare that it can handle a particular URL scheme. In this flow, the application will open a web browser to the OAuth provider's web site, and the web site will redirect back to the application using the URL scheme. The application can then handle the callback and complete the OAuth flow.

You'll have to check the documentation for your particular OAuth provider, your target framework[s], and your target platform[s] to figure out how to do set up this entire flow. Superficially, the flow is:

  1. The user clicks a social login button
  2. The application opens a web browser to the OAuth provider's web site with a callback URL that uses the application's URL scheme
  3. The user logs in to the OAuth provider
  4. The OAuth provider redirects back to the application using the callback URL
  5. The application handles the callback and sends the details back to Supabase.
  6. Supabase converts the OAuth token to a Supabase session.
  7. The application uses the Supabase session to authenticate the user.

Unfortunately, the details of this flow are different for every OAuth provider, every target framework, and every target platform. You'll have to do some research to figure out how to do this for your particular application.

Below is an example of how to do this for Google Sign In, but the details will be different for every provider.

Supabase OAuth

Once again, Gotrue client is written to be agnostic of platform. In order for Gotrue to sign in a user from an Oauth callback, the PKCE flow is preferred:

  1. The Callback Url must be set in the Supabase Admin panel
  2. The Application should have listener to receive that Callback
  3. Generate a sign in request using: client.SignIn(PROVIDER, options) and setting the options to use the PKCE FlowType
  4. Store ProviderAuthState.PKCEVerifier so that the application callback can use it to verify the returned code
  5. In the Callback, use stored PKCEVerifier and received code to exchange for a session.
var state = await client.SignIn(Constants.Provider.Github, new SignInOptions
{
    FlowType = Constants.OAuthFlowType.PKCE,
    RedirectTo = "http://localhost:3000/oauth/callback"
});

// In callback received from Supabase returning to RedirectTo (set above)
// Url is set as: http://REDIRECT_TO_URL?code=CODE
var session = await client.ExchangeCodeForSession(state.PKCEVerifier, RETRIEVE_CODE_FROM_GET_PARAMS);

Additional Example

Note: Make sure Project Settings -> Auth -> Auth settings -> User Signups is turned ON.

After your implementation with GoogleSignInClient or another, use the IdToken like this:

var identityToken = Encoding.UTF8.GetString(System.Text.Encoding.UTF8.GetBytes(GoogleIdToken), 0, GoogleIdToken.Length);

And finally use SignInWithIdToken for Login or Create New User:

var user = await Supabase.Auth.SignInWithIdToken(Supabase.Gotrue.Constants.Provider.Google, identityToken);

Thanks to Phantom-KNA for this example.

Native Sign In With Apple

Apple has recently taken more aggressive steps toward requiring developers to use the native Sign in with Apple flow on submitted applications. In particular, while you can use an OAuth flow for Sign in with Apple on web platforms, many developers have reported issues with Apple rejecting applications that use this flow on iOS.

To support Sign in with Apple, first you need to configure the flow with Apple and Supabase. You can find instructions for this in the Supabase documentation on Apple Sign in. Be sure to follow the steps very carefully, as a single typo can cause the whole thing to not work.

Tip: It may seem confusing to have to configure the flow for native Sign in with Apple "twice" - once for the OAuth web flow and once for native mobile/desktop flow. This is because the native flow gets tokens back from Apple, and then uses the web flow to complete the validation process.

Once all of the server configuration is done, you just need to configure a few options for the native dialog, call it to present it to the user, and then handle the callback. Once you have the information from the dialog (a JWT from Apple), you then send that JWT to Supabase to convert it to a Supabase session.

Tip: You have to make sure everything is all configured correction - the application bundle ID, the nonce, etc. If you get any of these wrong, the dialog will not work and the error message may or may not be very helpful. You may want to include some kind of debug logging to help you figure out what's going on in a way that you can monitor even in a non-debug build. This might be as simple as writing to a user-accessible file or perhaps involving presenting a debug console in the UI in some fasion.

If you run into issues, you may find this tech note from Apple on Sign in with Apple error messages helpful.

Native Sign in with Apple via Unity

You can use native Sign in with Apple functionality by installing a plugin.

Depending on the platform you are using, you may need to write a bit of Swift code to expose the native dialog, or you may be able to find an existing library. For example, here is information on using native Sign in with Apple on Xamarin.

Here is a snippet of pseudo-C# code to show how to use the native dialog:

// These are values that we will use to handle the flow.

// This is a one-time use code. It's used to verify the JWT from Apple.
private string? _nonce;

// This is a hash of the one-time use code.
private string? _nonceVerify;

// This is the identity token from Apple.
private string? _identityToken;

// This is the authorization code from Apple.
private string? _authorizationCode;

// This method brings up the native dialog.
public void SignInWithApple()
{
    // This generates a random nonce (a one-time code) used to verify the JWT from
    // Apple.
    _nonce = Supabase.Gotrue.Helpers.GenerateNonce();
    
    // This is a SHA256 hash of the nonce.
    _nonceVerify = Supabase.Gotrue.Helpers.GenerateSHA256NonceFromRawNonce(_nonce);

    // Here we set the options for the native dialog, including the nonce.
    AppleAuthLoginArgs loginArgs =
        new(LoginOptions.IncludeEmail | LoginOptions.IncludeFullName, _nonceVerify);

    // Here we are asking the native dialog wrapper library to present the native
    // dialog. We are passing in the nonce and a callback to handle the results.
    _appleAuthManager!.LoginWithAppleId(loginArgs, SuccessCallback, ErrorCallback);
}

// This method handles the callback from the native dialog and sets the data
// to be used later.
private void SuccessCallback(ICredential credential)
{
    // Obtained credential, cast it to IAppleIDCredential
    if (credential is IAppleIDCredential appleIdCredential)
    {
        // Apple User ID
        string? userId = appleIdCredential.User;

        // Email and first name are received ONLY in the first login
        // You may want to add logic to save these fields locally

        // Identity token
        _identityToken = Encoding.UTF8.GetString(appleIdCredential.IdentityToken, 0,
            appleIdCredential.IdentityToken.Length);

        // Authorization code
        _authorizationCode = Encoding.UTF8.GetString(appleIdCredential.AuthorizationCode, 0,
            appleIdCredential.AuthorizationCode.Length);

        // And now you have all the information to create/login a user in your system
        _doAppleSignIn = true;
    }
    else
    {
        // Error handling - you don't want to use Sign in with Apple on non Apple devices
    }
}

// Now we use an Update() declared async.
private async void Update()
{
    // Updates the AppleAuthManager instance to execute
    // pending callbacks inside Unity's execution loop
    _appleAuthManager?.Update();

    if (_doAppleSignIn)
    {
        _doAppleSignIn = false;
        // ReSharper disable once Unity.PerformanceCriticalCodeInvocation
        bool status = await SignInToSupabaseWithApple();
        if (!status)
            Debug.Log("Sign in failed", gameObject);
        _doAppleSignIn = false;
    }
}

// This method takes the data from the callback and sends it to Supabase to
// validate the JWT and create a Supabase session. Note that because this method
// goes over the network, it's declared async. In Unity this means you might want
// to call this method from an Update method marked async.
private async Task<bool> SignInToSupabaseWithApple()
{
    try
    {
        Session? session = await SupabaseManager.Client()?.Auth.SignInWithIdToken(Constants.Provider.Apple, _identityToken!, _nonce)!;
        if (session?.User?.Id != null)
        {
            // You logged in successfully! Depending on your application you may want to load another scene, etc.
        }
        else
        {
            // Something went wrong - you may want to display an error message to the user.
        }
    }
    catch (GotrueException e)
    {
        // Something went wrong - you may want to display an error message to the user. GotrueExceptions
        // generally mean the server sent back an error message.
    }
    catch (Exception e)
    {
        // Catch-all for anything else that might have gone wrong.
    }
    return true;
}

Offline Support

The Supabase Auth client supports online/offline usage. The Client now has a simple boolean option "Online" which can be set to to false. This can be combined with the NetworkStatus class to allow the client to automatically go online & offline based on the device's network status.

To use this new NetworkStatus, add the following:

// Create the client
var client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true });
// Create the network status monitor
var status = new NetworkStatus();
// Tell the network status monitor to update the client's online status
status.Client = client;
// Start the network status monitor
await status.StartAsync();
// rest of the usual client configuration

Only the stateful Client supports this feature, and only for the managed user sessions. Admin JWT methods and the stateless client are not affected.