目录

  • 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格式返回。