Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

OAuth i Social Login - sojusze Imperium

Do bramy obozu przychodzi obywatel z prowincji Galia. Możesz kazać mu założyć nowe konto: wymyślić hasło, potwierdzić adres, przejść całą procedurę rekrutacji. Albo zawrzeć sojusz z Galią: jej urzędnicy potwierdzą tożsamość przybysza, a Ty przyjmiesz to potwierdzenie jak własne.

Ten drugi sposób nazywa się OAuth 2.0 - i to on stoi za przyciskiem "Zaloguj przez Google", który znasz z setek stron. Uczy się go w tym miejscu z jednego powodu: hasło obywatela nigdy nie trafia do Twojego obozu. Nie musisz go przechowywać, hashować ani chronić przed wyciekiem, bo nigdy go nie widzisz.

Cztery kroki sojuszu

Zanim spojrzymy na kod, prześledźmy, co dzieje się przy tym przycisku - bo to ta sekwencja jest sercem OAuth, a kod tylko ją obsługuje.

  1. Obywatel klika "Zaloguj przez Google" na Twojej stronie.
  2. Twoja aplikacja przekierowuje go do Google. Od tej chwili on rozmawia z Google, nie z Tobą - i tam, na stronie Google, podaje swoje hasło.
  3. Google odsyła go z powrotem pod ustalony adres, dołączając jednorazowy kod. Twój serwer wymienia ten kod na dane obywatela.
  4. Twoja aplikacja wystawia własny token JWT - od tego momentu obywatel porusza się po obozie na Twojej przepustce, a rola Google się kończy.

Krok czwarty jest tym, o którym najczęściej się zapomina. Google potwierdza tożsamość jeden raz, przy wejściu. Cała późniejsza praca aplikacji opiera się na Twojej własnej przepustce - dokładnie takiej, jaką poznałeś w lekcji o JWT.

Strategia - obsługa powrotu z Galii

Kod tej wymiany bierze na siebie strategia Passport. Szkielet znasz; nowa jest tylko treść metody

validate
:

1@Injectable()
2export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
3  constructor(
4    private configService: ConfigService,
5    private usersService: UsersService,
6  ) {
7    super({
8      clientID: configService.get('GOOGLE_CLIENT_ID'),
9      clientSecret: configService.get('GOOGLE_CLIENT_SECRET'),
10      callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
11      scope: ['email', 'profile'],
12    });
13  }
14
15  async validate(accessToken: string, refreshToken: string, profile: any) {
16    const { name, emails } = profile;
17
18    return this.usersService.findOrCreateSocialUser({
19      email: emails[0].value,
20      firstName: name.givenName,
21      provider: 'google',
22      providerId: profile.id,
23    });
24  }
25}

W

super()
opisujemy sam sojusz.
clientID
i
clientSecret
to dokumenty Twojej aplikacji wydane przez Google - czytane z konfiguracji, nigdy wpisane w kod.
callbackURL
to adres z kroku trzeciego, pod który Google odeśle obywatela; ten sam adres musisz zgłosić w panelu Google, inaczej przekierowanie zostanie odrzucone.
scope
określa, o co prosisz - tutaj tylko o adres e-mail i podstawowy profil.

Zasada dotycząca

scope
jest krótka: proś o minimum. Każde dodatkowe uprawnienie to jeden ekran zgody więcej dla użytkownika i jedna rzecz więcej, którą musisz chronić.

Metoda

validate
uruchamia się dopiero po powrocie z Google, z gotowym profilem obywatela. Zwrócony obiekt trafia - jak w każdej strategii - do
request.user
.

Dwa identyfikatory zamiast hasła

Zwróć uwagę na parę

provider
i
providerId
w danych, które przekazujemy dalej. To ona zastępuje hasło.

providerId
to identyfikator obywatela w systemie Google - niezmienny, w przeciwieństwie do adresu e-mail, który da się zmienić. Sam
providerId
jednak nie wystarcza, bo obywatel o tym samym numerze może istnieć u innego dostawcy. Dlatego zapamiętujemy parę: kto potwierdził i kogo potwierdził.

1async findOrCreateSocialUser(data: SocialUserDto): Promise<User> {
2  const existing = await this.usersRepository.findOne({
3    where: { provider: data.provider, providerId: data.providerId },
4  });
5
6  if (existing) {
7    return existing;
8  }
9
10  return this.usersRepository.save({
11    email: data.email,
12    firstName: data.firstName,
13    provider: data.provider,
14    providerId: data.providerId,
15  });
16}

Nazwa

findOrCreate
opisuje całą logikę: przy pierwszym logowaniu tworzymy wpis, przy każdym kolejnym go odnajdujemy. Zauważ, czego w tej encji nie ma - kolumny z hasłem. Obywatel przyjęty przez sojusz nigdy hasła nie podawał, więc nie ma czego zapisać. To zresztą pułapka, o którą łatwo się potknąć: jeśli Twoja kolumna
password
jest wymagana (
nullable: false
), zapis takiego użytkownika się nie powiedzie.

Dwa endpointy zamykające obieg

Po stronie kontrolera sojusz sprowadza się do dwóch adresów:

1@Controller('auth')
2export class AuthController {
3  @Get('google')
4  @UseGuards(AuthGuard('google'))
5  googleLogin() {
6    // przekierowanie realizuje guard
7  }
8
9  @Get('google/callback')
10  @UseGuards(AuthGuard('google'))
11  googleCallback(@Request() req) {
12    return this.authService.generateToken(req.user);
13  }
14}

Pierwszy endpoint ma puste ciało i to nie pomyłka - jego jedynym zadaniem jest uruchomienie guarda, który przekieruje użytkownika do Google. Kod w tej metodzie nigdy by się nie wykonał.

Drugi to adres z

callbackURL
. Gdy obywatel wraca, guard wymienia kod na profil, strategia zapisuje lub odnajduje użytkownika, a my na końcu wystawiamy własny token JWT. I tu domyka się krok czwarty: od tej chwili aplikacja nie potrzebuje już Google.

Podsumowanie

Sojusz zawarty, brama otwarta dla obywateli sąsiednich prowincji:

  • OAuth 2.0 pozwala logować się przez zewnętrzny serwis, a hasło nigdy nie trafia do Twojej aplikacji,
  • obieg ma cztery kroki: kliknięcie, przekierowanie do dostawcy, powrót z kodem, wystawienie własnego JWT,
  • rola dostawcy kończy się po zalogowaniu - dalej działa Twoja przepustka,
  • clientID
    i
    clientSecret
    czytamy z konfiguracji,
    callbackURL
    musi zgadzać się z adresem zgłoszonym u dostawcy,
  • w
    scope
    proś o minimum potrzebnych danych,
  • tożsamość zapamiętujesz jako parę
    provider
    +
    providerId
    - to ona zastępuje hasło, bo e-mail bywa zmieniany,
  • findOrCreate
    tworzy wpis przy pierwszym logowaniu i odnajduje go przy każdym kolejnym,
  • użytkownik z OAuth nie ma hasła, więc kolumna
    password
    nie może być wymagana,
  • endpoint startowy ma puste ciało - całą pracę wykonuje guard.

W następnej lekcji zajmiemy się sesjami i ciasteczkami, czyli drugą drogą do fortu - stanową alternatywą dla przepustek. A na razie zapamiętaj: OAuth to sojusz, w którym sąsiad ręczy za przybysza; Ty przyjmujesz to ręczenie raz i od razu wystawiasz własny dokument.

Przejdź do CodeWorlds