Search code examples
swaggermicroservicesidentityserver4

Issues getting an api with swagger to authenticate with IdentityServer4 using .net5.0


I am currently learning how microservices work for an application i am building for my portfolio and for a small community of people that want this specific application. I have followed a tutorial online and successfully got IdentityServer4 to authenticate an MVC Client, however, I am trying to get swagger to work alongside the API's especially if they require authentication. Each time I try to authorize swagger with IdentityServer4, I am getting invalid_scope error each time I try authenticate. I have been debugging this problem for many hours an am unable to figure out the issue. I have also used the Microsoft eShopOnContainers as an example but still no luck. Any help would be greatly appreciated. Ill try keep the code examples short, please request any code not shown and ill do my best to respond asap. Thank you.

Identiy.API project startup.cs:

public class Startup {
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddIdentityServer()
                .AddInMemoryClients(Config.GetClients())
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryApiScopes(Config.GetApiScopes())
                .AddTestUsers(Config.GetTestUsers())
                .AddDeveloperSigningCredential();   // @note - demo purposes only. need X509Certificate2 for production)

        services.AddControllersWithViews();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();
        app.UseStaticFiles();
        app.UseIdentityServer();
        app.UseAuthorization();

        app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
    }
}

Config.cs (i removed the test users to keep the code shorter, since it is not relevant). @note -I created a WeatherSwaggerUI client specifically for use with swagger since this was apart of eShopOnContainers example project provided by microsoft:

public static class Config
{
    public static List<TestUser> GetTestUsers()
    {
        return new List<TestUser>(); // removed test users for this post
    }

    public static IEnumerable<Client> GetClients()
    {
        // @note - clients can be defined in appsettings.json
        return new List<Client>
        {
            // m2m client credentials flow client
            new Client
               {
               ClientId = "m2m.client",
               ClientName = "Client Credentials Client",

               AllowedGrantTypes = GrantTypes.ClientCredentials,
               ClientSecrets = { new Secret("SuperSecretPassword".ToSha256())},

                AllowedScopes = { "weatherapi.read", "weatherapi.write" }
            },
            // interactive client
            new Client
            {
                ClientId = "interactive",
                ClientSecrets = {new Secret("SuperSecretPassword".Sha256())},

                AllowedGrantTypes = GrantTypes.Code,

                RedirectUris = {"https://localhost:5444/signin-oidc"},
                FrontChannelLogoutUri = "https://localhost:5444/signout-oidc",
                PostLogoutRedirectUris = {"https://localhost:5444/signout-callback-oidc"},

                AllowOfflineAccess = true,
                AllowedScopes = {"openid", "profile", "weatherapi.read"},
                RequirePkce = true,
                RequireConsent = false,
                AllowPlainTextPkce = false
            },
            new Client
            {
                ClientId = "weatherswaggerui",
                ClientName = "Weather Swagger UI",
                AllowedGrantTypes = GrantTypes.Implicit,
                AllowAccessTokensViaBrowser = true,

                RedirectUris = {"https://localhost:5445/swagger/oauth2-redirect.html"},
                PostLogoutRedirectUris = { "https://localhost:5445/swagger/" },

                AllowedScopes = { "weatherswaggerui.read", "weatherswaggerui.write" },

            }
        };
    }

    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("weatherapi", "Weather Service")
            {
                Scopes = new List<string> { "weatherapi.read", "weatherapi.write" },
                ApiSecrets = new List<Secret> { new Secret("ScopeSecret".Sha256()) },
                UserClaims = new List<string> { "role" }
            },
            new ApiResource("weatherswaggerui", "Weather Swagger UI")
            {
                Scopes = new List<string> { "weatherswaggerui.read", "weatherswaggerui.write" }
            }
        };
    }

    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResource
            {
                Name = "role",
                UserClaims = new List<string> { "role" }
            },
        };
    }

    public static IEnumerable<ApiScope> GetApiScopes()
    {
        return new List<ApiScope>
        {
            // weather API specific scopes
            new ApiScope("weatherapi.read"),
            new ApiScope("weatherapi.write"),

            // SWAGGER TEST weather API specific scopes
            new ApiScope("weatherswaggerui.read"),
            new ApiScope("weatherswaggerui.write")
        };
    }
}

Next project is the just the standard weather api when creating a web api project with vs2019

WeatherAPI Project startup.cs (note i created extension methods as found in eShopOnContainers as i liked that flow):

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.AddCustomAuthentication(Configuration)
                .AddSwagger(Configuration);

    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();

            app.UseSwagger()
                .UseSwaggerUI(options =>
                {
                    options.SwaggerEndpoint("/swagger/v1/swagger.json", "Weather.API V1");
                    options.OAuthClientId("weatherswaggerui");
                    options.OAuthAppName("Weather Swagger UI");
                });
        }

        app.UseHttpsRedirection();

        app.UseRouting();

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

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

public static class CustomExtensionMethods
{
    public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddSwaggerGen(options =>
        {
            options.SwaggerDoc("v1", new OpenApiInfo
            {
                Title = "Find Scrims - Weather HTTP API Test",
                Version = "v1",
                Description = "Randomly generates weather data for API testing"
            });
            options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
            {
                Type = SecuritySchemeType.OAuth2,
                Flows = new OpenApiOAuthFlows()
                {
                    Implicit = new OpenApiOAuthFlow()
                    {
                        AuthorizationUrl = new Uri($"{ configuration.GetValue<string>("IdentityUrl")}/connect/authorize"),
                        TokenUrl = new Uri($"{ configuration.GetValue<string>("IdentityUrl")}/connect/token"),
                        Scopes = new Dictionary<string, string>()
                        {
                            { "weatherswaggerui", "Weather Swagger UI" }
                        },
                    }
                }
            });

            options.OperationFilter<AuthorizeCheckOperationFilter>();
        });

        return services;
    }

    public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
    {
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");

        var identityUrl = configuration.GetValue<string>("IdentityUrl");

        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(options =>
        {
            options.Authority = identityUrl;
            options.RequireHttpsMetadata = false;
            options.Audience = "weatherapi";
        });

        return services;
    }
}

Lastly is the AuthorizeCheckOperationFilter.cs

public class AuthorizeCheckOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
                           context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();

        if (!hasAuthorize) return;

        operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
        operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });

        var oAuthScheme = new OpenApiSecurityScheme
        {
            Reference = new OpenApiReference { 
                Type = ReferenceType.SecurityScheme, 
                Id = "oauth2" 
            }
        };

        operation.Security = new List<OpenApiSecurityRequirement>
        {
            new OpenApiSecurityRequirement
            {
                [oAuthScheme ] = new [] { "weatherswaggerui" }
            }
        };
    }
}

again, any help, or recommendations on a guide would be greatly appreciated as google has not provided me with any results to fixing this issue. Im quite new to IdentityServer4 and am assuming its a small issue due with clients and ApiResources and ApiScopes. Thank you.


Solution

  • The swagger client needs to access the api and to do so it requires api scopes. What you have for swagger scopes are not doing this. Change the scopes for swagger client ‘weatherswaggerui’ to include the api scopes like this:

    AllowedScopes = {"weatherapi.read"}