Search code examples
apipostmanidentityserver4

Identity Server 4 Postman Request Token not working


I have an API and I am using IdentityServer4 for authentication. So, I have a solution with my API and my Identity Server projects in it. So far so good. I've tested the API without authentication and it works fine. Now I'm trying to test it with Authentication, using Postman, and I am running into an issue. I am trying to request a token, using the Authorization Code grant type, and I put in all the info needed. The error I keep getting in the console is

09:51:31 Information] IdentityServer4.ResponseHandling.AuthorizeInteractionResponseGenerator Showing login: User is not authenticated

I've looked online for tutorials that show how to set up an API and Identity Server4, but all of them want to create a client and show a login screen. I don't need a login for this API, and I have no idea what my client is going to do. All I want is for the client to be able to authorize using a clientID and a client Secret. And then get an access token that they can use for the rest of the API calls. How can I do this? Below is my startup for my API and for the IDP

API Startup

 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();
            services.AddHttpContextAccessor();
            
            services.AddAuthentication("Bearer")
            .AddJwtBearer("Bearer", options =>
            {
                options.Authority = "https://localhost:5001";

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateAudience = false
                };
            });

            // adds an authorization policy to make sure the token is for scope 'api1'
            services.AddAuthorization(options =>
            {
                options.AddPolicy("ApiScope", policy =>
                {
                    policy.RequireAuthenticatedUser();
                    policy.RequireClaim("scope", "URCSAPI");
                });
            });
        }

        // 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.UseRouting();

            app.UseAuthentication();

            app.UseAuthorization();

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

IDP Startup

 public class Startup
    {
        public IWebHostEnvironment Environment { get; }

        public Startup(IWebHostEnvironment environment)
        {
            Environment = environment;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            var URCSIDPDataDBConnectionString =
                "Server=localhost;Database=URCSIDP;Trusted_Connection=True;MultipleActiveResultSets=true";

            // uncomment, if you want to add an MVC-based UI
            services.AddControllersWithViews();

            var builder = services.AddIdentityServer();
                //.AddInMemoryIdentityResources(Config.Ids)
                //.AddInMemoryApiResources(Config.Apis)
                //.AddInMemoryClients(Config.Clients)
                //.AddTestUsers(TestUsers.Users);

            // not recommended for production - you need to store your key material somewhere secure
            builder.AddDeveloperSigningCredential();
            //builder.AddSigningCredential(LoadCertificateFromStore());

            var migrationsAssembly = typeof(Startup)
                .GetTypeInfo().Assembly.GetName().Name;

            builder.AddConfigurationStore(options =>
            {
                options.ConfigureDbContext = builder => 
                    builder.UseSqlServer(URCSIDPDataDBConnectionString,
                    options => options.MigrationsAssembly(migrationsAssembly));
            });

            builder.AddOperationalStore(options =>
            {
                options.ConfigureDbContext = builder => 
                    builder.UseSqlServer(URCSIDPDataDBConnectionString,
                    options => options.MigrationsAssembly(migrationsAssembly));
            });
        }

        public void Configure(IApplicationBuilder app)
        {
            if (Environment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            //InitializeDatabase(app);

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

            app.UseIdentityServer();

            // uncomment, if you want to add MVC
            //app.UseAuthorization();
            //app.UseEndpoints(endpoints =>
            //{
            //    endpoints.MapDefaultControllerRoute();
            //});
        }

        public X509Certificate2 LoadCertificateFromStore()
        {
            string thumbPrint = "d4d681b3de4cd26fc030292aeea170e553810bdb";

            using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
            {
                store.Open(OpenFlags.ReadOnly);
                var certCollection = store.Certificates.Find(X509FindType.FindByThumbprint,
                    thumbPrint, true);
                if (certCollection.Count == 0)
                {
                    throw new Exception("The specified certificate wasn't found.");
                }
                return certCollection[0];
            }
        }

    }

Update: Adding the config file I used to add the info to the DB:

 public static class Config
    {
        public static IEnumerable<IdentityResource> Ids =>
            new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Address()
            };

        public static IEnumerable<ApiResource> Apis =>
            new ApiResource[]
            {
                new ApiResource(
                    "URCSAPI",
                    "Unit Rate ContractSystem API")
                {
                    ApiSecrets = { new Secret("apisecret".Sha256()) }
                }
            };

        public static IEnumerable<Client> Clients =>
            new Client[]
            {
                new Client
                {
                    AccessTokenType = AccessTokenType.Reference,
                    AccessTokenLifetime = 120,
                    AllowOfflineAccess = true,
                    UpdateAccessTokenClaimsOnRefresh = true,
                    ClientName = "Tesla",
                    ClientId = "Tesla",
                    AllowedGrantTypes = GrantTypes.ClientCredentials,
                    RequirePkce = false,
                    RedirectUris = new List<string>()
                    {
                        "https://localhost:6001/signin-oidc"
                    },
                    PostLogoutRedirectUris = new List<string>()
                    {
                        "https://localhost:6001/signout-callback-oidc"
                    },
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Address,
                        "URCSAPI"
                    },
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    }
                }
            };
    }

Solution

  • If you are trying to protect an API that is not used with an interactive interface (e.g. website with a user in a browser), then Auth Code flow is not the method you want to use. Auth Code flow is an "interactive" flow, meaning the user login via a browser or mobile app on a form. If your API is designed for machine-to-machine use or someone else is writing code to directly access your API, then you should use the Client Credentials flow. This flow simply passes the clientId and clientSecret to obtain an access token to use against the API.

    See this page on the ID4 website: https://docs.identityserver.io/en/latest/quickstarts/1_client_credentials.html

    Look at your IdentityServer Config.cs and make sure that everything in there is correct. For testing, it's easier to use the InMemory Stores (uncomment the "AddInMemory" lines) instead of the EF-based stores in the DB (you can make tweaks faster and then move to DB-based stores later).

    You will need to have a Client that is of type "ClientCredentials" (see the "Defining the client" section at the URL above). The basic example they provide looks like:

    public static IEnumerable<Client> Clients =>
    new List<Client>
    {
        new Client
        {
            ClientId = "client",
    
            // no interactive user, use the clientid/secret for authentication
            AllowedGrantTypes = GrantTypes.ClientCredentials,
    
            // secret for authentication
            ClientSecrets =
            {
                new Secret("secret".Sha256())
            },
    
            // scopes that client has access to
            AllowedScopes = { "api1" }
        }
    };