Auth (JWT)
pkg/auth/verifier verifies bearer JWTs against one or more trusted issuers and provides a *verifier.Registry into DI. The pkg/auth/fiber and pkg/auth/grpc adapters turn that registry into guard middleware that stashes a verified *verifier.Principal in the request context.
Add verifier.NewModule() to the runtime. It provides *verifier.Registry at modules.auth.verifier.<instance>; each configured issuer’s JWKS is fetched (or discovered via OIDC) and refreshed in the background.
lakta.NewRuntime( config.NewModule(config.WithConfigDirs(".", "./config"), config.WithArgs(os.Args[1:])), tint.NewModule(), slog.NewModule(), verifier.NewModule(), fiberserver.NewModule(fiberserver.WithRouterCtx(registerRoutes)),)modules: auth: verifier: default: roles_claim: "roles" # scope_claim defaults to "scope" issuers: - issuer: "https://accounts.example.com" audience: ["my-api"] algorithms: ["RS256"] # jwks_url omitted -> discovered from the issuer's OIDC document - issuer: "https://auth.internal" audience: ["my-api"] jwks_url: "https://auth.internal/.well-known/jwks.json" algorithms: ["ES256"] clock_skew: "30s"Each issuer’s audience and algorithms must be non-empty; alg:none is always rejected and clock_skew is capped at 5 minutes.
Static dev key
Section titled “Static dev key”static_key is an isolated HS256 path for local development. It only loads when LAKTA_PROFILE is set to one of its profiles (default dev, local, test); otherwise Init fails safe rather than trusting the secret. Never enable it in production.
# under modules.auth.verifier.default:static_key: secret: "dev-only-secret" profiles: ["dev", "local"]Protecting fiber routes
Section titled “Protecting fiber routes”WithRouterCtx gives the router the runtime context, so you can Invoke the registry and build the guard middleware. authfiber.New verifies the bearer token; compose RequireScope, RequireRole, RequireAny, or RequireAll after it. Use Optional to allow anonymous requests through.
func registerRoutes(ctx context.Context, app *fiber.App) { reg, err := lakta.Invoke[*verifier.Registry](ctx) if err != nil { panic(err) } authn, err := authfiber.New(reg, config.DefaultInstanceName) // errors only if reg is nil if err != nil { panic(err) }
items := app.Group("/items", authn) items.Get("/", listItems) items.Post("/", createItem, authfiber.RequireScope("items:write"))}Protecting gRPC methods
Section titled “Protecting gRPC methods”Build an interceptor with authgrpc.NewUnaryServerInterceptor and pass it via grpcserver.WithUnaryInterceptor. Wrap it in selector.UnaryServerInterceptor to skip anonymous methods. Enforce fine-grained access inside handlers with authgrpc.RequireScope / authgrpc.RequireRole.
// reg is the *verifier.Registry resolved from DI, as in the fiber example.interceptor, err := authgrpc.NewUnaryServerInterceptor(reg, config.DefaultInstanceName)if err != nil { return err}
grpcserver.NewModule( grpcserver.WithService(&v1.MyService_ServiceDesc, NewServer()), grpcserver.WithUnaryInterceptor(interceptor),)
func (s *MyServer) DeleteThing(ctx context.Context, req *pb.DeleteThingRequest) (*pb.DeleteThingResponse, error) { if err := authgrpc.RequireScope(ctx, "things:delete"); err != nil { return nil, err } // ...}Reading the principal
Section titled “Reading the principal”After a token verifies, the adapters stash a *verifier.Principal. Retrieve it with verifier.PrincipalFrom(ctx) — the same call works from fiber (c.Context()) and gRPC handlers.
p, ok := verifier.PrincipalFrom(c.Context())if !ok { // anonymous request (only reachable behind Optional)}// p.Subject, p.Issuer, p.Audience, p.Scopes, p.Roles, p.Claims (raw map), p.Tokenif p.HasRole("admin") { // ...}Connect
Section titled “Connect”pkg/auth/connect fills the connect module’s auth slot:
connect.WithSlot(connect.SlotAuth, authconnect.New( authconnect.WithPublicProcedures(func(procedure string) bool { return procedure == "/pkg.Svc/Public" }),))A missing token passes only for public procedures; an invalid token is always rejected. Handlers read the *verifier.Principal (with Session from sid and the self-redacting Raw token for forwarding) via verifier.PrincipalFrom(ctx). Every rejection emits a security_event log line with the internal reason (missing, expired, audience, …); the wire error stays an opaque UNAUTHENTICATED. Streams are cancelled when the token expires.
With an authorizer
Section titled “With an authorizer”authconnect.WithAuthorizer(a) wires the auth slot, the authz slot (a.Slot()) and a mounted-service hook (a.Mounted) in one option. When a also has IsPublic(procedure string) bool, those procedures accept calls without a token. Add connect.WithRequiredSlots(connect.SlotAuth, connect.SlotAuthz) so a missing authorizer, or a handler mounted outside the slots, fails Init:
connect.NewModule( authconnect.WithAuthorizer(authorizer), connect.WithRequiredSlots(connect.SlotAuth, connect.SlotAuthz), connect.WithService(registrar),)Lazy keys and readiness
Section titled “Lazy keys and readiness”Issuer keys are discovered and fetched in the background (or on the first request), so services start while the identity provider is down. Until the first fetch succeeds, requests fail with UNAVAILABLE and the readiness check (registered on *health.Health when present) fails. Later refresh failures keep the cached keys.
Production guards
Section titled “Production guards”Under any profile other than dev or test (including an unset one), the verifier refuses an empty issuer list, static_key, and non-HTTPS issuer or JWKS URLs. token_type: Bearer additionally requires the payload typ claim (Keycloak access tokens).
Configuration Reference
Section titled “Configuration Reference”Config path: modules.auth.verifier.<name>
issuersissuers.issuerissuer is matched against the token iss exactly (no prefix/substring)
issuers.audienceaudience MUST be non-empty; the token aud must intersect it
issuers.jwks_urlJWKSURL is the JWKS endpoint; empty triggers OIDC discovery from Issuer
issuers.algorithmsalgorithms is a hard allowlist, e.g. [RS256, ES256]; alg:none is rejected
issuers.clock_skewclockSkew is capped at maxClockSkew
issuers.token_typetokenType, when set, requires the payload claim "typ" to equal it
static_keystatic_key.algorithmLAKTA_MODULES__AUTH__VERIFIER__<NAME>__STATIC_KEY__ALGORITHMstatic_key.secretLAKTA_MODULES__AUTH__VERIFIER__<NAME>__STATIC_KEY__SECRETstatic_key.profilesLAKTA_MODULES__AUTH__VERIFIER__<NAME>__STATIC_KEY__PROFILESscope_claimLAKTA_MODULES__AUTH__VERIFIER__<NAME>__SCOPE_CLAIMroles_claimLAKTA_MODULES__AUTH__VERIFIER__<NAME>__ROLES_CLAIM