identityserver4源码解析_3_认证接口
目录
- identityserver4源码解析_1_项目结构
- identityserver4源码解析_2_元数据接口
- identityserver4源码解析_3_认证接口
- identityserver4源码解析_4_令牌发放接口
- identityserver4源码解析_5_查询用户信息接口
- identityserver4源码解析_6_结束会话接口
- identityserver4源码解析_7_查询令牌信息接口
- identityserver4源码解析_8_撤销令牌接口
协议
五种认证方式
-
authorization code 授权码模式:认证服务返回授权码,后端用clientid和密钥向认证服务证明身份,使用授权码换取id token 和/或 access token。本模式的好处是由后端请求token,不会将敏感信息暴露在浏览器。本模式允许使用refreshtoken去维持长时间的登录状态。使用此模式的客户端必须有后端参与,能够保障客户端密钥的安全性。此模式从authorization接口获取授权码,从token接口获取令牌。
-
implict 简化模式:校验跳转uri验证客户端身份之后,直接发放token。通常用于纯客户端应用,如单页应用javascript客户端。因为没有后端参与,密钥存放在前端是不安全的。由于安全校验较宽松,本模式不允许使用refreshtoken来长时间维持登录状态。本模式的所有token从authorization接口获取。
-
hybrid 混合流程:混合流程顾名思义组合使用了授权码模式+简化模式。前端请求授权服务器返回授权码+id_token,这样前端立刻可以使用用户的基本信息;后续请求后端使用授权码+客户端密钥获取access_token。本模式能够使用refreshtoken来长时间维持登录状态。使用本模式必须有后端参与保证客户端密钥的安全性。混合模式极少使用,除非你的确需要使用它的某些特性(如一次请求获取授权码和用户资料),一般最常见的还是授权码模式。
-
resource owner password credential 用户名密码模式:一般用于无用户交互场景,或者第三方对接(如对接微信登录,实际登录界面就变成了微信的界面,如果不希望让客户扫了微信之后再跑你们系统登录一遍,就可以在后端用此模式静默登录接上自家的sso即可)
-
client credential 客户端密钥模式:仅需要约定密钥,仅用于完全信任的内部系统
认证方式特点对比
特点 | 授权码模式 | 简化模式 | 混合模式 |
---|---|---|---|
所有token从authorization接口返回 | no | yes | yes |
所有token从token接口返回 | yes | no | no |
所有tokens不暴露在浏览器 | yes | no | no |
能够验证客户端密钥 | yes | no | yes |
能够使用刷新令牌 | yes | no | yes |
仅需一次请求 | no | yes | no |
大部分请求由后端进行 | yes | no | 可变 |
支持返回类型对比
返回类型 | 认证模式 | 说明 |
---|---|---|
code | authorization code flow | 仅返回授权码 |
id_token | implicit flow | 返回身份令牌 |
id_token token | implicit flow | 返回身份令牌、通行令牌 |
code id_token | hybrid flow | 返回授权码、身份令牌 |
code token | hybrid flow | 返回授权码、通行令牌 |
code id_token token | hybrid flow | 返回授权码、身份令牌、通行令牌 |
授权码模式解析
相对来说,授权码模式还是用的最多的,我们详细解读一下本模式的协议内容。
授权时序图
认证请求
认证接口必须同时支持get和post两种请求方式。如果使用get方法,客户端必须使用uri query传递参数,如果使用post方法,客户端必须使用form传递参数。
参数定义
- scope:授权范围,必填。必须包含openid。
- response_type:返回类型,必填。定义了认证服务返回哪些参数。对于授权码模式,本参数只能是code。
- client_id:客户端id,必填。
- redirect_uri:跳转地址,必填。授权码生成之后,认证服务会带着授权码和其他参数回跳到此地址。此地址要求使用https。如果使用http,则客户端类型必须是confidential。
- state:状态字段,推荐填写。一般用于客户端与认证服务比对此字段,来防跨站伪造攻击,同时state也可以存放状态信息,如发起认证时的页面地址,用于认证完成后回到原始页面。
- 其他:略。上面五个是和oauth2.0一样的参数,oidc还定义了一些扩展参数,用的很少,不是很懂,感兴趣的自己去看协议。
请求报文示例
http/1.1 302 found location: https://server.example.com/authorize? response_type=code &scope=openid%20profile%20email &client_id=s6bhdrkqt3 &state=af0ifjsldkj &redirect_uri=https%3a%2f%2fclient.example.org%2fcb
认证请求校验
- 必填校验
- response_type必须为code
- scope必填,必须包含openid
认证终端用户
- 下面两种情况认证服务必须认证用户
- 用户尚未认证
- 认证请求包含参数prompt=login,即使用户已经认证过也需要重新认证
- 认证请求包含参数prompt=none,然后用户尚未被认证,则需要返回错误信息
认证服务必须想办法防止过程中的跨站伪造攻击和点击劫持攻击。
获取终端用户授权/同意
终端用户通过认证之后,认证服务必须与终端用户交互,询问用户是否同意对客户端的授权。
认证响应
成功响应
使用 application/x-www-form-urlencoded格式返回结果
例如:
http/1.1 302 found location: https://client.example.org/cb? code=splxlobezqqybys6wxsbia &state=af0ifjsldkj
失败响应
错误代码包括这些
oauth2.0定义的响应代码
- invalid_request:非法请求,未提供必填参数,参数非法等情况
- unauthorized_client:客户端未授权
- access_denied:用户无权限
- unsupported_response_type
- invalid_scope:非法的scope参数
- server_error
- temporarily_unavailable
另外oidc还扩展了一些响应代码,不常见,略
例如:
http/1.1 302 found location: https://client.example.org/cb? error=invalid_request &error_description= unsupported%20response_type%20value &state=af0ifjsldkj
客户端校验授权码
协议规定客户端必须校验授权码的正确性
源码解析
从authorizeendpoint的processasync方法作为入口开始认证接口的源码解析。
- 判断请求方式是get还是post,获取入参,如果是其他请求方式415状态码
- 从session中获取user
- 入参和user作为入参,调用父类processauthorizerequestasync方法
public override async task<iendpointresult> processasync(httpcontext context) { logger.logdebug("start authorize request"); namevaluecollection values; if (httpmethods.isget(context.request.method)) { values = context.request.query.asnamevaluecollection(); } else if (httpmethods.ispost(context.request.method)) { if (!context.request.hasformcontenttype) { return new statuscoderesult(httpstatuscode.unsupportedmediatype); } values = context.request.form.asnamevaluecollection(); } else { return new statuscoderesult(httpstatuscode.methodnotallowed); } var user = await usersession.getuserasync(); var result = await processauthorizerequestasync(values, user, null); logger.logtrace("end authorize request. result type: {0}", result?.gettype().tostring() ?? "-none-"); return result; }
认证站点如果cookie中存在当前会话信息,则直接返回用户信息,否则调用cookie架构的认证方法,会跳转到登录页面。
public virtual async task<claimsprincipal> getuserasync() { await authenticateasync(); return principal; } protected virtual async task authenticateasync() { if (principal == null || properties == null) { var scheme = await getcookieschemeasync(); var handler = await handlers.gethandlerasync(httpcontext, scheme); if (handler == null) { throw new invalidoperationexception($"no authentication handler is configured to authenticate for the scheme: {scheme}"); } var result = await handler.authenticateasync(); if (result != null && result.succeeded) { principal = result.principal; properties = result.properties; } } }
认证请求处理流程大致分为三步
- authorizerequestvalidator校验所有参数
- 认证接口consent入参为null,不需要处理用户交互判断
- 生成返回报文
internal async task<iendpointresult> processauthorizerequestasync(namevaluecollection parameters, claimsprincipal user, consentresponse consent) { if (user != null) { logger.logdebug("user in authorize request: {subjectid}", user.getsubjectid()); } else { logger.logdebug("no user present in authorize request"); } // validate request var result = await _validator.validateasync(parameters, user); if (result.iserror) { return await createerrorresultasync( "request validation failed", result.validatedrequest, result.error, result.errordescription); } var request = result.validatedrequest; logrequest(request); // determine user interaction var interactionresult = await _interactiongenerator.processinteractionasync(request, consent); if (interactionresult.iserror) { return await createerrorresultasync("interaction generator error", request, interactionresult.error, interactionresult.errordescription, false); } if (interactionresult.islogin) { return new loginpageresult(request); } if (interactionresult.isconsent) { return new consentpageresult(request); } if (interactionresult.isredirect) { return new customredirectresult(request, interactionresult.redirecturl); } var response = await _authorizeresponsegenerator.createresponseasync(request); await raiseresponseeventasync(response); logresponse(response); return new authorizeresult(response); }
生成返回信息
此处只有authorizationcode、implicit、hybrid三种授权类型的判断,用户名密码、客户端密钥模式不能使用authorize接口。
public virtual async task<authorizeresponse> createresponseasync(validatedauthorizerequest request) { if (request.granttype == granttype.authorizationcode) { return await createcodeflowresponseasync(request); } if (request.granttype == granttype.implicit) { return await createimplicitflowresponseasync(request); } if (request.granttype == granttype.hybrid) { return await createhybridflowresponseasync(request); } logger.logerror("unsupported grant type: " + request.granttype); throw new invalidoperationexception("invalid grant type: " + request.granttype); }
- 如果state字段不为空,使用加密算法得到state的hash值
- 构建authorizationcode对象,存放在store中,store是idsv4用于持久化的对象,默认实现存储在内存中,可以对可插拔服务进行注入替换,实现数据保存在在mysql、redis等流行存储中
- 将授权码对象的id返回
protected virtual async task<authorizeresponse> createcodeflowresponseasync(validatedauthorizerequest request) { logger.logdebug("creating authorization code flow response."); var code = await createcodeasync(request); var id = await authorizationcodestore.storeauthorizationcodeasync(code); var response = new authorizeresponse { request = request, code = id, sessionstate = request.generatesessionstatevalue() }; return response; } protected virtual async task<authorizationcode> createcodeasync(validatedauthorizerequest request) { string statehash = null; if (request.state.ispresent()) { var credential = await keymaterialservice.getsigningcredentialsasync(); if (credential == null) { throw new invalidoperationexception("no signing credential is configured."); } var algorithm = credential.algorithm; statehash = cryptohelper.createhashclaimvalue(request.state, algorithm); } var code = new authorizationcode { creationtime = clock.utcnow.utcdatetime, clientid = request.client.clientid, lifetime = request.client.authorizationcodelifetime, subject = request.subject, sessionid = request.sessionid, codechallenge = request.codechallenge.sha256(), codechallengemethod = request.codechallengemethod, isopenid = request.isopenidrequest, requestedscopes = request.validatedscopes.grantedresources.toscopenames(), redirecturi = request.redirecturi, nonce = request.nonce, statehash = statehash, wasconsentshown = request.wasconsentshown }; return code; }
返回结果
- 如果responsemode等于query或者fragment,将授权码code及其他信息拼装到uri,返回302重定向请求
例子:
302 https://mysite.com?code=xxxxx&state=xxx
- 如果是formpost方式,会生成一段脚本返回到客户端。窗口加载会触发form表单提交,将code、state等信息包裹在隐藏字段里提交到配置的rediret_uri。
<html> <head> <meta http-equiv='x-ua-compatible' content='ie=edge' /> <base target='_self'/> </head> <body> <form method='post' action='https://mysite.com'> <input type='hidden' name='code' value='xxx' /> <input type='hidden' name='state' value='xxx' /> <noscript> <button>click to continue</button> </noscript> </form> <script>window.addeventlistener('load', function(){document.forms[0].submit();});</script> </body> </html>
private async task renderauthorizeresponseasync(httpcontext context) { if (response.request.responsemode == oidcconstants.responsemodes.query || response.request.responsemode == oidcconstants.responsemodes.fragment) { context.response.setnocache(); context.response.redirect(buildredirecturi()); } else if (response.request.responsemode == oidcconstants.responsemodes.formpost) { context.response.setnocache(); addsecurityheaders(context); await context.response.writehtmlasync(getformposthtml()); } else { //_logger.logerror("unsupported response mode."); throw new invalidoperationexception("unsupported response mode"); } }
客户端在回调地址接收code,即可向token接口换取token。
其他
简单看一下简化流程和混合流程是怎么创建返回报文的。
简化流程生成返回报文
- 如果返回类型包含token,生成通行令牌
- 如果返回类型包含id_token,生成身份令牌
可以看到,简化流程的所有token都是由authorization接口返回的,一次请求返回所有token。
protected virtual async task<authorizeresponse> createimplicitflowresponseasync(validatedauthorizerequest request, string authorizationcode = null) { logger.logdebug("creating implicit flow response."); string accesstokenvalue = null; int accesstokenlifetime = 0; var responsetypes = request.responsetype.fromspaceseparatedstring(); if (responsetypes.contains(oidcconstants.responsetypes.token)) { var tokenrequest = new tokencreationrequest { subject = request.subject, resources = request.validatedscopes.grantedresources, validatedrequest = request }; var accesstoken = await tokenservice.createaccesstokenasync(tokenrequest); accesstokenlifetime = accesstoken.lifetime; accesstokenvalue = await tokenservice.createsecuritytokenasync(accesstoken); } string jwt = null; if (responsetypes.contains(oidcconstants.responsetypes.idtoken)) { string statehash = null; if (request.state.ispresent()) { var credential = await keymaterialservice.getsigningcredentialsasync(); if (credential == null) { throw new invalidoperationexception("no signing credential is configured."); } var algorithm = credential.algorithm; statehash = cryptohelper.createhashclaimvalue(request.state, algorithm); } var tokenrequest = new tokencreationrequest { validatedrequest = request, subject = request.subject, resources = request.validatedscopes.grantedresources, nonce = request.raw.get(oidcconstants.authorizerequest.nonce), includeallidentityclaims = !request.accesstokenrequested, accesstokentohash = accesstokenvalue, authorizationcodetohash = authorizationcode, statehash = statehash }; var idtoken = await tokenservice.createidentitytokenasync(tokenrequest); jwt = await tokenservice.createsecuritytokenasync(idtoken); } var response = new authorizeresponse { request = request, accesstoken = accesstokenvalue, accesstokenlifetime = accesstokenlifetime, identitytoken = jwt, sessionstate = request.generatesessionstatevalue() }; return response; }
混合流程生成返回报文
这段代码充分体现了它为啥叫混合流程,把生成授权码的方法调一遍,再把简化流程的方法调一遍,code和token可以一起返回。
protected virtual async task<authorizeresponse> createhybridflowresponseasync(validatedauthorizerequest request) { logger.logdebug("creating hybrid flow response."); var code = await createcodeasync(request); var id = await authorizationcodestore.storeauthorizationcodeasync(code); var response = await createimplicitflowresponseasync(request, id); response.code = id; return response; }
推荐阅读
-
Mybaits 源码解析 (十一)----- 设计模式精妙使用:静态代理和动态代理结合使用:@MapperScan将Mapper接口生成代理注入到Spring
-
Mybaits 源码解析 (五)----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
-
Asp.NetCoreWebApi图片上传接口(二)集成IdentityServer4授权访问(附源码)
-
Mybaits 源码解析 (三)----- Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
-
深入理解 lambda表达式 与 函数式编程 函数式接口源码解析(二)
-
IdentityServer4源码解析_4_令牌发放接口
-
identityserver4源码解析_3_认证接口
-
identityserver4源码解析_2_元数据接口
-
IdentityServer4源码解析_1_项目结构
-
IdentityServer4实现.Net Core API接口权限认证(快速入门)