AspNetCore3.1_Secutiry源码解析_6_Authentication_OpenIdConnect
目录
- aspnetcore3.1_secutiry源码解析_1_目录
- aspnetcore3.1_secutiry源码解析_2_authentication_核心流程
- aspnetcore3.1_secutiry源码解析_3_authentication_cookies
- aspnetcore3.1_secutiry源码解析_4_authentication_jwtbear
- aspnetcore3.1_secutiry源码解析_5_authentication_oauth
- aspnetcore3.1_secutiry源码解析_6_authentication_openidconnect
- aspnetcore3.1_secutiry源码解析_7_authentication_其他
- aspnetcore3.1_secutiry源码解析_8_authorization_授权框架
oidc简介
oidc是基于oauth2.0的上层协议。
oauth有点像卖电影票的,只关心用户能不能进电影院,不关心用户是谁。而oidc则像身份证,扫描就可以上飞机,一次扫描,机场不仅能知道你是否能上飞机,还可以知道你的身份信息。
oidc兼容oauth2.0, 可以实现跨*域的sso(单点登录、登出),下个系列要学习的identityserver4就是对oidc协议族的一个具体实现框架。
更多理论知识看下面的参考资料,本系列主要过下源码脉络
博客园
协议
依赖注入
默认架构名称是openidconnect,处理器类是openidconnecthandler,配置类是openidconnectoptions
public static authenticationbuilder addopenidconnect(this authenticationbuilder builder) => builder.addopenidconnect(openidconnectdefaults.authenticationscheme, _ => { }); public static authenticationbuilder addopenidconnect(this authenticationbuilder builder, action<openidconnectoptions> configureoptions) => builder.addopenidconnect(openidconnectdefaults.authenticationscheme, configureoptions); public static authenticationbuilder addopenidconnect(this authenticationbuilder builder, string authenticationscheme, action<openidconnectoptions> configureoptions) => builder.addopenidconnect(authenticationscheme, openidconnectdefaults.displayname, configureoptions); public static authenticationbuilder addopenidconnect(this authenticationbuilder builder, string authenticationscheme, string displayname, action<openidconnectoptions> configureoptions) { builder.services.tryaddenumerable(servicedescriptor.singleton<ipostconfigureoptions<openidconnectoptions>, openidconnectpostconfigureoptions>()); return builder.addremotescheme<openidconnectoptions, openidconnecthandler>(authenticationscheme, displayname, configureoptions); }
配置类 - openidconnectoptions
构造函数
callbackpath: 回调地址,即远程认证之后跳回的地址
signedoutcallbackpath:登出后的回调地址
remotesignoutpath:远程登出地址
scope添加openid(用户id),profile(用户基本信息),所以如果client没有这两个基本的权限是会被远程认证拒绝的。
删除了nonce,aud等claim,添加了sub(用户id,必须有),name,profile,email等claim。mapuniquejsonkey方法的意思是如果某claim无值,远程认证服务返回的用户json数据中中存在此key且有值,则将值插入claim中,否则什么也不做。
然后new了防重放攻击的nonce cookie。
public openidconnectoptions() { callbackpath = new pathstring("/signin-oidc"); signedoutcallbackpath = new pathstring("/signout-callback-oidc"); remotesignoutpath = new pathstring("/signout-oidc"); events = new openidconnectevents(); scope.add("openid"); scope.add("profile"); claimactions.deleteclaim("nonce"); claimactions.deleteclaim("aud"); claimactions.deleteclaim("azp"); claimactions.deleteclaim("acr"); claimactions.deleteclaim("iss"); claimactions.deleteclaim("iat"); claimactions.deleteclaim("nbf"); claimactions.deleteclaim("exp"); claimactions.deleteclaim("at_hash"); claimactions.deleteclaim("c_hash"); claimactions.deleteclaim("ipaddr"); claimactions.deleteclaim("platf"); claimactions.deleteclaim("ver"); // http://openid.net/specs/openid-connect-core-1_0.html#standardclaims claimactions.mapuniquejsonkey("sub", "sub"); claimactions.mapuniquejsonkey("name", "name"); claimactions.mapuniquejsonkey("given_name", "given_name"); claimactions.mapuniquejsonkey("family_name", "family_name"); claimactions.mapuniquejsonkey("profile", "profile"); claimactions.mapuniquejsonkey("email", "email"); _noncecookiebuilder = new openidconnectnoncecookiebuilder(this) { name = openidconnectdefaults.cookienonceprefix, httponly = true, samesite = samesitemode.none, securepolicy = cookiesecurepolicy.sameasrequest, isessential = true, }; }
配置校验 - validate
父类remoteauthenticationoptions会校验signinschema不允许与当前schema相同(signinschema微软只提供了cookie的实现,登录似乎除了cookie没有别的方式可以维持登录态?)
校验max-age不能为负数
clientid不能为空
callbackpath必须有值
configurationmanager不能为null
public override void validate() { base.validate(); if (maxage.hasvalue && maxage.value < timespan.zero) { throw new argumentoutofrangeexception(nameof(maxage), maxage.value, "the value must not be a negative timespan."); } if (string.isnullorempty(clientid)) { throw new argumentexception("options.clientid must be provided", nameof(clientid)); } if (!callbackpath.hasvalue) { throw new argumentexception("options.callbackpath must be provided.", nameof(callbackpath)); } if (configurationmanager == null) { throw new invalidoperationexception($"provide {nameof(authority)}, {nameof(metadataaddress)}, " + $"{nameof(configuration)}, or {nameof(configurationmanager)} to {nameof(openidconnectoptions)}"); } }
属性
/// <summary> /// gets or sets timeout value in milliseconds for back channel communications with the remote identity provider. /// </summary> /// <value> /// the back channel timeout. /// </value> public timespan backchanneltimeout { get; set; } = timespan.fromseconds(60); /// <summary> /// the httpmessagehandler used to communicate with remote identity provider. /// this cannot be set at the same time as backchannelcertificatevalidator unless the value /// can be downcast to a webrequesthandler. /// </summary> public httpmessagehandler backchannelhttphandler { get; set; } /// <summary> /// used to communicate with the remote identity provider. /// </summary> public httpclient backchannel { get; set; } /// <summary> /// gets or sets the type used to secure data. /// </summary> public idataprotectionprovider dataprotectionprovider { get; set; } /// <summary> /// the request path within the application's base path where the user-agent will be returned. /// the middleware will process this request when it arrives. /// </summary> public pathstring callbackpath { get; set; } /// <summary> /// gets or sets the optional path the user agent is redirected to if the user /// doesn't approve the authorization demand requested by the remote server. /// this property is not set by default. in this case, an exception is thrown /// if an access_denied response is returned by the remote authorization server. /// </summary> public pathstring accessdeniedpath { get; set; } /// <summary> /// gets or sets the name of the parameter used to convey the original location /// of the user before the remote challenge was triggered up to the access denied page. /// this property is only used when the <see cref="accessdeniedpath"/> is explicitly specified. /// </summary> // note: this deliberately matches the default parameter name used by the cookie handler. public string returnurlparameter { get; set; } = "returnurl"; /// <summary> /// gets or sets the authentication scheme corresponding to the middleware /// responsible of persisting user's identity after a successful authentication. /// this value typically corresponds to a cookie middleware registered in the startup class. /// when omitted, <see cref="authenticationoptions.defaultsigninscheme"/> is used as a fallback value. /// </summary> public string signinscheme { get; set; } /// <summary> /// gets or sets the time limit for completing the authentication flow (15 minutes by default). /// </summary> public timespan remoteauthenticationtimeout { get; set; } = timespan.fromminutes(15); public new remoteauthenticationevents events { get => (remoteauthenticationevents)base.events; set => base.events = value; } /// <summary> /// defines whether access and refresh tokens should be stored in the /// <see cref="authenticationproperties"/> after a successful authorization. /// this property is set to <c>false</c> by default to reduce /// the size of the final authentication cookie. /// </summary> public bool savetokens { get; set; } /// <summary> /// determines the settings used to create the correlation cookie before the /// cookie gets added to the response. /// </summary> public cookiebuilder correlationcookie { get => _correlationcookiebuilder; set => _correlationcookiebuilder = value ?? throw new argumentnullexception(nameof(value)); }
配置后处理逻辑 - openidconnectpostconfigureoptions
主要处理如果dataprotectionprovider,statedataformat等对象没有配置的话,则构造默认实现类。options.metadataaddress += ".well-known/openid-configuration",这是配置的元数据地址,描述了oidc的所有接口地址和其他信息。
public class openidconnectpostconfigureoptions : ipostconfigureoptions<openidconnectoptions> { private readonly idataprotectionprovider _dp; public openidconnectpostconfigureoptions(idataprotectionprovider dataprotection) { _dp = dataprotection; } /// <summary> /// invoked to post configure a toptions instance. /// </summary> /// <param name="name">the name of the options instance being configured.</param> /// <param name="options">the options instance to configure.</param> public void postconfigure(string name, openidconnectoptions options) { options.dataprotectionprovider = options.dataprotectionprovider ?? _dp; if (string.isnullorempty(options.signoutscheme)) { options.signoutscheme = options.signinscheme; } if (options.statedataformat == null) { var dataprotector = options.dataprotectionprovider.createprotector( typeof(openidconnecthandler).fullname, name, "v1"); options.statedataformat = new propertiesdataformat(dataprotector); } if (options.stringdataformat == null) { var dataprotector = options.dataprotectionprovider.createprotector( typeof(openidconnecthandler).fullname, typeof(string).fullname, name, "v1"); options.stringdataformat = new securedataformat<string>(new stringserializer(), dataprotector); } if (string.isnullorempty(options.tokenvalidationparameters.validaudience) && !string.isnullorempty(options.clientid)) { options.tokenvalidationparameters.validaudience = options.clientid; } if (options.backchannel == null) { options.backchannel = new httpclient(options.backchannelhttphandler ?? new httpclienthandler()); options.backchannel.defaultrequestheaders.useragent.parseadd("microsoft asp.net core openidconnect handler"); options.backchannel.timeout = options.backchanneltimeout; options.backchannel.maxresponsecontentbuffersize = 1024 * 1024 * 10; // 10 mb } if (options.configurationmanager == null) { if (options.configuration != null) { options.configurationmanager = new staticconfigurationmanager<openidconnectconfiguration>(options.configuration); } else if (!(string.isnullorempty(options.metadataaddress) && string.isnullorempty(options.authority))) { if (string.isnullorempty(options.metadataaddress) && !string.isnullorempty(options.authority)) { options.metadataaddress = options.authority; if (!options.metadataaddress.endswith("/", stringcomparison.ordinal)) { options.metadataaddress += "/"; } options.metadataaddress += ".well-known/openid-configuration"; } if (options.requirehttpsmetadata && !options.metadataaddress.startswith("https://", stringcomparison.ordinalignorecase)) { throw new invalidoperationexception("the metadataaddress or authority must use https unless disabled for development by setting requirehttpsmetadata=false."); } options.configurationmanager = new configurationmanager<openidconnectconfiguration>(options.metadataaddress, new openidconnectconfigurationretriever(), new httpdocumentretriever(options.backchannel) { requirehttps = options.requirehttpsmetadata }); } } } private class stringserializer : idataserializer<string> { public string deserialize(byte[] data) { return encoding.utf8.getstring(data); } public byte[] serialize(string model) { return encoding.utf8.getbytes(model); } }
处理器类 - openidconnecthandler
处理认证 - handremoteauthenticate
oidc登录示例图
代码解析
mysite向oidc的认证节点地址/connect/authorize发送请求,oidc站点根据response_mode用get或者form_post方式调用mysite的回调地址mysite/signin-oidc,handleremoteauthenticateasync就是处理oidc站点的响应的方法。
- 判断get/post,从请求中提取参数,如果是get请求,id_token,access_token不允许放在query中
- 从state参数读取信息放到properties
- 校验correlationid,防跨站伪造攻击
- 如果返回了id_token,校验token,将信息写入httpcontext
- 如果返回了授权码code的处理
代码量还是比较多,有些地方目前还不是特别理解,需求后面熟悉协议内容在回过头来看下。总体上就是对oidc站点返回信息的校验和处理。
/// <summary> /// invoked to process incoming openidconnect messages. /// </summary> /// <returns>an <see cref="handlerequestresult"/>.</returns> protected override async task<handlerequestresult> handleremoteauthenticateasync() { logger.enteringopenidauthenticationhandlerhandleremoteauthenticateasync(gettype().fullname); openidconnectmessage authorizationresponse = null; if (string.equals(request.method, "get", stringcomparison.ordinalignorecase)) { authorizationresponse = new openidconnectmessage(request.query.select(pair => new keyvaluepair<string, string[]>(pair.key, pair.value))); // response_mode=query (explicit or not) and a response_type containing id_token // or token are not considered as a safe combination and must be rejected. // see http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#security if (!string.isnullorempty(authorizationresponse.idtoken) || !string.isnullorempty(authorizationresponse.accesstoken)) { if (options.skipunrecognizedrequests) { // not for us? return handlerequestresult.skiphandler(); } return handlerequestresult.fail("an openid connect response cannot contain an " + "identity token or an access token when using response_mode=query"); } } // assumption: if the contenttype is "application/x-www-form-urlencoded" it should be safe to read as it is small. else if (string.equals(request.method, "post", stringcomparison.ordinalignorecase) && !string.isnullorempty(request.contenttype) // may have media/type; charset=utf-8, allow partial match. && request.contenttype.startswith("application/x-www-form-urlencoded", stringcomparison.ordinalignorecase) && request.body.canread) { var form = await request.readformasync(); authorizationresponse = new openidconnectmessage(form.select(pair => new keyvaluepair<string, string[]>(pair.key, pair.value))); } if (authorizationresponse == null) { if (options.skipunrecognizedrequests) { // not for us? return handlerequestresult.skiphandler(); } return handlerequestresult.fail("no message."); } authenticationproperties properties = null; try { properties = readpropertiesandclearstate(authorizationresponse); var messagereceivedcontext = await runmessagereceivedeventasync(authorizationresponse, properties); if (messagereceivedcontext.result != null) { return messagereceivedcontext.result; } authorizationresponse = messagereceivedcontext.protocolmessage; properties = messagereceivedcontext.properties; if (properties == null || properties.items.count == 0) { // fail if state is missing, it's required for the correlation id. if (string.isnullorempty(authorizationresponse.state)) { // this wasn't a valid oidc message, it may not have been intended for us. logger.nulloremptyauthorizationresponsestate(); if (options.skipunrecognizedrequests) { return handlerequestresult.skiphandler(); } return handlerequestresult.fail(resources.messagestateisnullorempty); } properties = readpropertiesandclearstate(authorizationresponse); } if (properties == null) { logger.unabletoreadauthorizationresponsestate(); if (options.skipunrecognizedrequests) { // not for us? return handlerequestresult.skiphandler(); } // if state exists and we failed to 'unprotect' this is not a message we should process. return handlerequestresult.fail(resources.messagestateisinvalid); } if (!validatecorrelationid(properties)) { return handlerequestresult.fail("correlation failed.", properties); } // if any of the error fields are set, throw error null if (!string.isnullorempty(authorizationresponse.error)) { // note: access_denied errors are special protocol errors indicating the user didn't // approve the authorization demand requested by the remote authorization server. // since it's a frequent scenario (that is not caused by incorrect configuration), // denied errors are handled differently using handleaccessdeniederrorasync(). // visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information. if (string.equals(authorizationresponse.error, "access_denied", stringcomparison.ordinal)) { var result = await handleaccessdeniederrorasync(properties); if (!result.none) { return result; } } return handlerequestresult.fail(createopenidconnectprotocolexception(authorizationresponse, response: null), properties); } if (_configuration == null && options.configurationmanager != null) { logger.updatingconfiguration(); _configuration = await options.configurationmanager.getconfigurationasync(context.requestaborted); } populatesessionproperties(authorizationresponse, properties); claimsprincipal user = null; jwtsecuritytoken jwt = null; string nonce = null; var validationparameters = options.tokenvalidationparameters.clone(); // hybrid or implicit flow if (!string.isnullorempty(authorizationresponse.idtoken)) { logger.receivedidtoken(); user = validatetoken(authorizationresponse.idtoken, properties, validationparameters, out jwt); nonce = jwt.payload.nonce; if (!string.isnullorempty(nonce)) { nonce = readnoncecookie(nonce); } var tokenvalidatedcontext = await runtokenvalidatedeventasync(authorizationresponse, null, user, properties, jwt, nonce); if (tokenvalidatedcontext.result != null) { return tokenvalidatedcontext.result; } authorizationresponse = tokenvalidatedcontext.protocolmessage; user = tokenvalidatedcontext.principal; properties = tokenvalidatedcontext.properties; jwt = tokenvalidatedcontext.securitytoken; nonce = tokenvalidatedcontext.nonce; } options.protocolvalidator.validateauthenticationresponse(new openidconnectprotocolvalidationcontext() { clientid = options.clientid, protocolmessage = authorizationresponse, validatedidtoken = jwt, nonce = nonce }); openidconnectmessage tokenendpointresponse = null; // authorization code or hybrid flow if (!string.isnullorempty(authorizationresponse.code)) { var authorizationcodereceivedcontext = await runauthorizationcodereceivedeventasync(authorizationresponse, user, properties, jwt); if (authorizationcodereceivedcontext.result != null) { return authorizationcodereceivedcontext.result; } authorizationresponse = authorizationcodereceivedcontext.protocolmessage; user = authorizationcodereceivedcontext.principal; properties = authorizationcodereceivedcontext.properties; var tokenendpointrequest = authorizationcodereceivedcontext.tokenendpointrequest; // if the developer redeemed the code themselves... tokenendpointresponse = authorizationcodereceivedcontext.tokenendpointresponse; jwt = authorizationcodereceivedcontext.jwtsecuritytoken; if (!authorizationcodereceivedcontext.handledcoderedemption) { tokenendpointresponse = await redeemauthorizationcodeasync(tokenendpointrequest); } var tokenresponsereceivedcontext = await runtokenresponsereceivedeventasync(authorizationresponse, tokenendpointresponse, user, properties); if (tokenresponsereceivedcontext.result != null) { return tokenresponsereceivedcontext.result; } authorizationresponse = tokenresponsereceivedcontext.protocolmessage; tokenendpointresponse = tokenresponsereceivedcontext.tokenendpointresponse; user = tokenresponsereceivedcontext.principal; properties = tokenresponsereceivedcontext.properties; // no need to validate signature when token is received using "code flow" as per spec // [http://openid.net/specs/openid-connect-core-1_0.html#idtokenvalidation]. validationparameters.requiresignedtokens = false; // at least a cursory validation is required on the new idtoken, even if we've already validated the one from the authorization response. // and we'll want to validate the new jwt in validatetokenresponse. var tokenendpointuser = validatetoken(tokenendpointresponse.idtoken, properties, validationparameters, out var tokenendpointjwt); // avoid reading & deleting the nonce cookie, running the event, etc, if it was already done as part of the authorization response validation. if (user == null) { nonce = tokenendpointjwt.payload.nonce; if (!string.isnullorempty(nonce)) { nonce = readnoncecookie(nonce); } var tokenvalidatedcontext = await runtokenvalidatedeventasync(authorizationresponse, tokenendpointresponse, tokenendpointuser, properties, tokenendpointjwt, nonce); if (tokenvalidatedcontext.result != null) { return tokenvalidatedcontext.result; } authorizationresponse = tokenvalidatedcontext.protocolmessage; tokenendpointresponse = tokenvalidatedcontext.tokenendpointresponse; user = tokenvalidatedcontext.principal; properties = tokenvalidatedcontext.properties; jwt = tokenvalidatedcontext.securitytoken; nonce = tokenvalidatedcontext.nonce; } else { if (!string.equals(jwt.subject, tokenendpointjwt.subject, stringcomparison.ordinal)) { throw new securitytokenexception("the sub claim does not match in the id_token's from the authorization and token endpoints."); } jwt = tokenendpointjwt; } // validate the token response if it wasn't provided manually if (!authorizationcodereceivedcontext.handledcoderedemption) { options.protocolvalidator.validatetokenresponse(new openidconnectprotocolvalidationcontext() { clientid = options.clientid, protocolmessage = tokenendpointresponse, validatedidtoken = jwt, nonce = nonce }); } } if (options.savetokens) { savetokens(properties, tokenendpointresponse ?? authorizationresponse); } if (options.getclaimsfromuserinfoendpoint) { return await getuserinformationasync(tokenendpointresponse ?? authorizationresponse, jwt, user, properties); } else { using (var payload = jsondocument.parse("{}")) { var identity = (claimsidentity)user.identity; foreach (var action in options.claimactions) { action.run(payload.rootelement, identity, claimsissuer); } } } return handlerequestresult.success(new authenticationticket(user, properties, scheme.name)); } catch (exception exception) { logger.exceptionprocessingmessage(exception); // refresh the configuration for exceptions that may be caused by key rollovers. the user can also request a refresh in the event. if (options.refreshonissuerkeynotfound && exception is securitytokensignaturekeynotfoundexception) { if (options.configurationmanager != null) { logger.configurationmanagerrequestrefreshcalled(); options.configurationmanager.requestrefresh(); } } var authenticationfailedcontext = await runauthenticationfailedeventasync(authorizationresponse, exception); if (authenticationfailedcontext.result != null) { return authenticationfailedcontext.result; } return handlerequestresult.fail(exception, properties); } }
处理远程登出 - handleremotesignoutasync
openidconecthandler跟oauthhandler一样,继承自remoteauthenticationhandler,但是openid还实现了iauthenticationsignouthandler接口,因为openid是支持单点登录登出的,本地登出之后需要通知认证服务远程登出(注销本地站点cookie),这样实现帐号的同步登出(注销sso站点cookie)。
- 远程登出支持get和form-post两种提交方式,客户端根据请求方式,将报文拼装好。
- 触发远程登出事件
- 使用signoutscheme认证,得到身份信息 - context.authenticateasync(options.signoutscheme)
- context.proerties中必须有iss信息,issuer就是提供认证方
- 调用本地登出方法 - context.signoutasync(options.signoutscheme)
protected virtual async task<bool> handleremotesignoutasync() { openidconnectmessage message = null; if (string.equals(request.method, "get", stringcomparison.ordinalignorecase)) { message = new openidconnectmessage(request.query.select(pair => new keyvaluepair<string, string[]>(pair.key, pair.value))); } // assumption: if the contenttype is "application/x-www-form-urlencoded" it should be safe to read as it is small. else if (string.equals(request.method, "post", stringcomparison.ordinalignorecase) && !string.isnullorempty(request.contenttype) // may have media/type; charset=utf-8, allow partial match. && request.contenttype.startswith("application/x-www-form-urlencoded", stringcomparison.ordinalignorecase) && request.body.canread) { var form = await request.readformasync(); message = new openidconnectmessage(form.select(pair => new keyvaluepair<string, string[]>(pair.key, pair.value))); } var remotesignoutcontext = new remotesignoutcontext(context, scheme, options, message); await events.remotesignout(remotesignoutcontext); if (remotesignoutcontext.result != null) { if (remotesignoutcontext.result.handled) { logger.remotesignouthandledresponse(); return true; } if (remotesignoutcontext.result.skipped) { logger.remotesignoutskipped(); return false; } if (remotesignoutcontext.result.failure != null) { throw new invalidoperationexception("an error was returned from the remotesignout event.", remotesignoutcontext.result.failure); } } if (message == null) { return false; } // try to extract the session identifier from the authentication ticket persisted by the sign-in handler. // if the identifier cannot be found, bypass the session identifier checks: this may indicate that the // authentication cookie was already cleared, that the session identifier was lost because of a lossy // external/application cookie conversion or that the identity provider doesn't support sessions. var principal = (await context.authenticateasync(options.signoutscheme))?.principal; var sid = principal?.findfirst(jwtregisteredclaimnames.sid)?.value; if (!string.isnullorempty(sid)) { // ensure a 'sid' parameter was sent by the identity provider. if (string.isnullorempty(message.sid)) { logger.remotesignoutsessionidmissing(); return true; } // ensure the 'sid' parameter corresponds to the 'sid' stored in the authentication ticket. if (!string.equals(sid, message.sid, stringcomparison.ordinal)) { logger.remotesignoutsessionidinvalid(); return true; } } var iss = principal?.findfirst(jwtregisteredclaimnames.iss)?.value; if (!string.isnullorempty(iss)) { // ensure a 'iss' parameter was sent by the identity provider. if (string.isnullorempty(message.iss)) { logger.remotesignoutissuermissing(); return true; } // ensure the 'iss' parameter corresponds to the 'iss' stored in the authentication ticket. if (!string.equals(iss, message.iss, stringcomparison.ordinal)) { logger.remotesignoutissuerinvalid(); return true; } } logger.remotesignout(); // we've received a remote sign-out request await context.signoutasync(options.signoutscheme); return true; }
处理本地登出 - context.signoutasync(options.signoutscheme)
方法的注释:将用户重定向到身份认证站点登出。
- forwardxxx是所有认证配置项的基类,可以拦截使用自己配置的scheme。
- 构造要发送给oidc服务的报文,包括issueraddress(endsessionendpoint:即结束会话节点地址),postlogoutredirecturi(登出回跳地址)等。
- 构造redirecturi(登录流程结束最终回到的地址):优先使用httpcontext.properties中的redirecturi,然后使用配置中的signedoutredirecturi,最后使用请求源地址。
- 获取idtoken,放到登出请求中
- state字段加密后(包含了redirecturi等信息),放入请求消息
- 给oidc站点发送get或者formpost请求
/// <summary> /// redirect user to the identity provider for sign out /// </summary> /// <returns>a task executing the sign out procedure</returns> public async virtual task signoutasync(authenticationproperties properties) { var target = resolvetarget(options.forwardsignout); if (target != null) { await context.signoutasync(target, properties); return; } properties = properties ?? new authenticationproperties(); logger.enteringopenidauthenticationhandlerhandlesignoutasync(gettype().fullname); if (_configuration == null && options.configurationmanager != null) { _configuration = await options.configurationmanager.getconfigurationasync(context.requestaborted); } var message = new openidconnectmessage() { enabletelemetryparameters = !options.disabletelemetry, issueraddress = _configuration?.endsessionendpoint ?? string.empty, // redirect back to signeoutcallbackpath first before user agent is redirected to actual post logout redirect uri postlogoutredirecturi = buildredirecturiifrelative(options.signedoutcallbackpath) }; // get the post redirect uri. if (string.isnullorempty(properties.redirecturi)) { properties.redirecturi = buildredirecturiifrelative(options.signedoutredirecturi); if (string.isnullorwhitespace(properties.redirecturi)) { properties.redirecturi = originalpathbase + originalpath + request.querystring; } } logger.postsignoutredirect(properties.redirecturi); // attach the identity token to the logout request when possible. message.idtokenhint = await context.gettokenasync(options.signoutscheme, openidconnectparameternames.idtoken); var redirectcontext = new redirectcontext(context, scheme, options, properties) { protocolmessage = message }; await events.redirecttoidentityproviderforsignout(redirectcontext); if (redirectcontext.handled) { logger.redirecttoidentityproviderforsignouthandledresponse(); return; } message = redirectcontext.protocolmessage; if (!string.isnullorempty(message.state)) { properties.items[openidconnectdefaults.userstatepropertieskey] = message.state; } message.state = options.statedataformat.protect(properties); if (string.isnullorempty(message.issueraddress)) { throw new invalidoperationexception("cannot redirect to the end session endpoint, the configuration may be missing or invalid."); } if (options.authenticationmethod == openidconnectredirectbehavior.redirectget) { var redirecturi = message.createlogoutrequesturl(); if (!uri.iswellformeduristring(redirecturi, urikind.absolute)) { logger.invalidlogoutquerystringredirecturl(redirecturi); } response.redirect(redirecturi); } else if (options.authenticationmethod == openidconnectredirectbehavior.formpost) { var content = message.buildformpost(); var buffer = encoding.utf8.getbytes(content); response.contentlength = buffer.length; response.contenttype = "text/html;charset=utf-8"; // emit cache-control=no-cache to prevent client caching. response.headers[headernames.cachecontrol] = "no-cache, no-store"; response.headers[headernames.pragma] = "no-cache"; response.headers[headernames.expires] = headervalueepocdate; await response.body.writeasync(buffer, 0, buffer.length); } else { throw new notimplementedexception($"an unsupported authentication method has been configured: {options.authenticationmethod}"); } logger.authenticationschemesignedout(scheme.name); }
oidc处理完后跳到回调地址
oidc站点处理完登出请求之后(怎么处理的,应该是清除了oidc的cookie,或许回收了token?目前不清楚。后面看identitserver怎么实现的),回跳到callback地址,执行下面的callback方法
callback方法很简单,就是将state字段解码,将redirect_uri拿到,然后跳过去。
/// <summary> /// response to the callback from openid provider after session ended. /// </summary> /// <returns>a task executing the callback procedure</returns> protected async virtual task<bool> handlesignoutcallbackasync() { var message = new openidconnectmessage(request.query.select(pair => new keyvaluepair<string, string[]>(pair.key, pair.value))); authenticationproperties properties = null; if (!string.isnullorempty(message.state)) { properties = options.statedataformat.unprotect(message.state); } var signout = new remotesignoutcontext(context, scheme, options, message) { properties = properties, }; await events.signedoutcallbackredirect(signout); if (signout.result != null) { if (signout.result.handled) { logger.signoutcallbackredirecthandledresponse(); return true; } if (signout.result.skipped) { logger.signoutcallbackredirectskipped(); return false; } if (signout.result.failure != null) { throw new invalidoperationexception("an error was returned from the signedoutcallbackredirect event.", signout.result.failure); } } properties = signout.properties; if (!string.isnullorempty(properties?.redirecturi)) { response.redirect(properties.redirecturi); } return true; }
登出时序图
可以看到,oidc的登出只处理了oidc认证站点的cookie,mysite本地的cookie是没有处理的,因为当前schema是openidconnnect,本地cookie是signinschema的事情,所以登出需要掉两次signout方法
httpcontext.signoutasync("cookies"); //清除本地cookie httpcontext.signoutasync("openidconnect") //清除远程sso站点cookie
处理质询 - handlechallengeasync
- oauth&pkce的处理,pkce = proof key for code exchange。主要用于nativeapp防跨站攻击的,因为nativeapp没有cookie支持,无法使用state字段,所以需要其他的安全保障。
- 拼装请求参数,根据配置,如果是get,302跳转到oidc站点;如果是form-post,提交表单到oidc站点。
/// <summary> /// responds to a 401 challenge. sends an openidconnect message to the 'identity authority' to obtain an identity. /// </summary> /// <returns></returns> protected override async task handlechallengeasync(authenticationproperties properties) { await handlechallengeasyncinternal(properties); var location = context.response.headers[headernames.location]; if (location == stringvalues.empty) { location = "(not set)"; } var cookie = context.response.headers[headernames.setcookie]; if (cookie == stringvalues.empty) { cookie = "(not set)"; } logger.handlechallenge(location, cookie); } private async task handlechallengeasyncinternal(authenticationproperties properties) { logger.enteringopenidauthenticationhandlerhandleunauthorizedasync(gettype().fullname); // order for local redirecturi // 1. challenge.properties.redirecturi // 2. currenturi if redirecturi is not set) if (string.isnullorempty(properties.redirecturi)) { properties.redirecturi = originalpathbase + originalpath + request.querystring; } logger.postauthenticationlocalredirect(properties.redirecturi); if (_configuration == null && options.configurationmanager != null) { _configuration = await options.configurationmanager.getconfigurationasync(context.requestaborted); } var message = new openidconnectmessage { clientid = options.clientid, enabletelemetryparameters = !options.disabletelemetry, issueraddress = _configuration?.authorizationendpoint ?? string.empty, redirecturi = buildredirecturi(options.callbackpath), resource = options.resource, responsetype = options.responsetype, prompt = properties.getparameter<string>(openidconnectparameternames.prompt) ?? options.prompt, scope = string.join(" ", properties.getparameter<icollection<string>>(openidconnectparameternames.scope) ?? options.scope), }; // https://tools.ietf.org/html/rfc7636 if (options.usepkce && options.responsetype == openidconnectresponsetype.code) { var bytes = new byte[32]; cryptorandom.getbytes(bytes); var codeverifier = base64urltextencoder.encode(bytes); // store this for use during the code redemption. see runauthorizationcodereceivedeventasync. properties.items.add(oauthconstants.codeverifierkey, codeverifier); using var sha256 = sha256.create(); var challengebytes = sha256.computehash(encoding.utf8.getbytes(codeverifier)); var codechallenge = webencoders.base64urlencode(challengebytes); message.parameters.add(oauthconstants.codechallengekey, codechallenge); message.parameters.add(oauthconstants.codechallengemethodkey, oauthconstants.codechallengemethods256); } // add the 'max_age' parameter to the authentication request if maxage is not null. // see http://openid.net/specs/openid-connect-core-1_0.html#authrequest var maxage = properties.getparameter<timespan?>(openidconnectparameternames.maxage) ?? options.maxage; if (maxage.hasvalue) { message.maxage = convert.toint64(math.floor((maxage.value).totalseconds)) .tostring(cultureinfo.invariantculture); } // omitting the response_mode parameter when it already corresponds to the default // response_mode used for the specified response_type is recommended by the specifications. // see http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#responsemodes if (!string.equals(options.responsetype, openidconnectresponsetype.code, stringcomparison.ordinal) || !string.equals(options.responsemode, openidconnectresponsemode.query, stringcomparison.ordinal)) { message.responsemode = options.responsemode; } if (options.protocolvalidator.requirenonce) { message.nonce = options.protocolvalidator.generatenonce(); writenoncecookie(message.nonce); } generatecorrelationid(properties); var redirectcontext = new redirectcontext(context, scheme, options, properties) { protocolmessage = message }; await events.redirecttoidentityprovider(redirectcontext); if (redirectcontext.handled) { logger.redirecttoidentityproviderhandledresponse(); return; } message = redirectcontext.protocolmessage; if (!string.isnullorempty(message.state)) { properties.items[openidconnectdefaults.userstatepropertieskey] = message.state; } // when redeeming a 'code' for an accesstoken, this value is needed properties.items.add(openidconnectdefaults.redirecturiforcodepropertieskey, message.redirecturi); message.state = options.statedataformat.protect(properties); if (string.isnullorempty(message.issueraddress)) { throw new invalidoperationexception( "cannot redirect to the authorization endpoint, the configuration may be missing or invalid."); } if (options.authenticationmethod == openidconnectredirectbehavior.redirectget) { var redirecturi = message.createauthenticationrequesturl(); if (!uri.iswellformeduristring(redirecturi, urikind.absolute)) { logger.invalidauthenticationrequesturl(redirecturi); } response.redirect(redirecturi); return; } else if (options.authenticationmethod == openidconnectredirectbehavior.formpost) { var content = message.buildformpost(); var buffer = encoding.utf8.getbytes(content); response.contentlength = buffer.length; response.contenttype = "text/html;charset=utf-8"; // emit cache-control=no-cache to prevent client caching. response.headers[headernames.cachecontrol] = "no-cache, no-store"; response.headers[headernames.pragma] = "no-cache"; response.headers[headernames.expires] = headervalueepocdate; await response.body.writeasync(buffer, 0, buffer.length); return; } throw new notimplementedexception($"an unsupported authentication method has been configured: {options.authenticationmethod}"); }
完
openidconnect的代码还是有点复杂的,很多细节无法覆盖到,后面学习了协议再回头梳理一下。
推荐阅读
-
Mybaits 源码解析 (十一)----- 设计模式精妙使用:静态代理和动态代理结合使用:@MapperScan将Mapper接口生成代理注入到Spring
-
Mybaits 源码解析 (十二)----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?
-
Ribbon源码解析
-
Mybaits 源码解析 (五)----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
-
Eureka获取服务列表源码解析
-
Tomcat源码分析三:Tomcat启动加载过程(一)的源码解析
-
spring5 源码深度解析-----ApplicationContext容器refresh过程
-
netty源码解解析(4.0)-23 ByteBuf内存管理:分配和释放
-
从源码解析Python的Flask框架中request对象的用法
-
AbstractQueuedSynchronizer(AQS)源码解析