Heim  >  Artikel  >  Web-Frontend  >  Erstellen von jargons.dev [# Das Authentifizierungssystem

Erstellen von jargons.dev [# Das Authentifizierungssystem

王林
王林Original
2024-08-28 06:09:36313Durchsuche

As a developer, Authentication is one of the things that I've got the most respect for; In my experience doing authentication (maybe on a basic level), I've always struggled with one thing or the other especially when I've got to integrate OAuth.

Prior to working on this for jargons.dev, my most recent experience doing Auth was on Hearts where I integrated GitHub OAuth.

<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>

So yea! I also had my (traditional ?) struggles working on this for jargons.dev too; but honestly this was only because of the differences in setup (i.e. technology) though — My experience on Hearts was integrating GitHub OAuth with Server Actions in NextJS meanwhile on jargons.dev, I'm integrating GitHub OAuth with Astro.

The Iterations

As I currently write, the authentication system has gone through 3 Iterations, with more planned (details on next iteration in this issue #30); these iterations over the weeks of development have implemented improvements or refactored a thing or two due to some uncovered limitation.

First Iteration

This iteration implemented in base authentication functionality that allows initiation of a GitHub OAuth flow, response handling that exchanges the authentication code for an accessToken that we securely store on user's cookies.

The imperative changes worth stating about this iteration is that

  • I integrated a GitHub App OAuth wich uses permissions with its fine-grained token offering; this promises a short-lived accessToken with a refreshToken.
  • I implemented 2 API route for handling the Auth related requests
    • api/github/oauth/callback - which handles the response from the OAuth flow by redirecting to a specific path the request was made from with the flow authorization code
    • api/github/oauth/authorize- a route called from the redirect path, served with the flow authorization code, exchanges the code for access token and returns it as response.
  • I implemented the first action (not related to the new and experimental Astro Server Actions, I done this long before the announcement ?) — this is a term I just made up to call functions that are ran on the server-side of Astro "before the page loads", you shall know it by its naming convension: doAction, and its style of taking the astroGlobal object as the only parameter, it's usually async function that returns a response object.

    • doAuth - this action integrates on any page I wish to protect, it checks for the presence of an access token in cookie; — if present: it exchanges that for user data, returns a boolean value isAuthed alongside it to confirm authentication for protected page; — if no token is found: it checks the presence of the oath flow authorization code in url search params, exchanges that for access token (by calling the api/github/oauth/authorize route) and saves it secure to cookies, then uses the cookie appropriately; now in cases where no accessToken is found in cookies and no auth code is in url search params, then the returned value isAuthed is false and it will be used on the protected page to redirect to the login page.

      const { isAuthed, authedData: userData } = await doAuth(Astro);
      if (!isAuthed) return redirect(`/login?return_to=${pathname}`);
      
    • ...this doAuth action also returns a utility function getAuthUrl that is used to generate a GitHub OAuth flow url which is in turn added as link to the "Connect with GitHub" on the login page and once clicked, it starts an OAuth flow

<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>

See PR:

Building jargons.dev [# The Authentication System feat: implement auth (with github oauth) #8

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

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

Dritte Iteration

Diese Iteration überarbeitet die meisten Teile der Implementierung in der „ersten Iteration“, da während der Arbeit, die ich an einem anderen Skript durchgeführt habe, eine gewisse Einschränkung aufgetreten ist.

Zu diesem Zeitpunkt arbeitete ich am „Submit Word“-Skript; Dieses Skript nutzt die GitHub-API und erstellt eine Pull-Anfrage, um Änderungen, die vom Fork-Zweig des aktuell authentifizierten Benutzers vorgenommen wurden, mit dem Basis-Hauptzweig (jargons.dev) zusammenzuführen. Dies wird natürlich durch das in Cookies gespeicherte Zugriffstoken des Benutzers ermöglicht, das in den Anforderungsheadern als „Autorisierungs-Bearer-Token“ vom SDK, also Octokit, verwendet wird und unsere Interaktion mit den GitHub-APIs erleichtert.

Die Einschränkung

Während eines Testmoments, als ich das Submit-Word-Skript ausprobierte, stieß ich auf einen Fehler...

Fehler: Auf die Ressource kann durch die Integration nicht zugegriffen werden

...das wurde schnell zu einem Hindernis und ich habe @gr2m konsultiert, mit dem wir schnell die Einschränkung aufgedeckt haben, die mit meinen Integrationen der GitHub-App zusammenhängt.

Wie anfangs erwähnt, verwendet die GitHub-App „Berechtigungen“ mit einem feinkörnigen Token – ein neuer Token-Typ, den GitHub aus einigen sehr guten Gründen empfiehlt, wobei der unten zitierte einer uns hier beschäftigt...

GitHub Apps bieten mehr Kontrolle darüber, was die App tun kann. Anstelle der breiten Bereiche, die OAuth-Apps verwenden, verwenden GitHub-Apps fein abgestufte Berechtigungen. Wenn Ihre App beispielsweise den Inhalt eines Repositorys lesen muss, benötigt eine OAuth-App den Repo-Bereich, der es der App auch ermöglichen würde, die Inhalte und Einstellungen des Repositorys zu bearbeiten. Eine GitHub-App kann schreibgeschützten Zugriff auf Repository-Inhalte anfordern, wodurch die App keine privilegierteren Aktionen wie das Bearbeiten der Repository-Inhalte oder -Einstellungen ausführen kann.

...das bedeutet, dass ein Benutzer bei Verwendung von „Berechtigungen“ (d. h. fein abgestuften Berechtigungen) Schreibzugriff auf das Upstream-/Basis-Repository haben muss, in diesem Fall unser jargons.dev-Repository; wie in den GitHub-Dokumenten zum Erstellen einer Pull-Anfrage angegeben.

Sag was!? Nein!!!

Zu diesem Zeitpunkt stellten wir fest, dass das einfache alte Zielfernrohr genau das war, was wir brauchten. Um auf die benötigte Ressource zugreifen zu können, war der public_repo-Bereich alles.

Der Tausch gegen die OAuth-App von GitHub von der GitHub-App

Um voranzukommen, musste ich von „Berechtigungen“ zu „Bereich“ wechseln, und wir fanden das in der „OAuth-App“ von GitHub; Dies war die Grundlage, auf der die dritte Iteration gepatcht wurde.

Diese Iteration konzentrierte sich also hauptsächlich auf den Austausch der GitHub-OAuth-Integration und stellte außerdem sicher, dass die implementierten Helfer/Funktionen/APIs in dieser Iteration denen ähneln, die von der GitHub-App zur Verfügung gestellt wurden, um die Anzahl der Änderungen, die ich vornehmen wollte, zu reduzieren in Anerkennung der neuen Implementierung über die gesamte Codebasis hinweg.

Die Kompromisse

Die GitHub-App ist großartig. Ich muss zugeben, dass ich für die Zukunft immer noch daran denke, ob wir am Ende eine Lösung für den Fehler finden: „Ressource nicht zugänglich durch Integrationsfehler, aber die Funktionalität zum Erstellen einer Pull-Anfrage wurde ausgeführt.“ durch das Submit-Word-Skript ist ein zwingender Teil des Projekts, also müssen wir sicher sein, dass es funktioniert.

Es ist wichtig anzumerken, dass ich mich zugunsten der Funktionalität mit einigen Kompromissen zufrieden geben musste...

  • Kein kurzlebiges Token mehr – die GitHub-App stellt ein AccessToken bereit, das nach einem bestimmten Zeitraum abläuft, und ein RefreshToken zum Aktualisieren dieses Tokens; das ist sehr gut für die Sicherheit; Im Gegensatz zur OAuth-App, die ein AccessToken bereitstellt, das niemals abläuft, und natürlich kein RefreshToken bereitstellt
  • Kein Token mehr, das nur über jargons.dev funktioniert – ich habe verstanden (wichtig zu erwähnen), dass das über den über jargons.dev initiierten OAuth-Flow generierte AccessToken nur für Anfragen über jargons.dev verwendet werden kann, was dies nicht möglich macht Nehmen Sie dieses Token, um es an anderer Stelle als Autorisierung zu verwenden. Im Gegensatz zur OAuth-App, die ein AccessToken bereitstellt, das überall abgerufen und verwendet werden kann, als würden Sie ein normales persönliches Zugriffstoken verwenden, das von Ihren GitHub-Konten generiert wird.

Die Workarounds

  • Keine kurzlebigen Tokens mehr – ich habe beim Speichern des AccessTokens im Cookie absichtlich einen Ablauf von 8 Stunden hinzugefügt, um sicherzustellen, dass es zumindest aus dem Cookie gelöscht wird, wodurch ein neuer OAuth-Flow ausgelöst wird, um ein neues AccessToken sicherzustellen (falls das wirklich der Fall ist). ist der Fall) wird aus dem Fluss generiert.
  • Kein Token mehr, der nur über jargons.dev funktioniert – Haha, es muss unbedingt erwähnt werden, dass das AccessToken zu dem Zeitpunkt, an dem wir es im Cookie speichern, verschlüsselt wird, was bedeutet, dass es weniger wahrscheinlich ist, dass das verschlüsselte Token nützlich ist irgendwo anders, weil sie etwas entschlüsseln müssten, das wir verschlüsselt haben. Man kann also sagen, dass wir den Token gesperrt haben, den nur jargons.dev entsperren kann.

Siehe 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

Das obige ist der detaillierte Inhalt vonErstellen von jargons.dev [# Das Authentifizierungssystem. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn