基于Graphcool-Yoga与Prisma Bindings的Passport多策略认证配置问题
Alright, let's break down how to implement full authentication with Passport's Local, Bearer, GitHub, and Twitter strategies in your Graphcool-Yoga + Prisma Bindings project. I'll walk you through each step, including fixing your existing user resolver and completing the auth.js config.
1. First, Install Required Dependencies
Start by adding all the Passport strategies and supporting packages:
npm install passport passport-local passport-http-bearer passport-github2 passport-twitter bcryptjs express-session
2. Complete Passport Configuration (auth.js)
This file will handle all four authentication strategies, plus user serialization/deserialization for session-based OAuth flows:
const passport = require('passport'); const LocalStrategy = require('passport-local').Strategy; const BearerStrategy = require('passport-http-bearer').Strategy; const GitHubStrategy = require('passport-github2').Strategy; const TwitterStrategy = require('passport-twitter').Strategy; const bcrypt = require('bcryptjs'); // Initialize Passport passport.initialize(); // --- Local Strategy (Username/Password Login) --- passport.use(new LocalStrategy( async (username, password, done) => { try { // Fetch user via Prisma Bindings const user = await context.db.query.user({ where: { username } }); if (!user) return done(null, false, { message: 'Invalid username' }); // Validate password hash const passwordMatch = await bcrypt.compare(password, user.passwordHash); if (!passwordMatch) return done(null, false, { message: 'Invalid password' }); // Return sanitized user data (exclude sensitive fields) return done(null, { id: user.id, username: user.username }); } catch (err) { return done(err); } } )); // --- Bearer Strategy (Token-Based Authentication) --- passport.use(new BearerStrategy( async (token, done) => { try { // Fetch user with matching token (requires a `token` field in your Prisma User model) const user = await context.db.query.user({ where: { token } }); if (!user) return done(null, false); return done(null, user, { scope: 'all' }); } catch (err) { return done(err); } } )); // --- GitHub OAuth Strategy --- passport.use(new GitHubStrategy({ clientID: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, callbackURL: "http://localhost:4000/auth/github/callback" // Update with your frontend/API URL }, async (accessToken, refreshToken, profile, done) => { try { // Check if user already exists via GitHub ID let user = await context.db.query.user({ where: { githubId: profile.id } }); // Create new user if they don't exist if (!user) { user = await context.db.mutation.createUser({ data: { username: profile.username, githubId: profile.id, email: profile.emails?.[0]?.value || null, active: true } }); } return done(null, user); } catch (err) { return done(err); } } )); // --- Twitter OAuth Strategy --- passport.use(new TwitterStrategy({ consumerKey: process.env.TWITTER_CONSUMER_KEY, consumerSecret: process.env.TWITTER_CONSUMER_SECRET, callbackURL: "http://localhost:4000/auth/twitter/callback" // Update with your frontend/API URL }, async (token, tokenSecret, profile, done) => { try { // Check if user already exists via Twitter ID let user = await context.db.query.user({ where: { twitterId: profile.id } }); // Create new user if they don't exist if (!user) { user = await context.db.mutation.createUser({ data: { username: profile.username, twitterId: profile.id, active: true } }); } return done(null, user); } catch (err) { return done(err); } } )); // Serialize/deserialize users for session-based OAuth flows passport.serializeUser((user, done) => done(null, user.id)); passport.deserializeUser(async (id, done) => { try { const user = await context.db.query.user({ where: { id } }); done(null, user); } catch (err) { done(err); } }); module.exports = passport;
3. Integrate Passport with Graphcool-Yoga
Update your Yoga server setup to include Passport middleware and fix your user resolver:
const { GraphQLServer } = require('graphcool-yoga'); const passport = require('./auth'); const session = require('express-session'); const db = require('./prisma-bindings'); // Your Prisma Bindings instance const resolvers = { Query: { user: async (root, args, context, info) => { // Fix: Use Passport's authenticate callback pattern instead of direct invocation return new Promise((resolve, reject) => { passport.authenticate('bearer', { session: false }, (err, user) => { if (err || !user) return reject(new Error('Not Authorised')); if (!args.id) return reject(new Error('Id cannot be empty')); // Fetch the requested user (add additional authorization checks if needed) context.db.query.user({ where: { id: args.id, active: true } }, info) .then(resolve) .catch(reject); })(context.request); // Pass Yoga's request object to Passport }); } }, Mutation: { // Local login mutation to return a bearer token localLogin: async (root, { username, password }, context) => { return new Promise((resolve, reject) => { passport.authenticate('local', { session: false }, (err, user) => { if (err || !user) return reject(new Error('Invalid credentials')); // Generate a secure token (use JWT in production instead of random bytes) const token = require('crypto').randomBytes(16).toString('hex'); // Update user's token in the database context.db.mutation.updateUser({ where: { id: user.id }, data: { token } }).then(updatedUser => { resolve({ token, user: updatedUser }); }).catch(reject); })({ body: { username, password } }); // Simulate Express request object }); } } }; // Initialize Yoga server const server = new GraphQLServer({ typeDefs: './schema.graphql', resolvers, context: req => ({ ...req, db }) }); // Add Express middleware for sessions (required for OAuth callbacks) server.express.use(session({ secret: 'your-strong-session-secret', // Replace with a secure secret in production resave: false, saveUninitialized: false })); server.express.use(passport.initialize()); server.express.use(passport.session()); // Enable session support for OAuth // Add OAuth callback routes server.express.get('/auth/github', passport.authenticate('github')); server.express.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/' }), (req, res) => { // Generate token and redirect to frontend with token const token = require('crypto').randomBytes(16).toString('hex'); req.db.mutation.updateUser({ where: { id: req.user.id }, data: { token } }).then(() => { res.redirect(`http://your-frontend-url?token=${token}`); }); } ); server.express.get('/auth/twitter', passport.authenticate('twitter')); server.express.get('/auth/twitter/callback', passport.authenticate('twitter', { failureRedirect: '/' }), (req, res) => { const token = require('crypto').randomBytes(16).toString('hex'); req.db.mutation.updateUser({ where: { id: req.user.id }, data: { token } }).then(() => { res.redirect(`http://your-frontend-url?token=${token}`); }); } ); server.start(() => console.log('Server running on http://localhost:4000'));
4. Update Your Prisma Datamodel
Add these fields to your datamodel.prisma to support all authentication methods:
type User { id: ID! @unique username: String! @unique passwordHash: String # For local login (never store plaintext passwords!) token: String @unique # For bearer token authentication githubId: String @unique # For GitHub OAuth twitterId: String @unique # For Twitter OAuth email: String active: Boolean! @default(true) # Add your other user fields here }
5. Key Production Notes
- Bearer Token Security: Replace random token generation with JWT (use the
jsonwebtokenpackage) to add expiration dates and signed claims. - Password Hashing: When registering users, always hash passwords with
bcrypt.hash()before storing them inpasswordHash. - Authorization Checks: Extend your resolvers to verify users have permission to access resources (e.g., a user should only be able to fetch their own data unless they're an admin).
- Environment Variables: Store API keys, secrets, and callback URLs in environment variables (never hardcode them!).
内容的提问来源于stack exchange,提问作者Bharat Chhabra

