title: “aspnetcore3.1_secutiry源码解析_5_authentication_oauth”
date: 2020-03-24t23:27:45+08:00
draft: false

系列文章目录

  • 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(https://holdengong.com/aspnetcore3.1_secutiry源码解析_5_authentication_oauth)
  • aspnetcore3.1_secutiry源码解析_6_authentication_openidconnect
  • aspnetcore3.1_secutiry源码解析_7_authentication_其他
  • aspnetcore3.1_secutiry源码解析_8_authorization_核心项目
  • aspnetcore3.1_secutiry源码解析_9_authorization_policy

oauth简介

现在随便一个网站,不用注册,只用微信扫一扫,然后就可以自动登录,然后第三方网站右上角还出现了你的微信头像和昵称,怎么做到的?

sequencediagram 用户->>x站点: 请求微信登录 x站点->>微信: 请求 oauth token 微信->>用户: x站点请求基本资料权限,是否同意? 用户->>微信: 同意 微信->>x站点: token x站点->>微信: 请求user基本资料(token) 微信->微信: 校验token 微信->>x站点: user基本资料

大概就这么个意思,oauth可以让第三方获取有限的授权去获取资源。

入门的看博客

英文好有基础的直接看协议

依赖注入

配置类:oauthoptions
处理器类: oauthhandler

public static class oauthextensions
{
    public static authenticationbuilder addoauth(this authenticationbuilder builder, string authenticationscheme, action<oauthoptions> configureoptions)
        => builder.addoauth<oauthoptions, oauthhandler<oauthoptions>>(authenticationscheme, configureoptions);

    public static authenticationbuilder addoauth(this authenticationbuilder builder, string authenticationscheme, string displayname, action<oauthoptions> configureoptions)
        => builder.addoauth<oauthoptions, oauthhandler<oauthoptions>>(authenticationscheme, displayname, configureoptions);

    public static authenticationbuilder addoauth<toptions, thandler>(this authenticationbuilder builder, string authenticationscheme, action<toptions> configureoptions)
        where toptions : oauthoptions, new()
        where thandler : oauthhandler<toptions>
        => builder.addoauth<toptions, thandler>(authenticationscheme, oauthdefaults.displayname, configureoptions);

    public static authenticationbuilder addoauth<toptions, thandler>(this authenticationbuilder builder, string authenticationscheme, string displayname, action<toptions> configureoptions)
        where toptions : oauthoptions, new()
        where thandler : oauthhandler<toptions>
    {
        builder.services.tryaddenumerable(servicedescriptor.singleton<ipostconfigureoptions<toptions>, oauthpostconfigureoptions<toptions, thandler>>());
        return builder.addremotescheme<toptions, thandler>(authenticationscheme, displayname, configureoptions);
    }
}

oauthoptions – 配置类

classdiagram class oauthoptions{ clientid clientsecret authorizationendpoint tokenendpoint userinformationendpoint scope events claimactions statedataformat } class remoteauthenticationoptions{ backchanneltimeout backchannelhttphandler backchannel dataprotectionprovider callbackpath accessdeniedpath returnurlparameter signinscheme remoteauthenticationtimeout savetokens } class authenticationschemeoptions{ } oauthoptions–>remoteauthenticationoptions remoteauthenticationoptions–>authenticationschemeoptions

下面是校验逻辑,这些配置是必需的。

public override void validate()
{
    base.validate();

    if (string.isnullorempty(clientid))
    {
        throw new argumentexception(string.format(cultureinfo.currentculture, resources.exception_optionmustbeprovided, nameof(clientid)), nameof(clientid));
    }

    if (string.isnullorempty(clientsecret))
    {
        throw new argumentexception(string.format(cultureinfo.currentculture, resources.exception_optionmustbeprovided, nameof(clientsecret)), nameof(clientsecret));
    }

    if (string.isnullorempty(authorizationendpoint))
    {
        throw new argumentexception(string.format(cultureinfo.currentculture, resources.exception_optionmustbeprovided, nameof(authorizationendpoint)), nameof(authorizationendpoint));
    }

    if (string.isnullorempty(tokenendpoint))
    {
        throw new argumentexception(string.format(cultureinfo.currentculture, resources.exception_optionmustbeprovided, nameof(tokenendpoint)), nameof(tokenendpoint));
    }

    if (!callbackpath.hasvalue)
    {
        throw new argumentexception(string.format(cultureinfo.currentculture, resources.exception_optionmustbeprovided, nameof(callbackpath)), nameof(callbackpath));
    }
}

oauthpostconfigureoptions – 配置处理

  1. dataprotectionprovider没有配置的话则使用默认实现
  2. backchannel没有配置的话则处理构造默认配置
  3. statedataformat没有配置的话则使用propertiesdataformat
public void postconfigure(string name, toptions options)
{
    options.dataprotectionprovider = options.dataprotectionprovider ?? _dp;
    if (options.backchannel == null)
    {
        options.backchannel = new httpclient(options.backchannelhttphandler ?? new httpclienthandler());
        options.backchannel.defaultrequestheaders.useragent.parseadd("microsoft asp.net core oauth handler");
        options.backchannel.timeout = options.backchanneltimeout;
        options.backchannel.maxresponsecontentbuffersize = 1024 * 1024 * 10; // 10 mb
    }

    if (options.statedataformat == null)
    {
        var dataprotector = options.dataprotectionprovider.createprotector(
            typeof(thandler).fullname, name, "v1");
        options.statedataformat = new propertiesdataformat(dataprotector);
    }
}

这个statedataformat就是处理state字段的加密解密的,state在认证过程中用于防止跨站伪造攻击和存放一些状态信息,我们看一下协议的定义

 state
         recommended.  an opaque value used by the client to maintain
         state between the request and callback.  the authorization
         server includes this value when redirecting the user-agent back
         to the client.  the parameter should be used for preventing
         cross-site request forgery as described in section 10.12.

比如,认证之后的回跳地址就是存放在这里。所以如果希望从state字段中解密得到信息的话,就需要使用到propertiesdataformat。propertiesdataformat没有任何代码,继承自securedataformat。 为什么这里介绍这么多呢,因为实际项目中用到过这个。

public class securedataformat<tdata> : isecuredataformat<tdata>
{
    private readonly idataserializer<tdata> _serializer;
    private readonly idataprotector _protector;

    public securedataformat(idataserializer<tdata> serializer, idataprotector protector)
    {
        _serializer = serializer;
        _protector = protector;
    }

    public string protect(tdata data)
    {
        return protect(data, purpose: null);
    }

    public string protect(tdata data, string purpose)
    {
        var userdata = _serializer.serialize(data);

        var protector = _protector;
        if (!string.isnullorempty(purpose))
        {
            protector = protector.createprotector(purpose);
        }

        var protecteddata = protector.protect(userdata);
        return base64urltextencoder.encode(protecteddata);
    }

    public tdata unprotect(string protectedtext)
    {
        return unprotect(protectedtext, purpose: null);
    }

    public tdata unprotect(string protectedtext, string purpose)
    {
        try
        {
            if (protectedtext == null)
            {
                return default(tdata);
            }

            var protecteddata = base64urltextencoder.decode(protectedtext);
            if (protecteddata == null)
            {
                return default(tdata);
            }

            var protector = _protector;
            if (!string.isnullorempty(purpose))
            {
                protector = protector.createprotector(purpose);
            }

            var userdata = protector.unprotect(protecteddata);
            if (userdata == null)
            {
                return default(tdata);
            }

            return _serializer.deserialize(userdata);
        }
        catch
        {
            // todo trace exception, but do not leak other information
            return default(tdata);
        }
    }
}

addremoteschema和addshema的差别就是做了下面的处理,确认始终有不是远程schema的signinschema

private class ensuresigninscheme<toptions> : ipostconfigureoptions<toptions> where toptions : remoteauthenticationoptions
{
    private readonly authenticationoptions _authoptions;

    public ensuresigninscheme(ioptions<authenticationoptions> authoptions)
    {
        _authoptions = authoptions.value;
    }

    public void postconfigure(string name, toptions options)
    {
        options.signinscheme = options.signinscheme ?? _authoptions.defaultsigninscheme ?? _authoptions.defaultscheme;
    }
}

oauthhandler

  • 解密state
  • 校验correlationid,防跨站伪造攻击
  • 如果error不为空说明失败返回错误
  • 拿到授权码code,换取token
  • 如果savetokens设置为true,将access_token,refresh_token,token_type存放到properties中
  • 创建凭据,返回成功
  protected override async task<handlerequestresult> handleremoteauthenticateasync()
        {
            var query = request.query;

            var state = query["state"];
            var properties = options.statedataformat.unprotect(state);

            if (properties == null)
            {
                return handlerequestresult.fail("the oauth state was missing or invalid.");
            }

            // oauth2 10.12 csrf
            if (!validatecorrelationid(properties))
            {
                return handlerequestresult.fail("correlation failed.", properties);
            }

            var error = query["error"];
            if (!stringvalues.isnullorempty(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 (stringvalues.equals(error, "access_denied"))
                {
                    return await handleaccessdeniederrorasync(properties);
                }

                var failuremessage = new stringbuilder();
                failuremessage.append(error);
                var errordescription = query["error_description"];
                if (!stringvalues.isnullorempty(errordescription))
                {
                    failuremessage.append(";description=").append(errordescription);
                }
                var erroruri = query["error_uri"];
                if (!stringvalues.isnullorempty(erroruri))
                {
                    failuremessage.append(";uri=").append(erroruri);
                }

                return handlerequestresult.fail(failuremessage.tostring(), properties);
            }

            var code = query["code"];

            if (stringvalues.isnullorempty(code))
            {
                return handlerequestresult.fail("code was not found.", properties);
            }

            var tokens = await exchangecodeasync(code, buildredirecturi(options.callbackpath));

            if (tokens.error != null)
            {
                return handlerequestresult.fail(tokens.error, properties);
            }

            if (string.isnullorempty(tokens.accesstoken))
            {
                return handlerequestresult.fail("failed to retrieve access token.", properties);
            }

            var identity = new claimsidentity(claimsissuer);

            if (options.savetokens)
            {
                var authtokens = new list<authenticationtoken>();

                authtokens.add(new authenticationtoken { name = "access_token", value = tokens.accesstoken });
                if (!string.isnullorempty(tokens.refreshtoken))
                {
                    authtokens.add(new authenticationtoken { name = "refresh_token", value = tokens.refreshtoken });
                }

                if (!string.isnullorempty(tokens.tokentype))
                {
                    authtokens.add(new authenticationtoken { name = "token_type", value = tokens.tokentype });
                }

                if (!string.isnullorempty(tokens.expiresin))
                {
                    int value;
                    if (int.tryparse(tokens.expiresin, numberstyles.integer, cultureinfo.invariantculture, out value))
                    {
                        // https://www.w3.org/tr/xmlschema-2/#datetime
                        // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
                        var expiresat = clock.utcnow + timespan.fromseconds(value);
                        authtokens.add(new authenticationtoken
                        {
                            name = "expires_at",
                            value = expiresat.tostring("o", cultureinfo.invariantculture)
                        });
                    }
                }

                properties.storetokens(authtokens);
            }

            var ticket = await createticketasync(identity, properties, tokens);
            if (ticket != null)
            {
                return handlerequestresult.success(ticket);
            }
            else
            {
                return handlerequestresult.fail("failed to retrieve user information from remote server.", properties);
            }
        }