fabric

Enabling Authentication

Enabling authentication and authorization in a Microbus application requires an initial setup. Thereafter though, restricting endpoints to only authorized actors is typically a one-liner declaration in service.yaml.

Step 1: Actor

An actor represents the user context of a request. Start by creating an Actor struct that defines the actor in your application.

package act

import "github.com/microbus-io/fabric/frame"

type Actor struct {
    // Identifiers
    Issuer   string   `json:"iss"`
    Subject  string   `json:"sub"`
    TenantID int      `json:"tenantID"`
    UserID   int      `json:"userID"`
    // Security claims
    Groups   []string `json:"groups"`
    Roles    []string `json:"roles"`
    Scopes   []string `json:"scopes"`
    // User preferences
    TimeZone string   `json:"timezone"`
    Locale   string   `json:"locale"`
    Name     string   `json:"name"`
}

func Of(r any) *Actor {
    var a Actor
    frame.Of(r).ParseActor(&a)
    return &a
}

The Actor should contain identifiers that uniquely identify the actor in your application, such as user ID, tenant ID, etc. Identifiers are most likely constant for the life of the actor.

The Actor should also include the security claims used as basis for the authorization requirements, such as roles or group associations. Security claims are typically assigned to an actor by an administrator and can change over the life of the actor.

The Actor may also include preferences that are under the control of the actor, such as time zone, locale, name or email.

Step 2: Token Issuer and Validator

To be authorized, requests must include a JWT token. On the backend, the JWT is validated and converted to an actor that is propagated downstream along the call stack. The token issuer core microservice is capable of issuing and validating JWTs. If the core microservice is insufficient to your needs, you may implement your own token issuer that provides a similar interface.

general:
  host: customissuer.myapp

functions:
- signature: IssueToken(claims any) (signedToken string)
  description: IssueToken generates a new JWT with a set of claims.
  path: :444/...
- signature: ValidateToken(signedToken string) (actor any, valid bool)
  description: ValidateToken validates a JWT previously generated by this issuer and returns the actor associated with it.
  path: :444/...

IssueToken generates a JWT given a set of claims. The token issuer core microservice puts all claims in the JWT and returns its signed representation. A custom implementation that does not want to expose the claims to the user may store the claims in a database and include in the token only an identifier that can later be used to retrieve them. A token issuer should set the validator claim of the JWT with its hostname as means to inform the authorization middleware who to call to validate the token.

{
	"iss": "microbus",
	"validator": "customissuer.myapp",
	//...
}

ValidateToken checks that the JWT has been issued by this issuer and returns the actor associated with it. The token issuer core microservice produces the actor’s properties from the claims of the JWT. A custom implementation may go to a database to obtain or augment the actor’s properties given an identifier found in the JWT. This pattern allows tokens to be updated with dynamic data, such as security claims and user preferences.

Port :444 is used in order to keep these endpoints internal.

Remember to add the custom issuer to the main app.

app.Add(
    customissuer.NewService(),
)

Step 3: Middleware

The authorization middleware looks for a JWT in the Authorization: Bearer header or in a cookie named Authorization. It contacts the token issuer microservice named in the validator claim to validate the token and obtain the actor associated with it. The actor is then propagated downstream to the target microservice and the rest of the call stack thereafter.

If your custom token issuer does not set the validator claim or if you want to look for the token in different request headers, you may change the middleware accordingly when initializing the HTTP ingress proxy.

Step 4: Authorization Requirements

Enter the authorization requirements for each of your restricted endpoints in the service.yaml of their corresponding microservices. Requirements are stipulated as a boolean expression over the properties of the actor.

functions:
  - signature: SalesReport(from time.Time, to time.Time) (sales SalesData)
    actor: group.sales && (roles.director || roles.manager)

If an endpoint’s output is conditional upon the actor, the endpoint may obtain the actor from the context via the frame and adjust accordingly.

func (svc *Service) SalesReport(ctx context.Context, from time.Time, to time.Time) (sales *SalesData, err error) {
	actor := act.Of(ctx)
	if actor.IsDirector() {
		// ...
	} else {
		// ...
	}
	return sales, nil
}

Step 5: Authenticator

Create a microservice that authenticates a user given their credentials and returns a JWT back to them. The login example microservice is an example of an authenticator that accepts a username and a password via a web form and returns a JWT via a Set-Cookie header. Single-page applications may be better served by a functional endpoint such as the following.

functions:
  - signature: Authenticate(username string, password string) (token string)