C
C#2mo ago
Swyftey

✅ C# private endpoint security: How should it be done?

Hi, I am working on a backend API which I want to be tightly coupled with Discord (some entity keys will be Discord snowflakes, like users and servers). My private endpoints will look like this: /users/:userId/servers/:serverId Because of this, I am trying to figure out how to go about authorization for my private endpoints. This is a bit tricky for me because I want to do a couple things: - Validate route parameter input (snowflake regex) - Verify user existence and ownership (ensure user ID provided in route matches the session identity user ID and that user exists in DB) - Verify server existence and ownership (check with discord API on writes, check database on reads, only if this is an ideal approach) As far as I know, I am sort of limited with my options(?), because I would prefer a custom failure response object (not just basic 401-404 which .NET provides for policies, route constraints or whatnot), the cheaper checks to run first (route validation and user ownership verification), and I want reusability for multiple private endpoints, or probably all? What are my options? What do developers normally do in these type of situations? Thanks for any help!
101 Replies
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
Oh man, that's huge topic :kekw: Here you have playlist for authentication and authorization in C# In general it's a huge topic and I am still not quite good at it but, since I am messing with it rn, I can tell few things: You can make requirements and requirement handlers, so for example we could have 2 requirements here:
public class UserIsOwnedRequirement : IAuthorizationRequirement
{
public required string UserId { get; init; }
}

public class UserIsOwnedHandler : AuthorizationHandler<UserIsOwnedRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserIsOwnedRequirement requirement)
{
if (requirement.UserId != context.User?.Claims
.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value)
{
context.Fail();
}
else
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}
public class UserIsOwnedRequirement : IAuthorizationRequirement
{
public required string UserId { get; init; }
}

public class UserIsOwnedHandler : AuthorizationHandler<UserIsOwnedRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserIsOwnedRequirement requirement)
{
if (requirement.UserId != context.User?.Claims
.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value)
{
context.Fail();
}
else
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}
And second requirement for checking server existence, server owner etc. You can then inject IAuthorizeService and use .AuthorizeAsync with those requirements. You have your return value from that and then you can decide what to return from your endpoint handler ^^. That's one way of doing that. Or you can simply check all of that in route handler, I overengineered that xD
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
It's not just for a bot. I'd rather not reveal too much, but basically I am working on a backend API for a SaaS website and a discord bot eventually. Users login via Discord and can register their servers, etc. To my understanding, they run in parallel and you cannot set a specific order unless you use it in code (not as an attribute). Am I correct on this?
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
I would just use that. And then in route handler: (sorry, I like controllers)
[HttpGet("/user/:userId/server/:serverId")]
public async Task<IActionResult> HandleStuff(long userId, long serverId)
{
// Validate any params here:

var userOwnership = await _authorizationService.AuthorizeAsync(HttpContext.User, null, [new UserIsOwnedRequirement { UserId = userId }]);

if (!userOwnership.Succeeded)
{
return Forbidden(); // or not found or whatever
}

var serverOwnership = await _authorizationService.AuthorizeAsync(HttpContext.User, null, [new ServerOwnershipRequirement { ServerId = serverId }]);

if (!serverOwnership.Succeed)
{
return Forbidden(); // or whatever
}

// Success path here
}
[HttpGet("/user/:userId/server/:serverId")]
public async Task<IActionResult> HandleStuff(long userId, long serverId)
{
// Validate any params here:

var userOwnership = await _authorizationService.AuthorizeAsync(HttpContext.User, null, [new UserIsOwnedRequirement { UserId = userId }]);

if (!userOwnership.Succeeded)
{
return Forbidden(); // or not found or whatever
}

var serverOwnership = await _authorizationService.AuthorizeAsync(HttpContext.User, null, [new ServerOwnershipRequirement { ServerId = serverId }]);

if (!serverOwnership.Succeed)
{
return Forbidden(); // or whatever
}

// Success path here
}
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
in that way you can run them with your desired order you can also pass all your requirements but you don't have guaranteed order as specified in docs
Swyftey
SwyfteyOP2mo ago
This is what I was concerned about. For example, I'd probably want the cheap checks to run first so that heavy API calls aren't firing even when the request fails by one of the cheap checks
ScuroGuardiano
ScuroGuardiano2mo ago
or you can just write single big requirement and handler for that. And in one handler check both, user ownership and server ownership
Swyftey
SwyfteyOP2mo ago
This is slightly confusing for me, I'll have to read up more on it I guess
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
That is true
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
This entire auth stuff in .NET is very confusing and complicated when you're learning it. But when you get it, you see how flexible it is
Swyftey
SwyfteyOP2mo ago
So, to see if I am understanding, let's say this specific controller's endpoints will all have the same authorization requirements after all (check user, check server ownership (only call discord API based on request method, etc), then I should just make one big policy like Scuro said and place as attribute over entire controller perhaps? I'm sure it just needs to click for me
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
It would probably be the latter here
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
Oh and you have also Resource Based Authorization, that is fun too! But I guess overkill for this scenario. https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-9.0
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
So, a policy can have more than 1 requirement? Like
ScuroGuardiano
ScuroGuardiano2mo ago
yes
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
.AddPolicy("test", new(), new())
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
This definitely would work, but I was hoping to try attrributes first unless it it doesn't end up being ideal for my use-case I see.
ScuroGuardiano
ScuroGuardiano2mo ago
Attribute wouldn't work here well
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
Can one policy have multiple requirements or do I have to make them different policy names? Like, "GetOwner" and "GetOwnerWithUpdate" or idk those were random names but you get the idea
ScuroGuardiano
ScuroGuardiano2mo ago
Well, you could make policy for that and then in requirements handler get route params from http context to validate that xD
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
oh, there is ❤️ That is fun too
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
Nevermind, I see it now
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
that makes sense
ScuroGuardiano
ScuroGuardiano2mo ago
Custom Authorization Policy Providers in ASP.NET Core
Learn how to use a custom IAuthorizationPolicyProvider in an ASP.NET Core app to dynamically generate authorization policies.
ScuroGuardiano
ScuroGuardiano2mo ago
this 😄
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
but you can't pass route params to attribute, can you?
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
Wait woah, what is an "AuthorizeAttribute" vs IAuthorizationRequirement / AuthorizationHandler
ScuroGuardiano
ScuroGuardiano2mo ago
AuthorizationHandler handles requirements 😄 Requirement is class that you can put in any data you want and have access to that in handler
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
Ohhh, AuthorizeAttribute seems to be just for further customization I am seeing
ScuroGuardiano
ScuroGuardiano2mo ago
whoah, I didn't know about that And was doing that in ValidatePrincipal event handler XD
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
smart
ScuroGuardiano
ScuroGuardiano2mo ago
That's nice, I must check it out
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
So basically using claims to hold the in-memory data potentially needed other than some other way? whoops. wrong reply
ScuroGuardiano
ScuroGuardiano2mo ago
Oh my god XD
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
I think what will be the toughest about all of this is just how many different ways there is to do one thing, and how to strategize/structure things
ScuroGuardiano
ScuroGuardiano2mo ago
Alright, pretty nice
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
Well, .NET gives you a lot of tools it's up to you how you use it 😄 Except for Blazor, I feel very limited while using Blazor.
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
oh no xD
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
understandable
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
Well, I am doing small project in work rn and I decided to not implement anything like that. I am just using roles from Microsoft Entra ID :kekw: But I can do that, coz project is very simple
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
Whoah, that's big
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
I see
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
So, based off of what you just said, if these were my two current endpoints: - PUT /users/:userId/servers/:serverId - GET /users/:userId/servers/:serverId I should probably: Make two policies (like "PrivateEndpointPolicyWrite" and "PrivateEndpointPolicyRead", these are just example names) and make the 3 requirements which are slightly dynamic, like with the write, the requirement is set to use Discord api (eg, UseDiscordApi = true) and the read is slightly different? If so, the only problem would be the order again, I think.
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
the firing order of them like
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
omfg i get it :facepalm:
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
so you're saying the API doesn't return on the first failure, it checks all at once? not API auth
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
yes
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
No description
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
"It is not short-circuit"
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
this is what I was concerned about mostly yes I understand this bit
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
In my head, I was thinking context.fail() was cancelling the auth immediately
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
I totally get this now at least the very basic idea of it it just clicked for me
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
no worries thank you for your help
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
lunch?! I haven't even slept yet, it's 6 AM
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
Jeesh So UK? Germany? I'm guessing somewhere around there I'm not good with locations but know Wales is around 5 hours ahead
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
Swyftey
SwyfteyOP2mo ago
anyways shit, goodnight lmao
ScuroGuardiano
ScuroGuardiano2mo ago
Definitely not Poland, we don't have lunch in here
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
tru, my first coffee today hmm, you must be in similar timezone as me xD
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
ScuroGuardiano
ScuroGuardiano2mo ago
I am procrastinating since 2h ago

Did you find this page helpful?