JavaScript >> Javascript 文檔 >  >> JavaScript

將 OAuth 與 PKCE 授權流程一起使用(代碼交換的證明密鑰)

如果您曾經創建過登錄頁面或身份驗證系統,那麼您可能熟悉 OAuth 2.0,這是用於授權的行業標準協議。它允許應用程序安全地訪問託管在另一個應用程序上的資源。在範圍級別使用不同的流程或授權授予訪問權限。

例如,如果我製作一個應用程序(Client ) 允許用戶(資源所有者 ) 做筆記並將它們保存為他們的 GitHub 帳戶 (Resource Server 中的 repo ),那麼我的應用程序將需要訪問他們的 GitHub 數據。用戶直接向我的應用程序提供他們的 GitHub 用戶名和密碼並授予對整個帳戶的完全訪問權限是不安全的。相反,使用 OAuth 2.0,他們可以通過授權流程,該流程將根據范圍授予對某些資源的有限訪問權限,而我將永遠無法訪問任何其他數據或他們的密碼。

使用 OAuth,流最終會從 授權服務器 請求令牌 ,並且該令牌可用於在約定的範圍內發出所有未來的請求。

授權類型

您擁有的申請類型將決定申請的資助類型。

授權類型 應用類型 示例
客戶端憑據 機器 服務器通過 cron 作業訪問 3rd-party 數據
授權碼 服務器端網絡應用 Node 或 Python 服務器處理前端和後端
PKCE 的授權碼 單頁網頁應用/移動應用 與後端分離的僅客戶端應用程序

對於機器對機器的通信,例如服務器上的 cron 作業將執行的操作,您將使用 Client Credentials 授權類型,它使用客戶端 ID 和客戶端密碼。這是可以接受的,因為客戶端 ID 和資源所有者是相同的,所以只需要一個。這是使用 /token 執行的 端點。

對於服務器端 Web 應用程序,例如 Python Django 應用程序、Ruby on Rails 應用程序、PHP Laravel 或 Node/Express 服務 React,授權代碼 使用flow,在服務端仍然使用client id和client secret,但用戶需要先通過第三方授權。這是使用 /authorize 執行的 和 /token 端點。

但是,對於僅客戶端的 Web 應用程序或移動應用程序,授權碼流程是不可接受的,因為客戶端密碼無法公開,並且無法保護它。為此,使用了授權代碼流的代碼交換證明密鑰 (PKCE) 版本。在這個版本中,客戶端從頭開始創建一個秘密,並在授權請求後提供它以檢索令牌。

由於 PKCE 是 OAuth 的一個相對較新的補充,許多身份驗證服務器還不支持它,在這種情況下,要么使用安全性較低的傳統流程,如隱式授予,令牌將在請求的回調中返回,但使用不鼓勵使用隱式授權流。 AWS Cognito 是一種流行的支持 PKCE 的授權服務器。

PKCE 流

PKCE 身份驗證系統的流程涉及一個用戶 , 一個客戶端app ,以及一個授權服務器 ,並且看起來像這樣:

  1. 用戶 到達應用程序 的入口頁面
  2. 應用程序 生成一個 PKCE 代碼質詢 並重定向到授權服務器 通過 /authorize 登錄頁面
  3. 用戶 登錄到授權服務器 並被重定向回應用程序 帶有授權碼
  4. 應用程序授權服務器請求令牌 使用代碼驗證器/挑戰 通過 /token
  5. 授權服務器 使用令牌進行響應,應用程序可以使用該令牌 代表用戶訪問資源

所以我們只需要知道我們的 /authorize/token 端點應該看起來像。我將通過一個為前端 Web 應用程序設置 PKCE 的示例。

GET /authorize 端點

流程從製作 GET 開始 對/authorize的請求 端點。我們需要在 URL 中傳遞一些參數,其中包括生成代碼挑戰代碼驗證器 .

參數 說明
response_type code
client_id 您的客戶 ID
redirect_uri 你的重定向 URI
code_challenge 你的代碼挑戰
code_challenge_method S256
scope 你的範圍
state 你的狀態(可選)

我們將構建 URL 並將用戶重定向到它,但首先我們需要進行驗證和質詢。

驗證者

第一步是生成代碼驗證器,PKCE 規範定義為:

我正在使用 oauth.net 的 Aaron Parecki 寫的隨機字符串生成器:

function generateVerifier() {
  const array = new Uint32Array(28)
  window.crypto.getRandomValues(array)

  return Array.from(array, (item) => `0${item.toString(16)}`.substr(-2)).join(
    ''
  )
}

挑戰

代碼質詢對代碼驗證器執行以下轉換:

因此,驗證者作為參數傳遞給挑戰函數並進行轉換。這是對隨機驗證字符串進行散列和編碼的函數:

async function generateChallenge(verifier) {
  function sha256(plain) {
    const encoder = new TextEncoder()
    const data = encoder.encode(plain)

    return window.crypto.subtle.digest('SHA-256', data)
  }

  function base64URLEncode(string) {
    return btoa(String.fromCharCode.apply(null, new Uint8Array(string)))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+\$/, '')
  }

  const hashed = await sha256(verifier)

  return base64URLEncode(hashed)
}

構建端點

現在您可以獲取所有需要的參數,生成驗證者和質詢,將驗證者設置為本地存儲,並將用戶重定向到認證服務器的登錄頁面。

async function buildAuthorizeEndpointAndRedirect() {
  const host = 'https://auth-server.example.com/oauth/authorize'
  const clientId = 'abc123'
  const redirectUri = 'https://my-app-host.example.com/callback'
  const scope = 'specific,scopes,for,app'
  const verifier = generateVerifier()
  const challenge = await generateChallenge(verifier)

  // Build endpoint
  const endpoint = `${host}?
    response_type=code&
    client_id=${clientId}&
    scope=${scope}&
    redirect_uri=${redirectUri}&
    code_challenge=${challenge}&
    code_challenge_method=S256`

  // Set verifier to local storage
  localStorage.setItem('verifier', verifier)

  // Redirect to authentication server's login page
  window.location = endpoint
}

在什麼時候調用這個函數取決於你——它可能會在點擊按鈕時發生,或者如果用戶在登陸應用程序時被認為沒有經過身份驗證,它會自動發生。在 React 應用程序中,它可能位於 useEffect() 中 .

useEffect(() => {
  buildAuthorizeEndpointAndRedirect()
}, [])

現在用戶將在身份驗證服務器的登錄頁面上,通過用戶名和密碼成功登錄後,他們將被重定向到 redirect_uri 從第一步開始。

POST /token 端點

第二步是檢索令牌。這是傳統授權代碼流程中通常在服務器端完成的部分,但對於 PKCE,它也是通過前端完成的。當授權服務器重定向回你的回調 URI 時,它會附帶一個 code 在查詢字符串中,您可以將其與驗證器字符串一起交換為最終的 token .

POST 令牌請求必須作為 x-www-form-urlencoded 請求。

標題 說明
Content-Type application/x-www-form-urlencoded
參數 說明
grant_type authorization_code
client_id 您的客戶 ID
code_verifier 您的代碼驗證器
redirect_uri 與步驟 1 相同的重定向 URI
code 代碼查詢參數
async function getToken(verifier) {
  const host = 'https://auth-server.example.com/oauth/token'
  const clientId = 'abc123'
  const redirectUri = `https://my-app-server.example.com/callback`

  // Get code from query params
  const urlParams = new URLSearchParams(window.location.search)
  const code = urlParams.get('code')

  // Build params to send to token endpoint
  const params = `client_id=${clientId}&
    grant_type=${grantType}&
    code_verifier=${verifier}&
    redirect_uri=${redirectUri}&
    code=${code}`

  // Make a POST request
  try {
    const response = await fetch(host, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: params,
    })
    const data = await response.json()

    // Token
    console.log(data)
  } catch (e) {
    console.log(e)
  }
}

獲得令牌後,應立即從 localStorage 中刪除驗證者 .

const response = await getToken(localStorage.getItem('verifier'))
localStorage.removeItem('verifier')

在存儲令牌時,如果您的應用程序真正只是前端,則選項是使用 localStorage .如果可以選擇使用服務器,則可以使用後端用於前端 (BFF) 來處理身份驗證。我推薦閱讀 A Critical Analysis of Refresh Token Rotation in Single-page Applications。

結論

到這裡就可以了 - 使用 PKCE 進行身份驗證的兩個步驟。首先,為 /authorize 建立一個 URL 在授權服務器上並將用戶重定向到它,然後 POST 到 /token 重定向上的端點。 PKCE 目前是我所知道的最安全的身份驗證系統,適用於僅前端的 Web 或移動應用程序。希望這可以幫助您理解並在您的應用中實現 PKCE!


Tutorial JavaScript 教程
  1. 童話故事和不變性的含義

  2. 如何使用 HTML、CSS 和 JavaScript 構建模態彈出框

  3. Raspberry Pi 和 Arduino 的注意事項

  4. 為什麼你永遠不應該在 Jest 中使用 .toBe

  5. JavaScript ES6+

  6. 如何在 JavaScript 中創建按鈕

  7. PowerShell 通用儀表板:製作交互式儀表板

  1. 如何防止正則表達式拒絕服務 (ReDoS) 攻擊

  2. SOLID 原則 #4:接口隔離(JavaScript)

  3. 💅 Styled-Components:擴展子組件

  4. 考慮為 dev.to 使用 PreactJs

  5. 如何使用 TypeScript 輕鬆修改 Minecraft

  6. 使用 Apollo Server 將文件上傳到 S3 對象存儲(或 MinIo)

  7. Defresh - 使用 1 個 <script> 標籤將您網站的鏈接加載速度縮短一半

  1. JavaScript 中的階乘函數(帶遞歸)

  2. 在生產環境中刪除 JS 控制台日誌的簡單方法

  3. 介紹反應pt。 1

  4. Quotlify,一個 React/Redux 示例項目