112 lines
3.1 KiB
Go
112 lines
3.1 KiB
Go
// Package identity extracts authenticated user identity from JWT claims
|
|||
|
|
// and injects forwarding headers into proxied requests.
|
||
|
|
//
|
||
|
|
// Headers injected after JWT validation:
|
||
|
|
//
|
||
|
|
// X-Forwarded-User: subject (sub claim)
|
||
|
|
// X-Forwarded-Roles: comma-separated roles or permissions
|
||
|
|
// X-Acting-Service: authorized party (azp claim), only for service accounts
|
||
|
|
// X-Auth-Verified: "true" when gateway validated the JWT
|
||
|
|
//
|
||
|
|
// Security contract: downstream services MUST only accept traffic from the
|
||
|
|
// gateway (enforced by NetworkPolicy). They trust these headers because the
|
||
|
|
// gateway is the sole ingress path.
|
||
|
|
package identity
|
||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/golang-jwt/jwt/v5"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Headers that the gateway controls. Incoming values from clients are
|
||
|
|
// stripped to prevent spoofing.
|
||
|
|
const (
|
||
|
|
HeaderUser = "X-Forwarded-User"
|
||
|
|
HeaderRoles = "X-Forwarded-Roles"
|
||
|
|
HeaderActingService = "X-Acting-Service"
|
||
|
|
HeaderAuthVerified = "X-Auth-Verified"
|
||
|
|
)
|
||
|
|
|
||
|
|
// managed lists all headers this package owns. Used for stripping and cleanup.
|
||
|
|
var managed = []string{
|
||
|
|
HeaderUser,
|
||
|
|
HeaderRoles,
|
||
|
|
HeaderActingService,
|
||
|
|
HeaderAuthVerified,
|
||
|
|
}
|
||
|
|
|
||
|
|
// StripIncoming removes all gateway-managed identity headers from an
|
||
|
|
// inbound request, preventing clients from spoofing identity.
|
||
|
|
// Call this early in the handler chain, before any routing.
|
||
|
|
func StripIncoming(r *http.Request) {
|
||
|
|
for _, h := range managed {
|
||
|
|
r.Header.Del(h)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Inject extracts identity from validated JWT claims and sets the
|
||
|
|
// corresponding forwarding headers on the request. Only call this
|
||
|
|
// after successful JWT validation.
|
||
|
|
func Inject(r *http.Request, claims jwt.MapClaims) {
|
||
|
|
r.Header.Set(HeaderAuthVerified, "true")
|
||
|
|
|
||
|
|
if sub := claimString(claims, "sub"); sub != "" {
|
||
|
|
r.Header.Set(HeaderUser, sub)
|
||
|
|
}
|
||
|
|
|
||
|
|
if roles := claimStringSlice(claims, "roles"); len(roles) > 0 {
|
||
|
|
r.Header.Set(HeaderRoles, strings.Join(roles, ","))
|
||
|
|
} else if perms := claimStringSlice(claims, "permissions"); len(perms) > 0 {
|
||
|
|
r.Header.Set(HeaderRoles, strings.Join(perms, ","))
|
||
|
|
}
|
||
|
|
|
||
|
|
if azp := claimString(claims, "azp"); azp != "" {
|
||
|
|
sub := claimString(claims, "sub")
|
||
|
|
// Only set acting-service when azp differs from sub
|
||
|
|
// (i.e., a service account acting, not the user themselves)
|
||
|
|
if azp != sub {
|
||
|
|
r.Header.Set(HeaderActingService, azp)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// claimString extracts a string value from claims, returning "" if
|
||
|
|
// the key is missing or not a string.
|
||
|
|
func claimString(claims jwt.MapClaims, key string) string {
|
||
|
|
val, ok := claims[key]
|
||
|
|
if !ok || val == nil {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
s, ok := val.(string)
|
||
|
|
if !ok {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
|
||
|
|
// claimStringSlice extracts a []string from claims. JWT libraries
|
||
|
|
// deserialize JSON arrays as []interface{}, so each element is
|
||
|
|
// type-asserted individually. Non-string elements are skipped.
|
||
|
|
func claimStringSlice(claims jwt.MapClaims, key string) []string {
|
||
|
|
val, ok := claims[key]
|
||
|
|
if !ok || val == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
raw, ok := val.([]interface{})
|
||
|
|
if !ok {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
out := make([]string, 0, len(raw))
|
||
|
|
for _, v := range raw {
|
||
|
|
if s, ok := v.(string); ok && s != "" {
|
||
|
|
out = append(out, s)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(out) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|