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.
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.
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.
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.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.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.Sojusz zawarty, brama otwarta dla obywateli sąsiednich prowincji:
clientID i clientSecret czytamy z konfiguracji, callbackURL musi zgadzać się z adresem zgłoszonym u dostawcy,scope proś o minimum potrzebnych danych,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,password nie może być wymagana,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.