-
Notifications
You must be signed in to change notification settings - Fork 365
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
Feature/support OIDC auth #3452
Conversation
This reverts commit 35e0c42.
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! Looks really nice, not requesting anything major.
pkg/api/oidc_login_handler.go
Outdated
if r.TLS != nil { | ||
scheme = "https" | ||
} | ||
u := url.URL{ | ||
Scheme: scheme, | ||
Host: r.Host, | ||
Path: BaseURL + "/oidc/callback", | ||
} |
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.
Can't we use a relative URL? This way seems to reimplement a particular relative URL.
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.
You mean dropping the host from the redirect URL?
OAuth requires providing an absolute URL for this parameter.
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.
:-(
If we had the request URL u
we could still u.Resolve(BaseURL + "/oidc/callback")
, but I can understand that this might not be readily available. (If you can get it onto r
, then of course that would be better!)
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!
pkg/api/oidc_login_handler.go
Outdated
IDTokenClaimsSessionKey = "id_token_claims" | ||
StateSessionKey = "state" | ||
|
||
stateByteSize = 32 |
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.
Is there a recommendation for this? The best I could find was this on login.gov:
state
A unique value at least 22 characters in length used for maintaining state between the request and the callback. This value will be returned to the client on a successful authorization.
Admittedly this is quite a weak recommendation.
22 characters from a set of (say) 64 different characters is 6*22=132 bites, or 32.5 bytes. But I would be happier to specify this in terms of characters, and use 22 or some recommended value. nanoid is your friend for doing this sort of thing.
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.
Done
pkg/api/oidc_login_handler.go
Outdated
if r.TLS != nil { | ||
scheme = "https" | ||
} | ||
u := url.URL{ | ||
Scheme: scheme, | ||
Host: r.Host, | ||
Path: BaseURL + "/oidc/callback", | ||
} |
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.
:-(
If we had the request URL u
we could still u.Resolve(BaseURL + "/oidc/callback")
, but I can understand that this might not be readily available. (If you can get it onto r
, then of course that would be better!)
pkg/ddl/000038_oidc_user.up.sql
Outdated
BEGIN; | ||
|
||
ALTER TABLE auth_users | ||
ADD COLUMN IF NOT EXISTS external_id VARCHAR(255) UNIQUE NULL; |
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 don't think we use the NULL
(non-)constraint in our DDL. I had to look up what it means, so I slightly prefer that you remove it from here.
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.
done
logger.WithError(err).Fatal("Failed to initialize OIDC provider") | ||
} | ||
cfg.GetBlockstoreDefaultNamespacePrefix() | ||
oauthConfig = &oauth2.Config{ |
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.
shouldn't we add the RedirectURL also?
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.
Since the redirect URL also contains the host, I construct it ad-hoc in every login.
pkg/api/controller.go
Outdated
session, err := c.sessionStore.Get(r, OIDCAuthSessionName) | ||
if err != nil { | ||
writeError(w, http.StatusInternalServerError, err) | ||
return | ||
} | ||
session.Values = nil | ||
err = session.Save(r, w) | ||
if err != nil { | ||
writeError(w, http.StatusInternalServerError, err) | ||
return | ||
} |
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 happens if OIDC is not configured?
shouldn't this return an error?
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 is the global logout, not specific to OIDC.
We could perform this part only if OIDC is configured - but maybe it's better to clear the session even if OIDC is disabled.
cmd/lakefs/cmd/run.go
Outdated
@@ -14,6 +14,7 @@ import ( | |||
"syscall" | |||
"time" | |||
|
|||
"github.com/coreos/go-oidc" |
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 is v2 - I think you want to use github.com/coreos/go-oidc/v3/oidc
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.
done
go.mod
Outdated
@@ -81,6 +81,14 @@ require ( | |||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 | |||
) | |||
|
|||
require ( | |||
github.com/coreos/go-oidc v2.2.1+incompatible // indirect |
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.
looks like you need to run go mod tidy
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.
done
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.
Some minor issues. Two suggestions. Very glad we have all the rest. Very cool!!
cmd/lakefs/cmd/run.go
Outdated
oidcConfig := cfg.GetAuthOIDCConfiguration() | ||
var oauthConfig *oauth2.Config | ||
var oidcProvider *oidc.Provider | ||
if oidcConfig != nil && oidcConfig.Enabled { |
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.
Suggest not to have the oidc config as a pointer in the first place - we have Enabled property, so having this scope of configuration disabled by default will do the work.
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.
Done
pkg/api/auth_middleware.go
Outdated
case "oidc_auth": | ||
user, err = userFromOIDC(ctx, logger, authService, session, oidcConfig) |
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.
Suggest to merge the cookie_auth and the oidc_auth. Handling authentication using session storage (aka cookie) can use one mechanism.
In this case we are adding a package that handle the cookie storage - it can be also used to handle the current server side cookie we used without OIDC. It just need to include this information as part of the session cookie.
Users will have to login again - but I think it will pay off in the long run.
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.
Done: also added (temporary) migration from the old cookie, so that users are not logged out.
) *Controller { | ||
gob.Register(oidc.Claims{}) |
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.
can we use json encoding and not gob? I assume this is for session information serialization.
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.
Synced f2f: not really an option here since internally gorilla sessions have a map[interface{}]interface{}
which is not support by the JSON encoder.
pkg/api/oidc_login_handler.go
Outdated
scheme := "http" | ||
if r.TLS != nil { | ||
scheme = "https" | ||
} |
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 do not perform ssl termination - I think you should use a configured base url to build the callback and it should include the scheme and host.
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.
Done
pkg/api/oidc_login_handler.go
Outdated
|
||
session, err := sessionStore.Get(r, OIDCAuthSessionName) | ||
if err != nil { | ||
logger.Errorf("failed to get oidc session: %w", err) |
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.
logger.Errorf("failed to get oidc session: %w", err) | |
logger.WithError(err).Error("failed to get oidc session") |
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.
Done
pkg/api/oidc_login_handler.go
Outdated
}) | ||
state, err := nanoid.New(stateLength) | ||
if err != nil { | ||
logger.Errorf("failed to generate state for oidc: %w", err) |
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.
logger.Errorf("failed to generate state for oidc: %w", err) | |
logger.WithError(err).Error("failed to generate state for oidc") |
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.
Done
pkg/api/controller.go
Outdated
err := doOIDCLogout(w, r, c.sessionStore) | ||
if err != nil { | ||
c.Logger.WithError(err).Error("failed to perform OIDC logout") | ||
} |
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.
Think we need to change one of two places. We can ignore the error here as loging a session in a system that doesn't have a session will fail. Or ignore it as we already just log 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.
Fixed: now ignoring the error from sessionStore.Get
when it makes sense.
pkg/auth/oidc/authenticator.go
Outdated
rawIDToken, ok := token.Extra("id_token").(string) | ||
if !ok { | ||
return nil, err | ||
} |
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.
There is no err here
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.
Oops! Done
pkg/ddl/000038_oidc_user.up.sql
Outdated
ALTER TABLE auth_users | ||
ADD COLUMN IF NOT EXISTS external_id VARCHAR(255) UNIQUE; |
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.
- Any reason to use varchar and not text?
- Missing drop column in the ...down.sql.
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.
- Changed to TEXT
- According to @arielshaqed's suggestion, living the down migration empty, and made the up-migration idempotent.
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.
Added some comments, the one I was worried about is counting on save state follow up by get. All the rest looks great, thanks!
cmd/lakefs/cmd/run.go
Outdated
oauthConfig = &oauth2.Config{ | ||
ClientID: oidcConfig.ClientID, | ||
ClientSecret: oidcConfig.ClientSecret, | ||
RedirectURL: oidcConfig.CallbackBaseURL + api.BaseURL + "/oidc/callback", |
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.
Need to trim leading "/" from callback base url so we will not end with "//"
pkg/api/auth_middleware.go
Outdated
// Deprecated | ||
// TODO(johnnyaug) remove this a week after released | ||
func migrateFromLegacyCookie(r *http.Request, w http.ResponseWriter, logger logging.Logger, sessionStore sessions.Store) { |
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.
Need to use code doc - start with the method name
pkg/api/auth_middleware.go
Outdated
if token == "" { | ||
migrateFromLegacyCookie(r, w, logger, sessionStore) | ||
} | ||
internalAuthSession, err = sessionStore.Get(r, InternalAuthSessionName) |
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 need to clear the cookie as you did - but reading the session after saving it - I don't think it will work.
When you "save" a cookie - you write a header. When you "read" a cookie, it is from the request header that we already parsed.
I maybe wrong as the code can implement some kind of workaround - just wanted to verify with you how it was implemented.
func (c *Controller) Login(w http.ResponseWriter, r *http.Request, body LoginJSONRequestBody) { | ||
err := clearSession(w, r, c.sessionStore, OIDCAuthSessionName) |
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.
need to clear the old one - not the new one while login
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.
synced f2f: clearing the OIDC session here, and setting the "internal login" one.
pkg/api/controller.go
Outdated
ctx := r.Context() | ||
session, _ := c.sessionStore.Get(r, OIDCAuthSessionName) | ||
if r.URL.Query().Get("state") != session.Values[StateSessionKey] { | ||
writeError(w, http.StatusBadRequest, "Invalid state parameter.") |
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.
writeError(w, http.StatusBadRequest, "Invalid state parameter.") | |
writeError(w, http.StatusBadRequest, "Invalid state parameter") |
pkg/api/oidc_login_handler.go
Outdated
writeError(w, http.StatusInternalServerError, "Failed to redirect to login page") | ||
return | ||
} | ||
|
||
session, _ := sessionStore.Get(r, OIDCAuthSessionName) | ||
session.Values[StateSessionKey] = state | ||
if err := session.Save(r, w); err != nil { | ||
logger.WithError(err).Error("failed to save oidc session") | ||
writeError(w, http.StatusInternalServerError, "Failed to redirect to login page") |
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.
If possible, change something small in the writeError in one of the two, so we can identify the issue without going into the logs. No need to expose more information if not needed, just a way to distinct between the two.
Support authentication with OIDC - step 1
Allows the user to configure an external OIDC provider. Once done, the login page will include a link to sign-in using the provider. When a user logs in with the provider, a corresponding lakeFS user is created.
Permissions (authorization) for the created user are still managed internally in lakeFS.