(이미지 출처: 코드스테이츠)
이번 스프린트로 OAuth 작동방식을 이해해보았다. 직접 OAuth로 로그인 가능한 어플리케이션을 제작해보면서 "브라우저" - "내 서버" - "인증 대행 서비스"간 요청/응답을 주고받는 로직을 구현하였다.
참고문서를 보면서 깃헙에 내 앱을 등록하였다.
여기서 Authorization callback URL를 입력하는데 이건 무엇이냐면,,,
OAuth 메커니즘이 인증 과정이 끝난 후 리디렉션을 통해 다시 내 앱으로 이동하는 원리이므로, 내 앱으로 돌아가기 위한 Authorization callback URL이 필요하다. Client ID 및 Client Secret 를 확인할 수 있다.
GitHub App에서 제공하는 Client ID 및 Client Secret의 정보를 기입한다.
주의: Client Secret은 항상 비밀로 지켜져야 한다!
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
이전 session과 token 스프린트와 달리 https가 아니라 http 프로토콜을 사용한다. 따라서 이번 스프린트는 인증서 파일들도 심지 않았다.
아래와 같은 요청을 처리하는 코드를 작성하였다.
Access token을 발급 받는 과정은 클라이언트에서보다, 서버에서 이루어지는 것이 더욱 안전하다. 그래서 클라이언트에서 받아온 Authorization code를 서버의 /callback
엔드포인트로 전달해 서버에서 github App에게 Access token 발급을 요청한다.
깃헙공식문서 를 참고하였다.
axios({
method: 'post',
url: `https://github.com/login/oauth/access_token`,
headers: {
accept: 'application/json',
}, //이 부분을 작성하지 않으면 객체형태로 응답을 받지 못한다.(이 부분 작성하지 않은 result를 콘솔로 찍어보니 그냥 문자열 형태...)
data: {
client_id: clientID,
client_secret: clientSecret,
code: req.body.authorizationCode
//클라이언트가 보낸 POST요청 내 body에서 받아온 authorization code를 가지고
//authorization server에 access token을 달라고 POST요청을 보낸다
}
})
받아온 Access token을 확인한 후, local에 저장된 resource images를 클라이언트로 보내주는 라우터이다.
여기서 중요한것...!!! 토큰은 클라이언트의 요청 내 헤더에서 담겨서 온다!
const accessToken = req.headers.authorization;
//토큰은 클라이언트의 요청 내 헤더에서 담겨서 온다!
if(!accessToken){
res.status(403).send({
message: 'no permission to access resources'
});
} else{
res.status(200).json({
images
})
}
App.js
에는 두 개의 컴포넌트가 있다. 현재 상태가 로그인이 되어있다면 Mypage 컴포넌트가, 되어있지 않다면 Login 컴포넌트가 실행된다.
앱을 실행했을 경우 첫 화면의 즉, Login 컴포넌트는 이런 모습이다.
여기서 'Github으로 로그인' 이라는 버튼을 클릭하게 되면 내가 저장해 놓은 특정 url로 이동하게 된다.
class Login extends Component {
constructor(props) {
super(props)
this.socialLoginHandler = this.socialLoginHandler.bind(this)
// TODO: GitHub로부터 사용자 인증을 위해 GitHub로 이동해야 합니다. 적절한 URL을 입력하세요.
// OAuth 인증이 완료되면 authorization code와 함께 callback url로 리디렉션 합니다.
this.GITHUB_LOGIN_URL = 'https://github.com/login/oauth/authorize?client_id=ee33270fac53d2e7b61c'
}
socialLoginHandler() { //로그인 버튼을 클릭했을 때 실행되는 이벤트핸들러함수
window.location.assign(this.GITHUB_LOGIN_URL)
//location.assign() 메소드: 파라미터로 전달한 URL에서 리소스를 로드한다.
}
render() {
//.....
export default Login;
위의 url은 어디서 온 것이냐하면... 아래 깃헙 공식문서에 있는 API이다. 이 url로 GET 요청을 보내게 되면 유저가 자신이 가진 깃헙 계정으로 로그인하게 만든다. 아까 Login 컴포넌트에 있던 'Github으로 로그인' 버튼을 누르자 깃허브의 로그인 페이지로 이동한 것을 확인할 수 있었다.
여기서 로그인을 하게되면 유저는 다시 원래의 사이트(깃헙 내앱 등록 시 설정해두었던 callback url)으로 돌아오게 되고(리디렉트), 어떤 일시적으로 사용할 수 있는 코드가 code
파라미터로 주어지게 된다.
만약 autorization 코드가 정상적으로 주어지게 된다면
App.js
에 있는 componentDidMount
함수에서 getAcessToken
함수로 autorizationCode
를 보낸다.
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)
}
}
위에서 호출한 getAcessToken
함수는 아래와 같이 구현하여, access token을 받아온다.
받아온 authorization code로 다시 OAuth App에 요청해서 access token을 받을 수 있다. access token은 보안 유지가 필요하기 때문에 클라이언트에서 직접 OAuth App에 요청을 하는 방법은 보안에 취약할 수 있다. authorization code를 서버로 보내주고 서버에서 access token 요청을 하는 것이 적절하다.
깃허브 공식 문서에 따르면 이 url로 POST 요청을 보내게 되면 access token을 받을 수 있다고 아래와 같이 설명하고 있다.
async getAccessToken(authorizationCode) {
let callbackURL = 'http://localhost:8080/callback';
await axios.post(callbackURL, {
authorizationCode
})
//body에 authorizationCode를 실어서 POST요청을 보낸다.
.then(res => {
this.setState({
isLogin: true,
accessToken: res.data.accessToken
});
})
.catch(err => {
//console.log(err);
})
}
isLogin이 true가 되면 이제 App.js에서 Login 컴포너트 -> Mypage 컴포넌트가 된다.
이제 깃허브에 로그인한 유저에 대한 정보를 요청할건데 이게 바로 공식 문서에 나온 API이다. 클라이언트가 엑세스 토큰을 가지고 resource server에 resource 엑세스하려고 할 때 꼭!!! authorization 헤더에 토큰을 명시해줘야한다!!!
async getGitHubUserInfo() {
const { accessToken } = this.props
let response = await axios.get('https://api.github.com/user', {
headers: {
authorization: `token ${accessToken}`, // 토큰 명시 필수!!!
}
})
const { name, login, html_url, public_repos } = response.data
this.setState({
name,
login,
html_url,
public_repos
})
}
(로그인 성공시 렌더되는 Mypage 컴포넌트 화면)