Skip to content
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

Merged
merged 37 commits into from
Jun 21, 2022
Merged

Feature/support OIDC auth #3452

merged 37 commits into from
Jun 21, 2022

Conversation

johnnyaug
Copy link
Contributor

@johnnyaug johnnyaug commented Jun 2, 2022

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.

@johnnyaug johnnyaug self-assigned this Jun 9, 2022
@johnnyaug johnnyaug added the include-changelog PR description should be included in next release changelog label Jun 9, 2022
@johnnyaug johnnyaug requested review from nopcoder, arielshaqed and guy-har and removed request for nopcoder and arielshaqed June 12, 2022 09:10
@johnnyaug johnnyaug marked this pull request as ready for review June 12, 2022 09:10
Copy link
Contributor

@arielshaqed arielshaqed left a 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.

cmd/lakefs/cmd/run.go Outdated Show resolved Hide resolved
docs/reference/oidc.md Show resolved Hide resolved
pkg/api/auth_middleware.go Show resolved Hide resolved
pkg/api/oidc_login_handler.go Outdated Show resolved Hide resolved
pkg/api/oidc_login_handler.go Outdated Show resolved Hide resolved
Comment on lines 40 to 47
if r.TLS != nil {
scheme = "https"
}
u := url.URL{
Scheme: scheme,
Host: r.Host,
Path: BaseURL + "/oidc/callback",
}
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.down.sql Outdated Show resolved Hide resolved
pkg/ddl/000038_oidc_user.down.sql Outdated Show resolved Hide resolved
webui/src/pages/auth/login.jsx Outdated Show resolved Hide resolved
@johnnyaug johnnyaug requested a review from arielshaqed June 12, 2022 16:02
Copy link
Contributor

@arielshaqed arielshaqed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

IDTokenClaimsSessionKey = "id_token_claims"
StateSessionKey = "state"

stateByteSize = 32
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 40 to 47
if r.TLS != nil {
scheme = "https"
}
u := url.URL{
Scheme: scheme,
Host: r.Host,
Path: BaseURL + "/oidc/callback",
}
Copy link
Contributor

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!)

BEGIN;

ALTER TABLE auth_users
ADD COLUMN IF NOT EXISTS external_id VARCHAR(255) UNIQUE NULL;
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

cmd/lakefs/cmd/run.go Outdated Show resolved Hide resolved
logger.WithError(err).Fatal("Failed to initialize OIDC provider")
}
cfg.GetBlockstoreDefaultNamespacePrefix()
oauthConfig = &oauth2.Config{
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Comment on lines 178 to 188
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
}
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@@ -14,6 +14,7 @@ import (
"syscall"
"time"

"github.com/coreos/go-oidc"
Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

@nopcoder nopcoder left a 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!!

oidcConfig := cfg.GetAuthOIDCConfiguration()
var oauthConfig *oauth2.Config
var oidcProvider *oidc.Provider
if oidcConfig != nil && oidcConfig.Enabled {
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 109 to 110
case "oidc_auth":
user, err = userFromOIDC(ctx, logger, authService, session, oidcConfig)
Copy link
Contributor

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.

Copy link
Contributor Author

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.

pkg/api/auth_middleware.go Show resolved Hide resolved
) *Controller {
gob.Register(oidc.Claims{})
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines 64 to 67
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


session, err := sessionStore.Get(r, OIDCAuthSessionName)
if err != nil {
logger.Errorf("failed to get oidc session: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.Errorf("failed to get oidc session: %w", err)
logger.WithError(err).Error("failed to get oidc session")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

})
state, err := nanoid.New(stateLength)
if err != nil {
logger.Errorf("failed to generate state for oidc: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.Errorf("failed to generate state for oidc: %w", err)
logger.WithError(err).Error("failed to generate state for oidc")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 214 to 217
err := doOIDCLogout(w, r, c.sessionStore)
if err != nil {
c.Logger.WithError(err).Error("failed to perform OIDC logout")
}
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines 28 to 31
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, err
}
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops! Done

Comment on lines 3 to 4
ALTER TABLE auth_users
ADD COLUMN IF NOT EXISTS external_id VARCHAR(255) UNIQUE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Any reason to use varchar and not text?
  2. Missing drop column in the ...down.sql.

Copy link
Contributor Author

@johnnyaug johnnyaug Jun 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Changed to TEXT
  2. According to @arielshaqed's suggestion, living the down migration empty, and made the up-migration idempotent.

@johnnyaug johnnyaug requested a review from nopcoder June 20, 2022 11:54
Copy link
Contributor

@nopcoder nopcoder left a 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!

oauthConfig = &oauth2.Config{
ClientID: oidcConfig.ClientID,
ClientSecret: oidcConfig.ClientSecret,
RedirectURL: oidcConfig.CallbackBaseURL + api.BaseURL + "/oidc/callback",
Copy link
Contributor

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 "//"

Comment on lines 62 to 64
// Deprecated
// TODO(johnnyaug) remove this a week after released
func migrateFromLegacyCookie(r *http.Request, w http.ResponseWriter, logger logging.Logger, sessionStore sessions.Store) {
Copy link
Contributor

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

Comment on lines 134 to 137
if token == "" {
migrateFromLegacyCookie(r, w, logger, sessionStore)
}
internalAuthSession, err = sessionStore.Get(r, InternalAuthSessionName)
Copy link
Contributor

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)
Copy link
Contributor

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

Copy link
Contributor Author

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.

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.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
writeError(w, http.StatusBadRequest, "Invalid state parameter.")
writeError(w, http.StatusBadRequest, "Invalid state parameter")

Comment on lines 30 to 38
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")
Copy link
Contributor

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.

@johnnyaug johnnyaug merged commit b55e3a4 into master Jun 21, 2022
@johnnyaug johnnyaug deleted the feature/support_oidc_auth branch June 21, 2022 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
include-changelog PR description should be included in next release changelog
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants