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.
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.
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:
- User-Id aus AppAPI.
- Bei Bedarf Gruppen laden (
Groups.UserGroupsin go-nc-exapp). - Einmal
Mintfür diesen Request. Authorization: Bearer <token>an jeden Microservice-Aufruf.
| 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 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.
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.
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.
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.
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
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)
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()).
| 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.