Статті цієї серії
Щоб зробити статті короткими та зрозумілими, ми розділяємо цю тему на три частини.
- Налаштування nodeJS з TypeScript
- Налаштування MongoDB
- Налаштування локальної та соціальної автентифікації за допомогою passport.js
Passport.js — це потужний і гнучкий проміжний шар для автентифікації в Node.js. Він спрощує процес додавання автентифікації користувачів у ваші веб-застосунки.
Переваги passport.js включають:
- Зменшений час розробки: Спрощує впровадження автентифікації.
- Покращена безпека: Використовує найкращі практики для безпечної автентифікації.
- Покращений досвід користувача: Забезпечує плавні та послідовні потоки автентифікації.
Отже, давайте почнемо з установки passport. Для цього навчального посібника ми реалізуємо локальну автентифікацію за допомогою passport. Також у цій статті ми розглянемо, як обгортати запити.
Тож давайте почнемо з інсталяції. Наступна команда встановить passport та інші необхідні плагіни для налаштування автентифікації.
npm install passport passport-local express-session body-parser connect-mongo passport-google-oauth20 bcrypt
Далі налаштуємо його. Перш ніж ініціалізувати, ми повинні визначити стратегію, яку будемо використовувати для входу. Тут ми створюємо дві стратегії: локальну та Google. Інші доступні стратегії можна знайти тут
https://www.passportjs.org/packages/
Отже, ми створимо модель користувача, яка буде спільною для всіх резолверів. Створіть файл /graphql/modals/i-user.ts
export interface IUser {
id: string,
name: string,
email: string
}
Тепер ми створимо допоміжний файл, який будемо використовувати в усіх стратегіях.
Створіть файл /login_strategy/helpers.ts
import MongoStore from "connect-mongo";
import { IUser } from "../graphql/modals/i-user";
import config from './../loadConfig.js';
export const passportSerializer = (user: IUser, cb) => {
process.nextTick(function() {
return cb(null, {
id: user.id,
name: user.name,
email: user.email
});
});
}
export const passportDeserializer = (user: IUser, cb) => {
process.nextTick(function() {
return cb(null, user);
});
}
export const sessionConfig = {
secret: 'keyboard cat',
resave: false,
saveUninitialized: false,
cookie: { secure: false },
store: MongoStore.create({
mongoUrl: config.db.mongo.url,
collectionName: "sessions",
stringify: false,
autoRemove: "interval",
autoRemoveInterval: 1
})
}
Тепер час створити стратегії.
/login_strategy/local.ts
import { Strategy as LocalStrategy } from 'passport-local';
import { User } from '../repository/mongoDb/user';
import bcrypt from 'bcrypt';
import { v4 as uuid } from 'uuid';
const strategy = new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
},
async (email, password, done) => {
const user = await User.find({ email: email }).findOne();
if (!user) return done(null, false);
const match = await bcrypt.compare(password, user.password);
if (!match) return done(null, false);
return done(null, user);
}
)
export const createNewUser = async (req, res) => {
// Отримуємо дані з тіла запиту
const { name, email, password } = req.body;
// Базова перевірка введених даних (слід додати більш надійні перевірки)
if (!name || !email || !password) {
return res.status(400).json({ error: 'Не вистачає обов'язкових полів' });
}
const hashedPassword = bcrypt.hashSync(password, 10);
const newUser = new User({
id: uuid(),
name: name,
email: email,
password: hashedPassword,
provider: 'local',
providerId: ''
});
await newUser.save();
res.status(201).json({ message: 'Користувача створено успішно', status: true });
}
export default strategy;
/login_strategy/google.ts
import { Strategy } from "passport-google-oauth20";
import { v4 as uuid } from 'uuid';
import { User } from "../repository/mongoDb/user";
import config from '../loadConfig.js';
const strategy = new Strategy({
clientID: config.auth.google.GOOGLE_CLIENT_ID,
clientSecret: config.auth.google.GOOGLE_CLIENT_SECRET,
callbackURL: config.auth.google.callbackURL,
},
async function verify(accessToken, refreshToken, profile, cb) {
console.log('inside stragey', accessToken, refreshToken, profile)
const user = await User.find({ providerId: profile.id}).findOne();
if (user && user.id) {
return cb(null, user);
} else {
const newUser = new User({
id: uuid(),
name: profile.displayName,
email: profile.emails?.[0]?.value,
password: uuid(),
provider: 'google',
providerId: profile.id
});
const user = await newUser.save();
return cb(null, user)
}
}
);
export default strategy;
Щоб інтегрувати Google автентифікацію, потрібно створити клієнт OAuth і отримати облікові дані. Оновіть облікові дані в конфігураційному файлі. Для URL зворотного виклику ви можете встановити будь-який URL, на який Google буде перенаправляти після успішної автентифікації. Для цього навчального посібника ми вказуємо http://127.0.0.1:4000/auth/google/callback
Використовуйте наступне посилання для додаткової інформації.
[
Інтеграція Google Sign-In у ваш веб-застосунок | Автентифікація | Google для розробників
Попередження: Бібліотека Google Sign-In за бажанням використовує API FedCM, і їх використання стане обов'язковим.
Проведемо модифікацію резолвера користувача, щоб він повертав поточного авторизованого користувача.
import { User } from "../../repository/mongoDb/user.js";
import { Resolvers } from "../modals/generated/user.js";
import { v4 as uuid } from 'uuid';
export const resolvers: Resolvers = {
Query: {
currentUser: (parent, args, context) => {
// ми будемо інжектити контекст з інформацією про поточного користувача
// щоб усі функції могли використовувати поточну інформацію про логін
return context.user;
},
users: async (parent, args, context) => {
return await User.find({});
},
},
// Ми можемо прибрати мутацію тут, оскільки реєстрацію ми робимо не через
// GraphQL, оскільки ми робимо автентифікацію через GraphQL.
}
Тепер, коли ми створили всі необхідні файли, давайте з’єднаємо все це в index.ts
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import mongoose from 'mongoose';
import schema from './graphql/schema.js';
import express from 'express';
import cors from 'cors';
import session from 'express-session';
import passport from 'passport';
import { IUser } from './graphql/modals/i-user.js';
import localStrategy, { createNewUser } from './login_strategy/local.js';
import googleStrategy from './login_strategy/google.js';
import http from 'http';
import bodyParser from 'body-parser';
import { GraphQLError } from 'graphql';
import config from './loadConfig.js';
import { passportSerializer, passportDeserializer, sessionConfig } from './login_strategy/helpers.js';
import { GraphqlContext } from './graphql/modals/graphql_context.js';
declare global {
namespace Express {
interface User extends IUser {}
}
}
mongoose
.connect(config.db.mongo.url, {})
.then(() => {
console.log(`Db Connected`);
})
.catch(err => {
console.log(err.message);
});
// Необхідна логіка для інтеграції з Express
// налаштовуємо сесію та пов'язані параметри
const app = express();
const httpServer = http.createServer(app);
app.use(express.urlencoded({ extended: false }));
app.use(session(sessionConfig));
passport.serializeUser(passportSerializer);
passport.deserializeUser(passportDeserializer);
//Визначаємо стратегії входу
passport.use(localStrategy);
passport.use(googleStrategy);
passport.use(microsoftStrategy);
app.use(passport.initialize());
app.use(passport.session());
// Та ж ініціалізація ApolloServer, плюс плагін для нашого httpServer.
const server = new ApolloServer({
schema,
});
await server.start();
// Роутери для кожного сценарію входу
// локальна автентифікація
app.post('/login',
passport.authenticate('local', { failureRedirect: '/loginFailure' }),
function(req, res) {
res.json({message:"Успішно", user: req.user });
}
);
app.post('/signup', createNewUser);
// Google автентифікація
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] }));
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/loginFailure' }),
function(req, res) {
// Успішна автентифікація, переадресовуємо додому.
console.log(req.session, req.user)
res.redirect('/');
}
);
// Налаштовуємо наші середовища Express для обробки CORS, парсингу тіла,
// і нашу функцію expressMiddleware.
app.use(
'/',
cors(),
bodyParser.json({}),
expressMiddleware(server, {
context: async ({ req, res }) => {
// ми також можемо перевірити ролі/дозволи користувачів тут.
// Це зробить увесь GraphQL аутентифікованим.
// Якщо ви не хочете цього, ви можете повністю видалити цей
// проміжний шар
const user = req.user;
if (!user || !user.id) {
// Кидок `GraphQLError` дозволяє нам вказати HTTP статус код,
// стандартні `Error` матимуть статус 500 за замовчуванням
throw new GraphQLError('Користувач не авторизований', {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
});
}
return { user }
},
}),
);
const PORT = config.port || 4000;
await new Promise((resolve) => httpServer.listen({ port: PORT }));
Якщо ми запустимо додаток зараз, ми отримаємо наступні кінцеві точки, які будуть доступні через form-data (використовується для автентифікації) і повністю аутентифікований GraphQL.
Ось кілька прикладів запитів для створення і входу з локальною автентифікацією.
Створення локального користувача
==================
POST /signup
Content-Type: application/x-www-form-urlencoded
Тіло
email: "[email protected]"
password: "password"
name: "Noble"
Увійти як локальний користувач
===================
POST /login
Content-Type: application/x-www-form-urlencoded
Тіло
email: "[email protected]"
password: "password"
Вхід через Google / реєстрація
GET /auth/google
Повний код доступний у репозиторії
https://github.com/rock-o/nodejs-graphql-starter
Перекладено з: Configuring local and social authentication using passsport.js