Skip to content

Commit

Permalink
fix: ability to set cookies in trpc
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbrusegard committed Nov 17, 2024
1 parent 1570073 commit d186ffb
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 61 deletions.
14 changes: 2 additions & 12 deletions src/app/[locale]/auth/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { FeideLogo } from '@/components/assets/logos/FeideLogo';
import { FeideButton } from '@/components/auth/FeideButton';
import { Button } from '@/components/ui/Button';
import { Separator } from '@/components/ui/Separator';
import { api } from '@/lib/api/server';
import { Link } from '@/lib/locale/navigation';
import { FingerprintIcon } from 'lucide-react';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import ExternalLink from 'next/link';

export default async function SignInPage({
params,
Expand All @@ -15,7 +13,6 @@ export default async function SignInPage({
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations('auth');
const feideUrlHref = await api.auth.getFeideUrlHref();
return (
<div className='relative flex h-full flex-col transition-opacity duration-500'>
<div className='mb-4 space-y-2 text-center'>
Expand All @@ -25,14 +22,7 @@ export default async function SignInPage({
<Separator />
<div className='absolute bottom-0 left-0 w-full space-y-4'>
<p className='text-center font-montserrat'>{t('signInWith')}</p>
<Button
className='w-full bg-[#3FACC2]/90 hover:bg-[#3FACC2] dark:bg-[#222832] hover:dark:bg-[#222832]/40'
asChild
>
<ExternalLink href={feideUrlHref}>
<FeideLogo title='Feide' />
</ExternalLink>
</Button>
<FeideButton />
<Button
className='flex w-full gap-1 bg-primary/80 font-montserrat font-semibold text-black text-md dark:bg-primary/50 dark:text-white hover:dark:bg-primary/40'
asChild
Expand Down
57 changes: 31 additions & 26 deletions src/app/api/auth/feide/route.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,43 @@
import { env } from '@/env';
import { validateFeideAuthorizationCode } from '@/server/auth/feide';
import { validateFeideAuthorization } from '@/server/auth/feide';
import { cookies } from 'next/headers';
import { type NextRequest, NextResponse } from 'next/server';
import { OAuth2RequestError } from 'oslo/oauth2';

export async function GET(request: NextRequest) {
const url = new URL(request.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');

if (!code || !state) {
const cookieStore = await cookies();
const storedState = cookieStore.get('feide-state')?.value;
const codeVerifier = cookieStore.get('feide-code-verifier')?.value;
console.log('step 1');
console.log(code, state, storedState, codeVerifier);

if (!code || !state || !storedState || !codeVerifier) {
return NextResponse.json(null, { status: 400 });
}
try {
const tokens = await validateFeideAuthorizationCode(code, state);
if (!tokens) {
return NextResponse.json(null, { status: 500 });
}
const userInfoResponse = await fetch(env.FEIDE_USERINFO_ENDPOINT, {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
});
const userInfo = await userInfoResponse.json();
console.log(userInfo);
} catch (error) {
// the specific error message depends on the provider
if (error instanceof OAuth2RequestError) {
// invalid code
return new Response(null, {
status: 401,
});
}
return new Response(null, {
status: 500,
});

console.log('step 2');
if (state !== storedState) {
return NextResponse.json(null, { status: 403 });
}

console.log('step 3');
const tokens = await validateFeideAuthorization(code, codeVerifier);
if (!tokens) {
return NextResponse.json(null, { status: 500 });
}
console.log('step 4');
const userInfoResponse = await fetch(env.FEIDE_USERINFO_ENDPOINT, {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
});
console.log('step 5');
const userInfo = await userInfoResponse.json();
console.log(userInfo);
return new Response(null, {
status: 500,
});
}
5 changes: 3 additions & 2 deletions src/app/api/data/[trpc]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { createContext } from '@/server/api/context';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import type { NextRequest } from 'next/server';

const handleRequest = (req: NextRequest) =>
fetchRequestHandler({
function handleRequest(req: NextRequest) {
return fetchRequestHandler({
endpoint: '/api/data',
req,
router,
Expand All @@ -19,6 +19,7 @@ const handleRequest = (req: NextRequest) =>
}
: undefined,
});
}

export {
handleRequest as GET,
Expand Down
25 changes: 25 additions & 0 deletions src/components/auth/FeideButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client';
import { FeideLogo } from '@/components/assets/logos/FeideLogo';
import { Button } from '@/components/ui/Button';
import { api } from '@/lib/api/client';
import { useRouter } from 'next/navigation';

function FeideButton() {
const router = useRouter();
const signInMutation = api.auth.signInFeide.useMutation({
onSuccess: (data) => {
router.push(data);
},
});

return (
<Button
className='w-full bg-[#3FACC2]/90 hover:bg-[#3FACC2] dark:bg-[#222832] hover:dark:bg-[#222832]/40'
onClick={() => signInMutation.mutate()}
>
<FeideLogo title='Feide' />
</Button>
);
}

export { FeideButton };
9 changes: 2 additions & 7 deletions src/components/providers/TRPCProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { env } from '@/env';
import { api } from '@/lib/api/client';
import { createQueryClient } from '@/lib/api/queryClient';
import { type QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { loggerLink, unstable_httpBatchStreamLink } from '@trpc/client';
import { httpBatchLink, loggerLink } from '@trpc/client';
import { useState } from 'react';
import SuperJSON from 'superjson';

Expand Down Expand Up @@ -32,14 +32,9 @@ function TRPCProvider(props: { children: React.ReactNode }) {
process.env.NODE_ENV === 'development' ||
(op.direction === 'down' && op.result instanceof Error),
}),
unstable_httpBatchStreamLink({
httpBatchLink({
transformer: SuperJSON,
url: `${env.NEXT_PUBLIC_SITE_URL}/api/data`,
headers: () => {
const headers = new Headers();
headers.set('x-trpc-source', 'nextjs-react');
return headers;
},
}),
],
}),
Expand Down
30 changes: 25 additions & 5 deletions src/server/api/routers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { env } from '@/env';
import { publicProcedure } from '@/server/api/procedures';
import { RefillingTokenBucket } from '@/server/api/rate-limit/refillingTokenBucket';
import { createRouter } from '@/server/api/trpc';
import { getFeideAuthorizationUrl } from '@/server/auth/feide';
import { createFeideAuthorization } from '@/server/auth/feide';

import { TRPCError } from '@trpc/server';
import { headers } from 'next/headers';
import { cookies, headers } from 'next/headers';

const ipBucket = new RefillingTokenBucket<string>(5, 60);

const authRouter = createRouter({
getFeideUrlHref: publicProcedure.query(async () => {
signInFeide: publicProcedure.mutation(async () => {
const headerStore = await headers();
const clientIP = headerStore.get('X-Forwarded-For');

Expand All @@ -19,8 +20,27 @@ const authRouter = createRouter({
message: 'Rate limit exceeded. Please try again later.',
});
}
const feideUrl = await getFeideAuthorizationUrl();
return feideUrl.href;

const cookieStore = await cookies();
const { state, codeVerifier, url } = await createFeideAuthorization();
cookieStore.set('feide-state', state, {
path: '/',
httpOnly: true,
sameSite: 'lax',
maxAge: 60 * 10,
secure: env.NODE_ENV === 'production',
});
cookieStore.set('feide-code-verifier', codeVerifier, {
path: '/',
httpOnly: true,
sameSite: 'lax',
maxAge: 60 * 10,
secure: env.NODE_ENV === 'production',
});

console.log(cookieStore.getAll());

return url.href;
}),
});

Expand Down
20 changes: 11 additions & 9 deletions src/server/auth/feide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,22 @@ const feideOAuthClient = new OAuth2Client(
},
);

function getFeideAuthorizationUrl() {
async function createFeideAuthorization() {
const state = generateState();
const codeVerifier = generateCodeVerifier(); // Optional for PKCE flow

return feideOAuthClient.createAuthorizationURL({
const codeVerifier = generateCodeVerifier();
const url = await feideOAuthClient.createAuthorizationURL({
state,
scopes: ['openid', 'profile', 'email'],
codeVerifier,
});
return {
state,
codeVerifier,
url,
};
}

async function validateFeideAuthorizationCode(
code: string,
codeVerifier: string,
) {
async function validateFeideAuthorization(code: string, codeVerifier: string) {
try {
const tokens = await feideOAuthClient.validateAuthorizationCode(code, {
codeVerifier,
Expand All @@ -43,9 +44,10 @@ async function validateFeideAuthorizationCode(
} catch (error) {
if (error instanceof OAuth2RequestError) {
// probably invalid credentials etc
// will be handled by returning null
}
console.error(error);
}
}

export { getFeideAuthorizationUrl, validateFeideAuthorizationCode };
export { createFeideAuthorization, validateFeideAuthorization };

0 comments on commit d186ffb

Please sign in to comment.