Merge pull request #346 from meysamhadeli/feat/add-support-role-base-policy

feat: add support role base authorization policy
This commit is contained in:
Meysam Hadeli 2025-05-12 00:47:16 +03:30 committed by GitHub
commit dedf6086fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 242 additions and 155 deletions

View File

@ -28,8 +28,7 @@
},
"Jwt": {
"Authority": "https://localhost:4000",
"Audience": "booking-monolith",
"RequireHttpsMetadata": false
"Audience": "booking-monolith"
},
"HealthOptions": {
"Enabled": false

View File

@ -1,6 +1,7 @@
using BookingMonolith.Identity.Identities.Constants;
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
using IdentityModel;
namespace BookingMonolith.Identity.Configurations;
@ -11,10 +12,7 @@ public static class Config
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResources.Phone(),
new IdentityResources.Address(),
new(Constants.StandardScopes.Roles, new List<string> {"role"})
new IdentityResources.Email()
};
@ -25,20 +23,34 @@ public static class Config
new(Constants.StandardScopes.PassengerApi),
new(Constants.StandardScopes.BookingApi),
new(Constants.StandardScopes.IdentityApi),
new(Constants.StandardScopes.BookingModularMonolith),
new(Constants.StandardScopes.BookingMonolith),
new(JwtClaimTypes.Role, new List<string> {"role"})
};
public static IList<ApiResource> ApiResources =>
new List<ApiResource>
{
new(Constants.StandardScopes.FlightApi),
new(Constants.StandardScopes.PassengerApi),
new(Constants.StandardScopes.BookingApi),
new(Constants.StandardScopes.IdentityApi),
new(Constants.StandardScopes.BookingModularMonolith),
new(Constants.StandardScopes.BookingMonolith),
new(Constants.StandardScopes.FlightApi)
{
Scopes = { Constants.StandardScopes.FlightApi }
},
new(Constants.StandardScopes.PassengerApi)
{
Scopes = { Constants.StandardScopes.PassengerApi }
},
new(Constants.StandardScopes.BookingApi)
{
Scopes = { Constants.StandardScopes.BookingApi }
},
new(Constants.StandardScopes.IdentityApi)
{
Scopes = { Constants.StandardScopes.IdentityApi }
},
new(Constants.StandardScopes.BookingMonolith)
{
Scopes = { Constants.StandardScopes.BookingMonolith }
},
};
public static IEnumerable<Client> Clients =>
@ -56,15 +68,16 @@ public static class Config
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
JwtClaimTypes.Role, // Include roles scope
Constants.StandardScopes.FlightApi,
Constants.StandardScopes.PassengerApi,
Constants.StandardScopes.BookingApi,
Constants.StandardScopes.IdentityApi,
Constants.StandardScopes.BookingModularMonolith,
Constants.StandardScopes.BookingMonolith,
},
AccessTokenLifetime = 3600, // authorize the client to access protected resources
IdentityTokenLifetime = 3600 // authenticate the user
IdentityTokenLifetime = 3600, // authenticate the user,
AlwaysIncludeUserClaimsInIdToken = true // Include claims in ID token
}
};
}

View File

@ -1,5 +1,6 @@
using BookingMonolith.Identity.Identities.Constants;
using BookingMonolith.Identity.Identities.Models;
using BuildingBlocks.Constants;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Core;
using BuildingBlocks.EFCore;
@ -43,14 +44,14 @@ public class IdentityDataSeeder : IDataSeeder
{
if (!await _identityContext.Roles.AnyAsync())
{
if (await _roleManager.RoleExistsAsync(Constants.Role.Admin) == false)
if (await _roleManager.RoleExistsAsync(IdentityConstant.Role.Admin) == false)
{
await _roleManager.CreateAsync(new Role { Name = Constants.Role.Admin });
await _roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.Admin });
}
if (await _roleManager.RoleExistsAsync(Constants.Role.User) == false)
if (await _roleManager.RoleExistsAsync(IdentityConstant.Role.User) == false)
{
await _roleManager.CreateAsync(new Role { Name = Constants.Role.User });
await _roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.User });
}
}
}
@ -65,7 +66,7 @@ public class IdentityDataSeeder : IDataSeeder
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(InitialData.Users.First(), Constants.Role.Admin);
await _userManager.AddToRoleAsync(InitialData.Users.First(), IdentityConstant.Role.Admin);
await _eventDispatcher.SendAsync(
new UserCreated(
@ -83,7 +84,7 @@ public class IdentityDataSeeder : IDataSeeder
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(InitialData.Users.Last(), Constants.Role.User);
await _userManager.AddToRoleAsync(InitialData.Users.Last(), IdentityConstant.Role.User);
await _eventDispatcher.SendAsync(
new UserCreated(

View File

@ -3,6 +3,7 @@ using BookingMonolith.Identity.Data;
using BookingMonolith.Identity.Identities.Models;
using BuildingBlocks.Web;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
@ -43,6 +44,21 @@ public static class IdentityServerExtensions
//ref: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
identityServerBuilder.AddDeveloperSigningCredential();
builder.Services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
};
});
return builder;
}
}

View File

@ -2,12 +2,6 @@ namespace BookingMonolith.Identity.Identities.Constants;
public static class Constants
{
public static class Role
{
public const string Admin = "admin";
public const string User = "user";
}
public static class StandardScopes
{
public const string Roles = "roles";
@ -15,7 +9,6 @@ public static class Constants
public const string PassengerApi = "passenger-api";
public const string BookingApi = "booking-api";
public const string IdentityApi = "identity-api";
public const string BookingModularMonolith = "booking-modular-monolith";
public const string BookingMonolith = "booking-monolith";
}
}

View File

@ -1,6 +1,7 @@
using Ardalis.GuardClauses;
using BookingMonolith.Identity.Identities.Exceptions;
using BookingMonolith.Identity.Identities.Models;
using BuildingBlocks.Constants;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Core;
using BuildingBlocks.Core.CQRS;
@ -109,7 +110,7 @@ internal class RegisterNewUserHandler : ICommandHandler<RegisterNewUser, Registe
};
var identityResult = await _userManager.CreateAsync(applicationUser, request.Password);
var roleResult = await _userManager.AddToRoleAsync(applicationUser, Constants.Constants.Role.User);
var roleResult = await _userManager.AddToRoleAsync(applicationUser, IdentityConstant.Role.User);
if (identityResult.Succeeded == false)
{

View File

@ -17,7 +17,7 @@ grant_type=password
&client_secret=secret
&username=samh
&password=Admin@123456
&scope=booking-modular-monolith
&scope=booking-modular-monolith role
###

View File

@ -27,8 +27,7 @@
},
"Jwt": {
"Authority": "https://localhost:3000",
"Audience": "booking-modular-monolith",
"RequireHttpsMetadata": false
"Audience": "booking-modular-monolith"
},
"PersistMessageOptions": {
"Interval": 30,

View File

@ -1,9 +1,9 @@
namespace Identity.Configurations;
using System.Collections.Generic;
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
using Identity.Constants;
using Identity.Identity.Constants;
using IdentityModel;
namespace BookingMonolith.Identity.Configurations;
public static class Config
{
@ -12,10 +12,7 @@ public static class Config
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResources.Phone(),
new IdentityResources.Address(),
new(Constants.StandardScopes.Roles, new List<string> {"role"})
new IdentityResources.Email()
};
@ -27,17 +24,33 @@ public static class Config
new(Constants.StandardScopes.BookingApi),
new(Constants.StandardScopes.IdentityApi),
new(Constants.StandardScopes.BookingModularMonolith),
new(JwtClaimTypes.Role, new List<string> {"role"})
};
public static IList<ApiResource> ApiResources =>
new List<ApiResource>
{
new(Constants.StandardScopes.FlightApi),
new(Constants.StandardScopes.PassengerApi),
new(Constants.StandardScopes.BookingApi),
new(Constants.StandardScopes.IdentityApi),
new(Constants.StandardScopes.BookingModularMonolith),
new(Constants.StandardScopes.FlightApi)
{
Scopes = { Constants.StandardScopes.FlightApi }
},
new(Constants.StandardScopes.PassengerApi)
{
Scopes = { Constants.StandardScopes.PassengerApi }
},
new(Constants.StandardScopes.BookingApi)
{
Scopes = { Constants.StandardScopes.BookingApi }
},
new(Constants.StandardScopes.IdentityApi)
{
Scopes = { Constants.StandardScopes.IdentityApi }
},
new(Constants.StandardScopes.BookingModularMonolith)
{
Scopes = { Constants.StandardScopes.BookingModularMonolith }
},
};
public static IEnumerable<Client> Clients =>
@ -55,6 +68,7 @@ public static class Config
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
JwtClaimTypes.Role, // Include roles scope
Constants.StandardScopes.FlightApi,
Constants.StandardScopes.PassengerApi,
Constants.StandardScopes.BookingApi,
@ -62,7 +76,8 @@ public static class Config
Constants.StandardScopes.BookingModularMonolith,
},
AccessTokenLifetime = 3600, // authorize the client to access protected resources
IdentityTokenLifetime = 3600 // authenticate the user
IdentityTokenLifetime = 3600, // authenticate the user,
AlwaysIncludeUserClaimsInIdToken = true // Include claims in ID token
}
};
}

View File

@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using BuildingBlocks.Constants;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Core;
using BuildingBlocks.EFCore;
@ -47,14 +48,14 @@ public class IdentityDataSeeder : IDataSeeder
{
if (!await _identityContext.Roles.AnyAsync())
{
if (await _roleManager.RoleExistsAsync(Constants.Role.Admin) == false)
if (await _roleManager.RoleExistsAsync(IdentityConstant.Role.Admin) == false)
{
await _roleManager.CreateAsync(new Role { Name = Constants.Role.Admin });
await _roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.Admin });
}
if (await _roleManager.RoleExistsAsync(Constants.Role.User) == false)
if (await _roleManager.RoleExistsAsync(IdentityConstant.Role.User) == false)
{
await _roleManager.CreateAsync(new Role { Name = Constants.Role.User });
await _roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.User });
}
}
}
@ -69,7 +70,7 @@ public class IdentityDataSeeder : IDataSeeder
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(InitialData.Users.First(), Constants.Role.Admin);
await _userManager.AddToRoleAsync(InitialData.Users.First(), IdentityConstant.Role.Admin);
await _eventDispatcher.SendAsync(
new UserCreated(
@ -87,7 +88,7 @@ public class IdentityDataSeeder : IDataSeeder
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(InitialData.Users.Last(), Constants.Role.User);
await _userManager.AddToRoleAsync(InitialData.Users.Last(), IdentityConstant.Role.User);
await _eventDispatcher.SendAsync(
new UserCreated(

View File

@ -1,7 +1,9 @@
using BookingMonolith.Identity.Configurations;
using BuildingBlocks.Web;
using Identity.Data;
using Identity.Identity.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
@ -44,6 +46,21 @@ public static class IdentityServerExtensions
//ref: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
identityServerBuilder.AddDeveloperSigningCredential();
builder.Services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
};
});
return builder;
}
}

View File

@ -2,12 +2,6 @@ namespace Identity.Identity.Constants;
public static class Constants
{
public static class Role
{
public const string Admin = "admin";
public const string User = "user";
}
public static class StandardScopes
{
public const string Roles = "roles";

View File

@ -1,3 +1,4 @@
using BuildingBlocks.Constants;
using Duende.IdentityServer.EntityFramework.Entities;
namespace Identity.Identity.Features.RegisteringNewUser.V1;
@ -114,7 +115,7 @@ internal class RegisterNewUserHandler : ICommandHandler<RegisterNewUser, Registe
};
var identityResult = await _userManager.CreateAsync(applicationUser, request.Password);
var roleResult = await _userManager.AddToRoleAsync(applicationUser, Constants.Constants.Role.User);
var roleResult = await _userManager.AddToRoleAsync(applicationUser, IdentityConstant.Role.User);
if (identityResult.Succeeded == false)
{

View File

@ -29,7 +29,7 @@ grant_type=password
&client_secret=secret
&username=samh
&password=Admin@123456
&scope=flight-api
&scope=flight-api role
### change scope base on microservices scope (eg. passenger-api, ...)
###

View File

@ -27,9 +27,7 @@
},
"Jwt": {
"Authority": "http://identity:80",
"Audience": "booking-api",
"RequireHttpsMetadata": false,
"MetadataAddress": "http://identity:80/.well-known/openid-configuration"
"Audience": "booking-api"
},
"Grpc": {
"FlightAddress": "flight:5003",

View File

@ -13,9 +13,7 @@
},
"Jwt": {
"Authority": "http://localhost:6005",
"Audience": "booking-api",
"RequireHttpsMetadata": false,
"MetadataAddress": "http://localhost:6005/.well-known/openid-configuration"
"Audience": "booking-api"
},
"RabbitMqOptions": {
"HostName": "localhost",

View File

@ -14,9 +14,7 @@
},
"Jwt": {
"Authority": "http://identity:80",
"Audience": "flight-api",
"RequireHttpsMetadata": false,
"MetadataAddress": "http://identity:80/.well-known/openid-configuration"
"Audience": "flight-api"
},
"RabbitMqOptions": {
"HostName": "rabbitmq",

View File

@ -20,9 +20,7 @@
},
"Jwt": {
"Authority": "http://localhost:6005",
"Audience": "flight-api",
"RequireHttpsMetadata": false,
"MetadataAddress": "http://localhost:6005/.well-known/openid-configuration"
"Audience": "flight-api"
},
"RabbitMqOptions": {
"HostName": "localhost",

View File

@ -1,9 +1,9 @@
namespace Identity.Configurations;
using System.Collections.Generic;
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
using Identity.Constants;
using Identity.Identity.Constants;
using IdentityModel;
namespace BookingMonolith.Identity.Configurations;
public static class Config
{
@ -12,10 +12,7 @@ public static class Config
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResources.Phone(),
new IdentityResources.Address(),
new(Constants.StandardScopes.Roles, new List<string> {"role"})
new IdentityResources.Email()
};
@ -25,17 +22,30 @@ public static class Config
new(Constants.StandardScopes.FlightApi),
new(Constants.StandardScopes.PassengerApi),
new(Constants.StandardScopes.BookingApi),
new(Constants.StandardScopes.IdentityApi)
new(Constants.StandardScopes.IdentityApi),
new(JwtClaimTypes.Role, new List<string> {"role"})
};
public static IList<ApiResource> ApiResources =>
new List<ApiResource>
{
new(Constants.StandardScopes.FlightApi),
new(Constants.StandardScopes.PassengerApi),
new(Constants.StandardScopes.BookingApi),
new(Constants.StandardScopes.FlightApi)
{
Scopes = { Constants.StandardScopes.FlightApi }
},
new(Constants.StandardScopes.PassengerApi)
{
Scopes = { Constants.StandardScopes.PassengerApi }
},
new(Constants.StandardScopes.BookingApi)
{
Scopes = { Constants.StandardScopes.BookingApi }
},
new(Constants.StandardScopes.IdentityApi)
{
Scopes = { Constants.StandardScopes.IdentityApi }
},
};
public static IEnumerable<Client> Clients =>
@ -53,13 +63,15 @@ public static class Config
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
JwtClaimTypes.Role, // Include roles scope
Constants.StandardScopes.FlightApi,
Constants.StandardScopes.PassengerApi,
Constants.StandardScopes.BookingApi,
Constants.StandardScopes.IdentityApi
Constants.StandardScopes.IdentityApi,
},
AccessTokenLifetime = 3600, // authorize the client to access protected resources
IdentityTokenLifetime = 3600 // authenticate the user
IdentityTokenLifetime = 3600, // authenticate the user,
AlwaysIncludeUserClaimsInIdToken = true // Include claims in ID token
}
};
}

View File

@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using BuildingBlocks.Constants;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Core;
using BuildingBlocks.EFCore;
@ -43,14 +44,14 @@ public class IdentityDataSeeder : IDataSeeder
private async Task SeedRoles()
{
if (await _roleManager.RoleExistsAsync(Constants.Role.Admin) == false)
if (await _roleManager.RoleExistsAsync(IdentityConstant.Role.Admin) == false)
{
await _roleManager.CreateAsync(new Role { Name = Constants.Role.Admin });
await _roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.Admin });
}
if (await _roleManager.RoleExistsAsync(Constants.Role.User) == false)
if (await _roleManager.RoleExistsAsync(IdentityConstant.Role.User) == false)
{
await _roleManager.CreateAsync(new Role { Name = Constants.Role.User });
await _roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.User });
}
}
@ -62,7 +63,7 @@ public class IdentityDataSeeder : IDataSeeder
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(InitialData.Users.First(), Constants.Role.Admin);
await _userManager.AddToRoleAsync(InitialData.Users.First(), IdentityConstant.Role.Admin);
await _eventDispatcher.SendAsync(new UserCreated(InitialData.Users.First().Id, InitialData.Users.First().FirstName + " " + InitialData.Users.First().LastName, InitialData.Users.First().PassPortNumber));
}
@ -74,7 +75,7 @@ public class IdentityDataSeeder : IDataSeeder
if (result.Succeeded)
{
await _userManager.AddToRoleAsync(InitialData.Users.Last(), Constants.Role.User);
await _userManager.AddToRoleAsync(InitialData.Users.Last(), IdentityConstant.Role.User);
await _eventDispatcher.SendAsync(new UserCreated(InitialData.Users.Last().Id, InitialData.Users.Last().FirstName + " " + InitialData.Users.Last().LastName, InitialData.Users.Last().PassPortNumber));
}

View File

@ -1,7 +1,9 @@
using BookingMonolith.Identity.Configurations;
using BuildingBlocks.Web;
using Identity.Data;
using Identity.Identity.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
@ -44,6 +46,21 @@ public static class IdentityServerExtensions
//ref: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
identityServerBuilder.AddDeveloperSigningCredential();
builder.Services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
};
});
return builder;
}
}

View File

@ -2,12 +2,6 @@ namespace Identity.Identity.Constants;
public static class Constants
{
public static class Role
{
public const string Admin = "admin";
public const string User = "user";
}
public static class StandardScopes
{
public const string Roles = "roles";

View File

@ -1,3 +1,5 @@
using BuildingBlocks.Constants;
namespace Identity.Identity.Features.RegisteringNewUser.V1;
using System;
@ -113,7 +115,7 @@ internal class RegisterNewUserHandler : ICommandHandler<RegisterNewUser, Registe
};
var identityResult = await _userManager.CreateAsync(applicationUser, request.Password);
var roleResult = await _userManager.AddToRoleAsync(applicationUser, Constants.Constants.Role.User);
var roleResult = await _userManager.AddToRoleAsync(applicationUser, IdentityConstant.Role.User);
if (identityResult.Succeeded == false)
{

View File

@ -1,3 +1,4 @@
using BuildingBlocks.Constants;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Core;
using BuildingBlocks.EFCore;
@ -23,14 +24,14 @@ public class IdentityTestDataSeeder(
private async Task SeedRoles()
{
if (await roleManager.RoleExistsAsync(Constants.Role.Admin) == false)
if (await roleManager.RoleExistsAsync(IdentityConstant.Role.Admin) == false)
{
await roleManager.CreateAsync(new Role { Name = Constants.Role.Admin });
await roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.Admin });
}
if (await roleManager.RoleExistsAsync(Constants.Role.User) == false)
if (await roleManager.RoleExistsAsync(IdentityConstant.Role.User) == false)
{
await roleManager.CreateAsync(new Role { Name = Constants.Role.User });
await roleManager.CreateAsync(new Role { Name = IdentityConstant.Role.User });
}
}
@ -42,7 +43,7 @@ public class IdentityTestDataSeeder(
if (result.Succeeded)
{
await userManager.AddToRoleAsync(InitialData.Users.First(), Constants.Role.Admin);
await userManager.AddToRoleAsync(InitialData.Users.First(), IdentityConstant.Role.Admin);
await eventDispatcher.SendAsync(new UserCreated(InitialData.Users.First().Id, InitialData.Users.First().FirstName + " " + InitialData.Users.First().LastName, InitialData.Users.First().PassPortNumber));
}
@ -54,7 +55,7 @@ public class IdentityTestDataSeeder(
if (result.Succeeded)
{
await userManager.AddToRoleAsync(InitialData.Users.Last(), Constants.Role.User);
await userManager.AddToRoleAsync(InitialData.Users.Last(), IdentityConstant.Role.User);
await eventDispatcher.SendAsync(new UserCreated(InitialData.Users.Last().Id, InitialData.Users.Last().FirstName + " " + InitialData.Users.Last().LastName, InitialData.Users.Last().PassPortNumber));
}

View File

@ -10,9 +10,7 @@
},
"Jwt": {
"Authority": "http://identity:80",
"Audience": "passenger-api",
"RequireHttpsMetadata": false,
"MetadataAddress": "http://identity:80/.well-known/openid-configuration"
"Audience": "passenger-api"
},
"MongoOptions": {
"ConnectionString": "mongodb://mongo:27017",

View File

@ -11,9 +11,7 @@
},
"Jwt": {
"Authority": "http://localhost:6005",
"Audience": "passenger-api",
"RequireHttpsMetadata": false,
"MetadataAddress": "http://localhost:6005/.well-known/openid-configuration"
"Audience": "passenger-api"
},
"RabbitMqOptions": {
"HostName": "localhost",

View File

@ -0,0 +1,10 @@
namespace BuildingBlocks.Constants;
public static class IdentityConstant
{
public static class Role
{
public const string Admin = "admin";
public const string User = "user";
}
}

View File

@ -1,54 +1,48 @@
using BuildingBlocks.Constants;
using BuildingBlocks.Web;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
namespace BuildingBlocks.Jwt;
using Duende.IdentityServer.EntityFramework.Entities;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
public static class JwtExtensions
namespace BuildingBlocks.Jwt
{
public static IServiceCollection AddJwt(this IServiceCollection services)
public static class JwtExtensions
{
var jwtOptions = services.GetOptions<JwtBearerOptions>("Jwt");
services.AddAuthentication(
o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(cfg => cfg.SlidingExpiration = true)
.AddJwtBearer(
JwtBearerDefaults.AuthenticationScheme,
options =>
{
options.Authority = jwtOptions.Authority;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ClockSkew = TimeSpan.FromSeconds(2), // For prevent add default value (5min) to life time token!
ValidateLifetime = true, // Enforce token expiry
};
options.RequireHttpsMetadata = jwtOptions.RequireHttpsMetadata;
options.MetadataAddress = jwtOptions.MetadataAddress;
});
if (!string.IsNullOrEmpty(jwtOptions.Audience))
public static IServiceCollection AddJwt(this IServiceCollection services)
{
// Bind Jwt settings from configuration
var jwtOptions = services.GetOptions<JwtBearerOptions>("Jwt");
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = jwtOptions.Authority;
options.Audience = jwtOptions.Audience;
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuers = [jwtOptions.Authority],
ValidateAudience = true,
ValidAudiences = [jwtOptions.Audience],
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(2), // Reduce default clock skew
// For IdentityServer4/Duende, we should also validate the signing key
ValidateIssuerSigningKey = true,
NameClaimType = "name", // Map "name" claim to User.Identity.Name
RoleClaimType = "role", // Map "role" claim to User.IsInRole()
};
// Preserve ALL claims from the token (including "sub")
options.MapInboundClaims = false;
});
services.AddAuthorization(
options =>
{
// Set JWT as the default scheme for all [Authorize] attributes
options.DefaultPolicy =
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
options.AddPolicy(
nameof(ApiScope),
policy =>
@ -57,9 +51,27 @@ public static class JwtExtensions
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", jwtOptions.Audience);
});
});
}
return services;
// Role-based policies
options.AddPolicy(
IdentityConstant.Role.Admin,
x =>
{
x.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
x.RequireRole(IdentityConstant.Role.Admin);
}
);
options.AddPolicy(
IdentityConstant.Role.User,
x =>
{
x.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
x.RequireRole(IdentityConstant.Role.User);
}
);
});
return services;
}
}
}