agentsclimarketplace

Kora http client auth

Skill kora-projects/kora-skills/plugins/kora-v1/skills/kora-http-client-auth

Authentication for outgoing Kora HTTP clients. Covers the built-in BasicAuthHttpClientInterceptor, ApiKeyHttpClientInterceptor and BearerAuthHttpClientInterceptor, the HttpClientTokenProvider interface, attaching them with @InterceptWith, and hand-written HttpClientInterceptor classes for custom schemes (OAuth2 client credentials, JWT with caching/refresh). Use when adding Basic/Bearer/API-key authorization to a @HttpClient, implementing token refresh for service-to-service calls, or debugging 401 responses from an external API. Not for server-side auth (use kora-http-server-auth).From its SKILL.md

Install
npx -y skills add kora-projects/kora-skills --skill kora-http-client-auth

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • reads credentialsReads from 2 credential sources: `api.token` and 1 more.
  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

10.1 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it

Kora HTTP Client Auth

Authenticate outgoing requests from a declarative @HttpClient. Kora ships ready-made HttpClientInterceptor implementations for Basic, API-key and Bearer schemes; attach any interceptor with @InterceptWith. For dynamic tokens (OAuth2 client credentials, refreshable JWT) you implement HttpClientTokenProvider or write your own HttpClientInterceptor.

Level: Intermediate (requires kora-http-client and kora-di-compile).

Key facts (verify against the source of truth):

  • There is no ru.tinkoff.kora:http-client-auth artifact. Auth lives in http-client-common and is wired through interceptors.
  • Built-in interceptors: BasicAuthHttpClientInterceptor, ApiKeyHttpClientInterceptor, BearerAuthHttpClientInterceptor.
  • HttpClientTokenProvider is the extension point for Bearer tokens; the Bearer interceptor calls it on every request.
  • Interceptors are attached with @InterceptWith(...), not an interceptors = {...} attribute on @HttpClient.
  • The target URL is set in config (httpClient.<client>.url), not a baseUrl annotation attribute.

Quick Start

build.gradle — note the mandatory annotation processor:

dependencies {
    koraBom platform("ru.tinkoff.kora:kora-parent:1.2.17")

    annotationProcessor "ru.tinkoff.kora:annotation-processors"

    implementation "ru.tinkoff.kora:config-hocon"
    implementation "ru.tinkoff.kora:http-client-common"
    implementation "ru.tinkoff.kora:http-client-ok"   // OkHttp transport
    implementation "ru.tinkoff.kora:json-module"
    implementation "ru.tinkoff.kora:logging-logback"
}
@KoraApp
public interface Application extends
    HoconConfigModule,
    JsonModule,
    LogbackModule,
    OkHttpClientModule,
    BearerAuthModule { }

Provide a token, register the built-in Bearer interceptor in a @Module, and attach it to the client:

@Component
public final class StaticTokenProvider implements HttpClientTokenProvider {

    private final ApiTokenConfig config;

    public StaticTokenProvider(ApiTokenConfig config) {
        this.config = config;
    }

    @Override
    public CompletionStage<String> getToken(HttpClientRequest request) {
        return CompletableFuture.completedFuture(config.token());
    }
}
@Module
public interface BearerAuthModule {

    default BearerAuthHttpClientInterceptor bearerAuther(HttpClientTokenProvider tokenProvider) {
        return new BearerAuthHttpClientInterceptor(tokenProvider);
    }
}
@HttpClient(configPath = "httpClient.secureApi")
public interface SecureApiClient {

    @InterceptWith(BearerAuthHttpClientInterceptor.class)
    @HttpRoute(method = HttpMethod.GET, path = "/protected")
    @Json
    ProtectedResponse getProtected();
}
httpClient.secureApi {
    url = "https://api.example.com"
}
api.token = ${API_TOKEN}   // externalize the secret

@ConfigSource for the token:

@ConfigSource("api")
public interface ApiTokenConfig {
    String token();
}

When to use vs NOT

Use this skill when you:

  • add Authorization: Basic/Bearer or an API-key header to outbound requests;
  • implement HttpClientTokenProvider for OAuth2 client-credentials or JWT refresh;
  • write a custom HttpClientInterceptor for a non-standard scheme;
  • get 401 Unauthorized from an external API and need to fix the credentials flow.

Do not use this skill when you:

  • authenticate requests on the server — see kora-http-server-auth;
  • need OAuth2 with a user context (authorization-code flow) — Kora ships only the building blocks; the client-credentials pattern here is service-to-service.

Reference files

TopicReference
Built-in interceptors (Basic / API-key / Bearer), @InterceptWith placement, configreferences/http-client-auth-reference.md
Custom HttpClientInterceptor (header & query-param API key)references/apikey-interceptor-reference.md
HttpClientTokenProvider with caching/refreshreferences/jwt-token-provider-reference.md
Thread-safe token cachereferences/token-cache-reference.md
OAuth2 client-credentials end-to-endreferences/oauth2-client-credentials-reference.md

Templates and a generator script live in assets/.


Core patterns

1. Built-in Basic / API-key / Bearer

Register the interceptor as a component in a @Module, then attach it.

@Module
public interface ApiKeyAuthModule {

    @ConfigSource("openapiAuth.apiKeyAuth")
    interface ApiKeyAuthConfig {
        String apiKey();
    }

    default ApiKeyHttpClientInterceptor apiKeyAuther(ApiKeyAuthConfig config) {
        return new ApiKeyHttpClientInterceptor(ApiKeyLocation.HEADER, "X-API-KEY", config.apiKey());
    }
}
@HttpClient(configPath = "httpClient.someClient")
public interface SomeClient {

    @InterceptWith(ApiKeyHttpClientInterceptor.class)
    @HttpRoute(method = HttpMethod.GET, path = "/hello/world")
    void hello();
}

@InterceptWith may sit on the interface (applies to every method) or on a single method. ApiKeyLocation is HEADER, QUERY, or COOKIE.

BasicAuthHttpClientInterceptor(username, password) Base64-encodes the credentials for you. BearerAuthHttpClientInterceptor takes an HttpClientTokenProvider (or a static token string) and adds the Authorization header per request.

2. Custom interceptor for a non-standard scheme

Implement HttpClientInterceptor directly when the built-ins do not fit. The signature returns a CompletionStage<HttpClientResponse> and you must call chain.process(...):

@Component
public final class CustomHeaderInterceptor implements HttpClientInterceptor {

    private final ApiKeyAuthConfig config;

    public CustomHeaderInterceptor(ApiKeyAuthConfig config) {
        this.config = config;
    }

    @Override
    public CompletionStage<HttpClientResponse> processRequest(
            Context ctx, InterceptChain chain, HttpClientRequest request) throws Exception {
        var authorized = request.toBuilder()
                .header("X-Custom-Token", config.value())
                .build();
        return chain.process(ctx, authorized);
    }
}

Use request.toBuilder().header(name, value) for headers and .queryParam(name, value) for query parameters; never mutate the original request.

3. Dynamic token via HttpClientTokenProvider

For tokens that must be fetched and refreshed (OAuth2 client credentials, JWT), implement HttpClientTokenProvider and return a CompletionStage<String> so the fetch stays non-blocking. Cache the token and refresh ahead of expiry. See references/jwt-token-provider-reference.md and references/oauth2-client-credentials-reference.md.


Common pitfalls

SymptomCauseFix
Required dependency not found: ...http-client-authThe artifact does not existDepend on http-client-common + a transport (http-client-ok); use interceptors
Interceptor never runsUsed a non-existent interceptors = {...} attributeAttach with @InterceptWith(YourInterceptor.class)
cannot find symbol: method baseUrl()@HttpClient has no baseUrl attributeSet httpClient.<client>.url in config (or configPath)
@Value/@ConfigValue not resolvedThose annotations do not exist in KoraBind a @ConfigSource interface and inject it via the constructor
401 after refreshToken expired mid-flightRefresh ahead of expiry with a margin (e.g. 60s); see token cache reference
Duplicate token fetches under loadConcurrent refreshDouble-check + lock (or volatile fields); see token cache reference
Secret committed to VCSHard-coded credentialsExternalize with ${VAR} substitution in config

Testing

Replace the real provider with a @TestComponent so no network call is made:

@TestComponent
public final class TestTokenProvider implements HttpClientTokenProvider {
    @Override
    public CompletionStage<String> getToken(HttpClientRequest request) {
        return CompletableFuture.completedFuture("test-token-12345");
    }
}
@KoraAppTest(Application.class)
class SecureApiClientTest {

    @Test
    void addsAuthorization(@TestComponent SecureApiClient client) {
        // Drive the client against a stub server (e.g. Testcontainers/WireMock)
        // and assert the upstream received the Authorization header.
        assertThat(client.getProtected()).isNotNull();
    }
}

See kora-testing-junit-java and kora-testing-blackbox for the full setup.


Related skills

  • kora-http-client — declarative HTTP clients, @HttpRoute, interceptors
  • kora-http-server-auth — server-side Basic/Bearer/API-key
  • kora-config-hocon — @ConfigSource, env substitution
  • kora-aop-logging — @Log for client calls
  • kora-telemetry-tracing — distributed tracing for outbound calls

Source of truth

  • Doc: .kora-agent/kora-docs/mkdocs/docs/en/documentation/http-client.md (Authorization section)
  • Guide: .kora-agent/kora-docs/mkdocs/docs/en/guides/http-client-advanced.md
  • Example: .kora-agent/kora-examples/guides/java/kora-java-guide-http-client-advanced-app
  • Example: .kora-agent/kora-examples/examples/java/kora-java-http-client

What ships with it: 13 files

47.8 KB alongside SKILL.md, 1 of them executable

evals/

scripts/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.