目录

协议

五种认证方式

  • 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. 在python使用selenium获取动态网页信息并用BeautifulSoup进行解析--动态网页爬虫

    爬虫抓取数据时有些数据是动态数据,例如是用js动态加载的,使用普通的urllib2 抓取数据是找不到相关数据的,这是爬虫初学者在使用的过程中,最容易发生的情况,明明在浏览器里有相应的信息,但是在pyt ...

  2. MySQL show命令的用法

    show tables或show tables from database_name; // 显示当前数据库中所有表的名称 show databases; // 显示mysql中所有数据库的名称 sh ...

  3. 《深入理解 Java 虚拟机》读书笔记:类文件结构

    正文 一.无关性的基石 1.两种无关性 平台无关性: Java 程序的运行不受计算机平台的限制,"一次编写,到处运行". 语言无关性: Java 虚拟机只与 Class 文件关联, ...

  4. 何用Java8 Stream API进行数据抽取与收集

    上一篇中我们通过一个实例看到了Java8 Stream API 相较于传统的的Java 集合操作的简洁与优势,本篇我们依然借助于一个实际的例子来看看Java8 Stream API 如何抽取及收集数据 ...

  5. 爬虫(三)解析js,抓取优酷免费视频的真实播放地址

    工具:google浏览器 + fiddler抓包工具 说明:这里不贴代码,[只讲思路!!!] 原始url = https://v.youku.com/v_show/id_XMzIwNjgyMDgwOA ...

  6. Javascript学习笔记-基本概念-语法、关键字和保留字、变量

    语法 1.区分大小写 2.标识符 所谓标识符,就是指变量.函数.属性的名字,或者函数的参数. 命名规则: 第一个字符必须是一个字母.下划线(_)或一个美元符号($): 其他字符可以是字母.下划线.美元 ...

  7. TCP/IP协议族的四个层次

    OSI7层模型的小结 : 在7层模型中,每一层都提供一个特殊的网络功能.从网络功能的角度看:下面4层(物理层.数据链路层.网络层和传输层)主要提供数据传输和交换功能,即以节点到节点之间的通信为主:第4 ...

  8. 《ASP.NET Core 3框架揭秘》读者群,欢迎加入

    作为一个17年的.NET开发者,我对一件事特别不能理解:我们的计算机图书市场充斥着一系列介绍ASP.NET Web Forms.ASP.NET MVC.ASP.NET Web API的书籍,但是却找不 ...

  9. FCC成都社区·前端周刊 第 1 期

    01. 2018 JavaScript 测试概览 文章介绍了JavaScript测试的关键术语.测试类型.工具和方法,并简要分析了工具jsdom.Istanbul.Karma.Chai.Wallaby ...

  10. MySQL数据库无完整备份删库,除了跑路还能怎么办?

    1.背景 前段时间,由于运维同事的一次误操作,清空了内网核心数据库,导致了公司内部管理系统长时间不可用,大量知识库内容由于没有备份险些丢失. 结合这两天微盟的删库跑路事件,我们可以看到,数据库的备份与 ...