Json web token scheme problem with webapi - asp.net-web-api

So I'm adding JWT authentication for a webapi. Everything works fine until I messed around with the scheme settings. Somehow the following settings dont work any more
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
//TODO
var userMachine = context.HttpContext.RequestServices.GetRequiredService<UserManager<User>>();
var user = userMachine.GetUserAsync(context.HttpContext.User);
if (user == null)
context.Fail("Unauthorized");
return Task.CompletedTask;
}
};
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
The problem is this scheme line of code
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
when I try to call a api method it somehow returns 404 instead of 401
But if I overwrite this scheme in the controller like this
[Route("api/[controller]")]
[ApiController]
[Authorize(AuthenticationSchemes = AuthSchemes)]
public class ValuesController : ControllerBase
{
private const string AuthSchemes = JwtBearerDefaults.AuthenticationScheme;
Wether I commented the previous scheme line of code or not the program runs.
And if I call a api with out the JWT is returns 401 now and everything works.
Anybody knows why? Thank you!
PS. I didn't add an identity. Instead I added identity manually from scratch. I noticed that the problem happens after I create a new custom user inheriting from IdentityUser and add an migration. So basically it happens after I add these code below:
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireNonAlphanumeric = false;
}).AddEntityFrameworkStores<ApplicationContext>();
in program.cs
and
public class ApplicationContext : IdentityDbContext<ApplicationUser>
{
public ApplicationContext(DbContextOptions<ApplicationContext> options):base(options)
{
}
}
in Dbcontext.
Seems like for it to work I have to use the built-in identity. But the question is why it doesnt work if I do it manually?

Related

What am I missing? ASP.NET Core 6 keycloak integration, authentication fails after successful login

Here is what I did: using my local keycloak server (thru docker), I created a realm, users, role and client with this setup :
I set up credentials and got secret key and stuff and that's it, I haven't set anything, no mappers, client scope, etc.
I did this as our other applications that is using other languages such as PHP or nodejs have similar settings.
services.AddAuthentication(options =>
{
//Sets cookie authentication scheme
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(cookie =>
{
//Sets the cookie name and maxage, so the cookie is invalidated.
cookie.Cookie.Name = "keycloak.cookie";
cookie.Cookie.MaxAge = TimeSpan.FromMinutes(60);
cookie.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
cookie.SlidingExpiration = true;
})
.AddOpenIdConnect(options =>
{
//Use default signin scheme
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
//Keycloak server
options.Authority = Configuration.GetSection("Keycloak")["ServerRealm"];
//Keycloak client ID
options.ClientId = Configuration.GetSection("Keycloak")["ClientId"];
//Keycloak client secret
options.ClientSecret = Configuration.GetSection("Keycloak")["ClientSecret"];
//Keycloak .wellknown config origin to fetch config
// options.MetadataAddress = Configuration.GetSection("Keycloak")["Metadata"];
//Require keycloak to use SSL
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
//Save the token
options.SaveTokens = true;
//Token response type, will sometimes need to be changed to IdToken, depending on config.
options.ResponseType = OpenIdConnectResponseType.Code;
//SameSite is needed for Chrome/Firefox, as they will give http error 500 back, if not set to unspecified.
options.NonceCookie.SameSite = SameSiteMode.None;
options.CorrelationCookie.SameSite = SameSiteMode.None;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "https://schemas.scopic.com/roles"
};
Configuration.Bind("<Json Config Filter>", options);
options.Events.OnRedirectToIdentityProvider = async context =>
{
context.ProtocolMessage.RedirectUri = "http://localhost:13636/home";
await Task.FromResult(0);
};
});
Then I created a fresh ASP.NET Core MVC application and setup the OpenId options like so
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
My HomeController looks like this:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
bool value = User.Identity.IsAuthenticated;
return View();
}
[Authorize]
public IActionResult Privacy()
{
return View();
}
}
When I access localhost:13636/Privacy to test, the Keycloak login page is triggered which is correct, but after successful login and a redirect to /home, User.Identity.IsAuthenticated is false and it seems like the application doesn't know that authentication has been successful.
What needs to be done after this?
Or am I missing some configuration/settings/options?
Summary of what I did
Setup keycloak dashboard (created Realm, client, user and roles)
Setup a simple ASP.NET Core MVC application, pass openid options and controller.
The keycloak login page is triggered but authentication fail during test
Try to add to the AddCookie handler the following setting:
options.Cookie.SameSite = SameSiteMode.None;
To make sure the cookies are set with SameSite=none.

_userManager.FindByEmailAsync(User.FindFirstValue(ClaimTypes.Email)) returns null

I am having an issue with Claims not populating with ClaimsPrinciple after creating a JWT. I am using ASP.NET Core 6 on VS 2022. The issue raised after configuring identity to include Roles and RolesUsers. I had no issues prior to including these 2 identity tables from the automated generated ones from IdentityModel.
now on creation, I show no errors and receive the JWT token without any issues, but afterwards when I try to authorize the user that log in the ClaimIdentity does not propagate and errors on _userManager.FindByEmailAsync(User.FindFirstValue(ClaimTypes.Email)) showing null.
Here is some code to show the current state of the project.
First is the Method that handles the validation for login users.
[Authorize]
[HttpGet]
public async Task<ActionResult<UserDto>> GetCurrentUser()
{
// Null Exception Error
var user = await _userManager.FindByEmailAsync(User.FindFirstValue(ClaimTypes.Email));
return CreateUserObject(user);
}
UserDto CreateUserObject( AppUser user )
{
return new UserDto
{
DisplayName = user.DisplayName,
Image = null,
Token = _tokenService.CreateToken(user),
Username = user.UserName
};
}
This is my Token Service that handles creating the JWT token from users that Register or Login.
public class TokenService
{
private readonly IConfiguration _config;
public TokenService(IConfiguration config)
{
_config = config;
}
public string CreateToken(AppUser user)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Email, user.Email)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["TokenKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(7.0),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
This is what I changed prior to having this issue in my IdentityServiceExtension Class.
public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
{
services.AddIdentity<AppUser, AppRole>(opt => //Changed AddIdentityCore to AddIdentity to apply AppUser & AppRole
{
opt.Password.RequireNonAlphanumeric = false;
})
.AddEntityFrameworkStores<DataContext>()
.AddSignInManager<SignInManager<AppUser>>()
.AddRoleManager<RoleManager<AppRole>>(); //Added Role Manager for Roles to loaded.
var Key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"]));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = Key,
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddScoped<TokenService>();
// Added Roles to Policy
services.AddAuthorization(opt =>
{
opt.AddPolicy("Verified", pol =>
pol.RequireRole("User", "Staff", "Admin", "Guest"));
opt.AddPolicy("Restricted", pol =>
pol.RequireRole("User", "Staff", "Admin"));
opt.AddPolicy("EmployeeAccess", pol =>
pol.RequireRole("Staff", "Admin"));
opt.AddPolicy("ManagerAccess", pol =>
pol.RequireRole("Admin"));
});
//////////////////////
return services;
}
Hopefully this is enough information to help me with this issue. I have searched all over online and the resolutions I have seen does not match to my particular issue to solve the problem.
I surprisingly found the issue, so the reason I was having errors was due to not configuring Identity to handle all Identity Models. Prior to my change, I only handled users, but by adding roles and roleusers I had to handle all of Identity Model to prevent losing the claims. Due to this fact, I had to install another Microsoft Package,Microsoft.AspNetCore.Identity.UI, to gain access to the Identity Helper Method (.AddDefaultIdentity()) to configure the generated identity tables. Once added, Identity was fully configured and the issue was resolved. I hope anyone else that need help can use this as a possible solution.

jwt token in DOTNET WebApi

I literally read the internet through, tried multiple approaches and so on - with no luck.
I am trying to make a pretty small WebApi project with a few controllers protected by a jwt token.
I generate the token using following code:
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("may the force"));
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims.ToArray()),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
I have setup the authentication (in Startup.cs) bits like
services.AddAuthentication(x => {
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}
)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("may the force"))
};
});
Using my AuthController I can generate the Token(first code block in this question) - and the Controllers needed to be "protected" is annotated with [Authorize]
If I take the result from AuthController and paste it at https://jwt.io the signature is valid.
I am testing the controllers using postman and setting the request header Authorization to Bearer <the token>
Can anyone point me in the right direction?
Thanks alot!
UPDATE: I misused the jwt.io site initially, thus some of the comments underneath here are no longer valid.
UPDATE 2: Realized that the server tried to use cookie based authentication. Changing the annotation to [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] made the trick - but still not sure why

Is possible to protect scope (web api) and authenticate client (web app mvc) in same project?

Good morning,
I need to have in same project both web api and web app mvc.
Web api has to be protected via bearer token and web app mvc has to be authenticated via identity server.
Is it possible protecting a scope and a client in same project?
I think I have to do something like this in startup
//this to protect scope api1
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5000/";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
});
//this to authenticate mvc client
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
options.AccessDeniedPath = "/account/denied";
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000",
options.RequireHttpsMetadata = false;
options.ResponseType = "id_token token";
options.ClientId = "mvc-implicit";
options.SaveTokens = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("api1");
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapJsonKey("role", "role", "role");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
Now, I have to call my Api1 using client_credential with an external client.
But it returns me at login page.
Is it possible to do what I want?
Protected WebApi and Authenticated MVC client in same project?
Now, I have to call my Api1 using client_credential with an external client. But it returns me at login page.
That seems you misunderstand the scenario . Your MVC application is client also is a resource application which protected by Identity Server (in Config.cs):
public static IEnumerable<ApiResource> GetApis()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
};
}
I assume you have api controller in your MVC application :
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET: api/Values
[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
And you have config to protect the api actions by using AddJwtBearer :
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5000/";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
});
That means any request to access the Get action should have an authentication bearer header with access token append , the access token is issued by your Identity Server(endpoint is http://localhost:5000/) and the audience is api1 .
Now your another client could use client credential flow to acquire access token to access your web application :
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
});
And call your protected actions :
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("http://localhost:64146/api/values");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
So it won't redirect to login page , since client credential in fact is sending HTTP POST request to get access token with app's credential . There is no login page in this scenario .

IdentityServer4 and .netcore WebApp/WebAPI cookie authentication/authorization

I have Three application viz(IdentityServer4 App, .Net Core2.0 WebApp, .NetCore2.0 WebAPI)
When I open the webapp if its un-authenticated, It gets navigated to identity server where I supply the credentials. After successful authentication it navigates back to webapp with the required cookies in place. Things are fine till here.
Now within webapp I am making calls to webapi (with cookies set by identity server in webapp) but each time it returns as 401 unauthorized.
Code sample in webapp:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o =>
{
o.Cookie.Name = Config.CookieName;
o.Cookie.SameSite = SameSiteMode.None;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = Config.IdentityUrl;
options.RequireHttpsMetadata = false;
options.ClientId = Config.ClientId;
options.SaveTokens = true;
});
And Code sample used in WebAPI in configure service method ConfigureServices:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o => {
o.Cookie.Name = Config.CookieName;
o.Cookie.SameSite = SameSiteMode.None;
o.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = redirectContext =>
{
redirectContext.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
}
};
})
.AddIdentityServerAuthentication(options =>
{
options.Authority = Config.IdentityUrl;
options.RequireHttpsMetadata = false;
options.ApiName = Config.ApiName;
});
also I have app.UseAuthentication() method in Configure method
What I get a feeling of it has to do with something session-id may be. If so it the case please help if not then what you could make out as not doing right please help.
I traced log it shows just following thing in there:
Cookie was not authenticated. Failure Message: Unprotect ticket failed.
Authentication Cookie was chanllenged.
Any help would be appreciated.
Here is the magical line of code.Added in
ConfigureServices
method before
services.AddAuthentication
This was reason because of which cookie was not getting validated.
services.AddDataProtection().PersistKeysToFileSystem(PersistKeysLocation.GetKeyRingDirInfo())
.SetApplicationName(Config.ApplicationName);

Resources