Files
Commilitia-Drop/internal/jwtauth/claims.go
T

87 lines
3.2 KiB
Go

package jwtauth
import (
"context"
"strings"
)
// Claims is the authenticated identity for a request. In prod it is sourced from the
// Auth Broker at the edge: broker /verify authenticates the caller and injects X-Auth-*
// headers this process trusts (Caddy strips any client-supplied X-Auth-* at the trust
// boundary, so only the broker can set them). In dev it is synthesised from the dev token.
type Claims struct {
UserID string // X-Auth-Subject (Casdoor sub)
Name string // X-Auth-Name (display name); may be empty
Avatar string // X-Auth-Avatar (profile picture URL from the broker account); may be empty
Groups []string // X-Auth-Roles, comma-split
// Scope is the raw X-Auth-Scope: a global SSO user is "full"; a cdrop delegated
// session is "app:commilitia-drop:<tier>". Tier() reads the capability grade off the end.
Scope string
// DeviceID is X-Auth-Meta: the cdrop device_id this session was minted for — the
// join key to the devices row. Empty for an unmanaged caller (e.g. a global SSO
// browser that never paired through cdrop).
DeviceID string
}
// ScopeTier returns the capability grade — the last colon-separated segment of a broker
// scope ("app:commilitia-drop:guest" → "guest", "full" → "full"). A tierless scope is its own tier.
// Shared by Claims.Tier() and the session-list overlay so the two never diverge.
func ScopeTier(scope string) string {
if i := strings.LastIndex(scope, ":"); i >= 0 {
return scope[i+1:]
}
return scope
}
// Tier returns the capability grade off the caller's scope (see ScopeTier).
func (c *Claims) Tier() string {
return ScopeTier(c.Scope)
}
// Guest reports whether these claims came from a restricted guest session (a
// scan-login borrow). Guest sessions can transfer files but not manage devices,
// approve other devices, or mint long-lived tokens.
func (c *Claims) Guest() bool { return c.Tier() == "guest" }
type ctxKey int
const (
claimsCtxKey ctxKey = iota
deviceCtxKey
deviceTypeCtxKey
)
func ClaimsFromContext(ctx context.Context) (*Claims, bool) {
c, ok := ctx.Value(claimsCtxKey).(*Claims)
return c, ok
}
// ContextWithClaims attaches claims to a context — the inverse of ClaimsFromContext.
// The auth middleware uses this; it is also the seam handlers and tests use to inject
// claims directly.
func ContextWithClaims(ctx context.Context, c *Claims) context.Context {
return context.WithValue(ctx, claimsCtxKey, c)
}
func DeviceNameFromContext(ctx context.Context) (string, bool) {
n, ok := ctx.Value(deviceCtxKey).(string)
return n, ok
}
// ContextWithDeviceName attaches a device name to the context — the inverse of
// DeviceNameFromContext. Used by tests that exercise handlers directly without
// running the full auth middleware.
func ContextWithDeviceName(ctx context.Context, name string) context.Context {
return context.WithValue(ctx, deviceCtxKey, name)
}
// DeviceTypeFromContext returns the client-declared device type set by the auth
// middleware (browser / macos / windows / linux / ios), defaulting to "browser".
func DeviceTypeFromContext(ctx context.Context) string {
t, ok := ctx.Value(deviceTypeCtxKey).(string)
if !ok || t == "" {
return "browser"
}
return t
}