[Youtube clone coding] #7 User Authentication

#7.0, #7.1, #7.2 Create Account

이제 User의 회원가입, 로그인을 다루는 부분을 만들어보자.

 

  1. User.js에 User schema 만들기
    import mongoose from "mongoose";
    
    const userSchema = new mongoose.Schema({
        name: { type: String, required: true},
        username: { type: String, required: true, unique: true},
        password: {type: String, required: true},
        email: { type: String, required: true, unique: true},
        location: {type: String},
    })
    
    const User = mongoose.model("User", userSchema);
    export default User;

     

  2. usercontroller.js에 join, login 등의 함수 만들기
    export const getJoin = (req, res) => {
        return res.render("join", {pagetitle: "Create Account"})
    };
    
    export const postJoin = (req, res) => {
        console.log(req.body);
        return res.end();
    };

     

  3. join.pug, login.pug 등의 만들기
    extends base.pug
    
    block content 
        form(method='POST')
            input(name='name', placeholder='name',type='text',required)
            input(name='username', placeholder='username',type='text',required)
            input(name='password', placeholder='password',type='password',required)
            input(name='email', placeholder='email',type='email',required)
            input(name='location', placeholder='location',type='text')
            input(type='submit',value='Join')

     

     

    Video 부분을 다룰 때와 거의 비슷하다! 일단 여기까지 새로운 개념은

    → input tag의 type으로 password, email 등을 넣어줄 수 있다는 것

    → Schema에서 unique: true 옵션을 추가해서 primary key로 만들어 줄 수 있다는 것

    등이 있었다.

     

    이제 실제로 User를 db에 추가하고, 로그인하는 기능까지 만들어보자.

    먼저 usercontroller.js의 함수를 조금 수정해주었다.

    export const postJoin = async(req, res) => {
        const { name, username, password, email, location} = req.body;
        await User.create({
            name, username, password, email, location
        });
    
        return res.redirect("/login");
    };

     

    Video 때와 동일하게 User.create로 만들었다!

    db에 추가된 것을 확인할 수 있었다.

    그런데 위와 같이 password db에 그대로 저장하면 해킹을 당할 우려가 있다.

    따라서 우리는 해시함수를 이용해서 비밀번호의 해시값을 저장해야 한다. (사실 그렇게 해도 알아낼 방법은 있긴 하지만…)

     

    먼저 자바스크립트에서 사용할 해시함수를 설치해주자 → npm i bcrypt

    import mongoose from "mongoose";
    import bcrypt from "bcrypt";
    
    const userSchema = new mongoose.Schema({
        name: { type: String, required: true},
        username: { type: String, required: true, unique: true},
        password: {type: String, required: true},
        email: { type: String, required: true, unique: true},
        location: {type: String},
    })
    
    userSchema.pre("save", async function () {
        this.password = await bcrypt.hash(this.password, 5);
    })
    
    const User = mongoose.model("User", userSchema);
    export default User;

     

    위와 같이 bcrypt 모듈을 통해 password를 해싱하는 middleware를 만들어주었다.

    해시값으로 저장된 것을 확인했다!

     

#7.3 Form Validation

Video part에서 했던 것과 같이 Video.exists() function을 활용하여 username과 email의 중복을 막을 수 있다.

그런데 이렇게 하면 비슷한 코드를 중복해서 사용해야 된다.

이때 사용할 수 있는 MongoDB의 옵션이 존재한다.

 

그냥 or과 똑같은 역할을 수행한다고 보면 될 것 같다.

export const postJoin = async(req, res) => {
    const { name, username, password, email, location} = req.body;
    const pageTitle = "Create Account";
    const exists = await User.exists({$or : [{username}, {email}]});
    if(exists){
        return res.render("join", {pageTitle, errorMessage : 'The username/email is already taken.'})
    }
    await User.create({
        name, username, password, email, location
    });

    return res.redirect("/login");
};

 

에러를 반환하지 않고 미리 잘 처리해주었다.

 

extends base.pug

block content 
    if errorMessage 
        p=errorMessage
    form(method='POST')
        input(name='name', placeholder='name',type='text',required)
        input(name='username', placeholder='username',type='text',required)
        input(name='password', placeholder='password',type='password',required)
        input(name='password2', placeholder='Confirm password',type='password',required)
        input(name='email', placeholder='email',type='email',required)
        input(name='location', placeholder='location',type='text')
        input(type='submit',value='Join')

 

export const postJoin = async(req, res) => {
    const { name, username, password, password2, email, location} = req.body;
    const pageTitle = "Create Account";
    
    if(password !== password2){
        return res.render("join", {pageTitle, errorMessage : 'Password Confirmation Fail'});
    }

    const exists = await User.exists({$or : [{username}, {email}]});
    if(exists){
        return res.render("join", {pageTitle, errorMessage : 'The username/email is already taken.'});
    }
    await User.create({
        name, username, password, email, location
    });

    return res.redirect("/login");
};

 

password를 확인하는 부분까지 추가해주었다.

 

 

#7.4 Status Codes (Bonus)

다양한 status code가 존재한다.

위의 문서에 따르면 2xx는 success, 3xx는 redirection, 4xx는 client error, 5xx는 server error라고 한다.

우리는 보통 status code가 200인 것을 통해 성공적으로 수행된 것을 확인한다.

 

이제 에러를 처리할 때 4xx를 사용해보자. 4xx에도 종류가 많지만, 우리는 Bad Request를 뜻하는 400을 사용할 것이다.

export const postJoin = async(req, res) => {
    const { name, username, password, password2, email, location} = req.body;
    const pageTitle = "Create Account";
    
    if(password !== password2){
        return res.status(400).render("join", {pageTitle, errorMessage : 'Password Confirmation Fail'});
    }

    const exists = await User.exists({$or : [{username}, {email}]});
    if(exists){
        return res.status(400).render("join", {pageTitle, errorMessage : 'The username/email is already taken.'});
    }
    await User.create({
        name, username, password, email, location
    });

    return res.redirect("/login");
};

 

res.render사이에 status(400)을 추가해주면 된다!

 

export const watch = async(req, res) => {
    const { id } = req.params;
    const video = await Video.findById(id);
    if(!video){
        return res.status(404).render("404", {pageTitle: "Video Not Found."});
    }
    return res.render("watch", {pageTitle: video.title, video});
};

export const getEdit = async(req, res) => {
    const { id } = req.params;
    const video = await Video.findById(id);
    if(!video){
        return res.status(404).render("404", {pageTitle: "Video Not Found."});
    }
    return res.render("edit", {pageTitle: `Editing '${video.title}'`,video});
};

 

videocontroller.js에도 404 status를 추가해주었다.

이제 400 코드를 받는 것을 확인할 수 있었다.

 

#7.5, #7.6 Login

이제 Login 부분을 구현해주자. 일단 처음은 join과 거의 동일하다.

  1. rootRouter에 getLogin router를 만들어주고,
  2. usercontroller.js에 getLogin 함수를 넣어준다.
  3. 마지막으로 login.pug를 만든다.
import express from "express";
import { home, search} from "../controllers/videocontroller";
import { getJoin, getLogin, join, login, postJoin } from "../controllers/usercontroller";

const rootRouter = express.Router();

rootRouter.get("/", home);
rootRouter.route("/join").get(getJoin).post(postJoin);
rootRouter.route("/login").get(getLogin).post(postLogin);
rootRouter.get("/search", search);

export default rootRouter;

rootRouter.js

export const getLogin = (req, res) => {
    return res.render("login",{pageTitle: "Login"});
};

usercontroller.js

extends base.pug

block content 
    if errorMessage 
        span=errorMessage
    form(method='POST')
        input(name='username', placeholder='username',type='text',required)
        input(name='password', placeholder='password',type='password',required)
				input(type='submit',value='Login')
    hr
    div 
        span Don't have an account? 
        a(href='/join') Join →

login.pug

 

이제 postLogin을 좀 다뤄주자. 먼저 존재하지 않는 username을 입력하면 오류를 반환해야 한다.

export const postLogin = async(req, res) => {
    const { username, password } = req.body;
    const exists = await User.exists({username})
    if(!exists){
        return res.status(400).render("login",{pageTitle: "Login", errorMessage: "Not existing username.."});
    }
    
    return res.end()
}

 

 

 

이제 password를 비교해야 한다. 우리가 db에 password의 해시값을 저장했으므로 그냥 자바스크립트 코드가 아닌 다른 방법으로 비교해야 한다.

export const postLogin = async(req, res) => {
    const { username, password } = req.body;
    const pageTitle = "Login";
    const user = await User.findOne({username});
    if(!user){
        return res.status(400).render("login",{pageTitle, errorMessage: "Not existing username.."});
    }
    
    const ok = await bcrypt.compare(password, user.password);
    if(!ok){
        return res.status(400).render("login",{pageTitle, errorMessage: "Wrong Password.."});
    }

    return res.redirect("/");
}

 

위와 같이 postLogin 함수를 바꿔주었다.

 

#7.7, #7.8 Sessions and Cookies

npm i express-session

브라우저가 서버(백엔드)에 요청을 전달하면 서버가 쿠키를 설정해준다.

브라우저는 서버에 요청을 보낼 때 이 쿠키를 함께 전달하고, 서버는 쿠키를 통해 사용자를 검증한다.

서버는 session에 쿠키를 보관하고 있기 때문에 검증이 가능하다.

import express from "express";
import morgan from "morgan";
import session from "express-session";
import rootRouter from "./routers/rootRouter";
import videoRouter from "./routers/videoRouter";
import userRouter from "./routers/userRouter";

const app = express();
const logger = morgan("dev");

app.set("view engine", "pug");
app.set("views", process.cwd() + "/src/views");
app.use(logger);
app.use(express.urlencoded({ extended: true }));

app.use(
    session({
        secret : "jin!",
        resave : true,
        saveUninitialized : true,
    })
);

app.use("/",rootRouter);
app.use("/videos",videoRouter);
app.use("/users",userRouter);

export default app;

 

위와 같이 session middleware를 넣어줄 수 있다.

 

일단 지금 만든 세션은 서버가 재시작할 때마다 초기화된다. 나중에 mongoDB에 연결해야 한다.

 

#7.9, #7.10 Logged In User

app.use(
    session({
        secret : "jin",
        resave : true,
        saveUninitialized : true,
    })
);

app.use((req, res, next) => {
    req.sessionStore.all((err, sessions) => {
        console.log(sessions);
        next();
    });
});

 

Server.js에 sessionStore를 출력해주는 코드를 추가해주었다.

 

export const postLogin = async(req, res) => {
    const { username, password } = req.body;
    const pageTitle = "Login";
    const user = await User.findOne({username});
    if(!user){
        return res.status(400).render("login",{pageTitle, errorMessage: "Not existing username.."});
    }
    
    const ok = await bcrypt.compare(password, user.password);
    if(!ok){
        return res.status(400).render("login",{pageTitle, errorMessage: "Wrong Password.."});
    }
    req.session.loggedIn = true;
    req.session.user = user;
    
    return res.redirect("/");
}

 

postLogin 함수 안에, 로그인에 성공할 시 session의 loggedIn을 true로 바꿔주고 user에 user 객체를 선언해주는 코드를 넣어줬다.

 

그 다음 id: admin, pw: admin으로 로그인해줬더니,

session에 내 cookie 값과 user 정보가 저장되었다!

이렇게 session을 가지고 특정 사용자가 로그인되었는지 아닌지를 확인할 수 있다.

 

이제 로그인 했을 때와 아닐 떄를 구분하여 보여지는 html을 살짝 수정해줘보자.

그러기 위해서는 pug에도 세션 정보를 전달해줘야 한다.

 

res.locals → 변수를 전역적으로 보내줄 수 있다. 즉, 전역 변수를 하나 선언해주는 느낌, res.render로 전달해줄 필요가 없다!

따라서 우리는 req.session의 값들을 res.locals의 변수들에 넣어주고, 해당 변수들을 이용해서 pug에서 접근할 것이다.

 

export const localsMiddleware = (req, res, next) => {
    res.locals.loggedIn = Boolean(req.session.loggedIn);
    res.locals.siteName = "Wetube";
    res.locals.loggedInUser = req.session.user;
    next();
}

middleware.js

middleware들을 server.js에 계속 넣다보면 파일이 너무 커질 수 있기 때문에 middleware.js를 따로 만들어주었다.

import express from "express";
import morgan from "morgan";
import session from "express-session";
import rootRouter from "./routers/rootRouter";
import videoRouter from "./routers/videoRouter";
import userRouter from "./routers/userRouter";
import { localsMiddleware } from "./middleware";

const app = express();
const logger = morgan("dev");

app.set("view engine", "pug");
app.set("views", process.cwd() + "/src/views");
app.use(logger);
app.use(express.urlencoded({ extended: true }));

app.use(
    session({
        secret : "jin",
        resave : true,
        saveUninitialized : true,
    })
);

app.use(localsMiddleware);
app.use("/",rootRouter);
app.use("/videos",videoRouter);
app.use("/users",userRouter);

export default app;

 

그리고 server.js의 특정 위치에 localsMiddleware를 import해주었다. 이때 위치 순서를 잘 지켜줘야 하는데, 그 이유는 이게 말 그대로 middleware이기 때문에 특정 값을 얻어서 그 값을 처리해야 하기 때문이다. 따라서 session을 만드는 부분 뒤에 반드시 들어가야 한다.

 

위의 res.locals 변수들을 활용하여 base.pug를 조금 수정해주었다.

 

 

#7.12, #7.13 MongoStore

connect-mongo를 사용하여 세션을 mongoDB에 저장시켜보자! → npm i connect-mongo

import express from "express";
import morgan from "morgan";
import session from "express-session";
import rootRouter from "./routers/rootRouter";
import videoRouter from "./routers/videoRouter";
import userRouter from "./routers/userRouter";
import { localsMiddleware } from "./middleware";
import MongoStore from "connect-mongo";

const app = express();
const logger = morgan("dev");

app.set("view engine", "pug");
app.set("views", process.cwd() + "/src/views");
app.use(logger);
app.use(express.urlencoded({ extended: true }));

app.use(
    session({
        secret : "jin",
        resave : true,
        saveUninitialized : true,
        store : MongoStore.create({mongoUrl : 'mongodb://127.0.0.1:27017/wetube'})
    })
);

app.use(localsMiddleware);
app.use("/",rootRouter);
app.use("/videos",videoRouter);
app.use("/users",userRouter);

export default app;

 

MongoStore.create로 mongoDB와 연결해준다.

이렇게 mongoDB의 sessions table에 세션 정보가 저장된 것으 확인할 수 있었다!

 

그런데 현재는 백엔드에 접속하는 모든 사용자 각각의 세션을 모두 저장하고 있다. 이는 별로 좋은 방법이 아니다.

위의 사진은 로그인하지 않은 익명의 두 사용자의 세션 정보까지 저장한 것을 보여주는 것이다.

따라서 이제는 로그인을 한 유저의 세션 정보만 저장하도록 만들어보자.

 

app.use(
    session({
        secret : "jin",
        resave : false,
        saveUninitialized : false,
        store : MongoStore.create({mongoUrl : 'mongodb://127.0.0.1:27017/wetube'})
    })
);

 

server.js에 있던 middleware이다. resave와 saveUninitialize를 false로 만들어주면 req.session에 변화가 있을 때만 해당 값을 db에 저장한다.

우리는 login에 성공하면 req.session의 loggedIn과 user를 변경해주었기에, login을 했을 때만 db에 세션 정보가 저장되게 된다.

 

 

#7.14, #7.15 Expiration and Secrets

이제 cookie의 proprety 각각에 대해 알아보자.

Domain – 쿠키를 만든 backend, 브라우저는 해당 backend에만 쿠키를 전송한다.

Path – 쿠키를 만든 url

Expires – 쿠키의 수명을 뜻한다. 그냥 Session이라고 되어있는 경우 정해진 수명이 없고, 브라우저가 프로그램을 종료할 때 쿠키도 사라지게 된다. / Maxage – 쿠키가 사라지는 기한(Millisecond 단위)

app.use(
    session({
        secret : "jin",
        resave : false,
        saveUninitialized : false,
        store : MongoStore.create({mongoUrl : 'mongodb://127.0.0.1:27017/wetube'}),
        cookie : {maxAge : 20000}
    })
);

 

위와 같이 선언하면 쿠키는 20초후에 사라지게 된다. 즉, 로그인이 20초동안만 유효하다는 것이다.

 

 

middleware에서 session을 만들 때 secret 값을 넣어주었다.

이 값은 우리의 쿠키에 sign 할 때 사용되는 값으로, 우리가 얻은 쿠키가 올바른 서버에서 준 것이 맞는지를 검증하기 위해 사용된다. (session hijacking 공격을 당한 것이 아닌지)

따라서 이 값은 노출돼선 안된다.

 

이를 위해서 우리는 .env 파일을 만들어준다.

COOKIE_SECRET=Cykor{J1nD0g3}
DB_URL=mongodb://127.0.0.1:27017/wetube

 

(그리고 이 .env 파일은 .gitignore에 추가해준다 → 해당 파일은 git에 안 올릴거니까!)

npm i dotenv → import “dotenv/config” in init.js

import "dotenv/config"
import "./db";
import "./models/Video";
import "./models/User";
import app from "./server";

const PORT = 4000;

const handleListening = () => 
    console.log(`✅ Server listening on port http://localhost:${PORT} 🚀`);

app.listen(PORT, handleListening);

 

package.json의 script에 의해 가장 먼저 실행되는 init.js에 dotenv/config를 import 해준다.

이렇게 하면 다른 파일들에 dotenv를 import하지 않아도 .env 파일에 접근하여 변수를 읽어올 수 있다.

 

import express from "express";
import morgan from "morgan";
import session from "express-session";
import rootRouter from "./routers/rootRouter";
import videoRouter from "./routers/videoRouter";
import userRouter from "./routers/userRouter";
import { localsMiddleware } from "./middleware";
import MongoStore from "connect-mongo";

const app = express();
const logger = morgan("dev");

app.set("view engine", "pug");
app.set("views", process.cwd() + "/src/views");
app.use(logger);
app.use(express.urlencoded({ extended: true }));

app.use(
    session({
        secret : process.env.COOKIE_SECRET,
        resave : false,
        saveUninitialized : false,
        store : MongoStore.create({mongoUrl : process.env.DB_URL}),
        cookie : {maxAge : 20000}
    })
);

app.use(localsMiddleware);
app.use("/",rootRouter);
app.use("/videos",videoRouter);
app.use("/users",userRouter);

export default app;

server.js

import mongoose from "mongoose";

mongoose.connect(process.env.DB_URL);

const db = mongoose.connection;

const handleOpen = () => console.log("✅ Connected to DB");
const handleError = (error) => console.log(`❌ DB ERROR : ${error}`);

db.on("error", handleError);
db.once("open", handleOpen);

db.js

그 후 server.js와 db.js에서 Secret 값이나 db_url에 접근할 때 process.env로 접근하도록 만들어주었다. 이렇게 되면 다른 사람들은 내 코드를 봐도 해당 변수들이 어떤 값을 가지고 있는지 알 수 없다!

 

#7.16 ~ #7.21 Github Login

Github의 OAuth application을 이용해서 내 홈페이지에 github 로그인을 구현해보자.

다음 페이지로 가서, OAuth 페이지로 간다.

각 항목에 다음과 같이 URL을 입력해주면,

위와 같이 만들 수 있다!

 

이제 로그인 단계를 생각해보자.

  1. 먼저, 사용자가 내 페이지에서 로그인하려고 할 때 Github 인증 페이지로 redirect 시켜야 한다.
extends base.pug

block content 
    if errorMessage 
        span=errorMessage
    form(method='POST')
        input(name='username', placeholder='username',type='text',required)
        input(name='password', placeholder='password',type='password',required)
        input(type='submit',value='Login')
    br
    a(href='https://github.com/login/oauth/authorize') Continue with Github →
    hr
    div 
        span Don't have an account? 
        a(href='/join') Join →

 

login.pug에 위의 url을 추가해주었다.

이동해보면 아직은 404를 반환한다. 그 이유는 아무런 parameter를 전달해주지 않았기 때문이다.

먼저 client_id를 보내주었다.

다음과 같이 Authorize Wetube가 떴다. 그러나 아직 사용자의 Public data만 확인할 수 있다. 더 많은 정보를 보기 위해서는 scope 인자를 추가해 줘야 한다.

(allow_signup → github 로그인으로 넘어갔을 때 회원가입을 가능하게 할지 말지 설정)

 

scope에 user:email을 추가해줬더니 user의 email까지 확인할 수 있게 되었다. 이렇게 scope에는 공백으로 나눠서 확인하고 싶은 정보들을 넣어주면 된다. (옵션은 scope docs 참고)

 

그런데 login.pug에 url을 길게 남겨주는 것은 보기 좋지는 않다. 따라서 route와 controller를 이용해서 조금 더 보기 좋게 만들어주자.

import express from "express";
import { edit, deleteU, logout, see, startGithubLogin } from "../controllers/usercontroller";

const userRouter = express.Router();

userRouter.get("/edit", edit);
userRouter.get("/delete", deleteU);
userRouter.get("/logout", logout);
userRouter.get("/github/start",startGithubLogin);
userRouter.get("/:id", see);

export default userRouter;

userRouter.js

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

usercontroller.js

extends base.pug

block content 
    if errorMessage 
        span=errorMessage
    form(method='POST')
        input(name='username', placeholder='username',type='text',required)
        input(name='password', placeholder='password',type='password',required)
        input(type='submit',value='Login')
    br
    a(href='users/github/start') Continue with Github →
    hr
    div 
        span Don't have an account? 
        a(href='/join') Join →

login.pug

userRouter.js, usercontroller.js, login.pug를 위와 같이 만들어주었다.

이제 user profile과 email을 확인할 수 있다. Authorize를 누르면?

우리가 처음에 설정해준 callback URL로 옮겨진다. 따라서 해당 URL에 대해서도 처리해주자.

 

이제 user의 정보를 가져와보자.

먼저 .env 파일에 GH_CLIENT와 GH_SECRET을 추가해주었다.

COOKIE_SECRET = Cykor{J1nD0g3}
DB_URL = mongodb://127.0.0.1:27017/wetube
GH_CLIENT = 07e7847a9e401ff36a8c
GH_SECRET = b246b5c5307c2c4e5c429fc62cc20ddf2b2ff364

 

사실 GH_CLIENT는 공개되어도 상관 없는 정보이지만(그리고 어차피 url에 드러난다), GH_SECRE은 절대 공개되어서는 안된다.

 

이제 GH_CLIENT, GH_SECRET, 그리고 query의 code(Github에서 준 token)을 가지고 finishGithubLogin 함수를 완성해보자.

export const finishGithubLogin = async(req, res) => {
    const baseURL = "https://github.com/login/oauth/authorize";
    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}`;
    const data = await fetch(finalURL, {
        method : "POST",
        headers: {
            Accept: application/json
        },
    })
    const json = await data.json();
}

 

일단 위와 같이 만들어주었다. 그런데 NodeJS에는 fetch가 존재하지 않기 때문에 따로 import 해줘야 한다! → npm i node-fetch → NodeJS 18버전 부터는 안해도 된다!! ㅎㅎ

다음과 같이 json을 받아왔다..

이제 위의 json을 바탕으로 3단계를 진행해보자.

export const finishGithubLogin = async(req, res) => {
    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}`;
    console.log(finalURL);
    const tokenRequset = await (
        await fetch(finalURL, {
            method : "POST",
            headers: {
                Accept: "application/json",
            },
        })).json();
    if ("access_token" in tokenRequset){
        const { access_token } = tokenRequset;
        const userRequest = await (
            await fetch("https://api.github.com/user", {
                headers: {
                    Authorization : `token  ${access_token}`
                }
            })
        ).json();
        console.log(userRequest);
    }else{
        return res.redirect("/login");
    }
    
}

 

api.github로 연결되는 부분을 추가해주었다. 이는 tokenRequset에 access_token이 있을 때만 가능하다.

그런데 현재는 email이 비어있다. 이제 이부분을 가져오도록 바꿔보자.

 

우리가 위에서 얻은 access_token은 우리가 scope에서 선언한 행동만을 가능하게 해준다. 모든 행동을 가능하게 해주는 것은 아니다!

위의 docs를 참고하면 우리는 user의 email을 가져올 수 있다.

export const finishGithubLogin = async(req, res) => {
    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}`;
    console.log(finalURL);
    const tokenRequset = await (
        await fetch(finalURL, {
            method : "POST",
            headers: {
                Accept: "application/json",
            },
        })).json();
    if ("access_token" in tokenRequset){
        const { access_token } = tokenRequset;
        const apiURL = "https://api.github.com";
        const userData = await (
            await fetch(`${apiURL}/user`, {
                headers: {
                    Authorization : `token  ${access_token}`
                }
            })
        ).json();

        const emailData = await (
            await fetch(`${apiURL}/user/emails`, {
                headers: {
                    Authorization : `token  ${access_token}`
                }
            })
        ).json();

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

    }else{
        return res.redirect("/login");
    }
    
}

 

위와 같이 apiURL/user/emails 를 통해 user의 email 데이터를 가져왔다.

우리는 이중에서 primary와 verified가 모두 true인 email만 가져올 것이다. 이를 find 함수로 구현해준 것이다.

 

이제 마지막으로, 중복된 email로 로그인하려는 사용자를 처리해주는 부분을 완성해보자.

export const finishGithubLogin = async(req, res) => {
    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}`;
    console.log(finalURL);
    const tokenRequset = await (
        await fetch(finalURL, {
            method : "POST",
            headers: {
                Accept: "application/json",
            },
        })).json();
    if ("access_token" in tokenRequset){
        const { access_token } = tokenRequset;
        const apiURL = "https://api.github.com";
        const userData = await (
            await fetch(`${apiURL}/user`, {
                headers: {
                    Authorization : `token  ${access_token}`
                }
            })
        ).json();

        const emailData = await (
            await fetch(`${apiURL}/user/emails`, {
                headers: {
                    Authorization : `token  ${access_token}`
                }
            })
        ).json();

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

        const existingUser = await User.findOne({email : emailObj.email});
        if (existingUser){
            req.session.loggedIn = true;
            req.session.user = existingUser;
            res.redirect("/");
        }
        else{
            
        }
    }else{
        return res.redirect("/login");
    }
    
}

 

github가 전달해준 user의 email을 db에서 찾아서, 해당 email을 가진 user가 있으면 이전에 했던 것처럼 session의 loggedIn을 true, user를 existingUser로 바꿔주고 home으로 리다이렉트 시켜줬다.

 

const userSchema = new mongoose.Schema({
    name: { type: String, required: true},
    socialOnly : {type: Boolean, default : false},
    username: { type: String, required: true, unique: true},
    password: {type: String},
    email: { type: String, required: true, unique: true},
    location: {type: String},
})

 

먼저, User.js의 userSchema에 socialOnly를 추가해주었다. 이는 소셜 로그인인지 아닌지를 구분하는 것으로, 소셜 로그인 시에는 비밀번호가 존재하지 않기 때문에 추가해준 것이다.

→ 추가로 password의 required 역시 지워줬다. 소셜 로그인은 비밀번호가 없기 때문이다!

export const finishGithubLogin = async(req, res) => {
    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}`;
    console.log(finalURL);
    const tokenRequset = await (
        await fetch(finalURL, {
            method : "POST",
            headers: {
                Accept: "application/json",
            },
        })).json();
    if ("access_token" in tokenRequset){
        const { access_token } = tokenRequset;
        const apiURL = "https://api.github.com";
        const userData = await (
            await fetch(`${apiURL}/user`, {
                headers: {
                    Authorization : `token  ${access_token}`
                }
            })
        ).json();

        const emailData = await (
            await fetch(`${apiURL}/user/emails`, {
                headers: {
                    Authorization : `token  ${access_token}`
                }
            })
        ).json();

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

        console.log(userData);
        const existingUser = await User.findOne({email : emailObj.email});
        if (existingUser){
            req.session.loggedIn = true;
            req.session.user = existingUser;
            res.redirect("/");
        }
        else{
            //should create an account
            const user = await User.create({
                name: userData.name,
                socialOnly: true,
                username: userData.login, 
                password: "", 
                email: emailObj.email, 
                location: userData.location,
            });

            req.session.loggedIn = true;
            req.session.user = user;
            res.redirect("/");
        }
    }else{
        return res.redirect("/login");
    }
    
}

 

완성하면 다음과 같다. 만약 github로 로그인하려는데 db에 같은 이메일을 가진 user가 존재하지 않는다면, github가 넘겨준 email과 user 정보들을 활용하여 새로운 user를 db에 추가해주고, 해당 user로 로그인하는 것이다.

위와 같이 ‘heojin2003@korea.ac.kr’ 이메일을 가진 user가 db에 없는 상태일 때 github로 로그인을 시도하면,

새로운 user가 db에 추가된다! socialOnly가 true로 되어있고, 다른 정보들 역시 github로부터 받아온 것임을 확인할 수 있다.

 

#7.22 Logout

export const finishGithubLogin = async(req, res) => {
    ...

        console.log(userData);
        let user = await User.findOne({email : emailObj.email});
        if (!user){
            //should create an account
            user = await User.create({
                avatarURL: userData.avatarURL,
                name: userData.name,
                socialOnly: true,
                username: userData.login, 
                password: "", 
                email: emailObj.email, 
                location: userData.location,
            });
        }
        req.session.loggedIn = true;
        req.session.user = user;
        res.redirect("/");
    }else{
        return res.redirect("/login");
    }
    
}

 

먼저 위의 함수를 조금 더 깔끔하게 바꿔주었다. (추가로 avatarURL을 schema에 추가해줬다.)

export const logout = (req, res) => {
    req.session.destroy();
    return res.redirect("/")
};

 

logout 함수는 간단하다. 그냥 req.session.destroy() 해주면 된다!

 

 


 

** 본 글은 노마드 코더의 ‘유튜브 클론코딩’ 강의를 참조하여 작성하였습니다. **

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤