BACK TO STORIESSTORIES · 読み物

Login con Twitch in Next.js e Auth.js

nextjs twitch authentication

Login con Twitch in Next.js e Auth.js


Oggi costruiamo un'app Next.js dove l'utente entra col suo account Twitch e vede la lista dei canali che segue, con l'indicazione di chi è in live in quel momento. OAuth vero, API vere, niente scorciatoie: alla fine avrai un flusso di autenticazione completo che puoi riusare per qualsiasi provider.


I link utili prima di partire:



🔧 Setup: la console Twitch


Il primo passo è sulla console developer di Twitch: crea un account se non ce l'hai, poi registra una nuova applicazione. Il campo importante è l'OAuth Redirect URL, dove va inserito:

TEXT · コード
http://localhost:3000/api/auth/callback/twitch

È l'indirizzo a cui Twitch rispedirà l'utente dopo il login, ed è Auth.js a gestirlo per noi. Dalla console recupera Client ID e Client Secret e mettili in un file .env.local:

BASH · コード
AUTH_TWITCH_ID=il-tuo-client-id
AUTH_TWITCH_SECRET=il-tuo-client-secret
AUTH_SECRET=una-stringa-casuale-lunga

Quel AUTH_SECRET serve ad Auth.js per firmare le sessioni: generalo con npx auth secret o con qualsiasi generatore di stringhe casuali.

Occhio ai nomi delle variabili: in Next.js tutto ciò che ha il prefisso NEXT_PUBLIC_ finisce nel bundle del browser, leggibile da chiunque apra i DevTools. Client secret e chiavi di firma non devono mai averlo: le variabili AUTH_* qui sopra restano sul server, dove i segreti devono stare.

📦 Setup: il progetto


Creiamo il progetto e installiamo Auth.js (il pacchetto si chiama ancora next-auth, nella versione 5):

BASH · コード
npx create-next-app@latest twitch-follows
cd twitch-follows
npm install next-auth@beta

🔑 La configurazione di Auth.js


Tutta la configurazione vive in un file auth.ts nella radice del progetto: qui dichiariamo il provider Twitch (Auth.js ne ha uno pronto), gli scope che ci servono e due callback per portarci a casa il token di accesso.

TYPESCRIPT · コード
1// auth.ts
2import NextAuth from 'next-auth';
3import Twitch from 'next-auth/providers/twitch';
4
5export const { handlers, auth, signIn, signOut } = NextAuth({
6  providers: [
7    Twitch({
8      authorization: {
9        params: {
10          // oltre al login, chiediamo di leggere i canali seguiti
11          scope: 'openid user:read:email user:read:follows',
12        },
13      },
14    }),
15  ],
16  callbacks: {
17    jwt({ token, account }) {
18      // al primo login, salviamo il token Twitch e l'id utente
19      if (account) {
20        token.accessToken = account.access_token;
21        token.userId = account.providerAccountId;
22      }
23      return token;
24    },
25    session({ session, token }) {
26      // e li esponiamo nella sessione, per usarli lato server
27      session.accessToken = token.accessToken as string;
28      session.userId = token.userId as string;
29      return session;
30    },
31  },
32});

Tre cose da notare. Primo: il provider ufficiale basta e avanza, ci aggiungiamo solo gli scope extra (il predefinito gestisce il login, ma per leggere i canali seguiti serve user:read:follows). Secondo: client id e secret non compaiono nel codice, Auth.js li legge da solo dalle variabili AUTH_TWITCH_*. Terzo: i callback jwt e session sono il passaggio di mano del token: Twitch lo consegna ad Auth.js, noi lo travasiamo nella sessione per poterlo usare nelle chiamate API.


Il route handler è di una modestia imbarazzante:

TYPESCRIPT · コード
1// app/api/auth/[...nextauth]/route.ts
2import { handlers } from '@/auth';
3
4export const { GET, POST } = handlers;

🚪 La pagina di login


Niente use client, niente hook: una server action e un bottone.

TSX · コード
1// app/login/page.tsx
2import { signIn } from '@/auth';
3
4export default function LoginPage() {
5  return (
6    <main className="login">
7      <form
8        action={async () => {
9          'use server';
10          await signIn('twitch', { redirectTo: '/' });
11        }}
12      >
13        <button type="submit">Entra con Twitch</button>
14      </form>
15    </main>
16  );
17}

📡 Le chiamate alle API di Twitch


I canali seguiti si leggono da GET /channels/followed (è il motivo dello scope user:read:follows di prima), i dettagli dei canali da GET /users e le dirette in corso da GET /streams/followed. Le API di Twitch sono paginate, quindi ci scriviamo un piccolo helper che gira le pagine fino in fondo:

TYPESCRIPT · コード
1// lib/twitch.ts
2const TWITCH_API = 'https://api.twitch.tv/helix';
3
4async function twitchGetAll(path: string, accessToken: string) {
5  const items: any[] = [];
6  let cursor: string | undefined;
7
8  do {
9    const url = `${TWITCH_API}${path}${cursor ? `&after=${cursor}` : ''}`;
10    const response = await fetch(url, {
11      headers: {
12        'Client-Id': process.env.AUTH_TWITCH_ID!,
13        Authorization: `Bearer ${accessToken}`,
14      },
15    });
16
17    if (!response.ok) {
18      throw new Error(`Twitch ha risposto ${response.status}`);
19    }
20
21    const data = await response.json();
22    items.push(...data.data);
23    cursor = data.pagination?.cursor;
24  } while (cursor);
25
26  return items;
27}
28
29// i canali seguiti dall'utente
30export const getFollowedChannels = (userId: string, token: string) =>
31  twitchGetAll(`/channels/followed?user_id=${userId}&first=100`, token);
32
33// i dettagli dei canali (avatar, descrizione)
34export const getUsers = (ids: string[], token: string) =>
35  twitchGetAll(`/users?id=${ids.join('&id=')}`, token);
36
37// chi è in live adesso, tra i canali seguiti
38export const getFollowedStreams = (userId: string, token: string) =>
39  twitchGetAll(`/streams/followed?user_id=${userId}&first=100`, token);

🏠 La homepage


La pagina principale è un server component: legge la sessione con auth(), chiama le tre API e incrocia i risultati. Tutto sul server, il token non tocca mai il browser.

TSX · コード
1// app/page.tsx
2import { auth } from '@/auth';
3import { redirect } from 'next/navigation';
4import {
5  getFollowedChannels,
6  getFollowedStreams,
7  getUsers,
8} from '@/lib/twitch';
9
10export default async function Home() {
11  const session = await auth();
12  if (!session) redirect('/login');
13
14  const { accessToken, userId } = session;
15
16  const follows = await getFollowedChannels(userId, accessToken);
17  const details = await getUsers(
18    follows.map((f) => f.broadcaster_id),
19    accessToken
20  );
21  const streams = await getFollowedStreams(userId, accessToken);
22
23  const channels = details
24    .sort((a, b) => a.display_name.localeCompare(b.display_name))
25    .map((channel) => ({
26      ...channel,
27      stream: streams.find((s) => s.user_id === channel.id),
28    }));
29
30  return (
31    <main className="channels">
32      {channels.map((channel) => (
33        <article key={channel.id} className="channel">
34          <img src={channel.profile_image_url} alt={channel.display_name} />
35          <h2>{channel.display_name}</h2>
36          {channel.stream ? (
37            <p className="live">🔴 LIVE · {channel.stream.title}</p>
38          ) : (
39            <p className="offline">offline</p>
40          )}
41        </article>
42      ))}
43    </main>
44  );
45}

Il flusso completo: sessione → canali seguiti → dettagli (ordinati per nome) → live in corso → merge. Lo stile lo lascio a te: la struttura è tutta qui.

Una nota sul token scaduto: il token di Twitch dura qualche ora, e quando scade le API rispondono 401. La gestione minima è intercettare l'errore e rimandare l'utente al login (signOut seguito da redirect); quella seria usa il refresh token che Twitch fornisce, rinnovando l'access token nel callback jwt quando sta per scadere. Per un progetto dimostrativo la prima basta; sappi che la seconda esiste, prima di andare in produzione.

✅ Conclusione


Ricapitolando: un'app registrata sulla console Twitch, Auth.js che gestisce tutto il ballo OAuth con un provider e due callback, il token custodito lato server e tre chiamate API incrociate per costruire la pagina. Il bello di questo flusso è che è riutilizzabile pari pari: cambia il provider (Google, GitHub, Discord...), cambiano gli scope e gli endpoint, ma lo scheletro resta identico. Imparalo una volta con Twitch, e ogni "Accedi con" che incontrerai sarà una variazione sul tema.