본문 바로가기

Study/React

react-firebase | authentication

+) authentication concepts | 인증에 대한 기본 개념

firebase authentication | youtube | react firebase authentication tutorial

1. connecting firebase to our projects

$ npm install firebase 
$ yarn add firebase

 

[그림] firebase project setting

 

  1. 프로젝트 폴더에서 firebase 패키지를 설치한다.
  2. firebase console > 새로운 프로젝트 만들기
  3. 해당 프로젝트 설정(setting) 페이지 > 그림의 버튼을 클릭하면, console의 프로젝트와 실제 프로젝트 폴더(1번에서 패키지를 설치한 폴더)를 연결할 수 있도록 가이드를 제공한다. 
  4. 가이드에서 아래와 같은 형식의 코드를 제공한다. 

 

// services/firebase.js
import { initializeApp } from "firebase/app";

// Your web app's Firebase configuration
const firebaseConfig = {
  // 아래의 값들은 가이드에서 자동으로 제공해 주는 값을 복사해서 사용하면 됨
  apiKey,
  authDomain, 
  projectId, 
  storageBucket, 
  messagingSenderId,
  appId, 
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

2. using authentication in our project

project > authentication > sign-in method

 

  1. google firebase console에서 연결 한 프로젝트 > authentication > sign-in method 탭에 들어간다. 
  2. google, facebook, github, twitter... 등 다양한 authentication provider가 존재하는데 그 중 원하는 provider를 선택 > enable > save 한다. 

 

// services/firebase.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";

const firebaseConfig = {
  // ...
};

const app = initializeApp(firebaseConfig);

// Initialize Firebase Authentication and get a reference to the service
export const auth = getAuth(app);

3. google authentication 

- signInWithPopup

// services/firebase.js
import { initializeApp } from "firebase/app";
import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/auth";

// ...

export const auth = getAuth(app);
const provider = new GoogleAuthProvider();

export const signInWithGoogle = () => {
  signInWithPopup(auth, provider)
    .then((result) => {
      // This gives you a Google Access Token. You can use it to access the Google API.
      const credential = GoogleAuthProvider.credentialFromResult(result);
      const token = credential.accessToken;
      // The signed-in user info.
      const user = result.user;
      // ...
    }).catch((error) => {
      const errorCode = error.code;
      const errorMessage = error.message;
      // The email of the user's account used.
      const email = error.customData.email;
      // The AuthCredential type that was used.
      const credential = GoogleAuthProvider.credentialFromError(error);
      // ...
    });
}

 

  1. 로그인에 성공했을 때 전달되는 객체 데이터의 _tokenResponse를 이용할 수 있다. 
  2. => ex) idToken, expiresIn, localId...  

- signOut

import { getAuth, signOut } from "firebase/auth";

const auth = getAuth();
signOut(auth).then(() => {
  // Sign-out successful.
}).catch((error) => {
  // An error happened.
});

4. email authentication

- sign up new account with emails   

const SIGN_UP = 'https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API_KEY]'

// sendSignInReq
const sendSignupReq = async () => {
  setIsLoading(true);
  
  try {
    const res = await fetch(SIGN_UP, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: emailInput.current.value,
        password: passwordInput.current.value,
        returnSecureToken: true,
        }),
    });
    
    setIsLoading(false);
    
    const data = await res.json();
    console.log(data);
    emailInput.current.value = '';
    passwordInput.current.value = '';
    
    } catch (error) {
      alert(error);
      setIsLoading(false);
    }
};

 

  1. 여기에서 사용되는 API KEY는 사용하고 있는 firebase 프로젝트를 식별하는 key이다.
    • firebase 콘솔의 설정 > project setting 메뉴에서 확인할 수 있다.
    • api key와 같은 정보는, 깃헙에 올리는 files에 포함하기 보다는 env 파일에서 따로 관리하는 것이 더 낫다.  
  2. body 부분에는 email, password, returnSecureToken(t/f)을 payload로 작성하게 되어있다. 
  3. firebase가 제공하는 auth는 최소 6자리의 비밀번호를 요구한다. 

- sign in 

const SIGN_IN = 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=[API_KEY]'

 

- change password 

const CHAHGE_PW = 'https://identitytoolkit.googleapis.com/v1/accounts:update?key=[API_KEY]';

 

  1. 패스워드를 변경하는 endpoint는 대표적인 보호 리소스(protected resouce) 중 하나이다.
  2. 따라서 sign up/sign in과는 달리, request body에 idToken을 포함해야 한다. 
  3. 이메일 주소가 아니라 token을 사용하는 이유는, 그 주소를 아는 다른 사용자에 의해 비밀번호가 변경되는 상황을 막기 위해서이다. 

5. github authentication 

6. google, github login example

// services/firebase/auth.js
import {
  GoogleAuthProvider,
  GithubAuthProvider,
  signInWithPopup,
} from 'firebase/auth'
import { getAuth } from 'firebase/auth'

const auth = getAuth();

export class AuthService {
  login(provider) {
    let authProvider
    switch (provider) {
      case 'Google':
        authProvider = new GoogleAuthProvider()
        break
      case 'Github':
        authProvider = new GithubAuthProvider()
        break
      default:
        break
    }
    try {
      signInWithPopup(auth, authProvider)
        .then((result) => console.log(result))
    } catch (error) {
      console.log(error)
    }
  }
}

// index.js
// ...
const authService = new AuthService()

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <BrowserRouter>
    <App authService={authService} />
  </BrowserRouter>,
)

// AuthForm.js
const AuthForm = ({ authService }) => {

  return (
    <section className={classes.auth}>
      <button onClick={() => authService.login('Google')}>Google</button>
      <button onClick={() => authService.login('Github')}>Github</button>
    </section>
  )
}

+) 7. Authentication context

// context/AuthContext.js
const AuthContext = React.createContext({
  token: '',
  isLoggedIn: false,
  loginHandler: (toekn) => {},
  logoutHandler: () => {},
});

export const AuthContextProvider = ({ children }) => {
  const [token, setToken] = useState(null);
  const isLoggedIn = !!token;

  const loginHandler = (token) => {
    setToken(token);
  };
  const logoutHandler = () => {
    setToken(null);
  };
  const contextValue = {
    token,
    isLoggedIn,
    loginHandler,
    logoutHandler,
  };
  
  return (
    <AuthContext.Provider value={contextValue}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

// LoginForm.js
requestSignIn()
  .then(data => {
    authCtx.loginHandler(data.idToken);
    history.replace('/'); // 이전 페이지로 돌아갈 수 없도록 replace를 사용
  })
  
// NavBar.js
{authCtx.isLoggedin && <p>login!</p>}

 

  1. 로그인 요청이 성공하면, response로 idToken이라는 속성의 데이터를 전달받는데, 이 값을 이용해서 protected contents에 접근할 수 있다.
  2. 이 token은 특정 기간(기본값 1시간 = 3600s) 이 지나면 만료된다(expired).

 

sign up/ sign in response, expiresin은 토큰 만료 기간을 의미한다

+) 8. localstorage 

  1. 어플리케이션을 새로고침 하더라도 login state를 유지하는 것이 현실적이다.
  2. 로그인 상태를 유지하려면, js 파일 외부에 상태를 저장해야 한다. => 브라우저의 localStorage, cookies를 이용한다. 

 

+) ??? XSS에 취약한 경우 localStorage를 이용하는 것은 문제가 많을 수 있다. 

 

const initialToken = localStorage.getItem('token');
const [token, setToken] = useState(initialToken);

const isLoggedIn = !!token;

const loginHandler = (token) => {
  setToken(token);
  localStorage.setItem('token', token); 
};
const logoutHandler = () => {
  setToken(null);
  localStorage.removeItem('token');
};

+) 9. auto logout 

token 만료 기간(1시간)이 지난 후에는 자동으로 로그아웃 되어야 한다. 

 

// loginService
export class AuthService {
  login(provider) {
  // ...
    try {
      signInWithPopup(auth, authProvider) //
        .then((result) => {
          const now = new Date()
          const expiresIn = +result._tokenResponse.expiresIn * 1000; // convert seconds to ms
          const expirationTime = addMilliseconds(now, expiresIn)
          localStorage.setItem('expiresIn', expirationTime)
        })
    } catch (error) {
      console.log(error)
    }
  }
}

// authContext
import React, { useState, useEffect, useCallback } from 'react';

// --------global--------
let logoutTimer;

const AuthContext = React.createContext({
  token: '',
  isLoggedIn: false,
  login: (token) => {},
  logout: () => {},
});

const calculateRemainingTime = (expirationTime) => {
  const currentTime = new Date().getTime();
  const adjExpirationTime = new Date(expirationTime).getTime();

  const remainingDuration = adjExpirationTime - currentTime;

  return remainingDuration;
};

const retrieveStoredToken = () => {
  const storedToken = localStorage.getItem('token');
  const storedExpirationDate = localStorage.getItem('expirationTime');

  const remainingTime = calculateRemainingTime(storedExpirationDate);

  if (remainingTime <= 3600) {
    localStorage.removeItem('token');
    localStorage.removeItem('expirationTime');
    return null;
  }

  return {
    token: storedToken,
    duration: remainingTime,
  };
};

// --------provider--------
export const AuthContextProvider = (props) => {
  const tokenData = retrieveStoredToken();
  
  let initialToken;
  if (tokenData) {
    initialToken = tokenData.token;
  }

  const [token, setToken] = useState(initialToken);

  const userIsLoggedIn = !!token;

  const logoutHandler = useCallback(() => {
    setToken(null);
    localStorage.removeItem('token');
    localStorage.removeItem('expirationTime');

    if (logoutTimer) {
      clearTimeout(logoutTimer);
    }
  }, []);

  const loginHandler = (token, expirationTime) => {
    setToken(token);
    localStorage.setItem('token', token);
    localStorage.setItem('expirationTime', expirationTime);

    const remainingTime = calculateRemainingTime(expirationTime);

    logoutTimer = setTimeout(logoutHandler, remainingTime);
  };

  useEffect(() => {
    if (tokenData) {
      console.log(tokenData.duration);
      logoutTimer = setTimeout(logoutHandler, tokenData.duration);
    }
  }, [tokenData, logoutHandler]);

  const contextValue = {
    token: token,
    isLoggedIn: userIsLoggedIn,
    login: loginHandler,
    logout: logoutHandler,
  };

  return (
    <AuthContext.Provider value={contextValue}>
      {props.children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

 

  1. 사용자가 로그인 했을 때
    • expirationTime |  현재 시각 + 1시간,  expirationTime을 localStorage에 저장한다. 
    • remaingTime | expirationTime - 현재 시각,  setTimeout(logout, remainingTime)
      • 현재 시각과 expirationTime을 getTime 메서드를 이용해 숫자(ms)로 변환한다. 
  2.  로그인 한 사용자가 페이지를 새로고침 했을 때 
    • localStorage에 저장되어 있는 token과 expiresIn을 받아와야 한다. 
  3. 사용자가 직접 로그아웃 했을 때
    •  clearTimeout을 이용해 타이머(setTimeout(logout, remainingTime))를 삭제해 주어야 한다. 
    • 이미 로그아웃한 사용자이므로, remainigTime이 지난 후 logout 함수를 실행하는 것은 논리에 맞지 않기 때문이다. 
    •  

 

'Study > React' 카테고리의 다른 글

컴포넌트 설계에 대해 궁금한 점  (0) 2022.10.09
Deployment | firebase hosting  (0) 2022.07.04
Validation  (0) 2022.06.27
custom Hooks  (0) 2022.06.24
HTTP Request  (0) 2022.06.23