ā Handling Supabase auth with dotnet backend. Is this way wrong/not the dotnet way?
I'm authenticating users on the frontend using supabase auth. After successful authentication I get returned user data, jwt, and more.
I then want communicate with some backend services, which I'm trying to write in dotnet.
I include the token from FE in the request and then handle it with some middleware to handle this like so:
But from my experience when doing auth on .Net you're "supposed" to use the
useAuthorisation/useAuthentication
middleware that's provided by default in the .Net framework?
Am I trying to force a very "not dotnet" style of coding onto the framework?
Is there a better way to do this?37 Replies
there's nothing wrong with just writing your own middleware like this, no
but auth is such a widespread topic, affecting other layers of the application, and with so many pitfalls, that it's definitely best practice to use the standard packages, and configure them to your needs
both packages are highly extensible for this reason
for starters, what's Suprabase?
so, probably a fairly standard OAuth implementation?
Unknown Userā¢2y ago
Message Not Public
Sign In & Join Server To View
exactly
depending on how "standard" the OAuth is, you might actually be able to skip THAT step as well, and use the existing OAuth handler
like, if you want are more detailed lecture on how to do it, I can definitely give one
and I don't mind too much, cause I remember how infuriating it was learning this for myself
not because it's exceptionally complicated, but there's a LOOOOOOT of moving parts, and some are lax in documentation
Unknown Userā¢2y ago
Message Not Public
Sign In & Join Server To View
you typo'd a bit there
good try, tho
š
Unknown Userā¢2y ago
Message Not Public
Sign In & Join Server To View
@TeBeCo
I looked at that library but my problem is that since I'm signing in the user in the frontend, the session exists in the context of the react application.
I don't see how I can share the exact same context between the FE and BE like you would need if you are using the C# libraries features like
var user = supabase.Auth.CurrentUser
.
Since i'm lacking the session context on the BE side the only way I can figure out how to get the user without it already existing in the BE context is by manually calling the auth endpoint to get the user given the token and supabase credentials.
I'm just very confused on how you would share the "session/context" using the more standard .net auth framework since the oauth login flow is more done on the FE side and the entire session/context is hosted on Supabase end.
If I write my own authentication handler, is it basically the same as my middleware but I wrap it/extend it as some IAuthorizationHandler
?so
yeah, I'm not gonna try and parse apart how you're understanding this in your head
it'll be quicker to just talk about what the architecture SHOULD be
hopefully the misunderstanding reveals itself
so, you have a client/server app?
Blazor frontend, ASP.NET Core backend?
something similar?
@GurkanG
I have a react frontend application and a dotnet core backend. They are totally separate.
On the frontend side I am signing in users using the supabase auth api.
This is done using the supabase node package. It basically lets me do
await supabase.auth.signIn(credentials).
. Supabase then handles everything else and returns a session and user.
This session context is stored in a react context as well.
On the backend side I need to authenticate the requests from my FE.
And thus I am sending along the JWT that I get from supabase after successful authentication on their end to my backend API.
Here, I am then using the JWT, along with a supabase secret key, to check against an endpoint /auth/user
if the JWT is correct, and if it is, I get the user returned to my backend API.
Now I know that the request from my FE is an authenticated one and I can access the user information of the user that initiated the request from my react application.
I can do what I want here since I've got all the information needed on the user. , ie, return data that is specific to the user.
@AnievNekaj Does this make sense?absolutely
does the backend communicate with Supabase, other than to validate the token?
and pull user info
Other than the request to authenticate the JWT (which, if successful return a user object) there is no other communication done from my BE to supabase.
I have everything else on my own side, with a postgres DB where any user specific data just has the
user_id
from supabase as the foreign keylovely
that's exactly correct
essentially what you're looking to do is have your client send the supabase token to the backend, and exchange it for the backend's own auth token
which you then use for all further operations
let's assume you want to use a JWT for this
all your authentication concerns for the server can be accomplished with the
Microsoft.AspNetCore.Authentication
middleware, configured with the pre-made Microsoft.AspNetCore.Authentication.JwtBearer
authentication scheme
plus your own configuration
the middleware provides the AuthenticationHandler
base class for customization
and then the JwtBearer scheme provides an implementation of it, which has its own extension points
in more complex scenarios, the middleware allows you to configure an arbitrary number of "schemes" each, with its own handlerSo I can implement
IAuthenticationService
to also add the supabase secret key to the request? Since I need not only the JWT but also that secret key for a valid request?correction:
AuthenticationHandler
yes, but also no
the magic here is that your exchanges with Supabase are actually NOT part of the authentication middleware
not directlyHaha and now i'm confused again
yeah
so, the way the middleware models things
"authentication" consists of 5 fundamental operations
which are implemented by
AuthenticationHandler<T>
and subclasses
Authenticate
Challenge
Forbid
SignIn
SignOut
however, SignIn and SignOut are actually not "core" operations
they're not implemented by AuthenticationHandler<T>
, but by subclases SignInAuthenticationHandler<T>
and SignOutAuthenticationHandler<T>
I.E.
not every authentication scheme actually INVOLVES these operations
and JWT auth is one of them
because there's no "session"
the scheme is stateless, either a request has a good token, or it doesn't
that's the "Authenticate" operation, BTW
the operation of looking at an incoming request, and trying to figure out who it came from
"Challenge" is the operation of telling the user "You need to take further action to authenticate", I.E. "you're not signed in, but you need to be"
"Forbid" is the operation of telling the user "You can't do this, you're shit out of luck", I.E. "you're signed in, and that's how I know you're not supposed to access this"
anyway
you could maybe argue that JWT ought to have SignIn and SignOut operations, but the way Microsoft implemented the scheme, it doesn't, and I'm sure there's good reason
all it means is that you need to setup a dedicated endpoint for the token exchange operation
anything not make sense?I appreciate you explaining this for me! It's always good to a have better understanding of how all this works š
It makes sense, but I'm still a bit unsure about how exactly I would fit my current needs to this way of working with auth.
I would implement my own handlers and then add those to the
Authentication.JwtBearer
authentication scheme? And it is within these AuthenticationHandler<T>
is where I would all the logic that is similar to my middleware, where I make a request to the supabase auth api endpoint and check wether i have a valid user response or not?no, the handler's already implemented for you, you just need to slap in a few configuration bits
concrete examples next
obviously
setup the middleware
next, setup and configure services, which control the middleware
I think I understand what I need to do.
I will try a bit on my end to implement this and I'll update this thread when I get it working!
Thanks again @AnievNekaj for the guidance and help
hang on, hang on
I'm still typing
haha š
AddAuthentication()
returns an AuthenticationBuilder
that lets you add schemes
an AddJwtBearer()
adds the JwtBearer scheme
the parameter JwtBearerDefaults.AuthenticationScheme
is the "default" scheme to be used by any requests/endpoints that don't specify which one should be used
I.E. in your case, all of them
cause you're only gonna have one
there's a lot of stuff available on JwtBearerOptions
and they're all pretty self-explanatory
the only thing I'm configuring here is how the tokens are validated
cause I'm using standard tokens
of note
I am using my own made-up secret to digitally sign tokens, and that's what that PostConfigureWith()
bit is about
the token is loaded through the config system, but the config system isn't ready to use at DI setup time
.PostConfigireWith()
is the Microsoft.Extensions.Options
system, letting me do options changes, as soon as a the DI system is available, so, I'm getting that IOptions<AuthenticationConfiguration>
value from DI
AuthenticationConfiguration
being my own class that ties into Microsoft.Extensions.Configuration
so, with this in place, all three of the core auth operations are done
on each request, the JwtHandler will look for a token in the request, check if it's valid, and if so, parse all of its claims, and make those claims available on IAuthenticationService
you can then pull IAuthenticationService
through DI to inspect them yourself
or you can configure the AuthorizationMiddleware
to inspect them
also
I left a chunk out here that I use, which is probably worth mentioning
one of the most useful extension points of any auth scheme is options.Events
here, I'm doing two things
A) the data structure for claims on IAuthenticationService
is kinda a pain in the ass
if I just wanna know "what's the user id", it's a multi-step process to look up if the claim is there, and then parse it if the value you want isn't a string
so, the IAuthenticationService
you see there is actually NOT Microsoft.AspNetCore.Authentication.IAuthenticationService
, it's a totally separate interface from my own business layer
I'm using OnTokenValidated
to call my own business logic on each request, which will go ahead and parse out the UserId I care about, and store it for me to easily reuse
also, it's more mockable and testable
maybe there's a way to customize IAuthenticationService
to put your own custom data on there, I've never looked into it
B) there's an additional GuildId
field that the user gets freedom to pick and change in the UI, at any time, so doesn't belong in the token, but which affects auth operations, so I'm using OnMessageReceived
to pick that out of an HTTP header on each request, parse it, and store it, same as UserID
just to illustrate, there's a lot of extension points for this middleware, not just for changing config options, but for injecting your own logic
and yes, if it came down to it, you could always just make your own AuthenticationHandler<T>
now
for the "sign-in" side of things
again, maybe it could make sense to run this through AuthenticationMiddleware
, and you'd probably do that by subclassing your own AuthenticationHandler<T>
that implements ISignInAuthenticationHandler<T>
and/or ISignOutAuthenticationHandlerT<>
and then you'd configure the middleware to use JwtBearer
as the scheme for Authentication
, Challenge
and Forbid
operations, and use your custom scheme for SignIn
and/or SignOut
in my mind, it made way more sense, and was simpler, to just implement a couple HTTP endpoints
this project uses gRPC, and I setup 2 endpoints
StartLogin
and CompleteLogin
StartLogin
literally just builds and returns the OAuth URL that the client should redirect to
since I can then set it up to pull the OAuth clientId from one place in server config
the fun part is the CompleteLogin
endpoint, which receives the temporary OAuth access token, validates it, retrieves user info, and builds our own token, precisely as you want to do
none of this involves the Authentication middleware, cause requests aren't being authentication
it's the server operating as the client against the OAuth provider
also, for good measure, I'm immediately telling Discord to revoke the tokens, since I don't need them anymore
but I'm doing this asynchronously, without (immediately) awaiting, cause I already have all the info needed to respond to the client
which is here
JwtSecurityTokenHandler
comes from System.IdentityModel.Tokens.Jwt
and is ready-made to build basically any kind of JWT you could ever want
@GurkanG asleep yet?@AnievNekaj I'm alive and kicking. Just had to go away for a bit haha
š
Is there any reason why I couldn't do something like this:
And then in the
SupabaseAuthenticationHandler
:
doesn't really make sense to me
you're looking for the Supabase token here?
you're expecting that token on EVERY request to your server?
and you're going to go check it against Supabase on EVERY request?
Well why wouldn't I?
How else would you make sure that each and every request is correct and authenticated? Since all requests to this API needs to be authenticated isn't it just smart to check that for every request?
no
you should not be using a Supabase access token if you're not accessing Supabase resources
But I need the token to get the user information though?
only once
only on initial sign in
at least, in my opinion
do you really gain any practical amount of security by validating CONSTANTLY
for one, is Supabase even gonna LET you spam them this often?
I'll wager you'd end up getting ratelimited
and the security you gain by doing this is "I'll know if Supabase locks the account or revokes the token before its expiration"
is that even a thing that can happen?
I haven't have a problem yet with other systems getting rate limited when doing a similar setup. But those backends I've written in node/express.
I mean, if you think about supabase as just an auth provider and it all does is acts like an extra microservice that "takes in an JWT, checks if it's correct, then returns user if it's not" is this really a big error?
not terribly, it's just inefficient
at the VERY least, you can cache token validations
the token is just text, validate it once, and that result isn't gonna change, unless Supabase allows for revokation
consider this
what I'm describing is precisely what Supabase does
they don't do their own auth, they farm it out to Microsoft, Discord, Facebook, etc.
the token you get to access supabase resources is not a token from any of those providers, it's their own, that they've exchanged
if I'm reading this right, anyway
in any case, if you really do want to call to a third party for every auth, and/or that's the way Supabase expects you to do it, what you proposed is pretty much the way to go
you also may not need to subclass your own
AuthenticationHandler<T>
for it
check if there's any hooks in JwtBearerEvents
you can use
at a glance, I'd say you can just use the OnMessageReceived
event, which takes an async
handler, and gives you the ability to call .Fail()
or .Success()
on the context
I.E. you can extract the token and validate it thereOkay I'll give it a go and see if I can get it working š
Thanks for the help! Appreciate it a lot
o7
Was this issue resolved? If so, run
/close
- otherwise I will mark this as stale and this post will be archived until there is new activity.