[TIL] 211202

Lee SyongΒ·2021λ…„ 12μ›” 2일
0

TIL

λͺ©λ‘ 보기
106/204
post-thumbnail

πŸ“ 였늘 ν•œ 것

  1. github 둜그인 κ΅¬ν˜„ν•˜κΈ°

  2. scope / URLSearchParams / node-fetch / access token


πŸ“š 배운 것

user authentication - 둜그인

1. github 둜그인 κ΅¬ν˜„ν•˜κΈ°

Authorizing OAuth Apps μ°Έκ³ 

πŸ’‘ web application flow

πŸ“Œ userκ°€ GitHub 신원을 μš”μ²­ν•˜λ„λ‘ λ¦¬λ””λ ‰μ…˜λœλ‹€.
πŸ“Œ userκ°€ 앱을 μŠΉμΈν•˜λ©΄ GitHub에 μ˜ν•΄ λ‹€μ‹œ μ›Ή μ‚¬μ΄νŠΈλ‘œ λ¦¬λ””λ ‰μ…˜λœλ‹€.
πŸ“Œ 앱이 user의 access token을 μ΄μš©ν•΄ API에 μ•‘μ„ΈμŠ€ν•œλ‹€.

λ¨Όμ €, github - developer settings δΈ­ OAuth appsμ—μ„œ WetubλΌλŠ” μ΄λ¦„μ˜ OAuth 앱을 λ§Œλ“€μ–΄μ•Ό ν•œλ‹€.
κ·Έ μ€‘μ—μ„œ Authorization callback URLμ΄λž€ userκ°€ 앱을 μŠΉμΈν–ˆμ„ λ•Œ githubκ°€ userλ₯Ό redirect μ‹œν‚€λŠ” url을 λ§ν•œλ‹€.

앱을 λ§Œλ“  ν›„ client IDλ₯Ό 확인할 수 μžˆλ‹€.

1) github둜 userλ₯Ό redirect

(1) μ•± 승인 링크 λ§Œλ“€κΈ°

GET https://github.com/login/oauth/authorize

Authorizing OAuth Apps의 μ•ˆλ‚΄λ₯Ό μ°Έκ³ ν•΄ login.pug νŒŒμΌμ—μ„œ githubμ—μ„œ μ£ΌλŠ” url둜 μ—°κ²°λ˜λŠ” 링크λ₯Ό λ§Œλ“ λ‹€.
κ·ΈλŸ¬λ‚˜ μ•„λž˜ λ§ν¬λ§ŒμœΌλ‘œλŠ” 클릭 μ‹œ 404 μ—λŸ¬κ°€ λœ¬λ‹€.

//- login.pug

li
  a(href="https://github.com/login/oauth/authorize") Continue with Github →

(2) νŒŒλΌλ―Έν„° μΆ”κ°€

client_id (ν•„μˆ˜)

url 끝에 Wetube μ›Ή μ‚¬μ΄νŠΈμ˜ client_id νŒŒλΌλ―Έν„°λ₯Ό μΆ”κ°€ν•œ ν›„ λ‹€μ‹œ 링크λ₯Ό ν΄λ¦­ν•˜λ©΄ (이미 github에 둜그인 λ˜μ–΄ μžˆλŠ” user의 경우) Wetube μ›Ή μ‚¬μ΄νŠΈλ₯Ό μŠΉμΈν•  것인지 λ¬»λŠ” νŽ˜μ΄μ§€κ°€ λ‚˜μ˜¨λ‹€.
μŠΉμΈν•˜κΈ°λ₯Ό ν΄λ¦­ν•˜λ©΄ WetubeλŠ” ν•΄λ‹Ή user의 신원과 ν”„λ‘œν•„ λ“±μ˜ public 데이터λ₯Ό 얻을 수 μžˆλ‹€.

li
  a(href="https://github.com/login/oauth/authorize?client_id=79a813159e30dd338d2b") Continue with Github →

scope

Scopes for OAuth Apps - Available scopes μ°Έκ³ 

user의 private λ°μ΄ν„°κΉŒμ§€ μ–»μ–΄μ˜€κΈ° μœ„ν•΄μ„œλŠ” scope νŒŒλΌλ―Έν„°λ₯Ό μ΄μš©ν•΄μ•Ό ν•œλ‹€.
이λ₯Ό μ΄μš©ν•΄ userλ‘œλΆ€ν„° μ–Όλ§ˆλ‚˜ λ§Žμ€ 정보λ₯Ό κ°€μ Έμ˜¬ 것인지 μ„€μ •ν•  수 μžˆλ‹€.
scope λͺ©λ‘μ€ 곡백으둜 κ΅¬λΆ„ν•˜μ—¬ 적을 수 μžˆλ‹€.

li
  a(href="https://github.com/login/oauth/authorize?client_id=79a813159e30dd338d2b&scope=read:user user:email") Continue with Github →

allow_signup

기본값은 trueμ΄λ―€λ‘œ userκ°€ github 계정이 μ—†λ‹€λ©΄ gihub 계정을 생성할 수 μžˆλŠ” 링크가 ν‘œμ‹œλœλ‹€.
false 값을 μ£Όλ©΄ ν‘œμ‹œλ˜μ§€ μ•ŠλŠ”λ‹€.

li
  a(href="https://github.com/login/oauth/authorize?client_id=79a813159e30dd338d2b&scope=read:user user:email&allow_signup=false") Continue with Github →

πŸ’‘ user의 github 신원 μš”μ²­ν•˜κΈ°

url에 κΈ°λ°˜ν•œλ‹€

πŸ“Œ μ†Œμ…œ 앱에 μ£ΌλŠ” url을 λ°›μ•„ 링크λ₯Ό λ§Œλ“ λ‹€
πŸ“Œ κ·Έ url에 μ†Œμ…œ 앱이 μ£ΌλŠ” idλ₯Ό μ λŠ”λ‹€ (ν•„μˆ˜)
πŸ“Œ κ·Έ μ™Έ scope(permission) λ“±μ˜ νŒŒλΌλ―Έν„°λ₯Ό μΆ”κ°€ν•œλ‹€

(3) URLSearchParams

κ·ΈλŸ¬λ‚˜, μ΄λ ‡κ²Œ μž‘μ„±ν•˜λ©΄ ν•΄λ‹Ή 링크가 ν•„μš”ν•œ 곳에 맀번 κΈ΄ url을 볡뢙해야 ν•˜κ³  μΆ”ν›„ μˆ˜μ •λ„ μ–΄λ ΅λ‹€.
μ½”λ“œλ₯Ό κ°œμ„ ν•˜κΈ° μœ„ν•΄ 링크의 url을 λ°”κΏ”μ€€ ν›„ ν•΄λ‹Ή route와 startGithubLogin 컨트둀러λ₯Ό μΆ”κ°€ν–ˆλ‹€.

//- login.pug

a(href="/users/github/start") Continue with Github →
// userRouter.js
import { startGithubLogin } from "../controllers/userController";

userRouter.get("/github/start", startGithubLogin);

config object에 νŒŒλΌλ§ˆν„° 이름과 값듀을 λ‹΄λŠ”λ‹€.
new URLSearchParams(config).toString()λ₯Ό μ΄μš©ν•΄ config objectλ₯Ό νŒŒλΌλ―Έν„° ν˜•νƒœλ‘œ λ°”κΏ€ 수 μžˆλ‹€.

export const startGithubLogin = (req, res) => {
  const baseUrl = "https://github.com/login/oauth/authorize";
  const config = {
    client_id: "79a813159e30dd338d2b",
    allow_signup: false,
    scope: "read:user user:email"
  };
  const params = new URLSearchParams(config).toString();
  const finalUrl = `${baseUrl}?${params}`;
  return res.redirect(finalUrl);
};

2) userλ₯Ό μ›Ή μ‚¬μ΄νŠΈλ‘œ λ‹€μ‹œ redirect

userκ°€ github의 μ•± 승인 νŽ˜μ΄μ§€μ—μ„œ μŠΉμΈν•˜κΈ°λ₯Ό ν΄λ¦­ν•˜λ©΄, githubλŠ” Authorization callback URL둜 userλ₯Ό redirect μ‹œν‚¨λ‹€.
μ΄λ•Œ κ·Έ url에 codeλ₯Ό ν•¨κ»˜ λ³΄λ‚΄λŠ”λ° 이λ₯Ό access token으둜 λ°”κΏ” github API에 μ ‘κ·Όν•¨μœΌλ‘œμ¨ user에 λŒ€ν•œ 정보λ₯Ό κ°€μ Έμ˜¬ 수 μžˆλ‹€.
λ¨Όμ € githubκ°€ μ€€ codeλ₯Ό access token으둜 λ°”κΏ”μ•Ό ν•œλ‹€.

일단은 Authorization callback URL을 route에 μΆ”κ°€ν•˜κ³  finishGithubLogin μ»¨νŠΈλ‘€λŸ¬λ„ μΆ”κ°€ν•œλ‹€.
아직은 아무 응닡도 해주지 μ•Šμ€ μƒνƒœμ΄λ‹€.

// userRouter.js
userRouter.get("/github/finish", finishGithubLogin);
// userController.js
export const finishGithubLogin = (req, res) => {};

(1) .env νŒŒμΌμ— νŒŒλΌλ―Έν„° μΆ”κ°€

μ•„λž˜μ˜ url둜 client_id, client_secret, code와 ν•¨κ»˜ POST requestλ₯Ό 보내야 ν•œλ‹€.

POST https://github.com/login/oauth/access_token

client_idλŠ” μ€‘λ³΅ν•˜μ—¬ μ‚¬μš©λ˜κΈ° λ•Œλ¬Έμ— μ–΄λ””μ„œλ“  μ“Έ 수 μžˆλ„λ‘ .env νŒŒμΌμ— μΆ”κ°€ν•΄ μ‚¬μš©ν•˜λ„λ‘ ν•œλ‹€.
client_secret은 μ½”λ“œμ— ν¬ν•¨λ˜μ–΄μ„œλŠ” μ•ˆλ˜κΈ° λ•Œλ¬Έμ— .env νŒŒμΌμ— μΆ”κ°€ν•΄ μ‚¬μš©ν•˜λ„λ‘ ν•œλ‹€.
codeλŠ” githubκ°€ μ£ΌλŠ” codeλ‘œμ„œ url에 ν‘œμ‹œλœλ‹€. req.query.codeλ₯Ό 톡해 얻을 수 μžˆλ‹€.

GH_CLIENT=sf2ls315ddwg2gj38d2a
GH_SECRET=fdfd2f325dfdfdf1dfdfd

(2) fetch()λ₯Ό μ΄μš©ν•΄ POST request 보내기

finishGithubLogin 컨트둀러λ₯Ό μˆ˜μ •

fetchλ₯Ό μ΄μš©ν•˜λ©΄ μ‹€μ œλ‘œ url에 λ°©λ¬Έν•˜μ§€ μ•Šκ³ λ„ κ·Έλ‘œλΆ€ν„° 데이터λ₯Ό κ°€μ Έμ˜¬ 수 μžˆλ‹€. [TIL] 211108 μ°Έκ³ 

// userController.js
export const finishGithubLogin = async (req, res) => {
  // finalUrl을 λ§Œλ“¦ ❗
  const baseUrl = "https://github.com/login/oauth/access_token";
  const config = {
    client_id: process.env.GH_CLIENT,
    client_secret: process.env.GH_SECRET,
    code: req.query.code,
  };
  const params = new URLSearchParmas(config).toString();
  const finalUrl = `${baseUrl}?${params}`;
  // finalUrlλ‘œλΆ€ν„° 데이터λ₯Ό κ°€μ Έμ˜΄ ❗ (finalUrl에 POST requestλ₯Ό 보냄)
  const data = await fetch(finalUrl, {
    method: "POST",
    headers: { // githubλ‘œλΆ€ν„° json을 return λ°›κΈ° μœ„ν•΄ μΆ”κ°€ν•΄μ•Ό 함
      Accept: "application/json",
    },
  });
  // κ·Έ 데이터λ₯Ό json ν˜•μ‹μœΌλ‘œ λ°”κΏˆ ❗
  const json = await data.json();
  return res.send(JSON.stringify(json)); // λΈŒλΌμš°μ €μ—μ„œ ν™•μΈν•˜κΈ° μœ„ν•΄ μž„μ‹œλ‘œ μž‘μ„±ν•œ μ½”λ“œ
};

그런데 fetchλŠ” 기본적으둜 ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ μ‚¬μš©μ΄ κ°€λŠ₯ν•œ ν•¨μˆ˜μ΄λ―€λ‘œ λ°±μ—”λ“œμ—μ„œ fetchλ₯Ό μ‚¬μš©ν•  수 μžˆλ„λ‘ μΆ”κ°€λ‘œ 섀정을 ν•΄μ•Ό ν•œλ‹€.

node-fetch μ„€μΉ˜

fetchλ₯Ό μ„€μΉ˜ν•œ ν›„ userController.js νŒŒμΌμ—μ„œ fetch λΌλŠ” μ΄λ¦„μœΌλ‘œ import ν•œλ‹€.
μ΅œμ‹  λ²„μ „μœΌλ‘œ μ„€μΉ˜ν•˜λ©΄ μ—λŸ¬κ°€ λœ¬λ‹€. 2.6.1 λ²„μ „μœΌλ‘œ μ„€μΉ˜ν•  것.

$ npm i node-fetch@2.6.1
// userController.js
import fetch from "node-fetch";

이제 login νŽ˜μ΄μ§€μ—μ„œ 'Continue with Github β†’'λ₯Ό ν΄λ¦­ν•˜λ©΄, 화면에 μ•„λž˜μ™€ 같이 λœ¬λ‹€.
github λ°±μ—”λ“œλ‘œ POST μš”μ²­μ„ 보내 access_token을 μ–»κ²Œ 된 것이닀.

{
access_token: "sho_sdfwdBJhosd7kgIp5Usfl30swRUsHEwpJ9df",
token_type: "bearer",
scope: "read:user,user:email"
}

3) access token으둜 github APIμ—μ„œ user 정보 κ°€μ Έμ˜€κΈ°

access token으둜 github APIλ₯Ό μ‚¬μš©ν•΄ user에 λŒ€ν•œ 정보λ₯Ό κ°€μ Έμ˜¬ 수 μžˆλ‹€.

github API url에 GET requestλ₯Ό λ³΄λ‚΄λ©΄μ„œ 인증을 μœ„ν•΄ access_token을 보내면, urlλ‘œλΆ€ν„° user에 λŒ€ν•œ 정보λ₯Ό λ°›μ•„μ˜¬ 수 μžˆλ‹€.

Authorization: token OAUTH-TOKEN
GET https://api.github.com/user

λ¨Όμ €, json object에 'access_token'이 μžˆμ„ λ•Œμ™€ 없을 κ²½μš°μ™€ 없을 경우λ₯Ό λ‚˜λˆ„μ–΄ finishGithubLogin 컨트둀러λ₯Ό λ‹€μŒκ³Ό 같이 μˆ˜μ •ν•œλ‹€.

+await μ•ˆμ— await을 λ„£λŠ” ν˜•νƒœλ‘œ μ½”λ“œλ₯Ό μˆ˜μ •ν–ˆλ‹€.

// userController.js
export const finishGithubLogin = async (req, res) => {
  // finalUrl을 λ§Œλ“¦
  const baseUrl = "https://github.com/login/oauth/access_token";
  const config = {
    client_id: process.env.GH_CLIENT,
    client_secret: process.env.GH_SECRET,
    code: req.query.code,
  };
  const params = new URLSearchParams(config).toString();
  const finalUrl = `${baseUrl}?${params}`;
  // finalUrlλ‘œλΆ€ν„° 데이터λ₯Ό κ°€μ Έμ˜΄ (finalUrl에 POST requestλ₯Ό 보냄)
  // κ·Έ 데이터λ₯Ό json ν˜•μ‹μœΌλ‘œ λ°”κΏˆ
  const tokenRequest = await (
    await fetch(finalUrl, {
      method: "POST",
      headers: {
        Accept: "application/json",
      },
    })
  ).json();
  // access token을 μ΄μš©ν•΄ github API에 μ ‘κ·Όν•΄ user에 λŒ€ν•œ 정보λ₯Ό κ°€μ Έμ˜΄
  if ("access_token" in tokenRequest) {
    const { access_token } = tokenRequest;
    const userData = await (
      await fetch("https://api.github.com/user", {
        headers: {
          Authorization: `token ${access_token}`,
        },
      })
    ).json();
    console.log(userData); // user 정보 확인을 μœ„ν•΄ μž„μ‹œλ‘œ μž‘μ„±ν•œ μ½”λ“œ
  } else {
    return res.redirect("/login");
  }
};

이제 login νŽ˜μ΄μ§€μ—μ„œ 'Continue with Github β†’'λ₯Ό ν΄λ¦­ν•˜λ©΄, μ½˜μ†” 창에 githubκ°€ 가지고 μžˆλŠ” user에 λŒ€ν•œ 정보(public 데이터)κ°€ λœ¬λ‹€.

그런데, μš°λ¦¬κ°€ ν•„μš”ν•œ email은 값이 null이라고 λœ¬λ‹€.
이λ₯Ό λŒ€λΉ„ν•˜μ—¬ 또 λ‹€λ₯Έ requestλ₯Ό λ§Œλ“€μ–΄μ•Ό ν•œλ‹€.

πŸ’‘ githubμ—μ„œ user의 email 정보 κ°€μ Έμ˜€κΈ°

github - reference - Users - Emails μ°Έκ³ 

user의 email을 κ°€μ Έμ˜€κΈ° μœ„ν•΄ get requestλ₯Ό μ–΄λ–€ url둜 보내야 ν• μ§€λŠ” μœ„ μ‚¬μ΄νŠΈλ₯Ό μ°Έκ³ ν•΄ μ•Œ 수 μžˆλ‹€.
λ‹Ήμ—°νžˆ λ˜‘κ°™μ€ access_token을 μ‚¬μš©ν•œλ‹€.
github API urlμ—μ„œ λ°˜λ³΅λ˜λŠ” 뢀뢄은 λ³€μˆ˜λ‘œ λ§Œλ“€μ–΄μ£Όμ—ˆλ‹€.

// access token을 μ΄μš©ν•΄ github API에 μ ‘κ·Όν•΄ user에 λŒ€ν•œ 정보λ₯Ό κ°€μ Έμ˜΄
if ("access_token" in tokenRequest) {
  const { access_token } = tokenRequest;
  const apiUrl = "https://api.github.com";
  // public 데이터
  const userData = await (
    await fetch(`${apiUrl}/user`, {
      headers: {
        Authorization: `token ${access_token}`,
      },
    })
  ).json();
  // private 데이터 (δΈ­ email)
  const emailData = await (
    await fetch(`${apiUrl}/user/emails`, {
      headers: {
        Authorization: `token ${access_token}`,
      },
    })
  ).json();
  console.log(userData, emailData); // user 정보 확인을 μœ„ν•΄ μž„μ‹œλ‘œ μž‘μ„±ν•œ μ½”λ“œ
} else {
  return res.redirect("/login");
}

이제 login νŽ˜μ΄μ§€μ—μ„œ 'Continue with Github β†’'λ₯Ό ν΄λ¦­ν•˜λ©΄, μ½˜μ†” 창에 user에 λŒ€ν•œ public 데이터와 private email 데이터가 λͺ¨λ‘ λœ¬λ‹€.

ν•œνŽΈ, κ·Έ μ€‘μ—μ„œ verifiedμ΄λ©΄μ„œ primary인 email을 μ°Ύμ•„μ•Ό ν•œλ‹€.
이λ₯Ό λͺ¨λ‘ λ§Œμ‘±ν•˜λŠ” email이 μ—†λ‹€λ©΄ userλ₯Ό login νŽ˜μ΄μ§€λ‘œ λŒλ €λ³΄λ‚Έλ‹€.

const email = emailData.find(email => email.primary === true && email.verified === true);
if (!email) {
  return res.redirect("/login");
}

이제 email을 κ°€μ Έλ‹€ μ“Έ 수 μžˆλ‹€.

κ·ΈλŸ¬λ‚˜, 아직도 μƒκ°ν•΄μ€˜μ•Ό ν•  것듀이 남아 μžˆλ‹€.
μ›Ή μ‚¬μ΄νŠΈμ—μ„œ emailκ³Ό passwordλ₯Ό μž…λ ₯ν•΄ 이미 계정을 μƒμ„±ν•œ userκ°€ github둜 λ‘œκ·ΈμΈν•˜λ €κ³  ν•  λ•Œ μ–΄λ–»κ²Œ ν•  것인가
λ“±μ˜ 문제λ₯Ό ν•΄κ²°ν•΄μ•Ό ν•œλ‹€.


✨ 내일 ν•  것

  1. user authentication 파트 마무리

  2. user 파트 μ²˜μŒλΆ€ν„° λκΉŒμ§€ λ‹€μ‹œ κ΅¬ν˜„ν•΄λ³΄κΈ°

profile
λŠ₯λ™μ μœΌλ‘œ μ‚΄μž, ν–‰λ³΅ν•˜κ²ŒπŸ˜

0개의 λŒ“κΈ€