Skip to main content
Entwickler Themen
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

7.3.2. go-usertoken

go-usertoken stellt kurzlebige RS256-User Tokens, Glossar, öffnet in neuem Tab bereit, mit denen eine Nextcloud-ExApp, Glossar, öffnet in neuem Tab den bereits von AppAPI, Glossar, öffnet in neuem Tab benannten Benutzer an Go-Microservices, Glossar, öffnet in neuem Tab weitergibt. Import-Pfad: gitea.neitzel.de/konrad/go-usertoken, Package: usertoken. Repo: gitea.neitzel.de/konrad/go-usertoken.

Die ExApp mintet den Token (privater Schlüssel nur dort – der Signer, Glossar, öffnet in neuem Tab ). Jeder Microservice prüft denselben Bearer – entweder mit dem passenden öffentlichen Schlüssel (Static Key, Glossar, öffnet in neuem Tab ) oder über einen OIDC Issuer, Glossar, öffnet in neuem Tab . Ruft ein Microservice einen anderen auf, wird derselbe Bearer weitergereicht, nicht neu signiert.

AppAPI und APP_SECRET gehören nicht zu dieser Library; dafür ist go-nc-exapp. Überblick zum ExApp-Stack: 7.3.1. Nextcloud ExApps. Englische Fachbegriffe sind im Glossar erklärt.

Einbinden

go get gitea.neitzel.de/konrad/go-usertoken

Privates Modul: GOPRIVATE=gitea.neitzel.de, damit go get nicht den öffentlichen Proxy trifft. Klonen über SSH-Port 2222: ssh://git@gitea.neitzel.de:2222/konrad/go-usertoken.git.

import "gitea.neitzel.de/konrad/go-usertoken"
sequenceDiagram
  participant ExApp
  participant MS1 as Microservice A
  participant MS2 as Microservice B
  ExApp->>ExApp: Signer.Mint
  ExApp->>MS1: Authorization Bearer
  MS1->>MS1: Verify
  MS1->>MS2: denselben Bearer weiterreichen
  MS2->>MS2: Verify

Typische Aufteilung:

Rolle Importiert Aufgabe
ExApp go-nc-exapp + go-usertoken AppAPI-User lesen, dann Token minten
Microservice nur go-usertoken Bearer prüfen, Caller, Glossar, öffnet in neuem Tab /Bearer aus dem Context lesen, Bearer weiterreichen

Ein Prozess wählt genau einen Verify-Modus: Static Key oder OIDC Issuer, nicht beides und nicht keines.

API im Detail

Signer / Mint (ExApp)

Nur die ExApp hält den privaten RSA-Schlüssel und mintet. Nicht in einem Microservice minten.

Symbol Rolle
NewSignerFromEnv() Liest USER_TOKEN_PRIVATE_KEY_FILE, USER_TOKEN_ISSUER, USER_TOKEN_AUDIENCE, optional USER_TOKEN_TTL
NewSigner(SignConfig) Explizite Konfiguration (PrivateKey, Issuer, Audience, TTL)
Signer.Mint(now, MintInput) Signiert ein RS256-JWT zum Zeitpunkt now

MintInput.Subject wird zu sub und preferred_username. Groups als nil lässt den groups-Claim weg; ein non-nil-Pointer schreibt den Claim (auch als leere Liste).

SignConfig.TTL bzw. leeres USER_TOKEN_TTL → Default 5 Minuten. Private Key: PEM PKCS#1 oder PKCS#8.

Fehler von NewSignerFromEnv / NewSigner / Mint sind Konfigurations- bzw. Signaturfehler. Sie wrapen nicht ErrUnauthorized (das ist nur für fehlgeschlagene Checks auf der Microservice-Seite).

Ablauf nach AppAPI-Auth:

  1. User-Id aus AppAPI.
  2. Bei Bedarf Gruppen laden (Groups.UserGroups in go-nc-exapp).
  3. Einmal Mint für diesen Request.
  4. Authorization: Bearer <token> an jeden Microservice-Aufruf.

Verify, HTTP-Middleware, gRPC-Interceptor (Microservice)

Symbol Rolle
FromEnv(ctx) Modus aus der Umgebung; ctx für OIDC-Discovery
New(ctx, Config) Explizit: PublicKey oder OIDCIssuer
Auth.Verify(ctx, raw) Roh-JWT ohne "Bearer "-Prefix; Fehler wrapen ErrUnauthorized
Auth.Middleware() HTTP: jede Route außer /health braucht einen gültigen Bearer
Auth.UnaryServerInterceptor() gRPC Unary: jeder RPC braucht einen Bearer
CallerFromContext / BearerFromContext Caller und Roh-JWT aus dem Request-Context

Nach erfolgreicher Prüfung liegen Caller und der Roh-JWT im Context. HTTP ohne gültigen Bearer → 401. gRPC → codes.Unauthenticated. Body/Status-Text ohne Detailgrund.

gRPC-Metadaten: native Clients senden authorization; der HTTP-Gateway-Header ist grpcgateway-authorization.

/health bleibt ohne Auth, damit Orchestratoren den Prozess prüfen können.

Static Public Key vs. OIDC

Static Key OIDC Issuer
Auswahl USER_TOKEN_PUBLIC_KEY_FILE gesetzt OIDC_ISSUER gesetzt
Schlüssel RSA Public Key PEM auf dem Microservice Discovery + JWKS am Issuer
iss feste Zeichenkette, exakter Vergleich (USER_TOKEN_ISSUER) Issuer-URL
aud Pflicht (USER_TOKEN_AUDIENCE) OIDC_AUDIENCE; leer = Audience-Check überspringen
Gruppen-Claim immer groups OIDC_GROUPS_CLAIM, Default groups
Skew USER_TOKEN_SKEW, Default 1m ebenfalls USER_TOKEN_SKEW, Default 1m

Beide gesetzt oder keines → Startfehler. Handler bleiben gleich, wenn das Deploy den Modus wechselt.

Static-Checks (kurz): nur RS256; Signatur; exaktes iss; aud enthält die konfigurierte Audience; exp / iat / optionales nbf innerhalb des Skews; nicht-leeres sub. Fehlt preferred_username, ist der Username = sub.

Die ExApp veröffentlicht kein Discovery/JWKS und kein Introspection-Endpoint. Produktrollen (z. B. realm_access) liest diese Library nicht.

Umgebungsvariablen

ExApp (Mint):

Variable Rolle
USER_TOKEN_PRIVATE_KEY_FILE RSA Private Key PEM
USER_TOKEN_ISSUER iss-String
USER_TOKEN_AUDIENCE aud-String
USER_TOKEN_TTL optional, Default 5m

Microservice – Static:

Variable Rolle
USER_TOKEN_PUBLIC_KEY_FILE RSA Public Key PEM
USER_TOKEN_ISSUER erwartetes iss
USER_TOKEN_AUDIENCE erwartetes aud
USER_TOKEN_SKEW optional, Default 1m

Microservice – OIDC:

Variable Rolle
OIDC_ISSUER Issuer-URL mit Discovery
OIDC_AUDIENCE erwartetes aud; leer überspringt den Check
OIDC_GROUPS_CLAIM Gruppen-Array-Claim, Default groups
USER_TOKEN_SKEW optional, Default 1m

iss und aud im Static-Modus sind dieselben Strings auf ExApp und Microservices – keine URLs. Beispiel: Issuer my-exapp, Audience my-services (Deploy-Konvention, kein Library-Default).

APP_SECRET, Nextcloud-Base-URL und ExApp-Id bleiben ExApp-Deploy-Settings und gehören nicht auf Microservices.

Claims

Signatur RS256. Default-Lebensdauer 5 Minuten.

Claim Wert
iss konfigurierter Issuer (Static) bzw. OIDC-Issuer-URL
sub Nextcloud-User-Id
preferred_username dieselbe User-Id
groups optional; Snapshot der Gruppen-Ids beim Mint. Claim weglassen, wenn Gruppen nicht geladen; [] bedeutet geladen und leer
aud eine Audience für die ganze Service-Familie
iat / exp Unix-Sekunden

Beispiel-Payload:

{
  "iss": "my-exapp",
  "sub": "alice",
  "preferred_username": "alice",
  "groups": ["ops"],
  "aud": "my-services",
  "iat": 1759050000,
  "exp": 1759050300
}

Caller.Groups ist nil, wenn der Claim fehlte, und non-nil (ggf. leer), wenn er vorhanden war.

Bearer zwischen Microservices weiterreichen

Nach der Middleware / dem Interceptor:

raw, ok := usertoken.BearerFromContext(ctx)
// outbound:
req.Header.Set("Authorization", "Bearer "+raw)

Kein neues Mint im Microservice. Derselbe Token gilt bis exp gegen jeden Microservice derselben Audience. Bearer-Werte nicht in Logs schreiben.

Beispiele

Schlüssel erzeugen

Einmalig RSA-Schlüsselpaar erzeugen. Private Key nur auf die ExApp; Public Key an jeden Microservice im Static-Modus.

openssl genrsa -out user-token.key 2048
openssl rsa -in user-token.key -pubout -out user-token.pub

ExApp: minten

signer, err := usertoken.NewSignerFromEnv()
if err != nil {
    return err
}
raw, err := signer.Mint(time.Now(), usertoken.MintInput{
    Subject: userID,
    Groups:  &groupIDs, // nil lässt groups weg
})
if err != nil {
    return err
}
req.Header.Set("Authorization", "Bearer "+raw)

Microservice: prüfen und weiterreichen

auth, err := usertoken.FromEnv(ctx)
if err != nil {
    return err
}
handler = auth.Middleware()(handler)

// im Handler
caller, ok := usertoken.CallerFromContext(r.Context())
if !ok {
    http.Error(w, "unauthorized", http.StatusUnauthorized)
    return
}
raw, _ := usertoken.BearerFromContext(r.Context())
out.Header.Set("Authorization", "Bearer "+raw)
_ = caller

Für gRPC statt Middleware: grpc.UnaryInterceptor(auth.UnaryServerInterceptor()).

Env-Übersicht

ExApp (Mint) Microservice (Static) Microservice (OIDC)
Private Key PEM USER_TOKEN_PRIVATE_KEY_FILE — —
Public Key PEM — USER_TOKEN_PUBLIC_KEY_FILE —
Issuer USER_TOKEN_ISSUER USER_TOKEN_ISSUER OIDC_ISSUER (URL)
Audience USER_TOKEN_AUDIENCE USER_TOKEN_AUDIENCE OIDC_AUDIENCE (leer = Skip)
Lifetime USER_TOKEN_TTL (Default 5m) — —
Clock skew — USER_TOKEN_SKEW (Default 1m) USER_TOKEN_SKEW (Default 1m)
Group claim immer groups groups OIDC_GROUPS_CLAIM (Default groups)

Keycloak schreibt Gruppen oft unter identity_groups – dann OIDC_GROUPS_CLAIM entsprechend setzen. Die ExApp schreibt immer groups.