From fbd0c6d10652e6533084fbda48b30a0f4dd4f1a5 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 29 Jul 2022 02:38:19 +0000 Subject: [PATCH 1/5] fix: handle getUser error --- coderd/httpmw/apikey.go | 26 +++++++++++++------ .../components/RequireAuth/RequireAuth.tsx | 5 ++-- .../SignInForm/SignInForm.stories.tsx | 23 ++++++++++++++++ site/src/components/SignInForm/SignInForm.tsx | 2 ++ site/src/pages/LoginPage/LoginPage.tsx | 17 ++++++++---- 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 6634363e4ce9e..1917a114d303f 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -51,6 +51,8 @@ type OAuth2Configs struct { Github OAuth2Config } +const loggedOutErrorMessage string = "You are logged out. Please log in to continue." + // ExtractAPIKey requires authentication using a valid API key. // It handles extending an API key if it comes close to expiry, // updating the last used time in the database. @@ -83,7 +85,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool } if cookieValue == "" { write(http.StatusUnauthorized, codersdk.Response{ - Message: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenKey), + Message: loggedOutErrorMessage, + Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenKey), }) return } @@ -91,7 +94,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool // APIKeys are formatted: ID-SECRET if len(parts) != 2 { write(http.StatusUnauthorized, codersdk.Response{ - Message: fmt.Sprintf("Invalid %q cookie API key format.", codersdk.SessionTokenKey), + Message: loggedOutErrorMessage, + Detail: fmt.Sprintf("Invalid %q cookie API key format.", codersdk.SessionTokenKey), }) return } @@ -100,13 +104,15 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool // Ensuring key lengths are valid. if len(keyID) != 10 { write(http.StatusUnauthorized, codersdk.Response{ - Message: fmt.Sprintf("Invalid %q cookie API key id.", codersdk.SessionTokenKey), + Message: loggedOutErrorMessage, + Detail: fmt.Sprintf("Invalid %q cookie API key id.", codersdk.SessionTokenKey), }) return } if len(keySecret) != 22 { write(http.StatusUnauthorized, codersdk.Response{ - Message: fmt.Sprintf("Invalid %q cookie API key secret.", codersdk.SessionTokenKey), + Message: loggedOutErrorMessage, + Detail: fmt.Sprintf("Invalid %q cookie API key secret.", codersdk.SessionTokenKey), }) return } @@ -114,7 +120,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool if err != nil { if errors.Is(err, sql.ErrNoRows) { write(http.StatusUnauthorized, codersdk.Response{ - Message: "API key is invalid.", + Message: loggedOutErrorMessage, + Detail: "API key is invalid.", }) return } @@ -129,7 +136,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool // Checking to see if the secret is valid. if subtle.ConstantTimeCompare(key.HashedSecret, hashed[:]) != 1 { write(http.StatusUnauthorized, codersdk.Response{ - Message: "API key secret is invalid.", + Message: loggedOutErrorMessage, + Detail: "API key secret is invalid.", }) return } @@ -174,7 +182,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool // Checking if the key is expired. if key.ExpiresAt.Before(now) { write(http.StatusUnauthorized, codersdk.Response{ - Message: fmt.Sprintf("API key expired at %q.", key.ExpiresAt.String()), + Message: loggedOutErrorMessage, + Detail: fmt.Sprintf("API key expired at %q.", key.ExpiresAt.String()), }) return } @@ -216,7 +225,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool }) if err != nil { write(http.StatusInternalServerError, codersdk.Response{ - Message: fmt.Sprintf("API key couldn't update: %s.", err.Error()), + Message: loggedOutErrorMessage, + Detail: fmt.Sprintf("API key couldn't update: %s.", err.Error()), }) return } diff --git a/site/src/components/RequireAuth/RequireAuth.tsx b/site/src/components/RequireAuth/RequireAuth.tsx index 1776720d90a7a..9a51ab658d2db 100644 --- a/site/src/components/RequireAuth/RequireAuth.tsx +++ b/site/src/components/RequireAuth/RequireAuth.tsx @@ -13,9 +13,10 @@ export const RequireAuth: React.FC = ({ children }) => { const xServices = useContext(XServiceContext) const [authState] = useActor(xServices.authXService) const location = useLocation() - const navigateTo = location.pathname === "/" ? "/login" : embedRedirect(location.pathname) + const isHomePage = location.pathname === "/" + const navigateTo = isHomePage ? "/login" : embedRedirect(location.pathname) if (authState.matches("signedOut")) { - return + return } else if (authState.hasTag("loading")) { return } else { diff --git a/site/src/components/SignInForm/SignInForm.stories.tsx b/site/src/components/SignInForm/SignInForm.stories.tsx index 1937dfd066a5b..88378c4acd23a 100644 --- a/site/src/components/SignInForm/SignInForm.stories.tsx +++ b/site/src/components/SignInForm/SignInForm.stories.tsx @@ -51,6 +51,17 @@ WithLoginError.args = { }, } +export const WithGetUserError = Template.bind({}) +WithGetUserError.args = { + ...SignedOut.args, + loginErrors: { + [LoginErrors.GET_USER_ERROR]: makeMockApiError({ + message: "You are logged out. Please log in to continue.", + detail: "API Key is invalid.", + }), + }, +} + export const WithCheckPermissionsError = Template.bind({}) WithCheckPermissionsError.args = { ...SignedOut.args, @@ -70,6 +81,18 @@ WithAuthMethodsError.args = { }, } +export const WithGetUserAndAuthMethodsError = Template.bind({}) +WithGetUserAndAuthMethodsError.args = { + ...SignedOut.args, + loginErrors: { + [LoginErrors.GET_USER_ERROR]: makeMockApiError({ + message: "You are logged out. Please log in to continue.", + detail: "API Key is invalid.", + }), + [LoginErrors.GET_METHODS_ERROR]: new Error("Failed to fetch auth methods"), + }, +} + export const WithGithub = Template.bind({}) WithGithub.args = { ...SignedOut.args, diff --git a/site/src/components/SignInForm/SignInForm.tsx b/site/src/components/SignInForm/SignInForm.tsx index a4d75dad63dd7..2860f6f3cedd2 100644 --- a/site/src/components/SignInForm/SignInForm.tsx +++ b/site/src/components/SignInForm/SignInForm.tsx @@ -25,6 +25,7 @@ interface BuiltInAuthFormValues { export enum LoginErrors { AUTH_ERROR = "authError", + GET_USER_ERROR = "getUserError", CHECK_PERMISSIONS_ERROR = "checkPermissionsError", GET_METHODS_ERROR = "getMethodsError", } @@ -36,6 +37,7 @@ export const Language = { emailRequired: "Please enter an email address.", errorMessages: { [LoginErrors.AUTH_ERROR]: "Incorrect email or password.", + [LoginErrors.GET_USER_ERROR]: "Failed to fetch user details.", [LoginErrors.CHECK_PERMISSIONS_ERROR]: "Unable to fetch user permissions.", [LoginErrors.GET_METHODS_ERROR]: "Unable to fetch auth methods.", }, diff --git a/site/src/pages/LoginPage/LoginPage.tsx b/site/src/pages/LoginPage/LoginPage.tsx index 305012425130d..ecbf3952bbec8 100644 --- a/site/src/pages/LoginPage/LoginPage.tsx +++ b/site/src/pages/LoginPage/LoginPage.tsx @@ -4,7 +4,7 @@ import React, { useContext } from "react" import { Helmet } from "react-helmet" import { Navigate, useLocation } from "react-router-dom" import { Footer } from "../../components/Footer/Footer" -import { SignInForm } from "../../components/SignInForm/SignInForm" +import { LoginErrors, SignInForm } from "../../components/SignInForm/SignInForm" import { pageTitle } from "../../util/page" import { retrieveRedirect } from "../../util/redirect" import { XServiceContext } from "../../xServices/StateContext" @@ -28,6 +28,10 @@ export const useStyles = makeStyles((theme) => ({ }, })) +interface LocationState { + isRedirect: boolean +} + export const LoginPage: React.FC = () => { const styles = useStyles() const location = useLocation() @@ -35,12 +39,14 @@ export const LoginPage: React.FC = () => { const [authState, authSend] = useActor(xServices.authXService) const isLoading = authState.hasTag("loading") const redirectTo = retrieveRedirect(location.search) + const locationState = location.state ? (location.state as LocationState) : null + const isRedirected = locationState !== null ? locationState.isRedirect : false const onSubmit = async ({ email, password }: { email: string; password: string }) => { authSend({ type: "SIGN_IN", email, password }) } - const { authError, checkPermissionsError, getMethodsError } = authState.context + const { authError, getUserError, checkPermissionsError, getMethodsError } = authState.context if (authState.matches("signedIn")) { return @@ -57,9 +63,10 @@ export const LoginPage: React.FC = () => { redirectTo={redirectTo} isLoading={isLoading} loginErrors={{ - authError, - checkPermissionsError, - getMethodsError, + [LoginErrors.AUTH_ERROR]: authError, + [LoginErrors.GET_USER_ERROR]: isRedirected ? getUserError : null, + [LoginErrors.CHECK_PERMISSIONS_ERROR]: checkPermissionsError, + [LoginErrors.GET_METHODS_ERROR]: getMethodsError, }} onSubmit={onSubmit} /> From 8dcd9586208bc84356bf6564b20dc2c9e4954713 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 29 Jul 2022 02:57:00 +0000 Subject: [PATCH 2/5] change language to use sign-out --- coderd/httpmw/apikey.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 1917a114d303f..e1d56539c00a0 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -51,7 +51,7 @@ type OAuth2Configs struct { Github OAuth2Config } -const loggedOutErrorMessage string = "You are logged out. Please log in to continue." +const signedOutErrorMessage string = "You are signed out. Please sign in to continue." // ExtractAPIKey requires authentication using a valid API key. // It handles extending an API key if it comes close to expiry, @@ -85,7 +85,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool } if cookieValue == "" { write(http.StatusUnauthorized, codersdk.Response{ - Message: loggedOutErrorMessage, + Message: signedOutErrorMessage, Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenKey), }) return @@ -94,7 +94,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool // APIKeys are formatted: ID-SECRET if len(parts) != 2 { write(http.StatusUnauthorized, codersdk.Response{ - Message: loggedOutErrorMessage, + Message: signedOutErrorMessage, Detail: fmt.Sprintf("Invalid %q cookie API key format.", codersdk.SessionTokenKey), }) return @@ -104,14 +104,14 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool // Ensuring key lengths are valid. if len(keyID) != 10 { write(http.StatusUnauthorized, codersdk.Response{ - Message: loggedOutErrorMessage, + Message: signedOutErrorMessage, Detail: fmt.Sprintf("Invalid %q cookie API key id.", codersdk.SessionTokenKey), }) return } if len(keySecret) != 22 { write(http.StatusUnauthorized, codersdk.Response{ - Message: loggedOutErrorMessage, + Message: signedOutErrorMessage, Detail: fmt.Sprintf("Invalid %q cookie API key secret.", codersdk.SessionTokenKey), }) return @@ -120,7 +120,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool if err != nil { if errors.Is(err, sql.ErrNoRows) { write(http.StatusUnauthorized, codersdk.Response{ - Message: loggedOutErrorMessage, + Message: signedOutErrorMessage, Detail: "API key is invalid.", }) return @@ -136,7 +136,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool // Checking to see if the secret is valid. if subtle.ConstantTimeCompare(key.HashedSecret, hashed[:]) != 1 { write(http.StatusUnauthorized, codersdk.Response{ - Message: loggedOutErrorMessage, + Message: signedOutErrorMessage, Detail: "API key secret is invalid.", }) return @@ -182,7 +182,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool // Checking if the key is expired. if key.ExpiresAt.Before(now) { write(http.StatusUnauthorized, codersdk.Response{ - Message: loggedOutErrorMessage, + Message: signedOutErrorMessage, Detail: fmt.Sprintf("API key expired at %q.", key.ExpiresAt.String()), }) return @@ -225,7 +225,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool }) if err != nil { write(http.StatusInternalServerError, codersdk.Response{ - Message: loggedOutErrorMessage, + Message: signedOutErrorMessage, Detail: fmt.Sprintf("API key couldn't update: %s.", err.Error()), }) return From 52c23fb55a327bc424ec580c04dce383a3506e0b Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 29 Jul 2022 03:04:42 +0000 Subject: [PATCH 3/5] update sign out message --- coderd/httpmw/apikey.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index e1d56539c00a0..ead5759eb1a51 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -51,7 +51,7 @@ type OAuth2Configs struct { Github OAuth2Config } -const signedOutErrorMessage string = "You are signed out. Please sign in to continue." +const signedOutErrorMessage string = "You are signed out or your session has expired. Please sign in again to continue." // ExtractAPIKey requires authentication using a valid API key. // It handles extending an API key if it comes close to expiry, From ade96de46c204deec024473dac1f44708702a1b6 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 29 Jul 2022 15:57:33 +0000 Subject: [PATCH 4/5] update internal error message --- coderd/httpmw/apikey.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index ead5759eb1a51..9ababe6cd45ef 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -51,7 +51,10 @@ type OAuth2Configs struct { Github OAuth2Config } -const signedOutErrorMessage string = "You are signed out or your session has expired. Please sign in again to continue." +const ( + signedOutErrorMessage string = "You are signed out or your session has expired. Please sign in again to continue." + internalErrorMessage string = "An internal error occurred. Please try again or contact the system administrator." +) // ExtractAPIKey requires authentication using a valid API key. // It handles extending an API key if it comes close to expiry, @@ -126,8 +129,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool return } write(http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching API key by id.", - Detail: err.Error(), + Message: internalErrorMessage, + Detail: fmt.Sprintf("Internal error fetching API key by id. %s", err.Error()), }) return } @@ -154,7 +157,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool oauthConfig = oauth.Github default: write(http.StatusInternalServerError, codersdk.Response{ - Message: fmt.Sprintf("Unexpected authentication type %q.", key.LoginType), + Message: internalErrorMessage, + Detail: fmt.Sprintf("Unexpected authentication type %q.", key.LoginType), }) return } @@ -225,7 +229,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool }) if err != nil { write(http.StatusInternalServerError, codersdk.Response{ - Message: signedOutErrorMessage, + Message: internalErrorMessage, Detail: fmt.Sprintf("API key couldn't update: %s.", err.Error()), }) return @@ -238,8 +242,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool roles, err := db.GetAuthorizationUserRoles(r.Context(), key.UserID) if err != nil { write(http.StatusUnauthorized, codersdk.Response{ - Message: "Internal error fetching user's roles.", - Detail: err.Error(), + Message: internalErrorMessage, + Detail: fmt.Sprintf("Internal error fetching user's roles. %s", err.Error()), }) return } From 4bcf5e722249d1072379bf99ba3dfecb667e6950 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 29 Jul 2022 16:19:48 +0000 Subject: [PATCH 5/5] simplify conditional --- site/src/pages/LoginPage/LoginPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/LoginPage/LoginPage.tsx b/site/src/pages/LoginPage/LoginPage.tsx index ecbf3952bbec8..b84dac9c87106 100644 --- a/site/src/pages/LoginPage/LoginPage.tsx +++ b/site/src/pages/LoginPage/LoginPage.tsx @@ -40,7 +40,7 @@ export const LoginPage: React.FC = () => { const isLoading = authState.hasTag("loading") const redirectTo = retrieveRedirect(location.search) const locationState = location.state ? (location.state as LocationState) : null - const isRedirected = locationState !== null ? locationState.isRedirect : false + const isRedirected = locationState ? locationState.isRedirect : false const onSubmit = async ({ email, password }: { email: string; password: string }) => { authSend({ type: "SIGN_IN", email, password })