> 웹 프론트엔드 > JS 튜토리얼 > jargons.dev 구축 [# 인증 시스템

jargons.dev 구축 [# 인증 시스템

王林
풀어 주다: 2024-08-28 06:09:36
원래의
430명이 탐색했습니다.

개발자로서 인증은 제가 가장 존경하는 것 중 하나입니다. 인증(어쩌면 기본 수준)을 수행한 경험에 따르면 특히 OAuth를 통합해야 할 때 항상 이것저것 문제로 어려움을 겪었습니다.

jargons.dev에서 이 작업을 수행하기 전 가장 최근에 인증을 수행한 경험은 GitHub OAuth를 통합한 Hearts에서였습니다.

그래요! 나는 또한 jargons.dev를 위해 이 작업을 수행하면서 (전통적인?) 어려움을 겪었습니다. 하지만 솔직히 이것은 설정(예: 기술)의 차이 때문이었습니다. Hearts에 대한 제 경험은 GitHub OAuth를 NextJS의 Server Actions와 통합하는 것이었고, jargons.dev에서는 GitHub OAuth를 Astro와 통합하고 있습니다.

반복

현재 제가 쓰고 있는 것처럼 인증 시스템은 3번의 반복을 거쳤으며 더 많은 계획이 있습니다(다음 반복에 대한 자세한 내용은 이번 호 #30 참조). 몇 주에 걸친 개발 반복을 통해 몇 가지 발견된 제한 사항으로 인해 한두 가지 개선 사항이 구현되거나 리팩터링되었습니다.

첫 번째 반복

이 반복은 GitHub OAuth 흐름 시작을 허용하는 기본 인증 기능, 사용자 쿠키에 안전하게 저장되는 accessToken에 대한 인증 코드를 교환하는 응답 처리로 구현되었습니다.

이번 반복에 대해 언급할 가치가 있는 중요한 변경 사항은 다음과 같습니다.
  • 세분화된 토큰 제공과 함께 권한을 사용하는 GitHub 앱 OAuth를 통합했습니다. 이는 RefreshToken을 사용하여 단기 accessToken을 약속합니다.
    • 인증 관련 요청을 처리하기 위해 2개의 API 경로를 구현했습니다.
    • api/github/oauth/callback - 흐름 인증 코드를 사용하여 요청이 이루어진 특정 경로로 리디렉션하여 OAuth 흐름의 응답을 처리합니다.
    • api/github/oauth/authorize- 리디렉션 경로에서 호출되고 흐름 인증 코드와 함께 제공되는 경로는 액세스 토큰에 대한 코드를 교환하고 이를 응답으로 반환합니다.
  • 첫 번째 작업을 구현했습니다(새롭고 실험적인 Astro 서버 작업과 관련이 없으며 발표하기 오래 전에 수행했습니까?) — 이것은 서버 측에서 실행되는 함수를 호출하기 위해 방금 만든 용어입니다. Astro는 "페이지가 로드되기 전에" 이름 지정 규칙인 doAction과 astroGlobal 객체를 유일한 매개변수로 사용하는 스타일로 이를 알 수 있으며 일반적으로 응답 객체를 반환하는 비동기 함수입니다.
    • doAuth - 이 작업은 보호하려는 모든 페이지에 통합되어 쿠키에 액세스 토큰이 있는지 확인합니다. — 존재하는 경우: 이를 사용자 데이터와 교환하고 보호된 페이지에 대한 인증을 확인하기 위해 부울 값 isAuthed를 반환합니다. — 토큰이 발견되지 않은 경우: URL 검색 매개변수에 맹세 흐름 인증 코드가 있는지 확인하고 이를 액세스 토큰과 교환하고(api/github/oauth/authorize 경로를 호출하여) 쿠키에 안전하게 저장한 다음 쿠키를 적절하게; 이제 쿠키에서 accessToken을 찾을 수 없고 URL 검색 매개변수에 인증 코드가 없는 경우 반환된 값 isAuthed는 false이며 보호된 페이지에서 로그인 페이지로 리디렉션하는 데 사용됩니다.
      const { isAuthed, authedData: userData } = await doAuth(Astro);
      if (!isAuthed) return redirect(`/login?return_to=${pathname}`);
      
      로그인 후 복사
    • ...이 doAuth 작업은 GitHub OAuth 흐름 URL을 생성하는 데 사용되는 유틸리티 함수 getAuthUrl도 반환합니다. 이 URL은 로그인 페이지의 "GitHub과 연결"에 대한 링크로 추가되고 클릭하면 시작됩니다. OAuth 흐름

홍보 보기:

Building jargons.dev [# The Authentication System

feat: 인증 구현(github oauth 사용) #8
Building jargons.dev [# The Authentication System
게시일:
2024년 3월 29일
<script> // Detect dark theme var iframe = document.getElementById('tweet-1735788008085856746-446'); if (document.body.className.includes('dark-theme')) { iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1735788008085856746&theme=dark" } </script> <script> // Detect dark theme var iframe = document.getElementById('tweet-1773811079145009311-808'); if (document.body.className.includes('dark-theme')) { iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1773811079145009311&theme=dark" } </script>

This Pull request implement the authentication feature in the project; using the github oauth, our primary goal is to get and hold users github accessToken in cookies for performing specific functionality. It is important to state that this feature does not take store this user's accessToken to any remote server, this token and any other information that was retrieved using the token are all saved securely on the users' end through usage of cookies.

Changes Made

  • Implemented the github oauth callback handler at /api/github/oauth/callback - this handler's main functionality is to receive github's authorization code and state to perform either of the following operations

    • Redirect to the path stated in the state params with the authorization code concatenated to it using the Astro.context.redirect method
    • or If a redirect=true value if found in the state param, then we redirect to the the path stated in the state params with the authorization code and redirect=true value concatenated to it using Astro.context.redirect method
  • Implemented the github oauth authorization handler at /api/github/oauth/authorization - this handler is a helper that primarily exchanges the authorization code for tokens and returns it in a json object.

  • Created a singleton instance of our github app at lib/octokit/app

  • Added a new crypto util function which provides encrypt and decrypt helper function has exports; it is intended to be used for securing the users related cookies

  • Implemented the doAuth action function - this function take the Astro global object as argument and performs the operations stated below

    /**
     * Authentication action with GitHub OAuth
     * @param {import("astro").AstroGlobal} astroGlobal 
     */
    export default async function doAuth(astroGlobal) {
      const { url: { searchParams }, cookies } = astroGlobal;
      const code = searchParams.get("code");
      const accessToken = cookies.get("jargons.dev:token", {
        decode: value => decrypt(value)
      });
    
      /**
       * Generate OAuth Url to start authorization flow
       * @todo make the `parsedState` data more predictable (order by path, redirect)
       * @todo improvement: store `state` in cookie for later retrieval in `github/oauth/callback` handler for cleaner url
       * @param {{ path?: string, redirect?: boolean }} state 
       */
      function getAuthUrl(state) {
        const parsedState = String(Object.keys(state).map(key => key + ":" + state[key]).join("|"));
        const { url } = app.oauth.getWebFlowAuthorizationUrl({
          state: parsedState
        });
        return url;
      }
    
      try {
        if (!accessToken && code) {
          const response = await GET(astroGlobal);
          const responseData = await response.json();
      
          if (responseData.accessToken && responseData.refreshToken) {
            cookies.set("jargons.dev:token", responseData.accessToken, {
              expires: resolveCookieExpiryDate(responseData.expiresIn),
              encode: value => encrypt(value)
            });
            cookies.set("jargons.dev:refresh-token", responseData.refreshToken, {
              expires: resolveCookieExpiryDate(responseData.refreshTokenExpiresIn),
              encode: value => encrypt(value)
            });
          }
        }
      
        const userOctokit = await app.oauth.getUserOctokit({ token: accessToken.value });
        const { data } = await userOctokit.request("GET /user");
      
        return {
          getAuthUrl,
          isAuthed: true,
          authedData: data
        }
      } catch (error) {
        return {
          getAuthUrl,
          isAuthed: false,
          authedData: null
        }
      }
    }
    로그인 후 복사
    Enter fullscreen mode Exit fullscreen mode
    • it provides (in its returned object) a helper function that can be used to generate a new github oauth url, this helper consumes our github app instance and it accepts a state object with path and redirectproperty to build out thestate` value that is held within the oauth url
    • it sets cookies data for tokens - it does this when it detects the presence of the authorization code in the Astro.url.searchParams and reads the absense no project related accessToken in cookie; this assumes that there's a new oauth flow going through it; It performs this operation by first calling the github oauth authorization handler at /api/github/oauth/authorization where it gets the tokens data that it adds to cookie and ensure its securely store by running the encrypt helper to encode it value
    • In cases where there's no authorization code in the Astro.url.searchParams and finds a project related token in cookie, It fetches users's data and provides it in its returned object for consumptions; it does this by getting the users octokit instance from our github app instance using the getUserOctokit method and the user's neccesasry tokens present in cookie; this users octokit instance is then used to request for user's data which is in turn returned
    • It also returns a boolean isAuthed property that can be used to determine whether a user is authenticated; this property is a statically computed property that only always returns turn when all operation reaches final execution point in the try block of the doAuth action function and it returns false when an error occurs anywhere in the operation to trigger the catch block of the doAuth action function
  • Added the login page which stands as place where where unauthorised users witll be redirected to; this page integrates the doAuth action, destruing out the getAuthUrl helper and the isAuthed property, it uses them as follows

    const { getAuthUrl, isAuthed } = await doAuth(Astro);
    
    if (isAuthed) return redirect(searchParams.get("redirect"));
    
    const authUrl = getAuthUrl({
      path: searchParams.get("redirect"),
      redirect: true
    });
    로그인 후 복사
    Enter fullscreen mode Exit fullscreen mode
    • isAuthed - this property is check on the server-side on the page to check if a user is already authenticated from within the doAuth and redirects to the value stated the page's Astro.url.searchParams.get("redirect")
    • When a user is not authenticated, it uses the getAuthUrl to generate a new github oauth url and imperatively set the argument state.redirect to true
    • Implemented a new user store with a Map store value $userData to store user data to state

Integration Demo: Protect /sandbox page

// pages/sandbox.astro

---
import BaseLayout from "../layouts/base.astro";
import doAuth from "../lib/actions/do-auth.js";
import { $userData } from "../stores/user.js";

const { url: { pathname }, redirect } = Astro;
const { isAuthed, authedData } = await doAuth(Astro);

if (!isAuthed) return redirect(`/login?redirect=<span class="pl-s1"><span class="pl-kos">${pathname}</span>`</span>);

$userData.set(authedData);
---

<BaseLayout pageTitle="Dictionary">
  <main class="flex flex-col max-w-screen-lg p-5 justify-center mx-auto min-h-screen">
    <div class="w-fit p-4 ring-2 rounded-full ring-gray-500 m-auto flex items-center space-x-3">
      <img class="w-10 h-10 p-1 rounded-full ring-2 ring-gray-500" 
        src={authedData.avatar_url} 
        alt={authedData.login}
      >
      <p>Hello, { authedData.login }</p>      
    </div>
  </main>
</BaseLayout>
로그인 후 복사
Enter fullscreen mode Exit fullscreen mode

Explainer

  • We destructure isAuthed and authedData from the doAuth action
  • Check whether a user is not authenticated? and do a redirect to login page stating the current pathname as value for the redirect search param (a data used in state to dictate where to redirect to after authentication complete) if no user is authenticated
  • or Proceed to consuming the authedData which will be available when a isAuthed is true. by setting it to the $userData map store property

Screencast/Screenshot

screencast-bpconcjcammlapcogcnnelfmaeghhagj-2024.03.29-20_36_15.webm

Note

  • Added new node package https://www.npmjs.com/package/@astrojs/node for SSR adapter intergation
View on GitHub

Second Iteration

This iteration implements improvements by making making the parsedState derived from the getAuthUrl function call more predictable removing the chances of an error in the api/github/oauth/callback route; it also renames some terms used in the search params and implements the the encodeURIComponent to make our redirect urls look less weird

See PR:

Building jargons.dev [# The Authentication System feat: implement `auth` (second iteration) improvements #28

Building jargons.dev [# The Authentication System
babblebey posted on

This PR implements some improvement to mark the second iteration of the auth feature in the project. Follow-up to #8

Changes Made

  • Addressed "todo make the parsedState data more predictable (order by path, redirect)" by implementing more predicatable manner of generating the state string for the oauth url; this is done by individually looking for the required state object key to fill in the parsedState string in required order. Leaving the new getAuthUrl helper function looking like so...
function getAuthUrl(state) {
  let parsedState = "";

  if (!isObjectEmpty(state)){
    if (state.path) parsedState += `path:<span class="pl-s1"><span class="pl-kos">${state.path}</span>`</span>;
    const otherStates = String(Object.keys(state)
      .filter(key => key !== "path" && key !== "redirect")
      .map(key => key + ":" + state[key]).join("|"));
    if (otherStates.length > 0) parsedState += `|<span class="pl-s1"><span class="pl-kos">${otherStates}</span>`</span>;
  }

  const { url } = app.oauth.getWebFlowAuthorizationUrl({
    state: parsedState
  });

  return url;
}
로그인 후 복사
Enter fullscreen mode Exit fullscreen mode
  • Implemented a new utility function isObjectEmpty to check if an object has value or not
  • Removed usage of redirect property in state object; its redundant ?‍?
  • Renamed login redirect path params property name to return_to from redirect for readability reasons
  • Implemented encodeURIComponent in login redirect path params value to stop its part on the url from looking like weird ?;
    • Takes the example url from looking like this... /login?return_to=/editor to looking like so... /login?return_to=%2Feditor

Related Isssue

Resolves #15

View on GitHub

세 번째 반복

이 반복은 다른 스크립트에서 작업을 수행하는 동안 표면화된 특정 제한으로 인해 "첫 번째 반복" 구현의 대부분을 리팩터링합니다.

이 시점에서 저는 "Submit Word" 스크립트를 작업하고 있었습니다. 이 스크립트는 GitHub API를 활용하고 현재 인증된 사용자의 포크 브랜치에서 기본(jargons.dev) 기본 브랜치로 변경된 내용을 병합하기 위한 풀 요청을 생성합니다. 이는 물론 GitHub API와의 상호 작용을 용이하게 하는 SDK(예: Octokit)에 의해 요청 헤더에 "Authorization Bearer 토큰"으로 사용되는 쿠키에 저장된 사용자의 액세스 토큰을 통해 가능해집니다.

한계

테스트 중 단어 스크립트 제출을 한바퀴 돌았는데 오류가 떴는데...

오류: 통합으로 리소스에 액세스할 수 없습니다.

...이것은 빠르게 방해 요소가 되었고 @gr2m에게 문의하여 GitHub 앱 통합과 관련된 제한 사항을 신속하게 발견했습니다.

처음에 언급한 대로 GitHub 앱은 세분화된 토큰과 함께 "권한"을 사용합니다. 이 토큰 유형은 GitHub에서 권장하는 새로운 토큰 유형입니다. 아래에 인용된 토큰이 여기서 우려되는 사항입니다...

GitHub 앱은 앱이 수행할 수 있는 작업에 대해 더 많은 제어 기능을 제공합니다. OAuth 앱이 사용하는 광범위한 범위 대신 GitHub 앱은 세분화된 권한을 사용합니다. 예를 들어 앱이 저장소의 콘텐츠를 읽어야 하는 경우 OAuth 앱에는 저장소 범위가 필요하며 이를 통해 앱이 저장소 콘텐츠와 설정을 편집할 수도 있습니다. GitHub 앱은 저장소 콘텐츠에 대한 읽기 전용 액세스를 요청할 수 있으며, 이로 인해 앱이 저장소 콘텐츠 또는 설정 편집과 같은 더 많은 권한 있는 작업을 수행할 수 없습니다.

...이는 "권한"(즉, 세분화된 권한)을 사용할 때 사용자가 upstream/base 저장소(이 경우 jargons.dev 저장소)에 대한 쓰기 액세스 권한을 가지고 있어야 함을 의미합니다. GitHub Create a Pull Request 문서에 명시된 바와 같습니다.

뭐라고!? 아니요!!!

그 시점에서 우리는 평범하고 오래된 스코프가 우리에게 꼭 필요한 것임을 알게 되었습니다. 필요한 리소스에 접근할 수 있도록 public_repo 범위가 전부였습니다.

GitHub 앱에서 GitHub의 OAuth 앱으로 교체

계속 진행하려면 "권한"에서 "범위"로 전환해야 했고, GitHub의 "OAuth 앱"에서 해당 위치를 찾았습니다. 이것이 세 번째 패치의 기반이 되었습니다.

따라서 이번 반복은 주로 GitHub OAuth 통합 교환에 중점을 두었으며, 또한 이 반복에서 구현된 도우미/함수/API가 GitHub 앱에서 제공한 것과 유사하도록 보장하여 변경하려는 양을 줄였습니다. 새로운 구현을 인정하여 전체 코드베이스에 걸쳐 적용되었습니다.

절충안

GitHub 앱은 훌륭합니다. 오류: 통합 오류로 리소스에 액세스할 수 없지만 끌어오기 요청을 생성하는 기능이 수행되는 문제에 대한 해결책을 찾게 된다면 앞으로도 계속 염두에 두고 있다는 점을 인정해야 합니다. submit-word 스크립트를 작성하는 것은 프로젝트의 필수 부분이므로 제대로 작동하는지 확인해야 합니다.

기능을 고려하면 몇 가지 절충안이 있었다는 점을 언급하는 것이 중요합니다...

  • 더 이상 단기 토큰이 없습니다. GitHub 앱은 특정 기간 후에 만료되는 accessToken과 이 토큰을 새로 고치는 RefreshToken을 제공합니다. 이는 보안에 매우 좋습니다. 만료되지 않는 accessToken을 제공하고 물론 새로고침 토큰도 제공하지 않는 OAuth 앱과 달리
  • jargons.dev를 통해서만 작동하는 토큰은 더 이상 없습니다. jargons.dev를 통해 시작된 OAuth 흐름을 통해 생성된 accessToken은 jargons.dev를 통해 요청하는 데만 사용할 수 있으므로 다음 작업이 불가능하다는 점을 이해했습니다(설명에 중요함). 이 토큰을 사용하여 다른 곳에서 인증으로 사용하세요. GitHub 계정에서 생성된 일반 개인 액세스 토큰을 사용하는 것처럼 다른 곳에서 가져와 사용할 수 있는 accessToken을 제공하는 OAuth 앱과 달리.

해결 방법

  • 더 이상 단기 토큰이 없습니다. accessToken을 쿠키에 저장할 때 적어도 쿠키에서 삭제되도록 의도적으로 accessToken에 8시간 만료를 추가했습니다. 따라서 새로운 accessToken을 보장하기 위해 새로운 OAuth 흐름을 트리거했습니다(만약 실제로 경우)가 흐름에서 생성됩니다.
  • jargons.dev를 통해서만 작동하는 토큰은 더 이상 없습니다. 하하, accessToken을 쿠키에 저장하는 시점에 암호화되므로 암호화된 토큰이 유용할 가능성이 낮다는 점을 명시해야 합니다. 왜냐하면 그들은 우리가 암호화한 것을 해독해야 하기 때문입니다. 따라서 우리는 jargons.dev만이 잠금을 해제할 수 있는 토큰에 잠금을 설정했다고 말할 수 있습니다.

홍보 보기:

Building jargons.dev [# The Authentication System refactor(auth): replace `github-app-oauth` with classic `oauth` app #33

Building jargons.dev [# The Authentication System
babblebey posted on

This Pull request refactors the authentication system, replacing the usage of github-app-oauth with classic github oauth app. This decision was taken because of the limitations discovered using the Pull Request endpoint (implementation in #25); the github-app-oauth uses permissions which requires a user to have write access to the upstream (i.e. write access to atleast pull-requests on our/this project repo) before a pull request can created from their forked repo branch to the main project repo.

This PR goes to implement classis oauth app, which uses scopes and allows user access to create the pull request to upstream repo on the public_repo scope. The changes made in this PR was done to mimic the normal Octokit.App's methods/apis as close as possible to allow compatibility with the implementation in #8 and #28 (or for cases when we revert back to using the github-app-oauth in the future --- maybe we end up finding a solution because honestly I really prefer the github-app-oauth ?).

It is also important to state that this oauth app option doesn't offer a short lived token (hence we only have an accessToken without expiry and No refreshToken), but I have configured the token to expire out of cookie in 8hours; even though we might be getting exactly thesame token back from github after this expires and we re-authorize the flow, I just kinda like that feeling of the cookies expiring after some hours and asking user to re-auth.

Changes Made

  • Initialized a new app object that returns few methods and objects
    • octokit - the main octokit instance of the oauth app

      /**
       * OAuth App's Octokit instance
       */
      const octokit = new Octokit({
        authStrategy: createOAuthAppAuth,
        auth: {
          clientId: import.meta.env.GITHUB_OAUTH_APP_CLIENT_ID,
          clientSecret: import.meta.env.GITHUB_OAUTH_APP_CLIENT_SECRET
        },
      });
      로그인 후 복사
      Enter fullscreen mode Exit fullscreen mode
    • oauth

      • getWebFlowAuthorizationUrl - method that generates the oauth flow url

        /**
         * Generate a Web Flow/OAuth authorization url to start an OAuth flow
         * @param {import("@octokit/oauth-authorization-url").OAuthAppOptions} options
         * @returns 
         */
        function getWebFlowAuthorizationUrl({state, scopes = ["public_repo"], ...options }) {
          return oauthAuthorizationUrl({
            clientId: import.meta.env.GITHUB_OAUTH_APP_CLIENT_ID,
            state,
            scopes,
            ...options
          });
        }
        로그인 후 복사
        Enter fullscreen mode Exit fullscreen mode
      • exchangeWebFlowCode - method that exchanges oauth web flow returned code for accessToken; this functionality was extracted from the github/oauth/authorize endpoint to have all auth related function packed in one place

        /**
         * Exchange Web Flow Authorization `code` for an `access_token` 
         * @param {string} code 
         * @returns {Promise<{access_token: string, scope: string, token_type: string}>}
         */
        async function exchangeWebFlowCode(code) {
          const queryParams = new URLSearchParams();
          queryParams.append("code", code);
          queryParams.append("client_id", import.meta.env.GITHUB_OAUTH_APP_CLIENT_ID);
          queryParams.append("client_secret", import.meta.env.GITHUB_OAUTH_APP_CLIENT_SECRET);
        
          const response = await fetch("https://github.com/login/oauth/access_token", {
              method: "POST",
              body: queryParams
            });
          const responseText = await response.text();
          const responseData = new URLSearchParams(responseText);
        
          return responseData;
        }
        로그인 후 복사
        Enter fullscreen mode Exit fullscreen mode
    • getUserOctokit - method that gets an octokit instance of a user.

      /**
       * Get a User's Octokit instance
       * @param {Omit<OctokitOptions, "auth"> & { token: string }} options
       * @returns {Octokit}
       */
      function getUserOctokit({ token, ...options }) {
        return new Octokit({
          auth: token,
          ...options
        });
      };
      로그인 후 복사
      Enter fullscreen mode Exit fullscreen mode
  • Integrated the app.oauth.exchangeWebFlowCode method into the github/oauth/authorize endpoint handler
  • Removed the refreshToken and refreshTokenExpiresIn from github/oauth/authorize endpoint response object.
  • Modified doAuth actions
    • Removed jargons.dev:refresh_token value set to cookie;
    • Corrected computation of userOctokit to use app.getUserOctokit from app.oauth.getUserOctokit (even though I could just move the getUserOctokit method to the app.oauth object in the new implmentation, I just prefer it this way ?).

?

Screencast

screencast-bpconcjcammlapcogcnnelfmaeghhagj-2024.04.07-07_37_31.webm

View on GitHub

위 내용은 jargons.dev 구축 [# 인증 시스템의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿