Login con Twitch in Next.js e Auth.js

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:
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:
AUTH_TWITCH_ID=il-tuo-client-id
AUTH_TWITCH_SECRET=il-tuo-client-secret
AUTH_SECRET=una-stringa-casuale-lungaQuel 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 variabiliAUTH_*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):
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.
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:
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.
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:
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.
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 (signOutseguito da redirect); quella seria usa il refresh token che Twitch fornisce, rinnovando l'access token nel callbackjwtquando 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.