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

identityserver4源码解析_3_认证接口

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

目录

协议

五种认证方式

  • authorization code 授权码模式:认证服务返回授权码,后端用clientid和密钥向认证服务证明身份,使用授权码换取id token 和/或 access token。本模式的好处是由后端请求token,不会将敏感信息暴露在浏览器。本模式允许使用refreshtoken去维持长时间的登录状态。使用此模式的客户端必须有后端参与,能够保障客户端密钥的安全性。此模式从authorization接口获取授权码,从token接口获取令牌。

  • implict 简化模式:校验跳转uri验证客户端身份之后,直接发放token。通常用于纯客户端应用,如单页应用javascript客户端。因为没有后端参与,密钥存放在前端是不安全的。由于安全校验较宽松,本模式不允许使用refreshtoken来长时间维持登录状态。本模式的所有token从authorization接口获取。

  • hybrid 混合流程:混合流程顾名思义组合使用了授权码模式+简化模式。前端请求授权服务器返回授权码+id_token,这样前端立刻可以使用用户的基本信息;后续请求后端使用授权码+客户端密钥获取access_token。本模式能够使用refreshtoken来长时间维持登录状态。使用本模式必须有后端参与保证客户端密钥的安全性。混合模式极少使用,除非你的确需要使用它的某些特性(如一次请求获取授权码和用户资料),一般最常见的还是授权码模式。

  • resource owner password credential 用户名密码模式:一般用于无用户交互场景,或者第三方对接(如对接微信登录,实际登录界面就变成了微信的界面,如果不希望让客户扫了微信之后再跑你们系统登录一遍,就可以在后端用此模式静默登录接上自家的sso即可)

  • client credential 客户端密钥模式:仅需要约定密钥,仅用于完全信任的内部系统

认证方式特点对比

特点 授权码模式 简化模式 混合模式
所有token从authorization接口返回 no yes yes
所有token从token接口返回 yes no no
所有tokens不暴露在浏览器 yes no no
能够验证客户端密钥 yes no yes
能够使用刷新令牌 yes no yes
仅需一次请求 no yes no
大部分请求由后端进行 yes no 可变

支持返回类型对比

返回类型 认证模式 说明
code authorization code flow 仅返回授权码
id_token implicit flow 返回身份令牌
id_token token implicit flow 返回身份令牌、通行令牌
code id_token hybrid flow 返回授权码、身份令牌
code token hybrid flow 返回授权码、通行令牌
code id_token token hybrid flow 返回授权码、身份令牌、通行令牌

授权码模式解析

相对来说,授权码模式还是用的最多的,我们详细解读一下本模式的协议内容。

授权时序图

sequencediagram 用户->>客户端: 请求受保护资源 客户端->>认证服务: 准备入参,发起认证请求 认证服务->>认证服务: 认证用户 认证服务->>用户: 是否同意授权 认证服务->>客户端: 发放授权码(前端进行) 客户端->>认证服务: 使用授权码请求token(后端进行) 认证服务->>认证服务: 校验客户端密钥,校验授权码 认证服务->>客户端: 发放身份令牌、通行令牌(后端进行) 客户端->>客户端: 校验身份令牌,获取用户标识

认证请求

认证接口必须同时支持get和post两种请求方式。如果使用get方法,客户端必须使用uri query传递参数,如果使用post方法,客户端必须使用form传递参数。

参数定义

  • scope:授权范围,必填。必须包含openid。
  • response_type:返回类型,必填。定义了认证服务返回哪些参数。对于授权码模式,本参数只能是code。
  • client_id:客户端id,必填。
  • redirect_uri:跳转地址,必填。授权码生成之后,认证服务会带着授权码和其他参数回跳到此地址。此地址要求使用https。如果使用http,则客户端类型必须是confidential。
  • state:状态字段,推荐填写。一般用于客户端与认证服务比对此字段,来防跨站伪造攻击,同时state也可以存放状态信息,如发起认证时的页面地址,用于认证完成后回到原始页面。
  • 其他:略。上面五个是和oauth2.0一样的参数,oidc还定义了一些扩展参数,用的很少,不是很懂,感兴趣的自己去看协议。

请求报文示例

http/1.1 302 found
  location: https://server.example.com/authorize?
    response_type=code
    &scope=openid%20profile%20email
    &client_id=s6bhdrkqt3
    &state=af0ifjsldkj
    &redirect_uri=https%3a%2f%2fclient.example.org%2fcb

认证请求校验

  • 必填校验
  • response_type必须为code
  • scope必填,必须包含openid

认证终端用户

  • 下面两种情况认证服务必须认证用户
    • 用户尚未认证
    • 认证请求包含参数prompt=login,即使用户已经认证过也需要重新认证
    • 认证请求包含参数prompt=none,然后用户尚未被认证,则需要返回错误信息

认证服务必须想办法防止过程中的跨站伪造攻击和点击劫持攻击。

获取终端用户授权/同意

终端用户通过认证之后,认证服务必须与终端用户交互,询问用户是否同意对客户端的授权。

认证响应

成功响应

使用 application/x-www-form-urlencoded格式返回结果
例如:

 http/1.1 302 found
  location: https://client.example.org/cb?
    code=splxlobezqqybys6wxsbia
    &state=af0ifjsldkj

失败响应

错误代码包括这些
oauth2.0定义的响应代码

  • invalid_request:非法请求,未提供必填参数,参数非法等情况
  • unauthorized_client:客户端未授权
  • access_denied:用户无权限
  • unsupported_response_type
  • invalid_scope:非法的scope参数
  • server_error
  • temporarily_unavailable
    另外oidc还扩展了一些响应代码,不常见,略

例如:

  http/1.1 302 found
  location: https://client.example.org/cb?
    error=invalid_request
    &error_description=
      unsupported%20response_type%20value
    &state=af0ifjsldkj

客户端校验授权码

协议规定客户端必须校验授权码的正确性

源码解析

从authorizeendpoint的processasync方法作为入口开始认证接口的源码解析。

  • 判断请求方式是get还是post,获取入参,如果是其他请求方式415状态码
  • 从session中获取user
  • 入参和user作为入参,调用父类processauthorizerequestasync方法
    public override async task<iendpointresult> processasync(httpcontext context)
        {
            logger.logdebug("start authorize request");

            namevaluecollection values;

            if (httpmethods.isget(context.request.method))
            {
                values = context.request.query.asnamevaluecollection();
            }
            else if (httpmethods.ispost(context.request.method))
            {
                if (!context.request.hasformcontenttype)
                {
                    return new statuscoderesult(httpstatuscode.unsupportedmediatype);
                }

                values = context.request.form.asnamevaluecollection();
            }
            else
            {
                return new statuscoderesult(httpstatuscode.methodnotallowed);
            }

            var user = await usersession.getuserasync();
            var result = await processauthorizerequestasync(values, user, null);

            logger.logtrace("end authorize request. result type: {0}", result?.gettype().tostring() ?? "-none-");

            return result;
        }

认证站点如果cookie中存在当前会话信息,则直接返回用户信息,否则调用cookie架构的认证方法,会跳转到登录页面。

public virtual async task<claimsprincipal> getuserasync()
{
    await authenticateasync();

    return principal;
}

protected virtual async task authenticateasync()
    {
        if (principal == null || properties == null)
        {
            var scheme = await getcookieschemeasync();

            var handler = await handlers.gethandlerasync(httpcontext, scheme);
            if (handler == null)
            {
                throw new invalidoperationexception($"no authentication handler is configured to authenticate for the scheme: {scheme}");
            }

            var result = await handler.authenticateasync();
            if (result != null && result.succeeded)
            {
                principal = result.principal;
                properties = result.properties;
            }
        }
    }

认证请求处理流程大致分为三步

  • authorizerequestvalidator校验所有参数
  • 认证接口consent入参为null,不需要处理用户交互判断
  • 生成返回报文
 internal async task<iendpointresult> processauthorizerequestasync(namevaluecollection parameters, claimsprincipal user, consentresponse consent)
{
    if (user != null)
    {
        logger.logdebug("user in authorize request: {subjectid}", user.getsubjectid());
    }
    else
    {
        logger.logdebug("no user present in authorize request");
    }

    // validate request
    var result = await _validator.validateasync(parameters, user);
    if (result.iserror)
    {
        return await createerrorresultasync(
            "request validation failed",
            result.validatedrequest,
            result.error,
            result.errordescription);
    }

    var request = result.validatedrequest;
    logrequest(request);

    // determine user interaction
    var interactionresult = await _interactiongenerator.processinteractionasync(request, consent);
    if (interactionresult.iserror)
    {
        return await createerrorresultasync("interaction generator error", request, interactionresult.error, interactionresult.errordescription, false);
    }
    if (interactionresult.islogin)
    {
        return new loginpageresult(request);
    }
    if (interactionresult.isconsent)
    {
        return new consentpageresult(request);
    }
    if (interactionresult.isredirect)
    {
        return new customredirectresult(request, interactionresult.redirecturl);
    }

    var response = await _authorizeresponsegenerator.createresponseasync(request);

    await raiseresponseeventasync(response);

    logresponse(response);

    return new authorizeresult(response);
}

生成返回信息

此处只有authorizationcode、implicit、hybrid三种授权类型的判断,用户名密码、客户端密钥模式不能使用authorize接口。

  public virtual async task<authorizeresponse> createresponseasync(validatedauthorizerequest request)
{
    if (request.granttype == granttype.authorizationcode)
    {
        return await createcodeflowresponseasync(request);
    }
    if (request.granttype == granttype.implicit)
    {
        return await createimplicitflowresponseasync(request);
    }
    if (request.granttype == granttype.hybrid)
    {
        return await createhybridflowresponseasync(request);
    }

    logger.logerror("unsupported grant type: " + request.granttype);
    throw new invalidoperationexception("invalid grant type: " + request.granttype);
}
  • 如果state字段不为空,使用加密算法得到state的hash值
  • 构建authorizationcode对象,存放在store中,store是idsv4用于持久化的对象,默认实现存储在内存中,可以对可插拔服务进行注入替换,实现数据保存在在mysql、redis等流行存储中
  • 将授权码对象的id返回
 protected virtual async task<authorizeresponse> createcodeflowresponseasync(validatedauthorizerequest request)
{
    logger.logdebug("creating authorization code flow response.");

    var code = await createcodeasync(request);
    var id = await authorizationcodestore.storeauthorizationcodeasync(code);

    var response = new authorizeresponse
    {
        request = request,
        code = id,
        sessionstate = request.generatesessionstatevalue()
    };

    return response;
}

protected virtual async task<authorizationcode> createcodeasync(validatedauthorizerequest request)
    {
        string statehash = null;
        if (request.state.ispresent())
        {
            var credential = await keymaterialservice.getsigningcredentialsasync();
            if (credential == null)
            {
                throw new invalidoperationexception("no signing credential is configured.");
            }

            var algorithm = credential.algorithm;
            statehash = cryptohelper.createhashclaimvalue(request.state, algorithm);
        }

        var code = new authorizationcode
        {
            creationtime = clock.utcnow.utcdatetime,
            clientid = request.client.clientid,
            lifetime = request.client.authorizationcodelifetime,
            subject = request.subject,
            sessionid = request.sessionid,
            codechallenge = request.codechallenge.sha256(),
            codechallengemethod = request.codechallengemethod,

            isopenid = request.isopenidrequest,
            requestedscopes = request.validatedscopes.grantedresources.toscopenames(),
            redirecturi = request.redirecturi,
            nonce = request.nonce,
            statehash = statehash,

            wasconsentshown = request.wasconsentshown
        };

        return code;
    }

返回结果

  • 如果responsemode等于query或者fragment,将授权码code及其他信息拼装到uri,返回302重定向请求
    例子:
302 https://mysite.com?code=xxxxx&state=xxx
  • 如果是formpost方式,会生成一段脚本返回到客户端。窗口加载会触发form表单提交,将code、state等信息包裹在隐藏字段里提交到配置的rediret_uri。
<html>
<head>
    <meta http-equiv='x-ua-compatible' content='ie=edge' />
    <base target='_self'/>
</head>
<body>
    <form method='post' action='https://mysite.com'>
        <input type='hidden' name='code' value='xxx' />
        <input type='hidden' name='state' value='xxx' />
        <noscript>
            <button>click to continue</button>
        </noscript>
    </form>
    <script>window.addeventlistener('load', function(){document.forms[0].submit();});</script>
</body>
</html>
private async task renderauthorizeresponseasync(httpcontext context)
{
    if (response.request.responsemode == oidcconstants.responsemodes.query ||
        response.request.responsemode == oidcconstants.responsemodes.fragment)
    {
        context.response.setnocache();
        context.response.redirect(buildredirecturi());
    }
    else if (response.request.responsemode == oidcconstants.responsemodes.formpost)
    {
        context.response.setnocache();
        addsecurityheaders(context);
        await context.response.writehtmlasync(getformposthtml());
    }
    else
    {
        //_logger.logerror("unsupported response mode.");
        throw new invalidoperationexception("unsupported response mode");
    }
}

客户端在回调地址接收code,即可向token接口换取token。

其他

简单看一下简化流程和混合流程是怎么创建返回报文的。

简化流程生成返回报文

  • 如果返回类型包含token,生成通行令牌
  • 如果返回类型包含id_token,生成身份令牌

可以看到,简化流程的所有token都是由authorization接口返回的,一次请求返回所有token。

protected virtual async task<authorizeresponse> createimplicitflowresponseasync(validatedauthorizerequest request, string authorizationcode = null)
    {
        logger.logdebug("creating implicit flow response.");

        string accesstokenvalue = null;
        int accesstokenlifetime = 0;

        var responsetypes = request.responsetype.fromspaceseparatedstring();

        if (responsetypes.contains(oidcconstants.responsetypes.token))
        {
            var tokenrequest = new tokencreationrequest
            {
                subject = request.subject,
                resources = request.validatedscopes.grantedresources,

                validatedrequest = request
            };

            var accesstoken = await tokenservice.createaccesstokenasync(tokenrequest);
            accesstokenlifetime = accesstoken.lifetime;

            accesstokenvalue = await tokenservice.createsecuritytokenasync(accesstoken);
        }

        string jwt = null;
        if (responsetypes.contains(oidcconstants.responsetypes.idtoken))
        {
            string statehash = null;
            if (request.state.ispresent())
            {
                var credential = await keymaterialservice.getsigningcredentialsasync();
                if (credential == null)
                {
                    throw new invalidoperationexception("no signing credential is configured.");
                }

                var algorithm = credential.algorithm;
                statehash = cryptohelper.createhashclaimvalue(request.state, algorithm);
            }

            var tokenrequest = new tokencreationrequest
            {
                validatedrequest = request,
                subject = request.subject,
                resources = request.validatedscopes.grantedresources,
                nonce = request.raw.get(oidcconstants.authorizerequest.nonce),
                includeallidentityclaims = !request.accesstokenrequested,
                accesstokentohash = accesstokenvalue,
                authorizationcodetohash = authorizationcode,
                statehash = statehash
            };

            var idtoken = await tokenservice.createidentitytokenasync(tokenrequest);
            jwt = await tokenservice.createsecuritytokenasync(idtoken);
        }

        var response = new authorizeresponse
        {
            request = request,
            accesstoken = accesstokenvalue,
            accesstokenlifetime = accesstokenlifetime,
            identitytoken = jwt,
            sessionstate = request.generatesessionstatevalue()
        };

        return response;
    }

混合流程生成返回报文

这段代码充分体现了它为啥叫混合流程,把生成授权码的方法调一遍,再把简化流程的方法调一遍,code和token可以一起返回。

protected virtual async task<authorizeresponse> createhybridflowresponseasync(validatedauthorizerequest request)
    {
        logger.logdebug("creating hybrid flow response.");

        var code = await createcodeasync(request);
        var id = await authorizationcodestore.storeauthorizationcodeasync(code);

        var response = await createimplicitflowresponseasync(request, id);
        response.code = id;

        return response;
    }