IdentityServer4源码解析_4_令牌发放接口

2022-10-09,,,,

目录

  • identityserver4源码解析_1_项目结构
  • identityserver4源码解析_2_元数据接口
  • identityserver4源码解析_3_认证接口
  • identityserver4源码解析_4_令牌发放接口
  • identityserver4源码解析_5_查询用户信息接口
  • identityserver4源码解析_6_结束会话接口
  • identityserver4源码解析_7_查询令牌信息接口
  • identityserver4源码解析_8_撤销令牌接口

协议

token接口

oidc服务需要提供token接口,提供accesstoken,idtoken,以及refreshtoken(可选)。在授权码模式下,token接口必须使用https。

请求

必须使用post方法,使用x-www-form-urlencoded序列化参数,clientid:clientsecret使用basic加密放在authorization头中

post /token http/1.1
host: server.example.com
content-type: application/x-www-form-urlencoded
authorization: basic czzcagrsa3f0mzpnwdfmqmf0m2jw

grant_type=authorization_code&code=splxlobezqqybys6wxsbia
&redirect_uri=https%3a%2f%2fclient.example.org%2fcb

请求校验

认证服务必须校验下列内容:

  • 验证client是否颁发了秘钥
  • 验证为该客户端颁发了授权码
  • 验证授权码有效性
  • 如果可能的话,验证授权码是否被使用过
  • 验证redirect_uri 与发起认证请求时的值一致

成功响应

在收到token请求,并校验通过之后,认证服务返回成功报文,报文包含了身份令牌和通行令牌。数据格式使用application/json。token_type必须返回bearer,其他类型token不在本协议范围内。在oauth2.0响应报文基础上,oidc增加了id_tken。所有token包含了token或者其他敏感信息的响应报文,必须包含以下响应头。

cache-control no-store
pragma no-cache

失败响应

如果认证失败返回application/json格式错误消息,状态码400

  http/1.1 400 bad request
  content-type: application/json
  cache-control: no-store
  pragma: no-cache

  {
   "error": "invalid_request"
  }

id token校验

客户端必须校验返回的id token, 校验条件如下。对照这些条件,就可以更懂microsoft.authentication.openidconnect里面的代码了,要做的事情很多。

  1. 如果id token被加密,使用客户端注册时候约定的秘钥和算法解密。如果约定了加密方式,id token未被加密,客户端应该拒绝。
  2. 签发方标识必须与iss声明一致
  3. 客户端必须校验aud声明包含了它的客户端id,如果id token未返回正确的audience或者反悔了不被新人的audience,应该拒绝
  4. 如果id token包含多个audience,需要校验是否有azp声明。azp即authorized party,标识被授权的client。
  5. 如果包含azp声明,客户端需要校验其值是否为自己的客户端id
  6. 如果id token由token接口直接颁发给客户端(授权码模式就是如此),客户端必须根据alg参数值的算法验证签名。客户端必须使用签发方提供的秘钥。
  7. alg值默认为rs256,客户端可以在注册的时候使用id_token_signed_response_alg参数指定配置。
  8. 如果jwt的alg头使用了基于mac地址的加密算法,如hs256, hs384,hs512,aud声明中的字节会用作验签。(意思是会把mac地址相关信息写在aud声明上?)
  9. the current time must be before the time represented by the exp claim.
    当前时间必须早于exp(token过期时间)。
  10. iat(签发时间)可以用于拒绝过早、或者过于频繁签发的token,可以用于预防重放攻击。可接受时间范围由客户端自行决定。
  11. 如果认证请求包含了nonce参数,客户端必须交验认证响应中返回的nonce值是否一致。防止重放攻击。
  12. 如果客户端请求了acr声明(authentication context class reference,认证会话上下文,用于表示当前认证会话),必须交验acr值是否合法。
  13. 如果客户端请求了auth_time声明,客户端应该校验认证时间是否已经超出,是否需要重新认证。

access token校验

如果id_token中包含了at_hash声明,需要做下面的校验。at_hash标明了access_token和id_token之间的会话关联关系,做这个校验可以防跨站伪造。

  1. 用idtoken的alg头标明的算法加密access_token,比如alg位rs256,则是用hsa-256算法加密。
  2. 取hash值左边一般使用base64url加密
  3. id token中的at_hash值必须跟上个步骤得到的值一致

校验规则很多,了解一下即可,绝大部分属于客户端需要做的部分,绝大部分跟安全有关。这一块的实现可以参考microsoft.authentication.openidconnect,这是客户端的实现。我们现在看的identityserver是认证服务端的实现。

源码

五种授权模式

有下面几种授权模式可以请求token接口

  • 授权码模式:最常用的code换token
  • 混合模式:混合模式是授权码模式+简化模式混合使用的方式,在用授权码code找token接口换通行/身份令牌的逻辑与授权码模式的逻辑是一样的。idsv4中,混合模式没有自己的单独实现,只是把授权码+简化模式的代码同时调用。
  • 客户端密钥模式:一般用于完全信任的内部系统,密钥换取access_token,由于没有用户参与,scope包含open_id是非法的
  • 用户名密码模式:一般用于第三方对接、无界面交互场景。即username+password换token/id_token,password不一定是密码,也可以是验证码或其他的什么东西,这个完全取决于开发自己的实现
  • 设备流模式(略)

注意:简化模式所有的token都是由认证接口(authorize)一次性返回的,不能使用token接口。

校验请求方法

token接口仅允许post方法,content-type必须为application/x-www-form-urlencoded,否则抛出invalidrequest错误。

public async task<iendpointresult> processasync(httpcontext context)
    {
        _logger.logtrace("processing token request.");

        // validate http
        if (!httpmethods.ispost(context.request.method) || !context.request.hasformcontenttype)
        {
            _logger.logwarning("invalid http request for token endpoint");
            return error(oidcconstants.tokenerrors.invalidrequest);
        }

        return await processtokenrequestasync(context);
    }

处理流程

  • 校验客户端
  • 校验请求参数
  • 创建返回值
  • 返回结果
private async task<iendpointresult> processtokenrequestasync(httpcontext context)
{
    _logger.logdebug("start token request.");

    // validate client
    var clientresult = await _clientvalidator.validateasync(context);

    if (clientresult.client == null)
    {
        return error(oidcconstants.tokenerrors.invalidclient);
    }

    // validate request
    var form = (await context.request.readformasync()).asnamevaluecollection();
    _logger.logtrace("calling into token request validator: {type}", _requestvalidator.gettype().fullname);
    var requestresult = await _requestvalidator.validaterequestasync(form, clientresult);

    if (requestresult.iserror)
    {
        await _events.raiseasync(new tokenissuedfailureevent(requestresult));
        return error(requestresult.error, requestresult.errordescription, requestresult.customresponse);
    }

    // create response
    _logger.logtrace("calling into token request response generator: {type}", _responsegenerator.gettype().fullname);
    var response = await _responsegenerator.processasync(requestresult);

    await _events.raiseasync(new tokenissuedsuccessevent(response, requestresult));
    logtokens(response, requestresult);

    // return result
    _logger.logdebug("token request success.");
    return new tokenresult(response);
}

校验客户端

  • 解码客户端秘钥,对应的处理类是basicauthenticationsecretparser,客户端id和秘钥用base64url加密方法放在authorzaition头上。base64url基本是明文的,因为授权码换token是后端进行的,所以安全性没有问题
  • 解码得到客户端id和秘钥之后,跟store对比校验客户端是否存在,秘钥是否一致。
public async task<clientsecretvalidationresult> validateasync(httpcontext context)
{
    _logger.logdebug("start client validation");

    var fail = new clientsecretvalidationresult
    {
        iserror = true
    };

    var parsedsecret = await _parser.parseasync(context);
    if (parsedsecret == null)
    {
        await raisefailureeventasync("unknown", "no client id found");

        _logger.logerror("no client identifier found");
        return fail;
    }

    // load client
    var client = await _clients.findenabledclientbyidasync(parsedsecret.id);
    if (client == null)
    {
        await raisefailureeventasync(parsedsecret.id, "unknown client");

        _logger.logerror("no client with id '{clientid}' found. aborting", parsedsecret.id);
        return fail;
    }

    secretvalidationresult secretvalidationresult = null;
    if (!client.requireclientsecret || client.isimplicitonly())
    {
        _logger.logdebug("public client - skipping secret validation success");
    }
    else
    {
        secretvalidationresult = await _validator.validateasync(parsedsecret, client.clientsecrets);
        if (secretvalidationresult.success == false)
        {
            await raisefailureeventasync(client.clientid, "invalid client secret");
            _logger.logerror("client secret validation failed for client: {clientid}.", client.clientid);

            return fail;
        }
    }

    _logger.logdebug("client validation success");

    var success = new clientsecretvalidationresult
    {
        iserror = false,
        client = client,
        secret = parsedsecret,
        confirmation = secretvalidationresult?.confirmation
    };

    await raisesuccesseventasync(client.clientid, parsedsecret.type);
    return success;
}

校验请求参数

  • 客户端的portocaltype必须位oidc,否则报错invalidclient
  • 校验granttype,必填,长度不能超过100。
  • granttype默认支持以下几种类型,还可以自定义granttype
    • authorization_code:授权码换token
    • client_credentials:客户端秘钥换token
    • password:用户名密码换token
    • refresn_token:刷新令牌换token
    • urn:ietf:params:oauth:grant-type:device_code:deviceflow,略
public async task<tokenrequestvalidationresult> validaterequestasync(namevaluecollection parameters, clientsecretvalidationresult clientvalidationresult)
{
    _logger.logdebug("start token request validation");

    _validatedrequest = new validatedtokenrequest
    {
        raw = parameters ?? throw new argumentnullexception(nameof(parameters)),
        options = _options
    };

    if (clientvalidationresult == null) throw new argumentnullexception(nameof(clientvalidationresult));

    _validatedrequest.setclient(clientvalidationresult.client, clientvalidationresult.secret, clientvalidationresult.confirmation);

    /////////////////////////////////////////////
    // check client protocol type
    /////////////////////////////////////////////
    if (_validatedrequest.client.protocoltype != identityserverconstants.protocoltypes.openidconnect)
    {
        logerror("invalid protocol type for client",
            new
            {
                clientid = _validatedrequest.client.clientid,
                expectedprotocoltype = identityserverconstants.protocoltypes.openidconnect,
                actualprotocoltype = _validatedrequest.client.protocoltype
            });

        return invalid(oidcconstants.tokenerrors.invalidclient);
    }

    /////////////////////////////////////////////
    // check grant type
    /////////////////////////////////////////////
    var granttype = parameters.get(oidcconstants.tokenrequest.granttype);
    if (granttype.ismissing())
    {
        logerror("grant type is missing");
        return invalid(oidcconstants.tokenerrors.unsupportedgranttype);
    }

    if (granttype.length > _options.inputlengthrestrictions.granttype)
    {
        logerror("grant type is too long");
        return invalid(oidcconstants.tokenerrors.unsupportedgranttype);
    }

    _validatedrequest.granttype = granttype;

    switch (granttype)
    {
        case oidcconstants.granttypes.authorizationcode:
            return await runvalidationasync(validateauthorizationcoderequestasync, parameters);
        case oidcconstants.granttypes.clientcredentials:
            return await runvalidationasync(validateclientcredentialsrequestasync, parameters);
        case oidcconstants.granttypes.password:
            return await runvalidationasync(validateresourceownercredentialrequestasync, parameters);
        case oidcconstants.granttypes.refreshtoken:
            return await runvalidationasync(validaterefreshtokenrequestasync, parameters);
        case oidcconstants.granttypes.devicecode:
            return await runvalidationasync(validatedevicecoderequestasync, parameters);
        default:
            return await runvalidationasync(validateextensiongrantrequestasync, parameters);
    }
}

参数校验 - 授权码模式

  • 客户端allowedgranttypes必须包含authorization_code或者hybrid,否则报错unauthorizedclient。
  • code必填,code长度不能超过100
  • 客户端传过来的code只是授权码的id,从store中取出来授权码对象,如果不存在返回错误invalidgrant
  • 从store中移除授权码,此处实现了code只是用一次
  • 如果授权码超出有效时长,返回错误invalidgrant
  • 校验授权码对象的客户端id与当前客户端是否一致
  • redirect_uri必填,且必须与授权码对象保存的redirect_uri一致,否则返回错误unauthorizedclient
  • 如果请求中没有任何scope,返回错误invalidrequest
  • 判断用户是否启用,这个判断是由可插拔服务iprofileservice的isactive方法来实现的,开发可以注入自己的实现。如果用户禁用将返回错误invalidgrant。
private async task<tokenrequestvalidationresult> validateauthorizationcoderequestasync(namevaluecollection parameters)
    {
        _logger.logdebug("start validation of authorization code token request");

        /////////////////////////////////////////////
        // check if client is authorized for grant type
        /////////////////////////////////////////////
        if (!_validatedrequest.client.allowedgranttypes.tolist().contains(granttype.authorizationcode) &&
            !_validatedrequest.client.allowedgranttypes.tolist().contains(granttype.hybrid))
        {
            logerror("client not authorized for code flow");
            return invalid(oidcconstants.tokenerrors.unauthorizedclient);
        }

        /////////////////////////////////////////////
        // validate authorization code
        /////////////////////////////////////////////
        var code = parameters.get(oidcconstants.tokenrequest.code);
        if (code.ismissing())
        {
            logerror("authorization code is missing");
            return invalid(oidcconstants.tokenerrors.invalidgrant);
        }

        if (code.length > _options.inputlengthrestrictions.authorizationcode)
        {
            logerror("authorization code is too long");
            return invalid(oidcconstants.tokenerrors.invalidgrant);
        }

        _validatedrequest.authorizationcodehandle = code;

        var authzcode = await _authorizationcodestore.getauthorizationcodeasync(code);
        if (authzcode == null)
        {
            logerror("invalid authorization code", new { code });
            return invalid(oidcconstants.tokenerrors.invalidgrant);
        }

        await _authorizationcodestore.removeauthorizationcodeasync(code);

        if (authzcode.creationtime.hasexceeded(authzcode.lifetime, _clock.utcnow.utcdatetime))
        {
            logerror("authorization code expired", new { code });
            return invalid(oidcconstants.tokenerrors.invalidgrant);
        }

        /////////////////////////////////////////////
        // populate session id
        /////////////////////////////////////////////
        if (authzcode.sessionid.ispresent())
        {
            _validatedrequest.sessionid = authzcode.sessionid;
        }

        /////////////////////////////////////////////
        // validate client binding
        /////////////////////////////////////////////
        if (authzcode.clientid != _validatedrequest.client.clientid)
        {
            logerror("client is trying to use a code from a different client", new { clientid = _validatedrequest.client.clientid, codeclient = authzcode.clientid });
            return invalid(oidcconstants.tokenerrors.invalidgrant);
        }

        /////////////////////////////////////////////
        // validate code expiration
        /////////////////////////////////////////////
        if (authzcode.creationtime.hasexceeded(_validatedrequest.client.authorizationcodelifetime, _clock.utcnow.utcdatetime))
        {
            logerror("authorization code is expired");
            return invalid(oidcconstants.tokenerrors.invalidgrant);
        }

        _validatedrequest.authorizationcode = authzcode;
        _validatedrequest.subject = authzcode.subject;

        /////////////////////////////////////////////
        // validate redirect_uri
        /////////////////////////////////////////////
        var redirecturi = parameters.get(oidcconstants.tokenrequest.redirecturi);
        if (redirecturi.ismissing())
        {
            logerror("redirect uri is missing");
            return invalid(oidcconstants.tokenerrors.unauthorizedclient);
        }

        if (redirecturi.equals(_validatedrequest.authorizationcode.redirecturi, stringcomparison.ordinal) == false)
        {
            logerror("invalid redirect_uri", new { redirecturi, expectedredirecturi = _validatedrequest.authorizationcode.redirecturi });
            return invalid(oidcconstants.tokenerrors.unauthorizedclient);
        }

        /////////////////////////////////////////////
        // validate scopes are present
        /////////////////////////////////////////////
        if (_validatedrequest.authorizationcode.requestedscopes == null ||
            !_validatedrequest.authorizationcode.requestedscopes.any())
        {
            logerror("authorization code has no associated scopes");
            return invalid(oidcconstants.tokenerrors.invalidrequest);
        }

        /////////////////////////////////////////////
        // validate pkce parameters
        /////////////////////////////////////////////
        var codeverifier = parameters.get(oidcconstants.tokenrequest.codeverifier);
        if (_validatedrequest.client.requirepkce || _validatedrequest.authorizationcode.codechallenge.ispresent())
        {
            _logger.logdebug("client required a proof key for code exchange. starting pkce validation");

            var proofkeyresult = validateauthorizationcodewithproofkeyparameters(codeverifier, _validatedrequest.authorizationcode);
            if (proofkeyresult.iserror)
            {
                return proofkeyresult;
            }

            _validatedrequest.codeverifier = codeverifier;
        }
        else
        {
            if (codeverifier.ispresent())
            {
                logerror("unexpected code_verifier: {codeverifier}. this happens when the client is trying to use pkce, but it is not enabled. set requirepkce to true.", codeverifier);
                return invalid(oidcconstants.tokenerrors.invalidgrant);
            }
        }

        /////////////////////////////////////////////
        // make sure user is enabled
        /////////////////////////////////////////////
        var isactivectx = new isactivecontext(_validatedrequest.authorizationcode.subject, _validatedrequest.client, identityserverconstants.profileisactivecallers.authorizationcodevalidation);
        await _profile.isactiveasync(isactivectx);

        if (isactivectx.isactive == false)
        {
            logerror("user has been disabled", new { subjectid = _validatedrequest.authorizationcode.subject.getsubjectid() });
            return invalid(oidcconstants.tokenerrors.invalidgrant);
        }

        _logger.logdebug("validation of authorization code token request success");

        return valid();
    }

参数校验 - 客户端秘钥模式

  • 校验client是否允许使用客户端秘钥认证模式
  • 校验客户端是否允许访问请求的授权范围scope
  • scope中包含openid的话返回错误invlidscope,因为本模式没有涉及用户信息
  • cope中包含offline_access则返回错误invalidscope,本模式不允许使用refresh_token
private async task<tokenrequestvalidationresult> validateclientcredentialsrequestasync(namevaluecollection parameters)
    {
        _logger.logdebug("start client credentials token request validation");

        /////////////////////////////////////////////
        // check if client is authorized for grant type
        /////////////////////////////////////////////
        if (!_validatedrequest.client.allowedgranttypes.tolist().contains(granttype.clientcredentials))
        {
            logerror("client not authorized for client credentials flow, check the allowedgranttypes setting", new { clientid = _validatedrequest.client.clientid });
            return invalid(oidcconstants.tokenerrors.unauthorizedclient);
        }

        /////////////////////////////////////////////
        // check if client is allowed to request scopes
        /////////////////////////////////////////////
        if (!await validaterequestedscopesasync(parameters, ignoreimplicitidentityscopes: true, ignoreimplicitofflineaccess: true))
        {
            return invalid(oidcconstants.tokenerrors.invalidscope);
        }

        if (_validatedrequest.validatedscopes.containsopenidscopes)
        {
            logerror("client cannot request openid scopes in client credentials flow", new { clientid = _validatedrequest.client.clientid });
            return invalid(oidcconstants.tokenerrors.invalidscope);
        }

        if (_validatedrequest.validatedscopes.containsofflineaccessscope)
        {
            logerror("client cannot request a refresh token in client credentials flow", new { clientid = _validatedrequest.client.clientid });
            return invalid(oidcconstants.tokenerrors.invalidscope);
        }

        _logger.logdebug("{clientid} credentials token request validation success", _validatedrequest.client.clientid);
        return valid();
    }

参数校验 - 用户名密码模式

  • 校验客户端是否允许使用用户名密码模式,校验失败返回unauthoriedclient错误
  • 校验客户端是否有权访问所请求的所有scope,校验失败返回invalidscope错误
  • 从请求中获取username和password参数,未提供username返回invalidgrant错误,未提供password则设置password位空值
  • username和password长度不能超过100,否则返回invalidgrant错误
  • 使用iresourceownerpasswordvalidator校验username和password,此接口需要开发实现后注入,否则会抛出异常
  • grantvalidationresult的subject不能为null,因此开发的iresourceownerpasswordvalidator实现,校验成功后必须给grantvalidationresult赋值
  • 校验用户是否禁用,根据可插拔服务iprofileservice的isacrtive方法判断,如果禁用返回ivalidgrant错误。
private async task<tokenrequestvalidationresult> validateresourceownercredentialrequestasync(namevaluecollection parameters)
{
    _logger.logdebug("start resource owner password token request validation");

    /////////////////////////////////////////////
    // check if client is authorized for grant type
    /////////////////////////////////////////////
    if (!_validatedrequest.client.allowedgranttypes.contains(granttype.resourceownerpassword))
    {
        logerror("client not authorized for resource owner flow, check the allowedgranttypes setting", new { client_id = _validatedrequest.client.clientid });
        return invalid(oidcconstants.tokenerrors.unauthorizedclient);
    }

    /////////////////////////////////////////////
    // check if client is allowed to request scopes
    /////////////////////////////////////////////
    if (!(await validaterequestedscopesasync(parameters)))
    {
        return invalid(oidcconstants.tokenerrors.invalidscope);
    }

    /////////////////////////////////////////////
    // check resource owner credentials
    /////////////////////////////////////////////
    var username = parameters.get(oidcconstants.tokenrequest.username);
    var password = parameters.get(oidcconstants.tokenrequest.password);

    if (username.ismissing())
    {
        logerror("username is missing");
        return invalid(oidcconstants.tokenerrors.invalidgrant);
    }

    if (password.ismissing())
    {
        password = "";
    }

    if (username.length > _options.inputlengthrestrictions.username ||
        password.length > _options.inputlengthrestrictions.password)
    {
        logerror("username or password too long");
        return invalid(oidcconstants.tokenerrors.invalidgrant);
    }

    _validatedrequest.username = username;


    /////////////////////////////////////////////
    // authenticate user
    /////////////////////////////////////////////
    var resourceownercontext = new resourceownerpasswordvalidationcontext
    {
        username = username,
        password = password,
        request = _validatedrequest
    };
    await _resourceownervalidator.validateasync(resourceownercontext);

    if (resourceownercontext.result.iserror)
    {
        // protect against bad validator implementations
        resourceownercontext.result.error = resourceownercontext.result.error ?? oidcconstants.tokenerrors.invalidgrant;

        if (resourceownercontext.result.error == oidcconstants.tokenerrors.unsupportedgranttype)
        {
            logerror("resource owner password credential grant type not supported");
            await raisefailedresourceownerauthenticationeventasync(username, "password grant type not supported", resourceownercontext.request.client.clientid);

            return invalid(oidcconstants.tokenerrors.unsupportedgranttype, customresponse: resourceownercontext.result.customresponse);
        }

        var errordescription = "invalid_username_or_password";

        if (resourceownercontext.result.errordescription.ispresent())
        {
            errordescription = resourceownercontext.result.errordescription;
        }

        loginformation("user authentication failed: ", errordescription ?? resourceownercontext.result.error);
        await raisefailedresourceownerauthenticationeventasync(username, errordescription, resourceownercontext.request.client.clientid);

        return invalid(resourceownercontext.result.error, errordescription, resourceownercontext.result.customresponse);
    }

    if (resourceownercontext.result.subject == null)
    {
        var error = "user authentication failed: no principal returned";
        logerror(error);
        await raisefailedresourceownerauthenticationeventasync(username, error, resourceownercontext.request.client.clientid);

        return invalid(oidcconstants.tokenerrors.invalidgrant);
    }

    /////////////////////////////////////////////
    // make sure user is enabled
    /////////////////////////////////////////////
    var isactivectx = new isactivecontext(resourceownercontext.result.subject, _validatedrequest.client, identityserverconstants.profileisactivecallers.resourceownervalidation);
    await _profile.isactiveasync(isactivectx);

    if (isactivectx.isactive == false)
    {
        logerror("user has been disabled", new { subjectid = resourceownercontext.result.subject.getsubjectid() });
        await raisefailedresourceownerauthenticationeventasync(username, "user is inactive", resourceownercontext.request.client.clientid);

        return invalid(oidcconstants.tokenerrors.invalidgrant);
    }

    _validatedrequest.username = username;
    _validatedrequest.subject = resourceownercontext.result.subject;

    await raisesuccessfulresourceownerauthenticationeventasync(username, resourceownercontext.result.subject.getsubjectid(), resourceownercontext.request.client.clientid);
    _logger.logdebug("resource owner password token request validation success.");
    return valid(resourceownercontext.result.customresponse);
}

参数校验 - refreshtoken

refreshtoken-刷新令牌。顾名思义,用于刷新通行令牌的凭证。拥有offline_access权限的客户端可以使用刷新令牌。只有授权码、混合流程等由后端参与的授权模式才允许使用刷新令牌。

  • 从请求中获取refresh_token参数值,如果为空则返回invalidrequest错误
  • 如果刷新令牌长度超过100,返回invalidgrant错误
  • 判断刷新令牌是否存在且有效
    • 是否能从store中查询到刷新令牌对象
    • 校验刷新令牌是否过期
    • 校验刷新令牌是否属于当前客户端
    • 校验客户端是否仍然有offline_access权限
    • 校验用户是否被禁用
private async task<tokenrequestvalidationresult> validaterefreshtokenrequestasync(namevaluecollection parameters)
{
    _logger.logdebug("start validation of refresh token request");

    var refreshtokenhandle = parameters.get(oidcconstants.tokenrequest.refreshtoken);
    if (refreshtokenhandle.ismissing())
    {
        logerror("refresh token is missing");
        return invalid(oidcconstants.tokenerrors.invalidrequest);
    }

    if (refreshtokenhandle.length > _options.inputlengthrestrictions.refreshtoken)
    {
        logerror("refresh token too long");
        return invalid(oidcconstants.tokenerrors.invalidgrant);
    }

    var result = await _tokenvalidator.validaterefreshtokenasync(refreshtokenhandle, _validatedrequest.client);

    if (result.iserror)
    {
        logwarning("refresh token validation failed. aborting");
        return invalid(oidcconstants.tokenerrors.invalidgrant);
    }

    _validatedrequest.refreshtoken = result.refreshtoken;
    _validatedrequest.refreshtokenhandle = refreshtokenhandle;
    _validatedrequest.subject = result.refreshtoken.subject;

    _logger.logdebug("validation of refresh token request success");
    return valid();
}

生成响应报文 - 授权码模式

  • 生成accesstoken和refreshtoken
    • 通行令牌由itokenservice接口,默认实现defaulttokenservice的createaccesstokenasync方法生成
    • 如果请求了offline_access才生成refresh_token
    • 如果code是oidc授权码,生成id_token。defaulttokenservice的createidentitytokenasync方法生成token对象,createsecuritytokenasync方法将token对象加密为jwt。
protected virtual async task<tokenresponse> processauthorizationcoderequestasync(tokenrequestvalidationresult request)
    {
        logger.logtrace("creating response for authorization code request");

        //////////////////////////
        // access token
        /////////////////////////
        (var accesstoken, var refreshtoken) = await createaccesstokenasync(request.validatedrequest);
        var response = new tokenresponse
        {
            accesstoken = accesstoken,
            accesstokenlifetime = request.validatedrequest.accesstokenlifetime,
            custom = request.customresponse,
            scope = request.validatedrequest.authorizationcode.requestedscopes.tospaceseparatedstring(),
        };

        //////////////////////////
        // refresh token
        /////////////////////////
        if (refreshtoken.ispresent())
        {
            response.refreshtoken = refreshtoken;
        }

        //////////////////////////
        // id token
        /////////////////////////
        if (request.validatedrequest.authorizationcode.isopenid)
        {
            // load the client that belongs to the authorization code
            client client = null;
            if (request.validatedrequest.authorizationcode.clientid != null)
            {
                client = await clients.findenabledclientbyidasync(request.validatedrequest.authorizationcode.clientid);
            }
            if (client == null)
            {
                throw new invalidoperationexception("client does not exist anymore.");
            }

            var resources = await resources.findenabledresourcesbyscopeasync(request.validatedrequest.authorizationcode.requestedscopes);

            var tokenrequest = new tokencreationrequest
            {
                subject = request.validatedrequest.authorizationcode.subject,
                resources = resources,
                nonce = request.validatedrequest.authorizationcode.nonce,
                accesstokentohash = response.accesstoken,
                statehash = request.validatedrequest.authorizationcode.statehash,
                validatedrequest = request.validatedrequest
            };

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

        return response;
    }

生成响应报文 - 客户端密钥模式

  • 仅生成accesstoken
protected virtual task<tokenresponse> processclientcredentialsrequestasync(tokenrequestvalidationresult request)
{
    logger.logtrace("creating response for client credentials request");

    return processtokenrequestasync(request);
}

protected virtual async task<tokenresponse> processtokenrequestasync(tokenrequestvalidationresult validationresult)
    {
        (var accesstoken, var refreshtoken) = await createaccesstokenasync(validationresult.validatedrequest);
        var response = new tokenresponse
        {
            accesstoken = accesstoken,
            accesstokenlifetime = validationresult.validatedrequest.accesstokenlifetime,
            custom = validationresult.customresponse,
            scope = validationresult.validatedrequest.scopes.tospaceseparatedstring()
        };

        if (refreshtoken.ispresent())
        {
            response.refreshtoken = refreshtoken;
        }

        return response;
    }

生成响应报文 - 用户名密码模式

  • 生成accesstoken
  • 如果申请了offline_access且有权限,同时返回refresh_token
  • 不会返回id_token,我理解的是授权码等模式是有限授权,需要code换id_token,才能拿到用户id以及其他的基本信息。而密码模式是完全信任授权,账号密码都给你了,还整id_token干嘛,你要啥信息,自己实现iresourceownerpasswordvalidator,自己去库里取就完事了,还要啥自行车。
protected virtual task<tokenresponse> processpasswordrequestasync(tokenrequestvalidationresult request)
    {
        logger.logtrace("creating response for password request");

        return processtokenrequestasync(request);
    }

生成响应报文 - 刷新令牌

  • 从请求中取出旧通行令牌
  • 判断客户端配置updateaccesstokenclaimsonrefresh-是否在刷新令牌的时候更新通行令牌的claims,默认false。如果为true,则创建新的token对象,否则使用旧的token,只是刷新token的创建时间和有效时间。
  • 判断客户端配置refreshtokenusage - 刷新令牌用法,0:reuse可重复使用 1:ontimeonly一次性,默认1。如果是一次性的话,从store中删除旧的刷新令牌,创建新的刷新令牌。
  • 判断客户端配置refreshtokenexpiration - 刷新令牌过期类型,0:sliding,1:absolute,默认1。如果是0,需要重新计算相对时间。
  • 如果刷新令牌请求包含了任意身份资源,创建新的身份令牌。
protected virtual async task<tokenresponse> processrefreshtokenrequestasync(tokenrequestvalidationresult request)
    {
        logger.logtrace("creating response for refresh token request");

        var oldaccesstoken = request.validatedrequest.refreshtoken.accesstoken;
        string accesstokenstring;

        if (request.validatedrequest.client.updateaccesstokenclaimsonrefresh)
        {
            var subject = request.validatedrequest.refreshtoken.subject;

            var creationrequest = new tokencreationrequest
            {
                subject = subject,
                validatedrequest = request.validatedrequest,
                resources = await resources.findenabledresourcesbyscopeasync(oldaccesstoken.scopes)
            };

            var newaccesstoken = await tokenservice.createaccesstokenasync(creationrequest);
            accesstokenstring = await tokenservice.createsecuritytokenasync(newaccesstoken);
        }
        else
        {
            oldaccesstoken.creationtime = clock.utcnow.utcdatetime;
            oldaccesstoken.lifetime = request.validatedrequest.accesstokenlifetime;

            accesstokenstring = await tokenservice.createsecuritytokenasync(oldaccesstoken);
        }

        var handle = await refreshtokenservice.updaterefreshtokenasync(request.validatedrequest.refreshtokenhandle, request.validatedrequest.refreshtoken, request.validatedrequest.client);

        return new tokenresponse
        {
            identitytoken = await createidtokenfromrefreshtokenrequestasync(request.validatedrequest, accesstokenstring),
            accesstoken = accesstokenstring,
            accesstokenlifetime = request.validatedrequest.accesstokenlifetime,
            refreshtoken = handle,
            custom = request.customresponse,
            scope = request.validatedrequest.refreshtoken.scopes.tospaceseparatedstring()
        };
    }

《IdentityServer4源码解析_4_令牌发放接口.doc》

下载本文的Word格式文档,以方便收藏与打印。