Skip to content

fix: handle getUser error #3285

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

Merged
merged 6 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions coderd/httpmw/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ type OAuth2Configs struct {
Github OAuth2Config
}

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,
// updating the last used time in the database.
Expand Down Expand Up @@ -83,15 +88,17 @@ 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: signedOutErrorMessage,
Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenKey),
})
return
}
parts := strings.Split(cookieValue, "-")
// 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: signedOutErrorMessage,
Detail: fmt.Sprintf("Invalid %q cookie API key format.", codersdk.SessionTokenKey),
})
return
}
Expand All @@ -100,27 +107,30 @@ 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: signedOutErrorMessage,
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: signedOutErrorMessage,
Detail: fmt.Sprintf("Invalid %q cookie API key secret.", codersdk.SessionTokenKey),
})
return
}
key, err := db.GetAPIKeyByID(r.Context(), keyID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
write(http.StatusUnauthorized, codersdk.Response{
Message: "API key is invalid.",
Message: signedOutErrorMessage,
Detail: "API key is invalid.",
})
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
}
Expand All @@ -129,7 +139,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: signedOutErrorMessage,
Detail: "API key secret is invalid.",
})
return
}
Expand All @@ -146,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
}
Expand Down Expand Up @@ -174,7 +186,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: signedOutErrorMessage,
Detail: fmt.Sprintf("API key expired at %q.", key.ExpiresAt.String()),
})
return
}
Expand Down Expand Up @@ -216,7 +229,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: internalErrorMessage,
Detail: fmt.Sprintf("API key couldn't update: %s.", err.Error()),
})
return
}
Expand All @@ -228,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
}
Expand Down
5 changes: 3 additions & 2 deletions site/src/components/RequireAuth/RequireAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ export const RequireAuth: React.FC<RequireAuthProps> = ({ 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 <Navigate to={navigateTo} />
return <Navigate to={navigateTo} state={{ isRedirect: !isHomePage }} />
Copy link
Contributor

Choose a reason for hiding this comment

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

does isRedirect replicate the functionality of embedRedirect?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we could use one both to know whether to show the error and where to go next, but it might be better to keep them separate like this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, we’d probably have to retrieve the redirect in the component but that didn’t seem like a very solid approach.

} else if (authState.hasTag("loading")) {
return <FullScreenLoader />
} else {
Expand Down
23 changes: 23 additions & 0 deletions site/src/components/SignInForm/SignInForm.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions site/src/components/SignInForm/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface BuiltInAuthFormValues {

export enum LoginErrors {
AUTH_ERROR = "authError",
GET_USER_ERROR = "getUserError",
CHECK_PERMISSIONS_ERROR = "checkPermissionsError",
GET_METHODS_ERROR = "getMethodsError",
}
Expand All @@ -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.",
},
Expand Down
17 changes: 12 additions & 5 deletions site/src/pages/LoginPage/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -28,19 +28,25 @@ export const useStyles = makeStyles((theme) => ({
},
}))

interface LocationState {
isRedirect: boolean
}

export const LoginPage: React.FC = () => {
const styles = useStyles()
const location = useLocation()
const xServices = useContext(XServiceContext)
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
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to use a ternary because of types? Or can we:

Suggested change
const locationState = location.state ? (location.state as LocationState) : null
const locationState = location.state ?? null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

location.state can be null, but since we are typecasting it, the compiler thinks it is never null, so we are typecasting it only if it is not null.

const isRedirected = locationState ? 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 <Navigate to={redirectTo} replace />
Expand All @@ -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}
/>
Expand Down