Search code examples
asp.net-coreoauth-2.0identityserver4openid-connect

Error code 500 "Unable to unprotect the message.State" when redirecting to Client in IdentityServer4


I'm using IdentityServer4 and trying to authenticate my Asp.Net Core 3.1 client manually (creating requests manually to understand the flow). Here is my client login:

[HttpGet]
public IActionResult ManualLogin()
{
    var myNonce = Guid.NewGuid().ToString();    
    var myState = Guid.NewGuid().ToString();

    var req = "https://localhost:5000/connect/authorize?" +
        "client_id=mvc" +
        "&redirect_uri=https://localhost:44381/signin-oidc" +
        "&response_type=code id_token" +
        "&scope=openid profile offline_access email" +
        "&response_mode=form_post" +
        $"&nonce={myNonce}" +
        $"&state={myState}";
        
    return Redirect(req);
}

this login method works fine and everything is ok but I don't want to use it:

//[HttpGet]
//public async Task LoginAsync()
//{
//     await HttpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties
//     {
//         RedirectUri = "https://localhost:44381/Home/external-login-callback"
//    });
// }

my client's startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // for using IHttpClientFactory
    services.AddHttpClient();

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    // adding Authentication services to DependencyInjection
    services.AddAuthentication(config =>
    {
        config.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;

        config.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)

        .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, config =>
        {
            config.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;

            config.Authority = "https://localhost:5000";

            config.ClientId = "mvc";

            config.ClientSecret = "secret";

            config.SaveTokens = true;

            config.UseTokenLifetime = false;

            // Hybrid Flow
            config.ResponseType = "code id_token";

            config.Scope.Add("openid");
            config.Scope.Add("offline_access");
            config.Scope.Add("profile");
            config.Scope.Add("email");

            config.GetClaimsFromUserInfoEndpoint = true;

            config.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = JwtClaimTypes.GivenName,
                RoleClaimType = JwtClaimTypes.Role,
            };

            // config.AuthenticationMethod = OpenIdConnectRedirectBehavior.FormPost;
        });

    services.AddControllersWithViews();
}

my Client's definition:

new Client
{
    ClientId = "mvc",
    ClientName ="My mvc client testing",
    ClientSecrets = { new Secret("secret".Sha256()) },

    AllowedGrantTypes = GrantTypes.Hybrid,

    // where to redirect to after login
    RedirectUris = { "https://localhost:44381/signin-oidc" },

    // where to redirect to after logout
    PostLogoutRedirectUris = { "https://localhost:44381/signout-callback-oidc" },

    AllowedScopes = new List<string>
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
    },

    AllowOfflineAccess = true,
    UpdateAccessTokenClaimsOnRefresh = true,
    AccessTokenType = AccessTokenType.Reference,
    RequireConsent = false,

    RequireClientSecret = true,
    //AlwaysIncludeUserClaimsInIdToken = true,
    RequirePkce = false,
}

My IS4's startup.cs:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();

        var builder = services.AddIdentityServer()
            .AddInMemoryIdentityResources(Config.IdentityResources)
            .AddInMemoryApiScopes(Config.ApiScopes)
            .AddInMemoryClients(Config.Clients)
            .AddTestUsers(TestUsers.Users);

        builder.AddDeveloperSigningCredential();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseStaticFiles();
        app.UseRouting();

        app.UseIdentityServer();
        app.UseAuthorization();

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

The client redirecting successfully to the IS4 login page, then I can authenticate the user, and when redirecting back to the signin-oidc url on my client, I got the 500 Internal Server Error:

Unable to unprotect the message.State.

Exception: Unable to unprotect the message.State.
Unknown location

Exception: An error was encountered while handling the remote login.
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler<TOptions>.HandleRequestAsync()

Exception: An error was encountered while handling the remote login.
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler<TOptions>.HandleRequestAsync()
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

I don't have multiple oidc, just one! What did I miss?


Update1: after @ToreNestenius comment:

I changed the redirect_uri of the request to:

&redirect_uri=https://localhost:44381/home/MyCallback"

and added the callback to the IS4 client's config, then here is my callback:

[HttpPost]
[ActionName("mycallback")]
public async Task mycallbackAsync(
    string code, 
    string scope, 
    string state, 
    string session_state,
    string login_required)
{
    var theRequest = $"https://localhost:5000/connect/token";

    var client = _httpClientFactory.CreateClient();
    
    var theContent = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string,string>("client_id","mvc"),
        new KeyValuePair<string,string>("client_secret","secret"),
        new KeyValuePair<string,string>("grant_type","hybrid"),
        new KeyValuePair<string,string>("code",code),
        new KeyValuePair<string,string>("redirect_uri", "https://localhost:5002/home/mycallback"),
    });

    theContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");

    var base64StringUserPass = Convert.ToBase64String(Encoding.ASCII.GetBytes($"mvc:secret"));
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64StringUserPass);

    var response = await client.PostAsync(req, theContent);
    if (!response.IsSuccessStatusCode)
    {
        Console.WriteLine(response.StatusCode);
        return;
    }
 
    var content = await response.Content.ReadAsStringAsync();
    var theAccessToken = JsonConvert.DeserializeObject<Token>(content);
    
    // -----------------------------
    // get user info
    //var access_token = theAccessToken.access_token;
    //string userInfo = await getUserInfoAsync(access_token);
}

Now I can handle the callback correctly and then issue an accessToken and getting the userInfo.


Solution

  • In your code you use

        "&redirect_uri=https://localhost:44381/signin-oidc" +
    

    That means that you are trying to redirect back to the OpenIDConnect authentication handler/scheme in your client. But it expects that the incoming request contains the state and nonce values that it does not recognize. Because the initial authentication request did not come from that handler.

    Because you want to learn OpenID-Connect and do it manually (like I did when I learned it). I would suggest that you change the redirectURi to be to a an action method in your own controller. like https://localhost:44381/test/callback"

    I suggest you avoid involving the OpenIDConnect handler until you understand the complete manual flow.

    I recently blogged about the state and nonce parameter here:

    The callback method signature should look something like this:

    /// <summary>
    /// This method is called with the authorization code and state parameter
    /// </summary>
    /// <param name="code">authorization code generated by the authorization server. This code is relatively short-lived, typically lasting between 1 to 10 minutes depending on the OAuth service.</param>
    /// <param name="state"></param>
    /// <returns></returns>
    
    [HttpPost]
    public IActionResult Callback(string code, string state)
    {
    
        //To be secure then the state parameter should be compared 
        to the state sent in the previous step
    
        var url = new Url(_openIdSettings.token_endpoint);
    
        //Get the tokens based on the code, using https://flurl.dev/docs/fluent-http/
        var token = url.PostUrlEncodedAsync(new
        {
            client_id = "authcodeflowclient",     
            client_secret = "mysecret",
            grant_type = "authorization_code",
            code_verifier = code_verifier,
            code = code,
            redirect_uri = "https://localhost:5001/CodeFlow/callback"
    
        }).ReceiveJson<Token>().Result;
    
        return View(token);
    }