-
Notifications
You must be signed in to change notification settings - Fork 0
Migrate OAuth state store from in-memory to Redis #24
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ed63f70
migrate OAuth state store from in-memory to Redis
Copilot e91ab34
fix: align default TTL and document ExpiresAt in Redis state store
Copilot 3ae271c
Update internal/infrastructure/cache/redis_oauth_state_store.go
slhmy 52c479d
style: run gofmt on state_store.go and redis_oauth_state_store.go
Copilot 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package cache | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "time" | ||
|
|
||
| "github.com/poly-workshop/identra/internal/infrastructure/oauth" | ||
| goredis "github.com/redis/go-redis/v9" | ||
| ) | ||
|
|
||
| // NewRedisOAuthStateStore creates a Redis-backed OAuth state store. | ||
| // Expiry is enforced by Redis TTL; the ExpiresAt field of returned State is not populated. | ||
| func NewRedisOAuthStateStore(ttl time.Duration, rdb goredis.UniversalClient) (oauth.StateStore, error) { | ||
| if ttl <= 0 { | ||
| ttl = time.Minute | ||
| } | ||
| if rdb == nil { | ||
| return nil, errors.New("redis client is required for oauth state store") | ||
| } | ||
| return &redisOAuthStateStore{rdb: rdb, ttl: ttl, prefix: "identra:oauth_state:"}, nil | ||
| } | ||
|
|
||
| type redisOAuthStateStore struct { | ||
| rdb goredis.UniversalClient | ||
| ttl time.Duration | ||
| prefix string | ||
| } | ||
|
|
||
| type oauthStateValue struct { | ||
| Provider string `json:"provider"` | ||
| RedirectURL string `json:"redirect_url"` | ||
| } | ||
|
|
||
| func (s *redisOAuthStateStore) key(state string) string { | ||
| return s.prefix + state | ||
| } | ||
|
|
||
| // Add stores the state with its provider and redirect URL in Redis with a TTL. | ||
| func (s *redisOAuthStateStore) Add(ctx context.Context, state, provider, redirectURL string) error { | ||
| val, err := json.Marshal(oauthStateValue{Provider: provider, RedirectURL: redirectURL}) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return s.rdb.Set(ctx, s.key(state), val, s.ttl).Err() | ||
| } | ||
|
|
||
| // consumeStateScript atomically retrieves and deletes the state key. | ||
| // Returns the value if found, or nil if not present. | ||
| var consumeStateScript = goredis.NewScript(` | ||
| local v = redis.call("GET", KEYS[1]) | ||
| if not v then return nil end | ||
| redis.call("DEL", KEYS[1]) | ||
| return v | ||
| `) | ||
|
|
||
| // Consume retrieves and atomically removes the state from Redis. | ||
| // Returns false (with no error) when the state is not found or has expired. | ||
| // ExpiresAt is not populated in the returned State because Redis enforces expiry via TTL. | ||
| func (s *redisOAuthStateStore) Consume(ctx context.Context, state string) (oauth.State, bool, error) { | ||
| res, err := consumeStateScript.Run(ctx, s.rdb, []string{s.key(state)}).Text() | ||
| if err != nil { | ||
| if errors.Is(err, goredis.Nil) { | ||
| return oauth.State{}, false, nil | ||
| } | ||
| return oauth.State{}, false, err | ||
| } | ||
|
|
||
| var val oauthStateValue | ||
| if err := json.Unmarshal([]byte(res), &val); err != nil { | ||
| return oauth.State{}, false, err | ||
| } | ||
|
|
||
| return oauth.State{ | ||
| Provider: val.Provider, | ||
| RedirectURL: val.RedirectURL, | ||
| }, true, nil | ||
| } | ||
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 |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package oauth | ||
|
|
||
| import ( | ||
| "context" | ||
| "sync" | ||
| "time" | ||
| ) | ||
|
Comment on lines
3
to
7
|
||
|
|
@@ -14,8 +15,11 @@ type State struct { | |
|
|
||
| // StateStore defines the interface for OAuth state storage. | ||
| type StateStore interface { | ||
| Add(state, provider, redirectURL string) | ||
| Consume(state string) (State, bool) | ||
| // Add stores a new state with its provider and redirect URL. | ||
| Add(ctx context.Context, state, provider, redirectURL string) error | ||
| // Consume returns the state details when valid and removes it from the store. | ||
| // Returns false when the state is not found or has expired. | ||
| Consume(ctx context.Context, state string) (State, bool, error) | ||
| } | ||
|
|
||
| type inMemoryStateStore struct { | ||
|
|
@@ -36,7 +40,7 @@ func NewInMemoryStateStore(ttl time.Duration) StateStore { | |
| } | ||
|
|
||
| // Add stores a new state with its provider and redirect URL. | ||
| func (s *inMemoryStateStore) Add(state, provider, redirectURL string) { | ||
| func (s *inMemoryStateStore) Add(_ context.Context, state, provider, redirectURL string) error { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
|
|
||
|
|
@@ -46,25 +50,26 @@ func (s *inMemoryStateStore) Add(state, provider, redirectURL string) { | |
| RedirectURL: redirectURL, | ||
| ExpiresAt: time.Now().Add(s.ttl), | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // Consume returns the state details when valid and removes it from the store. | ||
| func (s *inMemoryStateStore) Consume(state string) (State, bool) { | ||
| func (s *inMemoryStateStore) Consume(_ context.Context, state string) (State, bool, error) { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
|
|
||
| s.cleanupLocked() | ||
| value, ok := s.values[state] | ||
| if !ok { | ||
| return State{}, false | ||
| return State{}, false, nil | ||
| } | ||
| delete(s.values, state) | ||
|
|
||
| if time.Now().After(value.ExpiresAt) { | ||
| return State{}, false | ||
| return State{}, false, nil | ||
| } | ||
|
|
||
| return value, true | ||
| return value, true, nil | ||
| } | ||
|
|
||
| func (s *inMemoryStateStore) cleanupLocked() { | ||
|
|
||
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.
This new file isn’t gofmt-formatted (imports and indentation). Please run gofmt so it matches existing Redis cache implementations (e.g., redis_email_code_store.go) and keeps diffs readable.