Search code examples
c#asp.net-coreoauth-2.0aspnet-contrib

Exception: The oauth state was missing or invalid. (ASP.NET Core external identifier OAuth)


I am trying to implement external OAuth in Asp.Net Core (https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers). My application (GitLab) has Callback URL https://myhost.com/signin-gitlab (default used by middleware).

If I run the code below, I get "Exception: The oauth state was missing or invalid." However, if I remove "options.UserInformationEndpoint" from Startup.cs, then I get redirected to myhost.com/signin-gitlab with the code and state parameters, which the middleware should exchange for an access token. My question is, why does my state parameter get corrupted (with UserInformationEndpoint)? Why am I not getting an access token? What am I missing here?

My Startup class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using AspNet.Security.OAuth.GitLab;
using System.Net.Http;
using System.Net.Http.Headers;

namespace MyApp
{
    public class Startup
    {
        private readonly IConfiguration _cfg;

        public Startup(IConfiguration configuration) => _cfg = configuration;

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRouting();

            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                    .AddCookie()
                    .AddGitLab("Gitlab", options => {
                        options.ClientId = "...";
                        options.ClientSecret = "...";

                        options.AuthorizationEndpoint = "https://mygitlabserver.com/oauth/authorize";
                        options.TokenEndpoint = "https://mygitlabserver.com/oauth/token";
                        options.Scope.Clear();
                        options.Scope.Add("api");
                        options.SaveTokens = true;

                        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                        options.UserInformationEndpoint = "https://mygitlabserver.com/api/v4/user"; 
                    });

            services.AddMvc();

        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();
               
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }
    }
}

My Controller:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet("/")]
        public IActionResult Index()
        {
            return View();
        }

        [HttpGet("/login")]
        public IActionResult LogIn()
        {
            // 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 = "https://myhost.com/signin-gitlab" }, "Gitlab");
        }   
    }
}

Solution

  • First of all, ASP.NET external identity provider/social login is either done with or without Identity Framework. Without Identity, it should be setup like so (https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/social-without-identity?view=aspnetcore-5.0):

    public void ConfigureServices(IServiceCollection services)
    {
        // requires
        // using Microsoft.AspNetCore.Authentication.Cookies;
        // using Microsoft.AspNetCore.Authentication.Google;
        // NuGet package Microsoft.AspNetCore.Authentication.Google
        services
            .AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
            })
            .AddCookie()
            .AddGoogle(options =>
            {
                options.ClientId = Configuration["Authentication:Google:ClientId"];
                options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
            });
    
        services.AddRazorPages();
    }
    

    The second issue was inside the controller:

    return Challenge(new AuthenticationProperties { RedirectUri = "https://myhost.com/signin-gitlab" }, "Gitlab");
    

    Just as in the MVC Sample app by the aspnet-contrib team (https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/blob/dev/samples/Mvc.Client/Controllers/AuthenticationController.cs) it should actually be:

    return Challenge(new AuthenticationProperties { RedirectUri = "/" }, "Gitlab");
    

    Users were actually being authenticated, the problem was that they were then redirected to the OAuth middleware's internal route /signin-gitlab without state or code parameters instead of the home route/index action, hence the error.

    In other words, I mixed up:

    • RedirectUri (where user is redirected after authentication)
    • Callback URL (where the OAuth application redirects the user with state and code to the OAuth middleware internal route, which by default is /signin-gitlab, /signin-google, /signin-facebook but can also be overridden with the options.CallbackPath).

    Perhaps my confusion was caused by the fact that the callback url is called REDIRECT_URI in the GitLab docs (https://docs.gitlab.com/ee/api/oauth2.html).

    Thanks to Nan Yu (https://stackoverflow.com/a/61452853/2768479) and Tratcher (https://github.com/dotnet/aspnetcore/issues/22125, https://github.com/aspnet/Security/issues/1756) for their illuminating posts.