I have 3 projects JWT.IDP, JWT.API, JWT.MVC.
JWT.IDP - an API project validates user and issues the JWT token.
JWT.API - an API project for my business logic, CURD etc
JWT.MVC - an MVC application for UI.
My intention is to use this token generated in JWT.IDP and call the JWT.API functions from JWT.MVC
The IDP token is working perfectly fine, I can generate the token and my JWT.MVC Login controller is able to receive it. But when I am trying to use this token to access the JWT.API it gives a 500 error (Please see the last function in the below code (GetWeatherData)).
Can someone help, I am not an advanced user, the code written below is taken from several samples. So I am not sure whether it really is the right code.
namespace JWT.MVC.Controllers
{
public class LoginController : Controller
{
public IActionResult DoLogin()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DoLogin([Bind("EmailOrName,Password")] LoginRequestModel loginRequestModel)
{
var apiName = $"https://localhost:44318/api/User/login";
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.PostAsJsonAsync(apiName, loginRequestModel);
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<IEnumerable<AccessibleDb>>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
foreach (var item in data)
{
item.UserName = loginRequestModel.EmailOrName;
}
return View("SelectDatabase" , data);
}
public async Task<IActionResult> PostLogin(string db, string user)
{
TokenRequestModel tokenRequestModel = new TokenRequestModel() { Database = db, UserName = user };
var apiName = $"https://localhost:44318/api/User/tokenonly";
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.PostAsJsonAsync(apiName, tokenRequestModel);
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<AuthenticationModel>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
var stream = data.Token;
var handler = new JwtSecurityTokenHandler();
var jsonToken = handler.ReadToken(stream);
var tokenS = jsonToken as JwtSecurityToken;
var selectedDb = tokenS.Claims.First(claim => claim.Type == "Database").Value;
ViewBag.SelectedDb = selectedDb;
return View(data);
}
public async Task<IActionResult> GetWeatherData(string token)
{
var apiName = $"https://localhost:44338/weatherforecast";
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = await httpClient.GetAsync(apiName);
if (!response.IsSuccessStatusCode)
{
ViewBag.Error = response.StatusCode;
return View("Weatherdata");
}
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<WeatherForecast>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
return View("Weatherdata" , data);
}
}
}
Startup class for JWT.MVC is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Audience = "SecureApiUser";
options.Authority = "https://localhost:44318";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
Startup class for JWT.API is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//Copy from IS4
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Audience = "SecureApiUser";
options.Authority = "https://localhost:44318";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
//End
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.API", Version = "v1" });
});
}
Startup class for JWT.IDP is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//Configuration from AppSettings
services.Configure<JwtSettings>(Configuration.GetSection("JWT"));
//User Manager Service
services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<IdentityDbContext>();
services.AddScoped<IUserService, UserService>();
//Adding DB Context with MSSQL
services.AddDbContext<IdentityDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("IdentityDbConnectionString"),
b => b.MigrationsAssembly(typeof(IdentityDbContext).Assembly.FullName)));
//Adding Athentication - JWT
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.RequireHttpsMetadata = false;
o.SaveToken = false;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(Convert.ToInt32(Configuration["JWT:DurationInMinutes"])),
ValidIssuer = Configuration["JWT:Issuer"],
ValidAudience = Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"]))
};
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.IDP", Version = "v1" });
});
}
And the JWT Setting is as below
"JWT": {
"key": "C1CF4B7DC4C4175B6618DE4F55CA4",
"Issuer": "http://localhost:44318",
"Audience": "SecureApiUser",
"DurationInMinutes": 60
},
It's quite surprising that no one was able to identify the mistake. I made the following changes and it works perfectly fine now.
The ConfigureServices is like below in both MVC and API projects. No other changes to any other codes.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var authenticationProviderKey = "IdentityApiKey";
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("C1CF4B7DC4C4175B6618DE4F55CA4"));
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidateIssuer = true,
ValidIssuer = "http://localhost:44318",
ValidateAudience = true,
ValidAudience = "SecureApiUser",
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
RequireExpirationTime = true,
};
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = authenticationProviderKey;
})
.AddJwtBearer(authenticationProviderKey, x =>
{
x.RequireHttpsMetadata = false;
x.TokenValidationParameters = tokenValidationParameters;
});
//services.AddAuthentication(options =>
//{
// options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
// options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
//}).AddJwtBearer(options =>
//{
// options.Authority = "https://localhost:44318"; ;
// options.RequireHttpsMetadata = false;
// options.Audience = "SecureApiUser";
//});
//End
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.API2", Version = "v1" });
});
}
Related
I am using multiple authentication schemes. Here is my Startup.cs code -
public void ConfigureServices(IServiceCollection services)
{
//code
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Default";
options.DefaultSignInScheme = "Default";
options.DefaultChallengeScheme = "O365OpenId";
})
.AddCookie("Default",options =>
{
options.LoginPath = "/";
options.LogoutPath = "/Logout";
options.Cookie.Name = "ip" + guid.ToString();
options.AccessDeniedPath = "/Auth/Denied";
})
.AddCookie("External", options =>
{
options.LoginPath = "/Auth/External";
options.LogoutPath = "/Logout";
options.Cookie.Name = "ip" + guid.ToString();
options.AccessDeniedPath = "/Auth/Denied";
})
.AddOAuth("O365OpenId", options =>
{
//options.Authority = "given";
options.ClientId = "given";
options.ClientSecret = "given";
options.CallbackPath = "/auth/callback";
options.AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
options.AuthorizationEndpoint += "?prompt=select_account";
options.TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
options.UserInformationEndpoint = "https://graph.microsoft.com/oidc/userinfo";
options.AccessDeniedPath = "/Auth/Denied";
options.Scope.Add("openid");
options.Scope.Add("email");
//options.Scope.Add("profile");
//options.Scope.Add("offline_access");
//options.Scope.Add("user.read");
//options.Scope.Add("calendars.read");
options.SaveTokens = false;
//options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
//options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
//options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
//options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
//options.ClaimActions.MapJsonKey(ClaimTypes., "picture");
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
if (context.AccessToken is { })
{
context.Identity?.AddClaim(new Claim("access_token", context.AccessToken));
}
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(json.RootElement);
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("Admin", policy =>
{
policy.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.RequireRole("Admin");
});
options.AddPolicy("Regular&Admin", policy =>
{
policy.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.RequireRole("Regular");
});
options.AddPolicy("External", policy =>
{
policy.AuthenticationSchemes.Add("External");
policy.RequireAuthenticatedUser();
policy.RequireRole("External");
});
});
}
When I sign in user using the "Default" scheme, I find that the HttpContext.User.Identity.IsAuthenticated is true after HttpContext.SignInAsync("schema-name", claimsPrincipal); method call. Here is the code for signing in -
public async Task<IActionResult> Success(string returnUrl)
{
// code
var claimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
await HttpContext.SignInAsync("Default",claimsPrincipal);
// more code
}
isAuthenticated is true
But When I sign in user using "External" scheme, HttpContext.User.Identity.IsAuthenticated is false after HttpContext.SignInAsync("schema-name", claimsPrincipal); call. Here is the code for signing in -
public async Task<IActionResult> External([FromQuery] string ReturnUrl, LoginOTP loginOTP, string email)
{
//code
var claimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
await HttpContext.SignInAsync("External", claimsPrincipal);
//code
}
IsAuthenticated is false.
I cannot understand what I am doing wrong. Why is the IsAuthenticated returning false?
Please help. Thank you.
you have 3 authentication schemes
the first one is cookie-based and you named it: "Default",
the second one is also cookie-based and you named it: "External",
the third one is OAhut-based and you named it: "O365OpenId",
when referring to one of these schemes you should use the names that you have created.
now the issue is, that you're creating a ClaimeIdentity with one scheme but you signing the user with a different scheme.
for the external method it should be like this:
var claimIdentity = new ClaimsIdentity(claims, "External");
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
// code ...
await HttpContext.SignInAsync("External", claimsPrincipal);
now the claims principal will be associated with the "External" scheme and the user will be authenticated.
I have an API in .NET5 uses JWTBearer to secure the API. Now I wanted to configure my client app to use the token generated from the api/gettoken. It works fine in swagger, but I don't know how to configure my MVC and API(consuming API) to use this token. Can somebody help by providing the configureservices and configure methods in the startup
To Glenn,
I have 3 projects JWT.IDP, JWT.API, JWT.MVC. JWT.IDP issues the token, my intention is to use that token in JWT.API and call the JWT.API function from JWT.MVC. The IDP is working perfectly, I can generate the token and my JWT.MVC Login controller is able to receive it. The last function in the below code (GetWeatherData) is coded according to the idea you have given. If I don't pass the token, I used to get 401 error, now I get 500 Internal Server Error
namespace JWT.MVC.Controllers
{
public class LoginController : Controller
{
public IActionResult DoLogin()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DoLogin([Bind("EmailOrName,Password")] LoginRequestModel loginRequestModel)
{
var apiName = $"https://localhost:44318/api/User/login";
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.PostAsJsonAsync(apiName, loginRequestModel);
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<IEnumerable<AccessibleDb>>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
foreach (var item in data)
{
item.UserName = loginRequestModel.EmailOrName;
}
return View("SelectDatabase" , data);
}
public async Task<IActionResult> PostLogin(string db, string user)
{
TokenRequestModel tokenRequestModel = new TokenRequestModel() { Database = db, UserName = user };
var apiName = $"https://localhost:44318/api/User/tokenonly";
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.PostAsJsonAsync(apiName, tokenRequestModel);
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<AuthenticationModel>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
var stream = data.Token;
var handler = new JwtSecurityTokenHandler();
var jsonToken = handler.ReadToken(stream);
var tokenS = jsonToken as JwtSecurityToken;
var selectedDb = tokenS.Claims.First(claim => claim.Type == "Database").Value;
ViewBag.SelectedDb = selectedDb;
return View(data);
}
public async Task<IActionResult> GetWeatherData(string token)
{
var apiName = $"https://localhost:44338/weatherforecast";
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = await httpClient.GetAsync(apiName);
if (!response.IsSuccessStatusCode)
{
ViewBag.Error = response.StatusCode;
return View("Weatherdata");
}
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<WeatherForecast>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
return View("Weatherdata" , data);
}
}
}
Startup class for JWT.MVC is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Audience = "SecureApiUser";
options.Authority = "https://localhost:44318";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
Startup class for JWT.API is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//Copy from IS4
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Audience = "SecureApiUser";
options.Authority = "https://localhost:44318";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
//End
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.API", Version = "v1" });
});
}
Startup class for JWT.IDP is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//Configuration from AppSettings
services.Configure<JwtSettings>(Configuration.GetSection("JWT"));
//User Manager Service
services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<IdentityDbContext>();
services.AddScoped<IUserService, UserService>();
//Adding DB Context with MSSQL
services.AddDbContext<IdentityDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("IdentityDbConnectionString"),
b => b.MigrationsAssembly(typeof(IdentityDbContext).Assembly.FullName)));
//Adding Athentication - JWT
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.RequireHttpsMetadata = false;
o.SaveToken = false;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(Convert.ToInt32(Configuration["JWT:DurationInMinutes"])),
ValidIssuer = Configuration["JWT:Issuer"],
ValidAudience = Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"]))
};
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.IDP", Version = "v1" });
});
}
And the JWT Setting is as below
"JWT": {
"key": "C1CF4B7DC4C4175B6618DE4F55CA4",
"Issuer": "http://localhost:44318",
"Audience": "SecureApiUser",
"DurationInMinutes": 60
},
The short answer is
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", [token])
Keep "Bearer" as it is, it's just a constant. Replace [token], with the base 64 encoded token value returned to us from the OAuth protocol you are using.
I have a very simple code for the user to login as following:
[HttpPost]
[Route("login")]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
var user = await userManager.FindByEmailAsync(model.Email);
if (user != null && await userManager.CheckPasswordAsync(user, model.Password))
{
var userRoles = await userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
foreach (var userRole in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
}
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
var token = new JwtSecurityToken(
issuer: _configuration["JWT:ValidIssuer"],
audience: _configuration["JWT:ValidAudience"],
expires: DateTime.Now.AddHours(3),
claims: authClaims,
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
This sends me the token as expected
When I use the Token to send a request to a simple controller I get 401-Unauthorized
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet]
[Route("getme")]
public async Task<IActionResult> Get()
{
return Ok("Good");
}
}
This is my Startup.cs
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("ConnStr")));
// For Identity
services.AddIdentity<ApplicationUser, IdentityRole>(o =>
{
// configure identity options
o.Password.RequireDigit = false;
o.Password.RequireLowercase = false;
o.Password.RequireUppercase = false;
o.Password.RequireNonAlphanumeric = false;
o.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Adding Authentication
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
// Adding Jwt Bearer
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = Configuration["JWT:ValidAudience"],
ValidIssuer = Configuration["JWT:ValidIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))
};
});
and the following is my appsettings
I can verify the token at https://jwt.io/
For some reason when I send a request to TestController it always sends back an Unauthorized response. Any idea what am I doing wrong?
If this helps anyone, The issue was in my Startup file under Configure section I had UseAuthentication after UseAuthorization which cause the issue. The correct order as following fixed the issue
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseHttpsRedirection();
I use identity server 4 with blazor server side client
everything is ok but token not authorized api methods but token works in server authorized controllers
is something wrong with grant type or code flow ?
server config class :
public static class Configurations
{
public static IEnumerable<IdentityResource> GetIdentityResources() =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiResource> GetApis() =>
new List<ApiResource> {
new ApiResource("api1")
};
public static IEnumerable<ApiScope> GetApiScopes()
{
return new List<ApiScope>
{
// backward compat
new ApiScope("api1")
};
}
public static IEnumerable<Client> GetClients() => new List<Client>
{
new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.Code,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = {
"api1" ,
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
},
RedirectUris = { "https://localhost:44372/signin-oidc" },
AlwaysIncludeUserClaimsInIdToken = true,
AllowOfflineAccess = true,
RequireConsent = false,
RequirePkce = true,
}
};
}
server start up class :
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(config =>
{
config.UseInMemoryDatabase("Memory");
});
// AddIdentity registers the services
services.AddIdentity<IdentityUser, IdentityRole>(config =>
{
config.Password.RequiredLength = 4;
config.Password.RequireDigit = false;
config.Password.RequireNonAlphanumeric = false;
config.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(config =>
{
config.Cookie.Name = "IdentityServer.Cookie";
config.LoginPath = "/Auth/Login";
config.LogoutPath = "/Auth/Logout";
});
services.AddIdentityServer()
.AddAspNetIdentity<IdentityUser>()
//.AddInMemoryApiResources(Configurations.GetApis())
.AddInMemoryIdentityResources(Configurations.GetIdentityResources())
.AddInMemoryApiScopes(Configurations.GetApiScopes())
.AddInMemoryClients(Configurations.GetClients())
.AddDeveloperSigningCredential();
services.AddControllersWithViews();
}
api start up class :
services.AddAuthentication("Bearer").AddIdentityServerAuthentication(option =>
{
option.Authority = "https://localhost:44313";
option.RequireHttpsMetadata = false;
option.ApiName = "api1";
});
blazor server side start up class:
services.AddAuthentication(config =>
{
config.DefaultScheme = "Cookie";
config.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookie")
.AddOpenIdConnect("oidc", config =>
{
config.Authority = "https://localhost:44313/";
config.ClientId = "client";
config.ClientSecret = "secret";
config.SaveTokens = true;
config.ResponseType = "code";
config.SignedOutCallbackPath = "/";
config.Scope.Add("openid");
config.Scope.Add("api1");
config.Scope.Add("offline_access");
});
services.AddMvcCore(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser() // site-wide auth
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
To fix this issue, you have 2 options:
1- (Recommended) To add the scopes to API resource like this:
public static IEnumerable<ApiResource> GetApis() =>
new List<ApiResource> {
new ApiResource("api1")
{
Scopes = new []{ "api1" }
}
};
public static IEnumerable<ApiScope> GetApiScopes()
{
return new List<ApiScope>
{
// backward compat
new ApiScope("api1")
};
}
2- On API change your code to set ValidateAudience = false, like this:
services.AddAuthentication("Bearer").AddJwtBearer("Bearer",
options =>
{
options.Authority = "http://localhost:5000";
options.Audience = "api1";
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new
TokenValidationParameters()
{
ValidateAudience = false
};
});
Here is my blog about migrating IdentityServer4 to v4 https://nahidfa.com/posts/migrating-identityserver4-to-v4/
I have not actually used AddIdentityServerAuthentication in API but can you try the below code. Technically its same thing, but maybe this will work.
Change your api authentication from AddIdentityServerAuthentication to AddJwtBearer:
services.AddAuthentication("Bearer").AddIdentityServerAuthentication(option =>
{
option.Authority = "https://localhost:44313";
option.RequireHttpsMetadata = false;
option.ApiName = "api1";
});
to
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", option =>
{
option.Authority = "https://localhost:44313";
option.Audience = "api1";
option.SaveToken = true;
});
My setup: I've created and have running a WebAPI solution that performs the authentication of a username and password against a source (currently a db). This generates the JWT token and returns it to the requesting app (a ASP.NET Core 2.2 app).
Most solutions talk of securing the WebAPI exposed methods but my approach is to only do the authentication through WebAPI. The individual apps need to accept the token so they can determine authorization.
Now the question: what is the best approach to reading the token from the WebAPI (which I've done already), validating it, and then storing it for any/all controllers to know there is an authenticated user (via Authorize attribute) so long as the token is valid?
Debugging this more, it seems my token is not being added to the headers. I see this debug message:
Authorization failed for the request at filter 'Microsoft.AspNet.Mvc.Filters.AuthorizeFilter'
Code Update2 - code that gets the JWT:
var client = _httpClientFactory.CreateClient();
client.BaseAddress = new Uri(_configuration.GetSection("SecurityApi:Url").Value);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
//login
Task<HttpResponseMessage> response = ValidateUserAsync(client, username, password);
Task<Core.Identity.TokenViewModel> tokenResult = response.Result.Content.ReadAsAsync<Core.Identity.TokenViewModel>();
if (!response.Result.IsSuccessStatusCode)
{
if (tokenResult != null && tokenResult.Result != null)
{
ModelState.AddModelError("", tokenResult.Result.ReasonPhrase);
}
else
{
ModelState.AddModelError("", AppStrings.InvalidLoginError);
}
return View();
}
JwtSecurityToken token = new JwtSecurityToken(tokenResult.Result.Token);
int userId;
if (int.TryParse(token.Claims.First(s => s.Type == JwtRegisteredClaimNames.NameId).Value, out userId))
{
//load app claims
Core.Identity.UserInfo userInfo = Core.Identity.UserLogin.GetUser(_identityCtx, userId);
Core.Identity.UserStore uStore = new Core.Identity.UserStore(_identityCtx);
IList<Claim> claims = uStore.GetClaimsAsync(userInfo, new System.Threading.CancellationToken(false)).Result;
claims.Add(new Claim(Core.Identity.PowerFleetClaims.PowerFleetBaseClaim, Core.Identity.PowerFleetClaims.BaseUri));
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(claimsIdentity);
//complete
AuthenticationProperties authProperties = new AuthenticationProperties();
authProperties.ExpiresUtc = token.ValidTo;
authProperties.AllowRefresh = false;
authProperties.IsPersistent = true;
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, tokenResult.Result.Token);
//var stuff = HttpContext.SignInAsync(JwtBearerDefaults.AuthenticationScheme, principal, authProperties);
}
else
{
ModelState.AddModelError("", AppStrings.InvalidLoginError);
return View();
}
return RedirectToAction("Index", "Home");
Startup:
private void ConfigureIdentityServices(IServiceCollection services)
{
services.ConfigureApplicationCookie(options => options.LoginPath = "/Login");
//authentication token
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddCookie(opt =>
{
opt.LoginPath = "/Login";
opt.LogoutPath = "/Login/Logoff";
opt.Cookie.Name = Configuration.GetSection("SecurityApi:CookieName").Value;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidAudience = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetSection("SecurityApi:Key").Value)),
ValidateLifetime = true
};
});
Core.Startup authStart = new Core.Startup(this.Configuration);
authStart.ConfigureAuthorizationServices(services);
}
Auth:
public void ConfigureAuthorizationServices(IServiceCollection services)
{
services.AddDbContext<Identity.IdentityContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SecurityConn")));
services.AddScoped<DbContext, Identity.IdentityContext>(f =>
{
return f.GetService<Identity.IdentityContext>();
});
services.AddIdentityCore<Identity.UserInfo>().AddEntityFrameworkStores<Identity.IdentityContext>().AddRoles<Identity.Role>();
services.AddTransient<IUserClaimStore<Core.Identity.UserInfo>, Core.Identity.UserStore>();
services.AddTransient<IUserRoleStore<Core.Identity.UserInfo>, Core.Identity.UserStore>();
services.AddTransient<IRoleStore<Core.Identity.Role>, Core.Identity.RoleStore>();
services.AddAuthorization(auth =>
{
auth.AddPolicy(JwtBearerDefaults.AuthenticationScheme, new AuthorizationPolicyBuilder().AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build());
auth.AddPolicy(PFBaseClaim, policy => policy.RequireClaim(Identity.PFClaims.BaseUri));
});
}
In the end, my approach was to use a secure cookie and a base claim to prove the user authenticated.
private void ConfigureAuthentication(IServiceCollection services)
{
services.ConfigureApplicationCookie(options => options.LoginPath = "/Login");
//authentication token
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt =>
{
opt.LoginPath = "/Login";
opt.AccessDeniedPath = "/Login";
opt.LogoutPath = "/Login/Logoff";
opt.Cookie.Name = Configuration.GetSection("SecurityApi:CookieName").Value;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidAudience = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetSection("SecurityApi:Key").Value)),
ValidateLifetime = true
};
});
}
And at login:
AuthenticationProperties authProperties = new AuthenticationProperties();
authProperties.ExpiresUtc = token.ValidTo;
authProperties.AllowRefresh = false;
authProperties.IsPersistent = true;
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, userStore.CreateAsync(user).Result, authProperties);
return RedirectToAction("Index", "Home");