Rumah  >  Artikel  >  hujung hadapan web  >  Membina jargons.dev [# Sistem Pengesahan

Membina jargons.dev [# Sistem Pengesahan

王林
王林asal
2024-08-28 06:09:36408semak imbas

Sebagai pembangun, Pengesahan adalah salah satu perkara yang paling saya hormati; Dalam pengalaman saya melakukan pengesahan (mungkin pada tahap asas), saya sentiasa bergelut dengan satu perkara atau yang lain terutamanya apabila saya perlu mengintegrasikan OAuth.

Sebelum mengusahakan ini untuk jargons.dev, pengalaman terbaharu saya melakukan Auth adalah pada Hearts di mana saya menyepadukan GitHub OAuth.

Jadi ya! Saya juga mengalami kesukaran (tradisional?) saya mengerjakan perkara ini untuk jargons.dev juga; tetapi secara jujurnya ini hanya disebabkan oleh perbezaan dalam persediaan (iaitu teknologi) walaupun — Pengalaman saya di Hearts telah menyepadukan GitHub OAuth dengan Server Actions dalam NextJS manakala pada jargons.dev, saya menyepadukan GitHub OAuth dengan Astro.

Lelaran

Seperti yang sedang saya tulis, sistem pengesahan telah melalui 3 Lelaran, dengan lebih terancang (perincian tentang lelaran seterusnya dalam isu #30 ini); lelaran ini sepanjang minggu pembangunan telah melaksanakan penambahbaikan atau memfaktorkan semula satu atau dua perkara kerana beberapa had yang tidak diketahui.

Lelaran Pertama

Lelaran ini dilaksanakan dalam fungsi pengesahan asas yang membenarkan permulaan aliran OAuth GitHub, pengendalian respons yang menukar kod pengesahan untuk accessToken yang kami simpan dengan selamat pada kuki pengguna.

Perubahan penting yang patut dinyatakan tentang lelaran ini ialah
  • Saya menyepadukan Apl GitHub OAuth yang menggunakan kebenaran dengan tawaran token yang terperinci; ini menjanjikan aksesToken jangka pendek dengan refreshToken.
    • Saya melaksanakan 2 laluan API untuk mengendalikan permintaan berkaitan Auth
    • api/github/oauth/callback - yang mengendalikan respons daripada aliran OAuth dengan mengubah hala ke laluan tertentu permintaan dibuat dengan kod kebenaran aliran
    • api/github/oauth/authorize- laluan yang dipanggil dari laluan ubah hala, disampaikan dengan kod kebenaran aliran, menukar kod untuk token akses dan mengembalikannya sebagai respons.
  • Saya melaksanakan tindakan pertama (tidak berkaitan dengan Tindakan Pelayan Astro baharu dan eksperimen, saya melakukan ini jauh sebelum pengumuman ?) — ini adalah istilah yang baru saya reka untuk memanggil fungsi yang dijalankan di bahagian pelayan Astro "sebelum halaman dimuatkan", anda akan mengetahuinya melalui konvensyen penamaannya: doAction, dan gayanya mengambil objek astroGlobal sebagai satu-satunya parameter, biasanya fungsi async yang mengembalikan objek respons.
    • doAuth - tindakan ini disepadukan pada mana-mana halaman yang saya ingin lindungi, ia menyemak kehadiran token akses dalam kuki; — jika ada: ia menukar itu untuk data pengguna, mengembalikan nilai boolean isAuthed bersamanya untuk mengesahkan pengesahan untuk halaman yang dilindungi; — jika tiada token ditemui: ia menyemak kehadiran kod kebenaran aliran sumpah dalam param carian url, menukarnya untuk token akses (dengan memanggil laluan api/github/oauth/benar) dan menyimpannya selamat ke kuki, kemudian menggunakan kuki dengan sewajarnya; kini dalam kes di mana tiada accessToken ditemui dalam kuki dan tiada kod auth dalam param carian url, maka nilai yang dikembalikan adalahAuthed adalah palsu dan ia akan digunakan pada halaman yang dilindungi untuk mengubah hala ke halaman log masuk.
      const { isAuthed, authedData: userData } = await doAuth(Astro);
      if (!isAuthed) return redirect(`/login?return_to=${pathname}`);
      
    • ...tindakan doAuth ini juga mengembalikan fungsi utiliti getAuthUrl yang digunakan untuk menjana url aliran OAuth GitHub yang seterusnya ditambah sebagai pautan ke "Sambung dengan GitHub" pada halaman log masuk dan setelah diklik, ia memulakan Aliran OAuth

Lihat PR:

Building jargons.dev [# The Authentication System

feat: laksanakan auth (dengan github oauth) #8
Building jargons.dev [# The Authentication System
disiarkan pada
29 Mac 2024
<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);
---

11cef86d50b82c68655376cb62f0db5b
  e33cc9d786368fb494b726688270e59a
    713e9867a88cfb8c49f3f463c33c78a3
      3f1d768d1c2bece0763d63bdace7515a
      e388a4556c0f65e1904146cc1a846beeHello, { authedData.login }94b3e26ee717c64999d7867364b1b4a3      
    16b28748ea4df4d9c2150843fecfba68
  07ebbc2b3735b6cb0330715d08d2749e
a7404faa8989ab176c7e812b70808e66
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

Lelaran Ketiga

Lelaran ini memfaktorkan semula sebahagian besar pelaksanaan dalam "Lelaran Pertama" kerana pengehadan tertentu yang muncul semasa kerja yang saya lakukan pada skrip lain.

Pada ketika ini saya sedang mengusahakan skrip "Submit Word"; skrip ini memanfaatkan API GitHub dan mencipta Permintaan Tarik untuk menggabungkan perubahan yang dibuat daripada cawangan garpu pengguna yang sedang disahkan kepada cawangan utama asas (jargons.dev). Sudah tentu ini dimungkinkan oleh token akses pengguna yang disimpan pada kuki yang digunakan dalam pengepala permintaan sebagai "Token Pembawa Kebenaran" oleh SDK iaitu Octokit yang memudahkan interaksi kami dengan API GitHub.

Batasan

Semasa ujian ketika saya memberikan skrip serah perkataan berputar, saya telah menemui kesilapan...

Ralat: Sumber tidak boleh diakses melalui penyepaduan

...ini dengan cepat menjadi penyekat dan saya berunding dengan @gr2m yang dengannya kami segera menemui had yang berkaitan dengan penyepaduan Apl GitHub saya.

Seperti yang dinyatakan pada awalnya, Apl GitHub menggunakan "Kebenaran" dengan token yang halus - jenis token baharu yang digalakkan GitHub atas beberapa sebab yang sangat baik dengan yang dipetik di bawah sebagai salah satu yang membimbangkan kami di sini...

Apl GitHub memberikan lebih kawalan ke atas perkara yang boleh dilakukan oleh apl itu. Daripada skop luas yang digunakan apl OAuth, Apl GitHub menggunakan kebenaran terperinci. Contohnya, jika apl anda perlu membaca kandungan repositori, apl OAuth akan memerlukan skop repo, yang turut membenarkan apl mengedit kandungan dan tetapan repositori. Apl GitHub boleh meminta akses baca sahaja kepada kandungan repositori, yang tidak akan membenarkan apl mengambil tindakan yang lebih istimewa seperti mengedit kandungan atau tetapan repositori.

...ini bermakna apabila menggunakan "Kebenaran" (iaitu kebenaran terperinci), pengguna mesti mempunyai akses tulis kepada repositori hulu/asas yang dalam kes ini ialah repositori jargons.dev kami; seperti yang dinyatakan dalam dokumen GitHub Create a Pull Request.

Cakap apa!? Tidak!!!

Pada ketika itu kami mendapati skop lama yang jelas adalah tepat seperti yang kami perlukan; Untuk dapat mengakses sumber yang diperlukan, skop public_repo adalah segala-galanya.

Pertukaran untuk Apl OAuth GitHub daripada Apl GitHub

Untuk bergerak ke hadapan, saya terpaksa menukar daripada "kebenaran" kepada "skop" dan di mana kami dapati ia berada dalam "Apl OAuth" GitHub; ini adalah asas di mana lelaran ketiga telah ditampal.

Jadi lelaran ini tertumpu terutamanya pada pertukaran integrasi OAuth GitHub, juga memastikan pembantu/fungsi/api yang dilaksanakan dalam lelaran ini menyerupai yang disediakan oleh Apl GitHub untuk mengurangkan jumlah perubahan yang akan saya lakukan merentas seluruh pangkalan kod sebagai mengiktiraf pelaksanaan baharu.

Perdagangan Off

Apl GitHub hebat, saya mesti mengakui bahawa saya masih menyimpannya dalam fikiran saya untuk masa hadapan jika kita akhirnya mencari penyelesaian kepada Ralat: Sumber tidak boleh diakses oleh ralat penyepaduan, tetapi fungsi untuk mencipta Permintaan Tarik dilakukan oleh skrip serah-kata adalah bahagian penting projek, jadi anda pasti kami perlu memastikan ia berfungsi.

Adalah penting untuk menyatakan bahawa terdapat beberapa pertukaran yang perlu saya selesaikan untuk memihak kepada fungsi...

  • Tiada Lagi Token jangka pendek - Apl GitHub menyediakan accessToken yang tamat tempoh selepas tempoh tertentu dan refreshToken untuk memuat semula token ini; ini sangat baik untuk keselamatan; Tidak seperti Apl OAuth yang menyediakan aksesToken yang tidak pernah tamat tempoh sama sekali dan tidak memberikan refreshToken sudah tentu
  • Tiada Lagi token yang berfungsi hanya melalui jargons.dev - Saya faham (penting untuk menyatakan) bahawa accessToken yang dijana melalui aliran OAuth yang dimulakan melalui jargons.dev hanya boleh digunakan untuk membuat permintaan melalui jargons.dev, menjadikannya tidak mungkin untuk ambil token ini untuk digunakan sebagai kebenaran di tempat lain; Tidak seperti Apl OAuth yang menyediakan accessToken yang boleh diambil dan digunakan di mana-mana tempat lain seperti anda menggunakan token akses peribadi biasa yang dijana daripada akaun GitHub anda.

Penyelesaian

  • Tiada Lagi Token jangka pendek - Saya sengaja menambah tamat tempoh 8 jam untuk accessToken apabila menyimpannya pada kuki untuk memastikan ia sekurang-kurangnya dipadamkan daripada kuki, justeru mencetuskan aliran OAuth baharu untuk memastikan aksesToken baharu (jika itu benar-benar adalah kesnya) dijana daripada aliran.
  • Tiada Lagi token yang berfungsi hanya melalui jargons.dev - Haha, adalah penting untuk menyatakan bahawa apabila kita menyimpan accessToken ke kuki, ia akan disulitkan, yang bermaksud kemungkinan besar token yang disulitkan berguna di tempat lain kerana mereka perlu menyahsulit sesuatu yang kami telah disulitkan. Jadi, anda boleh katakan kami telah meletakkan kunci pada token yang hanya jargons.dev boleh membuka kunci.

Lihat PR:

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 {Promise4ae3230490577696c575abb709ab983c}
         */
        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 {Omit1e4fb368bde7d280f14ef2719604d1ea & { 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

Atas ialah kandungan terperinci Membina jargons.dev [# Sistem Pengesahan. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Artikel sebelumnya:Memahami Aliran PengesahanArtikel seterusnya:Memahami Aliran Pengesahan