62일차 [인증/보안] OAuth
2021. 10. 22 금요일
1. Today's Key Points!🔑
- OAuth 2.0
2. 정리해보자!🧹
OAuth 2.0
OAuth2.0은 인증을 위한 표준 프로토콜의 한 종류이다. 보안 된 리소스에 액세스하기 위해 클라이언트에게 권한을 제공하는 프로세스를 단순화하는 프로토콜 중 한 방법이다.
OAuth는 언제, 왜 쓸까?
유저 입장에서, 우리는 웹상에서 굉장히 많은 서비스를 이용하고 있고, 각각의 서비스들을 이용하기 위해서 회원가입 절차가 필요한 경우가 대부분이다. 그 서비스별로 ID와 Password를 다 기억하는 것은 매우 귀찮은 일이다. OAuth를 활용한다면 자주 사용하는 서비스의 ID와 Password만 기억해 놓고 해당 서비스들을 통해서 소셜 로그인을 할 수 있게된다. 뿐만 아니라 보안상의 이점도 있다. 검증되지 않은 App에서 OAuth를 사용해서 로그인한다면, 직접 유저의 민감한 정보가 App에 노출될 일이 없고 인증 권한에 대한 허가를 미리 유저에게 구해야 하기 때문에 더 안전하게 사용할 수 있다.
OAuth에서 꼭 알아야 할 용어
- Resource Owner : 액세스 중인 리소스의 유저. 김코딩의 구글 계정을 이용하여 App에 로그인할 경우, 이때 Resource owner은 김코딩이 된다.
- Client : Resource owner를 대신하여 보호된 리소스에 액세스하는 응용프로그램. 클라이언트는 서버, 데스크탑, 모바일 또는 기타 장치에서 호스팅할 수 있다.
- Resource server : client의 요청을 수락하고 응답할 수 있는 서버.
- Authorization server : Resource server가 액세스 토큰을 발급받는 서버. 즉 클라이언트 및 리소스 소유자를 성공적으로 인증한 후 액세스 토큰을 발급하는 서버를 말한다.
- Authorization grant : 클라이언트가 액세스 토큰을 얻을 때 사용하는 자격 증명의 유형.
- Authorization code : access token을 발급받기 전에 필요한 code. client ID로 이 code를 받아온 후, client secret과 code를 이용해 Access token 을 받아온다.
- Access token : 보호된 리소스에 액세스하는 데 사용되는 credentials. Authorization code와 client secret을 이용해 받아온 이 Access token으로 이제 resource server에 접근을 할 수 있다.
- Scope : scope는 토큰의 권한을 정의한다. 주어진 액세스 토큰을 사용하여 액세스할 수 있는 리소스의 범위이다.
소셜 로그인 로직 플로우
3. Sprint 과제 복기!🧐
github OAuth를 하기위해서는 github에 내 앱을 등록해야한다. https://github.com/settings/developers에서 등록하면 된다. 등록하는 과정은 생략하겠다. client_id, client_secret을 받고, env파일에 넣어둔다.
위에 정리해놓은 플로우대로 한번 진행해보자. 클라이언트에서 github 아이디로 로그인을 하려고 한다. 그럼 클라이언트에서는 github으로 로그인하기를 누르면 github로그인을 할 수 있는 곳으로 이동시켜주어야 한다.
//Login Component
import React, { Component } from "react";
class Login extends Component {
constructor(props) {
super(props);
this.socialLoginHandler = this.socialLoginHandler.bind(this);
// GitHub로부터 사용자 인증을 위해 GitHub로 이동해야 한다. 적절한 URL을 입력해주어야 한다.
// OAuth 인증이 완료되면 authorization code와 함께 callback url로 리디렉션 한다.
// 아까 발급받은 client_id를 파라미터로 넣어준다.
this.GITHUB_LOGIN_URL =
"https://github.com/login/oauth/authorize?client_id=088ba3fb0c476ab48f94";
}
socialLoginHandler() {
window.location.assign(this.GITHUB_LOGIN_URL);
}
render() {
return (
<div className="loginContainer">
OAuth 2.0으로 소셜 로그인을 구현해보세요.
<img
id="logo"
alt="logo"
src="https://image.flaticon.com/icons/png/512/25/25231.png"
/>
<button onClick={this.socialLoginHandler} className="socialloginBtn">
Github으로 로그인
</button>
</div>
);
}
}
export default Login;
github 로그인에 성공하면 authorizationCode를 내 앱에 발급해 줄 것이다. 그럼 이 코드를 서버에 전달해줘서 서버가 github 서버한데 access token을 달라고 요청할 것이다.
//서버에 code를 전달해주고 access token을 받아온다.
import React, { Component } from "react";
import { BrowserRouter as Router } from "react-router-dom";
import Login from "./components/Login";
import Mypage from "./components/Mypage";
import axios from "axios";
class App extends Component {
constructor() {
super();
this.state = {
isLogin: false,
accessToken: "",
};
this.getAccessToken = this.getAccessToken.bind(this);
}
async getAccessToken(authorizationCode) {
// 받아온 authorization code로 다시 OAuth App에 요청해서 access token을 받을 수 있다.
// access token은 보안 유지가 필요하기 때문에 클라이언트에서 직접 OAuth App에 요청을 하는 방법은 보안에 취약할 수 있다.
// authorization code를 서버로 보내주고 서버에서 access token 요청을 하는 것이 적절하다.
// 서버의 /callback 엔드포인트로 authorization code를 보내주고 access token을 받아온다.
// access token을 받아온 후
// - 로그인 상태를 true로 변경하고,
// - state에 access token을 저장하자
let res = await axios.post("http://localhost:8080/callback", {
authorizationCode,
});
this.setState({
isLogin: true,
accessToken: res.data.accessToken,
});
}
componentDidMount() {
const url = new URL(window.location.href);
const authorizationCode = url.searchParams.get("code");
if (authorizationCode) {
// authorization server로부터 클라이언트로 리디렉션된 경우, authorization code가 함께 전달된다.
// ex) http://localhost:3000/?code=5e52fb85d6a1ed46a51f
this.getAccessToken(authorizationCode);
}
}
render() {
const { isLogin, accessToken } = this.state;
return (
<Router>
<div className="App">
{isLogin ? <Mypage accessToken={accessToken} /> : <Login />}
</div>
</Router>
);
}
}
export default App;
그럼 서버에 callback 요청이 들어오면 토큰을 받아오는 것을 구현해보자.
require("dotenv").config();
const clientID = process.env.GITHUB_CLIENT_ID;
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
const axios = require("axios");
module.exports = (req, res) => {
// req의 body로 authorization code가 들어온다. 클라이언트에서 요청을 보낼때 body에 담아서 보냈기 때문이다.
const code = req.body.authorizationCode;
// 이제 authorization code를 이용해 access token을 발급받기 위한 post 요청을 보낸다.
axios
.post(
"https://github.com/login/oauth/access_token",
{ client_id: clientID, client_secret: clientSecret, code },
{ headers: { accept: "application/json" } }
)
.then((result) => {
res.status(200).json({ accessToken: result.data.access_token });
});
};
github에서 부여받은 client_id, client_secret, code를 담아서 post요청을 보내준다. headers에 accept:"application/json"을 해준 이유는 클라이언트가 서버에게 웬만하면 데이터 전송할 때 이러이러한 타입으로 가공해서 보내달라고 요청해서 우리가 읽을 수 있는 형태로 받기위해서 이다.
Content-Type과 다른점은 Content-Type에 적은 것은 우리가 보내는 타입이 이러이러한 것이다 라고 알려주는 것이고, accept는 이러이러한 타입으로 보내달라고 요청하는 것이라고 생각하면 된다.
이런 부분은 post요청을 할 때 중요하게 작용한다. get요청을 하면 어차피 URL 끝에 쿼리스트링으로 key=value이런 형식으로 날아가기 때문에 굳이 Content-Type 헤더가 필요없다. 서버 입장에서도 요청메시지의 method가 get이면 key=value 형식의 데이터라는 것을 유추할 수 있기 때문이다. 하지만 post나 put처럼 메시지 body에 json 형식의 데이터를 담아 보낼 때 Content-Type 값을 application/json으로 지정해서 보내야 서버에서도 어떤 형식의 데이터인지를 알 수 있을 것이다.
그럼 이제 access token을 받았으니 github에게 사용자 정보 get요청을 하러 가보자.
현재 access token은 상태로 저장이 되어있다. 그래서 Mypage 컴포넌트가 상태를 받아서 쓰면 된다.
//생략
class Mypage extends Component {
constructor(props) {
super(props);
this.state = {
images: [],
name: "",
login: "",
html_url: "",
public_repos: 0,
};
}
async getGitHubUserInfo() {
// GitHub API를 통해 사용자 정보를 받아오자.
let res = await axios.get("https://api.github.com/user", {
headers: { Authorization: `token ${this.props.accessToken}` },
});
let { name, login, html_url, public_repos } = res.data;
this.setState({ name, login, html_url, public_repos });
}
componentDidMount() {
this.getGitHubUserInfo();
this.getImages();
}
//생략
get 요청을 통해서 유저 정보들을 받아오고 받아온 정보들을 상태에 저장해서 클라이언트에 뿌려준다.
images는 우리 서버에서 받아온다. access token을 제대로 받아온것이 맞다면 images파일을 클라이언트로 보내줘야 한다.
const images = require("../resources/resources");
module.exports = (req, res) => {
if (!req.headers.authorization) {
return res
.status(403)
.json({ message: "no permission to access resources" });
} else {
return res.status(200).json({ images });
}
};
클라이언트에서는 어떻게 요청을 하는지 한번 보자.
async getImages() {
// TODO : 마찬가지로 액세스 토큰을 이용해 local resource server에서 이미지들을 받아와 주세요.
// resource 서버에 GET /images 로 요청하세요.
let res = await axios.get("http://localhost:8080/images", {
headers: { Authorization: `token ${this.props.accessToken}` },
});
this.setState({ images: res.data.images });
}
componentDidMount() {
this.getGitHubUserInfo();
this.getImages();
}
헤더에 발급받은 토큰을 담아서 get요청으로 images를 받아오고 이것을 상태에 저장시켜준다.
이것으로 간단한 github OAuth 구현이 끝이 났다. 추후에 google OAuth 구현을 해볼 것이다.
'코드스테이츠 수강 TIL > Section 3' 카테고리의 다른 글
Codestates HA3 Section3 회고 (0) | 2021.11.20 |
---|---|
63, 64일차 [컴퓨터 공학] 기초 (0) | 2021.11.02 |
61일차 [인증/보안] Token (0) | 2021.10.25 |
60일차 [인증/보안] Cookie/Session (0) | 2021.10.24 |
59일차 [데이터베이스] NoSQL (0) | 2021.10.23 |