h e 1 1 o !

โ˜”Google Calendar ์—ฐ๋™ ์•ฑ / Google OAuth ๊ตฌํ˜„ํ•˜๊ธฐ ๋ณธ๋ฌธ

p r o j e c t

โ˜”Google Calendar ์—ฐ๋™ ์•ฑ / Google OAuth ๊ตฌํ˜„ํ•˜๊ธฐ

hee.hee 2023. 8. 2. 14:33

๐Ÿ“Œ Google ์†Œ์…œ ๋กœ๊ทธ์ธ ํ”Œ๋กœ์šฐ

  1. ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ์š”์ฒญ
  2. Google OAuth 2.0 ์ธ์ฆ ์„œ๋ฒ„์— ๋ฆฌ๋””๋ ‰์…˜๋˜์–ด ๋กœ๊ทธ์ธ ์ฐฝ์ด ํ‘œ์‹œ
  3. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์„ ์™„๋ฃŒ(์ ‘๊ทผ scope ๋™์˜)
  4. ๋ฏธ๋ฆฌ ์„ค์ •ํ•œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์ฃผ์†Œ๋กœ code ์คŒ
  5. code์™€ accesstoken, refreshtoken ๊ตํ™˜
  6. token ์ €์žฅ
  7. access ํ† ํฐ์„ ํ—ค๋”์— ํฌํ•จ์‹œ์ผœ ์š”์ฒญ
  8. access ํ† ํฐ ๋งŒ๋ฃŒ โ†’ ์˜ค๋ฅ˜
  9. refresh ํ† ํฐ์œผ๋กœ access ํ† ํฐ ๊ฐฑ์‹  ์š”์ฒญ
  10. ์ƒˆ๋กœ์šด access ํ† ํฐ ์‘๋‹ต
  11. ์ƒˆ๋กœ์šด access ํ† ํฐ ์ €์žฅ
  12. access ํ† ํฐ์„ ํ—ค๋”์— ํฌํ•จ์‹œ์ผœ ์š”์ฒญ

๋กœ๊ทธ์ธ์„ ์œ„ํ•œ ๊ตฌ๊ธ€ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ, ์ธ์ฆ ๋ฐฉ๋ฒ•์€ ์—ฌ๋Ÿฌ ๋ฌธ์„œ, ๋ธ”๋กœ๊ทธ์— ๋‚˜์™€์žˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” ๊ตฌ๊ธ€ cloud ์ธก ์„ค์ •์„ ์™„๋ฃŒํ–ˆ์Œ์„ ๊ฐ€์ •ํ•˜๊ณ  ํด๋ผ์ด์–ธํŠธ ์ธก ์ฝ”๋“œ ๋กœ์ง์„ ์•Œ์•„๋ณผ ๊ฒƒ์ด๋‹ค.

Google Cloud

 


 

โ˜” Google ๋กœ๊ทธ์ธ ๊ตฌํ˜„

๐Ÿ“– Google ๋กœ๊ทธ์ธ ๊ตฌํ˜„ ๋ฐฉ๋ฒ• ์ข…๋ฅ˜

  • OAuth 2.0 ์ธ์ฆ ํ”Œ๋กœ์šฐ ์ง์ ‘ ๊ตฌํ˜„ โœ”๏ธ
  • react-google-login ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  • googleapis(node.js ๋ชจ๋“ˆ)
  • Firebase Authentication
  • Google Identity Platform
  • Google Cloud Identity-Aware Proxy

๋‚˜๋Š” ์ธ์ฆ ํ›„์—๋„ ๊ตฌ๊ธ€ ์บ˜๋ฆฐ๋” api๋ฅผ ์‚ฌ์šฉํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—

๋‚ด๊ฐ€ ๊ตฌํ˜„ํ•  ์•ฑ์˜ ๊ธฐ๋Šฅ์— ๋”์šฑ ์ ํ•ฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง์ ‘ ๊ตฌํ˜„ํ–ˆ๋‹ค.

์œ„ ์„ธ๊ฐ€์ง€๋ฅผ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

 

 

๐Ÿธ ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ์š”์ฒญ ์ค€๋น„

google cloud project ์„ค์ •์„ ํ†ตํ•˜์—ฌ ์•„๋ž˜์˜ ๋ณ€์ˆ˜ ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์•ผํ•œ๋‹ค.

scope๋ฅผ ์ œ์™ธํ•œ ๋ชจ๋“  ๊ฐ’์„ ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ์ €์žฅํ•˜์—ฌ ์‚ฌ์šฉ ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

  • google client ID
  • google client secret
  • google API key
  • google calendar ID (์บ˜๋ฆฐ๋” ๊ณต๊ฐœ ์„ค์ •๊ณผ id๊ฐ€ ํ•„์š”ํ•˜๋‹ค)
  • scope

google cloud project ์„ค์ •์„ ๋งˆ์นœ ํ›„์—๋Š” ํ•„์š”ํ•œ api ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ scope๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค.

 

scope ์•ˆ๋‚ด ์˜ˆ์‹œ

๋‚˜๋Š” calendar event list์— ๋Œ€ํ•˜์—ฌ get, insert๋ฅผ ํ•  ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์˜ scope๋ฅผ ๊ตฌ์„ฑํ•˜์˜€๋‹ค.

  '<https://www.googleapis.com/auth/calendar.readonly>',
  '<https://www.googleapis.com/auth/calendar>',
  '<https://www.googleapis.com/auth/calendar.events>',

 

 

๐Ÿธ ๊ตฌ๊ธ€ ์†Œ์…œ ๋กœ๊ทธ์ธ ์š”์ฒญ

OpenID Connect  |  Authentication  |  Google Developers

const scopes = [
  '<https://www.googleapis.com/auth/calendar.readonly>',
  '<https://www.googleapis.com/auth/calendar>',
  '<https://www.googleapis.com/auth/calendar.events>',
];
const oAuthURL = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${
  process.env.REACT_APP_GOOGLE_CLIENT_ID
}&response_type=code&redirect_uri=http://localhost:3000/oauth2callback&
access_type=offline&prompt=consent&scope=${scopes.join(' ')}`;
const googleOauthHandler = () => {
  window.location.assign(oAuthURL);
};

export default googleOauthHandler;
  • client_id
  • response_type: ๊ตฌ๊ธ€์ธก ์ธ์ฆ์— ์„ฑ๊ณตํ•  ์‹œ, access token๊ณผ ๊ตํ™˜ํ•˜๊ธฐ ์œ„ํ•ด ๋ฐ›์„ ์‘๋‹ต ํƒ€์ž…์œผ๋กœ, code๋กœ ์„ค์ •
  • redirect_uri: ์„ฑ๊ณต ์‹œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์ฃผ์†Œ. ์ด ์ฃผ์†Œ์˜ params๋กœ code ๊ฐ’์ด ์˜จ๋‹ค. google cloud์—์„œ ๋ฏธ๋ฆฌ ์„ค์ •ํ•œ ์ฃผ์†Œ์™€ ์ผ์น˜ํ•ด์•ผํ•œ๋‹ค.
  • (์„ ํƒ) access_type: refresh ํ† ํฐ์„ ๋ฐ›๊ธฐ ์œ„ํ•ด์„œ๋Š” offline์œผ๋กœ ์„ค์ •ํ•œ๋‹ค
    ์ตœ์ดˆ ๋กœ๊ทธ์ธ ์‹œ ์ด ๊ฐ’์ด offline์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์œผ๋ฉด ๊ตฌ๊ธ€์€ refresh ํ† ํฐ์„ ์‘๋‹ตํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋งŒ์•ฝ ์ด๋ฏธ ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ•˜๊ณ , ์ดํ›„์— refresh ํ† ํฐ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์ฟ ํ‚ค๋ฅผ ์ง€์›Œ์„œ ๋‹ค์‹œ ๋กœ๊ทธ์ธ ์š”์ฒญํ•œ๋‹ค.
    ํ—ˆ์šฉ๋˜๋Š” ๊ฐ’์€ offline ๋ฐ online. ํšจ๊ณผ๋Š” ์˜คํ”„๋ผ์ธ ์•ก์„ธ์Šค์— ๋ฌธ์„œํ™”๋˜์–ด ์žˆ๋‹ค. ์•ก์„ธ์Šค ํ† ํฐ์ด ์š”์ฒญ๋˜๋ฉด offline ๊ฐ’์ด ์ง€์ •๋˜์ง€ ์•Š์œผ๋ฉด ํด๋ผ์ด์–ธํŠธ๋Š” ๊ฐฑ์‹  ํ† ํฐ์„ ์ˆ˜์‹ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • (์„ ํƒ) prompt: ๋กœ๊ทธ์ธ ํ•  ๋•Œ๋งˆ๋‹ค refresh ํ† ํฐ์„ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•ด consent๋กœ ์„ค์ •ํ•œ๋‹ค.
    prompt consent๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ Google ๊ณ„์ •์— ์•ก์„ธ์Šคํ•˜๋ ค๋Š” ์„œ๋“œํŒŒํ‹ฐ ์•ฑ ๋˜๋Š” ์›น์‚ฌ์ดํŠธ์—์„œ ์š”์ฒญํ•˜๋Š” ๊ถŒํ•œ์— ๋Œ€ํ•œ ๋™์˜๋ฅผ ๋ฌป๋Š” ์ฐฝ์„ ๋งํ•œ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ด ์ฐฝ์—์„œ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•˜๋ฉด ํ•ด๋‹น ์•ฑ์ด๋‚˜ ์›น์‚ฌ์ดํŠธ๋Š” ์‚ฌ์šฉ์ž์˜ Google ๊ณ„์ •์— ์•ก์„ธ์Šคํ•˜์—ฌ ํ•„์š”ํ•œ ์ •
    ๋ณด๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค. ๊ด€๋ จ ๋ฌธ์„œ
prompt (์„ ํƒ์‚ฌํ•ญ)
์Šน์ธ ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์žฌ์ธ์ฆ ๋ฐ ๋™์˜๋ฅผ ์š”์ฒญํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ์ง€์ •ํ•˜๋Š” ๋ฌธ์ž์—ด ๊ฐ’์˜ ๋ชฉ๋ก์ด๋ฉฐ ๊ณต๋ฐฑ์œผ๋กœ ๊ตฌ๋ถ„๋ฉ๋‹ˆ๋‹ค.
๊ฐ€๋Šฅํ•œ ๊ฐ’์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

โ€ข none
์Šน์ธ ์„œ๋ฒ„๋Š” ์ธ์ฆ ๋˜๋Š” ์‚ฌ์šฉ์ž ๋™์˜ ํ™”๋ฉด์„ ํ‘œ์‹œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์•„์ง ์ธ์ฆ๋˜์ง€ ์•Š์•˜๊ณ  ์š”์ฒญ๋œ ๋ฒ”์œ„์— ๋Œ€ํ•ด ์‚ฌ์ „ ๊ตฌ์„ฑ๋œ ๋™์˜๊ฐ€ ์—†์œผ๋ฉด ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. none์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ธฐ์กด ์ธ์ฆ ๋˜๋Š” ๋™์˜๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โ€ขconsent
์Šน์ธ ์„œ๋ฒ„๋Š” ํด๋ผ์ด์–ธํŠธ์— ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์ „์— ์‚ฌ์šฉ์ž์—๊ฒŒ ๋™์˜๋ฅผ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.

โ€ขselect_account
์Šน์ธ ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์‚ฌ์šฉ์ž ๊ณ„์ •์„ ์„ ํƒํ•˜๋ผ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์Šน์ธ ์„œ๋ฒ„์— ์—ฌ๋Ÿฌ ๊ณ„์ •์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ํ˜„์žฌ ์„ธ์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ ๊ณ„์ • ์ค‘์—์„œ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ’์ด ์ง€์ •๋˜์ง€ ์•Š๊ณ  ์‚ฌ์šฉ์ž๊ฐ€ ์ด์ „์— ์•ก์„ธ์Šค๋ฅผ ์Šน์ธํ•œ ์ ์ด ์—†์œผ๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ ๋™์˜ ํ™”๋ฉด์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

'prompt'๋ฅผ 'consent'๋กœ ์„ค์ •ํ•˜๊ฒŒ ๋˜๋ฉด ๋งค ๋กœ๊ทธ์ธ ๋งˆ๋‹ค ์‚ฌ์šฉ์ž์—๊ฒŒ ๋™์˜๋ฅผ ์š”์ฒญํ•˜๊ธฐ ๋•Œ๋ฌธ์—

๊ฐ•์ œ๋กœ Refresh Token์„ ๋ฐ›๋„๋ก ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

์•„๋ž˜๋Š” ๋„์›€์„ ์–ป์€ ๋ธ”๋กœ๊ทธ.

Google์€ Refresh Token์„ ์‰ฝ๊ฒŒ ๋‚ด์ฃผ์ง€ ์•Š๋Š”๋‹ค

 

๐Ÿ˜ˆ url ์ฃผ์†Œ๋Š” ๋‹น์—ฐํžˆ ์ค„๋ฐ”๊ฟˆํ•˜๋ฉด ์•ˆ๋œ๋‹ค. ๋‚˜๋Š” ์ž๋™์œผ๋กœ ๊ณ„์† ์ค„๋ฐ”๊ฟˆ์ด ๋˜์–ด์„œ ์˜ค๋ฅ˜๊ฐ€ ๋‚ฌ์—ˆ๋‹ค.

 

 

๐Ÿธ  ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ํŽ˜์ด์ง€

import React, { useEffect } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { isLoginAtom, isLoadingAtom } from '../state/atoms';

function Oauth2callback() {
  const navigate = useNavigate();
  const setIsLogin = useSetRecoilState(isLoginAtom);
  const setIsLoading = useSetRecoilState(isLoadingAtom);
  useEffect(() => {
    setIsLoading(true);
    const fetchData = () => {
      const url = new URL(window.location.href);
      const { searchParams } = url;
      const code = searchParams.get('code');

      if (code) {
// code๋กœ access ํ† ํฐ ์š”์ฒญ
      }
    };
    fetchData();
  }, []);

  return <div>๊ตฌ๊ธ€ ์—ฐ๋™์ค‘ ... </div>;
}

export default Oauth2callback;
  • ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ(์‚ฌ์šฉ์ž ๋™์˜ ๋“ฑ)์ด ์ •์ƒ์ ์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋ฉด ๊ตฌ๊ธ€์€ ๋ฏธ๋ฆฌ ์•ฝ์†๋œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์ฃผ์†Œ์˜ params์— code๋ฅผ ๋„ฃ์–ด ์‘๋‹ต
  • ๊ทธ๋Ÿผ, ํ•ด๋‹น ํŽ˜์ด์ง€์—์„œ๋Š” ์–ป์€ code๋ฅผ access ํ† ํฐ๊ณผ ๊ตํ™˜ํ•˜์—ฌ ์ €์žฅํ•˜๊ณ 
  • ์›ํ•˜๋Š” ํŽ˜์ด์ง€๋กœ navigate ํ•œ๋‹ค
  • // code๋กœ access ํ† ํฐ ์š”์ฒญ  ์ฃผ์„ ์ฒ˜๋ฆฌํ•œ ๋ถ€๋ถ„์€ ์•„๋ž˜์—์„œ ์ž์„ธํžˆ ๋‹ค๋ฃฌ๋‹ค.

 

๐Ÿธ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ํŽ˜์ด์ง€ access ํ† ํฐ ์š”์ฒญ ์ฝ”๋“œ

axios
          .post(
            '<https://oauth2.googleapis.com/token>',
            {
              code,
              client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
              client_secret: process.env.REACT_APP_GOOGLE_SECRETE,
              redirect_uri: '',
              grant_type: 'authorization_code',
            },
            {
              headers: {
                'Content-Type': 'application/json',
              },
            },
          )
          .then((response) => {
            console.log(response);
            const accessToken = response.data.access_token;
            localStorage.setItem('access_token', accessToken);

            const refreshToken = response.data.refresh_token;
            localStorage.setItem('refresh_token', refreshToken);

            setIsLogin(true);
            navigate('/');
          })
          .then(() => {
            setIsLoading(false);
          })
          .catch((error) => console.log(error));
      }

โœ”๏ธ params ์— ํฌํ•จ๋œ code (๊ตฌ๊ธ€ ์ธก์—์„œ ๊ฐ–๊ณ  ์žˆ๋Š” code์™€ ์ด ๊ฐ’์ด ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค)

โœ”๏ธ client secret: ์ตœ์ดˆ ์š”์ฒญ ์‹œ์—๋„ ํฌํ•จ๋˜์–ด ์žˆ๋˜ ๊ฐ’๊ณผ ๊ฐ™๋‹ค

โœ”๏ธ client_secret: google cloud ๊ตฌ์„ฑํ•  ๋•Œ ์ €์žฅํ•œ ๊ฐ’

post ์š”์ฒญ์˜ ์‘๋‹ต์—๋Š” access token๊ณผ refresh token ๊ฐ’์ด ํฌํ•จ๋˜์–ด ์žˆ๋‹ค. localstorage์— ์ €์žฅํ•œ๋‹ค.

 

 

 


 

 

โ˜” Development Detail ๋” ๋ณด๊ธฐ

๐ŸŽ access token, refresh token

access token์€ ์‹ค์‚ฌ์šฉํ•˜๋Š” ํ† ํฐ, refresh token์€ ๊ฐฑ์‹ ์„ ์œ„ํ•œ ๋น„์ƒ์šฉ ํ† ํฐ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์‰ฝ๋‹ค.

โœ”๏ธ google ์•ก์„ธ์Šค ํ† ํฐ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ 1์‹œ๊ฐ„(3,600์ดˆ) ๋™์•ˆ ์œ ํšจํ•˜๋‹ค. ์œ ํšจ ์‹œ๊ฐ„์„ ๋”ฐ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ๊ธธ๊ฒŒ ์žก์„ ๊ฒฝ์šฐ, ์ œ์•ฝ ์กฐ๊ฑด์„ ์ถฉ์กฑ์‹œ์ผœ์•ผ ํ•œ๋‹ค. ์œ„์˜ ๋ฌธ์„œ์—์„œ ์ž์„ธํžˆ ๋ณผ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฐธ๊ณ ํ•œ๋‹ค.

โœ”๏ธ access token์ด ๋งŒ๋ฃŒ๋˜๋ฉด refresh ํ† ํฐ์„ ๋ณด๋‚ด, ์ƒˆ๋กœ์šด access token ์œผ๋กœ ๊ฐฑ์‹ ํ•œ๋‹ค. ๊ฐ„๋‹จํ•˜๊ฒŒ ์•„๋ž˜์™€ ๊ฐ™์€ ํ๋ฆ„์ด๋‹ค.

 

ํ† ํฐ ์œ ํ˜•  |  ์ธ์ฆ  |  Google Cloud

 

๐Ÿ’ก access token, refresh token flow

  1. access ํ† ํฐ ๋งŒ๋ฃŒ โ†’ ์˜ค๋ฅ˜(401)
  2. refresh ํ† ํฐ์œผ๋กœ access ํ† ํฐ ๊ฐฑ์‹  ์š”์ฒญ(POST)
  3. ์ƒˆ๋กœ์šด access ํ† ํฐ ์‘๋‹ต
  4. ์ƒˆ๋กœ์šด access ํ† ํฐ ์ €์žฅ
  5. access ํ† ํฐ์„ ํ—ค๋”์— ํฌํ•จ์‹œ์ผœ ์š”์ฒญ

๐Ÿ’ก refresh token์€ ์˜๋ฌด๊ฐ€ ์•„๋‹ˆ๋‹ค

access token ์ด ์ธ์ฆ์˜ ๊ธฐ๋ณธ์ด์ง€, refresh ํ† ํฐ์€ ์•„๋‹ˆ๋‹ค. access ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ํ•˜๋ฉด ๋œ๋‹ค.

ํ•˜์ง€๋งŒ ํ•œ ์‹œ๊ฐ„ ๋งˆ๋‹ค ๋กœ๊ทธ์•„์›ƒ-๋กœ๊ทธ์ธ์„ ํ•˜๋Š” ๊ฒƒ์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ์ข‹์ง€ ์•Š๋‹ค.

์„œ๋น„์Šค๋ฅผ ๋ณด๋‹ค ๋งค๋„๋Ÿฝ๊ฒŒ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด refresh ํ† ํฐ๋„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹๋‹ค.

 

โ— ๋ชจ๋“  google api ์š”์ฒญ์ด access token์„ ํ•„์š”๋กœ ํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค

์‚ฌ์šฉํ•˜๊ณ ์ž ํ•˜๋Š” ๊ตฌ๊ธ€ api๊ฐ€ โ€˜์ธ์ฆโ€™์„ ํ•„์š”ํ•œ๋‹ค๋ฉด, ์œ„์˜ ์š”์ฒญ์„ ํ†ตํ•ด ๋ฐ›์€ access token์„ ํ—ค๋”์— ํฌํ•จ์‹œ์ผœ ์š”์ฒญํ•ด์•ผํ•œ๋‹ค.

๋‚˜๋Š” calendar api๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, get์€ โ€˜์ธ์ฆโ€™์ด ํ•„์š”ํ•˜์ง€ ์•Š์ง€๋งŒ list insert(post) ์š”์ฒญ์€ ์ธ์ฆ์ด ํ•„์š”ํ•˜๋‹ค.

๊ทธ๋ž˜์„œ list๋ฅผ ์ƒˆ๋กœ post ํ•  ๋•Œ๋งŒ ์ธ์ฆ ํ† ํฐ ๊ฐ’์„ ๊ฐ™์ด ๋„˜๊ฒผ๋‹ค. ํ•„์š”ํ•œ api ๋ฌธ์„œ๋ฅผ ๊ผผ๊ผผํžˆ ํ™•์ธํ•˜์ž.

 

 

 

๐Ÿธ axios interceptor์™€ access token ๊ฐฑ์‹  ์š”์ฒญ

access token์ด ์–ธ์ œ ๋งŒ๋ฃŒ๋ ์ง€, ์šฐ๋ฆฌ๋Š” ๋ชจ๋ฅธ๋‹ค.

ํ† ํฐ์ด ํ•„์š”ํ•œ api์— ์š”์ฒญ์„ ํ–ˆ๋Š”๋ฐ, ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๊ณ  ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  ๋‚˜์„œ์•ผ ํ† ํฐ์ด ๋งŒ๋ฃŒ๋œ์ง€ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿผ, ์ด๋ฏธ ์š”์ฒญํ•œ request๋Š” ์–ด๋–ป๊ฒŒ ํ• ๊นŒ?

์ด ๋•Œ axios interceptor๊ฐ€ ๋งŽ์ด ์“ฐ์ธ๋‹ค.

axios interceptor๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด then ๋˜๋Š” catch ์ฒ˜๋ฆฌ๋˜๊ธฐ ์ „์— ์š”์ฒญ๊ณผ ์‘๋‹ต์„ ๊ฐ€๋กœ์ฑ„,

์›ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ˆ˜ํ–‰ ํ›„ then ๋˜๋Š” catch ๋กœ ๋ฑ‰์–ด๋‚ธ๋‹ค.

๊ทธ๋Ÿผ, ์•„๋ž˜์™€ ๊ฐ™์€ ๋กœ์ง์œผ๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ๋œ๋‹ค.

axios interceptor์™€ ํ•จ๊ป˜ axios instance๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

 

โœ”๏ธ axios interceptor ์—๋Ÿฌ โ†’ ์žฌ์š”์ฒญ flow

  1. response์— 401 access token ๋งŒ๋ฃŒ ์—๋Ÿฌ๊ฐ€ ์˜จ๋‹ค
  2. refresh token์„ ์‹ค์–ด 'https://oauth2.googleapis.com/token?'โ€™ ์— POST ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค
  3. ์ƒˆ๋กœ์šด access token์ด ์˜จ๋‹ค
  4. ์ €์žฅํ•œ๋‹ค
  5. ์ƒˆ๋กœ์šด access token์„ ์‚ฌ์šฉํ•˜์—ฌ original request๋ฅผ ์žฌ์š”์ฒญํ•œ๋‹ค </aside>

 

ํ† ํฐ ๊ฐฑ์‹  ํ›„ ์žฌ์š”์ฒญ ์ฝ”๋“œ

PostEventInstance.interceptors.response.use(
  (response: AxiosResponse) => {
    return response;
  },
  async (error: AxiosError) => {
    const originalRequest = error.config as InternalAxiosRequestConfig;

    if (error.response?.status === 401 && !originalRequest.retry) {
      originalRequest.retry = true;
      const refreshToken = localStorage.getItem('refresh_token');
      return axios
        .post(
          '<https://oauth2.googleapis.com/token?'>,
          {
            grant_type: 'refresh_token',
            client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
            refresh_token: refreshToken,
            client_secret: process.env.REACT_APP_GOOGLE_SECRETE,
          },
          {
            headers: {
              'Content-Type': 'application/json',
            },
          },
        )
        .then((response) => {
          localStorage.setItem('access_token', response.data.access_token);
          PostEventInstance.defaults.headers.Authorization = `Bearer ${response.data.access_token}`;
          console.log('ํ† ํฐ ๊ฐฑ์‹ ');

          originalRequest.headers.Authorization = `Bearer ${response.data.access_token}`;
          console.log('์ƒˆํ† ํฐ ์žฅ์ฐฉ');

          return PostEventInstance(originalRequest);
        })
        .catch((error) => {
          console.log(error);
        });
    }
    return Promise.resolve();
  },
);

๊ฐฑ์‹  ์š”์ฒญ ์‹œ client_id, refresh_token, client_secret ์„ ํฌํ•จ์‹œ์ผœ์•ผํ•œ๋‹ค.

 

 

๐Ÿ˜ˆ ์—๋Ÿฌ ํŒŒํ‹ฐ: refresh ํ† ํฐ ๋„์ž…๊ธฐ

refresh ํ† ํฐ์„ ๋„์ž…ํ•˜๊ณ  ๋งŽ์€ ์˜ค๋ฅ˜๋ฅผ ๊ฒช์—ˆ๋‹ค. ํฌ๊ฒŒ ์•„๋ž˜ ์„ธ ๊ฐ€์ง€๋ฅผ ์•Œ์•„๋‘๋ฉด ์ข‹๋‹ค.

 

โ— ๊ตฌ๊ธ€์€ ์ตœ์ดˆ ๋กœ๊ทธ์ธ ์‹œ์— refresh token ์š”์ฒญ์„ ํ•œ ๊ฒฝ์šฐ์—๋งŒ refresh token์„ ์ œ๊ณตํ•œ๋‹ค

์ตœ์ดˆ ๋กœ๊ทธ์ธ ์‹œ refresh ํ† ํฐ์„ ์š”์ฒญํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Oauth ์š”์ฒญ ์‹œ **access_type**์„ offline์œผ๋กœ ์„ค์ •ํ•œ๋‹ค.

์ด ๊ฐ’์ด offline์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์œผ๋ฉด ๊ตฌ๊ธ€์€ refresh ํ† ํฐ์„ ์‘๋‹ตํ•˜์ง€ ์•Š๋Š”๋‹ค.

โœ”๏ธ ๋งŒ์•ฝ ์ด๋ฏธ ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ•˜๊ณ , ์ดํ›„์— refresh ํ† ํฐ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์ฟ ํ‚ค๋ฅผ ์ง€์›Œ์„œ ๋‹ค์‹œ ๋กœ๊ทธ์ธ ์š”์ฒญํ•œ๋‹ค

 

โ— refresh token์€ ์ตœ์ดˆ ๋กœ๊ทธ์ธ ์‹œ์—๋งŒ ์ œ๊ณตํ•œ๋‹ค

๋กœ๊ทธ์•„์›ƒ ํ›„ ๋‹ค์Œ ๋กœ๊ทธ์ธ์„ ํ•  ๋•Œ๋Š” refresh token์€ ์‘๋‹ต๋˜์ง€ ์•Š๋Š”๋‹ค.

๋งŒ์•ฝ ๋กœ๊ทธ์•„์›ƒ ์‹œ token ๊ฐ’์„ ๋ชจ๋‘ ์‚ญ์ œํ•œ๋‹ค๋ฉด, ๋‹ค์Œ ๋กœ๊ทธ์ธ ๋•Œ access token๋งŒ ์˜จ๋‹ค.

๊ทธ๋Ÿผ ํ•ด๋‹น access token ๋งŒ๋ฃŒ ์‹œ ํ† ํฐ์„ ๊ฐฑ์‹ ํ•  ์ˆ˜ ์—†๋‹ค. ์ด ์˜ค๋ฅ˜๋ฅผ ๊ฒช์œผ๋ฉฐ ์ ˆ๋ง์Šค๋Ÿฌ์› ๋‹ค.

 

โ— ๋งค๋ฒˆ ๋กœ๊ทธ์ธํ•  ๋•Œ๋งˆ๋‹ค refresh token์„ ๋ฐ›๊ณ ์‹ถ๋‹ค๋ฉด

๊ตฌ๊ธ€ Oauth ์š”์ฒญ ์‹œ, prompt๋ฅผ consent๋กœ ์„ค์ •ํ•œ๋‹ค.

์ด๋ ‡๊ฒŒ ์„ค์ •ํ•˜๋ฉด ๋กœ๊ทธ์ธ ํ•  ๋•Œ๋งˆ๋‹ค ์‚ฌ์šฉ์ž์—๊ฒŒ ์ตœ์ดˆ ๋กœ๊ทธ์ธํ•  ๋•Œ์ฒ˜๋Ÿผ ๊ตฌ๊ธ€ ๊ณ„์ •์„ ์„ ํƒํ•˜๊ณ  ์ธ์ฆ scope์— ๋Œ€ํ•œ ๋™์˜๋ฅผ ์š”์ฒญํ•œ๋‹ค.

์ด ๋•Œ refresh token๋„ ์ƒˆ๋กœ ๋ฐœ๊ธ‰ํ•ด์ค€๋‹ค.

prompt 
prompt consent๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ Google ๊ณ„์ •์— ์•ก์„ธ์Šคํ•˜๋ ค๋Š” ์„œ๋“œํŒŒํ‹ฐ ์•ฑ ๋˜๋Š” ์›น์‚ฌ์ดํŠธ์—์„œ ์š”์ฒญํ•˜๋Š” ๊ถŒํ•œ์— ๋Œ€ํ•œ ๋™์˜๋ฅผ ๋ฌป๋Š” ์ฐฝ์„ ๋งํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ด ์ฐฝ์—์„œ ๊ถŒํ•œ์„ ํ—ˆ์šฉํ•˜๋ฉด ํ•ด๋‹น ์•ฑ์ด๋‚˜ ์›น์‚ฌ์ดํŠธ๋Š” ์‚ฌ์šฉ์ž์˜ Google ๊ณ„์ •์— ์•ก์„ธ์Šคํ•˜์—ฌ ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

prompt ๋ฅผ consent ๋กœ ์„ค์ •ํ•˜๊ฒŒ ๋˜๋ฉด ๋งค ๋กœ๊ทธ์ธ ๋งˆ๋‹ค ์‚ฌ์šฉ์ž์—๊ฒŒ ๋™์˜๋ฅผ ์š”์ฒญํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ•์ œ๋กœ Refresh Token ์„ ๋ฐ›๋„๋ก ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด๋Ÿฌํ•œ ๊ณผ์ •์„ ๊ฑฐ์ณ ๋‚˜์˜ ๊ตฌ๊ธ€ ์ธ์ฆ ์š”์ฒญ ์ฝ”๋“œ๊ฐ€ ๋งŒ๋“ค์–ด์กŒ๋‹ค.

const scopes = [
  '<https://www.googleapis.com/auth/calendar.readonly>',
  '<https://www.googleapis.com/auth/calendar>',
  '<https://www.googleapis.com/auth/calendar.events>',
];
const oAuthURL = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${
  process.env.REACT_APP_GOOGLE_CLIENT_ID
}&response_type=code&redirect_uri=http://localhost:3000/oauth2callback&
access_type=offline&prompt=consent&scope=${scopes.join(' ')}`;
const googleOauthHandler = () => {
  window.location.assign(oAuthURL);
};

export default googleOauthHandler;

 

์ฐธ๊ณ ๋กœ refresh token ์—ญ์‹œ ๋งŒ๋ฃŒ ๋  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ“– refresh token ๋„์ž… ํ›„ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜ ํ•ด๊ฒฐ ๊ณผ์ •์—์„œ ์ด ๋ฌธ์„œ์—์„œ ๋„์›€์„ ์–ป์—ˆ๋‹ค.

 

 

 


 

โ˜” ์ฐธ๊ณ ํ•œ ๋ฌธ์„œ

Google Cloud

Google Calendar api ๋ฌธ์„œ

OpenID Connect  |  Authentication  |  Google Developers

https://developers.google.com/identity/openid-connect/openid-connect?hl=ko#re-consent

Google์€ Refresh Token์„ ์‰ฝ๊ฒŒ ๋‚ด์ฃผ์ง€ ์•Š๋Š”๋‹ค

ํ† ํฐ ์œ ํ˜•  |  ์ธ์ฆ  |  Google Cloud

refresh token ๋งŒ๋ฃŒ์— ๋Œ€ํ•œ ๋ฌธ์„œ