feat: add support role base authorization policy

This commit is contained in:
Meysam Hadeli 2025-05-12 00:46:19 +03:30
parent 879fde8d80
commit eb5bf1da61
28 changed files with 242 additions and 155 deletions

View File

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

View File

@ -1,6 +1,7 @@
using BookingMonolith.Identity.Identities.Constants; using BookingMonolith.Identity.Identities.Constants;
using Duende.IdentityServer; using Duende.IdentityServer;
using Duende.IdentityServer.Models; using Duende.IdentityServer.Models;
using IdentityModel;
namespace BookingMonolith.Identity.Configurations; namespace BookingMonolith.Identity.Configurations;
@ -11,10 +12,7 @@ public static class Config
{ {
new IdentityResources.OpenId(), new IdentityResources.OpenId(),
new IdentityResources.Profile(), new IdentityResources.Profile(),
new IdentityResources.Email(), new IdentityResources.Email()
new IdentityResources.Phone(),
new IdentityResources.Address(),
new(Constants.StandardScopes.Roles, new List<string> {"role"})
}; };
@ -25,20 +23,34 @@ public static class Config
new(Constants.StandardScopes.PassengerApi), new(Constants.StandardScopes.PassengerApi),
new(Constants.StandardScopes.BookingApi), new(Constants.StandardScopes.BookingApi),
new(Constants.StandardScopes.IdentityApi), new(Constants.StandardScopes.IdentityApi),
new(Constants.StandardScopes.BookingModularMonolith),
new(Constants.StandardScopes.BookingMonolith), new(Constants.StandardScopes.BookingMonolith),
new(JwtClaimTypes.Role, new List<string> {"role"})
}; };
public static IList<ApiResource> ApiResources => public static IList<ApiResource> ApiResources =>
new List<ApiResource> new List<ApiResource>
{ {
new(Constants.StandardScopes.FlightApi), new(Constants.StandardScopes.FlightApi)
new(Constants.StandardScopes.PassengerApi), {
new(Constants.StandardScopes.BookingApi), Scopes = { Constants.StandardScopes.FlightApi }
new(Constants.StandardScopes.IdentityApi), },
new(Constants.StandardScopes.BookingModularMonolith), new(Constants.StandardScopes.PassengerApi)
new(Constants.StandardScopes.BookingMonolith), {
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 => public static IEnumerable<Client> Clients =>
@ -56,15 +68,16 @@ public static class Config
{ {
IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Profile,
JwtClaimTypes.Role, // Include roles scope
Constants.StandardScopes.FlightApi, Constants.StandardScopes.FlightApi,
Constants.StandardScopes.PassengerApi, Constants.StandardScopes.PassengerApi,
Constants.StandardScopes.BookingApi, Constants.StandardScopes.BookingApi,
Constants.StandardScopes.IdentityApi, Constants.StandardScopes.IdentityApi,
Constants.StandardScopes.BookingModularMonolith,
Constants.StandardScopes.BookingMonolith, Constants.StandardScopes.BookingMonolith,
}, },
AccessTokenLifetime = 3600, // authorize the client to access protected resources 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.Constants;
using BookingMonolith.Identity.Identities.Models; using BookingMonolith.Identity.Identities.Models;
using BuildingBlocks.Constants;
using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
@ -43,14 +44,14 @@ public class IdentityDataSeeder : IDataSeeder
{ {
if (!await _identityContext.Roles.AnyAsync()) 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) if (result.Succeeded)
{ {
await _userManager.AddToRoleAsync(InitialData.Users.First(), Constants.Role.Admin); await _userManager.AddToRoleAsync(InitialData.Users.First(), IdentityConstant.Role.Admin);
await _eventDispatcher.SendAsync( await _eventDispatcher.SendAsync(
new UserCreated( new UserCreated(
@ -83,7 +84,7 @@ public class IdentityDataSeeder : IDataSeeder
if (result.Succeeded) if (result.Succeeded)
{ {
await _userManager.AddToRoleAsync(InitialData.Users.Last(), Constants.Role.User); await _userManager.AddToRoleAsync(InitialData.Users.Last(), IdentityConstant.Role.User);
await _eventDispatcher.SendAsync( await _eventDispatcher.SendAsync(
new UserCreated( new UserCreated(

View File

@ -3,6 +3,7 @@ using BookingMonolith.Identity.Data;
using BookingMonolith.Identity.Identities.Models; using BookingMonolith.Identity.Identities.Models;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -43,6 +44,21 @@ public static class IdentityServerExtensions
//ref: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html //ref: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
identityServerBuilder.AddDeveloperSigningCredential(); 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; return builder;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
namespace Identity.Configurations;
using System.Collections.Generic;
using Duende.IdentityServer; using Duende.IdentityServer;
using Duende.IdentityServer.Models; using Duende.IdentityServer.Models;
using Identity.Constants; using Identity.Identity.Constants;
using IdentityModel;
namespace BookingMonolith.Identity.Configurations;
public static class Config public static class Config
{ {
@ -12,10 +12,7 @@ public static class Config
{ {
new IdentityResources.OpenId(), new IdentityResources.OpenId(),
new IdentityResources.Profile(), new IdentityResources.Profile(),
new IdentityResources.Email(), new IdentityResources.Email()
new IdentityResources.Phone(),
new IdentityResources.Address(),
new(Constants.StandardScopes.Roles, new List<string> {"role"})
}; };
@ -27,17 +24,33 @@ public static class Config
new(Constants.StandardScopes.BookingApi), new(Constants.StandardScopes.BookingApi),
new(Constants.StandardScopes.IdentityApi), new(Constants.StandardScopes.IdentityApi),
new(Constants.StandardScopes.BookingModularMonolith), new(Constants.StandardScopes.BookingModularMonolith),
new(JwtClaimTypes.Role, new List<string> {"role"})
}; };
public static IList<ApiResource> ApiResources => public static IList<ApiResource> ApiResources =>
new List<ApiResource> new List<ApiResource>
{ {
new(Constants.StandardScopes.FlightApi), new(Constants.StandardScopes.FlightApi)
new(Constants.StandardScopes.PassengerApi), {
new(Constants.StandardScopes.BookingApi), Scopes = { Constants.StandardScopes.FlightApi }
new(Constants.StandardScopes.IdentityApi), },
new(Constants.StandardScopes.BookingModularMonolith), 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 => public static IEnumerable<Client> Clients =>
@ -55,6 +68,7 @@ public static class Config
{ {
IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Profile,
JwtClaimTypes.Role, // Include roles scope
Constants.StandardScopes.FlightApi, Constants.StandardScopes.FlightApi,
Constants.StandardScopes.PassengerApi, Constants.StandardScopes.PassengerApi,
Constants.StandardScopes.BookingApi, Constants.StandardScopes.BookingApi,
@ -62,7 +76,8 @@ public static class Config
Constants.StandardScopes.BookingModularMonolith, Constants.StandardScopes.BookingModularMonolith,
}, },
AccessTokenLifetime = 3600, // authorize the client to access protected resources 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;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Constants;
using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
@ -47,14 +48,14 @@ public class IdentityDataSeeder : IDataSeeder
{ {
if (!await _identityContext.Roles.AnyAsync()) 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) if (result.Succeeded)
{ {
await _userManager.AddToRoleAsync(InitialData.Users.First(), Constants.Role.Admin); await _userManager.AddToRoleAsync(InitialData.Users.First(), IdentityConstant.Role.Admin);
await _eventDispatcher.SendAsync( await _eventDispatcher.SendAsync(
new UserCreated( new UserCreated(
@ -87,7 +88,7 @@ public class IdentityDataSeeder : IDataSeeder
if (result.Succeeded) if (result.Succeeded)
{ {
await _userManager.AddToRoleAsync(InitialData.Users.Last(), Constants.Role.User); await _userManager.AddToRoleAsync(InitialData.Users.Last(), IdentityConstant.Role.User);
await _eventDispatcher.SendAsync( await _eventDispatcher.SendAsync(
new UserCreated( new UserCreated(

View File

@ -1,7 +1,9 @@
using BookingMonolith.Identity.Configurations;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Identity.Data; using Identity.Data;
using Identity.Identity.Models; using Identity.Identity.Models;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -44,6 +46,21 @@ public static class IdentityServerExtensions
//ref: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html //ref: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
identityServerBuilder.AddDeveloperSigningCredential(); 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; return builder;
} }
} }

View File

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

View File

@ -1,3 +1,4 @@
using BuildingBlocks.Constants;
using Duende.IdentityServer.EntityFramework.Entities; using Duende.IdentityServer.EntityFramework.Entities;
namespace Identity.Identity.Features.RegisteringNewUser.V1; 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 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) if (identityResult.Succeeded == false)
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
namespace Identity.Configurations;
using System.Collections.Generic;
using Duende.IdentityServer; using Duende.IdentityServer;
using Duende.IdentityServer.Models; using Duende.IdentityServer.Models;
using Identity.Constants; using Identity.Identity.Constants;
using IdentityModel;
namespace BookingMonolith.Identity.Configurations;
public static class Config public static class Config
{ {
@ -12,10 +12,7 @@ public static class Config
{ {
new IdentityResources.OpenId(), new IdentityResources.OpenId(),
new IdentityResources.Profile(), new IdentityResources.Profile(),
new IdentityResources.Email(), new IdentityResources.Email()
new IdentityResources.Phone(),
new IdentityResources.Address(),
new(Constants.StandardScopes.Roles, new List<string> {"role"})
}; };
@ -25,17 +22,30 @@ public static class Config
new(Constants.StandardScopes.FlightApi), new(Constants.StandardScopes.FlightApi),
new(Constants.StandardScopes.PassengerApi), new(Constants.StandardScopes.PassengerApi),
new(Constants.StandardScopes.BookingApi), new(Constants.StandardScopes.BookingApi),
new(Constants.StandardScopes.IdentityApi) new(Constants.StandardScopes.IdentityApi),
new(JwtClaimTypes.Role, new List<string> {"role"})
}; };
public static IList<ApiResource> ApiResources => public static IList<ApiResource> ApiResources =>
new List<ApiResource> new List<ApiResource>
{ {
new(Constants.StandardScopes.FlightApi), new(Constants.StandardScopes.FlightApi)
new(Constants.StandardScopes.PassengerApi), {
new(Constants.StandardScopes.BookingApi), Scopes = { Constants.StandardScopes.FlightApi }
},
new(Constants.StandardScopes.PassengerApi)
{
Scopes = { Constants.StandardScopes.PassengerApi }
},
new(Constants.StandardScopes.BookingApi)
{
Scopes = { Constants.StandardScopes.BookingApi }
},
new(Constants.StandardScopes.IdentityApi) new(Constants.StandardScopes.IdentityApi)
{
Scopes = { Constants.StandardScopes.IdentityApi }
},
}; };
public static IEnumerable<Client> Clients => public static IEnumerable<Client> Clients =>
@ -53,13 +63,15 @@ public static class Config
{ {
IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Profile,
JwtClaimTypes.Role, // Include roles scope
Constants.StandardScopes.FlightApi, Constants.StandardScopes.FlightApi,
Constants.StandardScopes.PassengerApi, Constants.StandardScopes.PassengerApi,
Constants.StandardScopes.BookingApi, Constants.StandardScopes.BookingApi,
Constants.StandardScopes.IdentityApi Constants.StandardScopes.IdentityApi,
}, },
AccessTokenLifetime = 3600, // authorize the client to access protected resources 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;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Constants;
using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
@ -43,14 +44,14 @@ public class IdentityDataSeeder : IDataSeeder
private async Task SeedRoles() 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) 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)); 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) 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)); 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 BuildingBlocks.Web;
using Identity.Data; using Identity.Data;
using Identity.Identity.Models; using Identity.Identity.Models;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -44,6 +46,21 @@ public static class IdentityServerExtensions
//ref: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html //ref: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
identityServerBuilder.AddDeveloperSigningCredential(); 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; return builder;
} }
} }

View File

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

View File

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

View File

@ -1,3 +1,4 @@
using BuildingBlocks.Constants;
using BuildingBlocks.Contracts.EventBus.Messages; using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
@ -23,14 +24,14 @@ public class IdentityTestDataSeeder(
private async Task SeedRoles() 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) 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)); 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) 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)); 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": { "Jwt": {
"Authority": "http://identity:80", "Authority": "http://identity:80",
"Audience": "passenger-api", "Audience": "passenger-api"
"RequireHttpsMetadata": false,
"MetadataAddress": "http://identity:80/.well-known/openid-configuration"
}, },
"MongoOptions": { "MongoOptions": {
"ConnectionString": "mongodb://mongo:27017", "ConnectionString": "mongodb://mongo:27017",

View File

@ -11,9 +11,7 @@
}, },
"Jwt": { "Jwt": {
"Authority": "http://localhost:6005", "Authority": "http://localhost:6005",
"Audience": "passenger-api", "Audience": "passenger-api"
"RequireHttpsMetadata": false,
"MetadataAddress": "http://localhost:6005/.well-known/openid-configuration"
}, },
"RabbitMqOptions": { "RabbitMqOptions": {
"HostName": "localhost", "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 BuildingBlocks.Web;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
namespace BuildingBlocks.Jwt;
using Duende.IdentityServer.EntityFramework.Entities; using Duende.IdentityServer.EntityFramework.Entities;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
public static class JwtExtensions namespace BuildingBlocks.Jwt
{ {
public static class JwtExtensions
{
public static IServiceCollection AddJwt(this IServiceCollection services) public static IServiceCollection AddJwt(this IServiceCollection services)
{ {
// Bind Jwt settings from configuration
var jwtOptions = services.GetOptions<JwtBearerOptions>("Jwt"); var jwtOptions = services.GetOptions<JwtBearerOptions>("Jwt");
services.AddAuthentication( services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
o => .AddJwtBearer(options =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(cfg => cfg.SlidingExpiration = true)
.AddJwtBearer(
JwtBearerDefaults.AuthenticationScheme,
options =>
{ {
options.Authority = jwtOptions.Authority; options.Authority = jwtOptions.Authority;
options.Audience = jwtOptions.Audience;
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters options.TokenValidationParameters = new TokenValidationParameters
{ {
ValidateAudience = false, ValidateIssuer = true,
ClockSkew = TimeSpan.FromSeconds(2), // For prevent add default value (5min) to life time token! ValidIssuers = [jwtOptions.Authority],
ValidateLifetime = true, // Enforce token expiry 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()
}; };
options.RequireHttpsMetadata = jwtOptions.RequireHttpsMetadata; // Preserve ALL claims from the token (including "sub")
options.MetadataAddress = jwtOptions.MetadataAddress; options.MapInboundClaims = false;
}); });
if (!string.IsNullOrEmpty(jwtOptions.Audience))
{
services.AddAuthorization( services.AddAuthorization(
options => options =>
{ {
// Set JWT as the default scheme for all [Authorize] attributes
options.DefaultPolicy =
new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
options.AddPolicy( options.AddPolicy(
nameof(ApiScope), nameof(ApiScope),
policy => policy =>
@ -57,9 +51,27 @@ public static class JwtExtensions
policy.RequireAuthenticatedUser(); policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", jwtOptions.Audience); policy.RequireClaim("scope", jwtOptions.Audience);
}); });
});
// 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; return services;
} }
}
} }