Authentication

Live on this server

Secures an API with API keys, roles and data per user.

A notes API where every user sees their own notes, and administrators see the notes of everyone. Clients send an API key in the X-API-Key header, and AuthenticateAsync turns the key into a user - or rejects it. This is where you would look the key up in your database.

The user injector passes the user to the service methods as a parameter, and [RequireRole("ADMIN")] restricts a method to administrators. Basic, bearer (JWT) and client certificate authentication work the same way. Try it with the keys alice-key (a user) and bob-key (an administrator).

To run it, create a console app (dotnet new console), add GenHTTP (dotnet add package GenHTTP.Full), replace Program.cs with the code below and start it with dotnet run - it answers at http://localhost:8080/.

Program.cs86 lines
using GenHTTP.Engine.Internal;

using GenHTTP.Api.Content.Authentication;
using GenHTTP.Api.Protocol;

using GenHTTP.Modules.Authentication;
using GenHTTP.Modules.Layouting;
using GenHTTP.Modules.Practices;
using GenHTTP.Modules.Reflection;
using GenHTTP.Modules.Reflection.Injectors;
using GenHTTP.Modules.Webservices;

var auth = ApiKeyAuthentication.Create()
                               .Authenticator(AuthenticateAsync);

// lets the service methods declare the user as a parameter
var injection = Injection.Default()
                         .Add(new UserInjector<NoteUser>());

var app = Layout.Create()
                .AddService<NoteService>("notes", injectors: injection)
                .Authentication(auth);

await Host.Create()
          .Handler(app)
          .Defaults()
          .RunAsync();

// this is where you would look up the key in your database
static ValueTask<IUser?> AuthenticateAsync(IRequest request, string key)
{
    IUser? user = key switch
    {
        "alice-key" => new NoteUser("alice", "USER"),
        "bob-key" => new NoteUser("bob", "USER", "ADMIN"),
        _ => null // unknown key, access denied
    };

    return new(user);
}

public record NoteUser(string Name, params string[] Roles) : IUser
{

    public string DisplayName => Name;

}

public record Note(string Owner, string Text);

public class NoteService
{
    private static readonly List<Note> Notes = [];

    // GET /notes/
    [ResourceMethod]
    public List<Note> Mine(NoteUser user)
    {
        lock (Notes) return Notes.Where(n => n.Owner == user.Name).ToList();
    }

    // PUT /notes/ with the text as body
    [ResourceMethod(Method.Put)]
    public Note Add(NoteUser user, [FromBody] string text)
    {
        var note = new Note(user.Name, text);

        lock (Notes)
        {
            if (Notes.Count >= 50) Notes.RemoveAt(0); // keeps the latest notes only

            Notes.Add(note);
        }

        return note;
    }

    // GET /notes/all, for administrators only
    [ResourceMethod("all")]
    [RequireRole("ADMIN")]
    public List<Note> All()
    {
        lock (Notes) return [..Notes];
    }

}

Try it

Send the requests to the sample running on this server - change the path, headers or body as you like, or copy them as curl commands.

  1. Add a note as Alice

    PUT
    Headers and body
    curl -i -X PUT -H 'X-API-Key: alice-key' -H 'Content-Type: text/plain' -d 'Buy milk' 'https://genhttp.org/demo/authentication/notes/'
  2. List the notes of Alice

    GET
    Headers
    curl -i -H 'X-API-Key: alice-key' 'https://genhttp.org/demo/authentication/notes/'
  3. List all notes as Alice

    403, Alice is no administrator

    GET
    Headers
    curl -i -H 'X-API-Key: alice-key' 'https://genhttp.org/demo/authentication/notes/all'
  4. List all notes as Bob

    GET
    Headers
    curl -i -H 'X-API-Key: bob-key' 'https://genhttp.org/demo/authentication/notes/all'
  5. Call without a key

    401

    GET
    curl -i 'https://genhttp.org/demo/authentication/notes/'