cover

概念概覽

sequenceDiagram
    participant User as 使用者
    participant App as 應用程式 (Client)
    participant Auth as 授權伺服器
    participant API as 資源伺服器

    User->>App: 1. 點擊「使用 Google 登入」
    App->>Auth: 2. 重導到授權頁面
    Auth->>User: 3. 請求使用者授權
    User->>Auth: 4. 同意授權
    Auth->>App: 5. 回傳 Authorization Code
    App->>Auth: 6. 用 Code 換取 Access Token
    Auth->>App: 7. 回傳 Access Token
    App->>API: 8. 帶 Token 存取資源
    API->>App: 9. 回傳受保護的資料

什麼是 OAuth 2.0?

你有沒有用過「使用 Google 帳號登入」或「使用 Facebook 登入」?有的話,你就已經用過 OAuth 了。那個跳出來問你「是否允許 XXX 存取你的帳號資料」的畫面,背後跑的就是 OAuth 流程。它解決了一個很實際的問題:怎麼讓第三方 App 拿到你的資料,但又不用把密碼交出去?

OAuth 2.0 是一個授權框架(Authorization Framework),它允許第三方應用程式在取得使用者同意後,代表使用者存取特定資源,而不需要知道使用者的密碼

OAuth 解決的問題

假設你開發了一個相片編輯 App,想讓使用者能存取他們的 Google 相簿。傳統做法:

不安全的方式:要求使用者提供 Google 帳號密碼

  • 使用者必須信任你的 App
  • App 可以存取使用者的所有 Google 服務
  • 密碼洩漏風險高

OAuth 方式:引導使用者到 Google 授權頁面

  • 使用者只授權「讀取相簿」權限
  • App 不會知道使用者密碼
  • 使用者可以隨時撤銷授權

好,所以 OAuth 的核心精神就是:「我可以讓你用我的東西,但我不會把鑰匙給你。」這個概念清楚了,接下來看看整個流程裡有哪些角色在互動。

OAuth 2.0 的角色

角色說明範例
Resource Owner資源擁有者(使用者)
Client請求存取資源的應用程式相片編輯 App
Authorization Server驗證身份並發放 TokenGoogle OAuth
Resource Server存放受保護資源的伺服器Google Photos API

理解了角色之後,接下來看看這些角色是怎麼互動的。下面的流程就是你每次點「用 Google 登入」時,背後實際發生的事情。

授權流程(Authorization Code Flow)

這是最常用且最安全的 OAuth 流程。你可以想像成去超商取包裹:你先出示身分證(Authorization Code),店員確認身份後才把包裹(Access Token)給你。重點是你不會直接在大街上拿到包裹,而是在櫃台後面完成的——這就是為什麼 Code 要在後端換成 Token。

┌─────────────────────────────────────────────────────────┐
│                                                          │
│  1. 使用者點擊「使用 Google 登入」                        │
│     ↓                                                    │
│  2. 重導到 Google 授權頁面                               │
│     ↓                                                    │
│  3. 使用者同意授權                                       │
│     ↓                                                    │
│  4. Google 回傳 Authorization Code 給 App               │
│     ↓                                                    │
│  5. App 用 Code 換取 Access Token(後端進行)            │
│     ↓                                                    │
│  6. App 使用 Access Token 呼叫 API                      │
│                                                          │
└─────────────────────────────────────────────────────────┘

實作範例

Step 1: 產生授權連結

const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'https://www.googleapis.com/auth/photoslibrary.readonly');
authUrl.searchParams.set('state', generateRandomState()); // 防止 CSRF
 
// 導向授權頁面
window.location.href = authUrl.toString();

Step 2: 處理回調並換取 Token

// 後端 /callback endpoint
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;
 
  // 驗證 state 防止 CSRF
  if (state !== req.session.oauthState) {
    return res.status(400).send('Invalid state');
  }
 
  // 用 authorization code 換取 access token
  const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      code,
      client_id: process.env.GOOGLE_CLIENT_ID,
      client_secret: process.env.GOOGLE_CLIENT_SECRET,
      redirect_uri: 'https://yourapp.com/callback',
      grant_type: 'authorization_code',
    }),
  });
 
  const { access_token, refresh_token } = await tokenResponse.json();
 
  // 儲存 token,使用者登入成功
  req.session.accessToken = access_token;
  res.redirect('/dashboard');
});

Step 3: 使用 Access Token 呼叫 API

const response = await fetch('https://photoslibrary.googleapis.com/v1/albums', {
  headers: {
    'Authorization': `Bearer ${accessToken}`,
  },
});
 
const albums = await response.json();

OK,到這邊你已經知道怎麼拿到 Token 了。但你有沒有注意到,上面的 response 裡其實回來了兩種 Token?它們的用途完全不一樣,搞混的話會踩坑,所以我們來看清楚。

Token 類型

Access Token

  • 用於存取受保護資源
  • 有效期短(通常 1 小時)
  • 每次 API 請求都需要帶上

Refresh Token

  • 用於取得新的 Access Token
  • 有效期長
  • 需安全儲存在後端

白話來說,Access Token 就像遊樂園的日票,過了今天就作廢;Refresh Token 則像季票,日票過期的時候拿季票去櫃台換一張新的就好,不用重新排隊買票(也就是不用再請使用者重新授權)。

// 當 Access Token 過期時,使用 Refresh Token 更新
async function refreshAccessToken(refreshToken) {
  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      refresh_token: refreshToken,
      client_id: process.env.GOOGLE_CLIENT_ID,
      client_secret: process.env.GOOGLE_CLIENT_SECRET,
      grant_type: 'refresh_token',
    }),
  });
 
  return response.json();
}

到這裡你可能會想:「等等,那我平常用的『Google 登入』到底是 OAuth 還是其他東西?」好問題。OAuth 負責的是「授權」,但「登入」其實牽涉到「認證」,這兩件事不一樣。這就要提到 OpenID Connect 了。

OAuth 2.0 vs OpenID Connect

項目OAuth 2.0OpenID Connect
目的授權(Authorization)身份驗證(Authentication)
回傳Access TokenAccess Token + ID Token
用途存取資源確認使用者身份

OpenID Connect 是建立在 OAuth 2.0 之上的身份層,當你需要「使用者是誰」的資訊時使用。

理解了原理之後,最後也是最重要的:安全性。OAuth 的設計初衷就是為了安全,但如果實作的時候不注意細節,反而會開後門。以下是幾個一定要記住的重點。

安全注意事項

  1. 永遠使用 HTTPS
  2. 驗證 state 參數:防止 CSRF 攻擊
  3. 保護 Client Secret:永遠不要暴露在前端
  4. 最小權限原則:只請求需要的 scope(想像成你把車交給代客泊車,你只會給他車鑰匙,不會連家裡的鑰匙一起給吧?scope 就是這個概念)
  5. 安全儲存 Token:後端儲存,不放 localStorage

常見的誤解

Q: OAuth 是認證(Authentication)嗎? 不是。OAuth 是授權(Authorization),解決的是「你能做什麼」的問題。至於「你是誰」,那是 OpenID Connect 的工作。很多人會搞混,因為「用 Google 登入」背後同時用了 OAuth + OIDC,但它們是不同層的東西。

Q: Access Token 存在前端安全嗎? 看你怎麼存。放 localStorage 的話,任何跑在同一個頁面的 JavaScript 都能拿到,有 XSS 風險。比較安全的做法是放在 httpOnly cookie 裡,這樣 JavaScript 碰不到它。

Q: 為什麼不直接回傳 Token,還要先拿 Authorization Code 再換? 因為 redirect URL 上帶的東西使用者是看得到的(就在瀏覽器網址列上)。如果直接把 Token 放在 URL 裡,等於公開展示你的通行證。先給一個短命的 Code,再由後端私下拿 Code 去換 Token,Token 就不會暴露在瀏覽器端了。

一句話總結

OAuth 就是:「你想看我的相簿?好,但你只能看照片,不能刪照片,而且這個許可 1 小時後就失效。」


延伸閱讀

OAuth 2.0 官方規範 (RFC 6749) Google OAuth 2.0 文件 Auth0 OAuth 2.0 教學 API 概念 JWT Token