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
로 인증을 하기 위해서는
의 값이 필요하다.
TOKEN_ENDPOINT
는 인증을 진행하기 위한 OAuth Server의 주소이다.
실습에서는 kcloak을 사용했다.
password grant 방식이기 때문에 grant type
은 password로 설정해주면 된다.
scope
에는 사용자 "권한"에 대한 정보를 담아서 보내주면 된다.
http 요청으로 auth 인증이 진행되기 때문에 Basic 인증을 사용하여 client id와 client secret의 값을 보낸다.
Basic 인증으로 보낸다는 것은
:
로 묶고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)
}
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는 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