目录

协议

五种认证方式

  • 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 返回授权码、身份令牌、通行令牌

授权码模式解析

相对来说,授权码模式还是用的最多的,我们详细解读一下本模式的协议内容。

授权时序图

sequenceDiagram
用户->>客户端: 请求受保护资源
客户端->>认证服务: 准备入参,发起认证请求
认证服务->>认证服务: 认证用户
认证服务->>用户: 是否同意授权
认证服务->>客户端: 发放授权码(前端进行)
客户端->>认证服务: 使用授权码请求token(后端进行)
认证服务->>认证服务: 校验客户端密钥,校验授权码
认证服务->>客户端: 发放身份令牌、通行令牌(后端进行)
客户端->>客户端: 校验身份令牌,获取用户标识

认证请求

认证接口必须同时支持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;
}

identityserver4源码解析_3_认证接口的更多相关文章

  1. identityserver4源码解析_2_元数据接口

    目录 identityserver4源码解析_1_项目结构 identityserver4源码解析_2_元数据接口 identityserver4源码解析_3_认证接口 identityserver4 ...

  2. IdentityServer4源码解析_4_令牌发放接口

    目录 identityserver4源码解析_1_项目结构 identityserver4源码解析_2_元数据接口 identityserver4源码解析_3_认证接口 identityserver4 ...

  3. IdentityServer4源码解析_5_查询用户信息接口

    协议简析 UserInfo接口是OAuth2.0中规定的需要认证访问的接口,可以返回认证用户的声明信息.请求UserInfo接口需要使用通行令牌.响应报文通常是json数据格式,包含了一组claim键 ...

  4. IdentityServer4源码解析_1_项目结构

    目录 IdentityServer4源码解析_1_项目结构 IdentityServer4源码解析_2_元数据接口 IdentityServer4源码解析_3_认证接口 IdentityServer4 ...

  5. Spring源码解析 - AbstractBeanFactory 实现接口与父类分析

    我们先来看类图吧: 除了BeanFactory这一支的接口,AbstractBeanFactory主要实现了AliasRegistry和SingletonBeanRegistry接口. 这边主要提供了 ...

  6. Django框架rest_framework中APIView的as_view()源码解析、认证、权限、频率控制

    在上篇我们对Django原生View源码进行了局部解析:https://www.cnblogs.com/dongxixi/p/11130976.html 在前后端分离项目中前面我们也提到了各种认证需要 ...

  7. Java基础——集合源码解析 List List 接口

    今天我们来学习集合的第一大体系 List. List 是一个接口,定义了一组元素是有序的.可重复的集合. List 继承自 Collection,较之 Collection,List 还添加了以下操作 ...

  8. .Net Core 认证系统之Cookie认证源码解析

    接着上文.Net Core 认证系统源码解析,Cookie认证算是常用的认证模式,但是目前主流都是前后端分离,有点鸡肋但是,不考虑移动端的站点或者纯管理后台网站可以使用这种认证方式.注意:基于浏览器且 ...

  9. 【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何在网关上实现客户端自定义限流功能,基本完成了关于网关的一些自定义扩展需求,后面几篇将介绍基于IdentityServer ...

随机推荐

  1. 三层交换机配置DHCP为不同VLAN分配IP地址

    三层交换的原理以及DHCP的原理,作者在这里就不详细的解释了,在这里通过一个案例来了解使用三层交换做DHCP服务器,并为不同网段分配IP地址.在生产环境中,使用路由器或交换机做DHCP服务器要常见一些 ...

  2. 有关PHP的可变函数

    事情的起因是这样子的,最近看到一道题,问的是 <?php $_POST['a']($_POST['b']);?> 这句代码有什么问题,答案很明显因为PHP的可变函数这个特性,导致了任意代码 ...

  3. 达拉草201771010105《面向对象程序设计(java)》第八周学习总结

    达拉草201771010105<面向对象程序设计(java)>第八周学习总结 实验六接口的定义与使用 实验时间 2018-10-18 1.实验目的与要求 (1) 掌握接口定义方法: (2) ...

  4. Angular 从入坑到挖坑 - 表单控件概览

    一.Overview angular 入坑记录的笔记第三篇,介绍 angular 中表单控件的相关概念,了解如何在 angular 中创建一个表单,以及如何针对表单控件进行数据校验. 对应官方文档地址 ...

  5. JDBC大数据的采取

    ## JDBC的大类型数据的存取 ## # 基本概念: |-- 大文本类型数据和大二进制数据: 主要思想用于将大型的二进制数据(字节) 或是大型的文本数据(字符)从磁盘文件中读取 到数据库中,或是从数 ...

  6. 微信小程序学习 动手撸一个校园网小程序

    动手撸一个校园网微信小程序 高考完毕,想必广大学子和家长们都在忙着查询各所高校的信息,刚好上手微信小程序,当练手也当为自己的学校做点宣传,便当即撸了一个校园网微信小程序. 效果预览 源码地址:Gith ...

  7. 前端的事件冒泡(例如点击一次onclick事件执行两次)解决办法

    问题概要: 当我运用antd 中 radio组件的时候发现radio组件是有bug的 就是你不能给他赋予id 和 value,同时也绑定不上onclick等事件.举个例子: 可以看到 你就算赋予了id ...

  8. css手写一个表头固定

    Bootstrap,layui等前端框架里面都对表头固定,表格滚动有实现,偏偏刚入职的公司选择了手动渲染表格,后期又觉得表格数据拉太长想要做表头固定.为了避免对代码改动太大,所以决定手写表头固定 主要 ...

  9. AJAX 的 Ajax返回数据之前的loading等待效果(gif效果等)

    首先,我们通过ajax请求,向后台传递参数,然后后台经过一系列的运算之后向前台返还数据,我希望在等待数据成功返还之前可以展示一个loading.gif图 不废话,在页面上执行点击事件(<a sc ...

  10. Git将文件上传至Github过程

    1.安装Git工具(在这里就不多说了) 2.我们需要先创建一个本地的版本库(其实也就是一个文件夹). 你可以直接在桌面右击新建文件夹,也可以右击打开Git bash命令行窗口通过命令来创建. 现在我通 ...