Search code examples
c#dockeridentityserver4azure-container-instances

Identityserver4 API authentication doesn't work as expected


I'm stuck with this problem for almost a month, so any help is appreciated. Let's get to the problem itself: I have an identity server and user management API (CRUD based) in one project. The identity server itself works as a login/register page for other web sites (I currently have only one ASP.NET framework MVC web site). The API is used to retrieve and update user profile from the MVC project and mobile app. The identity server and MVC project is backed up by docker containers.

The API authentication is done through the identity server bearer token. So, the API authentication works perfectly on localhost, however, when I deploy the identity server to Azure container instances, the API stops working, both from MVC and Postman. The error that I'm getting is :

An unhandled exception occurred while processing the request. WinHttpException: The operation timed out

System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() HttpRequestException: An error occurred while sending the request.

System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() IOException: IDX10804: Unable to retrieve document from: 'http://taxrefund-identity.westeurope.azurecontainer.io/.well-known/openid-configuration'.

Microsoft.IdentityModel.Protocols.HttpDocumentRetriever+d__8.MoveNext() InvalidOperationException: IDX10803: Unable to obtain configuration from: 'http://taxrefund-identity.westeurope.azurecontainer.io/.well-known/openid-configuration'.

Microsoft.IdentityModel.Protocols.ConfigurationManager+d__24.MoveNext()

The weirdest things is, that I can access the discovery endpoint through my browser with no problems.

My ConfigureServices method:

   public void ConfigureServices(IServiceCollection services)
    {
        services.AddEntityFrameworkSqlServer()
                .AddDbContext<ApplicationDbContext>(options =>
                    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
                        sqlServerOptionsAction: sqlOptions =>{
                            sqlOptions.EnableRetryOnFailure(maxRetryCount: 10, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
                        }), ServiceLifetime.Scoped
                 );
        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();
        services.AddTransient<IEmailSender, EmailSender>();
        services.AddScoped<DatabaseInitializer>();
        services.AddCors();
        // Adds IdentityServer
        var cert = new X509Certificate2(Path.Combine(Environment.ContentRootPath, "idsrv3test.pfx"), "idsrv3test");
        services.AddIdentityServer()
            .AddSigningCredential(cert)
            .AddInMemoryIdentityResources(Config.GetIdentityResources())
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients())
            .AddAspNetIdentity<ApplicationUser>()
            .Services.AddTransient<IProfileService, ProfileService>();

        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
        services.AddAuthentication()
            .AddGoogle("Google", options =>
            {
                options.ClientId = "**";
                options.ClientSecret = "**";
            })
            .AddMicrosoftAccount("Microsoft", options =>
            {
                options.ClientId = "**";
                options.ClientSecret = "**";
            });
        services.AddAuthentication("Bearer")
            .AddIdentityServerAuthentication(o =>
            {
                o.Authority = "http://taxrefund-identity.westeurope.azurecontainer.io/";
                o.ApiName = "Profile.API";
                o.ApiSecret = "**";
                o.RequireHttpsMetadata = false;
            });
        services.AddMvc();
        services.AddAntiforgery();
    }

Configure method:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, RoleManager<IdentityRole> roleManager, ApplicationDbContext context, UserManager<ApplicationUser> userManager)
    {
        loggerFactory.AddDebug();
        loggerFactory.AddConsole(LogLevel.Trace);
        loggerFactory.AddFile("logs.txt");

        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
        app.UseBrowserLink();
        app.UseCors(policy =>
        {
            policy.AllowCredentials();
            policy.AllowAnyOrigin();
            policy.AllowAnyHeader();
            policy.AllowAnyMethod();
            policy.WithExposedHeaders("WWW-Authenticate");
        });

        app.UseStaticFiles();
        app.UseIdentityServer();
        app.UseAuthentication();
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
        context.Database.Migrate();
        DatabaseInitializer.SeedData(roleManager);
    }

API resource configuration:

new ApiResource("Profile.API", "Profile management API")
{
    UserClaims = { ClaimTypes.Role },
    ApiSecrets =
    {
        new Secret("**".Sha256())
    }
}

I secure my API like this:

[Authorize(AuthenticationSchemes = "Bearer")]
[Route("api/Users")]
[Produces("application/json")]
public class ApplicationUsersAPIController : ControllerBase

To access it, I request a token from /connect/token endpoint (either with client credentials or Resource owner password/username) and then use it in the Authorization header with subsequent requests.

I have been stuck with this problem almost for a month now - it's getting frustrating now. I have read all the posts related to this problem and none of the solutions helped. I've tried degrading to earlier versions of system.net.http, different certificates and more solutions that helped others.

Also, the endpoint, without the [Authorize] attribute works just fine.

The only thing that I didn't try is to install an SSL certificate and make my urls https - I read that this shouldn't effect the functionality of identity server. I have no actual need for that right now, so let me know if it's necessary.

If there's a need for more information from me, let me know.

Much appreciated.


Solution

  • The final solution was to change the authority URL in AddIdentityServerAuthentication from:

    services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(o =>
                {
                    o.Authority = "http://taxrefund-identity.westeurope.azurecontainer.io/";
                    o.ApiName = "Profile.API";
                    o.ApiSecret = "**";
                    o.RequireHttpsMetadata = false;
                });
    

    To:

       services.AddAuthentication("Bearer")
                    .AddIdentityServerAuthentication(o =>
                    {
                        o.Authority = "http://localhost/"; //crucial part
                        o.ApiName = "Profile.API";
                        o.ApiSecret = "**";
                        o.RequireHttpsMetadata = false;
                    });
    

    That actually makes sense, since in this case, the identity server and the API is running in the same container instance/process, so, it is not able to access it self via the DNS URL, instead, it can access itself via the localhost URL.