-
Notifications
You must be signed in to change notification settings - Fork 3
add saml support (tested with okta saml) #163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
willesq
wants to merge
24
commits into
thand-io:main
Choose a base branch
from
willesq:saml-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 19 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
c18f23f
add saml support (tested with okta saml)
willesq d6e813c
update example
willesq 3c17a1d
split out callback funcs
willesq c0ca99f
add saml tests
willesq 442853b
update doc
willesq 0d9e65a
add diff from hugh
willesq 15f0614
update tests
willesq fb8b27d
Fix: Add missing go.sum entries for test module
willesq f444814
Address Copilot review comments for SAML PR
willesq 215e737
fix import
willesq e75fc0f
update swagger docs
willesq eefb998
Address additional Copilot review comments for SAML PR
willesq 2a6c182
update saml example
willesq b8300b2
Merge branch 'refs/heads/main' into saml-support
willesq 32e042b
address cursor comment
willesq 53d4142
Add core security infrastructure for SAML authentication
willesq 6e0502f
Fix critical SAML authentication vulnerabilities
willesq 067371a
Prevent session fixation and add CSRF protection to auth callbacks
willesq 5780dcc
Add security components to Server struct
willesq f31c206
Fix circular import dependency in SAML provider
willesq ac21745
Initialize SAML security components and middleware
willesq 659aa43
Fix SAML config parsing - add missing allow_idp_initiated and sessionβ¦
willesq 0bd13f1
Replace custom security implementations with battle-tested packages
willesq cb9db5e
csrf middleware integration tests
willesq File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| package daemon | ||
|
|
||
| import ( | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/sirupsen/logrus" | ||
| ) | ||
|
|
||
| // AssertionCache implements in-memory cache for SAML assertion ID replay protection. | ||
| // It provides thread-safe tracking of used assertion IDs to prevent replay attacks | ||
| // where an attacker captures a valid SAML assertion and attempts to reuse it. | ||
| type AssertionCache struct { | ||
| cache sync.Map // map[string]*assertionEntry | ||
| ttl time.Duration // Time-to-live for cached assertions | ||
| cleanupTicker *time.Ticker // Periodic cleanup ticker | ||
| stopCleanup chan struct{} // Channel to stop cleanup goroutine | ||
| } | ||
|
|
||
| // assertionEntry represents a cached assertion with its timing information | ||
| type assertionEntry struct { | ||
| addedAt time.Time // When the assertion was first cached | ||
| expiry time.Time // When the assertion entry expires | ||
| } | ||
|
|
||
| // NewAssertionCache creates a new assertion cache with the specified TTL and cleanup interval. | ||
| // The TTL should match the typical validity window of SAML assertions (usually 5 minutes). | ||
| // The cleanup interval determines how often expired entries are removed from memory. | ||
| func NewAssertionCache(ttl time.Duration, cleanupInterval time.Duration) *AssertionCache { | ||
| if ttl == 0 { | ||
| ttl = 5 * time.Minute // Default TTL matches typical SAML assertion validity | ||
| } | ||
| if cleanupInterval == 0 { | ||
| cleanupInterval = 1 * time.Minute // Default cleanup every minute | ||
| } | ||
|
|
||
| ac := &AssertionCache{ | ||
| ttl: ttl, | ||
| stopCleanup: make(chan struct{}), | ||
| } | ||
|
|
||
| // Start cleanup goroutine | ||
| ac.cleanupTicker = time.NewTicker(cleanupInterval) | ||
| go ac.cleanup() | ||
|
|
||
| logrus.WithFields(logrus.Fields{ | ||
| "ttl": ttl, | ||
| "cleanup_interval": cleanupInterval, | ||
| }).Info("Assertion cache initialized") | ||
|
|
||
| return ac | ||
| } | ||
|
|
||
| // CheckAndAdd atomically checks if an assertion ID exists in the cache and adds it if not. | ||
| // This method is the core of replay protection - it ensures that each assertion ID can | ||
| // only be used once within the TTL window. | ||
| // | ||
| // Returns true if the assertion was added (not a replay), false if it already exists (replay detected). | ||
| func (ac *AssertionCache) CheckAndAdd(assertionID string) bool { | ||
| if assertionID == "" { | ||
| logrus.Warn("Empty assertion ID provided to cache") | ||
| return false | ||
| } | ||
|
|
||
| now := time.Now() | ||
| entry := &assertionEntry{ | ||
| addedAt: now, | ||
| expiry: now.Add(ac.ttl), | ||
| } | ||
|
|
||
| // LoadOrStore is atomic - it returns the existing value if present, | ||
| // or stores the new value and returns it. The 'loaded' bool indicates | ||
| // whether the value was loaded (true) or stored (false). | ||
| _, loaded := ac.cache.LoadOrStore(assertionID, entry) | ||
|
|
||
| if loaded { | ||
| // Assertion ID already exists - this is a replay attack | ||
| logrus.WithFields(logrus.Fields{ | ||
| "assertion_id": assertionID, | ||
| "event": "replay_detected", | ||
| }).Warn("SAML assertion replay attack detected") | ||
| return false | ||
| } | ||
|
|
||
| // Successfully cached new assertion ID | ||
| logrus.WithFields(logrus.Fields{ | ||
| "assertion_id": assertionID, | ||
| "expiry": entry.expiry, | ||
| }).Debug("SAML assertion ID cached successfully") | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| // cleanup removes expired assertion entries from the cache. | ||
| // This goroutine runs periodically based on the cleanup interval and prevents | ||
| // unbounded memory growth by removing entries that have exceeded their TTL. | ||
| func (ac *AssertionCache) cleanup() { | ||
| for { | ||
| select { | ||
| case <-ac.cleanupTicker.C: | ||
| now := time.Now() | ||
| count := 0 | ||
|
|
||
| // Iterate through all cache entries | ||
| ac.cache.Range(func(key, value interface{}) bool { | ||
| entry := value.(*assertionEntry) | ||
| if now.After(entry.expiry) { | ||
| ac.cache.Delete(key) | ||
| count++ | ||
| } | ||
| return true // Continue iteration | ||
| }) | ||
|
|
||
| if count > 0 { | ||
| logrus.WithField("count", count).Debug("Cleaned up expired assertion cache entries") | ||
| } | ||
|
|
||
| case <-ac.stopCleanup: | ||
| // Graceful shutdown requested | ||
| ac.cleanupTicker.Stop() | ||
| logrus.Info("Assertion cache cleanup goroutine stopped") | ||
| return | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Stop gracefully stops the cleanup goroutine. | ||
| // This should be called when the server is shutting down to prevent goroutine leaks. | ||
| func (ac *AssertionCache) Stop() { | ||
| close(ac.stopCleanup) | ||
| } | ||
|
|
||
| // Size returns the current number of cached assertions. | ||
| // This is useful for monitoring and observability to track cache utilization. | ||
| func (ac *AssertionCache) Size() int { | ||
| count := 0 | ||
| ac.cache.Range(func(_, _ interface{}) bool { | ||
| count++ | ||
| return true | ||
| }) | ||
| return count | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says 'typically the metadata URL' but the example value on line 14 shows 'https://your-app.example.com/saml/metadata' which contradicts the actual implementation. According to the code in main.go line 95, the metadata URL is constructed as '/saml/metadata', but the entity_id is a separate configuration field. The comment should clarify that entity_id is a unique identifier for the SP, not necessarily the metadata URL.