IdentityServer / IdentityServer3

OpenID Connect Provider and OAuth 2.0 Authorization Server Framework for ASP.NET 4.x/Katana
https://identityserver.github.io/Documentation/
Apache License 2.0
2.01k stars 764 forks source link

ASP.NET Core applications fails with Invalid authorization code #3146

Closed apoutney closed 8 years ago

apoutney commented 8 years ago

I have an IdentityServer3 implementation running in .NET Framework 4.6.1 MVC application. This was being used to authenticate an ASP.NET Core RC1 MVC application and was working perfectly. We have recently upgraded our application to ASP.NET Core 1.0 and is now targeting the netcoreapp1.0 framework. Since then authorization has failed to validate the authorization code and giving an invalid_grant error. See the log information below.

It appears the the first token request is validating the authorization code successfully but then for some reason a second token request is initiated that parses a post body secret and fails to validate the authorization code.

Can anybody point me in the right direction to solve this issue. I have managed to replicate this in a brand new ASP.NET Core 1.0 MVC application that targets net461. The code for for which is below. Any help will be great-fully received.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using System.Security.Claims;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Net;
using System.IdentityModel.Tokens.Jwt;

namespace WebApplication1
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            //services.AddMvc();
            services.AddMvc(x =>
            {
                x.Filters.Add(new AuthorizeFilter(
                    new AuthorizationPolicy(
                        requirements: new List<RolesAuthorizationRequirement>()
                        {
                            new RolesAuthorizationRequirement(
                                new List<string>() { "SuperUser" })
                        },
                        authenticationSchemes: new List<string>() { "Cookies", "Oidc" })));
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>();

            app.UseCookieAuthentication(new CookieAuthenticationOptions()
            {
                AuthenticationScheme = "Cookies",
                AutomaticAuthenticate = true,
                ExpireTimeSpan = TimeSpan.FromMinutes(30),
                SlidingExpiration = true,
            });

            app.UseOpenIdConnectAuthentication(GetOptions());

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

        private OpenIdConnectOptions GetOptions()
        {
            var options = new OpenIdConnectOptions()
            {
                //Audience = vigoIdServerOptions.RedirectUri;
                AuthenticationScheme = "Oidc",
                Authority = "https://localhost:44300/identity",
                AutomaticChallenge = true,
                CallbackPath = new PathString("/signin-oidc/"),
                ClientId = "testClient",
                ClientSecret = "testClient",
                PostLogoutRedirectUri = "https://localhost:44335/",
                RequireHttpsMetadata = false,
                ResponseType = "code id_token token",
                SignInScheme = "Cookies",
                TokenValidationParameters = new TokenValidationParameters()
                {
                    NameClaimType = "preferred_username",
                    RoleClaimType = "role",
                },
                UseTokenLifetime = true
            };

            options.Scope.Add("openid");
            options.Scope.Add("all_claims");
            options.Scope.Add("offline_access");

            options.Events = new OpenIdConnectEvents() {
                OnAuthorizationCodeReceived = async n => await AuthorizationCodeReceived(n),
                OnRedirectToIdentityProvider = n => RedirectToIdentityProvider(n),
                OnRedirectToIdentityProviderForSignOut = n => RedirectToIdentityProviderForSignOut(n),
            };
            return options;
        }
        private static async Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext arg)
        {
            var claims = new List<Claim>();

            var tokenClient = new TokenClient("https://localhost:44300/identity/connect/token", "testClient", "testClient");
            var refreshResponse = await tokenClient.RequestAuthorizationCodeAsync(arg.ProtocolMessage.Code, "https://localhost:44335" + "/signin-oidc/");
            var expiresAt = DateTime.SpecifyKind(
                DateTime.UtcNow.AddSeconds(refreshResponse.ExpiresIn),
                DateTimeKind.Utc).ToString("o"); 

            claims.Add(new Claim("id_token", arg.ProtocolMessage.IdToken));
            claims.Add(new Claim("access_token", arg.ProtocolMessage.AccessToken));
            claims.Add(new Claim("refresh_token", refreshResponse.RefreshToken));
            claims.Add(new Claim("expires_at", expiresAt));

            var userInfoClient = new UserInfoClient(
                new Uri("https://localhost:44300/identity/connect/userinfo"),
                arg.ProtocolMessage.AccessToken);

            var userInfo = await userInfoClient.GetAsync();
            foreach (var userInfoClaim in userInfo.Claims)
                claims.Add(new Claim(userInfoClaim.Item1, userInfoClaim.Item2));

            var claimsPrincipal = new ClaimsPrincipal(
                new ClaimsIdentity(claims, arg.Ticket.AuthenticationScheme,
                "preferred_username", "role"));

            arg.Ticket = new AuthenticationTicket(claimsPrincipal,
                arg.Ticket.Properties,
                arg.Ticket.AuthenticationScheme);
        }

        private static Task RedirectToIdentityProvider(RedirectContext n)
        {
            //See https://github.com/aspnet/Security/issues/336#issuecomment-119304342 for details. Last accessed 12/04/2016.
            //This is the fix for redirect loops when the logged in user does not have the correct role for an action.
            if (n.HttpContext.User.Identity.IsAuthenticated
            && n.ProtocolMessage.RequestType != OpenIdConnectRequestType.Logout)
            {
                n.HandleResponse();
                n.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; //Currently returning 403
                                                                                   //n.HttpContext.Response.Redirect(Uri.EscapeUriString("/Home/Error")); //Could redirect somewhere else instead
            }
            return Task.FromResult(0);
        }
        private static Task RedirectToIdentityProviderForSignOut(RedirectContext n)
        {
            if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
            {
                var idTokenHint = n.Request.HttpContext.User.FindFirst("id_token");
                if (idTokenHint != null)
                    n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
            }
            return Task.FromResult(0);
        }
    }
}

Log File

isexpress.exe Information: 0 : 2016-08-18 12:38:20.591 +01:00 [Information] Start token request
2016-08-18 12:38:20.601 +01:00 [Debug] Start client validation
2016-08-18 12:38:20.601 +01:00 [Debug] Start parsing Basic Authentication secret
2016-08-18 12:38:20.601 +01:00 [Debug] Parser found secret: "BasicAuthenticationSecretParser"
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.601 +01:00 [Information] Secret id found: "testClient"
2016-08-18 12:38:20.608 +01:00 [Debug] Secret validator success: "HashedSharedSecretValidator"
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.608 +01:00 [Information] Client validation success
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.608 +01:00 [Information] Start token request validation
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.608 +01:00 [Information] Start validation of authorization code token request
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.673 +01:00 [Information] Validation of authorization code token request success
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.673 +01:00 [Information] Token request validation success
 {
  "ClientId": "testClient",
  "ClientName": "Test Client",
  "GrantType": "authorization_code",
  "AuthorizationCode": "300f4da601c6061c38a11a5b645a92fa",
  "Raw": {
    "grant_type": "authorization_code",
    "code": "300f4da601c6061c38a11a5b645a92fa",
    "redirect_uri": "https://localhost:44335/signin-oidc/"
  }
}
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.673 +01:00 [Information] Creating token response
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.673 +01:00 [Information] Processing authorization code request
2016-08-18 12:38:20.673 +01:00 [Debug] Creating access token
2016-08-18 12:38:20.673 +01:00 [Debug] Creating refresh token
2016-08-18 12:38:20.673 +01:00 [Debug] Setting an absolute lifetime: 300
2016-08-18 12:38:20.676 +01:00 [Debug] Creating JWT access token
2016-08-18 12:38:20.689 +01:00 [Debug] Creating identity token
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.689 +01:00 [Information] Getting claims for identity token for subject: 1d1d7eab-c363-e611-828a-acd1b8c0190c
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.689 +01:00 [Information] All claims rule found - emitting all claims for user.
2016-08-18 12:38:20.696 +01:00 [Debug] Creating JWT identity token
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.708 +01:00 [Information] End token request
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.708 +01:00 [Information] Returning token response.
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.719 +01:00 [Information] Start userinfo request
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.720 +01:00 [Information] Token found: AuthorizationHeader
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.720 +01:00 [Information] Start access token validation
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.733 +01:00 [Information] "Token validation success"
"{
  \"ValidateLifetime\": true,
  \"AccessTokenType\": \"Jwt\",
  \"ExpectedScope\": \"openid\",
  \"Claims\": {
    \"iss\": \"https://localhost:44300/identity\",
    \"aud\": \"https://localhost:44300/identity/resources\",
    \"exp\": \"1471523898\",
    \"nbf\": \"1471520298\",
    \"client_id\": \"testClient\",
    \"scope\": [
      \"openid\",
      \"profile\",
      \"all_claims\",
      \"offline_access\"
    ],
    \"sub\": \"1d1d7eab-c363-e611-828a-acd1b8c0190c\",
    \"auth_time\": \"1471520298\",
    \"idp\": \"idsrv\",
    \"amr\": \"password\"
  }
}"
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.733 +01:00 [Information] Creating userinfo response
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.734 +01:00 [Information] Scopes in access token: "openid profile all_claims offline_access"
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.745 +01:00 [Information] Requested claim types: all
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.755 +01:00 [Information] Profile service returned to the following claim types: "sub preferred_username email email_verified role role role role given_name family_name is_account_holder is_active date_user_created security_stamp"
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.755 +01:00 [Information] End userinfo request
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.755 +01:00 [Information] Returning userinfo response.
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.767 +01:00 [Information] Start token request
2016-08-18 12:38:20.768 +01:00 [Debug] Start client validation
2016-08-18 12:38:20.769 +01:00 [Debug] Start parsing Basic Authentication secret
2016-08-18 12:38:20.769 +01:00 [Debug] Start parsing for secret in post body
2016-08-18 12:38:20.769 +01:00 [Debug] Parser found secret: "PostBodySecretParser"
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.769 +01:00 [Information] Secret id found: "testClient"
2016-08-18 12:38:20.773 +01:00 [Debug] Secret validator success: "HashedSharedSecretValidator"
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.773 +01:00 [Information] Client validation success
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.773 +01:00 [Information] Start token request validation
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.773 +01:00 [Information] Start validation of authorization code token request
iisexpress.exe Error: 0 : 2016-08-18 12:38:20.774 +01:00 [Error] Invalid authorization code: 300f4da601c6061c38a11a5b645a92fa
 {
  "ClientId": "testClient",
  "ClientName": "Test Client",
  "GrantType": "authorization_code",
  "AuthorizationCode": "300f4da601c6061c38a11a5b645a92fa",
  "Raw": {
    "client_id": "testClient",
    "client_secret": "******",
    "code": "300f4da601c6061c38a11a5b645a92fa",
    "grant_type": "authorization_code",
    "redirect_uri": "https://localhost:44335/signin-oidc/"
  }
}
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.774 +01:00 [Information] End token request
iisexpress.exe Information: 0 : 2016-08-18 12:38:20.774 +01:00 [Information] Returning error: invalid_grant
brockallen commented 8 years ago

So IdSvr is now running in ASP.NET Core? Then it's probably this: https://github.com/IdentityServer/IdentityServer3/issues/3059. Try the nightly build from myget and see if it's fixed.

apoutney commented 8 years ago

No, the IdSvr is still running in .NET Framework 4.61 its my application that is trying to authenticate against it that has been upgraded from ASP.NET Core RC1 to the full version 1.0

brockallen commented 8 years ago

So if the only thing that has changed is that the client is ported from MVC 5 to ASP.NET Core MVC then that makes me wonder if it's a problem in the client and not in IdSvr... But the logs do look odd because the same code that's issued seems to be the same code that's sent to the token endpoint.

apoutney commented 8 years ago

Yes, I don't think the issue is in IdSvr itself, the client authenticates fine, its when i try to authorize a user that I get this issue. I've replicated this in a clean ASP.NET Core MVC application, the only thing I've added to the template is the OpenIdConnect authentication. So i believe the problem is here somewhere, I've just exhausted my knowledge trying to figure out whats causing it.

Ive gone back over some old log files and i cant see anywhere where the second token request gets started that parses the post body for the secret

apoutney commented 8 years ago

Ok a bit more information. As you'll see from the code I posted, I'm handlingOnAuthorizationCodeReceived of the OpenIdConnectOptions Events to add in all all the additional claims required as per Dominicks blog post here https://leastprivilege.com/2014/10/10/openid-connect-hybrid-flow-and-identityserver-v3/

This sends the first token request seen in the log to get the access token and refresh token to add to the claims

After the event has been handled something in OpendIdConnect sends a second token request, and its this that fails. I'm not sure but i dont recall it ever doing this in earlier versions of the .NET framework

If I don't handle this event then only the the token request from OpenIdConnect gets sent which gets validated and authorizes the user successfully but now I don't have all the claims information.

Can anyone point me to an implementation of this event handler that is ASP.NET Core compatible?

brockallen commented 8 years ago

In the new OIDC MW in Core, it does all of the calls to the token endpoint for you. So if you're doing it manually, you can remove that code.

apoutney commented 8 years ago

Ah ok, but does this also add the id_token, access_token and refresh_token to the claims because if i don't hand the event then I don't see them. Or do i not actually need to do this?

I did find out that adding the following line to the handler also works arg.HandleResponse();

I'm assuming that the oidc middleware doesn't know the event is being handled and adding this line lets it know so that it then doesn't do its own calls to the token endpoint. Although I'm not sure if this is the best thing to do as i don't know what else this is overriding in oidc

leastprivilege commented 8 years ago

yes. When you set SaveTokens=true

see also here: https://github.com/IdentityServer/IdentityServer4.Samples/blob/dev/MVC%20and%20API/src/AspNetCoreAuthentication/Controllers/HomeController.cs#L30