Commit 1d00b53a authored by Grant's avatar Grant
Browse files

show proper login error details (fixes #35)

parent 95bbd633
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ import { ChatContext } from "../contexts/ChatContext";

import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
import { AuthErrors } from "./AuthErrors";

const Chat = lazy(() => import("./Chat/Chat"));

@@ -33,6 +34,7 @@ const AppInner = () => {

      <DebugModal />
      <SettingsSidebar />
      <AuthErrors />

      <ToastContainer position="top-left" />
    </>
+143 −0
Original line number Diff line number Diff line
import {
  Button,
  Link,
  Modal,
  ModalBody,
  ModalContent,
  ModalFooter,
  ModalHeader,
} from "@nextui-org/react";
import { useEffect, useState } from "react";

const Params = {
  TYPE: "auth_type",
  ERROR: "auth_error",
  ERROR_DESC: "auth_error_desc",
  CAN_RETRY: "auth_retry",
};

/**
 * Show popups that detail auth error messages
 * @returns
 */
export const AuthErrors = () => {
  const [params, setParams] = useState(
    new URLSearchParams(window.location.search)
  );

  const onClose = () => {
    const url = new URL(window.location.href);
    url.search = "";
    // window.history.replaceState({}, "", url.toString());

    setParams(new URLSearchParams(window.location.search));
  };

  return (
    <>
      <RPError
        isOpen={params.get(Params.TYPE) === "rp"}
        onClose={onClose}
        params={params}
      />
      <OPError
        isOpen={params.get(Params.TYPE) === "op"}
        onClose={onClose}
        params={params}
      />
    </>
  );
};

/**
 * This is for RP errors, which can be triggered by modifying data sent in callbacks
 *
 * These errors can typically be retried
 *
 * @param param0
 * @returns
 */
const RPError = ({
  isOpen,
  onClose,
  params,
}: {
  isOpen: boolean;
  onClose: () => void;
  params: URLSearchParams;
}) => {
  return (
    <Modal isOpen={isOpen} onOpenChange={onClose} isDismissable={false}>
      <ModalContent>
        {(onClose) => (
          <>
            <ModalHeader>Login Error</ModalHeader>
            <ModalBody>
              <b>Error:</b> {params.get(Params.ERROR)}
              <br />
              <br />
              <b>Error Description:</b> {params.get(Params.ERROR_DESC)}
            </ModalBody>
            <ModalFooter>
              <Button color="primary" href="/api/login" as={Link}>
                Login
              </Button>
            </ModalFooter>
          </>
        )}
      </ModalContent>
    </Modal>
  );
};

/**
 * This is for OP errors, these might not be retryable
 * @param param0
 * @returns
 */
const OPError = ({
  isOpen,
  onClose,
  params,
}: {
  isOpen: boolean;
  onClose: () => void;
  params: URLSearchParams;
}) => {
  const canRetry = params.has(Params.CAN_RETRY);
  const [error, setError] = useState(params.get(Params.ERROR));
  const [errorDesc, setErrorDesc] = useState(params.get(Params.ERROR_DESC));

  useEffect(() => {
    switch (params.get(Params.ERROR)) {
      case "invalid_grant":
        setErrorDesc("Invalid token, try logging in again");
        break;
    }
  }, [params]);

  return (
    <Modal isOpen={isOpen} onOpenChange={onClose} isDismissable={false}>
      <ModalContent>
        {(onClose) => (
          <>
            <ModalHeader>Login Error</ModalHeader>
            <ModalBody>
              <b>Error:</b> {error}
              <br />
              <br />
              <b>Error Description:</b> {errorDesc}
            </ModalBody>
            <ModalFooter>
              {canRetry && (
                <Button color="primary" href="/api/login" as={Link}>
                  Login
                </Button>
              )}
            </ModalFooter>
          </>
        )}
      </ModalContent>
    </Modal>
  );
};
+56 −25
Original line number Diff line number Diff line
@@ -4,6 +4,22 @@ import { OpenID } from "../lib/oidc";
import { TokenSet, errors as OIDC_Errors } from "openid-client";
import { Logger } from "../lib/Logger";

const ClientParams = {
  TYPE: "auth_type",
  ERROR: "auth_error",
  ERROR_DESC: "auth_error_desc",
  CAN_RETRY: "auth_retry",
};

const buildQuery = (obj: { [k in keyof typeof ClientParams]?: string }) => {
  const params = new URLSearchParams();
  for (const [k, v] of Object.entries(obj)) {
    const k_: keyof typeof ClientParams = k as any;
    params.set(ClientParams[k_], v);
  }
  return "?" + params.toString();
};

const app = Router();

app.get("/login", (req, res) => {
@@ -30,11 +46,14 @@ app.get("/callback", async (req, res) => {
    if (e instanceof OIDC_Errors.RPError) {
      // client error

      res.status(400).json({
        success: false,
        error: e.name,
        error_description: e.message,
      });
      res.redirect(
        "/" +
          buildQuery({
            TYPE: "rp",
            ERROR: e.name,
            ERROR_DESC: e.message,
          })
      );
      return;
    }

@@ -47,34 +66,46 @@ app.get("/callback", async (req, res) => {
          Logger.error(
            "OpenID is improperly configured. Cannot exchange tokens, do I have valid credetials?"
          );
          res.status(500).json({
            success: false,
            error: "internal server error",
            error_description: "I'm misconfigured.",
          });
          res.redirect(
            "/" +
              buildQuery({
                TYPE: "op",
                ERROR: "Internal Server Error",
                ERROR_DESC: "I'm misconfigured.",
              })
          );
          return;
        case "invalid_grant":
          res.status(400).json({
            success: false,
            error: "invalid_grant",
            error_description: "retry /api/login",
          });
          res.redirect(
            "/" +
              buildQuery({
                TYPE: "op",
                ERROR: "invalid_grant",
                CAN_RETRY: "true",
              })
          );
          return;
      }

      res.status(400).json({
        success: false,
        error: e.error,
        error_description: e.error_description,
      });
      res.redirect(
        "/" +
          buildQuery({
            TYPE: "op",
            ERROR: e.error,
            ERROR_DESC: e.error_description,
          })
      );
      return;
    }

    res.status(500).json({
      success: false,
      error: "unknown error",
      error_description: "report this",
    });
    res.redirect(
      "/" +
        buildQuery({
          TYPE: "op",
          ERROR: "unknown error",
          ERROR_DESC: "report this",
        })
    );
    return;
  }