How do I allow specific role to access the endpoint? - c#

Trying to restrict access to an endpoint only for users with the role "Manager".
when I open swagger, I use Login endpoint to login(it's successful) and then I try to run 'AddCategory' endpoint and response is 404.
[Authorize(Roles = "Manager")]
[HttpPost("addCategory")]
[SwaggerResponse((int)HttpStatusCode.OK, type: null)]
[SwaggerResponse((int)HttpStatusCode.Unauthorized)]
[SwaggerResponse((int)HttpStatusCode.Forbidden)]
[SwaggerResponse((int)HttpStatusCode.InternalServerError)]
public Category AddCategory([FromBody] Category category)
{
context.Categories.Add(category);
context.SaveChanges();
return category;
}
Same happens if I do not log in. If I remove Authorize attribute, everything works fine.
I checked UserRoles table and user I'm logging in with is Manager. Also tried to put only [Authorize] attribute instead of role specific and still didn't work. In Swagger, Login endpoint returns 200 response but is there any other way to check if I'm logged in or not?
EDIT
This is how I Generate JwtToken on Login.
var roleId = _appDbContext.UserRoles.Where(x => x.UserId == user.Id)
.Select(x=>x.RoleId)
.FirstOrDefault();
var role = _appDbContext.Roles.Where(x=>x.Id==roleId).FirstOrDefault();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("Id", user.Id),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Role, role.Name)
}),
Expires = DateTime.UtcNow.AddSeconds(30), // 5-10
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
Decoded token looks like this
"alg": "HS256",
"typ": "JWT"
}.{
"Id": "3f4b1e8f-d15c-426e-9aa6-375ac53f27ae",
"email": "manager#example.com",
"sub": "manager#example.com",
"jti": "6e1fb89e-5408-4c61-8258-8d2c7cead4ab",
"role": "Manager",
"nbf": 1658758635,
"exp": 1658758665,
"iat": 1658758635
}.[Signature]
However, I've never added anything in [AspNetRoleClaims] table. Should I add data to that table too?
EDIT :
This is Program.cs
var key = Encoding.ASCII.GetBytes("llvudfvkwvepwkdnsnwmuulyvtrawppf");
var tokenValidationParams = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
RequireExpirationTime = false,
ClockSkew = TimeSpan.Zero
};
builder.Services.AddSingleton(tokenValidationParams);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt =>
{
jwt.SaveToken = true;
jwt.TokenValidationParameters = tokenValidationParams;
});
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Add app.UseAuthentication(); before app.UseAuthorization(); in program.cs file

Related

What is the Problems of my Code in Claim base Authorization ASP.NET CORE WEB API?

I want to use claim base Authorization in ASP.NET CORE Web API, I think my code is true but, it does not work for me and gives me the error 403 forbidden however I use the Right Token which has the right claim value in my AspNetUserClaims table
here is my Program.cs code
//Get Connection String
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseNpgsql(builder.Configuration["connection:connectionString"]));
builder.Services.Configure<ApplicationSettings>(builder.Configuration.GetSection("ApplicationSettings"));
builder.Services.AddMvc();
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<AppDbContext>();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("VIEW",
policy => policy.RequireClaim("VIEW"));
});
builder.Services.Configure<IdentityOptions>(options =>
options.User.RequireUniqueEmail = true);
//JWT Token Setup
var key = Encoding.UTF8.GetBytes(builder.Configuration["ApplicationSettings:JWT_Secret"]);
builder.Services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x => {
x.RequireHttpsMetadata = false;
x.SaveToken = false;
x.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
};
});
here is my UsersLoginController when the user login, this controller generates the Token after successful login
[HttpPost]
[Route("Login")]
public async Task<IActionResult> Login(LoginModel model)
{
var user = await _userManager.FindByNameAsync(model.UserName);
if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))
{
//Get role assigned to the user
var role = await _userManager.GetRolesAsync(user);
IdentityOptions _options = new IdentityOptions();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim("UserID",user.Id.ToString()),
new Claim(_options.ClaimsIdentity.RoleClaimType,role.FirstOrDefault())
}),
Expires = DateTime.UtcNow.AddDays(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_appSettings.JWT_Secret)), SecurityAlgorithms.HmacSha256Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(securityToken);
return Ok(new { token });
}
else
return BadRequest(new { message = "Username or password is incorrect." });
}
}
here is my UsersProfileDetailsController
[HttpGet]
[Authorize(Policy = "VIEW")]
public async Task<Object> GetUserProfile()
{
string userId = User.Claims.First(c => c.Type == "UserID").Value;
var user = await _userManager.FindByIdAsync(userId);
var role = await _userManager.GetRolesAsync(user);
return new
{
user.Id,
user.FirstName,
user.LastName,
user.Email,
user.UserName,
user.PhotoPath,
role,
};
}
Now when I login with Admin User which has the right Claimvalue VIEW in the Table AspNetUserClaims, it generates the Token for me but when I use this Token to access UserDetails from UsersProfileDetailsController it gives me the code 403 forbidden.
Can anyone help me with how to implement Claims base Authorization and what are my problems?
I use Postman for testing.
because you configured authoriztion as below:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("VIEW",
policy => policy.RequireClaim("VIEW"));
});
If you Press F12 and go to definition,you'll find the explaination of
the method
you added [Authorize(Policy = "VIEW")] on your controller, if you want authorize successfully, when you generate the token,you have to add the claim VIEW
Subject = new ClaimsIdentity(new Claim[]
{
new Claim("UserID",user.Id.ToString()),
//add a new claim named View here
new Claim("View",.......),
new Claim(_options.ClaimsIdentity.RoleClaimType,role.FirstOrDefault())
}),

JWT Token Accessing AuthenticatedUser

I'm trying to access the user Id from the token but everything I try returns null. The generated token has the necessary information so I don't think it's the token generation.
This is the part creates the token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_jwtSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor()
{
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(ClaimTypes.NameIdentifier, existingAppUser.Id),
new Claim("id", existingAppUser.Id),
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return new AuthenticationResult()
{
Token = tokenHandler.WriteToken(token)
};
When I decode the generated token I can see all of the claims in the token but I can't access it on the project.
This is the part trying to access the name identifier or the id claims
var claimsList = _httpContextAccessor.HttpContext.User.Claims.ToList();
var identityName = _httpContextAccessor.HttpContext.User.Identity.Name;
var nameId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var id = _httpContextAccessor.HttpContext.User.FindFirst(x => x.Type == "id")?.Value;
This is the JWT Configuration from Startup
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.SaveToken = true;
x.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuerSigningKey = false,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.Secret)),
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuer = false
};
});
services.AddAuthorization();
services.AddHttpContextAccessor();
This is the class I'm trying to access it from
public class CurrentUserService : ICurrentUserService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CurrentUserService( IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string UserId { get => _httpContextAccessor.HttpContext.User.Claims.Single(x => x.Type == "id").Value; }
public string GetUserId()
{
var claimsList = _httpContextAccessor.HttpContext.User.Claims.ToList();
var identityName = _httpContextAccessor.HttpContext.User.Identity.Name;
var nameId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var id = _httpContextAccessor.HttpContext.User.FindFirst(x => x.Type == "id")?.Value;
return "123";
}
}
I don't know what I am missing here. How do I get the userId from the token?
Well It turns out I forgot to put
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
in the necessary controllers.

Invalid jwt Token in asp.net core

I am trying to use a JWT in my Angular application and ASP.net core. For starters, I am using "Postman" to test my end points.
In my API this is how I set up the JWT
launchsettings.json
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:22468",
"sslPort": 0
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"ancmHostingModel": "InProcess"
},
"autosweeprfid_api": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5000"
}
}
}
appsettings.json
"Jwt": {
"SecretKey": "KqcL7s998JrfFHRP1",
"Issuer": "http://localhost:22468",
"Audience": "http://localhost:4201"
}
startup.cs => ConfigureServices
// Get the Validators in appsettings
var validIssuer = Configuration["Jwt:Issuer"];
var validAudience = Configuration["Jwt:Audience"];
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = validIssuer,
ValidAudience = validAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecretKey"]))
};
});
startup => Configure
app.UseCors("MyPolicy");
// Make the "PrivateImages" forlder servable
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), #"PrivateImages")),
RequestPath = new PathString("/PrivateImages")
});
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<MessageHub>("/MessageHub");
});
AuthorizationController
[HttpPost, Route("login")]
public IActionResult Login([FromBody] LoginModelDto model)
{
IActionResult response = Unauthorized();
User user = context.Users.SingleOrDefault(x => x.UserName == model.UserName);
if (user == null) return response;
if (!user.IsActive) return response;
var decryptedPassword = Decryption.Decrypt(user.Password, user.SaltValue);
if (decryptedPassword == model.Password)
{
var roles = (from a in context.UserRoles
join b in context.Roles on a.RoleId equals b.Id into ab
from x in ab.DefaultIfEmpty()
where a.UserId == user.Id
select new Role
{
RoleName = x.RoleName,
}).ToList();
var claims = new List<Claim>
{
new Claim(Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames.Sub, user.UserName),
new Claim("UserId", user.Id.ToString()),
new Claim("FirstName", user.FirstName.ToString()),
new Claim("MiddleName", user.MiddleName.ToString()),
new Claim("LastName", user.LastName.ToString()),
new Claim("EmailAddress", user.EmailAddress.ToString()),
new Claim(Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
foreach (var role in roles)
{
//claims.Add(new Claim(ClaimTypes.Role, role.RoleName));
claims.Add(new Claim("role", role.RoleName));
}
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"]));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokeOptions = new JwtSecurityToken(
issuer: "http://localhost:22468",
audience: "http://localhost:4201",
claims: claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
user.Online = true;
context.SaveChanges();
return Ok(new { Token = tokenString });
}
else
{
return response;
}
}
I can login just fine, but when I try to consume a method that has an attribute of [Authorize], Postman gives me a 401.
You have just configured authorization inside of your pipeline. You need to configure authentication as well. And remember it has to be done prior to authorization inside of the pipeline. Order matters because first we need to authenticate on who the user is and then we need to check what permissions he/she has.
app.UseRouting();
app.UseAuthentication(); //Authentication
app.UseAuthorization(); //Authorization
I got it. All codes are correct. I just missed using the "authentication" itself. So I just placed the line app.UseAuthentication(); between app.UseRouting(); and app.UseAuthorization(); and it went good. Thank you.

.NET core JWT authorization failed?

I'm trying to get JWT working here, the token is successfully received on my client end after login but when I request user info at the /info route, the authorization fails. Any help would be much appreciated, thanks in advance.
I get the error:
Route matched with {action = "GetInfo", controller = "Accounts", page = ""}. Executing controller action with signature System.Threading.Tasks.Task`1[ProjectConker.Controllers.AccountsInfo] GetInfo() on controller ProjectConker.Controllers.AccountsController (ProjectConker).
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
This is where the token is issued.
[HttpPost("login")]
public async Task<IActionResult> Post([FromBody]LoginInfo credentials)
{
if (credentials == null)
{
return BadRequest("Invalid client request");
}
var user = await UserManager.FindByNameAsync(credentials.Username);
await SignInManager.SignInAsync(user, isPersistent: false);
var result = await SignInManager.PasswordSignInAsync(user,
credentials.Password, isPersistent: false, lockoutOnFailure: false);
if (result.Succeeded)
{
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("**********"));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokeOptions = new JwtSecurityToken(
issuer: "http://localhost:5000",
audience: "http://localhost:5000",
claims: new List<Claim>(){
new Claim("username", credentials.Username)
},
expires: DateTime.Now.AddMinutes(5),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
return Ok(new { Token = tokenString, UserName = user.UserName });
}
else
{
return Unauthorized();
}
}
Save token to local storage
public Login(loginForm : ILoginForm) : Observable<ILoginForm>
{
return this.http.post<ILoginForm>(this.accountsUrl + "/login", loginForm, httpOptions)
.pipe(map<any, any>((data, index) => {
localStorage.setItem("auth_token", data.token);
this.username = data.username;
this.loggedIn = true;
console.log(data);
return data;
}));
}
Gets user information
public GetAccountInfo() : Observable<any>
{
httpOptions.headers.set('Authorization', localStorage.getItem('auth_token'));
return this.http.get(this.accountsUrl + "/info", httpOptions);
}
returns user info, but authorization fails here
[HttpGet]
[Route("info")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<AccountsInfo> GetInfo()
{
var usernameClaim = User.Claims.SingleOrDefault(c => c.Type == "username");
Console.WriteLine(usernameClaim.Value, ConsoleColor.Red);
var user = await UserManager.FindByNameAsync(usernameClaim.Value);
return new AccountsInfo{ DisplayName = user.UserName };
}
My startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "http://localhost:5000",
ValidAudience = "http://localhost:5000",
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("superSecretKey#345"))
};
});
//services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.AddHttpClient();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddCors(options => options.AddPolicy("CorsPolicy",
builder =>
{
builder.AllowAnyMethod().AllowAnyHeader()
.WithOrigins("*")
.AllowCredentials();
}));
services.AddSignalR();
services.AddEntityFrameworkSqlServer();
services.AddDbContext<ConkerDbContext>(
options => options.UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll));
services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<ConkerDbContext>();
services.AddScoped<SearchEngine>();
services.AddTransient<RoadmapService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
// app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseAuthentication();
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/api/chat");
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
Since Angular's HttpHeaders are immutable, you can't just use httpOptions.headers.set('Authorization', localStorage.getItem('auth_token'));, because it would have no effect on the original object.
First thing is the header is invalid, use the Bearer method provided by Ashique, then your GetAccountInfo call would look like this:
public GetAccountInfo() : Observable<any> {
const headers = new HttpHeaders({'Authorization': 'Bearer ' + localStorage.getItem('auth_token')});
return this.http.get(this.accountsUrl + "/info", {headers});
}
Here I assumed that you don't have other HttpOptions set, so I'm just passing the header to the HttpClient. Try it this way and let us know if it's still not working.
The default way to add authorization header in HTTP request for ASP.NET core token authentication is to add Bearer before the token. So the code should be like this-
httpOptions.headers.set('Authorization', "Bearer " + localStorage.getItem('auth_token'));
You can override the default behavior to remove the need of Bearer.
Please read the below post to help understand the reason for using bearer before token.
https://www.quora.com/Why-is-Bearer-required-before-the-token-in-Authorization-header-in-a-HTTP-request
Also try this,
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(1),
SigningCredentials = creds,
IssuedAt = DateTime.Now,
NotBefore = DateTime.Now
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
What i mean is instead of creating a new JWTSecurityToken, Create a SecuritTokenDescriptor, instead of using function WriteToken, use CreateToken. I have used JWT in this way and it worked.
In the login end point where you create the token you are using key "**********" and in the Setup class you are using key "superSecretKey#345" , this is the problem , the authentication middleware is trying to validate the incoming JWT tokens with key different from the key used to issue the token , you have to use the same key for both issuing and validating , also put the key in somewhere else like "appsettings" to avoid this conflict
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("**********"));
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("superSecretKey#345"))

Identityserver4 and API in single project

I have an IdentityServer4 asp.net-core host setup for Resource Owner Password Grant using JWT Bearer tokens and an API in a separate asp.net-core host which has my API and two Angular clients.
The Authentication and Authorization is working from my two Angular clients to my API.
Now I need to expose an API in the IdentityServer4 host so I can create users from one of the Angular clients.
I have copied my Authentication and Authorization setup from my API over to my IdentityServer4 host, however, I cannot get it to Authenticate.
In the below code, within the API, I can set a breakpoint on the jwt.Authority... line and the first call will trigger this breakpoint in my API but not in the IdentityServer4 host.
Authentication
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(jwt =>
{
jwt.Authority = config.Authentication.Authority; //Breakpoint here
jwt.RequireHttpsMetadata = config.Authentication.RequireHttpsMetadata;
jwt.Audience = Common.Authorization.Settings.ServerApiName;
});
Authorization
I'm not sure if it's relevant, but I'm using role based authorization, the following is the setup for this.
var authPolicyBuilder = new AuthorizationPolicyBuilder()
.RequireRole(Common.Authorization.Settings.ServerApiRoleBasePolicyName)
.Build();
services.AddMvc(options =>
{
options.Filters.Add(new AuthorizeFilter(authPolicyBuilder));
...
services.AddAuthorization(options =>
{
options.AddPolicy(Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName, policy =>
{
policy.RequireClaim("role", Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName);
I've extracted the following from my logging:
What I see is that in the non-working case, I never get to the point of invoking the JWT validation (#3 in the working logs).
This is just a tiny extract of my logs, I can share them in entirety if needs be.
Working
1 Request starting HTTP/1.1 GET http://localhost:5100/packages/
(SourceContext:Microsoft.AspNetCore.Hosting.Internal.WebHost)
2 Connection id "0HLC8PLQH2NRU" started.
(SourceContext:Microsoft.AspNetCore.Server.Kestrel)
3 Request starting HTTP/1.1 GET http://localhost:5000/.well-known/openid-configuration
(SourceContext:Microsoft.AspNetCore.Hosting.Internal.WebHost)
--Truncated--
Not Working
1 Request starting HTTP/1.1 GET http://localhost:5000/users
(SourceContext:Microsoft.AspNetCore.Hosting.Internal.WebHost)
--Truncated--
Clients
new Client
{
ClientId = "setup_app",
ClientName = "Setup App",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = 3600,
IdentityTokenLifetime = 3600,
UpdateAccessTokenClaimsOnRefresh = true,
SlidingRefreshTokenLifetime = 3600,
AllowOfflineAccess = false,
RefreshTokenExpiration = TokenExpiration.Absolute,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
AlwaysSendClientClaims = true,
Enabled = true,
RequireConsent = false,
AlwaysIncludeUserClaimsInIdToken = true,
AllowedCorsOrigins = { config.CorsOriginSetupClient },
ClientSecrets =
{
new Secret(Common.Authorization.Settings.ServerApiSetupClientSecret.Sha256())
},
AllowedScopes =
{
Common.Authorization.Settings.ServerApiName,
}
},
new Client
{
ClientId = "client_app",
ClientName = "Client App",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = 3600,
IdentityTokenLifetime = 3600,
UpdateAccessTokenClaimsOnRefresh = true,
SlidingRefreshTokenLifetime = 3600,
AllowOfflineAccess = false,
RefreshTokenExpiration = TokenExpiration.Absolute,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
AlwaysSendClientClaims = true,
Enabled = true,
RequireConsent = false,
AlwaysIncludeUserClaimsInIdToken = true,
AllowedCorsOrigins = { config.CorsOriginSetupClient },
ClientSecrets =
{
new Secret(Common.Authorization.Settings.ServerApiAppClientSecret.Sha256())
},
AllowedScopes =
{
Common.Authorization.Settings.ServerApiName,
}
}
IdentityResources
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource(Common.Authorization.Settings.ServerApiScopeName, new []{
"role",
Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName,
Common.Authorization.Settings.ServerApiAppClientAdminRolePolicyName,
Common.Authorization.Settings.ServerApiAppClientUserRolePolicyName,
}),
};
User
var adminUser = new ApplicationUser
{
UserName = "admin",
Email = "admin#noreply",
};
adminUser.Claims = new List<IdentityUserClaim>
{
new IdentityUserClaim(new Claim(JwtClaimTypes.PreferredUserName, adminUser.UserName)),
new IdentityUserClaim(new Claim(JwtClaimTypes.Email, adminUser.Email)),
new IdentityUserClaim(new Claim("role", Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName)),
new IdentityUserClaim(new Claim("role", Common.Authorization.Settings.ServerApiRoleBasePolicyName)),
new IdentityUserClaim(new Claim("profileImage", $"https://robohash.org/{Convert.ToBase64String(System.Security.Cryptography.MD5.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(adminUser.UserName)))}?set=set2"))
};
adminUser.AddRole(Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName);
API
new ApiResource(Common.Authorization.Settings.ServerApiName, "Server API"){
ApiSecrets =
{
new Secret(Common.Authorization.Settings.ServerApiAppClientSecret.Sha256())
},
},
Look up here https://github.com/IdentityServer/IdentityServer4.Samples
Seems like it should be like:
Authentication:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = config.Authentication.Authority;
options.RequireHttpsMetadata = false;
options.ApiName = ServerApiName;
options.ApiSecret = ServerApiAppClientSecret;
});
Or with JWT you can try like:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = config.Authentication.Authority;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudiences = new[]
{
$"{config.Authentication.Authority}/resources",
ServerApiName
},
};
});
Also, you will able to add authorization policy, like:
Authorization:
services.AddMvc(opt =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireScope("api").Build();
opt.Filters.Add(new AuthorizeFilter(policy));
})
There is a MS out of the box service designed to add support for local APIs
First in IDS4 startup.cs, ConfigureServices add
services.AddLocalApiAuthentication();
Then in IDS4 config.cs in your client declaration
AllowedScopes = {
IdentityServerConstants.LocalApi.ScopeName, <<<< ---- This is for IDS4 Api access
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email
...
Still in config add the new scope to ApiScopes
new ApiScope(IdentityServerConstants.LocalApi.ScopeName)
Note
IdentityServerConstants.LocalApi.ScopeName resolves to 'IdentityServerApi'
Now in your shiny new Api located in the IDS4 project add the authorize tag to your api endpoint
[Authorize(IdentityServerConstants.LocalApi.PolicyName)]
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
Finally you need to request this new scope in your Client
Scope = "openid sig1 api1 profile email offline_access company IdentityServerApi",
Thats it
I think this could help, it's included in the community samples.

Categories

Resources