首页 > 解决方案 > 添加到 dotnetcore 中的 Slack 而没有身份框架错误:oauth 状态丢失或无效

问题描述

我正在尝试为我的 slackbot 创建一个非常简单的页面,以便用户可以登录和注册。但是,即使使用他们生成的“使用 Slack 登录”按钮,我也会收到错误消息“oauth 状态丢失或无效。”。“添加到 Slack”也会发生同样的错误。

我的代码基于https://dotnetthoughts.net/slack-authentication-with-aspnet-core/。尽管它已经过时了,但它是我能在网上找到的唯一例子。我试图弄清楚我需要改变什么才能让它与 dotnetcore 3 和 Slack 2.0 一起工作,但我已经走到了尽头。

在我的服务中,在调用 AddMvc 等之前,我有以下内容。

services.AddAuthentication(options =>
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = "MyAuthCookieName";
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.MaxAge = TimeSpan.FromDays(7);
        options.ExpireTimeSpan = TimeSpan.FromDays(7);

        options.LoginPath = $"/login";
        options.LogoutPath = $"/logout";
        options.AccessDeniedPath = $"/AccessDenied";
        options.SlidingExpiration = true;
        options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
    })
    //.AddSlack(options =>
    //{
    //    options.ClientId = Configuration["Slack:ClientId"];
    //    options.ClientSecret = Configuration["Slack:ClientSecret"];
    //});
    .AddOAuth("Slack", options =>
    {
        options.ClientId = Configuration["Slack:ClientId"];
        options.ClientSecret = Configuration["Slack:ClientSecret"];
        options.CallbackPath = new PathString("/signin-slack");
        options.AuthorizationEndpoint = $"https://slack.com/oauth/authorize";
        options.TokenEndpoint = "https://slack.com/api/oauth.access";
        options.UserInformationEndpoint = "https://slack.com/api/users.identity?token=";
        options.Scope.Add("identity.basic");
        options.Events = new OAuthEvents()
        {
            OnCreatingTicket = async context =>
            {
                var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint + context.AccessToken);
                var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
                response.EnsureSuccessStatusCode();
                var userObject = JObject.Parse(await response.Content.ReadAsStringAsync());
                var user = userObject.SelectToken("user");
                var userId = user.Value<string>("id");

                if (!string.IsNullOrEmpty(userId))
                {
                    context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId, ClaimValueTypes.String, context.Options.ClaimsIssuer));
                }

                var fullName = user.Value<string>("name");
                if (!string.IsNullOrEmpty(fullName))
                {
                    context.Identity.AddClaim(new Claim(ClaimTypes.Name, fullName, ClaimValueTypes.String, context.Options.ClaimsIssuer));
                }
            }
        };
    });

我的配置方法看起来像

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.Map("/login", builder =>
{
    builder.Run(async context =>
    {
        await context.ChallengeAsync("Slack", properties: new AuthenticationProperties { RedirectUri = "/" });
    });
});

app.Map("/logout", builder =>
{
    builder.Run(async context =>
    {
        await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        context.Response.Redirect("/");
    });
});

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
    endpoints.MapRazorPages();
});

除了“无效时缺少 oauth 状态”之外,如果在我的应用程序中我直接转到 /login,我不会收到错误消息,但似乎我没有登录为User.Identity.IsAuthenticated假。

我真的很茫然,可以使用一些非常感谢的帮助!

谢谢!

大规模更新

我让日志进入 slack 工作,但我无法让 Add to Slack 按钮工作。

这是我的新服务:

services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })
    .AddCookie(options =>
    {
        options.LoginPath = "/login";
        options.LogoutPath = "/logout";
    })
     .AddSlack(options =>
    {
        options.ClientId = Configuration["Slack:ClientId"];
        options.ClientSecret = Configuration["Slack:ClientSecret"];
        options.CallbackPath =  $"{SlackAuthenticationDefaults.CallbackPath}?state={Guid.NewGuid():N}";
        options.ReturnUrlParameter = new PathString("/");
        options.Events = new OAuthEvents()
        {
            OnCreatingTicket = async context =>
            {
                var request = new HttpRequestMessage(HttpMethod.Get, $"{context.Options.UserInformationEndpoint}?token={context.AccessToken}");
                var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
                response.EnsureSuccessStatusCode();
                var userObject = JObject.Parse(await response.Content.ReadAsStringAsync());
                var user = userObject.SelectToken("user");
                var userId = user.Value<string>("id");

                if (!string.IsNullOrEmpty(userId))
                {
                    context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId, ClaimValueTypes.String, context.Options.ClaimsIssuer));
                }

                var fullName = user.Value<string>("name");
                if (!string.IsNullOrEmpty(fullName))
                {
                    context.Identity.AddClaim(new Claim(ClaimTypes.Name, fullName, ClaimValueTypes.String, context.Options.ClaimsIssuer));
                }
            }
        };
    });

根据@timur,我刮掉了我的 app.Map 并使用了身份验证控制器:

public class AuthenticationController : Controller
{
    [HttpGet("~/login")]
    public async Task<IActionResult> SignIn()
    {
        return Challenge(new AuthenticationProperties { RedirectUri = "/" }, "Slack");
    }

    [HttpGet("~/signin-slack")]
    public IActionResult SignInSlack()
    {
        return RedirectToPage("/Index");
    }

    [HttpGet("~/logout"), HttpPost("~/logout")]
    public IActionResult SignOut()
    {
        return SignOut(new AuthenticationProperties { RedirectUri = "/" },
            CookieAuthenticationDefaults.AuthenticationScheme);
    }
}

Slack 提供了“添加到 Slack”按钮。

<a href="https://slack.com/oauth/authorize?scope=incoming-webhook,commands,bot&client_id=#############"><img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>

因此,当用户单击“登录”时,它会登录并获取他们的姓名等。您会注意到,在我的身份验证控制器中,我添加了一个路径为“~/signin-slack”的函数,这是因为我手动添加了“Options.CallbackPath”添加一个状态参数。如果我删除“Options.CallbackPath”,我会收到一条错误消息,指出 oauth 状态丢失或无效。

所以,我不确定我在 Slack 方面缺少什么。他们让它听起来很容易!

对不起,很长的帖子/更新。谢谢你的帮助。

标签: c#.net-coreslackslack-api

解决方案


您提到的同一篇文章下面有一个指向AspNet.Security.OAuth.Providers源代码库的链接。这似乎相当活跃,并且支持包括 Slack 在内的其他 oAuth 目标的 HEAPS。

我假设您已经创建并配置了您的 slack 应用程序。重定向 URL部分在那里至关重要,因为无论您指定 http 还是 http回调很重要(我的示例仅在我使用 https 时才有效)。

综上所述,我相信实施它的一般方法是

Install-Package AspNet.Security.OAuth.Slack -Version 3.0.0

并像这样编辑你Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options => { /* your options verbatim */ })
            .AddSlack(options =>
            {
                options.ClientId = "xxx";
                options.ClientSecret = "xxx";
            });
}

我看到您选择直接在 Startup 类中映射您的登录/注销路由,这实际上可能是问题所在 - 调用.Map()分支请求管道,因此您不会遇到您之前设置的相同中间件链),所以我去了使用单独的控制器(根据示例应用程序):

public class AuthenticationController : Controller
    {
        [HttpGet("~/signin")]
        public async Task<IActionResult> SignIn()
        {
            // Instruct the middleware corresponding to the requested external identity
            // provider to redirect the user agent to its own authorization endpoint.
            // Note: the authenticationScheme parameter must match the value configured in Startup.cs
            return Challenge(new AuthenticationProperties { RedirectUri = "/" }, "Slack");
        }

        [HttpGet("~/signout"), HttpPost("~/signout")]
        public IActionResult SignOut()
        {
            // Instruct the cookies middleware to delete the local cookie created
            // when the user agent is redirected from the external identity provider
            // after a successful authentication flow (e.g Google or Facebook).
            return SignOut(new AuthenticationProperties { RedirectUri = "/" },
                CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }

然而,看着你的代码片段,我怀疑你已经安装了这个 nuget 包并尝试使用它。这使我推荐了一些要检查的东西:

  1. 在 Slack 应用程序配置中仔细检查您的重定向 URL,
  2. 检查您的identity.basic范围是否实际为您的应用启用
  3. 尝试在单独的控制器而不是启动类中处理登录操作
  4. 确保您的应用程序使用 SSL 运行:(**Project properties** -> **Debug** tab -> **Enable SSL** checkbox如果 IIS express 托管,否则您可能需要做一些额外的工作
  5. 查看示例项目,它可能会让您了解您的设置有何不同

UPD:所以经过一番反复,我能够更好地了解您的问题。我确实相信您所观察到的内容与使用 slack 登录是分开的,而与他们的应用程序安装流程有关。正如您已经指出的,“添加到 slack”流程和用户登录之间的区别在于 - 该state参数不是您的源 URL 的一部分,因此不会在请求之间返回给您。这对于 oAuth 处理程序来说意义重大,因为它依赖于state验证请求的完整性,并且如果状态为空则简单地失败。在 github上进行了讨论,但我相信结果是 - 你将不得不自己跳过验证部分。所以我继承SlackAuthenticationHandler了 nuget 包附带的代码,并删除了给我带来问题的代码:

    public class SlackNoStateAuthenticationHandler : SlackAuthenticationHandler {
        public SlackNoStateAuthenticationHandler([NotNull] IOptionsMonitor<SlackAuthenticationOptions> options,
            [NotNull] ILoggerFactory logger,
            [NotNull] UrlEncoder encoder,
            [NotNull] ISystemClock clock) : base(options, logger, encoder, clock) { }

        public void GenerateCorrelationIdPublic(AuthenticationProperties properties)
        {
            GenerateCorrelationId(properties);
        }

        protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
        {
            var query = Request.Query;

            var state = query["state"];
            var properties = Options.StateDataFormat.Unprotect(state);

            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(new OAuthCodeExchangeContext(properties, 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);
            }
        }
    }

大部分代码是相关源代码的逐字复制,因此如果需要,您可以随时进行更多更改;

然后我们需要将 sensible state 参数注入到您的 URL 中。假设你有一个控制器和一个视图:

家庭控制器

public class HomeController : Controller
    { 
        private readonly IAuthenticationHandlerProvider _handler;

        public HomeController(IAuthenticationHandlerProvider handler)
        {
            _handler = handler;
        }

        public async Task<IActionResult> Index()
        {
            var handler = await _handler.GetHandlerAsync(HttpContext, "Slack") as SlackNoStateAuthenticationHandler; // we'd get the configured instance
            var props = new AuthenticationProperties { RedirectUri = "/" }; // provide some sane defaults
            handler.GenerateCorrelationIdPublic(props); // generate xsrf token and add it into the properties object
            ViewBag.state = handler.Options.StateDataFormat.Protect(props); // and push it into your view.
            return View();
        }
}

启动.cs

.AddOAuth<SlackAuthenticationOptions, SlackNoStateAuthenticationHandler>(SlackAuthenticationDefaults.AuthenticationScheme, SlackAuthenticationDefaults.DisplayName, options =>
            {
                options.ClientId = "your_id";
                options.ClientSecret = "your_secret";
            });

索引.cshtml

<a href="https://slack.com/oauth/authorize?client_id=<your_id>&scope=identity.basic&state=@ViewBag.state"><img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x"></a>

这使我能够成功完成请求,尽管我不完全确定这样做是否会被视为最佳实践


推荐阅读