Appearance
具体实现
- 路由:
/: 项目首页- 用户请求到达 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>
`);
});- 路由:
/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);
});- 路由:
/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>
`);
});- 路由:
/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);
}
});- 路由:
/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('/');
});