package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/thuongh2/go-security/authz"
"github.com/thuongh2/go-security/core"
"github.com/thuongh2/go-security/jwt"
"github.com/thuongh2/go-security/web/middleware"
"github.com/thuongh2/go-security/web/resource"
)
func main() {
// (1) Demo verifier: a single static HS256 key. The same secret is used by
// the /dev/token helper to mint tokens.
secret := []byte("demo-only-hs256-secret-change-me-32bytes!!")
signer, err := jwt.NewSigner(jwt.NewHS256(secret), "demo-key")
if err != nil {
log.Fatalf("signer: %v", err)
}
verifier, err := jwt.NewVerifierKey(jwt.HS256, jwt.NewHS256(secret))
if err != nil {
log.Fatalf("verifier: %v", err)
}
// (2) Claim validation: require exp; pin issuer/audience as a real
// deployment would. The default 60s clock skew applies.
validator := jwt.NewValidator(
jwt.WithRequiredIssuer("https://demo.go-security.local/"),
jwt.WithRequiredAudience("api://account-service"),
)
auth := resource.NewBearerTokenAuthenticator(verifier, validator, resource.DefaultClaimsMapper())
// (3) Authorization: scope -> SCOPE_* authorities via the default claims mapper.
authzMW, err := authz.Routes(
authz.Match("/api/admin/**").HasAuthority(core.Authority("SCOPE_admin")),
authz.Match("/api/**").HasAuthority(core.Authority("SCOPE_read")),
authz.Match("/dev/**").PermitAll(),
authz.AnyRequest().DenyAll(),
)
if err != nil {
log.Fatalf("authz routes: %v", err)
}
mux := http.NewServeMux()
mux.HandleFunc("/api/me", func(w http.ResponseWriter, r *http.Request) {
a := core.MustAuthentication(r.Context())
fmt.Fprintf(w, "name=%s authorities=%v\n", a.Name(), a.Authorities())
})
mux.HandleFunc("/api/admin/panel", func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, "admin: you hold SCOPE_admin")
})
// /dev/token mints a demo token so the example is runnable without an IdP.
// This endpoint exists ONLY for the demo and must never ship in production.
mux.HandleFunc("/dev/token", func(w http.ResponseWriter, r *http.Request) {
scope := r.URL.Query().Get("scope")
if scope == "" {
scope = "read"
}
now := time.Now()
token, err := jwt.EncodeRegistered(signer, jwt.RegisteredClaims{
Issuer: "https://demo.go-security.local/",
Subject: "demo-user",
Audience: jwt.Audience{"api://account-service"},
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)),
}, jwt.MapClaims{"scope": scope})
if err != nil {
http.Error(w, "mint failed", http.StatusInternalServerError)
return
}
fmt.Fprint(w, token)
})
// (4) Compose: headers -> RequireBearer (authenticate) -> authz (authorize).
// RequireBearer is scoped to /api/** so the public /dev/token mint endpoint
// is reachable without a token.
bearer := onlyFor(
middleware.Matcher("/api/**"),
resource.RequireBearer(auth, resource.WithEntryPoint(resource.BearerEntryPoint{Realm: "account-service"})),
)
chain := middleware.New(
middleware.SecurityHeaders(),
bearer,
authzMW,
)
handler := chain.Then(mux)
addr := ":8080"
log.Printf("listening on %s", addr)
if err := http.ListenAndServe(addr, handler); err != nil {
log.Fatal(err)
}
}
// onlyFor applies mw only to requests matching m; non-matching requests bypass
// mw entirely.
func onlyFor(m middleware.RequestMatcher, mw middleware.Middleware) middleware.Middleware {
return func(next http.Handler) http.Handler {
wrapped := mw(next)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if m.Matches(r) {
wrapped.ServeHTTP(w, r)
return
}
next.ServeHTTP(w, r)
})
}
}