作为一名开发人员,身份验证是我最尊重的事情之一;根据我进行身份验证(也许是基础级别)的经验,我总是为一件事或另一件事而苦苦挣扎,尤其是当我必须集成 OAuth 时。
在为 jargons.dev 进行此工作之前,我最近一次进行 Auth 的经验是在 Hearts 上集成了 GitHub OAuth。
所以是的!我在为 jargons.dev 做这件事时也遇到了(传统的?)困难;但老实说,这只是因为设置(即技术)方面的差异 - 我在 Hearts 上的经验是将 GitHub OAuth 与 NextJS 中的服务器操作集成,同时在 jargons.dev 上,我正在将 GitHub OAuth 与 Astro 集成。
正如我目前所写,身份验证系统已经经历了 3 次迭代,并计划进行更多迭代(下一次迭代的详细信息请参见本期#30);由于一些未发现的限制,这些几周的开发迭代已经实现了改进或重构了一两件事。
此迭代在基本身份验证功能中实现,允许启动 GitHub OAuth 流程,响应处理将身份验证代码交换为我们安全存储在用户 cookie 上的 accessToken。
本次迭代中值得一提的重要变化是
我实现了第一个操作(与新的实验性 Astro 服务器操作无关,我在宣布之前很久就这样做了?) - 这是我刚刚编造的一个术语,用于调用在服务器端运行的函数Astro“在页面加载之前”,你应该知道它的命名约定:doAction,以及它以 astroGlobal 对象作为唯一参数的风格,通常是返回响应对象的异步函数。
doAuth - 此操作集成在我希望保护的任何页面上,它检查 cookie 中是否存在访问令牌; — 如果存在:它将其交换为用户数据,并返回一个布尔值 isAuthed 以确认受保护页面的身份验证; — 如果未找到令牌:它会检查 url 搜索参数中是否存在 oath 流授权代码,将其交换为访问令牌(通过调用 api/github/oauth/authorize 路由)并将其安全保存到 cookie,然后使用适当地设置cookie;现在,如果 cookie 中没有找到 accessToken 并且 url 搜索参数中没有授权码,则返回值 isAuthed 为 false,并将在受保护页面上使用它来重定向到登录页面。
const { isAuthed, authedData: userData } = await doAuth(Astro); if (!isAuthed) return redirect(`/login?return_to=${pathname}`);
This Pull request implement the authentication feature in the project; using the github oauth, our primary goal is to get and hold users github accessToken in cookies for performing specific functionality. It is important to state that this feature does not take store this user's accessToken to any remote server, this token and any other information that was retrieved using the token are all saved securely on the users' end through usage of cookies.
Implemented the github oauth callback handler at /api/github/oauth/callback - this handler's main functionality is to receive github's authorization code and state to perform either of the following operations
Implemented the github oauth authorization handler at /api/github/oauth/authorization - this handler is a helper that primarily exchanges the authorization code for tokens and returns it in a json object.
Created a singleton instance of our github app at lib/octokit/app
Added a new crypto util function which provides encrypt and decrypt helper function has exports; it is intended to be used for securing the users related cookies
Implemented the doAuth action function - this function take the Astro global object as argument and performs the operations stated below
/**
* Authentication action with GitHub OAuth
* @param {import("astro").AstroGlobal} astroGlobal
*/
export default async function doAuth(astroGlobal) {
const { url: { searchParams }, cookies } = astroGlobal;
const code = searchParams.get("code");
const accessToken = cookies.get("jargons.dev:token", {
decode: value => decrypt(value)
});
/**
* Generate OAuth Url to start authorization flow
* @todo make the `parsedState` data more predictable (order by path, redirect)
* @todo improvement: store `state` in cookie for later retrieval in `github/oauth/callback` handler for cleaner url
* @param {{ path?: string, redirect?: boolean }} state
*/
function getAuthUrl(state) {
const parsedState = String(Object.keys(state).map(key => key + ":" + state[key]).join("|"));
const { url } = app.oauth.getWebFlowAuthorizationUrl({
state: parsedState
});
return url;
}
try {
if (!accessToken && code) {
const response = await GET(astroGlobal);
const responseData = await response.json();
if (responseData.accessToken && responseData.refreshToken) {
cookies.set("jargons.dev:token", responseData.accessToken, {
expires: resolveCookieExpiryDate(responseData.expiresIn),
encode: value => encrypt(value)
});
cookies.set("jargons.dev:refresh-token", responseData.refreshToken, {
expires: resolveCookieExpiryDate(responseData.refreshTokenExpiresIn),
encode: value => encrypt(value)
});
}
}
const userOctokit = await app.oauth.getUserOctokit({ token: accessToken.value });
const { data } = await userOctokit.request("GET /user");
return {
getAuthUrl,
isAuthed: true,
authedData: data
}
} catch (error) {
return {
getAuthUrl,
isAuthed: false,
authedData: null
}
}
}
Added the login page which stands as place where where unauthorised users witll be redirected to; this page integrates the doAuth action, destruing out the getAuthUrl helper and the isAuthed property, it uses them as follows
const { getAuthUrl, isAuthed } = await doAuth(Astro);
if (isAuthed) return redirect(searchParams.get("redirect"));
const authUrl = getAuthUrl({
path: searchParams.get("redirect"),
redirect: true
});
// pages/sandbox.astro
---
import BaseLayout from "../layouts/base.astro";
import doAuth from "../lib/actions/do-auth.js";
import { $userData } from "../stores/user.js";
const { url: { pathname }, redirect } = Astro;
const { isAuthed, authedData } = await doAuth(Astro);
if (!isAuthed) return redirect(`/login?redirect=<span class="pl-s1"><span class="pl-kos">${pathname}</span>`</span>);
$userData.set(authedData);
---
11cef86d50b82c68655376cb62f0db5b
e33cc9d786368fb494b726688270e59a
713e9867a88cfb8c49f3f463c33c78a3
3f1d768d1c2bece0763d63bdace7515a
e388a4556c0f65e1904146cc1a846beeHello, { authedData.login }94b3e26ee717c64999d7867364b1b4a3
16b28748ea4df4d9c2150843fecfba68
07ebbc2b3735b6cb0330715d08d2749e
a7404faa8989ab176c7e812b70808e66
Explainer
screencast-bpconcjcammlapcogcnnelfmaeghhagj-2024.03.29-20_36_15.webm
This iteration implements improvements by making making the parsedState derived from the getAuthUrl function call more predictable removing the chances of an error in the api/github/oauth/callback route; it also renames some terms used in the search params and implements the the encodeURIComponent to make our redirect urls look less weird
See PR:
This PR implements some improvement to mark the second iteration of the auth feature in the project. Follow-up to #8
function getAuthUrl(state) {
let parsedState = "";
if (!isObjectEmpty(state)){
if (state.path) parsedState += `path:<span class="pl-s1"><span class="pl-kos">${state.path}</span>`</span>;
const otherStates = String(Object.keys(state)
.filter(key => key !== "path" && key !== "redirect")
.map(key => key + ":" + state[key]).join("|"));
if (otherStates.length > 0) parsedState += `|<span class="pl-s1"><span class="pl-kos">${otherStates}</span>`</span>;
}
const { url } = app.oauth.getWebFlowAuthorizationUrl({
state: parsedState
});
return url;
}
Resolves #15
这次迭代重构了“第一次迭代”中实现的大部分部分,因为我在另一个脚本上工作时出现了一定的限制。
此时我正在编写“Submit Word”脚本;此脚本利用 GitHub API 并创建拉取请求,以将从当前经过身份验证的用户的 fork 分支所做的更改合并到基础 (jargons.dev) 主分支。当然,这是通过保存到 cookie 中的用户访问令牌实现的,该令牌在请求标头中由 SDK(即 Octokit)用作“授权承载令牌”,方便我们与 GitHub API 进行交互。
在测试期间,当我尝试提交单词脚本时,我遇到了错误......
错误:集成无法访问资源
...这很快就成为了一个障碍,我咨询了@gr2m,我们很快发现了与我的 GitHub 应用程序集成相关的限制。
正如最初所述,GitHub 应用程序使用带有细粒度令牌的“权限” - GitHub 出于一些非常好的原因而鼓励使用的新令牌类型,下面引用的一个是我们在这里关注的......
GitHub 应用程序提供了对应用程序功能的更多控制。 GitHub 应用程序使用细粒度的权限,而不是 OAuth 应用程序使用的广泛范围。例如,如果您的应用程序需要读取存储库的内容,则 OAuth 应用程序将需要存储库范围,这也允许应用程序编辑存储库内容和设置。 GitHub 应用程序可以请求对存储库内容的只读访问权限,这不会让应用程序执行更多特权操作,例如编辑存储库内容或设置。
...这意味着当使用“权限”(即细粒度权限)时,用户必须具有对上游/基础存储库的写入权限,在本例中是我们的 jargons.dev 存储库;如 GitHub 创建拉取请求文档中所述。
说什么!?没有!!!
就在那时,我们发现普通的旧作用域正是我们所需要的;为了能够访问所需的资源,public_repo 范围就是一切。
为了继续前进,我必须从“权限”切换到“范围”,我们在 GitHub 的“OAuth App”中发现了这一点;这是第三次迭代修补的基础。
因此,本次迭代主要关注于交换 GitHub OAuth 集成,同时确保本次迭代中实现的帮助程序/函数/api 与 GitHub 应用程序提供的类似,以减少我要做的更改量跨越整个代码库以感谢新的实现。
GitHub 应用程序很棒,我必须承认,如果我们最终找到错误的解决方案:资源无法通过集成错误访问,但创建拉取请求的功能已执行,我仍然会考虑到未来提交单词脚本是该项目的重要组成部分,因此我们必须确保它有效。
需要指出的是,为了支持功能,我必须做出一些权衡......
解决方法
查看公关:
This Pull request refactors the authentication system, replacing the usage of github-app-oauth with classic github oauth app. This decision was taken because of the limitations discovered using the Pull Request endpoint (implementation in #25); the github-app-oauth uses permissions which requires a user to have write access to the upstream (i.e. write access to atleast pull-requests on our/this project repo) before a pull request can created from their forked repo branch to the main project repo.
This PR goes to implement classis oauth app, which uses scopes and allows user access to create the pull request to upstream repo on the public_repo scope. The changes made in this PR was done to mimic the normal Octokit.App's methods/apis as close as possible to allow compatibility with the implementation in #8 and #28 (or for cases when we revert back to using the github-app-oauth in the future --- maybe we end up finding a solution because honestly I really prefer the github-app-oauth ?).
It is also important to state that this oauth app option doesn't offer a short lived token (hence we only have an accessToken without expiry and No refreshToken), but I have configured the token to expire out of cookie in 8hours; even though we might be getting exactly thesame token back from github after this expires and we re-authorize the flow, I just kinda like that feeling of the cookies expiring after some hours and asking user to re-auth.
octokit - the main octokit instance of the oauth app
/**
* OAuth App's Octokit instance
*/
const octokit = new Octokit({
authStrategy: createOAuthAppAuth,
auth: {
clientId: import.meta.env.GITHUB_OAUTH_APP_CLIENT_ID,
clientSecret: import.meta.env.GITHUB_OAUTH_APP_CLIENT_SECRET
},
});
oauth
getWebFlowAuthorizationUrl - method that generates the oauth flow url
/**
* Generate a Web Flow/OAuth authorization url to start an OAuth flow
* @param {import("@octokit/oauth-authorization-url").OAuthAppOptions} options
* @returns
*/
function getWebFlowAuthorizationUrl({state, scopes = ["public_repo"], ...options }) {
return oauthAuthorizationUrl({
clientId: import.meta.env.GITHUB_OAUTH_APP_CLIENT_ID,
state,
scopes,
...options
});
}
exchangeWebFlowCode - method that exchanges oauth web flow returned code for accessToken; this functionality was extracted from the github/oauth/authorize endpoint to have all auth related function packed in one place
/**
* Exchange Web Flow Authorization `code` for an `access_token`
* @param {string} code
* @returns {Promise4ae3230490577696c575abb709ab983c}
*/
async function exchangeWebFlowCode(code) {
const queryParams = new URLSearchParams();
queryParams.append("code", code);
queryParams.append("client_id", import.meta.env.GITHUB_OAUTH_APP_CLIENT_ID);
queryParams.append("client_secret", import.meta.env.GITHUB_OAUTH_APP_CLIENT_SECRET);
const response = await fetch("https://github.com/login/oauth/access_token", {
method: "POST",
body: queryParams
});
const responseText = await response.text();
const responseData = new URLSearchParams(responseText);
return responseData;
}
getUserOctokit - method that gets an octokit instance of a user.
/**
* Get a User's Octokit instance
* @param {Omit1e4fb368bde7d280f14ef2719604d1ea & { token: string }} options
* @returns {Octokit}
*/
function getUserOctokit({ token, ...options }) {
return new Octokit({
auth: token,
...options
});
};
?
screencast-bpconcjcammlapcogcnnelfmaeghhagj-2024.04.07-07_37_31.webm
以上是构建 jargons.dev [# 身份验证系统的详细内容。更多信息请关注PHP中文网其他相关文章!