Authentication

status: working in progress

What is Authentication?

Authentication is the process to identify who submitted the request. In httpc terms, to associate a user to the request context. A request is authenticated when it has a user associated. Otherwise, it’s an anonymous request.


The user is an IUser object. httpc provides no predefined attribute for the user. For type safety, you can define the user properties by expanding the IUser interface


A user can authenticate itself in several ways. It’s up to you to specify which and how many authentication types you want to support.


When building an API, the authentication process is involved in three areas:

1. Setup

For the first step, you need to specify which authentication types the server will support. Each authentication has configuration options to control its behavior.


For an httpc server, an authentication type is added through a middleware at application level. Meaning, it has a global scope and works for all incoming requests.

import { Application, AuthenticationBearerMiddleware, AuthenticationApiKeyMiddleware } from "@httpc/kit";

const app = new Application({
    middlewares: [
        AuthenticationBearerMiddleware(),
        AuthenticationApiKeyMiddleware(),
    ]
});

You can support multiple authentications. The server will execute them in order.


There’re several builtin middlewares covering the most common authentication types. You can also create a custom middleware to support your use case.

2. Guard

Once the authentication is set, you want to protect areas of your application where only an authenticated user can have access to.


You can protect the whole application, one or more sub groups or go down to specific functions. Just apply the Authenticated middleware to the section you want to guard.

Scope
import { httpCall, Authenticated } from "@httpc/kit";

const getMyProfile = httpCall(
    Authenticated(),  // <-- guard the function: request must be authenticated
    async () => {
        // do something
    }
);

You can be more selective and guard some execution paths with a condition inside functions. The useIsAuthenticated hook returns true/false if the request is authenticated.

import { httpCall, useIsAuthenticated, useUser } from "@httpc/kit";

const getDiscounts = httpCall(
    // no guard! anonymous request can access
    async () => {
        if (!useIsAuthenticated()) {
            // the user is not authenticated, return the signup 5% discount
            return [{ name: "signup-offer", discount: 5 }];
        } else {
            // the user is authenticated, returns its own active discounts
            const user = useUser();
            return await db.getUserDiscounts(user.id);
        }
    }
);

3. Access

Once a request passes the authentication guard, you need to know which user is sending the request. You can use the useUser hook to get the user associated to the request.

import { httpCall, Authenticated, useUser } from "@httpc/kit";

const getMyOrders = httpCall(
    Authenticated(),
    async () => {
        const user = useUser();
        return await db.getOrdersByUser(user.id);
    }
);

The useUser throws an UnauthorizedError when the request is not authenticated;

Builtin authentications

@httpc/kit offers builtin support for:

For each authentication, the framework provides a core middleware and a predefined service. You need to assign the middleware to the application to activate the relative authentication.

import { Application, AuthenticationBearerMiddleware } from "@httpc/kit";

const app = new Application({
    middlewares: [
        AuthenticationBearerMiddleware()
    ]
});

Often you don’t need to deal with the authentication service as it works under the hood covering most common use cases.


You can add any number of authentication to your application. The server executes middlewares in ordered sequence. If an authentication middleware doesn’t recognize the request, it will pass the request to the next one. The same happens when the authentication is already performed: if a request has already a user associated, an authenticated middleware skips its execution to the next step in the pipeline.


Usually you should register an authentication middleware at application level, that is, with a global scope. But you can also restrict an authentication to a sub group to limit its perimeter.

Bearer authentication (JWT)

Allow the server to use a JWT to perform authentication. JWTs are also called Bearer tokens from the authorization schema they employ.


The Bearer authentication requires a secret to decrypt tokens. Usually you can load it from an environment variable although other way are possible.


The main component is AuthenticationBearerMiddleware.

Basic Usage

Leverage the default behavior. Just add the middleware to the application and configure the secret.

import { Application, AuthenticationBearerMiddleware } from "@httpc/kit";

const app = new Application({
    middlewares: [
        AuthenticationBearerMiddleware({
            jwtSecret: process.env.JWT_SECRET, // load the secret from env
        })
    ]
});

By default, the server will:

  1. Look for a JWT in the request. If not found, the authentication is skipped
  2. Decrypt the JWT and validate it:
    • the encryption algorithm must be HS256
    • check expiration, if exp attribute is present
  3. If the JWT is malformed or the decryption or validation fails, throw an UnauthorizedError
  4. From the decrypted payload, construct a user object from:
    • the sub attribute, become the user.id
    • discard any standard JWT attributes like exp, aud, iss, …
    • the remaining attributes, become user properties
  5. If the sub attribute is missing or empty, throw an UnauthorizedError
  6. Associate the user to the request

Property customization

If you need to customize how the user attributes are extracted from the JWT payload, use the onDecode callback.

import { Application, AuthenticationBearerMiddleware, JwtPayload } from "@httpc/kit";

const app = new Application({
    middlewares: [
        AuthenticationBearerMiddleware({
            jwtSecret: process.env.JWT_SECRET, // load the secret from env
            onDecode: (payload: JwtPayload): IUser => {
                return { }; // return the user
            }
        })
    ]
});

The object returned from onDecode will be the user associated to the request. It’s your responsibility to throw an error if something is wrong, i.e. some attribute is missing or invalid.

Full customization

If you need full control on how the JWT is handled, you can use onAuthenticate callback. The server will only extract the token for you.

import { Application, AuthenticationBearerMiddleware } from "@httpc/kit";

const app = new Application({
    middlewares: [
        AuthenticationBearerMiddleware({
            onAuthenticate: (token: string): IUser => {
                // any custom logic to
                // 1. decrypt
                // 2. validate
                // 3. extract the user

                return { }; // return the user
            }
        })
    ]
});

The object returned from onAuthenticate will be the user associated to the request. It’s your responsibility to decrypt, validate and extract the user and, if something fails, to throw an error.


The framework provides a JwtService to help dealing with JWTs. The JwtService offers common operation like decode and validate with advanced options to meet advanced needs.

Custom service

For advanced scenarios, you can define a custom service to perform the authentication. A dedicated service is useful when you need other services and want to rely on dependency injection to get them. Or you want to scope the logic inside a class.


When you use a custom service, the AuthenticationBearerMiddleware can be used with no options as all the logic is deferred to the service.

import { Application, AuthenticationBearerMiddleware } from "@httpc/kit";

const app = new Application({
    middlewares: [
        AuthenticationBearerMiddleware()
    ]
});

You can define a custom bearer service:

Key components

Examples

import { Application, AuthenticationBearerMiddleware } from "@httpc/kit";

const app = new Application({
    middlewares: [
        AuthenticationBearerMiddleware({
            jwtSecret: process.env.JWT_SECRET
        })
    ]
});

Basic authentication

// TODO

ApiKey authentication

// TODO

Hooks

useUser

// TODO


The useUser hooks works like a hard guard.

useIsAuthenticated

// TODO

useAuthentication

// TODO

Interfaces

IUser

// TODO

IAuthenticationService<T>

// TODO

Custom authentication

// TODO