refactor auth to work cross domain and with http resources closes #100

This commit is contained in:
Milo Schwartz
2025-01-26 14:42:02 -05:00
parent 6050a0a7d7
commit 9f1f2910e4
27 changed files with 688 additions and 201 deletions

View File

@@ -5,14 +5,12 @@ import { Button } from "@app/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle
} from "@app/components/ui/card";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { AuthWithAccessTokenResponse } from "@server/routers/resource";
import { AxiosResponse } from "axios";
import { Loader2 } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
@@ -32,7 +30,17 @@ export default function AccessToken({
const [loading, setLoading] = useState(true);
const [isValid, setIsValid] = useState(false);
const api = createApiClient(useEnvContext());
const { env } = useEnvContext();
const api = createApiClient({ env });
function appendRequestToken(url: string, token: string) {
const fullUrl = new URL(url);
fullUrl.searchParams.append(
env.server.resourceSessionRequestParam,
token
);
return fullUrl.toString();
}
useEffect(() => {
if (!accessTokenId || !accessToken) {
@@ -51,7 +59,10 @@ export default function AccessToken({
if (res.data.data.session) {
setIsValid(true);
window.location.href = redirectUrl;
window.location.href = appendRequestToken(
redirectUrl,
res.data.data.session
);
}
} catch (e) {
console.error("Error checking access token", e);

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState, useSyncExternalStore } from "react";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
@@ -8,7 +8,6 @@ import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from "@/components/ui/card";
@@ -30,9 +29,6 @@ import {
Key,
User,
Send,
ArrowLeft,
ArrowRight,
Lock,
AtSign
} from "lucide-react";
import {
@@ -47,10 +43,8 @@ import { AxiosResponse } from "axios";
import LoginForm from "@app/components/LoginForm";
import {
AuthWithPasswordResponse,
AuthWithAccessTokenResponse,
AuthWithWhitelistResponse
} from "@server/routers/resource";
import { redirect } from "next/dist/server/api-utils";
import ResourceAccessDenied from "./ResourceAccessDenied";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
@@ -118,7 +112,9 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
const [otpState, setOtpState] = useState<"idle" | "otp_sent">("idle");
const api = createApiClient(useEnvContext());
const { env } = useEnvContext();
const api = createApiClient({ env });
function getDefaultSelectedMethod() {
if (props.methods.sso) {
@@ -169,6 +165,15 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
}
});
function appendRequestToken(url: string, token: string) {
const fullUrl = new URL(url);
fullUrl.searchParams.append(
env.server.resourceSessionRequestParam,
token
);
return fullUrl.toString();
}
const onWhitelistSubmit = (values: any) => {
setLoadingLogin(true);
api.post<AxiosResponse<AuthWithWhitelistResponse>>(
@@ -190,7 +195,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
const session = res.data.data.session;
if (session) {
window.location.href = props.redirect;
window.location.href = appendRequestToken(props.redirect, session);
}
})
.catch((e) => {
@@ -212,7 +217,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
setPincodeError(null);
const session = res.data.data.session;
if (session) {
window.location.href = props.redirect;
window.location.href = appendRequestToken(props.redirect, session);
}
})
.catch((e) => {
@@ -237,7 +242,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
setPasswordError(null);
const session = res.data.data.session;
if (session) {
window.location.href = props.redirect;
window.location.href = appendRequestToken(props.redirect, session);
}
})
.catch((e) => {
@@ -619,16 +624,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
</Tabs>
</CardContent>
</Card>
{/* {activeTab === "sso" && (
<div className="flex justify-center mt-4">
<p className="text-sm text-muted-foreground">
Don't have an account?{" "}
<a href="#" className="underline">
Sign up
</a>
</p>
</div>
)} */}
</div>
) : (
<ResourceAccessDenied />

View File

@@ -1,7 +1,6 @@
import {
AuthWithAccessTokenResponse,
GetResourceAuthInfoResponse,
GetResourceResponse
GetExchangeTokenResponse
} from "@server/routers/resource";
import ResourceAuthPortal from "./ResourceAuthPortal";
import { internal, priv } from "@app/lib/api";
@@ -12,9 +11,6 @@ import { verifySession } from "@app/lib/auth/verifySession";
import { redirect } from "next/navigation";
import ResourceNotFound from "./ResourceNotFound";
import ResourceAccessDenied from "./ResourceAccessDenied";
import { cookies } from "next/headers";
import { CheckResourceSessionResponse } from "@server/routers/auth";
import AccessTokenInvalid from "./AccessToken";
import AccessToken from "./AccessToken";
import { pullEnv } from "@app/lib/pullEnv";
@@ -48,7 +44,7 @@ export default async function ResourceAuthPage(props: {
// TODO: fix this
return (
<div className="w-full max-w-md">
{/* @ts-ignore */}
{/* @ts-ignore */}
<ResourceNotFound />
</div>
);
@@ -83,49 +79,41 @@ export default async function ResourceAuthPage(props: {
);
}
const allCookies = await cookies();
const cookieName =
env.server.resourceSessionCookieName + `_${params.resourceId}`;
const sessionId = allCookies.get(cookieName)?.value ?? null;
if (sessionId) {
let doRedirect = false;
try {
const res = await priv.get<
AxiosResponse<CheckResourceSessionResponse>
>(`/resource-session/${params.resourceId}/${sessionId}`);
if (res && res.data.data.valid) {
doRedirect = true;
}
} catch (e) {}
if (doRedirect) {
redirect(redirectUrl);
}
}
if (!hasAuth) {
// no authentication so always go straight to the resource
redirect(redirectUrl);
}
// convert the dashboard token into a resource session token
let userIsUnauthorized = false;
if (user && authInfo.sso) {
let doRedirect = false;
let redirectToUrl: string | undefined;
try {
const res = await internal.get<AxiosResponse<GetResourceResponse>>(
`/resource/${params.resourceId}`,
const res = await priv.post<
AxiosResponse<GetExchangeTokenResponse>
>(
`/resource/${params.resourceId}/get-exchange-token`,
{},
await authCookieHeader()
);
doRedirect = true;
if (res.data.data.requestToken) {
const paramName = env.server.resourceSessionRequestParam;
// append the param with the token to the redirect url
const fullUrl = new URL(redirectUrl);
fullUrl.searchParams.append(
paramName,
res.data.data.requestToken
);
redirectToUrl = fullUrl.toString();
}
} catch (e) {
userIsUnauthorized = true;
}
if (doRedirect) {
redirect(redirectUrl);
if (redirectToUrl) {
redirect(redirectToUrl);
}
}

View File

@@ -6,8 +6,8 @@ export function pullEnv(): Env {
nextPort: process.env.NEXT_PORT as string,
externalPort: process.env.SERVER_EXTERNAL_PORT as string,
sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
resourceSessionCookieName: process.env.RESOURCE_SESSION_COOKIE_NAME as string,
resourceAccessTokenParam: process.env.RESOURCE_ACCESS_TOKEN_PARAM as string
resourceAccessTokenParam: process.env.RESOURCE_ACCESS_TOKEN_PARAM as string,
resourceSessionRequestParam: process.env.RESOURCE_SESSION_REQUEST_PARAM as string
},
app: {
environment: process.env.ENVIRONMENT as string,

View File

@@ -7,8 +7,8 @@ export type Env = {
externalPort: string;
nextPort: string;
sessionCookieName: string;
resourceSessionCookieName: string;
resourceAccessTokenParam: string;
resourceSessionRequestParam: string;
},
email: {
emailEnabled: boolean;