[express] passport + local

DaeChan Jo·2023년 7월 23일
0

library

목록 보기
1/4

passport는 웹 애플리케이션에 인증을 추가하는 간단하고 모듈화된 방법을 제공하는 Node.js용 인증 미들웨어이다. 다양한 인증 전력을 지원하고 모듈식 아키텍처를 통해 필요한 인증전략만 선택하고 구현할 수 있다

주요 개념

  • 인증 전략 (strategy)
    passport는 전략을 사용해 인증을 구현한다. 각 전략은 특정 인증방법을 처리하는 별도의 모듈이다

  • 미들웨어
    passport는 Node.js 미들웨어이다.

passport는 요청 인증이라는 단일 목적을 수행하도록 설계되었다
애플리케이션에 대한 데이터 액세스와 같은 관련 없는 세부 사항을 위임하면서 캡슐화한다.

구성

예시

app.post('/요청경로', passport.authenticate('전략'));

해당 경로로 요청이 들어오면 passport미들웨어의 authenticate()가 미리 설정해 둔 전략을 사용해 인증절차를 수행한다.
이 때 인증에 실패하면 HTTP status code 401 응답이 전송되고 스택의 추가함수는 호출되지않고 앱은 자동으로 종료된다.

즉, passport에서 가장 중요한 포인트는 '전략'에 있다고 볼 수 있다.
요청을 인증하는데 사용되는 메커니즘을 전략으로 구현하고, API에 필요한 인증을 미리 구성해둔 전략을 가져와 미들웨어로 가져와 사용하면 된다.

가장 대표적으로 사용하는 전략으로 local, jwt, google 등이 있다.(500개 넘게 있다...)

사용

공식문서의 사용 예시를 보면 다음과 같다

app.post('/login/password',
  passport.authenticate('local', { failureRedirect: '/login', failureMessage: true }),
  function(req, res) {
    res.redirect('/~' + req.user.username);
  });

그렇다. 상당히 보기 불편하다
(백엔드 개발자는 신성한 라우터에 주렁주렁 코드를 작성할 수 없긔)

다른 경로에서 미리 전략을 구성하고 passport를 미리 미들웨어로 만들어 사용하면 다음과 같이 좀 더 (백엔드의결백증) 직관적인 라우터 작성이 가능하다.

userAuthRouter.post("/user/login", validateLogin, authenticateLocal, loginUser);

처음부터 하나하나 만들어보자
로컬전략을 사용해 로그인 성공시 jwt토큰을 발급하는 방법이다
(지극히 주관적인 사용 방법입니다. 더 좋은 방법이 있다면 알려주세욧...젭ㅏㄹ)


  • 전략 구성
    가장 먼저 사용할 전략부터 생성해준다.
    로그인 시 form으로 제출받은 이메일과 패스워드를 인증하는 가장 기본적인 local 전략이다.
    사실 이 '전략'들을 어디에 작성하면 좋을지 몰라서 나는 /config/strategys 경로에 작성했다.
const local = new LocalStrategy(
	{
		usernameField: "email",
		passwordField: "password",
	},
	async (email, password, done) => {
		try {
			const user = await User.findByEmail({ email });
			if (!user) {
				return done(null, false, {
					message: "Incorrect email or password",
				});
			}
			const result = await bcrypt.compare(password, user.password);
			if (!result) {
				return done(null, false, {
					message: "Incorrect email or password",
				});
			}
			return done(null, user);
		} catch (err) {
			return done(err);
		}
	},
);
  1. 가장 먼저 LocalStrategy 생성자를 호출해 새로운 local 인스턴스를 생성한다.

  2. 이 인스턴스는 첫 번째 인자로 사용할 필드와, 두 번째 인자로 콜백 함수를 받는다.
    이 때 생소한 done을 주목하자. done은 '전략'을 사용하는 passport.authenticate미들웨어에게 사용자 인증 결과를 전달하는 함수이다. 마치 next와 비슷한 역할을 하는것 같지만 다르다.

  3. 필드는 사용자가 로그인 시 제출한 입력 필드가 어떤 것인지를 지정하는 옵션이다. (예제에선 이메일과 패스워드. 꼭 지정해야한다)

  4. 전달받은 이메일과 패스워드로 DB와 대조해 해당 유저를 찾아낸다.
    예제에서 User.findByEmail은 서버를 3계층 구조로 나눠 모델에서 개인이 만들어 낸 정적메서드다. 어떤방식으로든 데이터베이스에서 유저를 찾아내면 된다.

  5. 해당 이메일의 유저가 없거나 비밀번호가 일치하지 않는다면 로직은 종료된다. (예제에서는 DB에 저장된 해시화된 비밀번호를 bcrypt라이브러리를 이용해 입력받은 비밀번호와 대조한다)

  6. 이메일로 유저를 찾고, 비밀번호가 동일하다면 해당 유저를 done로 전달한다


이 때 done를 다시 살펴보면 두 번째 인자로 실패, 유저 등을 넘겨받아 처리하는건 알겠는데 첫 번째 인자로 받는 null은 뭐하는 아이인가. 살펴보자
done(error: Error | null, user?: any, options?: DoneCallbackOptions): void;

첫 번째 인자로 인증 과정에서 오류가 발생한 경우 오류 객체를 전달하거나, 오류가 발생하지 않으면 null을 전달한다고 되어 있다
그럼 실패했을 때 왜 에러를 사용하지않고 null을 사용했을까 라는 의문점이 든다

  • done(null, false, ...)
    인증 실패 시 인증 과정에서 오류가 없었지만 자격 증명이 잘못되었거나 다른 이유로 인증 자체가 실패했음을 알려준다
    즉 이 접근 방식은 인증 프로세스 자체는 성공했음을 나타낸다는 뜻이고 passport는 요청을 계속 처리하지만 인증은 실패한것으로 간주한다

  • done(new Error('Authentication failed'), false)
    인증 프로세스 자체에 오류가 발생했음을 알려주고 잘못된 자격 증명과 관련된 것은 아님을 전달한다
    오류 개체를 전달하면 인증 실패에 대한 자세한 정보를 제공해 로깅 및 디버깅 목적에 유용하다

즉 done은 res객체에 직접 오류를 전달하지 못한다. 만약 사용하는 로깅방식이 에러미들웨에서 실패응답을 로깅하고있다면 done의 첫 번째 인자로 에러를 생성해 호출자인 passport.authenticate미들웨어에서 next로 보내 로깅할 수 있다.
(나도 작성하면서 알았다.. 나중에 수정해야겠음)


두 번째로, passport.authenticate미들웨어를 라우터에서 좀 더 깔끔하고 직관적으로 볼 수 있게 authenticateLocal을 작성해 보자. 저는 /middlewares 경로에 작성했슴니다.

const authenticateLocal = (req, res, next) => {
	passport.authenticate("local", { session: false }, (err, user, info) => {
		try {
			if (err) {
				return next(err.message);
			}
			if (!user) {
				return res.status(401).json({ message: info.message });
			}
			const payload = { user_id: user._id };
			const secretKey = process.env.JWT_SECRET_KEY;
			const expiresIn = process.env.JWT_ACCESS_TOKEN_EXPIRES;
			const token = generateToken(payload, secretKey, expiresIn);

			req.user = user;
			req.token = token;
			next();
		} catch (error) {
			next(error);
		}
	})(req, res, next);
};

session은 사용하지 않을거라 false로 설정해준다.
로컬 전략에서 넘겨준 done이 에러일경우 로직을 중단하고, 성공이면 payload에 해당 유저의 아이디를 담아 토큰을 생성해 req로 내보낸다.
토큰을 생성할 때 쓰이는 비밀키는 민감한 정보이니 꼭 환경변수로 설정해주자. 그리고 passport.authenticate()에 다시 (req, res, next)를 사용한 이유를 알아보자.

구조를 살펴보면 authenticateLocal은 미들웨어이고, 이 미들웨어는 passport의 authenticate미들웨어를 호출하고 있다. 즉 미들웨어 속 미들웨어인데 문제는 authenticate미들웨어의 콜백함수에는 next가 없어 할 일을 마치면 그냥 그대로 끝나버린다 (칼퇴오짐)
그렇기에 다시 (req, res, next)를 사용해 호출자인 authenticateLocal 에게 결과를 전해줘서 체인이 끊기지 않게 해줘야 정상적으로 작동하게 된다.
(저는 이렇게 사용했는데 더 좋은 방법이 있을ㅈ..읍읍)
실행 흐름을 다음 플로우를 보면 이해하기 쉽다.

이렇게 간단한 뭐가간단해 passprot local 전략을 이용한 로그인 인증방법을 알아보았다. 첫 프로젝트에 passport를 적용시킬 때 전략이니 뭐니 헷갈리는 부분도 많았고, 라우터의 청결?을 위해 전부 모듈화 시켜버리는 바람에 더 헷갈리는부분이 많았는데 이렇게 사용하는 예시를 찾기 힘들어서 더 애먹었던거같다. 다음번엔 발급받은 jwt를 이용한 passport-jwt 인가 방법으로 돌아오겠읍니다.

profile
BackEnd Developer

0개의 댓글