identityserver4源码解析_2_元数据接口
目录
- identityserver4源码解析_1_项目结构
- identityserver4源码解析_2_元数据接口
- identityserver4源码解析_3_认证接口
- identityserver4源码解析_4_令牌发放接口
- identityserver4源码解析_5_查询用户信息接口
- identityserver4源码解析_6_结束会话接口
- identityserver4源码解析_7_查询令牌信息接口
- identityserver4源码解析_8_撤销令牌接口
协议
这一系列我们都采用这样的方式,先大概看下协议,也就是需求描述,然后看idsv4怎么实现的,这样可以加深理解。
元数据接口的协议地址如下:
摘要
该协议定义了一套标准,用户能够获取到oidc服务的基本信息,包括oauth2.0相关接口地址。
webfinger - 网络指纹
先了解一下webfinger这个概念。
webfinger可以翻译成网络指纹,它定义了一套标准,描述如何通过标准的http方法去获取网络实体的资料信息。webfinger使用json来描述实体信息。
查询oidc服务元数据 - openid provider issuer discovery
可选协议。
定义了如何获取oidc服务元数据。如果客户端明确知道oidc服务的地址,可以跳过此部分。
个人理解是存在多个oidc服务的情况,可以部署一个webfinger服务,根据资源请求,路由到不同的oidc服务。
通常来说,我们只有一个oidc服务,我看了一下idsv4也没有实现这一部分协议,这里了解一下就可以了。
查询oidc服务配置信息 - openid provider configuration request
必选协议。
用于描述oidc服务各接口地址及其他配置信息。
get /.well-known/openid-configuration http/1.1 host: example.com
必须校验issuer与请求地址是否一致
启个idsrv服务调用试一下,返回结果如图
详细信息如下。
{ "issuer": "https://localhost:10000", //颁发者地址 "jwks_uri": "https://localhost:10000/.well-known/openid-configuration/jwks", //jwks接口地址,查询密钥 "authorization_endpoint": "https://localhost:10000/connect/authorize", //认证接口地址 "token_endpoint": "https://localhost:10000/connect/token", //令牌发放接口 "userinfo_endpoint": "https://localhost:10000/connect/userinfo", //查询用户信息接口 "end_session_endpoint": "https://localhost:10000/connect/endsession", //结束会话接口 "check_session_iframe": "https://localhost:10000/connect/checksession", //检查会话接口 "revocation_endpoint": "https://localhost:10000/connect/revocation", //撤销令牌接口 "introspection_endpoint": "https://localhost:10000/connect/introspect", //查询令牌详情接口 "device_authorization_endpoint": "https://localhost:10000/connect/deviceauthorization", //设备认证接口 "frontchannel_logout_supported": true, //是否支持前端登出 "frontchannel_logout_session_supported": true, //是否支持前端结束会话 "backchannel_logout_supported": true, //是否支持后端登出 "backchannel_logout_session_supported": true, //是否支持后端结束会话 "scopes_supported": [ //支持的授权范围,scope "openid", "profile", "userid", "username", "email", "mobile", "api", "offline_access" //token过期可用refresh_token刷新换取新token ], "claims_supported": [ //支持的声明 "sub", "updated_at", "locale", "zoneinfo", "birthdate", "gender", "preferred_username", "picture", "profile", "nickname", "middle_name", "given_name", "family_name", "website", "name", "userid", "username", "email", "mobile" ], "grant_types_supported": [ //支持的认证类型 "authorization_code", //授权码模式 "client_credentials", //客户端密钥模式 "refresh_token", //刷新token "implicit", //隐式流程, 一般用于单页应用javascript客户端 "password", //用户名密码模式 "urn:ietf:params:oauth:grant-type:device_code" //设备授权码 ], "response_types_supported": [ //支持的返回类型 "code", //授权码 "token", //通行令牌 "id_token", //身份令牌 "id_token token", //身份令牌+统通行令牌 "code id_token", //授权码+身份令牌 "code token", //授权码+通行令牌 "code id_token token" //授权码+身份令牌+通行令牌 ], "response_modes_supported": [ //支持的响应方法 "form_post", //form-post提交 "query", //get提交 "fragment" //fragment提交 ], "token_endpoint_auth_methods_supported": [ //发放令牌接口支持的认证方式 "client_secret_basic", //basic "client_secret_post" //post ], "id_token_signing_alg_values_supported": [ //身份令牌加密算法 "rs256" ], "subject_types_supported": [ "public" ], "code_challenge_methods_supported": [ "plain", "s256" ], "request_parameter_supported": true }
jwk - json web keys
idsv还注入这样一个接口:discoverykeyendpoint,尝试发现返回了一组密钥。协议内容如下。
get /.well-known/openid-configuration/jwks,返回结果如下
{ "keys": [ { "kty": "rsa", "use": "sig", "kid": "ls-eqor-3bkalkkuvh8q7q", "e": "aqab", "n": "08bllatz4jrtyme4bz9c7okvrzkly3kfgt5mmnslhl41nk_ev_8oudl8wmxunc2kerdnsy5xyk4aw3llvxzdivjxo9peblpsoap-werdi9gvyav-nj6ejqy3s7frskvzqybslnckm5wu0kjdqbvucfj7wfiz9ayy7ph7k10qn2utvt-qscluy0cj0stup_rquefp7_xhuw3a8iia8p6djfzibpwrvjoevwoi_zkiwfxshghoakbdlyquc2phozsqz7hvgeeapm06ypmwqvbe9_lbn2j_ul_vbuwc9kfbnozk_bmqhyf2nulwmtqmuecwk_hpjeeo62o_aft8edkgcq", "alg": "rs256" }, { "kty": "rsa", "use": "sig", "kid": "ls-eqor-3bkalkkuvh8q7q", "e": "aqab", "n": "08bllatz4jrtyme4bz9c7okvrzkly3kfgt5mmnslhl41nk_ev_8oudl8wmxunc2kerdnsy5xyk4aw3llvxzdivjxo9peblpsoap-werdi9gvyav-nj6ejqy3s7frskvzqybslnckm5wu0kjdqbvucfj7wfiz9ayy7ph7k10qn2utvt-qscluy0cj0stup_rquefp7_xhuw3a8iia8p6djfzibpwrvjoevwoi_zkiwfxshghoakbdlyquc2phozsqz7hvgeeapm06ypmwqvbe9_lbn2j_ul_vbuwc9kfbnozk_bmqhyf2nulwmtqmuecwk_hpjeeo62o_aft8edkgcq", "alg": "rs256" } ] }
源码解析
接口地址都在constants.cs这个文件,protocalroutepaths这个类里面定义的。现在知道为什么接口地址是.well-known/openid-configuration这样奇怪的一个路由了,这是oidc协议定的(对,都是产品的锅)。
oidc服务配置信息接口 - discoveryendpoint
代码很长,但是逻辑很简单,就是组装协议规定的所有地址和信息。
需要注意的支持的claims、支持的scope等信息是遍历所有identityresource、apiresource动态获取的。
基本上每个接口都可以配置是否显示在元数据文档中。
public async task<iendpointresult> processasync(httpcontext context) { _logger.logtrace("processing discovery request."); // validate http if (!httpmethods.isget(context.request.method)) { _logger.logwarning("discovery endpoint only supports get requests"); return new statuscoderesult(httpstatuscode.methodnotallowed); } _logger.logdebug("start discovery request"); if (!_options.endpoints.enablediscoveryendpoint) { _logger.loginformation("discovery endpoint disabled. 404."); return new statuscoderesult(httpstatuscode.notfound); } var baseurl = context.getidentityserverbaseurl().ensuretrailingslash(); var issueruri = context.getidentityserverissueruri(); // generate response _logger.logtrace("calling into discovery response generator: {type}", _responsegenerator.gettype().fullname); var response = await _responsegenerator.creatediscoverydocumentasync(baseurl, issueruri); return new discoverydocumentresult(response, _options.discovery.responsecacheinterval); } /// <summary> /// creates the discovery document. /// </summary> /// <param name="baseurl">the base url.</param> /// <param name="issueruri">the issuer uri.</param> public virtual async task<dictionary<string, object>> creatediscoverydocumentasync(string baseurl, string issueruri) { var entries = new dictionary<string, object> { { oidcconstants.discovery.issuer, issueruri } }; // jwks if (options.discovery.showkeyset) { if ((await keys.getvalidationkeysasync()).any()) { entries.add(oidcconstants.discovery.jwksuri, baseurl + constants.protocolroutepaths.discoverywebkeys); } } // endpoints if (options.discovery.showendpoints) { if (options.endpoints.enableauthorizeendpoint) { entries.add(oidcconstants.discovery.authorizationendpoint, baseurl + constants.protocolroutepaths.authorize); } if (options.endpoints.enabletokenendpoint) { entries.add(oidcconstants.discovery.tokenendpoint, baseurl + constants.protocolroutepaths.token); } if (options.endpoints.enableuserinfoendpoint) { entries.add(oidcconstants.discovery.userinfoendpoint, baseurl + constants.protocolroutepaths.userinfo); } if (options.endpoints.enableendsessionendpoint) { entries.add(oidcconstants.discovery.endsessionendpoint, baseurl + constants.protocolroutepaths.endsession); } if (options.endpoints.enablechecksessionendpoint) { entries.add(oidcconstants.discovery.checksessioniframe, baseurl + constants.protocolroutepaths.checksession); } if (options.endpoints.enabletokenrevocationendpoint) { entries.add(oidcconstants.discovery.revocationendpoint, baseurl + constants.protocolroutepaths.revocation); } if (options.endpoints.enableintrospectionendpoint) { entries.add(oidcconstants.discovery.introspectionendpoint, baseurl + constants.protocolroutepaths.introspection); } if (options.endpoints.enabledeviceauthorizationendpoint) { entries.add(oidcconstants.discovery.deviceauthorizationendpoint, baseurl + constants.protocolroutepaths.deviceauthorization); } if (options.mutualtls.enabled) { var mtlsendpoints = new dictionary<string, string>(); if (options.endpoints.enabletokenendpoint) { mtlsendpoints.add(oidcconstants.discovery.tokenendpoint, baseurl + constants.protocolroutepaths.mtlstoken); } if (options.endpoints.enabletokenrevocationendpoint) { mtlsendpoints.add(oidcconstants.discovery.revocationendpoint, baseurl + constants.protocolroutepaths.mtlsrevocation); } if (options.endpoints.enableintrospectionendpoint) { mtlsendpoints.add(oidcconstants.discovery.introspectionendpoint, baseurl + constants.protocolroutepaths.mtlsintrospection); } if (options.endpoints.enabledeviceauthorizationendpoint) { mtlsendpoints.add(oidcconstants.discovery.deviceauthorizationendpoint, baseurl + constants.protocolroutepaths.mtlsdeviceauthorization); } if (mtlsendpoints.any()) { entries.add(oidcconstants.discovery.mtlsendpointaliases, mtlsendpoints); } } } // logout if (options.endpoints.enableendsessionendpoint) { entries.add(oidcconstants.discovery.frontchannellogoutsupported, true); entries.add(oidcconstants.discovery.frontchannellogoutsessionsupported, true); entries.add(oidcconstants.discovery.backchannellogoutsupported, true); entries.add(oidcconstants.discovery.backchannellogoutsessionsupported, true); } // scopes and claims if (options.discovery.showidentityscopes || options.discovery.showapiscopes || options.discovery.showclaims) { var resources = await resourcestore.getallenabledresourcesasync(); var scopes = new list<string>(); // scopes if (options.discovery.showidentityscopes) { scopes.addrange(resources.identityresources.where(x => x.showindiscoverydocument).select(x => x.name)); } if (options.discovery.showapiscopes) { var apiscopes = from api in resources.apiresources from scope in api.scopes where scope.showindiscoverydocument select scope.name; scopes.addrange(apiscopes); scopes.add(identityserverconstants.standardscopes.offlineaccess); } if (scopes.any()) { entries.add(oidcconstants.discovery.scopessupported, scopes.toarray()); } // claims if (options.discovery.showclaims) { var claims = new list<string>(); // add non-hidden identity scopes related claims claims.addrange(resources.identityresources.where(x => x.showindiscoverydocument).selectmany(x => x.userclaims)); // add non-hidden api scopes related claims foreach (var resource in resources.apiresources) { claims.addrange(resource.userclaims); foreach (var scope in resource.scopes) { if (scope.showindiscoverydocument) { claims.addrange(scope.userclaims); } } } entries.add(oidcconstants.discovery.claimssupported, claims.distinct().toarray()); } } // grant types if (options.discovery.showgranttypes) { var standardgranttypes = new list<string> { oidcconstants.granttypes.authorizationcode, oidcconstants.granttypes.clientcredentials, oidcconstants.granttypes.refreshtoken, oidcconstants.granttypes.implicit }; if (!(resourceownervalidator is notsupportedresourceownerpasswordvalidator)) { standardgranttypes.add(oidcconstants.granttypes.password); } if (options.endpoints.enabledeviceauthorizationendpoint) { standardgranttypes.add(oidcconstants.granttypes.devicecode); } var showgranttypes = new list<string>(standardgranttypes); if (options.discovery.showextensiongranttypes) { showgranttypes.addrange(extensiongrants.getavailablegranttypes()); } entries.add(oidcconstants.discovery.granttypessupported, showgranttypes.toarray()); } // response types if (options.discovery.showresponsetypes) { entries.add(oidcconstants.discovery.responsetypessupported, constants.supportedresponsetypes.toarray()); } // response modes if (options.discovery.showresponsemodes) { entries.add(oidcconstants.discovery.responsemodessupported, constants.supportedresponsemodes.toarray()); } // misc if (options.discovery.showtokenendpointauthenticationmethods) { var types = secretparsers.getavailableauthenticationmethods().tolist(); if (options.mutualtls.enabled) { types.add(oidcconstants.endpointauthenticationmethods.tlsclientauth); types.add(oidcconstants.endpointauthenticationmethods.selfsignedtlsclientauth); } entries.add(oidcconstants.discovery.tokenendpointauthenticationmethodssupported, types); } var signingcredentials = await keys.getsigningcredentialsasync(); if (signingcredentials != null) { var algorithm = signingcredentials.algorithm; entries.add(oidcconstants.discovery.idtokensigningalgorithmssupported, new[] { algorithm }); } entries.add(oidcconstants.discovery.subjecttypessupported, new[] { "public" }); entries.add(oidcconstants.discovery.codechallengemethodssupported, new[] { oidcconstants.codechallengemethods.plain, oidcconstants.codechallengemethods.sha256 }); if (options.endpoints.enableauthorizeendpoint) { entries.add(oidcconstants.discovery.requestparametersupported, true); if (options.endpoints.enablejwtrequesturi) { entries.add(oidcconstants.discovery.requesturiparametersupported, true); } } if (options.mutualtls.enabled) { entries.add(oidcconstants.discovery.tlsclientcertificateboundaccesstokens, true); } // custom entries if (!options.discovery.customentries.isnullorempty()) { foreach (var customentry in options.discovery.customentries) { if (entries.containskey(customentry.key)) { logger.logerror("discovery custom entry {key} cannot be added, because it already exists.", customentry.key); } else { if (customentry.value is string customvaluestring) { if (customvaluestring.startswith("~/") && options.discovery.expandrelativepathsincustomentries) { entries.add(customentry.key, baseurl + customvaluestring.substring(2)); continue; } } entries.add(customentry.key, customentry.value); } } } return entries; }
然后是jwks描述信息的代码。关于加密的信息也是根据配置的securitkey去动态返回的。
public virtual async task<ienumerable<models.jsonwebkey>> createjwkdocumentasync() { var webkeys = new list<models.jsonwebkey>(); foreach (var key in await keys.getvalidationkeysasync()) { if (key.key is x509securitykey x509key) { var cert64 = convert.tobase64string(x509key.certificate.rawdata); var thumbprint = base64url.encode(x509key.certificate.getcerthash()); if (x509key.publickey is rsa rsa) { var parameters = rsa.exportparameters(false); var exponent = base64url.encode(parameters.exponent); var modulus = base64url.encode(parameters.modulus); var rsajsonwebkey = new models.jsonwebkey { kty = "rsa", use = "sig", kid = x509key.keyid, x5t = thumbprint, e = exponent, n = modulus, x5c = new[] { cert64 }, alg = key.signingalgorithm }; webkeys.add(rsajsonwebkey); } else if (x509key.publickey is ecdsa ecdsa) { var parameters = ecdsa.exportparameters(false); var x = base64url.encode(parameters.q.x); var y = base64url.encode(parameters.q.y); var ecdsajsonwebkey = new models.jsonwebkey { kty = "ec", use = "sig", kid = x509key.keyid, x5t = thumbprint, x = x, y = y, crv = cryptohelper.getcrvvaluefromcurve(parameters.curve), x5c = new[] { cert64 }, alg = key.signingalgorithm }; webkeys.add(ecdsajsonwebkey); } else { throw new invalidoperationexception($"key type: {x509key.publickey.gettype().name} not supported."); } } else if (key.key is rsasecuritykey rsakey) { var parameters = rsakey.rsa?.exportparameters(false) ?? rsakey.parameters; var exponent = base64url.encode(parameters.exponent); var modulus = base64url.encode(parameters.modulus); var webkey = new models.jsonwebkey { kty = "rsa", use = "sig", kid = rsakey.keyid, e = exponent, n = modulus, alg = key.signingalgorithm }; webkeys.add(webkey); } else if (key.key is ecdsasecuritykey ecdsakey) { var parameters = ecdsakey.ecdsa.exportparameters(false); var x = base64url.encode(parameters.q.x); var y = base64url.encode(parameters.q.y); var ecdsajsonwebkey = new models.jsonwebkey { kty = "ec", use = "sig", kid = ecdsakey.keyid, x = x, y = y, crv = cryptohelper.getcrvvaluefromcurve(parameters.curve), alg = key.signingalgorithm }; webkeys.add(ecdsajsonwebkey); } else if (key.key is jsonwebkey jsonwebkey) { var webkey = new models.jsonwebkey { kty = jsonwebkey.kty, use = jsonwebkey.use ?? "sig", kid = jsonwebkey.kid, x5t = jsonwebkey.x5t, e = jsonwebkey.e, n = jsonwebkey.n, x5c = jsonwebkey.x5c?.count == 0 ? null : jsonwebkey.x5c.toarray(), alg = jsonwebkey.alg, x = jsonwebkey.x, y = jsonwebkey.y }; webkeys.add(webkey); } } return webkeys; }
结语
这一节还是比较好理解的。总而言之就是oidc协议规定了,需要提供get接口,返回所有接口的地址,以及相关配置信息。idsv4的实现方式就是接口地址根据协议规定的去拼接,其他配置项信息根据开发的配置去动态获取,然后以协议约定的json格式返回。
推荐阅读
-
Mybaits 源码解析 (五)----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
-
Mybaits 源码解析 (三)----- Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
-
IdentityServer4源码解析_4_令牌发放接口
-
identityserver4源码解析_3_认证接口
-
identityserver4源码解析_2_元数据接口
-
Mybaits 源码解析 (五)----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
-
Mybaits 源码解析 (三)----- Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
-
identityserver4源码解析_2_元数据接口
-
identityserver4源码解析_3_认证接口
-
IdentityServer4源码解析_4_令牌发放接口