IdentityServer / IdentityServer4

OpenID Connect and OAuth 2.0 Framework for ASP.NET Core
https://identityserver.io
Apache License 2.0
9.23k stars 4.02k forks source link

Client has invalid protocol type for token endpoint: null #809

Closed zngreg closed 7 years ago

zngreg commented 7 years ago

I have an issue where I'm trying to configure a client to use resource owner password and I'm using post (PostMan) to test the flow and I'm getting the error "Client "ClientId" has invalid protocol type for token endpoint: null" on caling /connect/token endpoint. I moved clients, Resources, users into database.

I have ICientStore as below :

public class ClientStore : IClientStore { private static SecurityContext _context;

    public ClientStore(SecurityContext context)
    {
        _context = context;
    }

    public Task<Client> FindClientByIdAsync(string id)
    {
        long clientId;

        var oauthClient = long.TryParse(id, out clientId) ? 
            _context.OAuthClients
            .Include(c => c.OAuthClientSecrets)
            .ThenInclude(c => c.OAuthSecret)
            .FirstOrDefault(c => c.Id == clientId && c.Status == RecordStatusEnum.Active) : 
            _context.OAuthClients
            .Include(c => c.OAuthClientSecrets)
            .ThenInclude(c => c.OAuthSecret)
            .FirstOrDefault(c => c.Name == id && c.Status == RecordStatusEnum.Active);

        if (oauthClient == null)
            return Task.FromResult(new Client());

        var client = new Client
        {
            AbsoluteRefreshTokenLifetime = oauthClient.AbsoluteRefreshTokenLifetime,
            AccessTokenLifetime = oauthClient.AccessTokenLifetime,
            AllowAccessTokensViaBrowser = oauthClient.AllowAccessTokensViaBrowser,
            AllowRememberConsent = oauthClient.AllowRememberConsent,
            ClientId = oauthClient.Id.ToString(),
            ClientName = oauthClient.Name,
            RequireConsent = oauthClient.RequireConsent,
            IdentityTokenLifetime = oauthClient.IdentityTokenLifetime,
            RefreshTokenExpiration = oauthClient.RefreshTokenExpiration,
            SlidingRefreshTokenLifetime = oauthClient.SlidingRefreshTokenLifetime,
            Enabled = oauthClient.Enabled,
            AuthorizationCodeLifetime = oauthClient.AuthorizationCodeLifetime,
            AccessTokenType = oauthClient.AccessTokenType,
            AllowPlainTextPkce = oauthClient.AllowPlainTextPkce,
            AlwaysSendClientClaims = oauthClient.AlwaysSendClientClaims,
            ClientUri = oauthClient.ClientUri,
            IncludeJwtId = oauthClient.IncludeJwtId,
            LogoUri = oauthClient.LogoUri,
            LogoutSessionRequired = oauthClient.LogoutSessionRequired,
            LogoutUri = oauthClient.LogoutUri,
            PrefixClientClaims = oauthClient.PrefixClientClaims,
            ProtocolType = oauthClient.ProtocolType,
            RefreshTokenUsage = oauthClient.RefreshTokenUsage, 
            RequireClientSecret = oauthClient.RequireClientSecret,
            RequirePkce = oauthClient.RequirePkce,
            UpdateAccessTokenClaimsOnRefresh = oauthClient.UpdateAccessTokenClaimsOnRefresh,
            EnableLocalLogin = oauthClient.EnableLocalLogin,
            AllowOfflineAccess =  oauthClient.AllowOfflineAccess
        };

        if (!string.IsNullOrEmpty(oauthClient.AllowedGrantTypes)) client.AllowedGrantTypes = JsonConvert.DeserializeObject<IEnumerable<string>>(oauthClient.AllowedGrantTypes);
        if (!string.IsNullOrEmpty(oauthClient.AllowedScopes)) client.AllowedScopes = JsonConvert.DeserializeObject<ICollection<string>>(oauthClient.AllowedScopes);
        if (!string.IsNullOrEmpty(oauthClient.AllowedCorsOrigins)) client.AllowedCorsOrigins = JsonConvert.DeserializeObject<ICollection<string>>(oauthClient.AllowedCorsOrigins);
        if (!string.IsNullOrEmpty(oauthClient.RedirectUris)) client.RedirectUris = JsonConvert.DeserializeObject<ICollection<string>>(oauthClient.RedirectUris);
        if (!string.IsNullOrEmpty(oauthClient.PostLogoutRedirectUris)) client.PostLogoutRedirectUris = JsonConvert.DeserializeObject<ICollection<string>>(oauthClient.PostLogoutRedirectUris);
        if (!string.IsNullOrEmpty(oauthClient.IdentityProviderRestrictions)) client.IdentityProviderRestrictions = JsonConvert.DeserializeObject<ICollection<string>>(oauthClient.IdentityProviderRestrictions);

        foreach (var secret in oauthClient.OAuthClientSecrets)
        {
            client.ClientSecrets.Add(new Secret
            {
                Description = secret.OAuthSecret.Description,
                Expiration = secret.OAuthSecret.Expiration,
                Type = secret.OAuthSecret.Type,
                Value = secret.OAuthSecret.Value
            });
        }

        return Task.FromResult(client);
    }
}

I implemented ResourceOwnerPasswordValidator as below :

using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using IdentityServer4.Validation; using Ilambu.Security.Entities; using Microsoft.AspNetCore.Identity;

namespace Ilambu.Security.IdentityServer { public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { readonly UserManager _userManager;

    public ResourceOwnerPasswordValidator(UserManager<SecurityUser> userManager)
    {
        _userManager = userManager;
    }

    public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
        var user = _userManager.FindByNameAsync(context.UserName).Result;

        var claims = user.Claims.Select(claim => new Claim(claim.ClaimType, claim.ClaimValue)).ToList();

        if (_userManager.CheckPasswordAsync(user, context.Password).Result)
            context.Result = new GrantValidationResult(user.Id.ToString(), "password", claims);

        return Task.FromResult(0);
    }
}

}

However, it's doesn't even come here.

Below is my startup.cs:

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();

        //_environment = env;
    }

    public IConfigurationRoot Configuration { get; }
    //private readonly IHostingEnvironment _environment;
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddIdentity<SecurityUser,SecurityRole>(options =>
        {
            options.Password.RequiredLength = 1;
            options.Password.RequireDigit = false;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequireUppercase = false;
            options.Password.RequireLowercase = false;
            options.Cookies.ApplicationCookie.LoginPath = "/account/login";
            options.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents()
            {
                OnRedirectToLogin = ctx =>
                {
                    if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == (int)HttpStatusCode.OK)
                        ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                    else
                        ctx.Response.Redirect(ctx.RedirectUri);

                    return Task.FromResult(0);
                }
            };
        })
          .AddEntityFrameworkStores<SecurityContext, long>()
          .AddDefaultTokenProviders();

        services.AddMvc();

        //Add Cors support to the service
        services.AddCors(o => o.AddPolicy("AllowAll", p =>
        {
            p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
        }));

        services.AddDbContext<SecurityContext>(
           o => o.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
               b => b.MigrationsAssembly("Ilambu.Security.Server")));

        services.AddScoped<IDbContext, SecurityContext>(provider => provider.GetService<SecurityContext>());
        services.AddTransient<LoginService>();

        //http://docs.identityserver.io/en/release/quickstarts/0_overview.html#basic-setup
        services.AddIdentityServer(options =>
            {
                options.Authentication.FederatedSignOutPaths.Add("/signout-callback-aad");
                options.Authentication.FederatedSignOutPaths.Add("/signout-callback-idsrv3");
            })
            .AddDbContextClients()
            .AddInMemoryApiResources(Resources.GetApiResources())
            .AddDbContextScopes()
            .AddTemporarySigningCredential()

            .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
            .AddExtensionGrantValidator<Extensions.ExtensionGrantValidator>()
            .AddSecretParser<ClientAssertionSecretParser>()
            .AddSecretValidator<PrivateKeyJwtSecretValidator>()
            .AddDbContextUsers();
    }

    // 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)
    {
        Func<LogEvent, bool> serilogFilter = (e) =>
        {
            var context = e.Properties["SourceContext"].ToString();

            return (context.StartsWith("\"IdentityServer") ||
                    context.StartsWith("\"IdentityModel") ||
                    e.Level == LogEventLevel.Error ||
                    e.Level == LogEventLevel.Fatal);
        };

        var serilog = new LoggerConfiguration()
            .MinimumLevel.Verbose()
            .Enrich.FromLogContext()
            .Filter.ByIncludingOnly(serilogFilter)
            //.WriteTo.File(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}")
            .WriteTo.File(@"identityserver4_log.txt")
            .CreateLogger();

        loggerFactory.AddSerilog(serilog);

        app.UseDeveloperExceptionPage();

        app.UseIdentityServer();

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
            AutomaticAuthenticate = false,
            AutomaticChallenge = false
        });

        app.UseGoogleAuthentication(new GoogleOptions
        {
            AuthenticationScheme = "Google",
            SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
            ClientId = "708996912208-9m4dkjb5hscn7cjrn5u0r4tbgkbj1fko.apps.googleusercontent.com",
            ClientSecret = "wdfPY6t8H8cecgjlxud__4Gh"
        });

        app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
        {
            AuthenticationScheme = "idsrv3",
            SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
            SignOutScheme = IdentityServerConstants.SignoutScheme,
            DisplayName = "IdentityServer3",
            Authority = "https://demo.identityserver.io/",
            ClientId = "implicit",
            ResponseType = "id_token",
            Scope = { "openid profile" },
            SaveTokens = true,
            CallbackPath = new PathString("/signin-idsrv3"),
            SignedOutCallbackPath = new PathString("/signout-callback-idsrv3"),
            RemoteSignOutPath = new PathString("/signout-idsrv3"),
            TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "name",
                RoleClaimType = "role"
            }
        });

        app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
        {
            AuthenticationScheme = "aad",
            SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
            SignOutScheme = IdentityServerConstants.SignoutScheme,
            DisplayName = "AAD",
            Authority = "https://login.windows.net/4ca9cb4c-5e5f-4be9-b700-c532992a3705",
            ClientId = "96e3c53e-01cb-4244-b658-a42164cb67a9",
            ResponseType = "id_token",
            Scope = { "openid profile" },
            CallbackPath = new PathString("/signin-aad"),
            SignedOutCallbackPath = new PathString("/signout-callback-aad"),
            RemoteSignOutPath = new PathString("/signout-aad"),
            TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "name",
                RoleClaimType = "role"
            }
        });

        app.UseStaticFiles();
        app.UseMvcWithDefaultRoute();
    }
}

Relevant parts of the log file

2017-02-10 17:09:10.694 +02:00 [Information] You are using the in-memory version of the persisted grant store. This will store consent decisions, authorization codes, refresh and reference tokens in memory only. If you are using any of those features in production, you want to switch to a different store implementation.
2017-02-10 17:09:10.780 +02:00 [Debug] Using built-in CookieAuthentication middleware with scheme: "idsrv"
2017-02-10 17:09:29.842 +02:00 [Debug] CORS request made for path: "/connect/token" from origin: "chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop"
2017-02-10 17:09:29.913 +02:00 [Debug] Client list checked and origin: "chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop" is not allowed
2017-02-10 17:09:29.914 +02:00 [Warning] CorsPolicyService did not allow origin: "chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop"
2017-02-10 17:09:30.077 +02:00 [Debug] Request path "/connect/token" matched to endpoint type Token
2017-02-10 17:09:30.080 +02:00 [Debug] Mapping found for endpoint: Token, creating handler: "IdentityServer4.Endpoints.TokenEndpoint"
2017-02-10 17:09:30.104 +02:00 [Information] Invoking IdentityServer endpoint: "IdentityServer4.Endpoints.TokenEndpoint" for "/connect/token"
2017-02-10 17:09:30.112 +02:00 [Verbose] Processing token request.
2017-02-10 17:09:30.116 +02:00 [Debug] Start token request.
2017-02-10 17:09:30.121 +02:00 [Debug] Start client validation
2017-02-10 17:09:30.125 +02:00 [Debug] Start parsing Basic Authentication secret
2017-02-10 17:09:30.128 +02:00 [Debug] Start parsing for secret in post body
2017-02-10 17:09:30.171 +02:00 [Debug] client id without secret found
2017-02-10 17:09:30.171 +02:00 [Debug] Parser found secret: "PostBodySecretParser"
2017-02-10 17:09:30.174 +02:00 [Debug] Start parsing for JWT client assertion in post body
2017-02-10 17:09:30.174 +02:00 [Debug] No JWT client assertion found in post body
2017-02-10 17:09:30.174 +02:00 [Debug] Secret id found: "2"
2017-02-10 17:09:38.229 +02:00 [Verbose] No endpoint entry found for request path: "/"
2017-02-10 17:10:36.472 +02:00 [Debug] Public Client - skipping secret validation success
2017-02-10 17:10:39.724 +02:00 [Debug] Client validation success
2017-02-10 17:10:41.862 +02:00 [Debug] Start token request validation
2017-02-10 17:10:43.414 +02:00 [Error] Client "2" has invalid protocol type for token endpoint: null
2017-02-10 17:10:43.528 +02:00 [Error] "{
  \"ClientId\": \"2\",
  \"ClientName\": \"TestClient\",
  \"Raw\": {
    \"grant_type\": \"password\",
    \"username\": \"zngreg@yahoo.com\",
    \"password\": \"***REDACTED***\",
    \"client_id\": \"2\"
  }
}"
2017-02-10 17:10:43.529 +02:00 [Verbose] Invoking result: "IdentityServer4.Endpoints.Results.TokenErrorResult"
2017-02-10 17:10:50.964 +02:00 [Verbose] No endpoint entry found for request path: "/lib/bootstrap/css/bootstrap.css"
2017-02-10 17:10:50.964 +02:00 [Verbose] No endpoint entry found for request path: "/css/site.css"
2017-02-10 17:10:50.992 +02:00 [Verbose] No endpoint entry found for request path: "/lib/jquery/jquery.js"
2017-02-10 17:10:51.515 +02:00 [Verbose] No endpoint entry found for request path: "/icon.png"
2017-02-10 17:10:51.515 +02:00 [Verbose] No endpoint entry found for request path: "/icon.jpg"
2017-02-10 17:10:51.546 +02:00 [Verbose] No endpoint entry found for request path: "/lib/bootstrap/js/bootstrap.js
zngreg commented 7 years ago

image

brockallen commented 7 years ago

I really can't tell (despite all the code/config you dumped), but this "invalid protocol type" makes me think that postman is sending json, but the token endpoint requires form-url-encoded. Perhaps that's your issue?

zngreg commented 7 years ago

I am using x-www-from-urlencoded image

leastprivilege commented 7 years ago

Well - ProtocolType is null. It needs to be set to ProtocolTypes.OpenIdConnect (which is the default value on the Client class). I guess a problem with the DB mapping.

zngreg commented 7 years ago

Thank you so much, this helped.

image

leastprivilege commented 7 years ago

Did this happen with our EF implementation - or your own persistence layer?

zngreg commented 7 years ago

It is my persistence layer, I implemented it since version 1.0.0-beta2. If there is a database/ EF implementation on client, scopes, resources, etc that is already available on IdentityServer4 I will be very interested in learning about it. Thank you

tibitoth commented 7 years ago

See this: https://github.com/IdentityServer/IdentityServer4.EntityFramework

lock[bot] commented 4 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.