Authentication

Live on this server

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

AuthenticationSample.cs102 lines
using GenHTTP.Api.Content;
using GenHTTP.Api.Content.Authentication;
using GenHTTP.Api.Protocol;

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

public static class AuthenticationSample
{

    public static IHandlerBuilder Create()
    {
        /*
         *
         * A notes API secured with API keys. Every user only sees their own notes,
         * while administrators may see the notes of all users.
         *
         * Clients send their key via the "X-API-Key" header: "alice-key" for a user,
         * "bob-key" for an administrator.
         *
         * Basic, bearer (JWT) and client certificate authentication work the same way.
         *
         * See https://genhttp.org/documentation/content/concerns/authentication/
         *
         */

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

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

        return Layout.Create()
                     .AddService<NoteService>("notes", injectors: injection)
                     .Authentication(auth);
    }

    private static ValueTask<IUser?> AuthenticateAsync(IRequest request, string key)
    {
        // this is where you would look up the key in your database
        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 /samples/authentication/notes/
        [ResourceMethod]
        public List<Note> Mine(NoteUser user)
        {
            lock (Notes) return Notes.Where(n => n.Owner == user.Name).ToList();
        }

        // PUT /samples/authentication/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)
            {
                // keeps the latest notes only
                if (Notes.Count >= 50) Notes.RemoveAt(0);

                Notes.Add(note);
            }

            return note;
        }

        // GET /samples/authentication/notes/all
        [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/samples/authentication/notes/'
  2. List the notes of Alice

    GET
    Headers
    curl -i -H 'X-API-Key: alice-key' 'https://genhttp.org/samples/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/samples/authentication/notes/all'
  4. List all notes as Bob

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

    401

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