dev.ansango / blog

Configurar Next.js con Aws Congnito, Amplify

· 6 min de lectura

Configurar Next.js con Aws Congnito, Amplify

c60aa460db9b42ece91ac75fadd27b45_MD5.jpeg

Configuración Inicial de AWS Cognito

  1. Crear un User Pool en Cognito:

    • En la consola de AWS, ve a Cognito > User Pool > Create user pool.
    • Configura:
      • Application type: Traditional web application / SPA
      • Nombre del grupo: your-name-user-group.
      • Atributos: Habilita correo electrónico como nombre de usuario.
  2. Obtén las credenciales:

    • USER_POOL_ID
    • USER_POOL_CLIENT_ID

Crear Una aplicación Next.js

  1. Ejecuta el comando de creación (usa la última versión de Next.js):

    npx create-next-app@latest
    
  2. Sigue las indicaciones del CLI:

    • ✔️ Nombre del proyecto: my-next-auth-app (o el nombre que prefieras).
    • ✔️ ¿Usar TypeScript?: Sí (Yes).
    • ✔️ ¿Usar el directorio App Router?: Sí (Yes).
    • ✔️ Configurar ESLint?: Sí.
    • ✔️ Configurar Tailwind CSS?: Opcional (recomendado).
    • ✔️ Configurar src/ directory?: Sí (mejor organización).
    • ✔️ Configurar rutas de importación?: No (a menos que lo necesites).
  3. Navega al directorio del proyecto:

    cd my-next-auth-app
    

Configurar AWS Amplify en Next.js

  1. Instalar dependencias:

    npm install aws-amplify @aws-amplify/adapter-nextjs
    
  2. Configurar Amplify con Cognito: Crea un archivo componente para la auth config src/components/amplify-config.ts:

'use client';

import { Amplify, type ResourcesConfig } from 'aws-amplify';

export const authConfig: ResourcesConfig['Auth'] = {
  Cognito: {
    userPoolId: String(process.env.NEXT_PUBLIC_USER_POOL_ID),
    userPoolClientId: String(process.env.NEXT_PUBLIC_USER_POOL_CLIENT_ID)
  }
};

Amplify.configure(
  {
    Auth: authConfig
  },
  { ssr: true }
);

export default function ConfigureAmplifyClientSide() {
  return null;
}
  1. Inicializar Amplify en app/layout.tsx:

import ConfigureAmplifyClientSide from '@/components/amplify-cognito-config';
import './globals.css';

type RootProps = Readonly<{
  children: React.ReactNode;
}>;

export default function RootLayout({ children }: RootProps) {
  return (
    <html lang="en">
      <body className="antialiased">
        <ConfigureAmplifyClientSide />
        {children}
      </body>
    </html>
  );
}
  1. Variables de entorno (.env.local):
NEXT_PUBLIC_USER_POOL_ID=
NEXT_PUBLIC_USER_POOL_CLIENT_ID=

Protección De rutas y sesión

Para que nuestras vistas y api esten protegidas puedes configurar el middleware en Next.js creando lo siguiente en src/middleware.ts

import { type NextRequest, NextResponse } from 'next/server';

import { authenticatedUser } from '@/lib/amplify';

export async function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;

  const user = await authenticatedUser({ request, response: NextResponse.next() });

  if (pathname === '/' && !user) {
    const url = request.nextUrl.clone();
    url.pathname = '/auth/login';
    return NextResponse.redirect(url);
  }

  if (pathname.startsWith('/api/') && !user) {
    return new NextResponse(
      JSON.stringify({
        error: 'No autorizado'
      }),
      {
        status: 403,
        headers: {
          'Content-Type': 'application/json'
        }
      }
    );
  }

  if (pathname === '/auth/login' && user) {
    const url = request.nextUrl.clone();
    url.pathname = '/';
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/', '/dashboard/:path*', '/profile/:path*', '/api/:path*', '/auth/login']
};

Con esto nos aseguramos de que si un usuario esta en la ruta raiz y no esta autenticado le redirigiremos a login, si intenta hacer peticiones a nuestra api, recibira un 403

Como ves hay una funcion que comprueba en servidor si esta autenticado el usuario en cognito, para ello crearemos un archivo en src/lib/amplify/auth-user.ts

import type { NextServer } from '@aws-amplify/adapter-nextjs';
import { fetchAuthSession, getCurrentUser } from 'aws-amplify/auth/server';

import { runWithAmplifyServerContext } from './run-context';

export async function authenticatedUser(context: NextServer.Context) {
  return await runWithAmplifyServerContext({
    nextServerContext: context,
    operation: async (contextSpec) => {
      try {
        const session = await fetchAuthSession(contextSpec);
        if (!session.tokens) {
          return;
        }
        const user = {
          …(await getCurrentUser(contextSpec))
        };
        return user;
      } catch (error) {
        console.error(error);
      }
    }
  });
}

y una inicializacion del contexto por separado **`src/lib/amplify/run-context.ts

import { createServerRunner } from '@aws-amplify/adapter-nextjs';

export const { runWithAmplifyServerContext } = createServerRunner({
  config: {
    Auth: {
      Cognito: {
        userPoolId: String(process.env.NEXT_PUBLIC_USER_POOL_ID),
        userPoolClientId: String(process.env.NEXT_PUBLIC_USER_POOL_CLIENT_ID)
      }
    }
  }
});

Inicio, Registro y cierre de sesión

Para manejar peticiones en cliente de forma sencilla podemos instalar Tanstack Query para ahorarnos algo de codigo

Inicio de sesión

'use client';

import { useMutation } from '@tanstack/react-query';

import { handleSignIn } from '@/core';
import { QUERY_KEYS, type LoginFormValues } from '@/lib';

export const useSingIn = () => {
  return useMutation({
    mutationKey: [QUERY_KEYS.AUTH, QUERY_KEYS.AUTH_SIGNIN],
    mutationFn: (data: LoginFormValues) => handleSignIn(data),
    onSuccess: () => {
      window.location.href = '/';
    },
    onError: (error) => {
      alert(error.message);
    }
  });
};
import { signIn } from 'aws-amplify/auth';

import { handlerErrorMessage } from '@/lib';

export async function handleSignIn({ email, password }: { email: string; password: string }) {
  try {
    const signInResult = await signIn({
      username: String(email),
      password: String(password)
    });

    return signInResult;
  } catch (error) {
    throw new Error(handlerErrorMessage(error));
  }
}

Con estas funciones podras implementar un formulario que Inicie correctamente sesion en tu aplicacion

Registro

'use client';

import { useMutation } from '@tanstack/react-query';

import { handleSignUp } from '@/core';
import { QUERY_KEYS, type SignUpFormValues } from '@/lib';

export const useSingUp = () => {
  return useMutation({
    mutationKey: [QUERY_KEYS.AUTH, QUERY_KEYS.AUTH_SIGNUP],
    mutationFn: (data: LoginFormValues) => handleSignUp(data),
    onSuccess: () => {
      window.location.href = '/auth/signup/confirm';
    },
    onError: (error) => {
      alert(error.message);
    }
  });
};
import { signUp } from 'aws-amplify/auth';

import { handlerErrorMessage } from '@/lib';

export async function handleSignUp({ email, password }: { email: string; password: string }) {
  try {
    await signUp({
      username: email,
      password: password,
      options: {
        userAttributes: {
          email
        },
        // optional
        autoSignIn: true
      }
    });
  } catch (error) {
    throw new Error(handlerErrorMessage(error));
  }
}
'use client';

import { useMutation } from '@tanstack/react-query';

import { handleSignUpConfirm } from '@/core';
import { QUERY_KEYS, type SignUpFormValues } from '@/lib';

export const useSingUpConfirm = () => {
  return useMutation({
    mutationKey: [QUERY_KEYS.AUTH, QUERY_KEYS.AUTH_SIGNUP_CONFIRM],
    mutationFn: (data: LoginFormValues) => handleSignIn(data),
    onSuccess: () => {
      window.location.href = '/auth/login';
    },
    onError: (error) => {
      alert(error.message);
    }
  });
};
import { autoSignIn, confirmSignUp } from 'aws-amplify/auth';

import { handlerErrorMessage } from '@/lib';

export async function handleSignUp({ email, code }: { email: string; code: string }) {
  try {
    await confirmSignUp({
      username: email,
      confirmationCode: code
    });
    await autoSignIn()
  } catch (error) {
    throw new Error(handlerErrorMessage(error));
  }
}

Con estas funciones podras implementar el flujo de registro, recuerda crear las pantallas de registro y confirmacion de registro, ya que al usuario le llegara un codigo de confirmacion por email.

Cierre de sesión

'use client';

import { useMutation } from '@tanstack/react-query';

import { handleSignOut } from '@/core';

import { QUERY_KEYS } from './keys';

export const useSignout = () => {
  return useMutation({
    mutationKey: [QUERY_KEYS.AUTH, QUERY_KEYS.AUTH_SIGNOUT],
    mutationFn: handleSignOut,
    onSuccess: () => {
      window.location.href = '/';
    },
    onError: (error) => {
      alert(error.message);
    }
  });
};
import { signOut } from 'aws-amplify/auth';

import { handlerErrorMessage } from '@/lib';

export async function handleSignOut() {
  try {
    await signOut();
  } catch (error) {
    throw new Error(handlerErrorMessage(error));
  }
}

Con estas funciones podras implementar el cierre correcto de sesion.


Despliegue AWS Amplify

  1. AWS Amplify > Create new app > Github (u otro proveedor)
  2. Selecciona un repositorio y una rama > Next > Deploy
  3. Recuerda configurar tus variables de entorno
    • NEXT_PUBLIC_USER_POOL_ID=
    • NEXT_PUBLIC_USER_POOL_CLIENT_ID=
  4. Por ultimo actualiza tu amplify.yml en Build Settings
version: 1
frontend:
  phases:
    preBuild:
      commands:
        - nvm use 20
        - env | grep -e NEXT_PUBLIC_USER_POOL_ID >> .env.production
        - env | grep -e NEXT_PUBLIC_USER_POOL_CLIENT_ID >> .env.production
        - npm ci --cache .npm --prefer-offline
    build:
      commands:
        - npm run build
  artifacts:
    baseDirectory: .next
    files:
      - '**/*'
  cache:
    paths:
      - .next/cache/**/*
      - .npm/**/*