OAuth2
在授权登录时,OAuth2 用得比较广泛。本文详细描述了不同客户端用OAuth2标准获取用户授权的方式。
名词解释
参与角色
- 客户端:第三方应用。需要从用账号中获取资源的应用。
- 资源服务器:资源获取 API 提供者。
- 授权服务器:提供同意或者拒绝用户授权的服务器。可以和资源服务器是同一或分离的服务器。
- 用户:资源拥有者。
其他
- 第三方应用注册:向资源方注册app,一般提供名称,网站,logo和重定向URI。
- 重定向URI:基于web的,必须用https协议避免授权时被拦截。原生APP一般注册一个自定义scheme,如demoapp://redirect
- Client ID和Secret:注册完成后返回一个Client ID和Secret。Client ID一般是公开的,可用于组成登录URL,或者放在javascript页面的脚本中。Secret则
必须
保密。对原生App或者单页js应用,不能用Secret。此类应用最好别生成secret。
授权类型
- 授权码(Authorization Code),支持web 服务器,基于浏览器App及原生APP。
- 密码:登录时提供用户名和密码。仅用于同一家公司的不同app
- 客户端证书(Client credentials),用于访问时不需要用户参与。静默授权。
- 隐式(Implicit):用于客户端没有密码的情况,已带PKCE的授权码取代。
下面对具有Web Server的web应用,web单页应用或基于浏览器的App,原生应用分别进行详述。
Web Server 应用
因为有服务器,且服务器非公开,所以是大多数的服务场景,且比较安全。
登录
https://authorization-server.com/auth?response_type=code&
client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=photos&state=1234zyx
- response_type=code - 表示服务器期望收到授权码。
- client_id - 创建app时收到的client id。
- redirect_uri - 授权完成时重定向到的地址
- scope - 一到多个范围值,指示可以访问到的用户账号的哪些部分。
- state - 第三方应用生成的随机串,可以用于后面验证,这样避免一些攻击。
弹出提示,说明哪个应用希望访问用户的哪些内容。如果用户同意访问,则重定向到第三方服务:
https://example-app.com/cb?code=AUTH_CODE_HERE&state=1234zyx
第三方应用应该比较state是否和发出的请求一致。避免被欺骗,换成其他AUTH_CODE_HERE
服务端获取access token
POST https://api.authorization-server.com/token
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=REDIRECT_URI&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
- redirect_uri=REDIRECT_URI 必须和原来注册时提供的一致
服务端返回:
{
"access_token":"RsT5OjbzRn430zqMLgV3Ia",
"expires_in":3600
}
或者遇到错误时:
{
"error":"invalid_request"
}
单页浏览器App
浏览器会加载全部代码,所以不能存储密钥和客户端密码。可以采用授权码类似,但每次请求动态生成密码,即PKCE扩展。 老的标准是用“implict”模式,直接给客户端返回token,这有安全问题。现在推荐用PKCE模式。
- 创建一个43-128长度字符串,叫code_verifier,验证码,如:
5d2309e5bb73b864f989753887fe52f79ce5270395e25862da6940d5
- 将其用SHA256编码,再转为url-safe的base64编码,叫code_challenge,挑战码:
MChCW5vD-3h03HMGFZYskOSTir7II_MMTb8a9rJNhnI
可以用 example-app.com/pkce 生成密码和hash - 和授权码登录类似,但增加了code_challenge
https://authorization-server.com/auth?response_type=code& client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=photos&state=1234zyx&code_challenge=CODE_CHALLENGE&code_challenge_method=S256
- code_challenge - URL-safe base64-encoded SHA256 hash ( secret)
- 返回
https://example-app.com/cb?code=AUTH_CODE_HERE&state=1234zyx
- 获取授权码
POST https://api.authorization-server.com/token
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=REDIRECT_URI&
client_id=CLIENT_ID&
code_verifier=CODE_VERIFIER
- *code_verifier=CODE_VERIFIER 一开始生成的随机字符串 这防止了授权码请求被拦截时,还是不能获取访问token,因为没有验证码。
原生应用 Apps
原生应用和基于浏览器应用差不多,都不能保证密钥的安全。所以也采用授权码加PKCE扩展。
授权
创建一个“登录”按钮,点击后唤起授权App或者Web page。原生app可以创建自定义的模式scheme如:”example-app://”
使用原生服务app
假设安装了facebook app,采用如下url进入:
fbauth2://authorize?response_type=code&client_id=CLIENT_ID
&redirect_uri=REDIRECT_URI&scope=email&state=1234zyx
- redirect_uri=REDIRECT_URI - 当授权完成,指示用户跳转的URI。如 fb00000000://authorize
- scope=email - 一到多个需要授权的资源
如果服务器支持PKCE,(自己提供的服务,应该支持PKCE)则应该带上相关参数:
- code_challenge=XXXXXXX - base64-encoded sha256 hash(code verifier string)
- code_challenge_method=S256 - hash方法, sha256.
使用独立浏览器
如果资源方没有独立App,则应该唤起一个独立浏览器来进行登录授权。不能直接用webview,这样无法保证是资源方提供的服务,容易被钓鱼。 iOS 9以后,可以用”SafariViewController”来打开嵌入式浏览器。它与独立浏览器共享cookie,也可以看到地址栏。还能阻止app偷窥以及修改浏览器内容,所以可以认为安全。
https://facebook.com/dialog/oauth?response_type=code&client_id=CLIENT_ID
&redirect_uri=REDIRECT_URI&scope=email&state=1234zyx
如果服务支持PKCE,则应该带上相关参数。
获取授权码
用户点击“同意授权(Approve)”后,将被重定向到应用服务:
fb00000000://authorize?code=AUTHORIZATION_CODE&state=1234zyx
应用服务首先应该校验state,再用code去获取访问token。
与web server获取访问token基本相同,但不再带密钥secret,如果服务支持PKCE 则带上相关参数如code_verifie,如下:
POST https://api.authorization-server.com/token
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=REDIRECT_URI&
client_id=CLIENT_ID&
code_verifier=VERIFIER_STRING
授权服务校验并返回访问token。如果支持PKCE,则服务应该知道code是挑战生成,需要比较code_verifier运算后与挑战所带的hash是否一致。这样可以支持一些不支持秘钥secret的客户端。
密码方式
密码方式应该是同一家单位的不同服务。如微信桌面版和移动版。
POST https://api.authorization-server.com/token
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID
没有secret字段,因为假设大多数这种场景是移动或桌面应用。
应用访问授权
某些情况下,应用需要授权访问服务提供者的服务,但不是代表用户,而是应用自身。如获取客户端访问统计数据。可以在后端用POST方法如下:
POST https://api.authorization-server.com/token
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
服务提供方校验后,可以像其他方式一样返回token。
获取资源
拿到访问token后,就可以访问资源了,如下:
curl -H "Authorization: Bearer RsT5OjbzRn430zqMLgV3Ia" \
https://api.authorization-server.com/1/me
一定要用https协议,并且不要忽略无效证书。Https是唯一能够保护请求被拦截和修改的方式。
https安全
原生app或者浏览器app为何不能直接放密钥?因为app是完全控制在用户手中的。可以用各种手段进行脱壳,反编译,调试,监听。 密钥可以直接在客户端采用strings等命令列出来。 就算进行混淆,和服务端通信时也可以通过 https proxy如charles proxy监听到。
参考
- OAuth2 相关标准
- OAuth2 标准:RFC 6749,2012,IETF OAuth Working Group
- OAuth 2.0 for Native Apps:RFC 8252
- JWT标准:RFC 7519
- PKCE(Proof Key for Code Exchange): RFC 7636
- OAuth 2.1 草案
- OAuth for Browser-Based Apps
- OAuth 2.0 Security Best Current Practice
- https://aaronparecki.com/oauth-2-simplified 作者Aaron Parecki 是W3C多个规范编辑, oauth.net网站主。
- Charles Proxy Https代理
- mitmproxy 免费MIT Https 代理
如非注明转载, 均为原创. 本站遵循知识共享CC协议,转载请注明来源