Reputation: 3705
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"]
}
Upvotes: 2
Views: 2755
Reputation: 3705
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
Upvotes: 0
Reputation: 117321
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
}
Upvotes: 1