-
Notifications
You must be signed in to change notification settings - Fork 1
Defining recommended session approach for apps #55
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
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
95aea95
Defining recommended session approach for apps
samlown 9746887
Fixing copilot recommendations
samlown 6cec96e
Session support for multiple authentication methods
samlown 2d1a854
Raise error when session cannot be loaded
samlown 075def8
Apply suggestion from @Copilot
samlown 1435aa7
Apply suggestion from @Copilot
samlown 68064e5
Apply suggestion from @Copilot - remove comment
samlown 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
Some comments aren't visible on the classic Files Changed page.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| package invopop | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "time" | ||
| ) | ||
|
|
||
| const ( | ||
| sessionTokenTTL = 300 // 5 minutes | ||
| sessionKey invopopContextKey = "session" | ||
| ) | ||
|
|
||
| // Session provides an opinionated session management structure for usage alongside | ||
| // application enrollments inside Invopop. Content is based on the enrollment provided | ||
| // by the Access service. Sessions may be persisted to secure temporary stores, such as | ||
| // cookies or key-value databases. | ||
| // | ||
| // Sessions should be used within your business or domain logic and can be used as a | ||
| // replacement of a regular Client instance. | ||
| // | ||
| // Instantiate sessions using the Access NewSession method, even if you intend to | ||
| // unmarshal them from a stored source to ensure that there is always a client available. | ||
| // | ||
| // Important: sessions should only be stored in cookies if the application using them | ||
| // will be used independently. For embedded applications, such as those running inside | ||
| // the Invopop Console, sessions should be created on each request. | ||
| type Session struct { | ||
| // EnrollmentID is the unique ID of the enrollment. | ||
| EnrollmentID string `json:"eid,omitempty"` | ||
| // OwnerID is the unique ID of the entity this enrollment belongs to. | ||
| OwnerID string `json:"oid,omitempty"` | ||
| // Sandbox indicates whether this enrollment is for a sandbox or live workspace. | ||
| Sandbox bool `json:"sbx,omitempty"` | ||
|
|
||
| // Data contains any config data stored inside the enrollment. This should not be | ||
| // modified. | ||
| Data json.RawMessage `json:"data,omitempty"` | ||
|
|
||
| // Meta contains any metadata stored alongside the base enrollment | ||
| // details that might be useful for the app. Use the Set and Get methods to manage | ||
| // this map. Only string key-value pairs are supported for simplicity in | ||
| // serialization. | ||
| Meta map[string]string `json:"meta,omitempty"` | ||
|
|
||
| // RedirectURI is a convenience field to store a redirection URI that may be | ||
| // used as part of an authentication flow to redirect the user back to where | ||
| // the came from. | ||
| RedirectURI string `json:"redirect_uri,omitempty"` | ||
|
|
||
| // Token is the authentication token provided by the enrollment, alongside | ||
| // the expiration unix timestamp. | ||
| Token string `json:"t,omitempty"` | ||
| TokenExpires int64 `json:"exp,omitempty"` | ||
|
|
||
| // extra is used to temporarily store any extra values in the session that may | ||
| // be used in the same request and must not be serialized. | ||
| extra map[any]any `json:"-"` | ||
|
|
||
| // client contains the invopop client prepared with the session's token, when loaded. | ||
| client *Client | ||
| } | ||
|
|
||
| // Authorize will try to authorize the session by confirming that | ||
| // the token is valid with the Access service, renewing in the process. | ||
| // | ||
| // Before making unnecessary requests, a check is made to see if the currently | ||
| // provided token is still valid based on the expiration timestamp. To force | ||
| // renewal, set the TokenExpires field to zero. | ||
| // | ||
| // If no client is available, or no token is present, an error will be returned. | ||
| func (s *Session) Authorize(ctx context.Context) error { | ||
| if !s.ShouldRenew() { | ||
| return nil | ||
| } | ||
| en, err := s.client.Access().Enrollment().Authorize(ctx) | ||
samlown marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if err != nil { | ||
| if IsNotFound(err) { | ||
| return fmt.Errorf("%w: application not enrolled", ErrAccessDenied) | ||
| } | ||
| return err | ||
| } | ||
| s.SetFromEnrollment(en) | ||
| return nil | ||
| } | ||
|
|
||
| // Client provides the invopop client prepared with the session's token, which may be nil | ||
| // if the session was not initialized with a client or token. | ||
| func (s *Session) Client() *Client { | ||
| return s.client | ||
| } | ||
|
|
||
| // SetFromEnrollment will update the session details based on the provided | ||
| // enrollment object. | ||
| func (s *Session) SetFromEnrollment(e *Enrollment) { | ||
| s.EnrollmentID = e.ID | ||
| s.OwnerID = e.OwnerID | ||
| s.Sandbox = e.Sandbox | ||
| s.Data = e.Data | ||
| s.Token = e.Token | ||
| s.TokenExpires = e.TokenExpires | ||
| s.client = s.client.SetAuthToken(e.Token) | ||
samlown marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Set will store a key-value pair inside the session much like in a context object | ||
| // that can be later retrieved using the Get method. This is useful to caching details | ||
| // such as a database connection or similar complex object. | ||
| func (s *Session) Set(key, value any) { | ||
| if s.extra == nil { | ||
| s.extra = make(map[any]any) | ||
| } | ||
| if value == nil { | ||
| delete(s.extra, key) | ||
| return | ||
| } | ||
| s.extra[key] = value | ||
| } | ||
|
|
||
| // Get will retrieve a value from the session's cache or return nil. | ||
| func (s *Session) Get(key any) any { | ||
| if s.extra == nil { | ||
| return nil | ||
| } | ||
| return s.extra[key] | ||
| } | ||
|
|
||
| // SetToken will update the session's token value and prepare for an authorization | ||
| // request to be sent to the Invopop API. | ||
| func (s *Session) SetToken(tok string) { | ||
| s.Token = tok | ||
| s.TokenExpires = 0 | ||
| s.client = s.client.WithAuthToken(tok) | ||
samlown marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Authorized indicates whether the session has a valid token that is not expired. | ||
| // The server may have a different state, so this does not guarantee that requests | ||
| // will succeed, but is a good pre-check before making calls. | ||
| func (s *Session) Authorized() bool { | ||
| return s.Token != "" && s.TokenExpires != 0 && time.Now().Unix() < s.TokenExpires | ||
| } | ||
|
|
||
| // ShouldRenew indicates whether the session's token is close to expiration | ||
| // and should be renewed. | ||
| func (s *Session) ShouldRenew() bool { | ||
| if s.TokenExpires == 0 { | ||
| return true | ||
| } | ||
| tn := time.Now().Unix() | ||
samlown marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // renew if less than 5 minutes to expiration | ||
| return (s.TokenExpires - tn) < sessionTokenTTL | ||
| } | ||
|
|
||
| // CanStore indicates whether the session has sufficient data to be stored. | ||
| func (s *Session) CanStore() bool { | ||
| return s.EnrollmentID != "" && s.Token != "" | ||
| } | ||
|
|
||
| // UnmarshalJSON implements the json.Unmarshaler interface to | ||
| // ensure that any token in the payload will be added to the client | ||
| // automatically. | ||
| func (s *Session) UnmarshalJSON(data []byte) error { | ||
| type sessionAlias Session | ||
| var sa sessionAlias | ||
| if err := json.Unmarshal(data, &sa); err != nil { | ||
| return err | ||
| } | ||
| sa.client = s.client // ensure client preserved | ||
| *s = Session(sa) | ||
| if s.Token != "" && s.client != nil { | ||
| s.client = s.client.SetAuthToken(s.Token) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // Context adds the session to the context so that it can be easily re-used inside | ||
| // other parts of the application. Use this sparingly, ideally you want to be passing | ||
| // the session directly between method calls, but given that a session may have different | ||
| // credentials for each incoming request, the context can be a lot more convenient. | ||
| func (s *Session) Context(ctx context.Context) context.Context { | ||
| return context.WithValue(ctx, sessionKey, s) | ||
| } | ||
|
|
||
| // GetSession tries to extract a session object from the provided context. | ||
| func GetSession(ctx context.Context) *Session { | ||
| s, ok := ctx.Value(sessionKey).(*Session) | ||
| if !ok { | ||
| return nil | ||
| } | ||
| return s | ||
| } | ||
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
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.
Uh oh!
There was an error while loading. Please reload this page.