2026-03-13 01:56:57 +01:00
|
|
|
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"
|
2026-03-13 01:56:57 +01:00
|
|
|
State string
|
2026-06-01 21:20:54 +02:00
|
|
|
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
|
2026-03-13 01:56:57 +01:00
|
|
|
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
|
2026-03-13 01:56:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|