aspnetcore: OAuth: The oauth state was invalid or missing

Describe the bug

I’m trying to set up OAuth with AdobeIO. I haven’t found any .NET libs around for it so I am trying to roll it myself.

It redirects me to Adobe just fine, but there’s something wrong with the callback because it always tells me the oauth state is invalid or missing.

Upon further investigation using a quick ISecureDataFormat I saw that the callback initially does have the oauth state which gets unprotected successfully, but Unprotect gets called again immediately afterwards with no protected text.

To Reproduce

public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services
                .AddAuthentication()
                .AddOAuth("adobe", options =>
                {
                    var handler = new HttpClientHandler();
                    handler.ServerCertificateCustomValidationCallback = (HttpRequestMessage req, X509Certificate2 cert, X509Chain chain, SslPolicyErrors errors) => {
                        Console.WriteLine("HELLO");
                        return true;
                    };
                    var httpClient = new HttpClient(handler);
                    options.ClientId = Configuration["AdobeAuth:ClientId"];
                    options.ClientSecret = Configuration["AdobeAuth:ClientSecret"];
                    options.CallbackPath = new Microsoft.AspNetCore.Http.PathString(Configuration["AdobeAuth:CallbackPath"]);
                    options.AuthorizationEndpoint = Configuration["AdobeAuth:AuthorizationEndpoint"];
                    options.TokenEndpoint = Configuration["AdobeAuth:AuthorizationEndpoint"];
                    options.Backchannel = httpClient;
                    options.SaveTokens = false;
                    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;

                    options.Scope.Clear();

                    var scopes = Configuration["AdobeAuth:Scopes"].Split(',');
                    foreach(var scope in scopes)
                    {
                        options.Scope.Add(scope);
                    }
                    options.Events.OnAccessDenied += (context) => {
                        Console.WriteLine("access denied");
                        return Task.CompletedTask;
                    };
                    options.Events.OnTicketReceived += (context) => {
                        Console.WriteLine("ticket received");
                        return Task.CompletedTask;
                    };
                    options.Events.OnCreatingTicket += (context) => {
                        Console.WriteLine("creating ticket");
                        return Task.CompletedTask;
                    };
                    options.Events.OnRedirectToAuthorizationEndpoint += (RedirectContext<OAuthOptions> context) => {
                        Console.WriteLine("redirecting to auth endpoint");
                        return Task.CompletedTask;
                    };
                    options.Events.OnRemoteFailure += (context) => {
                        Console.WriteLine("remote failure");
                        return Task.CompletedTask;
                    };
                    options.StateDataFormat = new TestSecureDataFormat();
                });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

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

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

        public class TestSecureDataFormat : ISecureDataFormat<AuthenticationProperties>
        {
            private readonly IDataProtector protector;
            public TestSecureDataFormat()
            {
                protector = DataProtectionProvider.Create("server").CreateProtector("authentication");
            }
            public string Protect(AuthenticationProperties data)
            {
                var json = Newtonsoft.Json.JsonConvert.SerializeObject(data);
                var p = protector.Protect(json);
                return p;
            }

            public string Protect(AuthenticationProperties data, string purpose)
            {
                return Protect(data);
            }

            [return: MaybeNull]
            public AuthenticationProperties Unprotect(string protectedText)
            {
                var u = protector.Unprotect(protectedText);
                var props = Newtonsoft.Json.JsonConvert.DeserializeObject<AuthenticationProperties>(u);
                return props;
            }

            [return: MaybeNull]
            public AuthenticationProperties Unprotect(string protectedText, string purpose)
            {
                return Unprotect(protectedText);
            }
        }
[Route("{controller}")]
    public class AuthController : Controller
    {
        private readonly IConfiguration configuration;
        public AuthController(IConfiguration configuration)
        {
            this.configuration = configuration;
        }

        [HttpGet("login")]
        public IActionResult Login()
        {
            var properties = new OAuthChallengeProperties()
            {
                RedirectUri = new PathString(this.configuration["AdobeAuth:CallbackPath"]),
            };

            return Challenge(properties, "adobe");
        }
    }
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <UserSecretsId>32de766e-0589-4a13-8b88-f04d4faa8c58</UserSecretsId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.1"/>
  </ItemGroup>
</Project>

Please ignore the Console.WriteLines, I have been pulling my hair out trying to figure this out for a while 😪

I also have no idea why I need to override the server cert verification. Either my machine doesn’t trust adobe’s cert, or they don’t trust mine? I’m not sure.

Further technical details

  • ASP.NET Core version: 5
  • Include the output of dotnet --info:
dotnet --info
.NET SDK (reflecting any global.json):
 Version:   5.0.202     
 Commit:    db7cc87d51  

Runtime Environment:    
 OS Name:     Windows   
 OS Version:  10.0.19042
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\5.0.202\

Host (useful for support):
  Version: 5.0.5
  Commit:  2f740adc14

.NET SDKs installed:
  2.1.523 [C:\Program Files\dotnet\sdk]
  3.1.201 [C:\Program Files\dotnet\sdk]
  3.1.408 [C:\Program Files\dotnet\sdk]
  5.0.103 [C:\Program Files\dotnet\sdk]
  5.0.202 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.All 2.1.25 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.27 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.25 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.27 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.14 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.25 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.27 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.1.3 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.8 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.12 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.14 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.3 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.5 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  • The IDE (VS / VS Code/ VS4Mac) you’re running on, and its version
Version: 1.55.2 (system setup)
Commit: 3c4e3df9e89829dce27b7b5c24508306b151f30d
Date: 2021-04-13T09:35:57.887Z
Electron: 11.3.0
Chrome: 87.0.4280.141
Node.js: 12.18.3
V8: 8.7.220.31-electron.0
OS: Windows_NT x64 10.0.19042

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 18 (9 by maintainers)

Most upvoted comments

Turns out the redirect was happening because the TokenEndpoint is https://ims-na1.adobelogin.com/ims/token not https://ims-na1.adobelogin.com/ims/authorize/v2 🙃

I was able to run this locally and found the issue. The app receives the callback: GET https://localhost:5001/signin-adobe?code=...&state=… It sends a request on the backchannel HttpClient to redeme the code for a token. This fails and returns a 302 redirect to Location: https://localhost:5001/signin-adobe?error=invalid_scope. The HttpClient automatically follows the redirect, making a new request back to your own server and signin path, but without the state.

I’m not sure why they thought a redirect was an appropriate way to report errors from the token endpoint. It does explain why you needed to add ServerCertificateCustomValidationCallback in startup, HttpClient didn’t trust your local development certificate for the redirected request (that part works for me, I trusted the dev cert).

I was able to supress the redirect like this:

                    var handler = new HttpClientHandler();
                    handler.AllowAutoRedirect = false;

To see the real error message:

System.Exception: An error was encountered while handling the remote login.
 ---> System.Exception: OAuth token endpoint failure: Status: Redirect;Headers: Date: Thu, 29 Apr 2021 18:25:08 GMT
Connection: keep-alive
Cache-Control: no-store
Set-Cookie: relay=aaff2386-255d-4686-bb31-9b6167a1130b; Path=/; Secure; SameSite=None, ftrset=682; Path=/; Secure; HttpOnly; SameSite=None, ftrset=682; Path=/; Secure; HttpOnly; SameSite=None
P3P: CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"
Server: ASIT
Location: https://localhost:5001/signin-adobe?error=invalid_scope
Vary: Accept-Encoding
X-DEBUG-ID: aaff2386-255d-4686-bb31-9b6167a1130b
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Via: e-uw2,e-ue1
;Body: ;
   --- End of inner exception stack trace ---
   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

I’ll let you sort out what’s wrong with the scope.

What CallbackPath did you use? It should be a unique value like “/signin-adobe”, it must not overlap with a controller route. OAuthChallengeProperties.RedirectUri should provide a different value than the CallbackPath for the middleware to redirect to after the CallbackPath.

When you get it working, consider contributing to https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers