Skip to content

具体实现

  1. 路由/ : 项目首页
    • 用户请求到达 Worker,检查认证 Cookie。
    • 如果未认证,显示 /login 链接。
    • 如果认证,利用Token 调用Google Userinfo API。
ts
/**
 * Handles the homepage route.
 * Checks the authentication status and either displays user info or a login link.
 */
app.get('/', async (c) => {
  const storedState = getCookie(c, 'oauth_state');
  
  if (storedState) {
    const tokenDataJson = await c.env.authTokens.get(storedState);
    if (tokenDataJson) {
      const tokenData: TokenData = JSON.parse(tokenDataJson);
      
      try {
        const userInfo = await fetchUserInfo(tokenData.access_token);
        
        return c.html(`
          <html>
            <body>
              <h1>User Info</h1>
              <pre>${JSON.stringify(userInfo, null, 2)}</pre>
              <a href="/logout">Logout</a>
            </body>
          </html>
        `);
      } catch (error) {
        return c.text('Failed to fetch user info', 500);
      }
    }
  }
  
  return c.html(`
    <html>
      <body>
        <h1>Welcome to the OAuth Demo App</h1>
        <a href="/login">Login with Google</a>
      </body>
    </html>
  `);
});
  1. 路由/login :登录页
    • 用户点击 /login 链接,重定向到 Google 的 OAuth2 授权 URL,请求包含 access_type=offline 参数。
ts
/**
 * Handles the /login route.
 * If the user is already authenticated, refresh the token if necessary.
 * Otherwise, redirect the user to the Google OAuth2 login page.
 */
app.get('/login', async (c) => {
  const storedState = getCookie(c, 'oauth_state');
  
  if (storedState) {
    const tokenDataJson = await c.env.authTokens.get(storedState);
    if (tokenDataJson) {
      const tokenData: TokenData = JSON.parse(tokenDataJson);
      if (tokenData.refresh_token) {
        /**
         * Represents the response from the Google OAuth token refresh request.
         */
        const refreshResponse = await fetch('https://oauth2.googleapis.com/token', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          },
          body: new URLSearchParams({
            client_id: c.env.CLIENT_ID,
            client_secret: c.env.CLIENT_SECRET,
            refresh_token: tokenData.refresh_token,
            grant_type: 'refresh_token'
          })
        });

        const newTokenData: TokenData = await refreshResponse.json();
        if (newTokenData.error) {
          return c.text(`Error: ${newTokenData.error}`, 400);
        }

        await c.env.authTokens.put(storedState, JSON.stringify(newTokenData), { expirationTtl: newTokenData.expires_in });
        setCookie(c, 'oauth_state', storedState, { httpOnly: true, secure: true, sameSite: 'Lax', maxAge: newTokenData.expires_in });
        return c.redirect('/userinfo');
      }
    }
  }

  const state = nanoid();
  const redirectUri = encodeURIComponent(c.env.REDIRECT_URI);
  const clientId = c.env.CLIENT_ID;
  const scope = encodeURIComponent('https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email');
  
  const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}&access_type=offline`;
  
  setCookie(c, 'oauth_state', state, { httpOnly: true, secure: true, sameSite: 'Lax' });
  
  return c.redirect(authUrl);
});
  1. 路由/auth:授权码交换令牌
    • Worker 接收到授权码后,与 Google API 交换获取访问令牌和刷新令牌。
    • 存储令牌信息并设置认证 Cookie,然后显示登录成功, 等待10秒后重定向回 /userinfo
ts
/**
 * Handles the /auth route.
 * Exchanges the authorization code for an access token and refresh token.
 * Stores the tokens in KV and sets a cookie.
 */
app.get('/auth', async (c) => {
  const query = c.req.query();
  const code = decodeURIComponent(query.code as string);
  const state = decodeURIComponent(query.state as string);
  
  const storedState = getCookie(c, 'oauth_state');
  
  if (state !== storedState) {
    return c.text('Invalid state parameter', 400);
  }

  let tokenData: TokenData;
  try {
    const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        code: code as string,
        client_id: c.env.CLIENT_ID,
        client_secret: c.env.CLIENT_SECRET,
        redirect_uri: c.env.REDIRECT_URI,
        grant_type: 'authorization_code'
      })
    });
    
    tokenData = await tokenResponse.json();
    if (tokenData.error) {
      return c.text(`Error: ${tokenData.error}`, 400);
    }
  } catch (error) {
    return c.text('Failed to fetch or parse token response', 500);
  }

  await c.env.authTokens.put(storedState, JSON.stringify(tokenData), { expirationTtl: tokenData.expires_in });

  return c.html(`
    <html>
      <head>
        <meta http-equiv="refresh" content="10;url=/userinfo">
      </head>
      <body>
        <h1>Authentication successful</h1>
        <p>Redirecting to user info in <span id="countdown">10</span> seconds...</p>
        <script>
          let countdown = 10;
          const countdownElement = document.getElementById('countdown');
          setInterval(() => {
            countdown--;
            if (countdownElement) {
              countdownElement.textContent = countdown.toString();
            }
          }, 1000);
        </script>
      </body>
    </html>
  `);
});
  1. 路由/userinfo: 用户信息页
    • 检查认证 Cookie,如果存在有效令牌,则处理用户请求,如获取用户信息。
ts
/**
 * Handles the /userinfo route.
 * Retrieves and returns user information if the user is authenticated.
 */
app.get('/userinfo', async (c) => {
  const storedState = getCookie(c, 'oauth_state');
  if (!storedState) {
    return c.text('Not authenticated', 401);
  }

  const tokenDataJson = await c.env.authTokens.get(storedState);
  if (!tokenDataJson) {
    return c.text('Not authenticated', 401);
  }

  const tokenData: TokenData = JSON.parse(tokenDataJson);
  
  try {
    const userInfo = await fetchUserInfo(tokenData.access_token);
    return c.json(userInfo, {
      headers: {
        'Content-Type': 'application/json'
      }
    });
  } catch (error) {
    return c.text('Failed to fetch user info', 500);
  }
});
  1. 路由/logout:撤销令牌并清除 Cookie
    • 撤销令牌,删除 KV 中的令牌数据,并清除认证 Cookie。
ts
/**
 * Handles the /logout route.
 * Revokes the token, deletes the token data from KV, and clears the authentication cookie.
 */
app.get('/logout', async (c) => {
  const storedState = getCookie(c, 'oauth_state');
  if (storedState) {
    const tokenDataJson = await c.env.authTokens.get(storedState);
    if (tokenDataJson) {
      const tokenData: TokenData = JSON.parse(tokenDataJson);
      await revokeToken(tokenData.access_token);
      await c.env.authTokens.delete(storedState);
    }
  }
  deleteCookie(c, 'oauth_state', { path: '/', secure: true, sameSite: 'Lax' });
  return c.redirect('/');
});