都 2023 年了,OAuth 为什么还是让人头疼?

【编者按】这篇文章介绍了 OAuth 的实践中的问题,如:OAuth 标准过于庞大和复杂、每个人的 OAuth 都有细微的不同、许多 API 在 OAuth 中添加了非标准的扩展、 调试 OAuth 很难、在 API 之上构建应用需要繁琐的审批、OAuth 存在安全性问题等。作者构建的一个开源服务 Nango,它旨在简化 OAuth 的流程和提高安全性,适用于多种 API,来解决这些问题。
链接:https://www.nango.dev/blog/why-is-oauth-still-hard
未经允许,禁止转载!
作者 | Robin Guldener       译者 | 明明如月
责编 | 夏萌
出品 | CSDN(ID:CSDNnews)
我们为 50 个最受欢迎的 API 实现了 OAuth。结果:一言难尽。
图片
图源:nango.dev
OAuth 是一个标准协议,它支持 OAuth 2.0 的客户端库已经存在,几乎适用于你能想象到的所有编程语言。你可能因此会得出结论,有了客户端库,你应该能在大约 10 分钟或至少一小时内实现任何 API 的 OAuth。然而,理想很丰满,现实很骨感。很难想象一个人能够在 10 分钟或一小时内实现任何 API 的 OAuth。
实践中的 OAuth
我们针对 50 种最受欢迎的 API 使用了OAuth,如 Google (Gmail、日历、表格等),HubSpot,Shopify,Salesforce,Stripe,Jira,Slack,Microsoft (Azure, Outlook, OneDrive),LinkedIn,Facebook 和其他使用OAuth 的 APIs。
我们的结论:现在 OAuth 的体验和 2008 年的 JavaScript 的浏览器 API  差不多。虽然大家普遍认同事情应该如何做,但实际上每个 API 都有自己对标准的解读,实现上的差异和特殊性,以及非标准行为和扩展。结果是:每个细节都有可能出现问题和错误。
到底问题出现在哪里,让我们深入研究一下!
问题1:OAuth 标准过于庞大和复杂
"这个 API 也使用 OAuth 2.0,我们几周前已经做过了。我明天就能搞定。"——实习生的最后一句话
OAuth 是一个庞大的标准。它由官方网站上的 17 个 RFC(定义标准的文档)共同构成。它们涵盖了从 OAuth 框架和 Bearer 令牌到威胁模型和私钥 JWT 的所有内容。
你可能会问:“所有这些 RFC 都和一个简单的第三方访问令牌授权 API 有关吗?”你说得对。让我们只关注那些可能与典型的 API 第三方访问用例相关的东西:
OAuth 标准:OAuth 2.0 现在是默认的,但一些 API 仍然使用 OAuth 1.0a(而且 2.1 就要来了)。一旦你知道你的 API 使用的是哪个,继续下一步:
授权类型:你需要authorization_code,client_credentials,还是device_code?它们分别是做什么的,你应该在什么时候使用它们?如果有疑问,就试试 authorization_code。
顺便说一下:刷新令牌也是一种授权类型,但它不是用来获取访问令牌,而是用来延长访问令牌的有效期。它们的工作方式是标准化的,但你如何首先请求它们却不是。稍后再说。
现在你已经准备好了你的请求,让我们看看官方 OAuth 参数;它们有 72 个,每个都有明确的含义和行为。你可以在这里查看它们。常见的例子有prompt,scope,audience,resource,assertion,和login_hint。然而,在我们的经验中,大多数 API 提供者似乎和你现在一样对这个列表一无所知,所以不用太担心。
如果你觉得这还是太复杂了,需要学习很多东西,我们倾向于同意你的看法。大多数构建公共 API 的团队似乎也同意这一点。他们没有遵循完整的 OAuth 2.0 标准,而只是根据他们的 API 用例选择性地实现了一些 OAuth 功能。这导致了文档中很长的页面,概述了 OAuth 对于这个特定 API 是如何工作的。但我们很难责怪他们;他们只是想要提供一个好的开发者体验(developer experience,DX),让他们的 API 更容易使用和理解,而不是遵循复杂的标准。他们可能认为 OAuth 2.0 标准太复杂或不适合他们的场景,所以他们只选择了一些他们认为有用的功能。虽然这可能是出于好心,但也可能导致混乱和不一致。
图片
Salesforce 的authorization_code OAuth流程。这个简单的10步过程的清晰视图有什么不好呢?
问题是,每个人对 OAuth 的理解都有不同的想法,所以你最终得到了很多不同的(子)实现。
问题 2:每个人的 OAuth 都有细微的不同
每个 API 都实现了不同的 OAuth 子集,这迫使你仔细阅读他们冗长的 OAuth 文档:
1、他们在授权调用中需要哪些参数?
对于 Jira,你必须设置 audience 参数来指定你要访问的 Jira 实例的 URL。Google 倾向于通过不同的 scope 来处理这个问题,但是非常关心 prompt 参数。与此同时,Microsoft 的某个人发现了 response_mode 参数,并要求你总是将它设置为 query。
Notion API 采取了一种激进的方法,摒弃了无处不在的 scope 参数。事实上,你甚至在他们的 API 文档中找不到“scope”这个词。Notion 称它们为capabilities,并且你在注册应用时设置它们。我们花了 30 分钟的困惑时间才明白发生了什么。他们为什么要重新发明这个轮子?
offline_access 的情况更糟:现在大多数 API 都会在一段时间后让访问令牌过期。要获得刷新令牌,你需要请求“offline_access”,这需要通过一个参数、一个 scope 或者你在注册 OAuth 应用时设置的东西来完成。询问你的 API 或 OAuth 医生以获取详细信息。
2、他们希望在令牌请求调用中看到什么?
一些 API,比如 Fitbit,坚持要在请求头中获取数据。大多数人真的希望它在正文中,编码为 x-www-url-form-encoded,除了少数几个,比如 Notion,它们更喜欢以 JSON 的形式获取。
有些人希望你用 Basic auth 来验证这个请求。许多人不在乎这个。但要小心,他们可能明天就改变主意了。
3、我应该把我的用户重定向到哪里去授权?
Shopify 和 Zendesk 有一种模式,每个用户都有一个子域名,比如 .myshopify.com。是的,这也包括 OAuth 授权页面,所以你最好在你的模型和前端代码中构建动态 URL。
Zoho Books 为不同地区的客户提供了不同的数据中心。希望他们记得他们的数据在哪里:要授权你的应用,你的美国客户应该访问 https://accounts.zoho.com,欧洲客户可以访问 https://accounts.zoho.eu,印度客户欢迎访问 https://accounts.zoho.in。列表还在继续。
4、但至少我可以选择我的回调 URL,对吧?
如果你输入 http://localhost:3003/callback 作为 Slack API 的回调,他们会友好地提醒你“出于安全考虑,请使用 https”。是的,也适用于 localhost。幸运的是 有解决方案可以在 localhost 上进行 OAuth 重定向。
我们可以继续说很久,但我们想你现在应该明白了。
图片
OAuth 太复杂了;让我们做一个更简单的 OAuth 版本,它有我们需要的一切!©XKCD
问题 3:许多 API 在 OAuth 中添加了非标准的扩展
尽管 OAuth 标准很全面,但许多 API 仍然发现它有一些功能缺失。我们遇到的一个常见问题是,除了 access_token 之外,你还需要一些数据才能与 API 交互。这些额外的数据如果可以在 OAuth 流程中与 access_token 一起返回给你,会更方便。
但这确实意味着更多的非标准行为,你需要为每个 API 特别实现。
下面是我们看到的一些非标准扩展的列表:
Quickbooks 使用了一个 realmID,你需要在每个 API 请求中传递它。他们唯一告诉你这个 realmID 的时候是作为 OAuth 回调中的一个额外参数。最好把它存放在某个安全的地方!
Braintree 做了同样的事情,用了一个 companyID
Salesforce 对于每个客户使用了不同的 API 基础 URL;他们称之为 instance_url。谢天谢地,他们在令牌响应中与访问令牌一起返回了用户的 instance_url,但你确实需要从那里解析出它并存储它。
不幸的是,Salesforce 还做了更让人恼火的事情:访问令牌在预设的一段时间后过期,这可以由用户自定义。到目前为止还好,但出于某种原因,他们在令牌响应中没有告诉你你刚刚收到的访问令牌何时会过期(其他人都做了这件事)。相反,你需要查询一个额外的令牌详情端点来获取令牌的(当前)过期日期。为什么呢,Salesforce,为什么?
Slack 有两种不同类型的 scope:作为 Slack 机器人持有的 scope 和允许你代表授权你应用的用户采取行动的 scope。聪明,但是他们没有只是为每个 scope 添加不同的 scope,而是实现了一个单独的 user_scopes 参数,你需要在授权调用中传递它。你最好注意这一点,并祝你好运找到对此支持的 OAuth 库。
为了简洁和简单起见,我们跳过了我们遇到的许多不太标准的 OAuth 流程。
问题 4:“invalid_request” —— 调试 OAuth 很难
调试分布式系统本就不易,如果服务返回的是广泛的、通用的错误消息,就更加困难了。
OAuth2 有标准化的错误消息,但它们在告诉你发生了什么方面,和标题中的例子一样有用(顺便说一下,这是 OAuth 标准推荐的错误消息之一)。
你可能会认为 OAuth 是一个标准,每个 API 都有文档,所以不需要调试。很多。我无法告诉你文档有多少次是错的。或者缺少细节。或者没有更新最新的变化。或者你第一次看它们时错过了什么。我们实现的大约 80% 的 OAuth 流程在第一次实现时都有一些问题,需要调试。
图片
Randall 似乎能看穿我调试 OAuth 流程的心情?©XKCD
有些流程也会因为看似随机的原因而中断:例如,LinkedIn OAuth 会在你传入 PKCE(Proof Key for Code Exchange)参数时中断。你得到的错误是什么?“client error - invalid OAuth request。”这是……有说服力的吗?我们花了一个小时才明白传入(可选的,通常被忽略的)PKCE 参数是什么导致了流程中断。另一个常见的错误是发送与你预先注册的应用不匹配的 scope。(预先注册 scope?是的,现在很多 API 都要求这样做。)这通常会导致一个关于 scope 有问题的通用错误消息。真是令人沮丧。
问题 5:在 API 之上构建应用需要繁琐的审批
事实是,如果你要利用第三方的 API 来为其他平台或服务构建应用,你可能处于弱势的位置。你的客户要求集成,是因为他们已经在使用其他系统了。现在你需要让他们满意。
客观地说,许多 API 都很灵活,提供了方便的自助注册流程,让开发者可以注册他们的应用并开始使用 OAuth。但是一些最受欢迎的 API 需要在你的应用变成公开并且可以被他们的任何用户使用之前进行审核。再次公平地说,大多数审核过程都是合理的,可以在几天内完成。它们可能对于最终用户的安全和质量有净增益。
图片
但但是一些出了名的例子可能需要花费几个月才能完成,有些甚至要求你进入收入分成协议:
如果你想访问包含更敏感用户数据的 scope,比如电子邮件内容,Google 需要一个“安全审核”。我们听说这些审核可能需要几天或几周才能通过,并且需要你在自己这边做不少工作。
想要与 Rippling 集成?准备好回答他们的 30 多个问题和安全预生产筛选吧。我们听说获得访问权限需要几个月(如果你被批准的话)。
HubSpot、Notion、Atlassian、Shopify 和几乎所有有集成市场或应用商店的人都需要审核才能在那里上架。有些审核很温和,有些则要求你提供演示登录、视频演示、博客文章(是的!)等等。不过,在市场或商店上架通常是可选的。
Ramp、Brex、Twitter 和相当多的其他服务没有为开发者提供自助注册流程,而是要求你填写表单以获得手动访问权限。许多人很快就会处理请求,但我们仍然在等待一些人在几周后回复。
Xero 是一个特别极端的例子,它是一个收费的 API:如果你想超过 25 个连接账户的限制,你必须成为 Xero 合作伙伴 并将你的应用列在他们的应用商店中。他们会从每个从那个商店生成的潜在客户中拿走(截至本文撰写时)15% 的收入分成。
问题 6:OAuth 存在安全性问题
随着 OAuth 的安全漏洞被发现,以及网络技术的进步,OAuth 标准也不断更新和完善。如果你想要实现当前的安全最佳实践,OAuth 工作组有一个详细的指南 供你参考。如果你正在与一个仍然使用 OAuth 1.0a 的 API 合作,你就会意识到向后兼容性是一个持续的挑战。
幸运的是,每一次迭代,安全性都在变得更好,但这通常是以开发者更多工作为代价的。即将到来的 OAuth 2.1 标准将使一些当前的最佳实践成为强制性的,并包括强制性的 PKCE(今天只有少数 API 需要这个)和刷新令牌的额外限制。
图片
至少 OAuth 已经实现了一个双因素认证模型。©XKCD
最大的变化可能是由过期的访问令牌和刷新令牌的引入引发的。理论上,这个过程看起来很简单:每当一个访问令牌过期时,用刷新令牌刷新它,并存储新的访问令牌和刷新令牌。实际上,当我们实现这个功能时,我们必须考虑:
竞争条件:我们如何确保在我们刷新当前访问令牌时没有其他请求运行?
一些 API 也会在你一定天数内不使用它们(或者用户已经撤销了访问权限)时让刷新令牌过期。预计一些刷新会失败。
一些 API 在每次刷新请求时都会给你一个新的刷新令牌……
……但有些则默默地假设你会保留旧的刷新令牌并继续使用它。
一些 API 会以绝对值告诉你访问令牌的过期时间。其他人只以相对的“从现在开始的秒数”来表示。还有一些,比如 Salesforce,不轻易透露这种信息。
最后:一些我们还没有谈到的事情
不幸的是,我们只是触及了你的 OAuth 实现的表面问题。现在你的 OAuth 流程运行起来了,你得到了访问令牌,是时候考虑以下问题了:
如何安全地存储这些访问令牌和刷新令牌。它们就像你用户账户的密码一样。但是单向加密不是一个选项;你需要安全的、可逆的加密。
检查授予的 scope 是否与请求的 scope 匹配(有些 API 允许用户在授权流程中更改他们授予的 scope)。
避免刷新令牌时出现多个请求同时修改同一个令牌的情况(也称为竞争条件)。
检测用户在提供者端撤销的访问令牌。
通知用户访问令牌过期,并引导他们重新授权你的应用。
如何撤销你不再需要的访问令牌(或者用户根据 GDPR 要求你删除的访问令牌)。
你还需要应对可用 OAuth scope 的变化、提供者 bug、缺失的文档等等问题。
有更好的方法吗?
如果你读到这里,你可能会想,“一定有更好的方法!”
我们认为有,这就是为什么我们正在构建 Nango:一个开源、自包含的服务,它提供了预先构建好的 OAuth 流程、安全的令牌存储和自动令牌刷新适用于 90 多个 OAuth API。
如果你试一试,我们很乐意听到你的反馈。如果你想和我们分享你最糟糕的 OAuth 恐怖故事,我们也很乐意在我们的 Slack 社区 中听到。
OAuth 的实践过程中,除了本文提到的问题外,还有哪些问题?