Athenz integration

This document explains how to integrate Armeria with Athenz, a platform for service authentication and authorization. It assumes that you have already configured an Athenz domain and installed the necessary service identity certificates locally.

Prerequisites

Add the following dependencies to your build.gradle file:

build.gradle
dependencies {
    implementation platform('com.linecorp.armeria:armeria-bom:1.33.0')

    ...
    implementation 'com.linecorp.armeria:armeria-athenz'
}

Creating a ZTS client

The ZtsBaseClient is a client used to interact with the Athenz ZTS (authZ Token System). It is required to create AthenzClient and AthenzService decorators. You can create an instance of ZtsBaseClient as follows:

import com.linecorp.armeria.client.athenz.ZtsBaseClient;
import com.linecorp.armeria.client.athenz.ZtsBaseClientBuilder;

ZtsBaseClient ztsBaseClient =
    ZtsBaseClient
        .builder("https://athenz.example.com:8443/zts/v1")
        // You need to specify your Athenz service key and certificate files.
        .keyPair("/var/lib/athenz/service.key.pem",
                 "/var/lib/athenz/service.cert.pem")
        // Uncomment the following line to use a proxy.
        // .proxyUri("http://my-proxy.example.com:8080")
        .build();

// ...

// Close the client when it is no longer needed.
ztsBaseClient.close();

Checking permissions with the AthenzService decorator

You can enforce Athenz authorization on your services by applying the AthenzService decorator. This decorator intercepts incoming requests and validates the client's token against the configured Athenz policies.

The code below configures a decorator for paths under /files. It will only grant access to requests that present a token with the specified action ("get") and resource ("files") for your Athenz domain. The action and resource must correspond to an assertion you have configured in Athenz.

import com.linecorp.armeria.server.Server;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.athenz.AthenzService;
import com.linecorp.armeria.server.athenz.AthenzPolicyConfig;

ServerBuilder serverBuilder = Server.builder();
serverBuilder.decoratorUnder(
    "/files",
    AthenzService.builder(ztsBaseClient)
                 .action("get")
                 .resource("files")
                 .policyConfig(new AthenzPolicyConfig("your-domain"))
                 .newDecorator());
serverBuilder.serviceUnder("/files", FileService.of(...));
...

The AthenzPolicyConfig object replaces the need for a zpu.conf file. It allows the server to download policy files directly at startup and perform authorization checks without relying on the zpu command-line utility.

If you are only using the decorator for permission checks, this is all the configuration required to integrate Athenz with Armeria.

Checking permissions with the @RequiresAthenzRole annotation

As an alternative to programmatically applying decorators, you can use the @RequiresAthenzRole annotation on service methods or classes for a more declarative approach.

import com.linecorp.armeria.server.athenz.RequiresAthenzRole;

class MyService {
    // Decorate the method with `@RequiresAthenzRole` to check the Athenz role.
    @RequiresAthenzRole(resource = "user", action = "get")
    @ProducesJson
    @Get("/user")
    public CompletableFuture<User> getUser() {
        // ...
    }
}

To enable this annotation, you must register an AthenzServiceDecoratorFactory with the server. This factory uses the ZtsBaseClient to create the necessary decorators that enforce the roles specified in the annotations.

import com.linecorp.armeria.common.DependencyInjector;
import com.linecorp.armeria.server.athenz.AthenzPolicyConfig;
import com.linecorp.armeria.server.athenz.AthenzServiceDecoratorFactory;
import com.linecorp.armeria.server.athenz.AthenzServiceDecoratorFactoryBuilder;

// Create and register `AthenzServiceDecoratorFactory`.
AthenzServiceDecoratorFactory athenzDecoratorFactory =
  AthenzServiceDecoratorFactory
    .builder(ztsBaseClient)
    .policyConfig(new AthenzPolicyConfig("my-domain"))
    .build();

DependencyInjector di =
  DependencyInjector.ofSingletons(athenzDecoratorFactory)
                    .orElse(DependencyInjector.ofReflective());
serverBuilder.dependencyInjector(di, true);

The @RequiresAthenzRole annotation can be applied not only to annotated services but also to methods in Thrift and gRPC services.

class UserGrpcServiceImpl extends UserGrpcServiceImplBase {
  @RequiresAthenzRole(resource = "user", action = "get")
  public void getUser(UserRequest request,
                      StreamObserver<UserResponse> responseObserver) {
    // ...
  }
}

Obtaining athenz tokens with AthenzClient

To communicate with other Athenz-protected services, you can use the AthenzClient decorator. This client automatically acquires an Athenz token from the ZTS, caches it, and attaches it to outgoing requests. It also handles token refreshes before expiration, so you do not need to manage the token lifecycle yourself.

To obtain an access token and send it in the Authorization header, configure the decorator with TokenType.ACCESS_TOKEN.

import com.linecorp.armeria.client.athenz.AthenzClient;
import com.linecorp.armeria.common.athenz.TokenType;

WebClient client =
  WebClient
    .builder("https://api.example.com/")
    .decorator(AthenzClient.newDecorator(ztsBaseClient, "my-domain",
                                         TokenType.ACCESS_TOKEN))
    .build();

// An Athenz access token is automatically acquired and set in the `Authorization` header.
client.get("/files");

To obtain a role token and send it in a role token header (e.g., Athenz-Role-Auth or Yahoo-Role-Auth), specify TokenType.ATHENZ_ROLE_TOKEN or TokenType.YAHOO_ROLE_TOKEN.

WebClient client =
WebClient
  .builder("https://api.example.com/")
  .decorator(AthenzClient.newDecorator(ztsBaseClient, "my-domain",
                                       TokenType.ATHENZ_ROLE_TOKEN))
  .build();