identityserver4源码解析_3_认证接口
holdengong 人气:1目录
- 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;
}
加载全部内容