欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

identityserver4源码解析_2_元数据接口

程序员文章站 2022-04-14 16:17:32
目录 "identityserver4源码解析_1_项目结构" "identityserver4源码解析_2_元数据接口" "identityserver4源码解析_3_认证接口" "identityserver4源码解析_4_令牌发放接口" "identityserver4源码解析_5_查询用户信 ......

目录

协议

这一系列我们都采用这样的方式,先大概看下协议,也就是需求描述,然后看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服务调用试一下,返回结果如图
identityserver4源码解析_2_元数据接口

详细信息如下。

{
    "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协议定的(对,都是产品的锅)。

identityserver4源码解析_2_元数据接口

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