认证(authenticate)和授权(authorize)是俩个容易被弄混的概念,尤其是只看英文。
认证意味着证实某个用户是他所声明的那个人;授权意味决定一个身份确定的用户能够访问哪些资源。
认证授权是目前大多数系统都必须要实现的功能,认证就是验证用户的身份,授权就是验证身份后对受限资源的访问控制。最开始是单个平台要做,后来在互联网时代到来,一个账户可登陆多个平台,然后是各种开放平台账户共享,认证授权变得越来越重要。关于验证授权方面的规范协议也相对成熟通用。
单店登录和统一认证中主要三个协议是OpenID
、OAuth
和SAML
被称为单点登录的三驾马车。
最早出现的认证授权协议是SMAL,一般用在企业级单点登录场景。平时接触到的不多。
OAuth是看到的用的比较多的认证协议,尤其是在API认证授权时用,最开始是1.0版本,后来因为有会话共计、会话劫持的漏洞,除了1.0a版本,但编程实现较麻烦,后来出现OAuth2版本,但不兼容之前的版本。一般见到大多数时用的是OAuth2.
OpenId一开始出现是为了解决认证的问题,后来出现了OpenId Connect,在OAuth的基础上也可以实现授权的功能。
对比点 | OAuth2 | OpenId | SMAL |
---|---|---|---|
票据格式 | JSON or SAML2 | JSON | XML |
支持授权 | Yes | No | Yes |
支持认证 | 伪认证 | Yes | Yes |
创建年份 | 2005 | 2006 | 2001 |
最新版本 | OAuth2 | OpenID Connect | SAML2.0 |
传输方式 | HTTP | HTTP GET and HTTP POST | HTTP重定向,SAML SOAP绑定 ,HTTP POST绑定 |
安全弱点 | 不能抵抗网络钓鱼,OAyth没有使用数据签名和加密等措施,数据安全完全依赖TLS | 不能抵抗网络钓鱼,一个钓鱼的IDP如果恶意记录下来用户的OpenID,将会造成很严重的隐私安全问题。 | XML签名存在漏洞,可能被伪造。 |
使用场景 | API授权 | 商用应用的单店登录 | 企业级的单店登录,但是对于移动端支持不是很好。 |
SAML2.0协议
SAML,全称为Security Assertion Markup Language,是一种用于安全性断言的标记语言,目前最新版本是2.0。
SAML在单点登录中大有用处:在SAML协议中,一旦用户身份被主网站(身份鉴别服务器,Identity Provider,IDP)认证过后,该用户再去范文其他在主网站注册过的应用(服务提供者,Service Providers,SP)时,都可以直接登录,而不用在输入身份和口令。
SAML协议的核心是:IDP和SP通过用户的浏览器的重定向访问来实现交换数据。
SP向IDP发出SAML身份认证请求消息,来请求IDP鉴别用户身份;IDP向用户索要用户名和口令,并验证其是否正确,如果验证无误,则想SP返回SAML身份认证应答,表示该用户已经登录成功了,此外应答中还包括一些额外的信息,来确保应答不被篡改和伪造。
下面我们以用户登录SP,SP向IDP发起求情来确认用户身份微粒子,看看SAML的工作流程。比如SP是Google的Apps,IDP是一所大学的身份服务器,Alice是该大学的一名学生。
现在Alice要通过浏览器查阅她的邮件,Alice一般会通过浏览器访问一个网页,比如https://mail.google.com/a/my-university.nl.因为这是个联合身份域,所以Google不会向用户索取用户名和密码,而是将其重定向到IDP来认证其身份。用户被重定向的URL类似于这种:
https://idp:uni.nl/sso?SAMLRequest=fVLLTuswEN0j8Q...C%3D
嵌入到HTTP请求中的SAMLRequest就是SAML认证请求消息。因为SAML是基于XML的(通常比较长),完整认证求情消息要经过压缩(为Url节省空间)和编码(防止特殊字符)才能传输。在压缩和编码之前,SAML消息有如下格式:
<AuthnRequest ID="kfcn...lfki"
Version="2.0"
IssueInstant="2013-02-05T08:28:50Z"
ProtocolBinding="urn:oasis:names:tc:SAML: 2.0:bindings:HTTP-POST"
ProviderName="google.com"
AssertionConsumerServiceURL="https://www.google.com/a/uni.nl/acs">
<Issuer>google.com</Issuer>
<NameIDPolicy AllowCreate="true"
Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"/>;
</AuthnRequest>;
上面的内容用最直白的方式解释出来就是:这个来自Google的求情,请验证当前用户身份,并将结果返回
当IDP收到消息并确认要接受认证求情之后,就会要求Alice输入用户名和口令来验证其身份(如果Alice已经登陆过了,就会跳过该步骤);当验证通过之后,Alice的浏览器将会跳转回google的特定页面(AssertionConsumerService 简称ACS)。同样,SAML身份认证响应的内容也是在压缩并编码后以参数形式传输。在压缩和编码之前,其结构如下:
<Response Version="2.0"
IssueInstant="2013-02-05T08:29:00Z"
Destination="https://www.google.com/a/my.uni.nl/acs" InResponseTo="kfcn...lfki">
<Issuer>https://idp.uni.nl/</Issuer>
<Status>
<StatusCode
Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</Status>
<Assertion Version="2.0"
IssueInstant="2013-02-05T08:29:00Z">
<Issuer>https://idp.uni.nl/</Issuer>
<Subject>
<NameID>alice</NameID>
<SubjectConfirmation ...>
<SubjectConfirmationData
NotOnOrAfter="2013-02-05T08:34:00Z"
Recipient="https://www.google.com/a/my.uni.nl/acs" InResponseTo="kfcn...lfki"/>
</SubjectConfirmation>
</Subject>
<Conditions NotBefore="2013-02-05T08:28:30Z" NotOnOrAfter="2013-02-05T08:34:00Z">
</Conditions>
<AuthnStatement
AuthnInstant="2013-02-05T08:29:00Z"
SessionNotOnOrAfter="2013-02-05T16:29:00Z>
</AuthnStatement>
</Assertion>
</Response>
虽然内容很多,但是其主要表达的是:该消息来自idp.uni.nl,名为Alice的用户身份已经被我验证,该消息的有效期分2分钟,此外重定向的URL中还要有该消息的签名以保证其不被篡改,验证签名的公钥和算法,都是IDP和SP提前协商好的。
当Google接受到SAML认证相应之后,会首先验证消息的签名是否正确以及是否超时失效,然后再从认证消息中提取出Google能识别用户身份(NameId 即Alice)如果以上的步骤都是顺利的,用户将会成功登陆Google。
为了便于解释,以上例子中的信息都保持可读性,如果想要去看看真实的SAML信息,建议使用火狐浏览器的插件工具SAML tracer。该插件将会在浏览器中添加一个窗口来显示SAML消息,一下是截图。
参考
https://www.jianshu.com/p/636c1ee16eba
OAuth2.0协议
协议中的各种角色:应用、API和用户
- 第三方应用:客户端。即尝试去获得用户账户信息的应用,用户需要先对此操作授权。
- API:资源服务器,提供用来获得用户信息的API。
- 授权服务器:让用户同意或者拒绝访问请求。在某些情况下,授权服务器和API资源服务器会是同一个,但是在大多数情况下,二者是独立的。
- 用户:资源的拥有者,当前请求正在尝试获得他们账户的部分信息。
创建App
在开始OAuth流程之前,首先需要注册一个新应用到服务器。通常在注册的过程中,需要提供应用的基本信息,比如应用名称、网站信息、logo等等。此外,你还需要注册一个重定向URI,用以将用户通过浏览器或者移动客户端重定向回Web服务器,这个URI很重要。
重定向URI
服务器只会把用户重定向回注册过的URI以避免有些安全攻击。任何HTTP的重定向请求都必须会用TLS保护,所以要求使用HTTPS。这样的要求是为了防止token在传输过程中被截获。对于原生APP,重定向的URI可以被注册为一个自定义的URL scheme,比如:
demoapp://redirect
Client ID & Secret
当注册完毕之后,一般会返回给用户一个客户端ID(Client ID)和一个客户端秘钥(Client Secret)。这个ID一般是公开的信息,用来构造登录URL,或者被包含在页面的JS代码中。这个秘钥必须是保密的。如果一个已经部署的应用不能保护秘钥,比如一个简单的JS网页,则不应该使用客户端秘钥,同时理论上服务器也不应该向这类应用颁发秘钥。
授权 得到授权码
OAuth2 流程的第一步是获得用户的授权。对于基于浏览器应用或是移动应用,这一步通常是在服务器显示给用户的接口上完成的。
OAuth2 提供了多种授权模式,根据不同情况而使用。
- 授权码模式:适用于web应用,浏览器应用或是移动APP
- 口令模式:适用于使用用户名和口令登录的模式
- 应用访问模式:适用于应用访问
- 默认模式:之前被推荐,在没有Secret的情况下使用,现在被没有客户端秘钥的授权码模式取代
Web 服务
Web服务是最常见的应用类型。用户所使用的Web App程序运行在服务器端,其源码不会公开暴露,这就代表着这类应用在授权的过程中可以使用客户端秘钥(Client Secret),以避免某些攻击手段。
授权
创建登录链接,将用户重定向到授权服务器:
https://oauth2server.com/auth?response_type=code&
client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=photos&state=abcdef
- code:代表服务器希望收到授权码
- client_id:注册应用时颁发的ID
- redirect_uri:指明当用户授权完成之后返回的地址;
- scope:一个或多个值,用来指明希望获得用户账户哪部分权限;
- state:由应用生成的一个随机字符串,会在之后的过程中去验证。
通过以上链接,用户将看到如下的界面
当用户点击allow后,授权服务器会把用户重定向回应用网站,并在连接中带上授权码
https://oauth2client.com/cb?code=AUTH_CODE_HERE&state=abcdef
- code:IDP服务器返回的授权码
- state:返回请求授权码过程中发送的state
当应用有接收到这个请求时,要首先验证state是否就是应用刚才发送的值。在发送state之后,可以吧state保存到Session以便于后序的比较。这样做的目的是防止应用接受任意伪造的授权码。
换取Token
然后使用授权码到资源服务器换取Token
POST https://api.oauth2server.com/token
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=REDIRECT_URI&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
- grant_type = authorization_code :代表授权模式为授权码模式
- code = AUTH_CODE_HERE:为上一步code中获取的授权码
- redirect_uri = REDIRECT_URI 重定向URI
- client_id = CLIENT_ID 注册应用时颁发的ID
- client_secret=CLIENT_SECRET 客户端秘钥,因为当前请求是服务器之间的传输,并没有暴露给用户。
IDP服务器会返回授权码和有效期:
{
"access_token":"RsT5OjbzRn430zqMLgV3Ia",
"expires_in":3600
}
或者是授权失败的提示
{
"error":"invalid_request"
}
注意:服务器要求应用必须提前注册重定向的URI
口令模式
这种模式直接使用用户名和密码来取回token。很显然,这种应用去手机用户的口令,所以之间以当前的App是专门为服务器设计的情况下使用。
POST https://api.oauth2server.com/token
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID
- grant_type=password 指明使用口令模式
- username=USERNAME 用户名
- password=PASSWORD 口令
- client_id=CLIENT_ID client
资源服务器返回的Token和其他模式一样。值得注意的是,因为口令模式多用于移动端和桌面应用,使用secret并不合适。
应用访问模式
在一些情况下,是应用去访问服务器获取资源。而不是某个用户。比如应用去获取一些服务在应用的配置信息,这些信息不属于某个用户的。这种情况下,应用也需要被授权,得到Token去访问数据,这就是应用访问模式的意义。
Oauth提供了Client_credentials模式来处理这个问题:
POST https://api.oauth2server.com/token
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
资源服务器返回形式和其他模式是相同的。
创建已经授权的请求
最后就是使用AccessToken获取资源,如何表示一个请求已经被授权?就是在Http头中加入Token:
curl -H "Authorization: Bearer RsT5OjbzRn430zqMLgV3Ia" \
https://api.oauth2server.com/1/me
另外尽量确保使用Https通信,这样才能保证信息安全。
与OAuth1.0版本差异
最后说明下两个版本OAuth协议有什么不同点。
认证和签名
OAuth1.0流程中有大量密码学上的签名操作,以保证数据完整性;OAuth2 由Https来保护数据。
用户体验
和OAuth 1.0相比,OAuth 2在移动端的体验更好。
性能
和OAuth 1.0相比,OAuth 2性能更好,减少了流程的步骤。
这里有一份很好理解的图解:
【简易图解】『 OAuth2.0』 猴子都能懂的图解
【简易图解】『 OAuth2.0』 『进阶』 授权模式总结
参考
https://www.jianshu.com/p/6392420faf99
OpenID Connect协议
如果要谈单点登录和身份认证,就不得不谈OpenID Connect(OIDC).最典型的使用实例就是使用Google账户登陆其他应用,这一经典的协议模式,为其他厂商的第三方登陆起到了标杆的作用。被广泛参考和使用。
OpenID Connect简介
OpenID Connect是基于OAuth2 规范族的客户操作的身份验证协议。他是用简单的REST/JSON 消息流来实现,和之前任何一种身份认证协议相比,开发者可以轻松集成。
OpenID Connect允许开发者验证跨网站和应用的用户,而无需拥有和管理密码文件。
OpenID Connect允许索尼又类型的客户,包括基于浏览器的JavaScript和本机移动应用程序,启动登录流动和接收可验证断言对登录用户的身份。
OIDC基础
简要而言,OIDC是一种安全机制,用于应用连接到身份认证服务器(Identity Service)获取用户信息,并将这些信息以安全可靠的方法返回给应用。
在最初因为OpenID经常和OAuth协议一起提及,所以二者经常被搞混。
- OpenID 是Authentication,即认证,对用户的身份进行认证,判断其身份是否有效,也就是让网站知道“你是你声称的那个用户”
- OAuth是Authorization 即授权,在已知用户身份合法的情况下,经用户授权来允许某些操作,也就是让网站知道“你能被允许做哪些事情”
由此可知,授权要在认证之后进行,只有确定用户身份之后才能授权
(身份认证)+OAuth2.0 = OpenID Connect
OpenID Connect是“认证”和“授权”的结果,因为其基于OAuth协议,所以OpenID Connect协议中包含了client_id、client_secret还有redirect_uri等字段标识。这些信息被保存在“身份认证服务器”,以确保特定的客户端收到的信息只来自于合法的应用平台。这样做的目的是为了防止client_id泄露而造成的恶意网站发起的OIDC流程。
举个栗子,某个用户使用FaceBook的子应用(例如faceBook中的某一款游戏),该应用可以通过FaceBook账号登录,则你可以在应用找那个发起请求到“身份认证服务器”(也就是FaceBook的服务器)请求登录,这时你会看到如下界面,询问是否授权。
在OAuth中,这些授权被称为scope.OpenID Connect也有自己特殊的scope--openid,他必须在第一次请求“身份鉴别服务器(Identity Provider简称IDP)”时发送过去。
OIDC 流程
OAuth2提供了Access Token来解决授权单方客户端访问受保护资源的问题;相似的,OIDC在这个基础上提供了ID Token来解决第三方客户端表示用户身份认证的问题。
OIDC的核心在于OAUth2的授权流程中,一并提供用户的身份认证信息(ID-Token),给到第三方客户端,ID-Token使用JWT(JSON Web Token - 在Web应用间安全地传递信息
)格式来包装,得益于JWT的自包含性,紧凑型以及防篡改机制,是的ID-Token可以安全的传递给第三方客户端程序并且容易被验证。应用服务器,在验证ID-Token正确性后,使用Access-Token向UserInfo接口换取用户的更多信息。
有上述可知,OIDC是遵循了OAuth协议流程,在申请Access-Token的同时,也返回了ID-Token来验证用户身份。
相关定义
- EU:End User 用户。
- RP:Relying Party 用来代指OAuth2中受信任的客户端,身份认证和授权信息的消费方。
- OP:OpenID Provider,有能力提供EU身份认证的服务方(比如OAuth2中的授权服务),用来为RP提供EU的身份认证信息。
- ID-Token:JWT格式的数据,包含EU身份认证的信息。
- UserInfo EndPoint:用户信息接口(受IAuth2保护),当RP使用ID-Token访问时,返回授权用户的信息,此接口必须使用HTTPS。
下面我们来俺看OIDC的具体协议流程。
根据应用客户端不同,OIDC的工作模式月应该是不同的。和OAuth类似,主要看是否客户端能否保证client_secret的安全性。
如果是JS应用,其所有的代码都会被加载到浏览器而暴露出来,没有后端可以保证client_secret的安全性,则需要使用默认模式流程(Implicit Flow)
如果是传统的客户端应用,后端代码和用户是隔离的保证client_secret不被泄漏。就可以使用授权码模式流程(Authentication Flow).
此外还有混合模式流程(Hybird Flow),简而言之就是以上二者的融合。
OAuth2中还有口令模式和应用访问模式 这两种方式来获取access Token,为什么OIDC中没有扩展这些方式呢?
口令模式是需要用户提供账号和密码给RP的,既然都已经有用户名和密码了,就不需要再获取什么用户身份了。至于应用访问模式,这种方式不需要用户参与,也就无需要认证和获取用户身份了。这也能反映出授权和认证的差异,以及只是用OAuth2来做身份认证的事情是远远不够的,也是不合适的。
授权码模式流程
和OAuth认证流程类似
1.RP发送一个认证请求给OP,其中附带client_id
2.OP对EU进行身份认证
3.OP返回响应,发送授权码给RP
4.RP使用授权码想OP索要ID-Token和Access-Token,OP验证无误后返回给RP;
5.RP使用Access-Token发送一个请求到UserInfo EndPoint;UserInfo EndPoint返回EU的Claims。
基于Authorization Code的认证请求
RP使用OAth2的Authorization-code的方式来完成用户身份认证,所有TOken都是通过OP的Token EndPoint来发放的。构建一个OIDC的Authentication Request需要提供如下参数:
- scope:必须。OIDC的请求必须包含值为“openid”的scope参数。
- response_type: 必选 同OAuth2
- client_id: 必选。同OAuth2
- redirect_uri:必选。同OAuth2
- state:推荐 同OAuth2.防止CSRF,XSRF。
示例如下:
GET /authorize?
response_type=code
&scope=openid%20profile%20email
&client_id=s6BhdRkqt3
&state=af0ifjsldkj
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb HTTP/1.1
Host: server.example.com
基于Authorization Code的认证请求的响应
在OP接收到认证请求之后,需要对请求参数做严格的验证,具体的规则参见http://openid.net/specs/openid-connect-core-1_0.html#AuthRequestValidation,验证通过后引导EU进行身份认证并且同意授权。在这一切都完成后,会重定向到RP指定的回调地址(redirect_uri),并且把code和state参数传递过去。比如:
HTTP/1.1 302 Found
Location: https://client.example.org/cb?
code=SplxlOBeZQQYbYS6WxSbIA
&state=af0ifjsldkj
获取ID Token
RP使用上一步获得的code来请求TOken EndPoint,这一步同OAuth2,就不再展开细说了。然后Token EndPoint会返回响应的Token,其中除了OAuth2规定的部分数据外,还会附加一个id_token字段,就是上面提到的ID Token。例如:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "SlAV32hkKG",
"token_type": "Bearer",
"refresh_token": "8xLOxBtZp8",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzc
yI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5
NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZ
fV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5Nz
AKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6q
Jp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJ
NqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7Tpd
QyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoS
K5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4
XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg"
}
其中看起来一堆乱码的部分就是JWT格式的ID-Token。在RP拿到这些信息之后,需要对id_token以及access_token进行验证(具体规则参见http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation和http://openid.net/specs/openid-connect-core-1_0.html#ImplicitTokenValidation)。至此,可以说用户身份认证就可以完成了。后序可以根据UserInfo EndPoint获取更完整的信息。
安全令牌 ID-Token
上面提到过OIDC对OAuth2最主要的扩展就是提供了ID-Token,下面我们来看看ID-Token的主要构成:
- iss = Issuer Identifier:必须。提供认证信息者的唯一标识。一般是Url的host+path部分
- sub = Subject Identifier:必须。iss提供EU的唯一标识;最长为255个ASCII个字符;
- aud = Audience(s):必须。标识ID-Token的受众。必须包含OAuth2的client_id;
- exp =Expiration time:必须。ID-Token的过期时间。
- iat = Issued At Time。必须。JWT构建的时间。
- auth_time = AuthenticationTime:EU完成认证的时间。如果RP发送认证请求的时候携带max_age的参数,则此Claim是必须的。
- nonce:RP发送请求的时候提供的随机字符串,用来减缓重放攻击,也可以来关联ID-Token和RP本身的Session信息。
- acr = Authentication Context Class Reference:可选。表示一个认证上下文引用值,可以用来标识认证上下文类。
- amr = Authentication Methods References:可选。表示一组认证方法。
- azp = Authorized party:可选。结合aud使用。只有在被认证的一方和受众(aud)不一致时才使用此值,一般情况下很少使用。
{
"iss": "https://server.example.com",
"sub": "24400320",
"aud": "s6BhdRkqt3",
"nonce": "n-0S6_WzA2Mj",
"exp": 1311281970,
"iat": 1311280970,
"auth_time": 1311280969,
"acr": "urn:mace:incommon:iap:silver"
}
另外ID Token必须使用JWT进行签名和JWE(JSON Web Encryption)加密。从而提供认证的完整性、不可否认性以及可选的保密性。
USerInfo EndPoint
可能有的读者发现了,ID-Token只有sub是EU相关的,这在一般情况下是不够的,必须还需要EU的用户名,头像等其他的资料,OIDC提供了一组公共的cliams,来提供更多用户的信息,这就是UserInfo EndPoint。
在RP得到Access Token后可以请求此资源,然后获得一组EU相关的Claims,这些信息可以说是ID-Token的扩展,ID-Token中只需包含EU的唯一标识sub即可(避免ID-Token过于庞大和暴露用户敏感信息),然后再通过此接口获取完整的EU的信息。此资源必须部署在TLS之上(即必须使用HTTPS)例如:
GET /userinfo HTTP/1.1
Host: server.example.com
Authorization: Bearer SlAV32hkKG
成功之后响应如下:
HTTP/1.1 200 OK
Content-Type: application/json
{
"sub": "248289761001",
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"preferred_username": "j.doe",
"email": "janedoe@example.com",
"picture": "http://example.com/janedoe/me.jpg"
}
其中sub代表EU的唯一标识,这个claim是必须的,其他都是可选的。