87 lines
3.2 KiB
Go
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
|
|
}
|