Files

5.0 KiB

go-usertoken guide

How to use the library. Symbol signatures and doc comments are in API. Words for the domain are in CONTEXT.md. Claim layout and the full environment list are in Knowledge platforms/nextcloud/exapps/authentication.md.

ExApp

Purpose

The ExApp mints a short-lived RS256 token for the user AppAPI already named. Only the ExApp holds the private key. Microservices hold the matching public key, or they trust an OIDC issuer instead.

When to use it

Mint a token when the ExApp calls a Microservice on behalf of the requesting user. Do not mint inside a Microservice.

Call sequence

  1. Generate an RSA key once and keep the private key on the ExApp. Copy user-token.pub to each Microservice that uses static mode.
openssl genrsa -out user-token.key 2048
openssl rsa -in user-token.key -pubout -out user-token.pub
  1. Set the ExApp environment:
Variable Role
USER_TOKEN_PRIVATE_KEY_FILE RSA private key PEM (PKCS#1 or PKCS#8)
USER_TOKEN_ISSUER iss string
USER_TOKEN_AUDIENCE aud string
USER_TOKEN_TTL optional, default 5m
  1. Call NewSignerFromEnv at startup, or NewSigner with a SignConfig.
  2. After AppAPI has named the user, call Signer.Mint with the current time and a MintInput.
  3. Send Authorization: Bearer plus the returned string.

MintInput.Subject becomes both sub and preferred_username. Groups nil omits the groups claim. A non-nil pointer includes the claim, including an empty list.

A SignConfig.TTL of zero uses 5 minutes, same as an empty USER_TOKEN_TTL.

Errors

NewSignerFromEnv returns an error when the private-key path is empty, the PEM is not an RSA key, USER_TOKEN_TTL is set and is not a time.Duration, or issuer or audience is empty.

NewSigner returns an error when the private key is nil or issuer or audience is empty.

Mint returns an error when the subject is empty or the key cannot sign. Configuration failures are not ErrUnauthorized. That sentinel is for failed checks on the Microservice side.

Example

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

Microservice

Purpose

A Microservice checks the bearer and stores the caller on the request context. One process uses either a static public key or an OIDC issuer.

When to use it

Wrap HTTP handlers, or install the gRPC interceptor, at process start. Forward the same raw JWT when this service calls another.

Call sequence

  1. Set exactly one mode.

Static public key (tokens this ExApp minted):

Variable Role
USER_TOKEN_PUBLIC_KEY_FILE RSA public key PEM
USER_TOKEN_ISSUER expected iss
USER_TOKEN_AUDIENCE expected aud
USER_TOKEN_SKEW optional, default 1m

OIDC issuer:

Variable Role
OIDC_ISSUER issuer URL that serves discovery
OIDC_AUDIENCE expected aud; empty skips the audience check
OIDC_GROUPS_CLAIM group array claim, default groups
USER_TOKEN_SKEW optional, default 1m
  1. Call FromEnv with a context. The context is used for OIDC discovery. Or call New with a Config that sets either PublicKey or OIDCIssuer.
  2. Wrap handlers with Auth.Middleware. For gRPC, use Auth.UnaryServerInterceptor. Native clients send metadata authorization. The HTTP gateway's forwarded header is grpcgateway-authorization.
  3. Inside a handler, read CallerFromContext and BearerFromContext.
  4. On an outbound call, set Authorization to Bearer plus the raw string from BearerFromContext.

/health is not authenticated. Any other HTTP path without a valid bearer is 401. A gRPC call without a valid bearer is Unauthenticated.

Caller.Groups is nil when the token omitted the claim, and non-nil (possibly empty) when the claim was present.

Errors

FromEnv and New return an error when both modes are set, when neither mode is set, when the public key file is unreadable, when USER_TOKEN_SKEW cannot be parsed, or when OIDC discovery fails. Those are configuration errors. They do not wrap ErrUnauthorized.

Verify returns an error that wraps ErrUnauthorized when the token is missing, expired, signed by the wrong key, or has the wrong issuer or audience. Middleware and UnaryServerInterceptor turn that into 401 or Unauthenticated and do not call the handler.

An empty OIDCAudience skips the audience check. Static mode always requires an audience.

Example

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

// inside a 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