+) authentication concepts | 인증에 대한 기본 개념
firebase authentication | youtube | react firebase authentication tutorial
1. connecting firebase to our projects
$ npm install firebase
$ yarn add firebase
- 프로젝트 폴더에서 firebase 패키지를 설치한다.
- firebase console > 새로운 프로젝트 만들기
- 해당 프로젝트 설정(setting) 페이지 > 그림의 버튼을 클릭하면, console의 프로젝트와 실제 프로젝트 폴더(1번에서 패키지를 설치한 폴더)를 연결할 수 있도록 가이드를 제공한다.
- 가이드에서 아래와 같은 형식의 코드를 제공한다.
// 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
- google firebase console에서 연결 한 프로젝트 > authentication > sign-in method 탭에 들어간다.
- 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);
// ...
});
}
- 로그인에 성공했을 때 전달되는 객체 데이터의 _tokenResponse를 이용할 수 있다.
- => 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);
}
};
- 여기에서 사용되는 API KEY는 사용하고 있는 firebase 프로젝트를 식별하는 key이다.
- firebase 콘솔의 설정 > project setting 메뉴에서 확인할 수 있다.
- api key와 같은 정보는, 깃헙에 올리는 files에 포함하기 보다는 env 파일에서 따로 관리하는 것이 더 낫다.
- body 부분에는 email, password, returnSecureToken(t/f)을 payload로 작성하게 되어있다.
- 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]';
- 패스워드를 변경하는 endpoint는 대표적인 보호 리소스(protected resouce) 중 하나이다.
- 따라서 sign up/sign in과는 달리, request body에 idToken을 포함해야 한다.
- 이메일 주소가 아니라 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>}
- 로그인 요청이 성공하면, response로 idToken이라는 속성의 데이터를 전달받는데, 이 값을 이용해서 protected contents에 접근할 수 있다.
- 이 token은 특정 기간(기본값 1시간 = 3600s) 이 지나면 만료된다(expired).
+) 8. localstorage
- 어플리케이션을 새로고침 하더라도 login state를 유지하는 것이 현실적이다.
- 로그인 상태를 유지하려면, 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;
- 사용자가 로그인 했을 때
- expirationTime | 현재 시각 + 1시간, expirationTime을 localStorage에 저장한다.
- remaingTime | expirationTime - 현재 시각, setTimeout(logout, remainingTime)
- 현재 시각과 expirationTime을 getTime 메서드를 이용해 숫자(ms)로 변환한다.
- 로그인 한 사용자가 페이지를 새로고침 했을 때
- localStorage에 저장되어 있는 token과 expiresIn을 받아와야 한다.
- 사용자가 직접 로그아웃 했을 때
- 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 |