supabase-community / supabase-csharp

A C# Client library for Supabase
https://github.com/supabase-community/supabase-csharp/wiki
MIT License
489 stars 50 forks source link

Razor Pages / MVC Authentication Example #73

Closed CallumVass closed 1 year ago

CallumVass commented 1 year ago

Feature request

Is your feature request related to a problem? Please describe.

First of all, thanks for a great project - I have looked at the examples but they all seem to be based off of Blazor WASM. I have started a new project using Razor Pages but there is a lack of documentation around how to do the authentication server side

Describe the solution you'd like

An example project that utilised Razor Pages or MVC pattern.

Describe alternatives you've considered

Looked at existing examples but they are based off of Blazor WASM

Additional context

I've added Supabase as a Scoped dependency following the video linked in the README:

builder.Services.AddScoped(_ => new Supabase.Client(supabaseOptions!.Url, supabaseOptions.AnonKey,
    new Supabase.SupabaseOptions
    {
        AutoRefreshToken = true
    }));

I'm using the PKCE flow for OAuth:

var signInUrl = await _supabaseClient.Auth.SignIn(Constants.Provider.Google, new SignInOptions
{
    FlowType = Constants.OAuthFlowType.PKCE,
    RedirectTo = $"{Request.Scheme}://{Request.Host}/login/callback"
});

Then in my login callback handler:

app.Map("/login/callback",
    async (Supabase.Client supabaseClient, HttpContext context) =>
    {
        if (context.Request.Query.ContainsKey("code"))
        {
            var code = context.Request.Query["code"];

            if (context.Request.Cookies.TryGetValue("PKCEVerifier", out var verifier))
            {
                var session = await supabaseClient.Auth.ExchangeCodeForSession(verifier, code!);

                context.Response.Cookies.Delete("PKCEVerifier");

                var identity = new ClaimsIdentity();
                if (!string.IsNullOrEmpty(session?.AccessToken))
                {
                    identity = new ClaimsIdentity(ParseClaimsFromJwt(session.AccessToken), "jwt");
                }

                var user = new ClaimsPrincipal(identity);
                await context.SignInAsync(new ClaimsPrincipal(user));

                context.Response.Redirect("/");
            }
        }
    });

But when I go to my home page, if I try and get the email for the current user (as an example), its null: _supabaseClient.Auth.CurrentUser?.Email;, however, I can see that I am authenticated as calling this returns true: User.Identity?.IsAuthenticated. So I guess I'm missing a way of provisioning the supabase client with the currently logged in user, any ideas? The reason being is because I have RLS enabled so I want the user to see their data only. Thanks

CallumVass commented 1 year ago

I think I've figured it out. Firstly I created a CustomSessionHandler which I registered in my DI container:

builder.Services.AddScoped(provider => new Client(supabaseOptions!.Url, supabaseOptions.AnonKey,
    new Supabase.SupabaseOptions
    {
        AutoRefreshToken = true,
        SessionHandler = new CustomSessionHandler(provider.GetRequiredService<IHttpContextAccessor>(),
            provider.GetRequiredService<ILogger<CustomSessionHandler>>())
    }));
public class CustomSessionHandler : IGotrueSessionPersistence<Session>
{
    private readonly ILogger<CustomSessionHandler> _logger;
    private readonly HttpContext? _context;

    public CustomSessionHandler(IHttpContextAccessor httpContextAccessor, ILogger<CustomSessionHandler> logger)
    {
        _logger = logger;
        _context = httpContextAccessor.HttpContext;
    }

    public void SaveSession(Session session)
    {
        _logger.LogInformation("Saving session");
        _context?.Response.Cookies.Append("sb_session", JsonSerializer.Serialize(session),
            new CookieOptions { HttpOnly = true, Secure = true });
    }

    public void DestroySession()
    {
        _logger.LogInformation("Destroying session");
        _context?.Response.Cookies.Delete("sb_session");
    }

    public Session? LoadSession()
    {
        _logger.LogInformation("Loading session");
        return _context?.Request.Cookies.TryGetValue("sb_session", out var session) == true
            ? JsonSerializer.Deserialize<Session>(session)
            : null;
    }
}

Then I've created middleware to load the session and re-initialize:

app.Use(async (context, next) =>
{
    if (context.User.Identity?.IsAuthenticated ?? false)
    {
        var supabaseClient = context.RequestServices.GetRequiredService<Supabase.Client>();
        supabaseClient.Auth.LoadSession();
        await supabaseClient.InitializeAsync();
    }

    await next.Invoke();
});

Let me know if this is the correct way or if there is another, more official way. Thanks.

acupofjose commented 1 year ago

Sorry for the late reply @CallumVass, busy last couple of days for me.

Yes, that solution is almost exactly how I would’ve approached it. Looks good to me! Thanks for taking the time to work through and post it.

CallumVass commented 1 year ago

Thanks for the reply, I'll close this now then.