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.01. Request a token
Start the auth server and billing API from the quick start. These commands use only the checked-in local demo credentials.
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.
{
"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.
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.
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.
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.
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.
| 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
Verify in memory.
No auth-server request is needed. Continue with claims and route-scope checks.
Fetch the JWKS.
A successful refresh replaces the key set. Concurrent lookups share the refresh.
Use the cached key.
The signature and every token claim must still pass validation.
Reject the token.
A missing key or an unusable cache cannot establish trust.
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
| 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.
| 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. |