> ## Documentation Index
> Fetch the complete documentation index at: https://docs.westyx.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Go SDK - Workload Identity Federation

> Bearer-JWT auth with Kubernetes, AWS, GCP, Azure, and AWS IAM (non-EKS) support for the Go SDK.

For deployments that prefer not to manage static API keys, the Go SDK can authenticate via **Workload Identity Federation (WIF)**. The SDK fetches a workload OIDC token from the running environment, exchanges it for a Nexus session JWT, and uses `Authorization: Bearer <jwt>` on every subsequent API call.

WIF is opt-in. When `WIFConfig.Enabled == false` (the zero value), the SDK falls back to the static `APIKey` flow and behaves exactly as in v0.1.0.

## Enabling

```go theme={null}
import nexus "gitlab.com/westyx/nexus/sdk/go"

client, err := nexus.NewClient(ctx, nexus.Config{
    BaseURL: "https://blue-ocean-a5rx7.westyx.dev",
    WIF: &nexus.WIFConfig{
        Enabled:  true,
        Provider: nexus.WIFProviderAuto, // default - auto-detect from env
    },
})
```

`APIKey` becomes optional when WIF is enabled. If you provide both, the SDK uses WIF and treats `APIKey` as a fallback if a refresh fails after the initial connect.

<Note>
  **Security note (v0.9.0):** `BaseURL` must use `https://` - `NewClient` refuses plain-http endpoints (loopback/localhost excepted for development), since credentials travel on every request.
</Note>

## Supported providers

The SDK probes the environment in this order when `Provider` is `WIFProviderAuto`:

| Provider constant                      | Auto-detect signal                                                                                                                            | Credential                                                                                                                                                                                        |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WIFProviderKubernetes`                | `/var/run/secrets/kubernetes.io/serviceaccount/token` exists                                                                                  | reads the projected service-account token file (rotated by kubelet)                                                                                                                               |
| `WIFProviderAWS`                       | `AWS_WEB_IDENTITY_TOKEN_FILE` set **and the file exists** (EKS IRSA)                                                                          | reads the IRSA token file at that path                                                                                                                                                            |
| `WIFProviderAzure` (Workload Identity) | `AZURE_FEDERATED_TOKEN_FILE` set and the file exists (AKS)                                                                                    | reads the projected federated token file                                                                                                                                                          |
| `WIFProviderGCP`                       | `metadata.google.internal` reachable on port 80                                                                                               | GETs `/computeMetadata/v1/instance/service-accounts/default/identity?audience=<aud>` with `Metadata-Flavor: Google`                                                                               |
| `WIFProviderAzure` (IMDS)              | `169.254.169.254/metadata/instance` reachable                                                                                                 | GETs an Azure IMDS managed-identity token - **requires `Audience: "api://<client-id>"`** (your app registration's Application ID URI; the generic default is refused because Azure AD rejects it) |
| `WIFProviderAWSIAM`                    | **not auto-detected - select explicitly** (resolvable AWS credentials alone, e.g. a dev laptop's `~/.aws/credentials`, are too weak a signal) | SigV4-signs an STS `GetCallerIdentity` request (never sent to AWS) that Nexus replays to prove your IAM role - works on **ECS/Fargate, Lambda, and plain EC2**                                    |

The OIDC providers are stdlib-only; `aws_iam` is the sole feature that uses the `aws-sdk-go-v2` config/signer dependency.

You can also pin a specific provider:

```go theme={null}
WIF: &nexus.WIFConfig{
    Enabled:  true,
    Provider: nexus.WIFProviderKubernetes,
}
```

## AWS IAM (non-EKS AWS compute)

ECS/Fargate tasks, Lambda functions, and plain EC2 instances have IAM credentials but no OIDC token, so the OIDC providers above cannot serve them. The `aws_iam` provider closes this gap:

```go theme={null}
WIF: &nexus.WIFConfig{
    Enabled:  true,
    Provider: nexus.WIFProviderAWSIAM,
    // AWSRegion: "eu-central-1", // optional - defaults to the standard AWS region chain (env, config file, EC2 IMDS)
}
```

How it works:

1. The SDK SigV4-signs an STS `GetCallerIdentity` request using the standard AWS credential chain (task role, instance profile, env). The request is **never sent to AWS by the SDK**.
2. The signed request (headers + body) is posted to `/v1/auth/token-exchange` as `{"provider":"aws_iam","aws_sts_request":...}`.
3. Nexus replays it against a pinned STS endpoint; AWS answers with the caller's IAM identity, which Nexus matches against an `aws_iam` trust policy (subject = the assumed-role ARN, normalized to `arn:aws:iam::<account>:role/<name>`).

Nothing else to configure: the SDK signs your service's own host (the `BaseURL` host) into the `X-Nexus-Server-ID` header inside the SigV4 signature, and Nexus verifies it against the host the exchange request actually arrived on. A captured signed request is therefore valid for that ONE service only - it cannot be replayed against any other service or deployment.

`aws_iam` is never auto-detected - select it explicitly. Resolvable AWS credentials alone (e.g. a developer laptop's `~/.aws/credentials`) are too weak a signal to silently authenticate with.

## Custom `TokenSource`

Provide a `TokenSourceFunc` to bypass auto-detection entirely. This is the right hook for tests, custom CI flows, or topologies the SDK doesn't natively recognise.

```go theme={null}
WIF: &nexus.WIFConfig{
    Enabled: true,
    TokenSource: func(ctx context.Context) (string, error) {
        // Anything that produces a JWT acceptable to /v1/auth/token-exchange.
        return readMyOIDCToken(ctx)
    },
}
```

When `TokenSource` is non-nil it takes precedence over `Provider`.

## Audience

GCP and Azure require an `audience` claim when issuing the OIDC token. Set it via `WIFConfig.Audience`:

```go theme={null}
WIF: &nexus.WIFConfig{
    Enabled:  true,
    Provider: nexus.WIFProviderGCP,
    Audience: "westyx-nexus",
}
```

The default is `"westyx-nexus"` if unset. Kubernetes service-account tokens carry the audience configured in the pod's projected token spec - the field is informational there.

**Azure IMDS is the exception:** the generic default is refused with a clear error, because Azure AD rejects it as a `resource`. On the IMDS path (plain VM / App Service - no federated token file) you MUST set `Audience` to your app registration's Application ID URI:

```go theme={null}
WIF: &nexus.WIFConfig{
    Enabled:  true,
    Provider: nexus.WIFProviderAzure,
    Audience: "api://<client-id>",
}
```

On AKS with Azure Workload Identity the projected federated token file (`$AZURE_FEDERATED_TOKEN_FILE`) is preferred automatically and no `Audience` is needed - the file's audience is set by the cluster.

## Token exchange flow

Internally, the SDK runs this protocol:

1. **OIDC token fetch** - `WIFConfig.TokenSource(ctx)` (or the auto-detected built-in equivalent) returns a workload-identity JWT. (For `aws_iam` this step signs an STS `GetCallerIdentity` request instead - there is no OIDC token.)
2. **Token exchange** - the SDK POSTs `{"oidc_token": "<jwt>"}` (or `{"provider":"aws_iam","aws_sts_request":...}`) to `/v1/auth/token-exchange`. The backend validates the credential, then returns:
   ```json theme={null}
   {
     "session_token": "<short-lived-nexus-jwt>",
     "token_type":    "Bearer",
     "expires_in":    3600,
     "service_kind":  "backend",
     "wif_provider":  "kubernetes"
   }
   ```
3. **Bearer auth on subsequent calls** - every `/v1/*` request carries `Authorization: Bearer <session_token>`.
4. **Pre-emptive refresh** - when a request finds the session expires within 60 s the SDK refreshes (re-running steps 1-2) before issuing the call. Concurrent callers see exactly one in-flight exchange.

## SSE `auth_expiring` event

About 5 minutes before the session JWT expires, the backend emits an `auth_expiring` SSE control event. The SDK reacts by triggering a non-blocking session refresh - it does NOT call `/sync`, and the event is NOT surfaced through the `StreamObserver`. When the server force-closes the stream at expiry, the SDK reconnects with the freshly-issued bearer.

## Errors

| Sentinel                    | When                                                                                                           |
| --------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `ErrWIFNotConfigured`       | `WIFConfig.Enabled=true` but no usable provider matched (auto-detect failed and no `TokenSource` was supplied) |
| `ErrWIFTokenExchangeFailed` | `POST /v1/auth/token-exchange` returned a non-2xx response or the body could not be decoded                    |
| `ErrSessionExpired`         | The session JWT expired and refresh failed; no static `APIKey` fallback available                              |

Use `errors.Is` for sentinel matching:

```go theme={null}
if errors.Is(err, nexus.ErrWIFTokenExchangeFailed) {
    // backend rejected the token - log and bail
}
```

## Service kind awareness

The token-exchange response includes `service_kind` (`"frontend"` or `"backend"`). The SDK stores this and exposes it via `client.Kind()`. If the resolved kind is `"frontend"`, `GetSecret(...)` returns `ErrServiceKindMismatch` regardless of the key type - frontend services are public by design and must not hold secrets.

## Project-level vs service-level trust policies

The Nexus backend supports trust policies at two scopes:

| Scope             | What it covers                               | Recommended for                                   |
| ----------------- | -------------------------------------------- | ------------------------------------------------- |
| **Project-level** | All services in the project share one policy | Most teams - no per-service cloud identity needed |
| **Service-level** | A specific policy bound to one service only  | High-security services (payments, PII)            |

### How it works

The SDK always connects to a specific service via its `baseUrl` (e.g. `https://worker.westyx.dev`). This is true regardless of whether you use WIF or a static API key - the URL slug identifies the service, and the backend resolves which configs, secrets, and flags to serve based on it.

At token exchange time the backend applies this resolution order:

1. Does a **service-level** trust policy match this workload's OIDC token claims? - if yes, authorise.
2. Does a **project-level** trust policy match? - if yes, authorise.
3. Neither matches - return 401.

This means you can configure **one project-level trust policy** and all services in the project will accept that workload's tokens. Each service SDK instance still uses its own `baseUrl`, so each gets its own service's data.

### Example: one cloud identity for 50 services

```
Project "acme-platform" (project-level trust policy: kubernetes / system:serviceaccount:prod:nexus-reader)

  SDK config for service "api-gateway":
    baseUrl = "https://api-gateway.westyx.dev"
    wif.enabled = true

  SDK config for service "worker":
    baseUrl = "https://worker.westyx.dev"
    wif.enabled = true

  SDK config for service "scheduler":
    baseUrl = "https://scheduler.westyx.dev"
    wif.enabled = true
```

All three services share the same Kubernetes service account. No separate cloud identities required.

### Security trade-off

<Warning>
  A project-level trust policy is less restrictive than a service-level one. Any workload that satisfies the policy can connect to **any** service in the project by targeting its `baseUrl`. If a pod is compromised, an attacker could read configs and secrets of sibling services.

  For services handling sensitive data (payments, credentials, PII) consider a dedicated service-level trust policy instead.
</Warning>

The Nexus UI surfaces this trade-off when configuring trust policies on the Security tab of a project.
