key-cape/src/internal/server/oidc/session.go

88 lines
2.2 KiB
Go
Raw Normal View History

package oidc
import (
"crypto/rand"
"encoding/base64"
"sync"
"time"
)
// PKCESession stores the in-flight authorization state server-side.
type PKCESession struct {
Code string
ClientID string
RedirectURI string
KEY-WP-0005-T01: IAM Profile core claims for the human PKCE flow 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>
2026-07-24 00:03:31 +02:00
PKCEChallenge string // S256 challenge
PKCEChallengeMethod string // always "S256"
State string
Nonce string
KEY-WP-0005-T01: IAM Profile core claims for the human PKCE flow 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>
2026-07-24 00:03:31 +02:00
Username string // set after auth
Scopes []string
ExpiresAt time.Time
KEY-WP-0005-T01: IAM Profile core claims for the human PKCE flow 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>
2026-07-24 00:03:31 +02:00
// MFAVerified records whether this authorization actually required and
// passed MFA validation (vs. MFA not being required for this user at
// all). Feeds the assurance claim's level in token.go
// (KEY-WP-0005-T01): aal2 when true, aal1 when false. Set once, at
// completeAuthorization -- never re-derived from stale enrollment state
// at token-exchange time.
MFAVerified bool
}
// SessionStore is an in-memory PKCE session store.
type SessionStore struct {
mu sync.Mutex
sessions map[string]*PKCESession // keyed by code
}
// NewSessionStore returns an initialised, empty SessionStore.
func NewSessionStore() *SessionStore {
return &SessionStore{
sessions: make(map[string]*PKCESession),
}
}
// Create stores the session and returns the generated authorization code.
func (s *SessionStore) Create(sess *PKCESession) string {
code := generateCode()
sess.Code = code
s.mu.Lock()
s.sessions[code] = sess
s.mu.Unlock()
return code
}
// Get retrieves a session by code. Returns false if not found or expired.
func (s *SessionStore) Get(code string) (*PKCESession, bool) {
s.mu.Lock()
sess, ok := s.sessions[code]
s.mu.Unlock()
if !ok {
return nil, false
}
if time.Now().After(sess.ExpiresAt) {
s.Delete(code)
return nil, false
}
return sess, true
}
// Delete removes a session by code. No-op if the code is not present.
func (s *SessionStore) Delete(code string) {
s.mu.Lock()
delete(s.sessions, code)
s.mu.Unlock()
}
// generateCode returns a cryptographically random, URL-safe string suitable
// for use as an authorization code.
func generateCode() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic("oidc: failed to generate random code: " + err.Error())
}
return base64.RawURLEncoding.EncodeToString(b)
}