C
C#9mo ago
reinaldyrfl

✅ How do you create custom authz/autho in ASP.NET Core?

As far as I know, on the internet there's only an implementation for JWT authentication. Yet, I need totally custom implementation that's not related to JWT at all. I have an additional case that every static file visit should also be authenticated (the specification said so). According to my implementation, anonymous access should be allowed. But for unknown reasons, I got empty content with 401 status code for any page and any method. I think I'm missing something, yet I don't know what it is.
8 Replies
reinaldyrfl
reinaldyrfl9mo ago
Currently my code is: Program.cs
builder.Services.AddAuthentication("Session")
.AddScheme<SessionAuthenticationSchemeOptions, SessionAuthenticationHandler>("Session", options => { });
builder.Services.AddAuthorization(options => {
options.AddPolicy("Session",
policyBuilder => policyBuilder.AddAuthenticationSchemes("Session").RequireAuthenticatedUser().Build());
});

/// ...
app.UseSessionAuthentication(new SessionAuthenticationOptions {
PolicyName = "Session"
});
app.UseAuthentication();
app.UseAuthorization();

/// ...
/// This is the static files route that should also be authenticated
app.UseStaticFiles(new StaticFileOptions {
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), @"Frontend", @"dist")),
RequestPath = new PathString("/")
});
builder.Services.AddAuthentication("Session")
.AddScheme<SessionAuthenticationSchemeOptions, SessionAuthenticationHandler>("Session", options => { });
builder.Services.AddAuthorization(options => {
options.AddPolicy("Session",
policyBuilder => policyBuilder.AddAuthenticationSchemes("Session").RequireAuthenticatedUser().Build());
});

/// ...
app.UseSessionAuthentication(new SessionAuthenticationOptions {
PolicyName = "Session"
});
app.UseAuthentication();
app.UseAuthorization();

/// ...
/// This is the static files route that should also be authenticated
app.UseStaticFiles(new StaticFileOptions {
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), @"Frontend", @"dist")),
RequestPath = new PathString("/")
});
SessionAuthenticationHandler.cs
public class SessionAuthenticationHandler : AuthenticationHandler<SessionAuthenticationSchemeOptions> {
private readonly ILoginClient _loginClient;
private readonly IMemoryCache _memoryCache;

public SessionAuthenticationHandler(IOptionsMonitor<SessionAuthenticationSchemeOptions> optionsMonitor,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
ILoginClient loginServerClient,
IMemoryCache memoryCache) : base(optionsMonitor,
logger,
encoder,
clock) {
_loginClient = loginServerClient;
_memoryCache = memoryCache;
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (Request.Query.TryGetValue("sessionId", out StringValues sessionId)) {
// I'm hiding implementation details because of Discord's message limit

string accessToken = ConvertSessionIdToAccessToken(sessionId.ToString());
return BuildTicket(accessToken);
}

// If query string is empty, we shall check for cookies
if (Request.Cookies.TryGetValue("ApplicationSession", out string? postChartAccessToken)) {
// TODO: Authenticate

return BuildTicket("");
}

// HERE! It should be authenticated even if it's from anonymous user.
return BuildTicket("");
}

private AuthenticateResult BuildTicket(string accessToken) {
var claims = new List<Claim> {
new(ClaimTypes.Name, "Session"),
new(ClaimTypes.Expiration, "3600"),
new(ClaimTypes.Anonymous, string.IsNullOrEmpty(accessToken) ? "true" : "false")
};

var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new GenericPrincipal(identity, null);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
public class SessionAuthenticationHandler : AuthenticationHandler<SessionAuthenticationSchemeOptions> {
private readonly ILoginClient _loginClient;
private readonly IMemoryCache _memoryCache;

public SessionAuthenticationHandler(IOptionsMonitor<SessionAuthenticationSchemeOptions> optionsMonitor,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
ILoginClient loginServerClient,
IMemoryCache memoryCache) : base(optionsMonitor,
logger,
encoder,
clock) {
_loginClient = loginServerClient;
_memoryCache = memoryCache;
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (Request.Query.TryGetValue("sessionId", out StringValues sessionId)) {
// I'm hiding implementation details because of Discord's message limit

string accessToken = ConvertSessionIdToAccessToken(sessionId.ToString());
return BuildTicket(accessToken);
}

// If query string is empty, we shall check for cookies
if (Request.Cookies.TryGetValue("ApplicationSession", out string? postChartAccessToken)) {
// TODO: Authenticate

return BuildTicket("");
}

// HERE! It should be authenticated even if it's from anonymous user.
return BuildTicket("");
}

private AuthenticateResult BuildTicket(string accessToken) {
var claims = new List<Claim> {
new(ClaimTypes.Name, "Session"),
new(ClaimTypes.Expiration, "3600"),
new(ClaimTypes.Anonymous, string.IsNullOrEmpty(accessToken) ? "true" : "false")
};

var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new GenericPrincipal(identity, null);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
SessionAuthenticationMiddleware.cs
public static class SessionAuthenticationExtension {
public static IApplicationBuilder UseSessionAuthentication(this IApplicationBuilder applicationBuilder,
SessionAuthenticationOptions options) {
return applicationBuilder.UseMiddleware<SessionAuthentication>(options);
}
}
public static class SessionAuthenticationExtension {
public static IApplicationBuilder UseSessionAuthentication(this IApplicationBuilder applicationBuilder,
SessionAuthenticationOptions options) {
return applicationBuilder.UseMiddleware<SessionAuthentication>(options);
}
}
SessionAuthentication.cs
public class SessionAuthentication {
private readonly RequestDelegate _next;
private readonly string _policyName;

public SessionAuthentication(RequestDelegate next, SessionAuthenticationOptions options) {
_next = next;
_policyName = options.PolicyName;
}

public async Task Invoke(HttpContext httpContext, IAuthorizationService authorizationService) {
AuthorizationResult authorized = await authorizationService.AuthorizeAsync(
httpContext.User, null, _policyName);

if (!authorized.Succeeded) {
await httpContext.ChallengeAsync();
return;
}

await _next(httpContext);
}
}
public class SessionAuthentication {
private readonly RequestDelegate _next;
private readonly string _policyName;

public SessionAuthentication(RequestDelegate next, SessionAuthenticationOptions options) {
_next = next;
_policyName = options.PolicyName;
}

public async Task Invoke(HttpContext httpContext, IAuthorizationService authorizationService) {
AuthorizationResult authorized = await authorizationService.AuthorizeAsync(
httpContext.User, null, _policyName);

if (!authorized.Succeeded) {
await httpContext.ChallengeAsync();
return;
}

await _next(httpContext);
}
}
JakenVeina
JakenVeina9mo ago
you're on the right track with implementing your own AuthenticationHandler class what's different about this approach than the ready-made Session auth scheme? I.E. Cookies you definitely do not need to be implementing your own middleware
Accord
Accord9mo 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.
reinaldyrfl
reinaldyrfl9mo ago
I don't understand why I didn't need to implement my own middleware. Since for my current case, anonymous access should be possible for now. Or.. should ai rename my scheme to anything other than "Session"?
JakenVeina
JakenVeina9mo ago
what does creating custom middleware have to do with allowing anonymous access? why do you think that anonymous access is only possible if you create custom middleware?
reinaldyrfl
reinaldyrfl9mo ago
I got where you confused at. I'm creating allow anonymous access inside the middleware only for development purposes. Yes I know that I can just toggle app.Configuration.Environment condition, but this is something I'd like to try (if this is possible) So assuming I don't need to change my scheme name to anything else and just do authenticated access all the time, and you've said I'm on the right track.. then what other things that I need to know moving on?
JakenVeina
JakenVeina9mo ago
if that's what you want to go with, you should A) name your middleware more appropriately, E.G. DevelopmentAuthorizationMiddleware, and then make it actually do what you describe, cause right now doesn't allow anonymous access for anything B) inject it like this:
if (environment is Environment.Development)
app.UseMiddleware<DevelopmentAuthorizationMiddleware>()
else
app.UseAuthorization();
if (environment is Environment.Development)
app.UseMiddleware<DevelopmentAuthorizationMiddleware>()
else
app.UseAuthorization();
more likely, you can find a way to achieve this through just policies, or just.... don't do this at all if you just want to avoid having to sign in all the time in DVLP, there's ways to do that through config and tooling
reinaldyrfl
reinaldyrfl9mo ago
Thank you so much!