0

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.

TheMah
  • 378
  • 5
  • 19

1 Answers1

2

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.

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);
}
Tore Nestenius
  • 16,431
  • 5
  • 30
  • 40
  • Yes, I'm using the latest version `Quickstart 2_InteractiveAspNetCore`. I set `RequirePkce=false` but doesn't fix the issue – TheMah Jul 08 '20 at 10:20
  • Are you running your IdentityServer in production or development mode? If in production, have you added AddSigningCredential ? – Tore Nestenius Jul 08 '20 at 10:58
  • I'm in development mode. I updated my question and added my IS4's startup.cs – TheMah Jul 08 '20 at 11:01
  • Thanks, I updated my question (Update 1) based on your answer, the callback reached but the user is not authenticated there – TheMah Jul 09 '20 at 06:37
  • 1
    It's a long way to actually get authenticated, when you do it manually like you do, your first job is to actually get the code and then the ID/access-tokens. when you got the ID-token then you can actually try to SignIn the user. But that's beyond this question. – Tore Nestenius Jul 09 '20 at 08:44
  • After checking your last edit (and helping from [link](https://rograce.github.io/openid-connect-documentation/explore_auth_code_flow.html)) I solved my issue, thank you – TheMah Jul 09 '20 at 23:47
  • Great! Glad you made it! – Tore Nestenius Jul 10 '20 at 08:57
  • I actually get this issue when my project is running. When I turn off it, I get to see the URL in the browser and the containing code is there. I even used Postman to get an access token. Regrettably, it doesn't seem that it works, that token. And the API I'm returning to isn't IS4, just plain web API. The endpoint isn't hit on breakpoint. Suggestions on how to trouble shoot? – Konrad Viltersten Jul 10 '21 at 19:50
  • I recommend you to post a new specific question, as its hard to make a good answer from the above. Do include logs and what error you get. – Tore Nestenius Jul 11 '21 at 09:56
  • Hello, If I change my redirect url value then I get a error like https://i.imgur.com/AT9yCcU.png – Constro Umbraco May 18 '22 at 10:03
  • A 503 error is a bit unexpected, that sounds like a non-openid-connect related problem. – Tore Nestenius May 18 '22 at 20:16
  • Yes, I solved this error but If I change my redirect URL and test then I get a new error like "Unable to unprotect the message.State." – Constro Umbraco May 19 '22 at 04:26
  • are you running multiple instances of the client or IdentityServer? – Tore Nestenius May 19 '22 at 05:08
  • No, I am running a single instance I have a scenario Like that. We have two applications which are AD B2C for SSO 1. App 1 - asp.net c# 2. App 2 - Sitecore (Based on .NET core) Please see this video for a better understanding. https://www.loom.com/share/50fd84380de04ea794a015e88f92e000 In this video, you can see I have 2 websites and once I come from website 1 to website 2 with relay state URL at that time when I redirect on relay state URL using SSO it asks me for login. I did not want this login I directly redirect to my relay state URL. – Constro Umbraco May 19 '22 at 05:45
  • The service that created the initial authentication, must also be the one that receives the redirect back from IdentityServer. Besides that I don't know the source of the problem. – Tore Nestenius May 19 '22 at 08:58