From service to service.

Connect a caller, protect an API, and understand what happens between them. Start with a token request, then let the Go toolkit handle the routine work.

Orionis 0.6.0

1. Request a token

Start the auth server and billing API from the quick start. These commands use only the checked-in local demo credentials.

Shell · request an access token
curl -fsS \
  -u 'orders-service:orders-local-secret-change-me' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials' \
  -d 'audience=billing-api' \
  -d 'scope=billing.invoice.create' \
  http://localhost:8080/oauth/token

Orionis authenticates the caller, checks the requested audience and scopes, and returns a Bearer token. The default lifetime is 900 seconds.

JSON · example response
{
  "access_token": "<signed JWT>",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "billing.invoice.create"
}

Copy access_token from the response into the command below. The bundled billing service returns 201 and identifies orders-service as the caller.

Shell · call the protected API
TOKEN='<paste access_token here>'
curl -i \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"order_id":"ord_demo_001","amount":1500}' \
  http://localhost:8081/invoices

The endpoint accepts client_secret_basic and client_secret_post. Use Basic authentication as shown; send credentials over TLS in shared environments. Multiple scopes are space-separated. Wildcard request selectors expand to allowed concrete scopes; route guards should require concrete permissions.

2. Connect the caller

Install the module in your Go service. The same provider can be reused across requests and goroutines.

Shell · add the module
go get github.com/stremovskyy/orionis@v0.6.0

For the local example, set ORIONIS_CLIENT_SECRET=orders-local-secret-change-me. Load the real caller secret from your secret manager in production.

Go · calling service
package main

import (
    "bytes"
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/stremovskyy/orionis/client"
)

func main() {
    hc, err := client.New().
        TokenURL("http://localhost:8080/oauth/token").
        As("orders-service", os.Getenv("ORIONIS_CLIENT_SECRET")).
        For("billing-api", "billing.invoice.create").
        BuildHTTPClient(http.DefaultClient)
    if err != nil {
        log.Fatal(err)
    }

    body := bytes.NewBufferString(`{"order_id":"ord_demo_001","amount":1500}`)
    req, err := http.NewRequest("POST", "http://localhost:8081/invoices", body)
    if err != nil {
        log.Fatal(err)
    }
    req.Header.Set("Content-Type", "application/json")
    res, err := hc.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer res.Body.Close()
    fmt.Println(res.StatusCode) // 201 with the bundled billing API
}

Let the provider manage the token.

The HTTP client adds Authorization: Bearer automatically. Tokens are cached by audience and normalized scope set. A new request obtains a replacement when the cached token is near expiry; simultaneous requests for the same target share one acquisition.

To share credentials across several APIs, build one provider with client.New().TokenURL(...).As(...).Build(), then use provider.For(audience, scopes...).HTTPClient(base) for each target.

3. Protect the API

The receiving service needs the expected issuer, its own audience, and the JWKS URL. Add a concrete permission to each protected route.

Go · GIN resource service
package main

import (
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/stremovskyy/orionis/ginorion"
)

func main() {
    guard, err := ginorion.New().
        Issuer("http://localhost:8080").
        Audience("billing-api").
        JWKS("http://localhost:8080/.well-known/jwks.json").
        Build()
    if err != nil {
        log.Fatal(err)
    }

    r := gin.Default()
    r.POST("/invoices", guard.Require("billing.invoice.create"),
        func(c *gin.Context) {
            claims := ginorion.MustClaims(c)
            c.JSON(http.StatusCreated, gin.H{
                "called_by": claims.ClientID,
            })
        })
    if err := r.Run(":8081"); err != nil {
        log.Fatal(err)
    }
}

This minimal handler demonstrates authorization. Use the bundled billing example for the request-body and invoice response contract.

Checks performed before the handler
Check What it establishes
kid + signature + algorithm The token was signed by a trusted key using an allowed algorithm.
iss and aud The expected issuer minted the token for this API.
exp, nbf, iat The token is within its accepted time window.
token_use, sub, client_id It is an access token with a service identity.
Required scopes The caller has every permission required by this route.

GIN integration is optional. Other HTTP stacks can use orionis.NewVerifier() directly. See the Go package reference for verifier, signer, and key-provider interfaces.

4. Understand JWKS

Build() prepares the remote provider without fetching keys. JWKS retrieval happens when a request needs a key.

02 / Remote key lifecycle

A JWT arrives → look up its kid
MATCHING KEY + FRESH CACHE

Verify in memory.

No auth-server request is needed. Continue with claims and route-scope checks.

FIRST LOOKUP / EXPIRED CACHE / UNKNOWN KID

Fetch the JWKS.

A successful refresh replaces the key set. Concurrent lookups share the refresh.

If the fetch fails ↓
KNOWN KEY WITHIN MAXSTALE

Use the cached key.

The signature and every token claim must still pass validation.

NO KEY / STALE WINDOW EXCEEDED

Reject the token.

A missing key or an unusable cache cannot establish trust.

Caching keeps normal verification local. An outage can only fall back to an already cached matching key within its allowed stale window.

The defaults are RefreshEvery(10 * time.Minute) and MaxStale(time.Hour). The stale window is measured from the last successful fetch, rather than added after the refresh interval. Both are configurable on the GIN builder. Static key providers verify entirely locally.

5. Endpoints & errors

Default auth server endpoints
Method Path Purpose
POST /oauth/token Issue a service access token.
GET /.well-known/jwks.json Publish public signing keys.
GET /.well-known/openid-configuration Publish issuer and endpoint metadata.
GET /healthz Report the initialized runtime’s health.
GET /readyz Report readiness, with configured rate limiting.

These are the default paths. A configured base_path prefixes all routes; for example, /auth/oauth/token. Health and readiness use the same payload because runtime initialization completes before routes are mounted.

Common errors and how to resolve them
Where / HTTP Error Check
Token / 401 invalid_client Client ID, secret, and whether the client is disabled.
Token / 403 invalid_target The audience is in the client’s allowed audiences.
Token / 400 invalid_scope The requested scopes are allowed.
Token / 400 invalid_request / unsupported_grant_type Form encoding, required audience, and client_credentials grant.
Resource / 401 missing_bearer_token / invalid_token Bearer header, issuer, audience, expiry, and signing key.
Resource / 403 insufficient_scope The token includes every scope required by the route.
Token or readiness / 429 rate_limited Respect Retry-After and reduce request frequency.
Continue the guideDeploy your auth server