[node.js] Auth

Suyeon·2021년 5월 24일
0

Node.js

목록 보기
3/4

Error Handling

// User.json
const { isEmail } = require('validator');

const userSchema = new mongoose.Schema({
	email: {
		type: String,
		unique: true, // cannot assign error message to unique!
		required: true,
		validate: [isEmail, 'Please enter a valid email'] // (*) Second args is error message
	}
})

// authController.js
const handleError = (err) => {
	console.log(err.message, err.code);
	let errors = { email: '', password: '' };
	
	// duplicate error(unique)
	if(err.code == 11000) {
		errors.email = 'That email is aleady registered.'
		return errors;
	}

	// validation error
	if(err.message.includes('user validation failed')) {
		Object.values(err.errors).forEach(({ properties }) => {
			errors[properties.path] = properties.message;
		}) 
	}

	if(...) {...}
	return errors;
}

module.exports.signup_post = async (req, res) => {
	const { email, password } = req.body
	try {
		const user = await.User.create({ email, password });
		res.status(201).json(user);
	} catch(error) {
		const errors = handleError(errors) // (*)
		res.status(400).json({ errors });
	}
} 
  • validate가 실패했을 때, validate의 second arg의 메세지가 반환됨
  • unique 의 경우, [isEmail, '...']와 같이 에러메세지를 할당할 수가 없다. 대신 duplicate error code인 11000을 사용할 수 있다. (console.log)

Hash a password

Mongoose Hook

  • Mongoose Hook을 사용해서 document가 저장되기 전(pre) 혹은 저장된 후(post)에 특정한 작업을 할 수 있음
  • doc은 db에 저장되는 document를 의미함
// User.js
const userSchema = {...};

// Fire a function after doc saved to db 
userSchema.post('save', function(doc, next){
	console.log(doc)
	next();
})

// Fire a function before doc saved to db 
userSchema.pre('save', function(next){
	console.log(this) // 'this' points to an instace(const user = User.create({...}))
	next();
})

Hasing

  • salt를 추가하지 않을 경우, hasing algorithm을 reverse해서 해킹을 할 수 있음
const bycript = require('bycript') 

UserSchema.pre('save', async function (next) {
  const user = this;
  const saltRounds = 10;

  // Only hash the password if it has been modified OR is new
  if (user.isModified('password')) {
    user.password = await bcrypt.hash(user.password, saltRounds);
  }
  next();
});

  • cookie의 default 만료기간(expire)은 session으로 설정되어 있음(탭을 닫으면 쿠키 삭제됨)
app.get('/set-cookie', (req, res) => {
	res.setHeader('Set-CooKie', 'newCookie=true');
	res.send('You got a cookie');
	// Check cookie in browser
})
  • Cookie-parser 라이브러리를 사용하면, cookie object의 method를 더욱 간편하게 사용할 수 있음
  • res.cookie(name, value, {options}) option을 통해서 쿠키 만료 기간, secure, httpOnly등을 설정할 수 있음
  • production에서는 항상 https에서 cookie를 사용할 것 (secure: true)
  • httpOnly: true: 프론트엔드에서 쿠키에 접근할 수 없음
// app.js
const cookieParser = require(cookie-parser);
app.use(cookieParser());

app.get('/set-cookie', (req, res) => {
	// res.setHeader('Set-CooKie', 'newCookie=true');
	res.cookie('newCookie', true, { maxAge: 60, secure: true, httpOnly: true }); // (*)
	res.send('You got a cookie');
})

app.get('/get-cookie', (req, res) => {
	const cookies = req.cookies;
	res.json(cookies);
})

JSON Web Token

  • cookie안에 jwt을 저장해서 브라우저와 서버간에 주고 받음(request, response)

Signup

// frontend
import { useHistory } from "react-router-dom";

const handleSubmit = async(e) => {
	e.preventDefault();

	try {
		const res = await fetch('/signup', {
			method: 'POST',
			body: JSON.stringify({ email, password }),
			headers: { 'Content-Type': 'application/json' }	
		});
		
		const data = await res.json();
		if(data.user) {
			// If user is authenticated, redirect to homepage
			let history = useHistory();
			history.push('/')	
		}
	} catch(err) {
		console.log(err)
	}
}

// authController.js
const jwt = require('jsonwebtoken')
const cookieParser = require('cookie-parser');

const MAX_AGE = 3 * 24 * 60 * 60; // 3days
const createToken = id => { 
	return jwt.sign({ id }, 'suyeon jwt secret', {
		expiresIn: MAX_AGE
	})
}

module.exports.signup_post = async (req, res) => {
	const { email, password } = req.body

	try {
		const user = await.User.create({ email, password });
		const token = createToken(user._id); // (*)
		res.cookie('jwt', token, { httpOnly: true, maxAge: MAX_AGE * 1000 });
		res.status(201).json({ user: user._id });
	} catch(error) {
			const errors = handleError(errors)
			res.status(400).json({ errors });
	}
}   

Login

  • schema에서 static method를 정의할 수 있음
// authController.js
module.exports.login_post = async (req, res) => {
	const { email, password } = req.body

	try {
		const user = await User.login(email, password); // (*) static method
		const token = createToken(user._id); 
		res.cookie('jwt', token, { httpOnly: true, maxAge: MAX_AGE * 1000 });
		res.status(200).json({ user: user._id });
	} catch(err) {
			const errors = handleError(errors)
			res.status(400).json({ errors });
	}
}

// User.js
userSchema.statics.login = async function(email, password) {
	const user = await this.findOne({ email }); // this === User
	if(user) {
		const auth = await bcyrpt.compare(password, user.password);
		if(auth) {
			return user;
		}
		throw Error('Incorrect password') 
	}
	throw Error('Incorrect eamil')
}

Logout

// authRoutes.js
router.get('/logout', authController.logout_get);

// authController.js
module.exports.logout_get = (req, res) => {
	res.cookie('jwt', '', { maxAge: 1 });
	history.push('/');
};

Protected Routes

  • auth 를 체크하는 미들웨어를 생성한 후, 각 route에 미들웨어를 적용한다.
  • app.get('url', middleware, (res, req) ⇒ {...})
// middleware > authMiddleware.js
const jwt = require('jsonwebtoken');

const requireAuth = (req, res, next) => {
	const token = req.cookie.jwt;
	if(token) {
		jwt.verify(token, 'Suyeon secret', (error, decodedToken) => {
			if(error) {
				res.redirect('/login');
			} else {
				next();
			}
		})
	} else {
		res.redirect('/login');
	}
} 

module.exports = { requireAuth }

// app.js
app.get('/smoothies', requireAuth, (res, req) => res.render(...)) // (*)

Check Current User

  • current user를 체크해서 user의 정보(이메일등)을 view에 디스플레이할 수 있음
  • *middleware를 가장 먼저 위치시키면, 모든 request마다 middleware가 적용됨
// authMiddleware.js
const checkUser = async (req, res, next) => {
	const token = req.cookie.jwt;
	if(token) {
		jwt.verify(token, 'Suyeon secret', (error, decodedToken) => {
			if(error) {
				res.locals.user = null;
				console.log(error);
				next();
			} else {
				let user = await User.findById(decodedToken.id); // (*)
				res.locals.user = user; // res.locals.NAME
				next();
			}
		})
	} else {
		res.locals.user = null;
		next();
	}
}

// app.js
app.get('*', checkUser); // (*)
app.get('/', (req, res) => ...);
app.get('/blog', (req, res) => ...);
app.use(authRoutes);

// View
user.email
profile
Hello World.

0개의 댓글