저번에는 NodeJS 환경에서 MongoDB로 로그인을 구현해보았으니, 이번에는GitHub로 로그인을 구현해보겠다.
사이트에 로그인하려고 보면 있는 구글로 로그인, 트위터로 로그인 등등을 만들어보는 것이다. 이번 경우, GitHub이므로 GitHub Docs에 가서 방법을 살펴보자.
github로 로그인을 하게 되면 과정은 아래와 같다.
- 사용자의 github ID를 요청하도록 redirect 된다.
- 사용자는 코드를 받으면 다시 사이트로 redirect 된다.
- 앱에서 사용자의 액세스 토큰으로 API에 액세스한다.
이러한 과정을 겪고나면 로그인 성공이라는 것이지만 더 자세한 설명을 통해 구현해보겠다. 문서에서도 설명해준다.
GET https://github.com/login/oauth/authorize
url 뒤에 매개 변수를 붙여서 redirect하면 된다. 매개 변수의 종류는 공식 문서를 참고하자.
로그인을 할 때, 필요한 매개 변수는
client_id, scope 이다. allow_signup도 들어갈 수 있겠지만 이건 GitHub 아이디가 없을 경우, 회원가입을 제공할지 여부이다. 기본값은 true로 거부한다면 false를 해야한다.
scope는 앱이 사용할 수 있는 범위를 설정하는 건데, 생략하게 되면 기본적으로 빈 목록으로 설정된다. 범위가 지정되면 이외의 액세스를 제한한다.
사용 가능한 범위는 이쪽에서 확인할 수 있다. 로그인을 할 때 필요한 것이 무엇인지 생각하고 설정하면 되겠다.
현재 필요한 것은 user의 프로필과 user email이다.
참고로 scope의 범위 설정은 공백으로 구분하기 때문에 유의.
const githubLoginStart = (req,res) =>{
const api = "https://github.com/login/oauth/authorize";
const config = {
client_id: "dafdsf15asdf46sadf",
allow_signup:false,
scope:"read:user user:email"
};
}
매개 변수를 api url에 더해주고 해당 url로 redirect하면 다시 사이트로 돌아온다. 다만, 여기서 주의해야 할 점이 있다.
config를 단순히 api url에 더하면 어떻게 될까?
> api+"/"+config.client_id+config.allow_signup+config.scope
< 'https://github.com/login/oauth/authorize/dafdsf15asdf46sadffalseread:user user:email'
url 주소라고 하기엔 위화감이 든다. url 주소에 공백은 있어서는 안되기 때문에 해당 주소를 알맞게 변환해주어야 한다.
const githubLoginStart = (req,res) =>{
const api = "https://github.com/login/oauth/authorize";
const config = {
client_id: "dafdsf15asdf46sadf",
allow_signup:false,
scope:"read:user user:email"
};
const params = new URLSearchParams(config).toString();
const url = api + "/" + params;
return res.redirect(url);
}
URL SearchParams 인터페이스는 URL의 쿼리 문자열로 작동하는 유틸리티 메서드를 정의한다고 공식 문서에 쓰여있다. 이걸 toString을 사용하면 URL에 사용하기 적합한 쿼리 문자열을 반환해준다.
따라서 작성한 url을 더해 redirect하면 완료.
매개 변수의 이름은 맞게 설정해주어야 오류가 안 나니 유의하자.
요청을 수락하면 github에서 임시 코드를 주는데, 해당 코드를 액세스 토큰으로 교환할 수 있다.
POST https://github.com/login/oauth/access_token
필요한 것은 client_id, client_secret, code이다.
이미 필요한 것은 준비되어 있을 것이다. code는 url의 쿼리를 보면 된다.
(client_secret은 id와 마찬가지로 github 페이지에 가면 얻을 수 있다.)
redirect_uri은 옵션인데, 인증 후 해당 url로 돌아갈 수 있는 것 같다.
user의 github id를 요청했을 때와 같이 url를 생성해 post하면 된다.
const githubLoginEnd = async (req,res) =>{
...
const params = new URLSearchParams(config).toString();
const url = api + "/" + params;
const response = await fetch(url,{
method:"post",
header:{
Accept: application/json
},
})
const data = await response.json();
}
이후 data의 값은 json으로 받게 될 것이다.
Accept: application/json { "access_token":"gho_16C7e42F292c6912E7710c838347Ae178B4a", "scope":"repo,gist", "token_type":"bearer" }
GET https://api.github.com/user
내가 예전에 구현했을 때와 달라진 것 같다. 단어만 달라진 거 같아서 그것만 수정해주니 정상적으로 작동한다.
const githubLoginEnd = async (req,res) =>{
...
const data = await response.json();
if("access_token" in data){
const {access_token} = data;
const api = "https://api.github.com";
const userData = await fecth(`${api}/user`,{
header:{
Authorization: `Bearer ${access_token}`
}
})
const user = await userData.json();
} else {
return res.send("error")
// 로그인에 실패했을 때 보낼 내용을 적자.
}
}
바뀌기 전에는 Bearer 부분이 token이었다.
가져온 user의 데이터를 살펴보면 email의 값이 null로 되어있는 경우가 있을 것이다. 이메일 설정을 안 했거나 private 이메일일 경우에 null로 표시되는데 지금 구현한 사이트에서는 이메일이 필수 항목이니 별도로 가져와주자.
const githubLoginEnd = async (req,res) =>{
...
const data = await response.json();
if("access_token" in data){
const {access_token} = data;
const api = "https://api.github.com";
...
const emailData = await fecth(`${api}/user/emails`,{
header:{
Authorization: `Bearer ${access_token}`
}
})
const userEmail = await emailData.json();
console.log(userEmail)
} else {
return res.send("error")
// 로그인에 실패했을 때 보낼 내용을 적자.
}
}
결과값
[ { email: 'email123@naver.com', primary: true, verified: true, visibility: 'private' }, { email: '1651615+email123@users.noreply.github.com', primary: false, verified: true, visibility: null } ]
똑같은 구조의 결과값이 나온다. 여기서 primary와 verified가 true인 이메일을 찾고 해당 값을 사용하면 된다.
const githubLoginEnd = async (req,res) =>{
...
const data = await response.json();
if("access_token" in data){
...
const user = await userData.json();
const userEmail = await emailData.json();
const emailObj = await userEmail.find((obj)=>obj.primary === true && obj.verified === true)
const emailOk = await User.exists({email:emailObj.email})
if(emailOk){
//해당 이메일을 사용한 사람이 user 중에 있다면 로그인시키기.
req.session.loggedIn = true;
req.session.user = emailOk;
} else{
//해당 이메일을 사용한 사람이 없다면 새로운 user를 생성하기.
const user = await User.create({
name:user.name,
username:user.login,
password:"",
email:emailObj.email,
socialOnly:true,
githubLogin:true,
})
req.session.loggedIn = true;
req.session.user = user;
// home으로 redirect하면 된다.
return res.send("success")
}
} else {
return res.send("error")
// 로그인에 실패했을 때 보낼 내용을 적자.
}
}
find를 통해 조건에 부합하는 email을 찾고, User의 데이터 안에 해당 이메일을 가진 user가 존재하는지 찾는다. 만약, 존재한다면 이미 계정이 있는 것이니 로그인 시켜준다.
user가 존재하지 않는다면, 새롭게 생성해주자.
가져온 데이터를 보고 맞는 곳에 넣어준다. socialOnly, githubLogin을 true로 변경해주는 이유는 다른 소셜 로그인이 있을 경우를 대비해서 넣어준다. (구글이나 트위터 등등)
다음은 세션에 정보를 전달하고 loggedIn을 true로 바꿔준 뒤 로그인이 되었을 때 동작을 해주면 github 로그인 완료이다.
이런 방식으로 구글, 카카오 로그인이 가능하고 회사마다 방법이 다르니 문서를 잘 살펴보길 바란다. 구글은 더 복잡하다고 들었다...