-
Notifications
You must be signed in to change notification settings - Fork 1
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
auth: implement login and callback handlers #8
Conversation
de657f4
to
ff87e3b
Compare
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.
Lots of minor suggestions and nit picks. But this all looks good! (Though I'll defer to @evict on approval.)
Only two things stand out:
-
Can we drop adding the
http.ResponseWriter
parameter to theStateSetter
andStateDeleter
hooks? It seems like they can only cause problems. (Unless they are just required, in which case ::sigh:: /shrug.) -
Re: "function hookers vs. an interface store"
IMHO, having an interface would be preferable. Since it requires the caller to define a type that contains the various methods. Rather than the slightly more fast and loose "here's some lambda functions".
It also makes sense since practically speaking, those HTTP endpoint would need to rely on some shared services. (e.g. a handle to database infrastructure.) And it's a little less awesome to just capture that in the closure of inline StateSetter: func (...)
expressions. (But I definitely don't feel strongly about this.)
README.md
Outdated
return "", nil | ||
} | ||
stateDeleter := func(w http.ResponseWriter, r *http.Request) { | ||
// TODO: Delete from session data. |
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.
When is stateDeleter
called? Presumably this should be called whenever a user logs out? When will the samsauth.NewHandler
call it?
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.
It is called within the callback handler: https://github.com/sourcegraph/sourcegraph-accounts-sdk-go/pull/8/files#diff-f5c9a39fac1719f2a73593994b931d8a874449548e8a3753e1be46e761e503eaR140
i.e. deletes upon auth flow completes (regardless of whether the auth flow was successfully or not)
Issuer: "https://accounts.sourcegraph.com", | ||
ClientID: os.Getenv("SAMS_CLIENT_ID"), | ||
ClientSecret: os.Getenv("SAMS_CLIENT_SECRET"), | ||
RequestScopes: []scopes.Scope{scopes.OpenID, scopes.Email, scopes.Profile}, |
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.
What is the significance of these scopes here? I am guessing that these are the scopes/permissions that the user would be authorizing when direct to SAMS, right? e.g. "the application 'cody backend' would like to have access to your user profile, and subscription:write"?
This is probably worth calling out in a comment. Since it's an important configuration knob. (Because anything the person using this SAMS login flow will need to know what scopes the user would have access to.) something something "when using the refresh token and generating an access token" the OAuth RFC says you can only request a subset of the scopes on the refresh token or something?
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.
What is the significance of these scopes here? I am guessing that these are the scopes/permissions that the user would be authorizing when direct to SAMS, right? e.g. "the application 'cody backend' would like to have access to your user profile, and subscription:write"?
Yes, however since all SAMS clients are internal/official clients, we do not show the consent screen to users at all (which we may in the far future).
This is probably worth calling out in a comment. Since it's an important configuration knob. (Because anything the person using this SAMS login flow will need to know what scopes the user would have access to.) something something "when using the refresh token and generating an access token" the OAuth RFC says you can only request a subset of the scopes on the refresh token or something?
Sure we can add a quick comment mentioning this, although we are really only using/having default OIDC scopes for user authentication (to get user profile info), for the sake of OIDC-compliant. Due to the fact we provide first-class Clients API, as long as the client has granted scopes registered in the SAMS database, clients can always ask for any granted scopes for any access tokens because that's not doing things on behalf of the user (unlike GitHub APIs) but on behalf of the client itself.
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.
Pushed a comment in 53afa47
mux.Handle("/auth/login", samsauthHandler.LoginHandler()) | ||
mux.Handle("/auth/callback", samsauthHandler.CallbackHandler( | ||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
userInfo := samsauth.UserInfoFromContext(r.Context()) |
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.
For clarity I'd suggest having a comment here to explain what's going on:
// The SAMS auth handler will complete the login process. And if successful,
// the user will be accessible from the request context.
//
// You can assume the `userInfo` will be present. As if there were any errors
// during the login process, the user-supplied handler will not be invoked.
// (And the FailureHandler will be serve the HTTP response instead.)
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.
Another thing that I noticed when patching this locally and wiring up SAMS auth:
Seems like we are missing a /logout
handler too... while SAMS isn't really needed on this code path, the caller is going to add whatever "local session creation" logic in the /auth/callback
, so they would want to have some sort of "local session deletion" logic in a /auth/logout
method as well. Right?
Longer term, if we put the SAMS login cookie on the sourcegraph.com domain, would we want samsauth
to handle creating/loading/deleting the session? (Or perhaps that would be some other package, e.g. samsauth_sessions.NewHandler
?
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.
For clarity I'd suggest having a comment here to explain what's going on:
Added in 2fa4f3f
Seems like we are missing a
/logout
handler too... while SAMS isn't really needed on this code path, the caller is going to add whatever "local session creation" logic in the/auth/callback
, so they would want to have some sort of "local session deletion" logic in a/auth/logout
method as well. Right?
Yes but this is a bit tricky, because the logout logic doesn't really follow a "manner", in the sense that it does not have to be a handler. Having a handler for /auth/logout
makes sense for some services, but probably not for a service that provide GQL endpoint (in which case the web app would actually hit GQL to accomplish sign-out).
Longer term, if we put the SAMS login cookie on the sourcegraph.com domain, would we want
samsauth
to handle creating/loading/deleting the session? (Or perhaps that would be some other package, e.g.samsauth_sessions.NewHandler
?
This PR is specially targeting decoupled-session use case, but I think the building blocks for doing shared-session is already possible using Clients API (SessionsService
). Once we established a usage pattern (aka. best practices), we can add more example docs but for now, I think we can/should leave that out. WDYT?
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.
I agree that we can add some example/docs in the future, but let's not worry about that now. 👍
auth/auth.go
Outdated
ctx := r.Context() | ||
|
||
nonce, err := h.config.StateGetter(r) | ||
h.config.StateDeleter(w, r) // Delete the state after getting it to make sure it's one-time use. |
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 "... to make sure it's one-time use." would probably be helpful to add as a note in the doc comments elsewhere, since it wasn't immediately obvious (to me at least).
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.
🤔 Where to add as a doc comment are you thinking of?
This is really internal detail about how to handle state in the most strict way, as the users of SAMS auth handler, should they care? i.e. All they need to make sure is to implement the StateStore
interface, and delete state when it is told to delete.
} | ||
|
||
// UserInfo contains the information about the authenticated user. | ||
type UserInfo struct { |
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.
Where does this come from? Is it from the OIDC or OAuth spec? Or is it specific to the SAMS system? It would be nice to call that out, since if it were the latter we'd need to generally keep it in-sync with any other fields we add to the SAMS gRPC API, right?
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.
It is mapping to whatever the SAMS discovery endpoint (1) said the "userinfo" endpoint's (2) response body is.
All of fields included in this PR are from OIDC spec, the server-side counterpart https://github.com/sourcegraph/sourcegraph-accounts/blob/c40d8f0d8374bb2222124f87f4f3626c51c4bd2e/backend/internal/api/model.go#L11-L17
auth/auth_test.go
Outdated
return mockState, nil | ||
}, | ||
StateDeleter: func(http.ResponseWriter, *http.Request) { | ||
mockState = "" |
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.
We should ensure that we don't call StateDeleter multiple times in a row, right? e.g.
assert.True(t, mockState != "", "mockState already deleted?")
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.
We should ensure that we don't call StateDeleter multiple times in a row, right?
Does it matter (practically)? 🤔
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.
Eh, probably not.
I mean, if we called StateDeleter
somewhere between 1x and 100x times for a given state value that would be weird. Right? But would it be worth the trouble to enforce that we only ever call it 1-time instead of 1 or possibly in some rare cases 2x? No.
I definitely do want to get rid of I am going to remove
Yep yep, that's what I am thinking too, OK, converting to interface store then! |
# Conflicts: # go.mod
@chrsmith thanks for the review! I think I have addressed all the feedback, PTAL! |
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.
LGTM. Thanks for making the changes.
I cloned this into a private repo on my machine, and everything worked fine. The only quirk was that this part of the code failed for me. (The SAMS dev environment wasn't returning the id_token
value in the access token's extra metadata.)
// Extract the ID Token from the access token, see http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse.
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.New(`missing "id_token" from the issuer's authorization response`)
}
But I assume I'm just doing something wrong, and/or this might require some other SAMS-changes that haven't been rolled out yet.
README.md
Outdated
} | ||
|
||
func (s *stateStore) SetState(r *http.Request, state string) error { | ||
// TODO: Save to session data. |
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.
Thanks for adding type stateStore struct
, with that big comment. IMHO, that helps clarify what the implementer needs to think about in order to use this.
If you want to go for the gold, you could remove all of these TODO's and just implement it with something like:
// { big descriptive comment from above }
//
// This example implementation of state store just keeps
// everything in-memory. Perhaps not something you'd want
// to take into production...
type exampleStateStore map[string]struct{}
Then maybe in theory the entire example could be a complete Go program that actually works?
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.
Frankly I am a bit hesitant to provide a dummy implementation here that is after all "incorrect" thus meant not to be used. Unless this SDK also provide a full-blown session solution, it is just too free form at the moment how user sessions are managed by each service. 🤔 I am more willing to point to the code of a real service and people can study/copy from under the current state of things.
mux.Handle("/auth/login", samsauthHandler.LoginHandler()) | ||
mux.Handle("/auth/callback", samsauthHandler.CallbackHandler( |
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.
Do we need to enforce the allowed HTTP methods of these routes?
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.
Ahh, the curse of example code haha, the stdlib mux doesn't provide method selector, but ultimately, I think there is no reason why we have to be strict about it.
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.
Overall LGTM from security POV. Only 2 comments but neither of them are blockers. 👍
Thanks for all the reviews! Let's land this and iterative on it while integrating a real service that uses it. |
Closes https://github.com/sourcegraph/sourcegraph-accounts/issues/43
This PR adds wrapping-handlers that are tailored to work with SAMS:
/userinfo
endpoint for you, and return a structured type (UserInfo
).prompt
andprompt_auth
, and could be more in the future.FAQs
Why not manage state cookie OOTB like gologin?
As also mentioned in the added README content, unencrypted cookies are strongly discouraged as they can be tampered with. Although encrypted cookie is the secure alternative, we do strongly recommend storing auth state value in a backend component like Redis. Therefore, function hookers are provided for the service to implement.
Resolved
Function hookers or interface store?
I started writing function hookers for managing auth state naturally, but maybe an interface store would look/work better? 🤔 Open to thoughts.
Test plan