Search code examples
oauth-2.0asp.net-core-webapiidentityserver4openid-connect

IdentityServer4 Invalid Scope in Client application


I am currently implementing IdentityServer4 with my microservices to authenticate users. My client application is a MVC project. While I run the project client application return an error as following,

Sorry, there was an error : invalid_scope Invalid scope Request Id: 8000008d-0000-f700-b63f-84710c7967bb

When I removed apiscopes from config file application works fine but then I didn't get any user claims in my authorization handler.

Here's my code

Config.cs

public static class Config
{
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile()
        };
    }

    public static IEnumerable<ApiResource> GetApis()
    {
        return new List<ApiResource>
        {
            new ApiResource("masterweb_api", "Master API")
                
        };
    }
    
    public static IEnumerable<Client> GetClients()
    {
        return new List<Client>
        {
            new Client
            {
                ClientId = "mvc",
                ClientSecrets =
                {
                    new Secret("Secret".Sha256())
                },
                ClientName = "MVC Client",
                AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
                RequireConsent = false,
                RequirePkce = false,
                // where to redirect to after login
            RedirectUris = { "https://localhost:44367/signin-oidc" },

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

                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    "masterweb_api"
                },
                AllowOfflineAccess = true,
                AlwaysSendClientClaims = true,
                AlwaysIncludeUserClaimsInIdToken = true
            }
            
        };
    }
}

AuthServer Startup.cs

public class Startup
{
    public IWebHostEnvironment Environment { get; }
    public IConfiguration Configuration { get; }
    public Startup(IWebHostEnvironment environment, IConfiguration configuration)
    {
        Environment = environment;
        Configuration = configuration;
    }
    // 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 https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();

        /****Register asp.net core Identity DBConetexts***/

        var idenConnectionString = Configuration["DbContextSettings:IdentityConnectionString"];
        var dbPassword = Configuration["DbContextSettings:DbPassword"];
        var builder = new NpgsqlConnectionStringBuilder(idenConnectionString)
        {
            Password = dbPassword
        };

        services.AddDbContext<MembershipDBContext>(opts => opts.UseNpgsql(builder.ConnectionString));
        

        services.AddIdentity<MembershipUser, MembershipRole>(options =>
        {
            options.Password.RequiredLength = 8;
            options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+ ";
            options.SignIn.RequireConfirmedEmail = false;
        }).AddRoles<MembershipRole>().AddEntityFrameworkStores<MembershipDBContext>()
        .AddDefaultTokenProviders();

        /****Identity Server implementation with asp.net core Identity***/

        var idsServerConnectionString = Configuration["DbContextSettings:IdentityServer4ConnectionString"];
        var migrationAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
        //var userConnectionString = Configuration["DbContextSettings:UserConnectionString"];
        var idsServerdbPassword = Configuration["DbContextSettings:DbPassword"];
        var idsServerbuilder = new NpgsqlConnectionStringBuilder(idsServerConnectionString)
        {
            Password = dbPassword
        };
        var idBuilder = services.AddIdentityServer(options =>
          {
              options.Events.RaiseErrorEvents = true;
              options.Events.RaiseInformationEvents = true;
              options.Events.RaiseFailureEvents = true;
              options.Events.RaiseSuccessEvents = true;
              options.UserInteraction.LoginUrl = "/Account/Login";
              options.UserInteraction.LogoutUrl = "/Account/Logout";
              options.Authentication = new AuthenticationOptions()
              {
                  CookieLifetime = TimeSpan.FromHours(10), // ID server cookie timeout set to 10 hours
                  CookieSlidingExpiration = true
              };
          }).AddDeveloperSigningCredential()
        .AddConfigurationStore(options =>
        {
            options.ConfigureDbContext = b => b.UseNpgsql(idsServerbuilder.ConnectionString, sql => sql.MigrationsAssembly(migrationAssembly));
        })
        .AddOperationalStore(options =>
        {
            options.ConfigureDbContext = b => b.UseNpgsql(idsServerbuilder.ConnectionString, sql => sql.MigrationsAssembly(migrationAssembly));
            options.EnableTokenCleanup = true;
        }).AddAspNetIdentity<MembershipUser>()
        .AddProfileService<ProfileService>();

        idBuilder.Services.ConfigureExternalCookie(options =>
        {
            options.Cookie.IsEssential = true;
            options.Cookie.SameSite = (SameSiteMode)(-1); //SameSiteMode.Unspecified in .NET Core 3.1
        });

        idBuilder.Services.ConfigureApplicationCookie(options =>
        {
            options.Cookie.IsEssential = true;
            options.Cookie.SameSite = (SameSiteMode)(-1); //SameSiteMode.Unspecified in .NET Core 3.1
        });
    }
    private void InitializeDatabase(IApplicationBuilder app)
    {
        using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
        {
            serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();

            var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
            context.Database.Migrate();
            if (!context.Clients.Any())
            {
                foreach (var client in Config.GetClients())
                {
                    context.Clients.Add(client.ToEntity());
                }
                context.SaveChanges();
            }

            

            if (!context.IdentityResources.Any())
            {
                foreach (var resource in Config.GetIdentityResources())
                {
                    context.IdentityResources.Add(resource.ToEntity());
                }
                context.SaveChanges();
            }

            if (!context.ApiResources.Any())
            {
                foreach (var resource in Config.GetApis())
                {
                    context.ApiResources.Add(resource.ToEntity());
                }
                context.SaveChanges();
            }
        }
    }
    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
    {
       
        // this will do the initial DB population
        InitializeDatabase(app);

        if (Environment.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // uncomment if you want to add MVC
        app.UseStaticFiles();
        app.UseRouting();

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

Api Startup.cs

public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        var key = AesOperation.AesKey;
        //var str = Configuration["PostgreSql:ConnectionString"]; 
        //var encryptedString = AesOperation.EncryptString(key, str);            

        var encryptedString = Configuration["PostgreSql:ConnectionString"];
        var decryptedString = AesOperation.DecryptString(key, encryptedString);
        var connectionString = decryptedString;

        encryptedString = Configuration["PostgreSql:DbPassword"];
        decryptedString = AesOperation.DecryptString(key, encryptedString);
        var dbPassword = decryptedString;

        var builder = new NpgsqlConnectionStringBuilder(connectionString)
        {
            Password = dbPassword
        };

        services.AddDbContext<MasterDbContext>(options => options.UseNpgsql(builder.ConnectionString));

        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo { Title = "Master Microservice", Version = "v1" });
        });

        services.AddMediatR(typeof(Startup));

        RegisterServices(services);

        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

        services.AddMvc(options =>
        {
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
            options.Filters.Add(new AuthorizeFilter(policy));
        }).SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

        services.AddAuthorization(options =>
        {
            options.AddPolicy("MasterCompanyCreatePolicy", policy => policy.RequireAssertion(context => AuthorizeAccess(context, "RoleMasterCompanyCreate", "UserMasterCompanyCreate")));
            options.AddPolicy("MasterCompanyReadPolicy", policy => policy.RequireAssertion(context => AuthorizeAccess(context, "RoleMasterCompanyRead", "UserMasterCompanyRead")));
            options.AddPolicy("MasterCompanyUpdatePolicy", policy => policy.RequireAssertion(context => AuthorizeAccess(context, "RoleMasterCompanyUpdate", "UserMasterCompanyUpdate")));
            options.AddPolicy("MasterCompanyDeletePolicy", policy => policy.RequireAssertion(context => AuthorizeAccess(context, "RoleMasterCompanyDelete", "UserMasterCompanyDelete")));
        });
        
        services.AddAuthentication("Bearer")
            .AddJwtBearer("Bearer", options =>
             {
                 options.Authority = "https://localhost:44396";
                 options.RequireHttpsMetadata = false;
                 options.Audience = "masterweb_api";
                 options.SaveToken = true;                     
             });
    }
    private bool AuthorizeAccess(AuthorizationHandlerContext context, string roleClaim, string userClaim)
    {
        return context.User.HasClaim(claim => claim.Type == roleClaim) &&
               context.User.HasClaim(claim => claim.Type == userClaim) ||
               context.User.IsInRole("SuperAdmin");
    }
    private void RegisterServices(IServiceCollection services)
    {
        DependencyContainer.RegisterServices(services);
    }

    // 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.UseSwagger();
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "Master Microservice V1");
        });

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

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

Mvc Startup.cs

public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options =>
        {
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
            options.Filters.Add(new AuthorizeFilter(policy));
        }).SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

        services.AddAuthorization(options =>
        {
            options.AddPolicy("MasterCompanyCreatePolicy", policy => policy.RequireAssertion(context => AuthorizeAccess(context, "RoleMasterCompanyCreate", "UserMasterCompanyCreate")));
            options.AddPolicy("MasterCompanyReadPolicy", policy => policy.RequireAssertion(context => AuthorizeAccess(context, "RoleMasterCompanyRead", "UserMasterCompanyRead")));
            options.AddPolicy("MasterCompanyUpdatePolicy", policy => policy.RequireAssertion(context => AuthorizeAccess(context, "RoleMasterCompanyUpdate", "UserMasterCompanyUpdate")));
            options.AddPolicy("MasterCompanyDeletePolicy", policy => policy.RequireAssertion(context => AuthorizeAccess(context, "RoleMasterCompanyDelete", "UserMasterCompanyDelete")));
            //options.AddPolicy("SaleCancelPolicy", policy => policy.RequireAssertion(context => AuthorizeAccess(context, "RoleSaleCancel", "UserSaleCancel")));


        });
        services.AddControllersWithViews();
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "cookie";
            options.DefaultChallengeScheme = "oidc";
        }).AddCookie("cookie")
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "cookie";
                options.Authority = "https://localhost:44396";                   
                options.RequireHttpsMetadata = false;
                options.ClientId = "mvc";
                options.ClientSecret = "Secret";
                options.ResponseType = "code id_token";         
                options.UsePkce = false;
                options.Scope.Add("masterweb_api");
                options.Scope.Add("profile");
                options.Scope.Add("offline_access");
                options.GetClaimsFromUserInfoEndpoint = true;
                options.SaveTokens = true;
            });
   }

    private bool AuthorizeAccess(AuthorizationHandlerContext context, string roleClaim, string userClaim)
    {
        return context.User.HasClaim(claim => claim.Type == roleClaim) &&
               context.User.HasClaim(claim => claim.Type == userClaim) ||
               context.User.IsInRole("SuperAdmin");
    }

    // 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();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

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

        app.UseEndpoints(endpoints =>
        {
                endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });

        RotativaConfiguration.Setup(env.WebRootPath, "Rotativa");
    }
}

How to solve this issue. I am totally clueless. Thanks in advance.


Solution

  • You should change the grant type to Code and also enable PKCE, because that is best practice to use when you use the authorization code flow.