0
votes

im stuck on finding the issue i have here. i have tried to find from questions in SO but could figure out the problem. so im quite desperate atm.

so in my solutions we have 3 projects

API

  • Production API Resource

IdentityServer4

  • IdentityServer4
  • WebAPI Management API to access Client, Scopes, Etc on IdentityServ4

Client APP

  • MVC App

Everything went fine. Client can login and authenticate via IS4 and access production resources. there is now comes a need to also create api to manage IS4 from the client app as well. but it seems that i cant Authenticate using the same token issued by the IS4.

the message on IS4 Log is as follows

info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1] Route matched with {action = "GetUserAccountsList", controller = "Accounts"}. Executing action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API) dbug: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[9] AuthenticationScheme: Bearer was not authenticated. info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] Authorization failed. info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3] Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'. info: Microsoft.AspNetCore.Mvc.ChallengeResult[1] Executing ChallengeResult with authentication schemes (Bearer). info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12] AuthenticationScheme: BearerIdentityServerAuthenticationJwt was challenged. info: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[12] AuthenticationScheme: Bearer was challenged. info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2] Executed action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API) in 0.212ms info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] Request finished in 0.6503ms 401

API Code on IS4 Web API

[Authorize(AuthenticationSchemes = "Bearer")]
    [HttpGet]
    public async Task<IActionResult> GetUserAccountsList()
    {
        var userAccounts = await _accountService.GetIdentityAccountsAsync();
        return new JsonResult(userAccounts);
    }

And On StartUp ConfigureServices

 public void ConfigureServices(IServiceCollection services)
    {
        var dbConnectionName = Constants.Environment.Development;
        if (_env.IsProduction())
        {
            dbConnectionName = Constants.Environment.Production;
        }

        services.AddDbContext<ApplicationDbContext>(options =>
       options.UseSqlServer(Configuration.GetConnectionString(dbConnectionName), sqlServerOptionsAction: sqlOptions =>
       {
           sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
       }));

        services.AddIdentity<ApplicationUser, IdentityRole>()
            //  use this if we want to implement default ASP.NET identity
            //services.AddDefaultIdentity<ApplicationUser>()
            .AddRoles<IdentityRole>()
            .AddRoleManager<RoleManager<IdentityRole>>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        // Configure DI
        ConfigureDependencies(services);
        services.AddMvc();

        #region Registering ASP.NET Identity Server

        var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
        services.AddIdentityServer(options =>
        {
            options.IssuerUri = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
            options.Authentication.CookieLifetime = TimeSpan.FromHours(2);
        })
         // change to certificate credentials on production
         // .AddSigningCredential(Certificate.Get())
         .AddDeveloperSigningCredential()
         .AddAspNetIdentity<ApplicationUser>()

        //// this adds the config data from DB (clients, resources) instead of memory
        //.AddInMemoryIdentityResources(Config.GetIdentityResources())
        //.AddInMemoryClients(Config.GetClients(_env.IsProduction()))
        //.AddInMemoryApiResources(Config.GetApiResources())

        .AddConfigurationStore(options =>
        {
            options.ConfigureDbContext = builder =>
                builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
                    sql => sql.MigrationsAssembly(migrationsAssembly));
        })

        //// this adds the operational data from DB (codes, tokens, consents)
        //.AddInMemoryPersistedGrants()
        .AddOperationalStore(options =>
        {
            options.ConfigureDbContext = builder =>
                builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
                    sql => sql.MigrationsAssembly(migrationsAssembly));

            // this enables automatic token cleanup. this is optional.
            options.EnableTokenCleanup = true;
            options.TokenCleanupInterval = 30;
        })
        .AddProfileService<IdentityProfileService>(); ;

        #endregion Registering ASP.NET Identity Server

        services.RegisterApplicationPolicy();

        #region External Auth

        services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
                options.RequireHttpsMetadata = false;
                options.ApiName = Constants.Resource.Identity;
                // options.SupportedTokens = SupportedTokens.Both;
            }); ;

        #endregion External Auth
    }

Startup.cs Configure

public void Configure(IApplicationBuilder app, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
    {
        InitializeDatabase(app, _env, userManager, roleManager);

        if (_env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
        app.UseIdentityServer();
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseCookiePolicy();
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }

Client on IS4 is seeded to DB and set up as follows

        public static class Resource
    {
        public static List<string> GetAllResourceList()
        {
            return new List<string>()
            {
                Clinic,
                Subscription,
                Module,
                Identity // this is IDS4 Server
            };
        }

        // this is used on db seed only.
        // resource name has to be updated on DB after db seed
        public const string Clinic = "Clinic";
        public const string Subscription = "Subscription";
        public const string Module = "Module";
        public const string Identity = "Identity";

        public const string ClinicAddress = "http://localhost:5100";
        public const string SubscriptionAddress = "http://localhost:5200";
        public const string ModuleAddress = "http://localhost:5300";
        public const string IdentityAddress = "http://localhost:5000";
    }

 public class Config
{
    public static IEnumerable<ApiResource> GetApiResources()
    {
        return Constants.Resource.GetAllResourceList().Select(s => new ApiResource(s));
    }

    // client want to access resources (aka scopes)
    public static IEnumerable<Client> GetClients(bool isDevelopment)
    {
        var client = new List<Client>();
        var mvcClient = new Client
        {
            ClientId = "mvc",
            ClientName = "MVC Client",
            AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
            RequireConsent = false,
            ClientSecrets =
            {
                new Secret("secret".Sha256())
            },
            RedirectUris = { $"{Constants.Address.GetClientServerAdress(isDevelopment)}/signin-oidc" },
            PostLogoutRedirectUris =
                {$"{Constants.Address.GetClientServerAdress(isDevelopment)}/signout-callback-oidc"},
            AlwaysIncludeUserClaimsInIdToken = true,
            AllowAccessTokensViaBrowser = true,
            AllowedScopes =
            {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
            },
            AllowOfflineAccess = true
        };
        foreach (var resource in Constants.Resource.GetAllResourceList())
        {
            mvcClient.AllowedScopes.Add(resource);
        }
        client.Add(mvcClient);
        return client;
    }

    //Add support for the standard openid (subject id) and profile scopes
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
        };
    }
}

On Client App Startup.cs is as follows

        public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

        // Adding Authentication options+

        services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";
                options.Authority = $"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}";
                options.ClientId = "mvc";
                options.ClientSecret = "secret";
                options.ResponseType = "code id_token";

                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;
                options.RequireHttpsMetadata = false;

                foreach (var resource in Constants.Resource.GetAllResourceList())
                {
                    options.Scope.Add(resource);
                }
                options.Scope.Add("offline_access");
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "role",
                };
            });
        // Adding Authorisation
        services.RegisterApplicationPolicy();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
        app.UseAuthentication();
        app.UseStaticFiles();
        app.UseMvcWithDefaultRoute();
    }

where we are trying to access Web Api on IS4 with the following code. See that the following call always return 401 Unauthorize

  var accessToken = await HttpContext.GetTokenAsync("access_token");

        var client = new HttpClient();
        client.SetBearerToken(accessToken);

        var response = await client.GetAsync($"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}/api/Accounts/GetUserAccountsList");
        if (response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            ViewBag.Json = JArray.Parse(content).ToString();
        }
        else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            return Unauthorized();
        }
        return View("Json");

Any advice on how to fix the problem will be helpfull. at the moment im using form authentication to do client management on IS4.

2

2 Answers

0
votes

IdentityServer is for authenticating existing users, not for managing users . So i would suggest you create another api app which protected by IdenitityServer to manage users , the api app and identity server app share same database .

But of course , you can also add api to your identity server app .According to your code , you should modify as :

  1. You should add Api resource in Config.cs in your Identity Server :

    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("api1", "My API")
        };
    }
    
  2. Modify your client in Config.cs in your Identity Server which allow client acquire access token to access api resource :

    AllowedScopes =
                {
                   IdentityServerConstants.StandardScopes.OpenId,
                   IdentityServerConstants.StandardScopes.Profile,
                   "api1"
                },
    
  3. Your identity server should validate the access token :

    services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = "https://localhost:44373"; //IDS's endpoint
                options.RequireHttpsMetadata = false;
                options.ApiName = "api1";   //api name
            });
    
  4. Modify your client to acquire access token using Hybrid Flow :

    .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = "Cookies";
    
        options.Authority = "https://localhost:44373/";
        options.RequireHttpsMetadata = false;
    
        options.ClientId = "mvc2";
        options.ClientSecret = "secret";
        options.ResponseType = "code id_token";
    
        options.SaveTokens = true;
        options.GetClaimsFromUserInfoEndpoint = true;
    
        options.Scope.Add("api1");  //Api name
        options.Scope.Add("offline_access");
    
    });
    
  5. After get access token , you can request the api endpoint with Authorization: Bearer xxxxheader .

0
votes

I finally figure out the cause of the request after checking the sent request using fiddler. the scope and resources setting was correct. The cause is identityServer authentication is comparing the token issuer.

both authority on client & identity is pointing to IDS http:localhost:5000 instead of https. therefore the token issuer is set as http. so i only need to change the authority to https. what a silly mistake of mine =).

for some reason WebApi's authorize attribute on IDS and Resource APIS behave differently where IDS checking issuer and resources doest. have to do more research on this issue.