[React] OAuth 2.0 사용하기 (w/ password grant)

강버섯·2022년 2월 8일
1

AUTHORIZATION

목록 보기
2/9

👉 시작하기 전에

context를 활용한 global state 관리를 통해 인증을 사용한다.

auth.js 👇

import { createContext, useContext } from "react";

export const AuthContext = createContext({
    //적어놓은 default 값이 적용되는 것은 아니지만 사용을 편리하게 하기 위해서 작성
    isSignedIn: false,
    profile: null,
    setIsSignedIn: () => {},
    setProfile: () => {},
})

//auth context를 사용하기 위한 hook 생성
export const useAuth = () => useContext(AuthContext)

생성한 context의 component로 가장 상단의 파일(_app.js or App.js)에서 다른 component를 감싸준다.
login에 관련된 정보는 global state에서 관리할 것이기 때문에 _app.js의 local state로 설정해준다.

_app.js 👇

import { useEffect, useState } from 'react'
import { AuthContext } from '../shared/context/auth'
import BaseLayout from '../shared/layouts/base'
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  const [isSignedIn, setIsSignedIn] = useState(false)
  const [profile, setProfile] = useState(null)

  return (
  <AuthContext.Provider value={{
    isSignedIn,
    profile,
    setIsSignedIn,
    setProfile,
  }}>
    <BaseLayout>
      <Component {...pageProps} />
    </BaseLayout>
  </AuthContext.Provider>
)
}

export default MyApp

👉 password grant 방식으로 auth 진행하기

password grant 로 인증을 하기 위해서는

  1. token endpoint
  2. grant type
  3. username, password : 사용자가 login을 하기 위해 필요한 정보
  4. client id
  5. client secret
  6. scope

의 값이 필요하다.

TOKEN_ENDPOINT는 인증을 진행하기 위한 OAuth Server의 주소이다.
실습에서는 kcloak을 사용했다.

password grant 방식이기 때문에 grant type은 password로 설정해주면 된다.

scope에는 사용자 "권한"에 대한 정보를 담아서 보내주면 된다.

http 요청으로 auth 인증이 진행되기 때문에 Basic 인증을 사용하여 client id와 client secret의 값을 보낸다.
Basic 인증으로 보낸다는 것은

  1. client id와 client secret을 :로 묶고
  2. base64 인코딩을 통해 token을 생성한 뒤,
  3. 이 token을 Basic <token>으로 보내는 것을 의미한다.

이렇게 만들어진 값은 Authorization header로 담아서 요청 시 보내주면 된다.

사용자가 로그인을 통해 인증을 받기 위한 위와 같은 값들이 준비되었다면, 인증을 받기 위한 TOKEN_ENDPOINT으로 post 요청을 보내면 된다.
요청을 보낼 시에는 요즘 주로 사용하는 json 형태가 아닌 form 형태로 전송을 해줘한다.
form 형태로의 전송은 qs.stringify를 사용하면 포맷을 생성할 수 있다.

auth에 관련한 정보들은 브라우저에 노출되면 안되는 민감한 정보이기 때문에, 이러한 정보는 next.js에서 제공하는 api 호출 기능을 사용하는 것이 좋다.

/api/auth/token.js 👇

import axios from "axios"
import qs from "qs"

export default async (req, res) => {
    const {username, password} = req.body
    //`.env.local` 파일을 통해 환경 변수를 사용할 수 있음 --> NEXT_PUBLIC으로 시작해야 next app이 인식을 함
    const OAUTH_TOKEN_ENDPOINT = process.env.NEXT_PUBLIC_OAUTH_TOKEN_ENDPOINT
    const OAUTH_CLIENT_ID = process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID
    const OAUTH_CLIENT_SECRET = process.env.NEXT_PUBLIC_OAUTH_CLIENT_SECRET
    const OAUTH_SCOPE = process.env.NEXT_PUBLIC_OAUTH_SCOPE

    // basic 인증으로 보내기
    const encode = Buffer.from(
        `${OAUTH_CLIENT_ID}:${OAUTH_CLIENT_SECRET}`
    ).toString("base64")
    const headers = {
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
        Authorization: `Basic ${encode}`
    }

    // oauth는 주로 form 방식으로 요청을 보냄(code/token 등을 요청 시) >> qs(queyr string)을 사용
    const resp = await axios.post(
        OAUTH_TOKEN_ENDPOINT,
        qs.stringify({
            grant_type: "password",
            username,
            password,
            scope: OAUTH_SCOPE
        }),
        {
            headers
        }
    )

    res.status(200).json(resp.data)
}

👉 login

usename과 password를 이용한 login을 진행하기 때문에, login page에서는 사용자의 username/password를 입력받은 후 해당 값을 이용하여 authorization server에 post 요청을 보내도록 한다.

성공적으로 인증을 받았다면, 응답 값으로 사용자의 정보(id_token에 암호화 되어있음), access_token, refresh_token의 값을 받아올 수 있다.
이렇게 받아온 값을 local 혹은 DB에 저장하도록하고 추후 인증이 필요한 경우, access_token을 재발급 받는 경우 등등에 사용하도록하면 된다.

login.js 👇

import axios from "axios";
import jwtDecode from "jwt-decode";
import { useRouter } from "next/router";
import React, { useState } from "react";
import { useAuth } from "../shared/context/auth";

const LoginPage = () => {
    const [username, setUsername] = useState("")
    const [password, setPassword] = useState("")

    const {setIsSignedIn, setProfile} = useAuth()
    const router = useRouter()

    const handleSubmit = async() => {
        const res = await axios.post("/api/oauth/token", {username, password})
        const {access_token, id_token, refresh_token} = res.data

        const decodedIdToken = jwtDecode(id_token)
        const profile = {email: decodedIdToken.email}

        localStorage.setItem("access_token", access_token)
        localStorage.setItem("refresh_token", refresh_token)
        localStorage.setItem("profile", JSON.stringify(profile))

        setIsSignedIn(true)
        setProfile(profile)

        //화면의 전환이 이뤄지도록 함 >> 새로고침 되는 것이 아니라 정보를 가지고 있는 상태에서 화면만 바뀜
      	// next.js 내부의 화면 전환이 이루어질 때
        router.push("/")
    }

    return (
        <div>
            <div>
                username :
                <input 
                type={"text"}
                value={username}
                onChange={(e) => setUsername(e.target.value)}/>
            </div>
            <div>
                password : 
                <input
                type={"text"}
                value={password}
                onChange={(e) => setPassword(e.target.value)}/>
            </div>
            <button onClick={handleSubmit}>Submit</button>
        </div>
    )
}

export default LoginPage;

👉 logout

logout는 login보다 간단하다.
login을 통해 받아와 local(or DB)에 저장된 정보들을 제거해주고 logout 후 이동하려는 화면으로 전환시켜주면 된다.

logout.js 👇

import { useRouter } from "next/router";
import React, { useEffect } from "react";
import { useAuth } from "../shared/context/auth";

const LogoutPage = () => {
    const {setIsSignedIn, setProfile} = useAuth()
    const router = useRouter()

    useEffect(() => {
        setIsSignedIn(false)
        setProfile(null)

        localStorage.removeItem("access_token")
        localStorage.removeItem("refresh_token")
        localStorage.removeItem("profile")

        router.push("/")
    },[])

    return <div>loading...</div>
}

export default LogoutPage
profile
무럭무럭 버섯농장

0개의 댓글