aspnet-contrib / AspNet.Security.OAuth.Providers

OAuth 2.0 social authentication providers for ASP.NET Core
Apache License 2.0
2.35k stars 533 forks source link

KeyNotFoundException: The given key was not present in the dictionary. #768

Open Pdawg05 opened 1 year ago

Pdawg05 commented 1 year ago

Describe the bug

I'm trying to implement OAuth with Slack in my ASP.NET 7 application. After the OAuth portal opens up and I login, a call back request is made to my https://localhost:7138/signin-slack endpoint. However, I then get this error.

System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
   at System.Text.Json.JsonElement.GetProperty(String propertyName)
   at AspNet.Security.OAuth.Slack.SlackAuthenticationOptions.<>c.<.ctor>b__0_0(JsonElement user)
   at Microsoft.AspNetCore.Authentication.OAuth.Claims.CustomJsonClaimAction.Run(JsonElement userData, ClaimsIdentity identity, String issuer)
   at Microsoft.AspNetCore.Authentication.OAuth.OAuthCreatingTicketContext.RunClaimActions(JsonElement userData)
   at Microsoft.AspNetCore.Authentication.OAuth.OAuthCreatingTicketContext.RunClaimActions()
   at AspNet.Security.OAuth.Slack.SlackAuthenticationHandler.CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
   at Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler`1.HandleRemoteAuthenticateAsync()
   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()

Steps To reproduce

My program.cs code:

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApplicationParts;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();

builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; } )
    .AddSlack(o =>
    {  
        o.ClientId = "CLIENT_ID";
        o.ClientSecret = "CLIENT_SECRET";
        o.AuthorizationEndpoint = "https://slack.com/oauth/authorize";
        o.TokenEndpoint = "https://slack.com/api/oauth.access";

    }).AddCookie(options =>
    {

        options.LoginPath = "/Authentication/signin";
        options.LogoutPath = "/signout";
    });

var app = builder.Build();

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

app.MapDefaultControllerRoute();
app.UseStaticFiles();
app.Run();

Here's my controller:

public class AuthenticationController : Controller
    {
        [HttpGet]
        public async Task<IActionResult> SignIn()
        {
            return View();
        } 

        [HttpPost]
        public async Task<IActionResult> AuthorizeSlack(string provider = "Slack")
        {
            Console.WriteLine("Got post request from button");
            // Note: the "provider" parameter corresponds to the external
            // authentication provider choosen by the user agent.
            if (string.IsNullOrWhiteSpace(provider))
            {
                return BadRequest();
            }

            if (!await HttpContext.IsProviderSupportedAsync(provider))
            {
                Console.WriteLine("provider not supported returning bad request");
                return BadRequest();
            }

            // 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 = "/Dashboard" }, "Slack");
        }

        [HttpPost]
        public IActionResult SignOutCurrentUser()
        {
            // 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);
        }
}

Expected behaviour

The authentication looks like it was successful?? What is going on here?

Actual behaviour

Also I have no idea if this is worth mentioning, but obviously most of this code is from the sample Mvc.Client you guys provide in this repo. I noticed when I add Slack to the Startup.cs file and attempt to login I get a 500 Error. All the other OAuth providers seem to work fine for me though...

System information

martincostello commented 1 year ago

It looks like one of these properties isn't present in the response from the Slack API for your user:

https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/blob/f8e13868c8d9c3428b738206acbdcbe196ca7564/src/AspNet.Security.OAuth.Slack/SlackAuthenticationOptions.cs#L29

Pdawg05 commented 1 year ago

Any idea why those fields aren't being returned by the Slack API? I'm providing all the information I need to like ClientSecret/ClientId

kevinchalet commented 1 year ago

Any idea why those fields aren't being returned by the Slack API? I'm providing all the information I need to like ClientSecret/ClientId

If I had to guess, I'd say it's not related to your Slack provider configuration, but either to your client registration at Slack or more likely to your user account. Try to intercept the userinfo response with Fiddler to see what field(s) is/are missing.

You can also give the OpenIddict Slack integration a try: it uses the newer OpenID Connect-based userinfo endpoint, which may return different information (see https://kevinchalet.com/2022/12/16/getting-started-with-the-openiddict-web-providers/ for an example with GitHub).

martincostello commented 1 year ago

It could also the case that the code has always been wrong and incorrectly assumed those fields are always present and you’re just the first person to ever use it in a configuration where it isn’t in the response.

Pdawg05 commented 1 year ago

This is the exact request being sent to my callback URL

GET /signin-slack?code=5006616070674.5010877158211.408b2d23bbe3fd6a90eab49f4abfa8059f00ce2ed7f03360828ed7e7b5b30c8b&state=CfDJ8NtH7axWlrdOho9veL-jhtqLCtljXXOaDEdY0LiR6Tq_Pdyzn4rgLCyuT2dxzpigSd05UKGw3WmEfGs87Aku5Dl9icPA5Of76l0-MnWxtWSRiOa-NcaXvxgiFe7cACUb4WeKqtpqZ5MQgDjIdW8IqnptxNM8zNhWybCRx3WUODnbmXiLB9-ztI_ZqbgHWw8WsLIqE9cuBIbLtbpFRRpQsdY HTTP/2
Host: localhost:7138
Cookie: .AspNetCore.Correlation.E4ZQCWp6vRXooZMcA2BmZH9t7hLdLzbqgVWnRccsOEk=N; .AspNetCore.Correlation.c1US1hgIZBc99B06T8RQxmio47zoJAPmNFC5FRQaG2I=N; .AspNetCore.Correlation.53cqjvn8SyVq-sMJ-4Rc5Y5XtRLQolvLydTlWAZ5F6g=N; .AspNetCore.Correlation.yoWf2KcH7KttZje84kMs6gi1cTFLUUvozXWgWYWs4zE=N
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.111 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="111", "Not(A:Brand";v="8"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

So it looks like code and state are the only fields present... Will try the OpenIddict Slack Integration and compare the requests/see if that works.

martincostello commented 1 year ago

The user document won’t be in that response.

That response is used to obtain the tokens so that your app can call the user information endpoint in the Slack API to get the information about you - that’s the response where the information is missing.

Pdawg05 commented 1 year ago

Tried your OpenIddict implementation and it worked perfectly. Thank you!!

kevinchalet commented 1 year ago

Tried your OpenIddict implementation and it worked perfectly. Thank you!!

Nice. When you have a moment, please try to capture the userinfo request so we can determine whether there’s a bug in the Slack aspnet-contrib provider.