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
|
|
@ -190,7 +190,7 @@ func newTestServer(t *testing.T) *TestServer {
|
|||
ClientConfig: clients,
|
||||
Sessions: sessions,
|
||||
Users: usersMock,
|
||||
SigningKey: privateKey,
|
||||
SigningKey: privateKey,
|
||||
Issuer: issuer,
|
||||
TokenLifetime: 15 * time.Minute,
|
||||
Emitter: emitter,
|
||||
|
|
@ -199,10 +199,10 @@ func newTestServer(t *testing.T) *TestServer {
|
|||
|
||||
// Userinfo handler.
|
||||
mux.Handle("/userinfo", &oidc.UserinfoHandler{
|
||||
Users: usersMock,
|
||||
Users: usersMock,
|
||||
SigningKey: &privateKey.PublicKey,
|
||||
Issuer: issuer,
|
||||
Emitter: emitter,
|
||||
Issuer: issuer,
|
||||
Emitter: emitter,
|
||||
})
|
||||
|
||||
// Healthz handler.
|
||||
|
|
@ -622,7 +622,12 @@ func TestCompleteTokenFlow(t *testing.T) {
|
|||
t.Fatalf("parse JWT claims: %v", err)
|
||||
}
|
||||
|
||||
requiredClaims := []string{"iss", "sub", "aud", "exp", "iat"}
|
||||
requiredClaims := []string{
|
||||
"iss", "sub", "aud", "exp", "iat",
|
||||
// IAM Profile v0.3 core claims (KEY-WP-0005-T01) -- required on
|
||||
// every production token, not scope-gated.
|
||||
"tenant", "principal_type", "groups", "roles", "assurance",
|
||||
}
|
||||
for _, c := range requiredClaims {
|
||||
if _, ok := claims[c]; !ok {
|
||||
t.Errorf("JWT missing claim %q", c)
|
||||
|
|
@ -632,4 +637,37 @@ func TestCompleteTokenFlow(t *testing.T) {
|
|||
if claims["aud"] != "demo-app" {
|
||||
t.Errorf("aud: want demo-app, got %v", claims["aud"])
|
||||
}
|
||||
|
||||
// testuser has no explicit Tenant assignment -- falls back to the
|
||||
// platform default (this workstation's single tenant, ADR-0006).
|
||||
if claims["tenant"] != "tenant:coulomb" {
|
||||
t.Errorf("tenant: want tenant:coulomb, got %v", claims["tenant"])
|
||||
}
|
||||
if claims["principal_type"] != "human" {
|
||||
t.Errorf("principal_type: want human, got %v", claims["principal_type"])
|
||||
}
|
||||
groups, ok := claims["groups"].([]interface{})
|
||||
if !ok || len(groups) != 1 || groups[0] != "developers" {
|
||||
t.Errorf("groups: want [developers], got %v", claims["groups"])
|
||||
}
|
||||
roles, ok := claims["roles"].([]interface{})
|
||||
if !ok || len(roles) != 0 {
|
||||
t.Errorf("roles: want [] (testuser has none), got %v", claims["roles"])
|
||||
}
|
||||
assurance, ok := claims["assurance"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("assurance: want an object, got %v", claims["assurance"])
|
||||
}
|
||||
// This flow's mockMFA has required: false -- MFA was never performed,
|
||||
// so assurance must reflect aal1, not aal2, regardless of the user's
|
||||
// static MFA enrollment state.
|
||||
if assurance["level"] != "aal1" {
|
||||
t.Errorf("assurance.level: want aal1 (MFA not required in this flow), got %v", assurance["level"])
|
||||
}
|
||||
if assurance["mfa"] != false {
|
||||
t.Errorf("assurance.mfa: want false, got %v", assurance["mfa"])
|
||||
}
|
||||
if assurance["source"] != "key-cape" {
|
||||
t.Errorf("assurance.source: want key-cape, got %v", assurance["source"])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue