KEY-WP-0005-T01: IAM Profile core claims for the human PKCE flow
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m50s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m50s
Verified first: grant_types_supported advertises client_credentials in discovery.go, but token.go only ever accepted authorization_code -- no service-token issuance path exists at all. Building one from scratch is materially bigger than extending the existing flow; explicitly not attempted here, left open in the workplan rather than declared done. What shipped for the human Authorization Code + PKCE flow: - domain.User.Tenant (new, omitempty) + token.go's effectiveTenant(): falls back to tenant:coulomb (this workstation's actual tenant, ADR-0006) when unset -- never an empty tenant claim, never a silent reassignment. - principal_type: "human", unconditional. - groups/roles promoted from scope-gated to unconditional core claims, always [] not null when empty. One pre-existing test asserted the old scope-gated groups behavior -- updated to match the new intentional behavior, not left failing or reverted. - assurance built from PKCESession.MFAVerified (new field, threaded through completeAuthorization's two call sites in authorize.go) -- whether MFA was actually verified in this session, not static enrollment state. aal2 only when required-and-passed this time, aal1 otherwise. go build/vet clean, go test ./... green repo-wide. Two new authorize_test.go cases assert MFAVerified on both paths. tests/profile/profile_test.go's TestCompleteTokenFlow (the repo's own full HTTP integration test) extended with real value assertions for all five claims, not just presence checks. Python conformance tool not run against a live instance (needs the full Authelia+LLDAP+privacyIDEA stack); TestCompleteTokenFlow's real HTTP round trip covers the equivalent claim checks instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
e51a2d74e9
commit
f1f7fa9dd7
8 changed files with 271 additions and 32 deletions
|
|
@ -21,7 +21,7 @@ type TokenHandler struct {
|
|||
ClientConfig map[string]*domain.Client
|
||||
Sessions *SessionStore
|
||||
Users domain.UserRepository
|
||||
SigningKey *rsa.PrivateKey
|
||||
SigningKey *rsa.PrivateKey
|
||||
Issuer string
|
||||
TokenLifetime time.Duration
|
||||
Emitter telemetry.Emitter
|
||||
|
|
@ -126,9 +126,15 @@ func (h *TokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
if scopeSet["email"] {
|
||||
claims["email"] = user.Email
|
||||
}
|
||||
if scopeSet["groups"] {
|
||||
claims["groups"] = user.Groups
|
||||
}
|
||||
|
||||
// Core claims required by net-kingdom/canon/standards/iam-profile_v0.3.md
|
||||
// for every production token -- not scope-gated, unlike the recommended
|
||||
// human claims above (KEY-WP-0005-T01).
|
||||
claims["tenant"] = effectiveTenant(user)
|
||||
claims["principal_type"] = "human"
|
||||
claims["groups"] = nonNilStrings(user.Groups)
|
||||
claims["roles"] = nonNilStrings(user.Roles)
|
||||
claims["assurance"] = assuranceClaim(sess.MFAVerified, now)
|
||||
|
||||
// 7. Sign JWT with RSA-SHA256.
|
||||
kid := "key-1" // static kid for v0.1
|
||||
|
|
@ -165,6 +171,58 @@ func (h *TokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IAM Profile core claims (KEY-WP-0005-T01)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// defaultTenant is the fallback tenant claim for users with no explicit
|
||||
// Tenant assignment yet. This workstation currently operates a single
|
||||
// tenant (tenant:coulomb, ADR-0006); later tenants (e.g. tenant:friendly:binky,
|
||||
// ADR-0013) require an explicit domain.User.Tenant value -- this default
|
||||
// never silently assigns a user to a tenant other than the platform's
|
||||
// original one.
|
||||
const defaultTenant = "tenant:coulomb"
|
||||
|
||||
// effectiveTenant resolves the tenant claim for a user, falling back to
|
||||
// defaultTenant when the user has no explicit tenant assignment. The IAM
|
||||
// Profile requires a non-empty tenant claim on every token.
|
||||
func effectiveTenant(user *domain.User) string {
|
||||
if user.Tenant != "" {
|
||||
return user.Tenant
|
||||
}
|
||||
return defaultTenant
|
||||
}
|
||||
|
||||
// nonNilStrings returns s, or an empty (non-nil) slice if s is nil, so the
|
||||
// claim always serializes as `[]`, never `null` -- the profile requires
|
||||
// groups/roles to be present, "possibly empty", not absent.
|
||||
func nonNilStrings(s []string) []string {
|
||||
if s == nil {
|
||||
return []string{}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// assuranceClaim builds the profile's `assurance` object from whether MFA
|
||||
// was actually verified during this authorization (session.MFAVerified),
|
||||
// not from static enrollment state -- a user who has MFA enrolled but
|
||||
// wasn't challenged for it in this particular flow gets aal1, not aal2.
|
||||
func assuranceClaim(mfaVerified bool, at time.Time) map[string]interface{} {
|
||||
level := "aal1"
|
||||
methods := []string{"pwd"}
|
||||
if mfaVerified {
|
||||
level = "aal2"
|
||||
methods = append(methods, "otp")
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"level": level,
|
||||
"methods": methods,
|
||||
"mfa": mfaVerified,
|
||||
"source": "key-cape",
|
||||
"at": at.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PKCE verification
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue