2

I am building an application where Web API and IdentityServer4 are inside the same .Net Core 2.0 project. This API is consumed by Aurelia SPA web app. IdentityServer4 set to use JWT and ImplicitFlow. Everything works good (Client app gets redirected to login, gets token, sends it back on header, etc.) up to the point where user needs to be authorized in the API controller, then it just cannot authorize user, because it's null.

There are many similar questions exists, but I tried all proposed solutions and none of them worked for me. I already spent 2 days on this issue and starting to loose hope and patience. I probably missing something obvious, but just can't find it. I posting my configs here - what is wrong with them? Will appreciate any help.

My Startup class (I have omitted some extra things like logging, localization, etc.):

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

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        services.AddCors(options =>
        {
            options.AddPolicy("default", policy =>
            {
                policy.WithOrigins(Config.APP1_URL)
                    .AllowAnyHeader()
                    .AllowAnyMethod();
            });
        });

        services.AddMvc();

        services.Configure<IdentityOptions>(options =>
        {
            // Password settings
            options.Password.RequireDigit = false;
            options.Password.RequiredLength = 8;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequireUppercase = false;
            options.Password.RequireLowercase = false;
            options.Password.RequiredUniqueChars = 4;

            // Lockout settings
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
            options.Lockout.MaxFailedAccessAttempts = 10;
            options.Lockout.AllowedForNewUsers = true;

            // User settings
            options.User.RequireUniqueEmail = true;
        });

        services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            .AddInMemoryPersistedGrants()
            .AddInMemoryIdentityResources(Config.GetIdentityResources())
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients())
            .AddAspNetIdentity<ApplicationUser>();

        services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = Config.HOST_URL + "/";
                options.RequireHttpsMetadata = false;
                options.ApiName = "api1";
            });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();

        app.UseIdentityServer();

        app.UseAuthentication();

        app.UseCors("default");

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

This is my Config class:

public class Config
{
    public static string HOST_URL = "http://dev.example.com:5000";
    public static string APP1_URL = "http://dev.example.com:9000";

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

    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("api1", "My API")
        };
    }

    public static IEnumerable<Client> GetClients()
    {
        return new List<Client>
        {
            new Client
            {
                ClientId = "reporter",
                ClientName = "ReporterApp Client",
                AccessTokenType = AccessTokenType.Jwt,
                AllowedGrantTypes = GrantTypes.Implicit,
                RequireConsent = false,
                AllowAccessTokensViaBrowser = true,
                RedirectUris =
                {
                    $"{APP1_URL}/signin-oidc"
                },
                PostLogoutRedirectUris = {
                    $"{APP1_URL}/signout-oidc"
                },
                AllowedCorsOrigins = {
                    APP1_URL
                },

                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    "api1"
                }
            }
        };
    }
}

And the token Aurelia app gets from IdentityServer:

{
    "alg": "RS256",
    "kid": "52155e28d23ddbab6154ce0c34511c9a",
    "typ": "JWT"
},
{
    "nbf": 1521195164,
    "exp": 1521198764,
    "iss": "http://dev.example.com:5000",
    "aud": ["http://dev.example.com:5000/resources", "api1"],
    "client_id": "reporter",
    "sub": "767381df-446a-4c34-af27-7bdf9e4563f3",
    "auth_time": 1521195163,
    "idp": "local",
    "scope": ["openid", "profile", "api1"],
    "amr": ["pwd"]
}
ahmad molaie
  • 1,512
  • 2
  • 21
  • 41
graycrow
  • 3,675
  • 6
  • 26
  • 28

2 Answers2

1

First thing swap your order UseAuthencation over writes some stuff.

app.UseAuthentication();
app.UseIdentityServer();

second change the cookie scheme. Identityserver4 has its own so your user is null because its not reading the cookie.

services.AddAuthentication(IdentityServerConstants.DefaultCookieAuthenticationScheme)
                .AddIdentityServerAuthentication(options =>
                {
                    // base-address of your identityserver
                    options.Authority = Configuration.GetSection("Settings").GetValue<string>("Authority");
                    // name of the API resource
                    options.ApiName = "testapi";
                    options.RequireHttpsMetadata = false;
                }); 

Idea number three:

I had to add the type to the api call so that it would read the bearer token.

[HttpPost("changefiscal")]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<ActionResult> ChangeFiscal([FromBody] long fiscalId)
  {
   // STuff here
   }
Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449
  • Thank you for reply. Unfortunately it didn't helped. Also, about cookies. I'm not sending any cookies in my request, I'm sending a bearer token in the authorization header, how cookies scheme gets involved here? Will appreciate a link to some article which explains it. – graycrow Mar 16 '18 at 13:47
  • I am actually doing this in my Identityserver i have an api endpoint in it. I remember it driving me crazy trying to get it to work the trick is to figuer out what i did to get it to work. I just added another idea try adding the AuthenticationSchemes. I really should create an article on this it took me days to get it working – Linda Lawton - DaImTo Mar 16 '18 at 14:02
  • I actually just found an solution in the another question here, on SO. Will post an answer in a few seconds. Thank you for helping, it's actually sent me in the right direction. – graycrow Mar 16 '18 at 14:18
0

Well, it's finally works now. I found an answer here.

All what was necessary to do, is to replace "Bearer" authentication scheme in the Startup class (services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)) with following:

services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })

Edit: It worked for a moment while I had a valid token. Probably authorization still works, but now I have another issue - authentication is broken, the login page goes into a loop. So complicated.

Edit 2. Working solution found here.

It's necessary to add both

services.AddMvc(config =>
            {
                var defaultPolicy = new AuthorizationPolicyBuilder(new[] { IdentityServerAuthenticationDefaults.AuthenticationScheme, IdentityConstants.ApplicationScheme })
                    .RequireAuthenticatedUser()
                    .Build();
                config.Filters.Add(new AuthorizeFilter(defaultPolicy));
            })

and default authentication scheme

services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)

Into Startup class

graycrow
  • 3,675
  • 6
  • 26
  • 28
  • Try putting a "/" at the end of your redirect URL. – JakeJ Mar 16 '18 at 18:18
  • If you don't feel like doing that, put a Session_Start() method in your Global.asax. (these two comments have to do with your infinite redirect) – JakeJ Mar 16 '18 at 18:19
  • Unfortunately "/" at the end of redirect URL causes "Invalid redirect_uri" error. – graycrow Mar 17 '18 at 17:32
  • Oh, it has to match on client startup and on the config on the IdentityServer side. If I remember correctly, you either need the Session_Start() method or the '/' at the end of the redirect. Even though you're going to the same place, it's doing a string match against the config. – JakeJ Mar 18 '18 at 15:16
  • If I manage to help you out, please check out my most recent question. I'm totally lost right now on cookie expiration and how to make it get set. :) – JakeJ Mar 18 '18 at 15:18