Skip to content

Instantly share code, notes, and snippets.

@pjchender
Last active October 23, 2019 03:47
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pjchender/bba7bb7bc819e6997d8a17b2a014c68f to your computer and use it in GitHub Desktop.
Save pjchender/bba7bb7bc819e6997d8a17b2a014c68f to your computer and use it in GitHub Desktop.
Learn to Use Passport(Passport 學習筆記)
// 匯入需要的模組
const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const path = require('path')
const logger = require('morgan')
const mongoose = require('mongoose')
const session = require('express-session')
const passport = require('./middleware/passport')
const MongoStore = require('connect-mongo')(session) // 直接執行並將 session 存進去,logout 後會自動刪除該 document
const dbconfig = require('./db')
// 和 mongoDB 連線
mongoose.connect(dbconfig.connection) // 等同於,mongoose.connect('mongodb://localhost:27017/bookworm')
const db = mongoose.connection
db.on('error', console.error.bind(console, 'connection error')) // mongo error handler
// 載入 express
const app = express()
// 設定 view engine 和模版路徑
app.set('view engine', 'pug')
app.set('views', path.join('./views'))
// middleware
app.use(logger('dev'))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(cookieParser())
app.use(express.static(path.join('./public'))) // 讀取 ./public 中的靜態檔案
// 使用 session 來追蹤使用者
app.use(session({
secret: 'I love NodeJS', // secret: 必要欄位,用來註冊 session ID cookie 的字串。如此將增加安全性,避免他人在瀏覽器中偽造 cookie。
resave: false, // resave: 不論是否 request 的過程中有無變更都重新將 session 儲存在 session store。
saveUninitialized: false, // saveUninitialized: 將 uninitialized session(新的、未被變更過的) 儲存在 session store 中。
store: new MongoStore({
mongooseConnection: db
})
}))
app.use(passport.initialize())
app.use(passport.session())
// 讓 userID 可以在 template 中被存取,名稱為 currentUser
app.use(function (req, res, next) {
res.locals.currentUser = req.session.userId // res.locals 屬性在所有 view 中都可以存取到
next() // 執行下一個 middleware
})
// 載入路由檔
const index = require('./routes/index')
app.use('/', index)
// catch 404 and forward to error handler
app.use(function (req, res, next) {
var err = new Error('File Not Found')
err.status = 404
next(err)
})
// error handler
// define as the last app.use callback
app.use(function (err, req, res, next) {
res.status(err.status || 500)
res.render('error', {
message: err.message,
error: {}
})
})
// listen on port 3000
app.listen(3000, function () {
console.log('Express app listening on port 3000')
})
// ./routes/index
const express = require('express')
const router = express.Router()
const User = require('../models/user')
const mid = require('../middleware') // 將自己寫的 middleware 載入
const passport = require('../middleware/passport')
// POST /login
router.post('/login', function (req, res, next) {
passport.authenticate('login', function (err, user, info){
if (err) return next(err)
if (!user) {
err = new Error('User not found')
res.status(409)
return next(err)
}
req.login(user, function (err) {
if (err) return next(err)
req.session.userId = user._id
return res.redirect('/profile')
})
})(req, res, next)
})
// POST /register
router.post('/register', function (req, res, next) {
passport.authenticate('signup', function (err, user, info) {
if (err) {
err = new Error('Singup Error')
err.status = 409
return next(err)
}
if (!user) {
err = new Error(info)
err.status = 409
return next(err)
}
req.login(user, function (err) {
if (err) return next(err)
console.log('User account created successfully')
req.session.userId = user._id
return res.redirect('/profile')
})
})(req, res, next)
})
const passport = require('passport')
const bcrypt = require('bcrypt') // hashing module
const LocalStrategy = require('passport-local')
const JwtStrategy = require('passport-jwt').Strategy
const ExtractJwt = require('passport-jwt').ExtractJwt
const User = require('../models/user')
const jwtConfig = require('../config/jwt')
/**
* passport.use('驗證策略名稱', '想建立的策略類型')
* passReqToCallback: 讓我們在後面的 callback 中可以使用 req 參數
*/
// Passport Initialization
passport.serializeUser(function (user, done) {
done(null, user._id)
})
passport.deserializeUser(function (id, done) {
User.findById(id, function (err, user) {
done(err, user)
})
})
let jwtStrategy = new JwtStrategy({
secreteOrKey: jwtConfig.secret,
jwtFromRequest: ExtractJwt.fromExtractors([
ExtractJwt.versionOneCompatibility({authScheme: 'Bearer'}),
ExtractJwt.fromAuthHeader()
])
}, function (payload, done) {
User.findById(payload.sub, function (err, user) {
if (err) return done(err)
if (!user) return done(null, false, {message: 'Wrong JWT Token'})
if (payload.aud !== user.email) return done(null, false, {message: 'Wrong JWT Token'})
const exp = payload.exp
const nbf = payload.nbf
const curr = ~~(new Date().getTime() / 1000)
if (curr > exp || curr < nbf) {
return done(null, false, 'Token Expired')
}
return done(null, user)
})
})
let loginStrategy = new LocalStrategy({
usernameField: 'email',
passReqToCallback: true
}, function (req, email, password, done) {
User.findOne({ email: email }, function (err, user) {
if (err) {
return done(err)
}
if (!user) {
return done(null, false, 'Username is not exists')
}
let isValidPassword = function (user, password) {
return bcrypt.compareSync(password, user.password)
}
if (!isValidPassword(user, password)) {
return done(null, false, 'Invalid Password')
}
return done(null, user)
})
})
let signupStrategy = new LocalStrategy({
usernameField: 'email',
passReqToCallback: true
}, function (req, email, password, done) {
if (password !== req.body.confirmPassword) {
return done(new Error('Confirmation is not match with password'))
}
// if (password.length < 8) {
// return done(new Error('Password must has 8 characters at least'))
// }
const findOrCreateUser = function () {
User.findOne({ email: email }, function (err, user) {
if (err) {
return done(err)
}
if (user) {
return done(null, false, 'Username Already Exists')
} else {
let newUser = new User()
newUser.email = email
newUser.password = bcrypt.hashSync(password, 10)
newUser.name = req.body.name
newUser.favoriteBook = req.body.favoriteBook
newUser.save(function (err, user) {
if (err) {
throw err
}
return done(null, user)
})
}
})
}
process.nextTick(findOrCreateUser)
})
passport.use('jwt', jwtStrategy)
passport.use('login', loginStrategy)
passport.use('signup', signupStrategy)
module.exports = passport
// ./models/user
const mongoose = require('mongoose') // ODM for Mongo
const UserSchema = new mongoose.Schema({
email: {
type: String,
required: true,
trim: true,
unique: true
},
name: {
type: String,
required: true,
trim: true
},
favoriteBook: {
type: String,
required: true,
trim: true
},
password: {
type: String,
required: true
}
})
var User = mongoose.model('User', UserSchema)
module.exports = User
@pjchender
Copy link
Author

pjchender commented Apr 14, 2017

[Passport 的使用]

  • 放在 routes 中

[觀念]

  • Strategies 要先定義好之後才能進入 routes
  • 當成功登入後可以得到 req.user 這個物件
  • 如果定義的 Strategy 驗證失敗,會在 callback 中回傳 err ,路由不會被呼叫,並回傳 401 Unauthorized 的 response

passport as a middleware

使用 app.post('/login', passport.authenticate('<strategy>'), middleware)

app.post('/login',
  passport.authenticate('local'),
  function (req, res) {
    //  如果這個函式執行了,表示認證成功
    //  可以透過 req.user 取得被認證的使用者
    res.redirect('/users/' + req.user.username)
  }
)

[客制化 callback]

  • 要注意 passport.authenticate 是寫在 middleware 的 callback 當中,如此才能拿到 req, res 物件
  • passport.authenticate 的 callback 中,如果認證失敗 user 為 false;如果錯誤發生,會設定 errinfo 則可以拿到 strategy 中 verify callback 所提供的更多訊息。
  • req.login('user', callback<err>) 來建立 session,若使用者登入成功,user 會被指定到 req.user(一般情況下,req.login() 會在 passport.authenticate 的 middleware 中被執行,但若是客制化的 callback 則要自己帶)。
  • req.logout() 會移除 req.user 這個屬性,並同時清除 login session(如果有的話)。
app.get('/login', function (req, res, next) {
  passport.authenticate('local', function (err, user, info) {
    if (err) { return next(err) }
    if (!user) { return res.redirect('/login') }
    req.logIn(user, function (err) {
      if (err) { return next(err) }
      return res.redirect('/users/' + user.username)
    })
  })(req, res, next)
})

[使用內建 callback]

使用自動轉導

app.post('/login', passport.authenticate('local', 
  {
    successRedirect: '/',
    failureRedirect: '/login',
  }
))

搭配 flash 做使用(需搭配 connect-flash 這個套件)

app.post('/login', passport.authenticate('local', 
  {
    successRedirect: '/',
    failureRedirect: '/login',
    successFlash: '自訂成功訊息',
    failureFlash: true,         // 可以得到為什麼失敗的訊息
    // failureFlash: '自訂錯誤訊息' 
  }
))

關閉 Session

  • 在預設的情況下,Passport 在登入之後會產生 Session,如果沒有需要的話,也可以透過 {session: false} 關閉
app.post('/login', passport.authenticate('local', 
  {
    successRedirect: '/',
    failureRedirect: '/login',
    session: false
  }
))

@pjchender
Copy link
Author

pjchender commented Apr 14, 2017

passport.js

  • 這裡放在 ./middleware/passport.js

[Session]

  • 在一個典型的 web 應用中,驗證訊息(credentials)只有在登入的時候會被傳送,如果驗證成功,會在 session 中記錄並儲存 cookies 在瀏覽器中。後續的 request 都不會在帶有 credentials,而是透過 cookie 來辨認 seesion ,為了要支援 login sessions,在 Passport 中會序列化(serialize)和反序列化(deserialize)在 session 中的使用者實例。
  • 在下面的例子中,只有使用者的 ID 序列化後存到 session,當後續 requests 時,透過 ID 來找到使用者,並存在 req.user
passport.serializeUser(function (user, done) {
  done(null, user.id)
})

passport.deserializeUser(function (id, done) {
  User.findById(id, function (err, user) {
    done(err, user)
  })
})

[LocalStrategy]

  • 使用 passport.use(<strategy>) 來設定 strategy
  • 在 Strategy 中需要使用 verify callback,當 passport authenticate 接收到一個 request 時,它會去解析 request 中和認證有關的訊息(credentials),接著它會把這些 credentials 作為代入 verify callback 的參數:
    -- 如果 credentials 有效(valid),則會呼叫 return done(null, user)
    -- 如果 credentials 無效,則會呼叫並可顯示錯誤訊息 return done(null, false, {message: 'Wrong Password'})
    -- 如果在過程中發生例外,例如連不上 db ,則會呼叫 return done(err)
  • 預設的情況下 LocalStrategy 會以 usernamepassword 當作驗證的欄位,如果有變更的話,可以透過usernameFieldpasswordField 來改變
// 這裡使用 passport-local 這個 Strategy
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy
const User = require('./models/user')

passport.use(new LocalStrategy({
  usernameField: 'email',
  passwordField: 'passwd'
},
  function (username, password, done) {
    User.findOne({ username: username }, function (err, user) {
      if (err) { return done(err) }
      if (!user) {
        return done(null, false, { message: 'Incorrect username.' })
      }
      if (!user.validPassword(password)) {
        return done(null, false, { message: 'Incorrect password.' })
      }
      return done(null, user)
    })
  }
))

[JwtStrategy]

  • new JwtStrategy(options, verify)
  • options
    -- secretOrKey: 必填欄位
    -- jwtFromRequest: 用來代入驗證的函式
    -- issuer: 可以驗證 iss
    -- audience : 可以驗證 aud
    -- algorithms: 列出允許的 algorithm
    -- ignoreExpiration: 如果設成 true 則不驗證過期日
    -- passReqToCallback: 如果設成 true 則可以在 verify 的 callback 中使用 req, verify(request, jwt_payload, done_callback)
  • verify 是一個 function verify(jwt_payload, done)
    -- payload 是解碼後的 JWT payload
    -- done 是一個 callback<error, user, infor>
  • 找出 JWT(Extractor)的方式包含
    -- fromHeader(header_name): 從指定的 http header name 中找 JWT
    -- fromBodyField(field_name): 從 body 的欄位中找 JWT
    -- fromUrlQueryParameter(param_name): 從 URL 的 query parameter 中找 JWT
    -- fromAuthHeaderWithScheme(auth_scheme): 從 authorization header 中找 JWT
    -- fromAuthHeader(): 以 scheme 'JWT' 尋找 authorization header(HTTP Header 的寫法要是{Authorization: JWT xxx.yyy.zzz})
    -- fromExtractors([array of extractor functions]) 可以用陣列的方式列出所有上述想要使用的方法
const JwtStrategy = require('passport-jwt').Strategy
const ExtractJwt = require('passport-jwt').ExtractJwt

const opts = {
  secreteOrKey: jwtConfig.secret,
  jwtFromRequest: ExtractJwt.fromExtractors([
    ExtractJwt.versionOneCompatibility({authScheme: 'Bearer'}),
    ExtractJwt.fromAuthHeader()
  ])
}

let jwtStrategy = new JwtStrategy(opts, function (payload, done) {
  User.findById(payload.sub, function (err, user) {
    if (err) return done(err)
    if (!user) return done(null, false, {message: 'Wrong JWT Token'})
    if (payload.aud !== user.email) return done(null, false, {message: 'Wrong JWT Token'})

    const exp = payload.exp
    const nbf = payload.nbf
    const curr = ~~(new Date().getTime() / 1000)
    if (curr > exp || curr < nbf) {
      return done(null, false, 'Token Expired')
    }
    return done(null, user)
  })
})

passport.use('jwt', jwtStrategy)

@pjchender
Copy link
Author

pjchender commented Apr 14, 2017

[app.js]

  • app.js

[middleware]

  • 使用 passport 的時候要記得在 middleware 中 app.use(passport.initialize())
  • 如果有使用 session(建議但非必須),則要使用 app.use(passport.session()),記得要放在 express.session() 的後面
app.configure(function () {
  app.use(express.static('public'))
  app.use(express.cookieParser())
  app.use(express.bodyParser())

  app.use(express.session({ secret: 'keyboard cat' }))

  app.use(passport.initialize())
  app.use(passport.session())

  app.use(app.router)
})

@pjchender
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment