How to add authentication in a minimal API?
Asked Answered
T

1

5

So I have an 'Echo' service that is just created so I can get a deeper understanding of Web Applications and Web APIs. It had a bunch of routes and in every situation it will just return a JSON result with information about the request and response.
So I have a method "header" which will add a header to the response. And a method "cookie" that will send back a cookie. And a few other methods with experimental routes.
And now I want to focus on authentication by using the authentication stuff and JWTs so I can go deeper into understanding what these things do. Because just adding AddAuthentication/AddJwtBearer by copy/pasting things from other projects is not what I want. I also don't want some database backend, but I might want to go for OAuth authentication through Google, Facebook and Twitter as a next step. But for now, I have a variable accounts that holds some valid login accounts and now I need to use this to make a login and make use of this authentication framework.
No database, no Azure, no complex stuff. Just plain, minimal API code. That's because this project is meant to understand the technique and experiment with it and I don't want to focus on anything else. Only the authentication. (Like I also used it to focus on how routing works, exactly.)
So what I want are a few steps:

  1. Learn how to add authentication in a minimal project.
  2. Make one route that requires authentication.
  3. Add authorization to this project.
  4. Make one route that requires authorization.
  5. Add Identity to this project.
  6. Make one route that uses Identity.

So, to begin this all, how do I do step 1?

var builder = WebApplication.CreateBuilder(args);

Dictionary<string, string> accounts = new Dictionary<string, string>() { { "wim", "123456" }, { "test", "abc123" } };

builder.Services.AddAuthentication()
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Unauthorized/";
        options.AccessDeniedPath = "/Account/Forbidden/";
    })
    .AddJwtBearer(options =>
    {
        options.Audience = "Everyone";
        options.Authority = "Wim";
    });

var app = builder.Build();

app
    .UseHsts()
    .UseAuthentication()
    .MapWhen(ContainsPhp, HandlePhp());

app.MapGet("/", (HttpContext context) => Echo(context));

app.MapGet("/cookie/{name}/{*values}", (HttpContext context, string name, string values) =>
{
    foreach (var value in values.Split("/"))
    {
        context.Response.Cookies.Append($"{name}.{value}", value);
    }
    return Echo(context);
});

app.MapGet("/header/{name}/{*values}", (HttpContext context, string name, string values) =>
{
    context.Response.Headers[name] = values.Split("/").ToArray();
    return Echo(context);
});

app.MapGet("{name}.html", (HttpContext context) => Echo(context));
app.MapGet("/{one}/{two}/{three}.{four}", (HttpContext context, string one, string two, string three, string four, [FromQuery] string five, [FromQuery] string six) =>
{
    context.Response.Headers["one"] = one;
    context.Response.Headers["two"] = two;
    context.Response.Headers["three"] = three;
    context.Response.Headers["four"] = four;
    context.Response.Headers["five"] = five;
    context.Response.Headers["six"] = six;
    return Echo(context);
});

app.MapGet("/{*rest}", (HttpContext context) => Echo(context));
app.MapGet("/echo/", (HttpContext context) => Echo(context));
app.MapGet("/echo/{*rest}", (HttpContext context) => Echo(context));
app.MapGet("{path}.html", (HttpContext context) => Echo(context));

app.Run();

// ----------------------------------------------------------------

bool ContainsPhp(HttpContext context) => context.Request.Path.Value?.ToLower().Contains(".php") ?? false;
Action<IApplicationBuilder> HandlePhp() => applicationBuilder =>
    applicationBuilder.Run((context) => Task.Run(() => context.Response.Redirect("https://www.php.net/")));

IResult Echo(HttpContext httpContext)
{
    return Results.Json(new
    {
        Request = new
        {
            host = httpContext.Request.Host,
            method = httpContext.Request.Method,
            path = httpContext.Request.Path,
            pathBase = httpContext.Request.PathBase,
            route = httpContext.Request.RouteValues,
            scheme = httpContext.Request.Scheme,
            Query = new
            {
                query = httpContext.Request.Query,
                queryString = httpContext.Request.QueryString,
            },
        },
        response = new
        {
            statusCode = httpContext.Response.StatusCode,
            cookies = httpContext.Response.Cookies,
            contentType = httpContext.Response.ContentType,
        },
        headers = new
        {
            response = httpContext.Response.Headers,
            request = httpContext.Request.Headers,
        },
        Connection = new
        {
            protocol = httpContext.Request.Protocol,
            localIpAddress = httpContext.Connection.LocalIpAddress?.ToString(),
            localPort = httpContext.Connection.LocalPort,
            remoteIpAddress = httpContext.Connection.RemoteIpAddress?.ToString(),
            remotePort = httpContext.Connection.RemotePort,
        },
        user = httpContext.User,
        items = httpContext.Items,
    }, new(JsonSerializerDefaults.Web) { WriteIndented = true });
}

I expect that the response JSON will show something in the User/Identity path. Right?


I got the authentication and authorization to work, sort of, and the result is in this question. But I expect there might be a better option for this, keeping it all to a minimum.

Teri answered 24/11, 2022 at 18:3 Comment(2)
Just to be clear, there are a lot of sites explaining al this, but they tend to include the use of Controllers, the Entity Framework, OAuth and/or Azure plus a lot of other distractions that I don't need.Teri
I’m voting to close this question because there's no good, single answer for it.Teri
L
13

Minimal APIs support authorization either via AuthotizeAttribute placed on handler:

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");

Or via RequireAuthorization method invocation on endpoint builder:

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

Read more .

To handle authentication you can create custom login endpoint and mark it as anonymous:

app.MapGet("/login", [AllowAnonymous] () => // ... login user ); // or call AllowAnonymous()
Layby answered 24/11, 2022 at 19:58 Comment(2)
Yes, I've seen that page before and it does some tricks with the Entity Framework. The problem is that I can add RequireAuthorization() to this API, but how does it get the authorization information? Where does it get stored and later retrieved? The JWT part should generate a token that is passed to the client and with the next call it receives it again and tells me the user is authenticated. How does "/auth" know the user is authorized?Teri
Another problem is that this code does authorization while my focus is authentication.Teri

© 2022 - 2024 — McMap. All rights reserved.