Next.js 14 SSO Github (Supabase Auth)
Tags: nextjs, javascript, supabase, typescript
Published: 2024-05-18 12:16:09

En este documento, voy a aclarar cómo integrar como sistema de autenticación SSO de Supabase en nuestro proyecto de Next.js v14. Para ello, iremos explicando punto por punto, que ocurre en cada proceso desde que iniciamos el proyecto, hasta que podemos realizar nuestra autenticación correctamente.

Para aclarar, nuestra autenticación se realizará desde el servidor, por lo que esto nos complica un poco el asunto, pero gracias a las Server Actions podremos hacer uso del cliente de Supabase desde el propio servidor. 


Partiendo de un proyecto de Next.js ya creado e inicializado, vamos a instalar las siguientes dependencias
```
npm install @supabase/supabase-js @supabase/ssr

```

Nos enfocaremos, sobre todo, en **@supabase/ssr**, la cual es un submodulo de supabase, el cual nos provee una serie de herramientas, que podremos usar desde los server components, y de la cual, nos ayudaremos para nuestra autenticación.

A continuación, tenemos que configurar nuestras variables de entorno para conectarnos con supabase. Esta información las obtenemos al crear un proyecto en Supabase. Para ello, seguimos la guia de su web.

```
NEXT_PUBLIC_SUPABASE_URL=<your_supabase_project_url>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>
```

Una vez realizado esto, ya nos vamos a centrar mas en Next.js. Para ello, en primer lugar tendremos que crear una funcion que haga de cliente, el cual, se encargará de hacer de "api client" de supabase, pero que podríamos ejecutar en el servidor.

La función de este cliente será la de poder interacturar con las cookies en las distintas peticiones que haremos en nuestra aplicación. Esto se debe, a que cuando usemos este cliente para cualquier acceso de supabase, como es en nuestro caso para la autenticación (o para acceso a la base de datos), usaremos esto para leer la información del usuario.  

```ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { cookies } from 'next/headers';

export const createClient = () => {
  const cookieStore = cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value, ...options });
          } catch (error) {
            // The `set` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: '', ...options });
          } catch (error) {
            // The `delete` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  );
};


```


En este paso solo tenemos que llamar al cliente creado, y utilizar los metodos para realizar el login. Si nos damos cuenta, queremos ejecutarlo en un componente que se ejecuta en el servidor, por lo que tendremos que hacer uso a las Server Actions.

Para ello, creamos la server action por separado (se podría poner dentro del mismo componente, pero esto es preferencia personal).

```tsx
import { createClient } from '@/utils/supabase/server';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';

export const signIn = async () => {
  'use server';

  const supabase = await createClient();
  const origin = headers().get('origin');
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'github',
    options: {
      redirectTo: `${origin}/auth/callback`,
    },
  });

  if (error) {
    redirect('/error');
  } else {
    return redirect(data.url);
  }
};

```

De esta manera, usamos el cliente de supabase creado previamente que se pueda ejecutar en el servidor, seleccionamos nuestro proveedor previamente configurado (en mi caso, github).

Como hemos creado una redireccion hacia  **/auth/callback**, tenemos que crear esa ruta en nuestro proyecto. Esto se encarga de recoger la respuesta de la autenticación que hemos realizado en el paso anterior, comprobar que contiene el parametro _code_ de github a partir del codigo que nos devuelve supabase, y usando la funcion de supabase **exchangeCodeForSession**.
Esto es tambien conocido como Proof Key for Code Exchange o PKCE, o como hacer una autenticación mas segura. Cuando github nos de el visto bueno de que el usuario con el que hemos realizado la autenticación es correcta, hacemos el redirect hacia nuestro **/login** del paso anterior.

```tsx
import { NextResponse } from 'next/server';
import { createClient } from '@/utils/supabase/server';

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get('code');

  if (code) {
    const supabase = createClient();
    await supabase.auth.exchangeCodeForSession(code);
  }

  return NextResponse.redirect(`${origin}`);
}


```


Por último nos queda por ver, que ocurre cuando nuestro usuario expira, y supabase intenta hacer un refresh. Si miramos en nuestro navegador, en el Application > Cookies, han desaparecido.

Esto ocurre porque los server components, solo tienen acceso de lectura de las cookies, no de escritura. Por lo tanto, tenemos que usar el [middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) de Next.js, para poder hacer un refresh de las cookies antes de que los server components se ejecuten.

Por lo tanto, creamos en nuestra carpeta de utils/supabase lo siguiente.

```ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';

export async function updateSession(request: NextRequest) {
  let response = NextResponse.next({
    request: {
      headers: request.headers,
    },
  });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return request.cookies.get(name)?.value;
        },
        set(name: string, value: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value,
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value,
            ...options,
          });
        },
        remove(name: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value: '',
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value: '',
            ...options,
          });
        },
      },
    }
  );

  await supabase.auth.getUser();

  return { supabase, response };
}

```

El uso de **middleware** es esencial para que podamos usar la autenticación en cada petición. Este se ejecuta antes de que cada petición sea completada. Por lo tanto, comprobamos si antes de cada petición tenemos un objeto User, y filtramos que la pagina de login, para que no tengamos ningún problema de redirect.

La respuesta del usuario, es seteada de nuevo en las Cookies, haciendo de refresh token, ya que la información necesaria viaja a través de las cabeceras de nuestra petición.

Así que crearemos el middleware a nivel global de nuestro proyecto.
```ts
import { type NextRequest, NextResponse } from 'next/server';
import { updateSession } from '@/utils/supabase/middleware';

export async function middleware(request: NextRequest) {
  const { supabase, response } = await updateSession(request);
  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user && !request.nextUrl.pathname.startsWith('/login')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return response;
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

```


Y por ultimo, lo llamamos desde el front a nuestro server action. Al tratarse de un server action, según Next.js, tenemos que llamarlo mediante "action".

```jsx
import { signIn } from './actions';

export default function Login() {
  return (
    <main>
      <form action={signIn}>
        <button>Sign in Github</button>
      </form>
    </main>
  );
}

```