package xguard import ( "context" "errors" "fmt" "net/http" "slices" "strings" "time" ) // AccessToken: access token JWT dari X-Guard yang sudah diverifikasi. Dipakai bila frontend login ke X-Guard lalu // memanggil API Go dengan header Authorization: Bearer . type AccessToken struct { Subject string ClientID string // aplikasi yang meminta token Scopes []string // Audience (klaim aud): aplikasi peminta dan API tujuan yang diminta. API yang menerima token dari aplikasi lain // sebaiknya mewajibkan audience miliknya (HasAudience). Audience []string ExpiresAt time.Time // Org: slug organisasi X-Guard pengguna. API yang melayani banyak organisasi wajib memeriksanya. Org string } func (t *AccessToken) HasScope(s string) bool { return slices.Contains(t.Scopes, s) } func (t *AccessToken) HasAudience(a string) bool { return slices.Contains(t.Audience, a) } // VerifyAccessToken memeriksa tanda tangan (JWKS X-Guard), iss, exp, dan bahwa // token diterbitkan untuk salah satu allowedClients (default: ClientID sendiri). func (c *Client) VerifyAccessToken(ctx context.Context, raw string, allowedClients ...string) (*AccessToken, error) { tok, err := c.jwtVerify.Verify(c.ctx(ctx), raw) if err != nil { return nil, fmt.Errorf("xguard: access token: %w", err) } if tok.Expiry.IsZero() || time.Now().After(tok.Expiry) { return nil, errors.New("xguard: access token kedaluwarsa") } var cl struct { ClientID string `json:"client_id"` Scp []string `json:"scp"` Org string `json:"org"` } if err := tok.Claims(&cl); err != nil { return nil, fmt.Errorf("xguard: access token: %w", err) } if len(allowedClients) == 0 { allowedClients = []string{c.cfg.ClientID} } if !slices.Contains(allowedClients, cl.ClientID) { return nil, fmt.Errorf("xguard: access token untuk client %q, bukan client yang diizinkan", cl.ClientID) } return &AccessToken{Subject: tok.Subject, ClientID: cl.ClientID, Scopes: cl.Scp, Audience: tok.Audience, ExpiresAt: tok.Expiry, Org: cl.Org}, nil } type accessTokenKey struct{} // RequireAccessToken: middleware yang menolak request tanpa access token X-Guard // yang valid (401). Token tersedia lewat AccessTokenFrom(r.Context()). func (c *Client) RequireAccessToken(next http.Handler, allowedClients ...string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { raw, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ") if !ok || raw == "" { w.Header().Set("WWW-Authenticate", `Bearer realm="x-guard"`) http.Error(w, "unauthorized", http.StatusUnauthorized) return } at, err := c.VerifyAccessToken(r.Context(), raw, allowedClients...) if err != nil { w.Header().Set("WWW-Authenticate", `Bearer realm="x-guard", error="invalid_token"`) http.Error(w, "unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), accessTokenKey{}, at))) }) } // AccessTokenFrom mengambil token yang sudah diverifikasi RequireAccessToken. func AccessTokenFrom(ctx context.Context) (*AccessToken, bool) { at, ok := ctx.Value(accessTokenKey{}).(*AccessToken) return at, ok }