From 596605e18fcf40d25c99e86a24cb246f1f72aa8e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 6 Jul 2023 12:29:31 -0400 Subject: [PATCH 1/3] feat: log out user on conver to oidc Log out user and redirect to login page and log out user when they convert to oidc. --- coderd/coderd.go | 10 +++- coderd/userauth.go | 48 +++++++++++++++---- site/src/components/SignInForm/SignInForm.tsx | 11 ++++- site/src/pages/LoginPage/LoginPageView.tsx | 4 ++ .../SecurityPage/SingleSignOnSection.tsx | 20 ++++++-- 5 files changed, 74 insertions(+), 19 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 9d2045b9ca2da..dc05c74999101 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -631,12 +631,18 @@ func New(options *Options) *API { r.Post("/login", api.postLogin) r.Route("/oauth2", func(r chi.Router) { r.Route("/github", func(r chi.Router) { - r.Use(httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, nil)) + r.Use( + httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, nil), + apiKeyMiddlewareOptional, + ) r.Get("/callback", api.userOAuth2Github) }) }) r.Route("/oidc/callback", func(r chi.Router) { - r.Use(httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, oidcAuthURLParams)) + r.Use( + httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, oidcAuthURLParams), + apiKeyMiddlewareOptional, + ) r.Get("/", api.userOIDC) }) }) diff --git a/coderd/userauth.go b/coderd/userauth.go index 6ecf19d1f6053..9bbabc079469b 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -1096,6 +1096,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C cookies []*http.Cookie ) + var isConvertLoginType bool err := api.Database.InTx(func(tx database.Store) error { var ( link database.UserLink @@ -1116,6 +1117,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C return err } params.User = user + isConvertLoginType = true } if user.ID == uuid.Nil && !params.AllowSignups { @@ -1284,18 +1286,44 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C return nil, database.APIKey{}, xerrors.Errorf("in tx: %w", err) } - //nolint:gocritic - cookie, key, err := api.createAPIKey(dbauthz.AsSystemRestricted(ctx), apikey.CreateParams{ - UserID: user.ID, - LoginType: params.LoginType, - DeploymentValues: api.DeploymentValues, - RemoteAddr: r.RemoteAddr, - }) - if err != nil { - return nil, database.APIKey{}, xerrors.Errorf("create API key: %w", err) + var key database.APIKey + if oldKey, ok := httpmw.APIKeyOptional(r); ok && isConvertLoginType { + // If this is a convert login type, and it succeeds, then delete the old + // session. Force the user to log back in. + err := api.Database.DeleteAPIKeyByID(r.Context(), oldKey.ID) + if err != nil { + // Do not block this login if we fail to delete the old API key. + // Just delete the cookie and continue. + api.Logger.Warn(r.Context(), "failed to delete old API key in convert to oidc", + slog.Error(err), + slog.F("old_api_key_id", oldKey.ID), + slog.F("user_id", user.ID), + ) + } + cookies = append(cookies, &http.Cookie{ + Name: codersdk.SessionTokenCookie, + Path: "/", + MaxAge: -1, + Secure: api.SecureAuthCookie, + HttpOnly: true, + }) + key = oldKey + } else { + //nolint:gocritic + cookie, newKey, err := api.createAPIKey(dbauthz.AsSystemRestricted(ctx), apikey.CreateParams{ + UserID: user.ID, + LoginType: params.LoginType, + DeploymentValues: api.DeploymentValues, + RemoteAddr: r.RemoteAddr, + }) + if err != nil { + return nil, database.APIKey{}, xerrors.Errorf("create API key: %w", err) + } + cookies = append(cookies, cookie) + key = *newKey } - return append(cookies, cookie), *key, nil + return cookies, key, nil } // convertUserToOauth will convert a user from password base loginType to diff --git a/site/src/components/SignInForm/SignInForm.tsx b/site/src/components/SignInForm/SignInForm.tsx index 4e5d552fc4371..4f3411605b377 100644 --- a/site/src/components/SignInForm/SignInForm.tsx +++ b/site/src/components/SignInForm/SignInForm.tsx @@ -37,7 +37,7 @@ const useStyles = makeStyles((theme) => ({ fontWeight: 600, }, }, - error: { + alert: { marginBottom: theme.spacing(4), }, divider: { @@ -69,6 +69,7 @@ export interface SignInFormProps { isSigningIn: boolean redirectTo: string error?: unknown + info?: string authMethods?: AuthMethods onSubmit: (credentials: { email: string; password: string }) => void // initialTouched is only used for testing the error state of the form. @@ -80,6 +81,7 @@ export const SignInForm: FC> = ({ redirectTo, isSigningIn, error, + info, onSubmit, initialTouched, }) => { @@ -100,10 +102,15 @@ export const SignInForm: FC> = ({ {commonTranslation.t("coder")} -
+
+ +
+ {info} +
+
= ({ const { error } = context const data = context.data as UnauthenticatedData const styles = useStyles() + // This allows messages to be displayed at the top of the sign in form. + // Helpful for any redirects that want to inform the user of something. + const info = new URLSearchParams(location.search).get("info") || undefined return isLoading ? ( @@ -37,6 +40,7 @@ export const LoginPageView: FC = ({ redirectTo={redirectTo} isSigningIn={isSigningIn} error={error} + info={info} onSubmit={onSignIn} />