Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ deps:
bun install --cwd frontend
go mod download

# Clean data
clean-data:
rm -rf data/

# Clean web UI build
clean-webui:
rm -rf internal/assets/dist
Expand Down
4 changes: 4 additions & 0 deletions cmd/tinyauth/tinyauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ func NewTinyauthCmdConfiguration() *config.Config {
},
},
},
OIDC: config.OIDCConfig{
PrivateKeyPath: "./tinyauth_oidc_key",
PublicKeyPath: "./tinyauth_oidc_key.pub",
},
Experimental: config.ExperimentalConfig{
ConfigFile: "",
},
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ code {
@apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all;
}

pre {
@apply bg-accent border border-border rounded-md p-2;
}

.lead {
@apply text-xl text-muted-foreground;
}
Expand Down
53 changes: 53 additions & 0 deletions frontend/src/lib/hooks/oidc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export type OIDCValues = {
scope: string;
response_type: string;
client_id: string;
redirect_uri: string;
state: string;
};

interface IuseOIDCParams {
values: OIDCValues;
compiled: string;
isOidc: boolean;
missingParams: string[];
}

const optionalParams: string[] = ["state"];

export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
let compiled: string = "";
let isOidc = false;
const missingParams: string[] = [];

const values: OIDCValues = {
scope: params.get("scope") ?? "",
response_type: params.get("response_type") ?? "",
client_id: params.get("client_id") ?? "",
redirect_uri: params.get("redirect_uri") ?? "",
state: params.get("state") ?? "",
};

for (const key of Object.keys(values)) {
if (!values[key as keyof OIDCValues]) {
if (!optionalParams.includes(key)) {
missingParams.push(key);
}
}
}

if (missingParams.length === 0) {
isOidc = true;
}

if (isOidc) {
compiled = new URLSearchParams(values).toString();
}
Comment thread
steveiliop56 marked this conversation as resolved.

return {
values,
compiled,
isOidc,
missingParams,
};
}
14 changes: 11 additions & 3 deletions frontend/src/lib/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,20 @@
"forgotPasswordTitle": "Forgot your password?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"errorSubtitleInfo": "The following error occurred while processing your request:",
"errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
}
"goToCorrectDomainTitle": "Go to correct domain",
"authorizeTitle": "Authorize",
"authorizeCardTitle": "Continue to {{app}}?",
"authorizeSubtitle": "Would you like to continue to this app? Please keep in mind that this app will have access to your email and other information.",
"authorizeLoadingTitle": "Loading...",
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
"authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds."
}
14 changes: 11 additions & 3 deletions frontend/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,20 @@
"forgotPasswordTitle": "Forgot your password?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"errorSubtitleInfo": "The following error occurred while processing your request:",
"errorSubtitle": "An error occurred while trying to perform this action. Please check your browser console or the app logs for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain"
}
"goToCorrectDomainTitle": "Go to correct domain",
"authorizeTitle": "Authorize",
"authorizeCardTitle": "Continue to {{app}}?",
"authorizeSubtitle": "Would you like to continue to this app? Please keep in mind that this app will have access to your email and other information.",
Comment thread
steveiliop56 marked this conversation as resolved.
Outdated
"authorizeLoadingTitle": "Loading...",
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
"authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds."
}
2 changes: 2 additions & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { AppContextProvider } from "./context/app-context.tsx";
import { UserContextProvider } from "./context/user-context.tsx";
import { Toaster } from "@/components/ui/sonner";
import { ThemeProvider } from "./components/providers/theme-provider.tsx";
import { AuthorizePage } from "./pages/authorize-page.tsx";

const queryClient = new QueryClient();

Expand All @@ -31,6 +32,7 @@ createRoot(document.getElementById("root")!).render(
<Route element={<Layout />} errorElement={<ErrorPage />}>
<Route path="/" element={<App />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/authorize" element={<AuthorizePage />} />
<Route path="/logout" element={<LogoutPage />} />
<Route path="/continue" element={<ContinuePage />} />
<Route path="/totp" element={<TotpPage />} />
Expand Down
129 changes: 129 additions & 0 deletions frontend/src/pages/authorize-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useUserContext } from "@/context/user-context";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Navigate, useNavigate } from "react-router";
import { useLocation } from "react-router";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardFooter,
} from "@/components/ui/card";
import { getOidcClientInfoScehma } from "@/schemas/oidc-schemas";
import { Button } from "@/components/ui/button";
import axios from "axios";
import { toast } from "sonner";
import { useOIDCParams } from "@/lib/hooks/oidc";
import { useTranslation } from "react-i18next";

export const AuthorizePage = () => {
const { isLoggedIn } = useUserContext();
const { search } = useLocation();
const { t } = useTranslation();
const navigate = useNavigate();

const searchParams = new URLSearchParams(search);
const {
values: props,
missingParams,
isOidc,
compiled: compiledOIDCParams,
} = useOIDCParams(searchParams);

const getClientInfo = useQuery({
queryKey: ["client", props.client_id],
queryFn: async () => {
const res = await fetch(`/api/oidc/clients/${props.client_id}`);
const data = await getOidcClientInfoScehma.parseAsync(await res.json());
return data;
},
enabled: isOidc,
});
Comment thread
steveiliop56 marked this conversation as resolved.

const authorizeMutation = useMutation({
mutationFn: () => {
return axios.post("/api/oidc/authorize", {
scope: props.scope,
response_type: props.response_type,
client_id: props.client_id,
redirect_uri: props.redirect_uri,
state: props.state,
});
},
mutationKey: ["authorize", props.client_id],
onSuccess: (data) => {
toast.info(t("authorizeSuccessTitle"), {
description: t("authorizeSuccessSubtitle"),
});
window.location.replace(data.data.redirect_uri);
},
onError: (error) => {
window.location.replace(
`/error?error=${encodeURIComponent(error.message)}`,
);
},
});

if (!isLoggedIn) {
return <Navigate to={`/login?${compiledOIDCParams}`} replace />;
}

if (missingParams.length > 0) {
return (
<Navigate
to={`/error?error=${encodeURIComponent(`Missing parameters: ${missingParams.join(", ")}`)}`}
replace
/>
);
}
Comment thread
steveiliop56 marked this conversation as resolved.
Outdated

if (getClientInfo.isLoading) {
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">
{t("authorizeLoadingTitle")}
</CardTitle>
<CardDescription>{t("authorizeLoadingSubtitle")}</CardDescription>
</CardHeader>
</Card>
);
}

if (getClientInfo.isError) {
return (
<Navigate
to={`/error?error=${encodeURIComponent(`Failed to load client information`)}`}
replace
/>
);
}
Comment thread
steveiliop56 marked this conversation as resolved.

return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">
{t("authorizeCardTitle", {
app: getClientInfo.data?.name || "Unknown",
})}
</CardTitle>
<CardDescription>{t("authorizeSubtitle")}</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2">
<Button
onClick={() => authorizeMutation.mutate()}
loading={authorizeMutation.isPending}
>
{t("authorizeTitle")}
</Button>
<Button
onClick={() => navigate("/")}
disabled={authorizeMutation.isPending}
variant="outline"
>
{t("cancelTitle")}
</Button>
</CardFooter>
</Card>
);
};
2 changes: 1 addition & 1 deletion frontend/src/pages/continue-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const ContinuePage = () => {
clearTimeout(auto);
clearTimeout(reveal);
};
}, []);
});

if (!isLoggedIn) {
return (
Expand Down
17 changes: 16 additions & 1 deletion frontend/src/pages/error-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,30 @@ import {
CardTitle,
} from "@/components/ui/card";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router";

export const ErrorPage = () => {
const { t } = useTranslation();
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const error = searchParams.get("error") ?? "";

return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">{t("errorTitle")}</CardTitle>
<CardDescription>{t("errorSubtitle")}</CardDescription>
<CardDescription className="flex flex-col gap-1.5">
{error ? (
<>
<p>{t("errorSubtitleInfo")}</p>
<pre>{error}</pre>
</>
) : (
<>
<p>{t("errorSubtitle")}</p>
</>
)}
</CardDescription>
Comment thread
steveiliop56 marked this conversation as resolved.
</CardHeader>
</Card>
);
Expand Down
Loading