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:cdrop:". 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:cdrop: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 } // 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 }