【问题标题】:How to customize the authorization error produced by OpenIddict?如何自定义 OpenIddict 产生的授权错误?
【发布时间】:2019-04-15 03:41:30
【问题描述】:

我在 .NET Core 2 API 中使用 OpenIddict 进行身份验证。客户端我依靠任何 API 错误来遵循自定义方案。但是,当例如刷新令牌已过时,我似乎无法找到如何自定义发回的错误。

永远不会到达 /token 端点,因此错误不在“我的控制”之下。

请求的结果是状态码 400,带有以下 JSON:

{"error":"invalid_grant","error_description":"The specified refresh token is no longer valid."}

我尝试使用自定义中间件来捕获所有状态代码(它确实如此),但结果在我的自定义中间件执行完成之前返回

如何正确自定义错误或拦截以更改它?谢谢!

【问题讨论】:

    标签: asp.net-core openiddict


    【解决方案1】:

    您可以使用 OpenIddict 的事件模型在令牌响应负载写入响应流之前对其进行自定义。这是一个例子:

    MyApplyTokenResponseHandler.cs

    public class MyApplyTokenResponseHandler : IOpenIddictServerEventHandler<ApplyTokenResponseContext>
    {
        public ValueTask HandleAsync(ApplyTokenResponseContext context)
        {
            var response = context.Response;
            if (string.Equals(response.Error, OpenIddictConstants.Errors.InvalidGrant, StringComparison.Ordinal) &&
               !string.IsNullOrEmpty(response.ErrorDescription))
            {
                response.ErrorDescription = "Your customized error";
            }
    
            return default;
        }
    }
    

    Startup.cs

    services.AddOpenIddict()
        .AddCore(options =>
        {
            // ...
        })
    
        .AddServer(options =>
        {
            // ...
            options.AddEventHandler<ApplyTokenResponseContext>(builder =>
                builder.UseSingletonHandler<MyApplyTokenResponseHandler>());
        })
    
        .AddValidation();
    

    【讨论】:

    • 很棒的信息,这让我可以自定义错误描述字符串。但是,如果我想将自己的原始 json 对象写入响应流,我该如何正确地做到这一点?
    • 您可以简单地将您的 JSON 项目添加到 response 对象(例如 response["my_json_array"] = JArray.FromObject(new[] { 42 }))或通过自己编写对输出流的响应来完全控制响应呈现(notification.Context.HttpContext.Response.Body)并调用notification.Context.HandleResponse() 通知 OpenIddict 您不希望它应用其默认逻辑。
    • 哇,一个不错的解决方案!
    【解决方案2】:

    永远不会到达 /token 端点,因此错误不在“我的控制范围内”。

    实际上,达到了/tokengrant_type的参数等于refresh_token但是刷新令牌过期时的拒绝逻辑不是我们处理的。 这是源代码中的某种“硬编码”

    if (token == null)
    {
        context.Reject(
            error: OpenIddictConstants.Errors.InvalidGrant,
            description: context.Request.IsAuthorizationCodeGrantType() ?
                "The specified authorization code is no longer valid." :
                "The specified refresh token is no longer valid.");
    
        return;
    }
    
    if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType())
    {
        if (!await TryRedeemTokenAsync(token))
        {
            context.Reject(
                error: OpenIddictConstants.Errors.InvalidGrant,
                description: context.Request.IsAuthorizationCodeGrantType() ?
                    "The specified authorization code is no longer valid." :
                    "The specified refresh token is no longer valid.");
    
            return;
        }
    }
    

    这里的context.Reject来自程序集AspNet.Security.OpenIdConnect.Server

    更多详情请见source code on GitHub

    我尝试使用自定义中间件来捕获所有状态代码(确实如此),但结果在我的自定义中间件执行完成之前返回。

    我试过了,我很确定我们可以使用自定义中间件来捕获所有状态码。关键是在next()调用之后检测状态码: p>

    app.Use(async(context , next )=>{
    
        // passby all other end points
        if(! context.Request.Path.StartsWithSegments("/connect/token")){
            await next();
            return;
        }
    
        // since we might want to detect the Response.Body, I add some stream here .
        // if you only want to detect the status code , there's no need to use these streams
        Stream originalStream = context.Response.Body;
        var hijackedStream = new MemoryStream();
        context.Response.Body = hijackedStream;
        hijackedStream.Seek(0,SeekOrigin.Begin);
    
        await next();
    
        // if status code not 400 , pass by
        if(context.Response.StatusCode != 400){
            await CopyStreamToResponseBody(context,hijackedStream,originalStream);
            return;
        }
    
        // read and custom the stream 
        hijackedStream.Seek(0,SeekOrigin.Begin);
        using (StreamReader sr = new StreamReader(hijackedStream))
        {
            var raw= sr.ReadToEnd();
            if(raw.Contains("The specified refresh token is no longer valid.")){
                // custom your own response
                context.Response.StatusCode = 401;
                // ...
                //context.Response.Body = ... /
            }else{
                await CopyStreamToResponseBody(context,hijackedStream,originalStream);
            }
        }
    });
    
    // helper to make the copy easy
    private async Task CopyStreamToResponseBody(HttpContext context,Stream newStream, Stream originalStream){
    
        newStream.Seek(0,SeekOrigin.Begin);
        await newStream.CopyToAsync(originalStream);
        context.Response.ContentLength =originalStream.Length;
        context.Response.Body = originalStream;
    }
    

    【讨论】:

    • 我认为 /token 端点未到达的原因是令牌端点所在的控制器的构造函数永远不会被调用。对于该控制器中 /token 路由的实际实现(自然)也是如此。不过,我很快会测试您的自定义中间件,以确认该变通方法是否有效。
    猜你喜欢
    • 2017-04-15
    • 2019-12-08
    • 1970-01-01
    • 1970-01-01
    • 2021-07-27
    • 1970-01-01
    • 1970-01-01
    • 2011-09-18
    • 2016-12-03
    相关资源
    最近更新 更多