goAuthly
Think of goAuthly as a bouncer. It doesn't throw the party. It just decides who gets in.
Lightweight, focused token and credential verification for Go services. Supports OAuth2 JWT, opaque tokens (RFC 7662), and Basic Authentication — with declarative and Lua-based claim policies.
Meet Schnallbert — the goAuthly mascot. He guards the gate so you don't have to.
What goAuthly Is
- A verification-only library — it validates tokens and credentials, nothing else.
- Supports JWT (via JWKS), opaque tokens (via introspection), and Basic Auth (bcrypt).
- Ships with thin adapters for gRPC, Fiber, and fasthttp.
- Offers declarative claim policies, Lua scripting, and actor validation (RFC 8693).
- Designed for production: no panics, constant-time comparisons, safe defaults.
What goAuthly Is NOT
- It is not an identity provider, OAuth2 server, or session manager.
- It does not issue tokens, manage users, or handle login flows.
- It does not include a logging framework — bring your own.
- It does not store secrets; you provide hashed passwords and endpoints.
Quick Start
go get github.com/keksclan/goAuthly
Run the end-to-end example locally:
go run ./examples/verify_demo
The example spins up a local HTTP server that serves a JWKS, implements introspection, mints a signed JWT, and demonstrates verifying both a JWT and an opaque token.
JWT Example
cfg := authly.Config{
Mode: authly.AuthModeOAuth2,
OAuth2: authly.OAuth2Config{
Mode: authly.OAuth2JWTOnly,
Issuer: "https://auth.example.com",
Audience: "my-api",
JWKSURL: "https://auth.example.com/.well-known/jwks.json",
},
}
engine, err := authly.New(cfg)
if err != nil {
log.Fatal(err)
}
result, err := engine.Verify(ctx, jwtToken)
if err != nil {
// token is invalid
}
fmt.Println(result.Subject, result.Claims)
Opaque Token Example
cfg := authly.Config{
Mode: authly.AuthModeOAuth2,
OAuth2: authly.OAuth2Config{
Mode: authly.OAuth2OpaqueOnly,
Introspection: authly.IntrospectionConfig{
Endpoint: "https://auth.example.com/introspect",
Auth: authly.ClientAuth{
Kind: authly.ClientAuthBasic,
ClientID: "my-client",
ClientSecret: "my-secret",
},
},
},
}
engine, err := authly.New(cfg)
result, err := engine.Verify(ctx, opaqueToken)
Basic Auth Example
import "golang.org/x/crypto/bcrypt"
hash, _ := bcrypt.GenerateFromPassword([]byte("s3cret"), bcrypt.DefaultCost)
cfg := authly.Config{
Mode: authly.AuthModeBasic,
BasicAuth: authly.BasicAuthConfig{
Enabled: true,
Users: map[string]string{
"admin": string(hash),
},
Realm: "MyAPI",
},
}
engine, err := authly.New(cfg)
result, err := engine.VerifyBasic(ctx, "admin", "s3cret")
// result.Type == "basic", result.Subject == "admin"
Custom Validator (e.g., database lookup)
cfg := authly.Config{
Mode: authly.AuthModeBasic,
BasicAuth: authly.BasicAuthConfig{
Enabled: true,
Validator: func(ctx context.Context, user, pass string) (bool, error) {
return myDB.CheckCredentials(ctx, user, pass)
},
},
}
When both Users and Validator are provided, Validator wins.
Mixed Mode Example
Use OAuth2 as the primary mode with Basic Auth also enabled:
cfg := authly.Config{
Mode: authly.AuthModeOAuth2,
OAuth2: authly.OAuth2Config{
Mode: authly.OAuth2JWTAndOpaque,
JWKSURL: "https://auth.example.com/.well-known/jwks.json",
Introspection: authly.IntrospectionConfig{
Endpoint: "https://auth.example.com/introspect",
},
},
BasicAuth: authly.BasicAuthConfig{
Enabled: true,
Users: map[string]string{"svc-account": hashedPassword},
},
}
engine, _ := authly.New(cfg)
// Use engine.Verify(ctx, token) for Bearer tokens
// Use engine.VerifyBasic(ctx, user, pass) for Basic Auth
Claim Policy Example
Policies: authly.Policies{
TokenClaims: authly.ClaimPolicy{
Required: []string{"sub", "iss"},
Denylist: []string{"password", "ssn"},
Allowlist: []string{"sub", "iss", "exp", "aud", "scope"},
EnforcedValues: map[string][]any{
"iss": {"https://auth.example.com"},
},
},
}
Type-specific policies override the shared TokenClaims:
Policies: authly.Policies{
JWTClaims: authly.ClaimPolicy{Required: []string{"sub", "exp"}},
OpaqueClaims: authly.ClaimPolicy{Required: []string{"sub", "client_id"}},
}
Lua Policy Example
Lua scripts run after declarative policies and enable conditional logic:
Policies: authly.Policies{
Lua: authly.LuaClaimsPolicy{
Enabled: true,
Script: `
if has("actor") then
require_claim("sub")
require_value("iss", "https://auth.example.com")
end
if token_type() == "opaque" then
require_claim("client_id")
end
`,
},
}
Available Lua functions: has(key), get(key), require_claim(key), require_value(key, val), require_one_of(key, {values}), reject(msg), token_type(), is_jwt(), is_opaque().
Adapter Examples
gRPC
import authlygrpc "github.com/keksclan/goAuthly/adapters/grpc"
server := grpc.NewServer(
grpc.UnaryInterceptor(authlygrpc.UnaryServerInterceptor(engine)),
grpc.StreamInterceptor(authlygrpc.StreamServerInterceptor(engine)),
)
// In your handler:
result := authlygrpc.ResultFromContext(ctx)
Fiber
import authlyfiber "github.com/keksclan/goAuthly/adapters/fiber"
app := fiber.New()
app.Use(authlyfiber.Middleware(engine))
app.Get("/protected", func(c *fiber.Ctx) error {
result := authlyfiber.ResultFromLocals(c)
return c.JSON(fiber.Map{"user": result.Subject})
})
fasthttp
import authlyfasthttp "github.com/keksclan/goAuthly/adapters/fasthttp"
handler := authlyfasthttp.Middleware(engine, func(ctx *fasthttp.RequestCtx) {
result := authlyfasthttp.ResultFromCtx(ctx)
ctx.WriteString("Hello, " + result.Subject)
})
fasthttp.ListenAndServe(":8080", handler)
All adapters support both Bearer and Basic authorization schemes automatically.
Security Notes
- Always set
Issuer, Audience, and AllowedAlgs to prevent token confusion attacks.
- Opaque token cache keys are SHA-256 hashed — raw tokens are never used as cache keys.
- Basic Auth passwords must be bcrypt hashes. Plaintext storage is a security violation.
- Constant-time comparison via bcrypt prevents timing attacks.
- Dummy bcrypt comparison on unknown usernames prevents user enumeration.
- No panics — all error paths return errors, never panic.
- See docs/security.md for the full security model.
- JWKS keys are cached with configurable TTL (default 15 min); stale keys can be served if refresh fails.
- Introspection responses are cached briefly (default 30s) to avoid hammering the IdP.
- Basic Auth with bcrypt is intentionally slow (~60ms per check at default cost) — this is a feature, not a bug.
- No background goroutines by default; add your own refresh loop if needed.
- See docs/performance.md for benchmarking tips.
Common Pitfalls
| Problem |
Cause |
Fix |
unsupported auth mode |
Wrong Mode or BasicAuth.Enabled not set |
Check Config.Mode matches your intent |
oauth2.jwks_url is required |
JWT mode without JWKS URL |
Set OAuth2.JWKSURL |
| Token rejected but looks valid |
Issuer/audience mismatch or clock skew |
Verify Issuer/Audience match your IdP; check server clock |
| Basic auth fails in production |
Plaintext password in Users map |
Use bcrypt.GenerateFromPassword |
| Introspection returns active but rejected |
Claim policy denying a claim |
Check Policies.TokenClaims and Lua script |
FAQ
Q: Can I use goAuthly to issue tokens?
A: No. goAuthly only verifies tokens and credentials. Use an identity provider for issuance.
Q: Is the Engine safe for concurrent use?
A: Yes. The Engine, default cache, and default HTTP client are all goroutine-safe.
Q: Can I mix JWT verification and Basic Auth?
A: Yes. Set Mode: AuthModeOAuth2 with BasicAuth.Enabled: true. Use Verify() for tokens and VerifyBasic() for credentials. The adapters handle this automatically.
Q: Why bcrypt and not argon2?
A: bcrypt is the minimum required hash. The custom Validator function lets you use any hash algorithm you prefer.
Q: Do I need to manage JWKS refresh?
A: No. The Engine caches JWKS keys automatically with configurable TTL. Set AllowStaleJWKS: true for resilience.
How to Contribute
See CONTRIBUTING.md for guidelines. In short:
- Fork the repo and create a feature branch.
- Write tests for any new functionality.
- Run
go test ./... and go vet ./... before submitting.
- Keep PRs focused — one feature or fix per PR.
- Follow existing code style and GoDoc conventions.
Examples
Full working example applications are available in the examples/ directory:
| Example |
Port |
Description |
| Fiber server |
:8081 |
Fiber v2 with JWT, opaque token, and Basic Auth routes |
| fasthttp server |
:8082 |
fasthttp with JWT, opaque token, and Basic Auth routes |
Each example includes a local mock JWKS and introspection server, RSA key generation at startup, and prints ready-to-use tokens to the console.
Running an example
cd examples/fiber-server
go run .
# or
cd examples/fasthttp-server
go run .
gRPC
There is no gRPC example in this repository. For gRPC usage, see the dedicated gRPC example project:
https://github.com/keksclan/goAuthly-grpc-example
Documentation
| Document |
Description |
| Architecture |
High-level flows, Mermaid diagrams |
| Configuration |
Go, Lua, and JSON config examples |
| Basic Auth |
Hashed passwords, custom validators, mixed deployments |
| Adapters |
gRPC, Fiber, fasthttp integration |
| Security |
Threat model, timing attacks, production config |
| Advanced Claims |
Declarative policies, Lua scripting, actor claims |
| Troubleshooting |
Common issues and debug steps |
| Performance |
Caching, hot paths, benchmarking |
License
See LICENSE.