Uwierzytelnianie z Sign In With Google

author-avatar
Kamil Foltyniak
5 min czytania02.06.2024
post-image

Tworzenie konta przez nowych użytkowników i logowanie się do systemu zazwyczaj wymaga podania co najmniej adresu email i hasła. Alternatywą dla tego typu rozwiązań jest dodatnie uwierzytelniania za pomocą zewnętrznych serwisów, które poświadczą wiarygodność użytkownika. Jedną z najbardziej popularnych opcji jest Sign In With Google i na tej usłudze skupimy się w tym artykule.

Do stworzenia prostej aplikacji wykorzystamy Next.js, Prismę i SQLite, ale będzie to o tyle uniwersalne, że spokojnie możesz wykorzystać do tego inne narzędzia.

Czym jest Sign In With Google i jakie korzyści daje

Sign In With Google w skrócie, ma na celu szybsze przeprowadzenie procesu uwierzytelniania w twojej aplikacji, na co składa się zarówno tworzenie nowego konta, jak i logowanie przez już istniejącego użytkownika. Takie rozwiązanie jest niezmiernie wygodne, ponieważ użytkownik nie musi wypełniać formularzu do rejestracji lub logowania, gdyż jest to zastępowane przez jeden guzik Zaloguj się przez Google.  Zamiast inputów do wpisywania hasła i adresu email wyświetlamy taki przycisk:

Screenshot from 2024-05-17 08-23-02.png

W kontekście formularza rejestracji może się zdarzyć, że będziemy wymagać od użytkownika podania dodatkowych danych (np. adres lub nr telefonu), więc w tym przypadku po uwierzytelnianiu przez  Sign In With Google wyświetlimy dodatkowy w formularz, na którym zbierzemy dodatkowe informacje.

Główne zalety tego rozwiązania:

  • otrzymujemy zaufane i bezpieczne źródło uwierzytelniania użytkowników
  • dostęp do danych osobistych jak email czy imię i nazwisko na etapie tworzenia konta
  • wygoda użytkowników, co przekłada się na większą konwersję na stronie

Jak to działa?

Mechanizm działania nie jest skomplikowany i cały proces można opisać w kilku krokach:

  1. Użytkownik klika w guzik "Zaloguj się przez Google"
  2.  Wyświetlany jest ekran zgód, na którym użytkownik potwierdza chęć uwierzytelnienia i przekazania danych profilowych do naszej aplikacji
  3. W odpowiedzi dostajemy ID token, który zawiera m. in. nazwę i email użytkownika
  4. Token wysyłamy do aplikacji backendowej, gdzie będzie on weryfikowany
  5. Po poprawnej weryfikacji dodajemy nowego użytkownika do bazy danych (jeżeli nie istnieje), tworzymy token JWT i zwracamy go do aplikacji frontendowej
  6.  Tak otrzymany token możemy przechowywać w Local Storage i wykorzystać do autoryzacji 

Punkt 5 i 6 nie musi wyglądać zawsze tak samo i zależy to od przyjętego sposobu uwierzytelniania w aplikacji (zamiast JWT mogą być sesje i cookies). W tym artykule przyjęliśmy prosty system tworzenia tokenów JWT dla zalogowanych użytkowników.

Stworzenie prostej aplikacji do uwierzytelniania

Do tyle jeżeli chodzi o teorię! Przejdźmy teraz do napisania aplikacji, w której zobaczymy, jak w kilku krokach zaimplementować Sign In With Google.

Założenie konta w Google API

W tym kroku stworzymy ekran zgód oraz otrzymamy unikalny klucz Client ID, który jest niezbędny do dodania przycisku logowania. Posłuży nam w inicjowaniu klienta na froncie oraz przy walidacji ID token na backendzie.

Tworzenie zgód

  1. Wchodzimy na stronę https://console.developers.google.com/apis
  2. Przechodzimy do zakładki OAuth consent screen i ustawiamy wartości
    • Name => Sign In With Google Test App
    • Support email & Developer contact information => podajemy nasz adres email
  3. Wybieramy UserType => External
  4. Klikamy Save and continue i przechodzmy cały formularz z domyślnymi wartościami

Tworzenie danych logowania

  1. Przechodzimy do zakładki Credentials
  2. Wybieramy Utwórz dane logowania => Identyfikator klienta OAuth
  3. Wybieramy Application type => Web application
  4. W sekcji Authorized JavaScript origins dodajemy dwa rekordy:
    • http://localhost
    • http://localhost:3000
  5. Klikamy Create

Aplikacja w Next.js

1. Zacznijmy od postawienia aplikacji (nazwijmy ją sign-in-with-google-app):

npx create-next-app@latest --ts

Teraz spróbujmy ją uruchomić:

cd sign-in-with-google-app
npm run dev

Aplikacja powinna odpalić się w przeglądarce na porcie 3000.

2. Następnym krokiem jest zainicjowanie klienta  Sign In With Google i dodanie przycisku logowania na ekranie:

// index.tsx

import Script from "next/script";

export default function Home() {
  // inicjujemy klienta Sign In With Google
  const initGoogleSignIn = () => {
    google.accounts.id.initialize({
      // dodajemy wygnenerowany wcześniej Client ID z Google API
      client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
      // callback wywoływany po zalogowaniu się przez użytkownika
      callback: handleCredentialResponse,
    });
    // renderujemy przycisk logowania
    google.accounts.id.renderButton(
      document.getElementById("googleSignInBtnWrapper")!,
      { type: "standard", shape: "pill" },
    );
  };

  const handleCredentialResponse = async (
    credentials: google.accounts.id.CredentialResponse,
  ) => {};

  // ładujemy skrypt i po jego wczytaniu inicjujemy klienta Sign In With Google
  return (
    <>
      <div id="googleSignInBtnWrapper" />
      <Script
        src="https://accounts.google.com/gsi/client"
        onReady={initGoogleSignIn}
      />
    </>
  );
}

Musimy jeszcze zainstalować odpowiednie typy dla google.accounts:

npm install --save @types/google.accounts

3. Gdy już mamy widoczny nasz przycisk na ekranie, możemy zabrać się za stworzenie prostego API, które będzie umożliwiało zalogowanie się i przechowywanie użytkowników w bazie danych. Do tego celu wykorzystamy narzędzie Prima:

npm install prisma --save-dev
npx prisma init --datasource-provider sqlite

4. Teraz dodajemy model User, który Prisma przemapuję na odpowiednią encję w bazie danych:

// prisma/schema.prisma

...

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  authProviderId String @unique
}

5. Następnie uruchamiamy migrację, która doda tabelę User:

npx prisma migrate dev --name init

6.  Pozostaje nam jeszcze dodanie endpointu do logowania użytkownika. Najpierw zainstalujemy niezbędne biblioteki:

npm i google-auth-library jsonwebtoken
npm i --save-dev @types/jsonwebtoken

Teraz możemy dodać logikę, która będzie odpowiadać za weryfikowanie ID token zalogowanego użytkownika i zwracać token JWT, który później możemy wykorzystać do autoryzacji.

// pages/api/login.ts

import type { NextApiRequest, NextApiResponse } from "next";
import { PrismaClient } from "@prisma/client";
import { OAuth2Client, TokenPayload } from "google-auth-library";
import jwt from "jsonwebtoken";

// inicjujemy klienta Prismy i OAuth2
const prisma = new PrismaClient();
const authClient = new OAuth2Client();

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  if (req.method === "POST") {
    const { idToken } = req.body;

    try {
      // weryfikujemy czy token ID jest na pewno poprawny
      const payload = await verifyIdToken(idToken);
      const userId = payload.sub;
      const userEmail = payload.email;

      let user = await prisma.user.findFirst({
        where: {
          authProviderId: userId,
        },
      });

      // jeżeli użytkownik nie istnieje, tworzymy go
      if (!user) {
        user = await prisma.user.create({
          data: {
            email: userEmail!,
            authProviderId: userId,
          },
        });
      }

      // generujemy token JWT
      const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET!);

      res.status(200).json({ token });
    } catch (error) {
      res.status(401).json({ message: "Login failed" });
    }
  }
}

async function verifyIdToken(idToken: string): Promise<TokenPayload> {
  // ta walidacja jest rekomendowana przez Google dla zwiększenia bezpieczeństwa,
  // ponieważ ID token mógł zostać zmieniony po stronie klienta
  const ticket = await authClient.verifyIdToken({
    idToken: idToken,
    audience: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
  });
  const payload = ticket.getPayload();

  if (!payload || !payload.sub || !payload.email) {
    throw new Error("Invalid token");
  }

  return payload;
}

7. Na koniec pozostaje nam tylko dodanie logiki do metody handleCredentialResponse, która wywoła endpoint /api/login:

export default function Home() {
  
  ...

  const handleCredentialResponse = async (
    credentials: google.accounts.id.CredentialResponse,
  ) => {
    const response = await fetch("/api/login", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ idToken: credentials.credential }),
    });
    const data = await response.json();

    console.log(data);
  };

  ...
}

Podsumowanie

Sign In With Google zwiększa komfort logowania się użytkownika na naszej stronie, a implementacja tego — jak mogliśmy się przekonać — nie jest zbyt złożona. W zaledwie kilku krokach możemy dodać tę wygodną formę uwierzytelniania w naszej aplikacji.

Link do kodu: https://github.com/folt3k/sign-in-with-google-test-app