Auth0 로그인 세션 만료 구현

ding·2024년 12월 24일

Auth0 로그인 관련 token & session

Access Token

  • Maximum Access Token Lifetime
    • Time until an access token issued for this API will expire
      ** By default, an access token for a custom API valid for 862400 sec(24hr). You can shorten the time period before the custom.
    • default 값: 86400sec = 24hr
  • Implicit / Hybrid Flow Access Token Lifetime
    • Time until an access token issued for this API, using either the implicity or hybrid flow, will expire. Cannot exceed the maximum access token lifetime.
    • default 값: 7200sec = 2hr
  • 설정 방법
    • Dashboard > Applications > APIs > select API tp view > Access Token Settings
  • https://auth0.com/docs/secure/tokens/access-tokens/update-access-token-lifetime

Refresh Token

  • 사용자가 로그아웃하지 않고도 Access Token이 만료된 후 새롭게 Access Token을 요청할 수 있도록 하는 메커니즘
  • Refresh Token Rotation
    • Refresh Token을 사용하여 새로운 Access Token을 요청할 때마다 새로운 Refresh Token이 함께 발급된다. (이전 Refresh Token은 만료)
    • default 값: 2,592,000 sec = 30 days
  • 설정 방법
    • Dashboard > Applications > applications > client app > Refresh Token Expiration & Refresh Token Rotation
    • offline_access scope 포함시키기
  • https://auth0.com/learn/refresh-tokens

Auth0 React SDK에서 Refresh Token 동작 방식

  1. 자동 관리
  • useRefreshTokens: true로 설정하면 Auth0 React SDK는 Refresh Token을 발급받아 자동으로 관리한다.
  • getAccessTokenSilently를 호출할 때, Access Token이 만료되었을 경우 SDK가 자동으로 Refresh Token을 사용해 새로운 Access Token을 발급받는다.
  1. Refresh Token의 저장소
  • cacheLocation 설정에 따라 Refresh Token이 메모리나 로컬 스토리지에 저장된다.
  1. 로그아웃 처리
  • 사용자가 로그아웃하면 Auth0 SDK는 Refresh Token을 삭제해 재사용을 방지한다.

Session

  • Idle Session Lifetime
    • Time until users are required to log in again due to inactivity
      **inactivity의 기준: interaction of Authorization Server의 유무
    • default 값: 43420min = 3days
  • Maximum Session Lifetime
    • Time until users are required to log in again regardless of activity
    • default 값: 10080min = 7days
  • 설정 방법
    • Dashboard > Tenant Settings > Advanced tab > Session Expiration
  • https://auth0.com/docs/manage-users/sessions/configure-session-lifetime-settings

Test

Idle session timeMax session timeMax access token결과
1min2min2min1분 넘으면 자동 logout
1min2min2min1분 넘으면 자동 logout
2min3min2min2분 뒤 client-side exception has occurred
2min3min2min2분 뒤 client-side exception has occurred

결론

  1. max access token lifetime <= idle session lifetime : application error
  2. access token lifetime > session lifetime : idle session time에 따라 로그아웃 됨. 단, 새로고침 등 token을 완전히 새로 받아오면 새로 받아온 시점 기준으로 토큰의 exp가 갱신됨.

구현

구현 조건

  1. user activity가 없을 때 10분이 지나면 Logout
  2. user activity가 있을 때 10분을 기준으로 연장됨

구현 방법

  1. Idle Session Lifetime 10min, Max Session Lifetime 24hr로 설정

2-1. Front에서 addEventListener를 활용해서 user action 감지

// 예시
import { useEffect } from 'react';
import { useAuth0 } from '@auth0/auth0-react';

const useUserActionTracker = () => {
  const { getAccessTokenSilently, logout } = useAuth0();
  let idleTimer;

  useEffect(() => {
    const resetIdleTimer = () => {
      clearTimeout(idleTimer);
      // 세션 연장을 위해 Access Token 갱신 시도
      refreshSession();
    };

    const refreshSession = async () => {
      try {
        await getAccessTokenSilently();
        console.log('Session extended');
      } catch (error) {
        console.error('Failed to refresh session', error);
        logout({ returnTo: window.location.origin });
      }
    };

    const events = ['mousemove', 'keydown', 'scroll', 'focus'];

    // 이벤트 리스너 추가
    events.forEach(event => {
      window.addEventListener(event, resetIdleTimer);
    });

    // 초기 타이머 설정
    idleTimer = setTimeout(() => {
      logout({ returnTo: window.location.origin });
    }, 10 * 60 * 1000); // 10분

    return () => {
      // 이벤트 리스너 및 타이머 정리
      events.forEach(event => {
        window.removeEventListener(event, resetIdleTimer);
      });
      clearTimeout(idleTimer);
    };
  }, [getAccessTokenSilently, logout]);
};

export default useUserActionTracker;

2-2. 세션 갱신 요청 남발을 막기 위해 debounce or throttle 활용

// 예시
import { useEffect, useRef } from 'react';
import { useAuth0 } from '@auth0/auth0-react';

const useUserActionTracker = () => {
  const { getAccessTokenSilently, logout } = useAuth0();
  const idleTimerRef = useRef(null);
  const lastRefreshTimeRef = useRef(Date.now());

  useEffect(() => {
    const refreshSession = async () => {
      try {
        const now = Date.now();
        // 9분 주기로 세션 갱신
        if (now - lastRefreshTimeRef.current > 9 * 60 * 1000) {
          await getAccessTokenSilently();
          lastRefreshTimeRef.current = now;
          console.log('Session extended');
        }
      } catch (error) {
        console.error('Failed to refresh session', error);
        logout({ returnTo: window.location.origin });
      }
    };

    const resetIdleTimer = () => {
      clearTimeout(idleTimerRef.current);
      idleTimerRef.current = setTimeout(() => {
        logout({ returnTo: window.location.origin });
      }, 10 * 60 * 1000); // 10분
      refreshSession();
    };

    const events = ['mousemove', 'keydown', 'scroll', 'focus'];

    // 이벤트 리스너 등록
    events.forEach(event => {
      window.addEventListener(event, resetIdleTimer);
    });

    // 초기 타이머 설정
    resetIdleTimer();

    return () => {
      // 이벤트 리스너 제거 및 타이머 정리
      events.forEach(event => {
        window.removeEventListener(event, resetIdleTimer);
      });
      clearTimeout(idleTimerRef.current);
    };
  }, [getAccessTokenSilently, logout]);
};

export default useUserActionTracker;

2-3. Refresh token 활용

// Auth0 Provider 설정
const Auth0Wrapper = ({ children }) => (
  <Auth0Provider
    domain="YOUR_AUTH0_DOMAIN"
    clientId="YOUR_CLIENT_ID"
    authorizationParams={{
      redirect_uri: window.location.origin,
      scope: 'openid profile email offline_access', // offline_access 추가
      audience: 'YOUR_API_AUDIENCE',
    }}
    useRefreshTokens={true}
    cacheLocation="localstorage" // Refresh Token 저장소 설정
  >
    {children}
  </Auth0Provider>
);

export default () => (
  <Auth0Wrapper>
    <App />
  </Auth0Wrapper>
);
  • 유저 액션 감지 및 타이머 설정
import { useEffect, useRef } from 'react';
import { useAuth0 } from '@auth0/auth0-react';

const useUserActionTracker = () => {
  const { getAccessTokenSilently, logout } = useAuth0();
  const idleTimerRef = useRef(null);
  const actionTimerRef = useRef(null);

  useEffect(() => {
    const refreshSession = async () => {
      try {
        await getAccessTokenSilently();
        console.log('Access Token refreshed');
      } catch (error) {
        console.error('Failed to refresh session:', error);
        logout({ returnTo: window.location.origin });
      }
    };

    const resetIdleTimer = () => {
      clearTimeout(idleTimerRef.current);
      clearTimeout(actionTimerRef.current);

      // 10분 뒤 자동 로그아웃
      idleTimerRef.current = setTimeout(() => {
        logout({ returnTo: window.location.origin });
      }, 10 * 60 * 1000); // 10분

      // 10분 뒤 Access Token 갱신
      actionTimerRef.current = setTimeout(() => {
        refreshSession();
      }, 10 * 60 * 1000); // 10분
    };

    const userActionEvents = ['mousemove', 'keydown', 'scroll', 'click'];

    // 유저 액션 리스너 등록
    userActionEvents.forEach(event =>
      window.addEventListener(event, resetIdleTimer)
    );

    // 초기 타이머 설정
    resetIdleTimer();

    return () => {
      // 리스너 및 타이머 정리
      userActionEvents.forEach(event =>
        window.removeEventListener(event, resetIdleTimer)
      );
      clearTimeout(idleTimerRef.current);
      clearTimeout(actionTimerRef.current);
    };
  }, [getAccessTokenSilently, logout]);
};

export default useUserActionTracker;
  • 클라이언트 최상위 컴포넌트에서 트래커 사용
import React from 'react';
import { Auth0Provider } from '@auth0/auth0-react';
import useUserActionTracker from './useUserActionTracker';

const App = () => {
  useUserActionTracker();

  return (
    <div>
      <h1>Welcome to My App</h1>
      <p>If you're inactive for 10 minutes, you will be logged out.</p>
    </div>
  );
};
  • Refresh Token 안전하게 저장
    • Auth0의 React SDK는 기본적으로 Refresh Token을 메모리에 저장한다.
      하지만, 페이지 리로드 시 세션이 유지되지 않는 문제가 있을 수 있으므로 localStoragehttpOnly 쿠키를 사용하도록 설정할 수 있다.
    • cacheLocation 설정:
      • cacheLocation = "localstorage"로 설정하면 Refresh Token이 브라우저의 localStorage에 저장됨.
import { Auth0Provider } from '@auth0/auth0-react';

const App = ({ children }) => (
  <Auth0Provider
    domain="YOUR_AUTH0_DOMAIN"
    clientId="YOUR_CLIENT_ID"
    authorizationParams={{
      redirect_uri: window.location.origin,
      scope: 'openid profile email offline_access', // Refresh Token을 위한 offline_access 추가
      audience: 'YOUR_API_AUDIENCE',
    }}
    useRefreshTokens={true} // Refresh Token 사용
    cacheLocation="localstorage" // localStorage에 토큰 저장
  >
    {children}
  </Auth0Provider>
);

export default App;

And then, you need to use LocalStorageCache class to access the keys and its value.
https://community.auth0.com/t/how-to-get-refresh-token-like-getaccesstokensilently/86973

    const refresh_token = new LocalStorageCache;
    const key = refresh_token.allKeys().find(key => key.includes('auth0spa'));
    const refresh_token_value = refresh_token.get(key);
    const finalRefreshToken = refresh_token_value?.body?.refresh_token
    localStorage.setItem('refresh_token', finalRefreshToken);

0개의 댓글