카테고리 없음

[구글 OAuth 로그인 만들기] js+node.js로 구현 (credential을 보내고 jwt를 받는 방식)

designer DK 2024. 11. 17. 20:26
728x90

기존에 구현해 둔 이메일-패스워드 로그인 방식이 백엔드로부터 jwt 토큰을 받고 로컬 스토리지에 저장하는 방식으로 제작 되어있었는데, 이 방식을 유지하면서 구글 OAuth 로그인을 추가로 구현해보았다.

 

방식을 간단하게 설명하면, 구글 로그인 관련 프론트엔드 코드에서 credential 코드를 보내고 백엔드에서는 이걸 받아서 기존 방식과 같은 jwt로 토큰을 만들어서 응답 데이터로 반환해서 기존 로그인 방식과 구글 로그인 방식이 한 로직으로 다 구현되도록 만들어보았다.

 

참고로 이번에 구글 로그인 기능을 구현해보면서 알게 된 부분이지만 지금 설명하는 이 방식은 여러가지 구글 오어스 구현 방식 중 하나일 뿐이며, 실제로 개발 방식에 따라 여러 방식으로 구현할 수 있긴 했다.

 

구글 OAuth 로그인을 구현하기 위해서는 

어떤 방식으로 진행하든 공통적으로 google cloud api 에서 클라이언트ID를 받는 과정이 따른다.

 

 

먼저 Google Cloud Api의 ‘콘솔’에 들어간다.

Google Cloud Api의 콘솔 화면

 

 

프로젝트가 없다면 새 프로젝트를 만든다.

 

 

API 및 서비스에서 우측 탭 중 OAuth 동의 화면을 클릭

 

사용자 유형 : 외부로 설정.

앱 이름, 지원 이메일, 개발자 연락처 정보 등의 기본정보를 입력

나머지는 쭉쭉 넘겨서 설정을 완료한다.

 

다음으로 사용자 인증정보 탭 클릭

 

사용자 인증 정보 만들기 버튼을 클릭하고, OAuth 클라이언트 ID를 선택

 

애플리케이션 유형을 선택하면 되는데 나는 지금 웹 프로젝트여서 웹 애플리케이션을 선택

클라이언트 ID 이름도 설정해준다.

 

승인된 JavaScript 원본, 승인된 리디렉션 URI 에 각각 사용할 메인 주소 경로와

로그인 후 리디렉션되는 경로를 설정해주면 된다.

 

 

지금 내가 구현하는 방식은 이런식으로 홈 경로 정도 넣어주면 되는데
구글 OAuth 구현 방식에 따라 승인된 리디렉션 URI의 경우 정확한 리디렉션 경로를 설정해줘야 하는 경우도 있다. (Ex. http://localhost:3000/callback)

 

그리고 지금은 테스트 단계여서 로컬주소만 있는데

나중에 배포까지 하게되면 실제 도메인 url 주소를 꼭 넣어줘야한다!

 

아무튼 이런 과정을 거치고나면 클라이언트 ID와 클라이언트시크릿이 생성되는데

이번 구현 방식엔 주로 클라이언트 ID가 사용된다.

 

 

 

다음은 프론트엔드에 대한 설명

 

프론트엔드에서 내가 구현하려는 로그인 방식(credential을 보내고 jwt를 받는 방식)을 기본 JS로 구현하기 위해서는 Google Identity Services SDK 라는게 필요했다.

 

따로 설치는 필요없고 헤더에 이런 코드만 넣어주면 사용 가능하다.

<!-- Google Identity Services SDK -->
<script src="https://accounts.google.com/gsi/client" async defer></script>

 

로그인 html의  body 안에 아래의 짧은 코드만 넣어주면 구글 로그인 버튼이 화면에 생긴다!

<!-- 구글 오어스 -->
<div id="g_id_onload" data-client_id="783123951661-0prqbf5u00ab0jb5hefbqce9tcm0n7lg.apps.googleusercontent.com"
data-callback="handleCredentialResponse" data-auto_prompt="false">
</div>

<div class="g_id_signin" data-type="standard"></div>

 

이 코드에서 아래쪽 div가 버튼을 화면에 띄우는 용도의 코드이고

위쪽 div가 아까 만든 클라이언트ID를 설정하고 handleCredentialResponse라는 함수를 호출하는 코드이다.

 

// Google에서 인증 후 반환된 credential을 처리하는 함수
function handleCredentialResponse(response) {
const credential = response.credential;

// 백엔드로 ID 토큰 (credential) 전달
fetch(`${URI}/api/auth/google`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ credential })
})
.then(res => res.json())
.then(data => {
if (data.token) {
console.log('Token from backend:', data.token);
// 토큰을 세션 스토리지에 저장하거나 앱 상태에 저장
sessionStorage.setItem('token', data.token);

// index.html로 페이지 이동
window.location.href = "/";
} else {
console.error('Failed to get access token:', data);
}
})
.catch(error => console.error('Error:', error));

console.log("크리덴셜", credential);
}

 

해당 함수를 보면 credential을 만들고 그걸 백엔드로 POST하는 기능을 담고 있다.

credential을 보내주고 응답 받는 값은 token이며 받게되면 세션스토리지에 저장한다.

 

일단 로그인 시 credential이 잘 생성되었는지 확인 필요!

 

 

 

 

 

마지막으로 벡엔드

 

일단 기존 로그인 관련 라우터에 /google 경로를 추가해서 기존 로그인과 경로를 구분했다.

경로로 가면 authController.loginWithGoogle 가 실행

const express = require("express");
const authController = require("../controllers/authController");
const router = express.Router();

router.post("/login", authController.loginWithEmail);
router.post("/google", authController.loginWithGoogle);

module.exports = router;

 

기존 authController.js에는 구글 로그인용 메서드인 loginWithGoogle을 추가해주었다.

//구글 오어스 로그인
authController.loginWithGoogle = async (req, res) => {
try {
const { credential } = req.body;

if (!credential) {
return res.status(400).json({ error: 'Credential not provided' });
}

//구글 통해서 만든 토큰 맞는지 확인
const googleClient = new OAuth2Client(GOOGLE_CLIENT_ID);
const ticket = await googleClient.verifyIdToken({
idToken: credential,
audience: GOOGLE_CLIENT_ID
});
const { email, name } = ticket.getPayload();

let user = await User.findOne({ email: email });

if (!user) {
const randomPassword = "" + Math.floor(Math.random() * 1000000)
const salt = await bcrypt.genSalt(10);
const newPassword = await bcrypt.hash(randomPassword, salt);
user = new User({
name: name,
email: email,
password: newPassword
});
await user.save();
}
const sessionToken = await user.generateToken();
res.status(200).json({ status: "ok", user, token: sessionToken });

} catch (error) {
res.status(400).json({ status: "fail", message: error.message });
};
};

 

대략 흐름을 보면

 

먼저 프론트엔드로부터 credential을 받아온다.

 

그다음 클라이언트ID를 통해 구글로 만든 credential이 맞는지 검증이 필요하게 되는데

이 부분의 구현을 위해 node.js용 라이브러리가 필요하다.

 

npm에서 

Google Auth Library 를 검색하고

 

npm install google-auth-library

이렇게 인스톨을 진행해주면 된다.

 

그리고 컨트롤러 파일 상단에

const { OAuth2Client } = require('google-auth-library');

이 코드를 넣어주면 사용할 수 있다.

(이 라이브러리 사용 시 node.js 버전이 안 맞으면 서버에서 에러코드가 뜨긴하는데 크게 신경안써도 됨)

[DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.

 

const { credential } = req.body;

if (!credential) {
return res.status(400).json({ error: 'Credential not provided' });
}

//구글 통해서 만든 토큰 맞는지 확인
const googleClient = new OAuth2Client(GOOGLE_CLIENT_ID);
const ticket = await googleClient.verifyIdToken({
idToken: credential,
audience: GOOGLE_CLIENT_ID
});
const { email, name } = ticket.getPayload();

 

이 라이브러리를 통해 프론트엔드로 부터 받은 credential을 검증하고

구글로 로그인 한 유저의 이메일과 이름 값을 가져온다.

 

그 다음은 구글 로그인 한 유저가 기존 유저 컬렉션에 있는 유저인지 확인하고

없던 유저이면 새로 추가해주는 코드.

if (!user) {
const randomPassword = "" + Math.floor(Math.random() * 1000000)
const salt = await bcrypt.genSalt(10);
const newPassword = await bcrypt.hash(randomPassword, salt);
user = new User({
name: name,
email: email,
password: newPassword
});
await user.save();
}

 

특이한 점은 구글 로그인 유저의 경우 패스워드를 알 수 없기 때문에

랜덤한 패스워드를 임의로 만들어서 넣어주게된다.

이 코드는 그 랜덤 패스워드를 생성해서 암호화하는 과정이다.

 

랜덤패스워드가 생성되면 이름, 이메일과 함께 유저 컬렉션에 저장한다.

 

모든 과정이 문제 없으면 generateToken을 통해 jwt 토큰을 생성해서 응답 해주게 된다.

const sessionToken = await user.generateToken();
res.status(200).json({ status: "ok", user, token: sessionToken });

 

참고로 generateToken는 User 스키마 모델에서 미리 세팅해놓은 메서드.

userSchema.methods.generateToken = async function () {
const token = await jwt.sign({_id:this._id}, JWT_SECRET_KEY, {expiresIn:"1d"});
return token;
}

 

아무튼 이렇게 했을 때 원했던 방식으로 로그인 구현이 잘 되었다.

 

약간의 아쉬운점은 구현하고 났을 때 콘솔에 coop에러가 떴는데

(Cross-Origin-Opener-Policy policy would block the window.closed call)

 

이건 아무리 리서치를 해봐도 결국 해결하진 못했다;;

다행인건 로그인 구현이나 서비스 전체에  영향을 주는 에러는 아니라고 한다.

 

아무튼 결론적으로 구글 OAuth 로그인 구현 성공!