Document every exported symbol and how callers use the library.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+179
@@ -0,0 +1,179 @@
|
||||
# API
|
||||
|
||||
Generated from the package comment and every exported declaration. Do not edit.
|
||||
Regenerate with `UPDATE_API_DOCS=1 go test -run TestAPIDoc -count=1`.
|
||||
|
||||
## Package usertoken
|
||||
|
||||
Package usertoken mints and checks the short-lived RS256 user token an ExApp sends to Microservices.
|
||||
|
||||
An ExApp calls [NewSignerFromEnv](#newsignerfromenv) and [Signer.Mint](#signermint) after AppAPI has named the user. A Microservice calls [FromEnv](#fromenv) and [Auth.Middleware](#authmiddleware) or [Auth.UnaryServerInterceptor](#authunaryserverinterceptor). One process trusts either a static public key or an OIDC issuer, not both.
|
||||
|
||||
The procedure, claims, and environment variables are in knowledge/platforms/nextcloud/exapps/authentication.md.
|
||||
|
||||
## Variables
|
||||
|
||||
### ErrUnauthorized
|
||||
|
||||
```go
|
||||
var ErrUnauthorized = errors.New("unauthorized")
|
||||
```
|
||||
|
||||
ErrUnauthorized is wrapped by every failed token check. Startup and configuration failures do not wrap it.
|
||||
|
||||
## Functions
|
||||
|
||||
### BearerFromContext
|
||||
|
||||
```go
|
||||
func BearerFromContext(ctx context.Context) (string, bool)
|
||||
```
|
||||
|
||||
BearerFromContext returns the raw JWT [Auth.Middleware](#authmiddleware) stored, without the "Bearer " prefix. Outbound calls send "Bearer " plus this string.
|
||||
|
||||
## Types
|
||||
|
||||
### Auth
|
||||
|
||||
```go
|
||||
type Auth struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
Auth verifies bearers for one Microservice process.
|
||||
|
||||
#### FromEnv
|
||||
|
||||
```go
|
||||
func FromEnv(ctx context.Context) (*Auth, error)
|
||||
```
|
||||
|
||||
FromEnv reads the process environment and builds an Auth. Set USER\_TOKEN\_PUBLIC\_KEY\_FILE or OIDC\_ISSUER, not both and not neither. Static mode also reads USER\_TOKEN\_ISSUER and USER\_TOKEN\_AUDIENCE. OIDC mode reads OIDC\_AUDIENCE and optional OIDC\_GROUPS\_CLAIM. Optional USER\_TOKEN\_SKEW is a time.Duration; empty uses 1 minute. It returns an error when the mode is ambiguous, the key file is unreadable, USER\_TOKEN\_SKEW cannot be parsed, or OIDC discovery fails. ctx is used for OIDC discovery.
|
||||
|
||||
#### New
|
||||
|
||||
```go
|
||||
func New(ctx context.Context, cfg Config) (*Auth, error)
|
||||
```
|
||||
|
||||
New builds an Auth from explicit configuration. ctx is used for OIDC discovery.
|
||||
|
||||
#### Auth.Middleware
|
||||
|
||||
```go
|
||||
func (a *Auth) Middleware() func(http.Handler) http.Handler
|
||||
```
|
||||
|
||||
Middleware requires a bearer on every path except /health. Success stores the Caller and the raw JWT on the request context.
|
||||
|
||||
#### Auth.UnaryServerInterceptor
|
||||
|
||||
```go
|
||||
func (a *Auth) UnaryServerInterceptor() grpc.UnaryServerInterceptor
|
||||
```
|
||||
|
||||
UnaryServerInterceptor requires a bearer on every RPC. Success stores the Caller and the raw JWT on the handler context, same as [Auth.Middleware](#authmiddleware). Native clients send metadata key "authorization". The HTTP gateway's forwarded header is "grpcgateway-authorization".
|
||||
|
||||
#### Auth.Verify
|
||||
|
||||
```go
|
||||
func (a *Auth) Verify(ctx context.Context, raw string) (Caller, error)
|
||||
```
|
||||
|
||||
Verify checks a raw JWT. raw has no "Bearer " prefix. A failed check wraps ErrUnauthorized. Startup and configuration failures do not.
|
||||
|
||||
### Caller
|
||||
|
||||
```go
|
||||
type Caller struct {
|
||||
Subject string
|
||||
Username string
|
||||
Groups []string
|
||||
}
|
||||
```
|
||||
|
||||
Caller is the user a verified token names. Groups is nil when the token omitted the claim, and non-nil (possibly empty) when the claim was present.
|
||||
|
||||
#### CallerFromContext
|
||||
|
||||
```go
|
||||
func CallerFromContext(ctx context.Context) (Caller, bool)
|
||||
```
|
||||
|
||||
CallerFromContext returns the user [Auth.Middleware](#authmiddleware) stored.
|
||||
|
||||
### Config
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
PublicKey *rsa.PublicKey
|
||||
Issuer string
|
||||
Audience string
|
||||
OIDCIssuer string
|
||||
OIDCAudience string
|
||||
GroupsClaim string
|
||||
Skew time.Duration
|
||||
}
|
||||
```
|
||||
|
||||
Config selects one verify mode. Set PublicKey for static RS256, or OIDCIssuer for discovery. Not both. Skew of zero uses 1 minute. GroupsClaim empty uses "groups" (OIDC only). Audience empty is allowed only for OIDC, where it skips the audience check.
|
||||
|
||||
### MintInput
|
||||
|
||||
```go
|
||||
type MintInput struct {
|
||||
Subject string
|
||||
Groups *[]string
|
||||
}
|
||||
```
|
||||
|
||||
MintInput is one user token. Groups nil omits the claim. A non-nil pointer includes it, including an empty list.
|
||||
|
||||
### SignConfig
|
||||
|
||||
```go
|
||||
type SignConfig struct {
|
||||
PrivateKey *rsa.PrivateKey
|
||||
Issuer string
|
||||
Audience string
|
||||
TTL time.Duration
|
||||
}
|
||||
```
|
||||
|
||||
SignConfig is the ExApp mint configuration. TTL of zero uses 5 minutes.
|
||||
|
||||
### Signer
|
||||
|
||||
```go
|
||||
type Signer struct {
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
|
||||
Signer mints RS256 user tokens. Only the ExApp should hold one.
|
||||
|
||||
#### NewSigner
|
||||
|
||||
```go
|
||||
func NewSigner(cfg SignConfig) (*Signer, error)
|
||||
```
|
||||
|
||||
NewSigner checks the key, issuer, and audience. It returns an error when PrivateKey is nil or issuer or audience is empty. A TTL of zero or less uses 5 minutes.
|
||||
|
||||
#### NewSignerFromEnv
|
||||
|
||||
```go
|
||||
func NewSignerFromEnv() (*Signer, error)
|
||||
```
|
||||
|
||||
NewSignerFromEnv reads USER\_TOKEN\_PRIVATE\_KEY\_FILE, USER\_TOKEN\_ISSUER, USER\_TOKEN\_AUDIENCE, and optional USER\_TOKEN\_TTL. It 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. An empty USER\_TOKEN\_TTL uses 5 minutes.
|
||||
|
||||
#### Signer.Mint
|
||||
|
||||
```go
|
||||
func (s *Signer) Mint(now time.Time, in MintInput) (string, error)
|
||||
```
|
||||
|
||||
Mint signs one RS256 token at now. Subject becomes both sub and preferred\_username. It returns an error when Subject is empty or the key cannot sign.
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
# go-usertoken guide
|
||||
|
||||
How to use the library. Symbol signatures and doc comments are in [API](api.md). Words for the domain are in [CONTEXT.md](../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.
|
||||
|
||||
```bash
|
||||
openssl genrsa -out user-token.key 2048
|
||||
openssl rsa -in user-token.key -pubout -out user-token.pub
|
||||
```
|
||||
|
||||
2. 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` |
|
||||
|
||||
3. Call `NewSignerFromEnv` at startup, or `NewSigner` with a `SignConfig`.
|
||||
4. After AppAPI has named the user, call `Signer.Mint` with the current time and a `MintInput`.
|
||||
5. 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
|
||||
|
||||
```go
|
||||
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` |
|
||||
|
||||
2. Call `FromEnv` with a context. The context is used for OIDC discovery. Or call `New` with a `Config` that sets either `PublicKey` or `OIDCIssuer`.
|
||||
3. Wrap handlers with `Auth.Middleware`. For gRPC, use `Auth.UnaryServerInterceptor`. Native clients send metadata `authorization`. The HTTP gateway's forwarded header is `grpcgateway-authorization`.
|
||||
4. Inside a handler, read `CallerFromContext` and `BearerFromContext`.
|
||||
5. 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
|
||||
|
||||
```go
|
||||
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
|
||||
```
|
||||
Reference in New Issue
Block a user