Skip to content

Commit e94fe20

Browse files
authored
fix: handle getUser error (coder#3285)
1 parent 4658b3f commit e94fe20

File tree

5 files changed

+67
-20
lines changed

5 files changed

+67
-20
lines changed

coderd/httpmw/apikey.go

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ type OAuth2Configs struct {
5151
Github OAuth2Config
5252
}
5353

54+
const (
55+
signedOutErrorMessage string = "You are signed out or your session has expired. Please sign in again to continue."
56+
internalErrorMessage string = "An internal error occurred. Please try again or contact the system administrator."
57+
)
58+
5459
// ExtractAPIKey requires authentication using a valid API key.
5560
// It handles extending an API key if it comes close to expiry,
5661
// updating the last used time in the database.
@@ -83,15 +88,17 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
8388
}
8489
if cookieValue == "" {
8590
write(http.StatusUnauthorized, codersdk.Response{
86-
Message: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenKey),
91+
Message: signedOutErrorMessage,
92+
Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenKey),
8793
})
8894
return
8995
}
9096
parts := strings.Split(cookieValue, "-")
9197
// APIKeys are formatted: ID-SECRET
9298
if len(parts) != 2 {
9399
write(http.StatusUnauthorized, codersdk.Response{
94-
Message: fmt.Sprintf("Invalid %q cookie API key format.", codersdk.SessionTokenKey),
100+
Message: signedOutErrorMessage,
101+
Detail: fmt.Sprintf("Invalid %q cookie API key format.", codersdk.SessionTokenKey),
95102
})
96103
return
97104
}
@@ -100,27 +107,30 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
100107
// Ensuring key lengths are valid.
101108
if len(keyID) != 10 {
102109
write(http.StatusUnauthorized, codersdk.Response{
103-
Message: fmt.Sprintf("Invalid %q cookie API key id.", codersdk.SessionTokenKey),
110+
Message: signedOutErrorMessage,
111+
Detail: fmt.Sprintf("Invalid %q cookie API key id.", codersdk.SessionTokenKey),
104112
})
105113
return
106114
}
107115
if len(keySecret) != 22 {
108116
write(http.StatusUnauthorized, codersdk.Response{
109-
Message: fmt.Sprintf("Invalid %q cookie API key secret.", codersdk.SessionTokenKey),
117+
Message: signedOutErrorMessage,
118+
Detail: fmt.Sprintf("Invalid %q cookie API key secret.", codersdk.SessionTokenKey),
110119
})
111120
return
112121
}
113122
key, err := db.GetAPIKeyByID(r.Context(), keyID)
114123
if err != nil {
115124
if errors.Is(err, sql.ErrNoRows) {
116125
write(http.StatusUnauthorized, codersdk.Response{
117-
Message: "API key is invalid.",
126+
Message: signedOutErrorMessage,
127+
Detail: "API key is invalid.",
118128
})
119129
return
120130
}
121131
write(http.StatusInternalServerError, codersdk.Response{
122-
Message: "Internal error fetching API key by id.",
123-
Detail: err.Error(),
132+
Message: internalErrorMessage,
133+
Detail: fmt.Sprintf("Internal error fetching API key by id. %s", err.Error()),
124134
})
125135
return
126136
}
@@ -129,7 +139,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
129139
// Checking to see if the secret is valid.
130140
if subtle.ConstantTimeCompare(key.HashedSecret, hashed[:]) != 1 {
131141
write(http.StatusUnauthorized, codersdk.Response{
132-
Message: "API key secret is invalid.",
142+
Message: signedOutErrorMessage,
143+
Detail: "API key secret is invalid.",
133144
})
134145
return
135146
}
@@ -146,7 +157,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
146157
oauthConfig = oauth.Github
147158
default:
148159
write(http.StatusInternalServerError, codersdk.Response{
149-
Message: fmt.Sprintf("Unexpected authentication type %q.", key.LoginType),
160+
Message: internalErrorMessage,
161+
Detail: fmt.Sprintf("Unexpected authentication type %q.", key.LoginType),
150162
})
151163
return
152164
}
@@ -174,7 +186,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
174186
// Checking if the key is expired.
175187
if key.ExpiresAt.Before(now) {
176188
write(http.StatusUnauthorized, codersdk.Response{
177-
Message: fmt.Sprintf("API key expired at %q.", key.ExpiresAt.String()),
189+
Message: signedOutErrorMessage,
190+
Detail: fmt.Sprintf("API key expired at %q.", key.ExpiresAt.String()),
178191
})
179192
return
180193
}
@@ -216,7 +229,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
216229
})
217230
if err != nil {
218231
write(http.StatusInternalServerError, codersdk.Response{
219-
Message: fmt.Sprintf("API key couldn't update: %s.", err.Error()),
232+
Message: internalErrorMessage,
233+
Detail: fmt.Sprintf("API key couldn't update: %s.", err.Error()),
220234
})
221235
return
222236
}
@@ -228,8 +242,8 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
228242
roles, err := db.GetAuthorizationUserRoles(r.Context(), key.UserID)
229243
if err != nil {
230244
write(http.StatusUnauthorized, codersdk.Response{
231-
Message: "Internal error fetching user's roles.",
232-
Detail: err.Error(),
245+
Message: internalErrorMessage,
246+
Detail: fmt.Sprintf("Internal error fetching user's roles. %s", err.Error()),
233247
})
234248
return
235249
}

site/src/components/RequireAuth/RequireAuth.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ export const RequireAuth: React.FC<RequireAuthProps> = ({ children }) => {
1313
const xServices = useContext(XServiceContext)
1414
const [authState] = useActor(xServices.authXService)
1515
const location = useLocation()
16-
const navigateTo = location.pathname === "/" ? "/login" : embedRedirect(location.pathname)
16+
const isHomePage = location.pathname === "/"
17+
const navigateTo = isHomePage ? "/login" : embedRedirect(location.pathname)
1718
if (authState.matches("signedOut")) {
18-
return <Navigate to={navigateTo} />
19+
return <Navigate to={navigateTo} state={{ isRedirect: !isHomePage }} />
1920
} else if (authState.hasTag("loading")) {
2021
return <FullScreenLoader />
2122
} else {

site/src/components/SignInForm/SignInForm.stories.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ WithLoginError.args = {
5151
},
5252
}
5353

54+
export const WithGetUserError = Template.bind({})
55+
WithGetUserError.args = {
56+
...SignedOut.args,
57+
loginErrors: {
58+
[LoginErrors.GET_USER_ERROR]: makeMockApiError({
59+
message: "You are logged out. Please log in to continue.",
60+
detail: "API Key is invalid.",
61+
}),
62+
},
63+
}
64+
5465
export const WithCheckPermissionsError = Template.bind({})
5566
WithCheckPermissionsError.args = {
5667
...SignedOut.args,
@@ -70,6 +81,18 @@ WithAuthMethodsError.args = {
7081
},
7182
}
7283

84+
export const WithGetUserAndAuthMethodsError = Template.bind({})
85+
WithGetUserAndAuthMethodsError.args = {
86+
...SignedOut.args,
87+
loginErrors: {
88+
[LoginErrors.GET_USER_ERROR]: makeMockApiError({
89+
message: "You are logged out. Please log in to continue.",
90+
detail: "API Key is invalid.",
91+
}),
92+
[LoginErrors.GET_METHODS_ERROR]: new Error("Failed to fetch auth methods"),
93+
},
94+
}
95+
7396
export const WithGithub = Template.bind({})
7497
WithGithub.args = {
7598
...SignedOut.args,

site/src/components/SignInForm/SignInForm.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface BuiltInAuthFormValues {
2525

2626
export enum LoginErrors {
2727
AUTH_ERROR = "authError",
28+
GET_USER_ERROR = "getUserError",
2829
CHECK_PERMISSIONS_ERROR = "checkPermissionsError",
2930
GET_METHODS_ERROR = "getMethodsError",
3031
}
@@ -36,6 +37,7 @@ export const Language = {
3637
emailRequired: "Please enter an email address.",
3738
errorMessages: {
3839
[LoginErrors.AUTH_ERROR]: "Incorrect email or password.",
40+
[LoginErrors.GET_USER_ERROR]: "Failed to fetch user details.",
3941
[LoginErrors.CHECK_PERMISSIONS_ERROR]: "Unable to fetch user permissions.",
4042
[LoginErrors.GET_METHODS_ERROR]: "Unable to fetch auth methods.",
4143
},

site/src/pages/LoginPage/LoginPage.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import React, { useContext } from "react"
44
import { Helmet } from "react-helmet"
55
import { Navigate, useLocation } from "react-router-dom"
66
import { Footer } from "../../components/Footer/Footer"
7-
import { SignInForm } from "../../components/SignInForm/SignInForm"
7+
import { LoginErrors, SignInForm } from "../../components/SignInForm/SignInForm"
88
import { pageTitle } from "../../util/page"
99
import { retrieveRedirect } from "../../util/redirect"
1010
import { XServiceContext } from "../../xServices/StateContext"
@@ -28,19 +28,25 @@ export const useStyles = makeStyles((theme) => ({
2828
},
2929
}))
3030

31+
interface LocationState {
32+
isRedirect: boolean
33+
}
34+
3135
export const LoginPage: React.FC = () => {
3236
const styles = useStyles()
3337
const location = useLocation()
3438
const xServices = useContext(XServiceContext)
3539
const [authState, authSend] = useActor(xServices.authXService)
3640
const isLoading = authState.hasTag("loading")
3741
const redirectTo = retrieveRedirect(location.search)
42+
const locationState = location.state ? (location.state as LocationState) : null
43+
const isRedirected = locationState ? locationState.isRedirect : false
3844

3945
const onSubmit = async ({ email, password }: { email: string; password: string }) => {
4046
authSend({ type: "SIGN_IN", email, password })
4147
}
4248

43-
const { authError, checkPermissionsError, getMethodsError } = authState.context
49+
const { authError, getUserError, checkPermissionsError, getMethodsError } = authState.context
4450

4551
if (authState.matches("signedIn")) {
4652
return <Navigate to={redirectTo} replace />
@@ -57,9 +63,10 @@ export const LoginPage: React.FC = () => {
5763
redirectTo={redirectTo}
5864
isLoading={isLoading}
5965
loginErrors={{
60-
authError,
61-
checkPermissionsError,
62-
getMethodsError,
66+
[LoginErrors.AUTH_ERROR]: authError,
67+
[LoginErrors.GET_USER_ERROR]: isRedirected ? getUserError : null,
68+
[LoginErrors.CHECK_PERMISSIONS_ERROR]: checkPermissionsError,
69+
[LoginErrors.GET_METHODS_ERROR]: getMethodsError,
6370
}}
6471
onSubmit={onSubmit}
6572
/>

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy