❔ Massive issue with Authentication

So im trying to add server password handling but instead of making my own handler im forced to implement some AuthenticationHandler for it, which I did, but now the AuthenticateResults aren't even being given to the request user so its useless, and the Authentication Checks are being ran on every Endpoint instead of the ones i added [Authorize(myStuff)] into, and i have no idea what to do Program.cs:
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(options =>
{

}).AddScheme<Utils.ServerKeyAuthenticationOptions, Utils.ServerKeyAuthenticationHandler>("serverPass", options => { });

builder.Services.AddControllers();

ConfigureServices(builder.Services);
var app = builder.Build();

app.UseHttpsRedirection();

app.UseMiddleware<Utils.RequestCounter>();


app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(options =>
{

}).AddScheme<Utils.ServerKeyAuthenticationOptions, Utils.ServerKeyAuthenticationHandler>("serverPass", options => { });

builder.Services.AddControllers();

ConfigureServices(builder.Services);
var app = builder.Build();

app.UseHttpsRedirection();

app.UseMiddleware<Utils.RequestCounter>();


app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
92 Replies
The Fog from Human Resources
my Handler:
public class ServerKeyAuthenticationHandler : AuthenticationHandler<ServerKeyAuthenticationOptions>
{
public ServerKeyAuthenticationHandler(
IOptionsMonitor<ServerKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("server-key"))
{
return AuthenticateResult.Fail(JsonConvert.SerializeObject(TemplateResponses.NoServerKey()));
}

string serverKey = Request.Headers["server-key"];

if (!Utils.Accounts.IsUserWhiteListed(serverKey))
{
return AuthenticateResult.Fail(JsonConvert.SerializeObject(TemplateResponses.InvalidServerKey(serverKey)));

}


var claims = new[] { new Claim(ClaimTypes.Name, serverKey) };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}

public class ServerKeyAuthenticationOptions : AuthenticationSchemeOptions
{
}
}
public class ServerKeyAuthenticationHandler : AuthenticationHandler<ServerKeyAuthenticationOptions>
{
public ServerKeyAuthenticationHandler(
IOptionsMonitor<ServerKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("server-key"))
{
return AuthenticateResult.Fail(JsonConvert.SerializeObject(TemplateResponses.NoServerKey()));
}

string serverKey = Request.Headers["server-key"];

if (!Utils.Accounts.IsUserWhiteListed(serverKey))
{
return AuthenticateResult.Fail(JsonConvert.SerializeObject(TemplateResponses.InvalidServerKey(serverKey)));

}


var claims = new[] { new Claim(ClaimTypes.Name, serverKey) };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}

public class ServerKeyAuthenticationOptions : AuthenticationSchemeOptions
{
}
}
snowy | switching accounts
I'm a bit confused, what do you mean by "Server Key Authentication"? What is the expected behavior? You mean like an api key?
The Fog from Human Resources
No it's more like The server can have a password and if a user sends the password to the server via POST request, they get a server key which allows them to use the other endpoints The expected behavior is: Password correct: it continues the request Password missing: 403 Password Wrong: 401
snowy | switching accounts
So there are public endpoints and secured endpoints. One of the public endpoints is basically an authentication and this authentication is password-only?
The Fog from Human Resources
Basically yds Yes
snowy | switching accounts
Any reason on why you don't want to use a JWT?
The Fog from Human Resources
And I want only certain endpoints to be affected by this which is why I use [Authorize] What's that
snowy | switching accounts
oh You're never messed with auth before?
snowy | switching accounts
JWT.IO - JSON Web Tokens Introduction
Learn about JSON Web Tokens, what are they, how they work, when and why you should use them.
snowy | switching accounts
Do you know when you login to a website and they give you an accessToken?
The Fog from Human Resources
And also the error result of the key challenge stuff doesn't return to the user it just leaved it blank Yes
snowy | switching accounts
It is usually a JWT, this JWT contains the user's data and gives him access to protected endpoints
The Fog from Human Resources
Closest thing I ever gotten to auth is discord auth stuff I mean my system would work perfectly fine the only issue is that my function is somehow invoked on all endpoints even the ones that don't even exist And it won't send a body to the user
snowy | switching accounts
You are in the right path, you made your own handler and added authentication Did you by any chance add it as a middleware?
The Fog from Human Resources
The only Middleware I have is a request counter I tried to add the body manually but then it just displayed {my Error Body}{my actual response body} On public endpoints that require nothing
snowy | switching accounts
It's really weird that it's triggering for every request But honestly if I were you I'd just use the JWT support Should work out of the box The only thing you'd implement is part of the JWT generation
The Fog from Human Resources
but the thing is id probably have to recode a lot
snowy | switching accounts
Not really Let me show you an example
The Fog from Human Resources
could i still use some [Attribute] to declare this function to require the password?
snowy | switching accounts
yes you'd use [Authorize] in the controller endpoints
The Fog from Human Resources
but wouldnt JWT be a little overkill for it?
snowy | switching accounts
Why would it
The Fog from Human Resources
i dunno as far as ik its for validating request signitures
snowy | switching accounts
It validates the token the user provides to know whether it is a valid token generated by a trusted issuer or not but you don't do any of that There is already microsoft libraries that do that for you "out of the box"
snowy | switching accounts
1. You basically just add Microsoft.AspNetCore.Authentication from Nuget to your project. 2. Call AddAuthentication as following:
var secretKey = configuration["JwtSecret"];
if (string.IsNullOrEmpty(secretKey))
throw new ArgumentNullException(nameof(secretKey), "JwtSecret is null or empty");

var keyBytes = Encoding.UTF8.GetBytes(secretKey);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(keyBytes),
ClockSkew = TimeSpan.Zero
});
var secretKey = configuration["JwtSecret"];
if (string.IsNullOrEmpty(secretKey))
throw new ArgumentNullException(nameof(secretKey), "JwtSecret is null or empty");

var keyBytes = Encoding.UTF8.GetBytes(secretKey);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(keyBytes),
ClockSkew = TimeSpan.Zero
});
You have to provide it a secret key, you should store it safely, either through dotnet secrets or a secret manager. For your specific scenario, you don't have to worry about ValidateIssuer or ValidateAudience, if you want to learn more about it, I'd just try watching an youtube video on it or reading some material. 3. After you've done that, go to your Program.cs and register the AddAuthentication and AddAuthorization middlewares before the MapControllers. 4. Last step would be having some sort of Login endpoint where you'd call your login logic, and if the password is valid, you'd generate a JWT for that specific user, or just a JWT that gives the user access to the api. There are a variety of ways to do so, like this one for example:
public string GenerateToken(AppUser user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.UserName)
};

var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSecret"]!));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
var token = new JwtSecurityToken(claims: claims, expires: DateTime.Now.AddDays(7), signingCredentials: creds);

return new JwtSecurityTokenHandler().WriteToken(token);
}
public string GenerateToken(AppUser user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.UserName)
};

var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSecret"]!));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
var token = new JwtSecurityToken(claims: claims, expires: DateTime.Now.AddDays(7), signingCredentials: creds);

return new JwtSecurityTokenHandler().WriteToken(token);
}
And you'd return this JWT for the user to use in the headers of each request. With an Authorization: Bearer <token> This is just to show you how you'd add JWT to your project If you want to learn more about it and how it works and what you can do with it I'd suggest reading about it somewhere or in the link I showed u and searching for aspnetcore specific stuff to know the implementation details As for why this gets triggered on every request I have no fucking clue You sure it triggers even on controllers that don't have the Authorize attribute?
The Fog from Human Resources
yes wait
The Fog from Human Resources
1. No error on the endpoint that should have it, 2. Tested on an endpoint that doesnt require authentication and it still challenges on first image you also see that the API just returns nothing instead of an error
snowy | switching accounts
Yeah I just tested your code I have no clue as for why it triggers for every request
The Fog from Human Resources
does it trigger for every request for you too?
snowy | switching accounts
maybe it expects you to write the logic to decide when to trigger or not I'd honestly just use the jwt solution
snowy | switching accounts
Stack Overflow
Custom AuthenticationHandler is called when a method has [AllowAnon...
I am trying to have my own custom authentication for my server. But it is called for every endpoint even if it has the [AllowAnonymous] attribute on the method. With my current code, I can hit my
snowy | switching accounts
weird behavior but designed to work like that
The Fog from Human Resources
so what do i do abt it its stupid that im being forced into using this even tho my old one did just that and wouldve worked [HttpGet("generate")] [Authorize(AuthenticationSchemes = "serverPass")] public IActionResult generateAccount() this is how i do the stuff that should be correct tho right?
snowy | switching accounts
You can validate whether the Authorize attribute is present
snowy | switching accounts
1sec im in a meeting
The Fog from Human Resources
alright SCbabymilk2
snowy | switching accounts
So, basically, you could modify your handler to look something like this:
Florian Voß
Florian Voß17mo ago
@fabiogaming do you call RequireAuthorization() anywhere in your program.cs? that would apply your auth on all endpoints
The Fog from Human Resources
I don't think so All the code related to it is the one I sent here
Florian Voß
Florian Voß17mo ago
can you share all of the middleware pls? we only see Building, up to Running pls
The Fog from Human Resources
Sure this is the only real middleware i use
public class RequestCounter
{
private readonly RequestDelegate _next;
public RequestCounter(RequestDelegate next)
{
_next = next;
}

public async Task Invoke(HttpContext context)
{
Utils.TotalRequests++;
await _next(context);
}
}
public class RequestCounter
{
private readonly RequestDelegate _next;
public RequestCounter(RequestDelegate next)
{
_next = next;
}

public async Task Invoke(HttpContext context)
{
Utils.TotalRequests++;
await _next(context);
}
}
snowy | switching accounts
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Context.GetEndpoint()?.Metadata.GetMetadata<AuthorizeAttribute>() is null)
{
return Task.FromResult(AuthenticateResult.NoResult());
}

if (!Request.Headers.ContainsKey("server-key"))
{
return Task.FromResult(AuthenticateResult.Fail(JsonConvert.SerializeObject(TemplateResponses.NoServerKey())));
}

string serverKey = Request.Headers["server-key"];

if (!Utils.Accounts.IsUserWhiteListed(serverKey))
{
return Task.FromResult(AuthenticateResult.Fail(JsonConvert.SerializeObject(TemplateResponses.InvalidServerKey(serverKey))));

}


var claims = new[] { new Claim(ClaimTypes.Name, serverKey) };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Context.GetEndpoint()?.Metadata.GetMetadata<AuthorizeAttribute>() is null)
{
return Task.FromResult(AuthenticateResult.NoResult());
}

if (!Request.Headers.ContainsKey("server-key"))
{
return Task.FromResult(AuthenticateResult.Fail(JsonConvert.SerializeObject(TemplateResponses.NoServerKey())));
}

string serverKey = Request.Headers["server-key"];

if (!Utils.Accounts.IsUserWhiteListed(serverKey))
{
return Task.FromResult(AuthenticateResult.Fail(JsonConvert.SerializeObject(TemplateResponses.InvalidServerKey(serverKey))));

}


var claims = new[] { new Claim(ClaimTypes.Name, serverKey) };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
Florian Voß
Florian Voß17mo ago
I meant all of your program.cs when I saaid middleware
snowy | switching accounts
I've been through that already
The Fog from Human Resources
ye sure
snowy | switching accounts
Read this
The Fog from Human Resources
good idea ill try it
snowy | switching accounts
AuthenticationHandler<T> is designed to work that way It'll intercept any request no matter whether or not it has the Authorize attribute I suppose it works that way to allow people to completely customize the authentication and authorization experience @fabiogaming But honestly Why not jwt my man
Florian Voß
Florian Voß17mo ago
I see, sorry for not following along
The Fog from Human Resources
i dunno im not a fan of using third parties unless its really needed and if theres no other way around i always write everything from scratch
snowy | switching accounts
You gotta know when to recurr to thir parties or not There isn't really a rule and it isn't bad to use third party libraries Even tho the JWT one is from aspnetcore itself
The Fog from Human Resources
to me this is abt full control
Florian Voß
Florian Voß17mo ago
do it like me, call AddMicrsoftIdentityWebApiAuthentication() and call it a day 😄
snowy | switching accounts
You know that by creating an aspnetcore webapi you're already using third party libraries right?
The Fog from Human Resources
yes but since its a dotnet / visual studio preset i dont rly count them
snowy | switching accounts
well the jwt package I linked is an official one aswell but meh
The Fog from Human Resources
this seems to work now the last issue is, that i dont receive those expected errors in the request console
snowy | switching accounts
You mean the AuthenticationResult.Fail?
The Fog from Human Resources
this for example is still null
The Fog from Human Resources
ye i tried a bodywriter but that went horribly wrong as it just stacked it onto everything as was being received as a stream or smth and not a string
snowy | switching accounts
I'm not following You're complaining that you ain't getting anything from the curl request?
The Fog from Human Resources
Normally it should tell me that I'm missing the server key Yes
snowy | switching accounts
do a curl -I link
snowy | switching accounts
or a curl -i idk which one You're getting a 401
The Fog from Human Resources
yes but no body
The Fog from Human Resources
its supposed to return the Failure Message in the actual response body
snowy | switching accounts
It's not supposed to, that's what you want, but I get your point lemme see
The Fog from Human Resources
everything else now seems to work now
snowy | switching accounts
Try the HandleChallengeAsync or the HandleForbiddenAsync override those methods and use the WriteResponseAsync
The Fog from Human Resources
how would i do that i think i found it System.InvalidOperationException: "StatusCode cannot be set because the response has already started."
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{

if (!Request.Headers.ContainsKey("server-key"))
{
Response.StatusCode = (int)HttpStatusCode.Forbidden;
Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(TemplateResponses.NoServerKey())));

}

string serverKey = Request.Headers["server-key"];

if (!Utils.Accounts.IsUserWhiteListed(serverKey))
{
Response.StatusCode = (int)HttpStatusCode.Unauthorized;
Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(TemplateResponses.InvalidServerKey(serverKey))));

}
return base.HandleChallengeAsync(properties);
}
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{

if (!Request.Headers.ContainsKey("server-key"))
{
Response.StatusCode = (int)HttpStatusCode.Forbidden;
Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(TemplateResponses.NoServerKey())));

}

string serverKey = Request.Headers["server-key"];

if (!Utils.Accounts.IsUserWhiteListed(serverKey))
{
Response.StatusCode = (int)HttpStatusCode.Unauthorized;
Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(TemplateResponses.InvalidServerKey(serverKey))));

}
return base.HandleChallengeAsync(properties);
}
snowy | switching accounts
another meeting
snowy | switching accounts
so Don't set the StatusCode use the HandleChallengeAsync and the HandleForbiddenAsync To write to the response body they will handle the status code iirc
The Fog from Human Resources
i removed the status code setting
The Fog from Human Resources
(btw side question, is there a way to get all endpoints and their type? I want to make a small automatically generated documentation on the root URL)
snowy | switching accounts
you know that swagger does that for you right Don't call the base. anymore
The Fog from Human Resources
ye i just did that it now works
snowy | switching accounts
check the status code just to make sure its correct
The Fog from Human Resources
ye its 200 but im changing it rn
The Fog from Human Resources
it now all works
snowy | switching accounts
. and congrats
The Fog from Human Resources
yee i remember anyways, thanks for your help
Accord
Accord17mo ago
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.
Want results from more Discord servers?
Add your server