当前位置: 移动技术网 > IT编程>开发语言>.net > identityserver4源码解析_2_元数据接口

identityserver4源码解析_2_元数据接口

2020年03月29日  | 移动技术网IT编程  | 我要评论

目录

协议

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

详细信息如下。

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

image

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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网