mirror of https://github.com/coder/coder.git
feat: log out and redirect user when converting to oidc (#8347)
* 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.
This commit is contained in:
parent
90a3debe3f
commit
2ee406d7b6
|
@ -632,12 +632,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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1110,6 +1110,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
|
||||
|
@ -1130,6 +1131,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 {
|
||||
|
@ -1292,18 +1294,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
|
||||
|
|
|
@ -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<React.PropsWithChildren<SignInFormProps>> = ({
|
|||
redirectTo,
|
||||
isSigningIn,
|
||||
error,
|
||||
info,
|
||||
onSubmit,
|
||||
initialTouched,
|
||||
}) => {
|
||||
|
@ -100,10 +102,15 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
|
|||
<strong>{commonTranslation.t("coder")}</strong>
|
||||
</h1>
|
||||
<Maybe condition={error !== undefined}>
|
||||
<div className={styles.error}>
|
||||
<div className={styles.alert}>
|
||||
<ErrorAlert error={error} />
|
||||
</div>
|
||||
</Maybe>
|
||||
<Maybe condition={Boolean(info) && info !== "" && error === undefined}>
|
||||
<div className={styles.alert}>
|
||||
<Alert severity="info">{info}</Alert>
|
||||
</div>
|
||||
</Maybe>
|
||||
<Maybe condition={passwordEnabled && showPasswordAuth}>
|
||||
<PasswordSignInForm
|
||||
onSubmit={onSubmit}
|
||||
|
|
|
@ -25,6 +25,9 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
|
|||
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 ? (
|
||||
<FullScreenLoader />
|
||||
|
@ -37,6 +40,7 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
|
|||
redirectTo={redirectTo}
|
||||
isSigningIn={isSigningIn}
|
||||
error={error}
|
||||
info={info}
|
||||
onSubmit={onSignIn}
|
||||
/>
|
||||
<footer className={styles.footer}>
|
||||
|
|
|
@ -5,8 +5,6 @@ import Box from "@mui/material/Box"
|
|||
import GitHubIcon from "@mui/icons-material/GitHub"
|
||||
import KeyIcon from "@mui/icons-material/VpnKey"
|
||||
import Button from "@mui/material/Button"
|
||||
import { useLocation } from "react-router-dom"
|
||||
import { retrieveRedirect } from "utils/redirect"
|
||||
import Typography from "@mui/material/Typography"
|
||||
import { convertToOAUTH } from "api/api"
|
||||
import { AuthMethods, LoginType, UserLoginType } from "api/typesGenerated"
|
||||
|
@ -27,19 +25,31 @@ type LoginTypeConfirmation =
|
|||
selectedType: LoginType
|
||||
}
|
||||
|
||||
export const redirectToOIDCAuth = (stateString: string, redirectTo: string) => {
|
||||
window.location.href = `/api/v2/users/oidc/callback?oidc_merge_state=${stateString}&redirect=${redirectTo}`
|
||||
export const redirectToOIDCAuth = (
|
||||
toType: string,
|
||||
stateString: string,
|
||||
redirectTo: string,
|
||||
) => {
|
||||
window.location.href = `/api/v2/users/${toType}/callback?oidc_merge_state=${stateString}&redirect=${redirectTo}`
|
||||
}
|
||||
|
||||
export const useSingleSignOnSection = () => {
|
||||
const location = useLocation()
|
||||
const redirectTo = retrieveRedirect(location.search)
|
||||
const [loginTypeConfirmation, setLoginTypeConfirmation] =
|
||||
useState<LoginTypeConfirmation>({ open: false, selectedType: undefined })
|
||||
|
||||
const mutation = useMutation(convertToOAUTH, {
|
||||
onSuccess: (data) => {
|
||||
redirectToOIDCAuth(data.state_string, encodeURIComponent(redirectTo))
|
||||
const loginTypeMsg =
|
||||
data.to_type === "github" ? "Github" : "OpenID Connect"
|
||||
redirectToOIDCAuth(
|
||||
data.to_type,
|
||||
data.state_string,
|
||||
// The redirect on success should be back to the login page with a nice message.
|
||||
// The user should be logged out if this worked.
|
||||
encodeURIComponent(
|
||||
`/login?info=Login type has been changed to ${loginTypeMsg}. Log in again using the new method.`,
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue