cover

OAuth 的核心精神就是:「我可以讓你用我的東西,但我不會把鑰匙給你。」

先講結論

OAuth 2.0 是授權框架,不是認證框架。它解決的問題是:怎麼讓第三方 App 拿到你的資料,又不用把密碼交出去。你每次點「使用 Google 登入」,背後跑的就是 OAuth 流程。

為什麼需要 OAuth?

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

沒有 OAuth 的做法:要求使用者提供 Google 帳號密碼。然後你的 App 就能存取使用者的所有 Google 服務——Gmail、日曆、雲端硬碟、付款資訊。使用者必須完全信任你,而你只要被駭一次,所有使用者的 Google 帳號就外洩了。

有 OAuth 的做法:引導使用者到 Google 授權頁面,使用者只授權「讀取相簿」權限。你的 App 不會知道密碼,使用者隨時可以撤銷授權。

你會把車交給代客泊車,但你不會連家裡的鑰匙一起給吧?OAuth 的 scope 就是這個概念。

授權流程怎麼跑

最常用的是 Authorization Code Flow,你可以想成去超商取包裹:先出示身分證(Authorization Code),店員確認後才把包裹(Access Token)給你。

Step 1:產生授權連結,導向 Google

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', 'photoslibrary.readonly');
authUrl.searchParams.set('state', generateRandomState()); // 防 CSRF

Step 2:後端用 Code 換 Token

app.get('/callback', async (req, res) => {
  const { code, state } = req.query;
  if (state !== req.session.oauthState) return res.status(400).send('Invalid state');
 
  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();
  req.session.accessToken = access_token;
  res.redirect('/dashboard');
});

Step 3:用 Token 打 API

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

為什麼不直接回傳 Token,要先拿 Code 再換?因為 redirect URL 上帶的東西使用者看得到(在瀏覽器網址列上)。先給短命的 Code,再由後端私下換 Token,Token 就不會暴露在瀏覽器端。

Access Token vs Refresh Token

Access Token 就像遊樂園的日票——短命(通常 1 小時),過期就作廢。

Refresh Token 就像季票——日票過期了拿季票去櫃台換新的,不用重新排隊買票(不用請使用者重新授權)。

async function refreshAccessToken(refreshToken) {
  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    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();
}

OAuth 2.0 vs OpenID Connect

OAuth 2.0OpenID Connect
目的授權(你能做什麼)認證(你是誰)
回傳Access TokenAccess Token + ID Token

你平常用的「Google 登入」其實同時用了 OAuth + OpenID Connect。OAuth 負責「讓 App 存取你的資料」,OIDC 負責「確認你就是那個 Google 帳號的主人」。搞混這兩個的人不少,因為使用者感受起來是同一個動作。

安全注意事項

這部分不能馬虎,實作錯了比沒有 OAuth 更危險:

  • 永遠用 HTTPS——Token 在 HTTP 上等於裸奔
  • 驗證 state 參數——防 CSRF 攻擊
  • Client Secret 不能出現在前端——它叫 Secret 是有原因的
  • Token 存後端,不放 localStorage——localStorage 對 XSS 完全沒有防禦力
  • scope 最小化——只要相簿就只請求相簿,不要貪心請求所有權限

常見誤解

「OAuth 就是登入嘛」——不是。OAuth 是授權,OIDC 才是認證。很多人搞混是因為「Google 登入」把兩件事包在一起了。

「Token 放 localStorage 很方便」——方便是方便,但任何跑在同一個頁面的 JavaScript 都能拿到。有 XSS 就等於 Token 外洩。放 httpOnly cookie 比較安全。

「Access Token 過期了就要使用者重新登入」——不用,這就是 Refresh Token 存在的意義。


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


延伸閱讀

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