diff --git a/services/iam-service-net/src/IamService.API/Program.cs b/services/iam-service-net/src/IamService.API/Program.cs index 77aa76a6..98a04529 100644 --- a/services/iam-service-net/src/IamService.API/Program.cs +++ b/services/iam-service-net/src/IamService.API/Program.cs @@ -2,6 +2,7 @@ using Asp.Versioning; using FluentValidation; using Hellang.Middleware.ProblemDetails; using IamService.API.Application.Behaviors; +using IamService.API.Swagger; using IamService.Infrastructure; using Serilog; @@ -208,6 +209,9 @@ builder.Services.AddSwaggerGen(options => // EN: Enable annotations / VI: Bật annotations options.EnableAnnotations(); + + // EN: Add /connect/token endpoint to Swagger / VI: Thêm /connect/token endpoint vào Swagger + options.DocumentFilter(); }); // EN: Add health checks / VI: Thêm health checks diff --git a/services/iam-service-net/src/IamService.API/Swagger/TokenEndpointDocumentFilter.cs b/services/iam-service-net/src/IamService.API/Swagger/TokenEndpointDocumentFilter.cs new file mode 100644 index 00000000..8fb8946b --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Swagger/TokenEndpointDocumentFilter.cs @@ -0,0 +1,164 @@ +// EN: Swagger Document Filter to add /connect/token endpoint +// VI: Swagger Document Filter để thêm /connect/token endpoint + +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace IamService.API.Swagger; + +/// +/// EN: Adds OAuth2 /connect/token endpoint to Swagger documentation. +/// VI: Thêm OAuth2 /connect/token endpoint vào Swagger documentation. +/// +public class TokenEndpointDocumentFilter : IDocumentFilter +{ + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + // EN: Add /connect/token endpoint + // VI: Thêm /connect/token endpoint + var tokenPath = new OpenApiPathItem(); + + var postOperation = new OpenApiOperation + { + Tags = new List { new() { Name = "OAuth2 Token" } }, + Summary = "OAuth2 Token Endpoint", + Description = """ + OAuth2/OIDC token endpoint for authentication. + + **Supported Grant Types:** + - `password` - Resource Owner Password Grant (login with email/password) + - `refresh_token` - Refresh an access token + - `client_credentials` - Service-to-service authentication + + **Default Client:** + - `client_id`: password-client + - `client_secret`: password-client-secret + """, + OperationId = "OAuth2Token", + RequestBody = new OpenApiRequestBody + { + Required = true, + Content = new Dictionary + { + ["application/x-www-form-urlencoded"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "object", + Required = new HashSet { "grant_type", "client_id" }, + Properties = new Dictionary + { + ["grant_type"] = new OpenApiSchema + { + Type = "string", + Description = "OAuth2 grant type", + Enum = new List + { + new OpenApiString("password"), + new OpenApiString("refresh_token"), + new OpenApiString("client_credentials") + }, + Example = new OpenApiString("password") + }, + ["client_id"] = new OpenApiSchema + { + Type = "string", + Description = "OAuth2 client ID", + Example = new OpenApiString("password-client") + }, + ["client_secret"] = new OpenApiSchema + { + Type = "string", + Description = "OAuth2 client secret", + Example = new OpenApiString("password-client-secret") + }, + ["username"] = new OpenApiSchema + { + Type = "string", + Description = "User email (required for password grant)", + Example = new OpenApiString("user@example.com") + }, + ["password"] = new OpenApiSchema + { + Type = "string", + Format = "password", + Description = "User password (required for password grant)", + Example = new OpenApiString("Password123!") + }, + ["refresh_token"] = new OpenApiSchema + { + Type = "string", + Description = "Refresh token (required for refresh_token grant)" + }, + ["scope"] = new OpenApiSchema + { + Type = "string", + Description = "Requested scopes (space-separated)", + Example = new OpenApiString("openid profile email api offline_access") + } + } + } + } + } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "Token response", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary + { + ["access_token"] = new OpenApiSchema + { + Type = "string", + Description = "JWT access token" + }, + ["token_type"] = new OpenApiSchema + { + Type = "string", + Example = new OpenApiString("Bearer") + }, + ["expires_in"] = new OpenApiSchema + { + Type = "integer", + Description = "Token lifetime in seconds", + Example = new OpenApiInteger(900) + }, + ["refresh_token"] = new OpenApiSchema + { + Type = "string", + Description = "Refresh token (if offline_access scope requested)" + }, + ["scope"] = new OpenApiSchema + { + Type = "string", + Description = "Granted scopes" + } + } + } + } + } + }, + ["400"] = new OpenApiResponse + { + Description = "Invalid request (missing parameters, invalid grant, etc.)" + }, + ["401"] = new OpenApiResponse + { + Description = "Invalid credentials or client authentication failed" + } + } + }; + + tokenPath.Operations.Add(OperationType.Post, postOperation); + swaggerDoc.Paths.Add("/connect/token", tokenPath); + } +} diff --git a/services/iam-service-net/src/IamService.API/keys/is-signing-key-29B7623C4F795E80DF646EEB20086183.json b/services/iam-service-net/src/IamService.API/keys/is-signing-key-29B7623C4F795E80DF646EEB20086183.json new file mode 100644 index 00000000..03b3bf88 --- /dev/null +++ b/services/iam-service-net/src/IamService.API/keys/is-signing-key-29B7623C4F795E80DF646EEB20086183.json @@ -0,0 +1 @@ +{"Version":1,"Id":"29B7623C4F795E80DF646EEB20086183","Created":"2026-01-13T04:39:41.382908Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8HAKkGoOZFhIq-FZTYbjyMD0Mqf_d3-TEdbnNuj5Kqnw7X75tEGZDMGOH4s2VT7wcheWFsypCWAleOoUCzpmIy8s5dpvzpKGjL4nO7aNyYjDLcI8AjG_NLHZ1FbopMnUMjpx7etVzAGQ5TcZX-XmY80l5iuaxVXpCEr5-PAYtMT_6jp5tcnKUatUVUk-H7k3bnyrgvqur9ekiAxshi8CN48xoVe95wRd8zwYNTKl846NgGMeD02VpVzq-cvm2nB-u5cIL5OH0FUcZbfyjgJOogl4FI1hVIrz00vyJ1cDh-Ew1p2wjIb5oVEsCCBN0iO_naD_ZFUMaZo4Ir8mHFVRiSokaD6SKVCEsOanxDXkE87VOBxjq8HFDWBMWtr-aE9C_QDN_RqLsg0x-jp4qskOB123Jr8d00DYSENFxBQqrYQrpGz74qekMxpOaRBr8x3wg7UIqIiwLrkMgPO9lP5dhKRPbefDUrD3SoRTXSnrgFvPirFD6bo5WSUMTSkYqkuIFLmjOYiSlvI8N_qT8QzQUSXRY7LZd0Tf8QrDvJNfWP9vegsjRAAH5EBjHZ1N6a0GFgkmZFChobcVc_p28hqVZjmk91LiS9oCq_CQ3HBO3b06dz_mwwWcV1XzLlK7iNjrfjuj2geGPcVOS9jjKL6eZzxRd_34zjFr50lxLnhLuZ0wVWlRzAzY0z0EWQFXaIlkFII5UWMpbUMPEJgieKeU0XRAc9zcDjTVzzzf5btsMGLMn77hUzmxeHc_O2FmLXVDgCa2Rrgm_BXgd4-_B2LJ2_rFhHTI4syuyhZ9JbTOPqmcLtCMRdSO7mtur7Ly17zXWND1isDov3wczmXQJrtgT7fKGlLrZuJN2lKOArhpmyX1CyrFcpMV9KL8acHK7FfXHg6zc7veEm_XY1IhNmMEv94SivVAfFbWhh7i2tDreU4-QSpXUVcrGCqOlKy8O0Qiv9utf-6wVcxKBNf_50HzTAOHM0dBj_nPjcpYzBLcyr36quNeaWikeD9Kop24iDyF8_i_Qy1_M_MGT4SQgPW69jaazKkK2gYLcssVhupONooVI95akrtq5DnTxzs-rbdzgiF59gDwvvkKtGK8KfA-6VSx3K4Wgs1JvqWj-uoVxJzaTOV-ozXXJ909KMuPiJ0J4VVt3Ipb9GtS-2WsqLPgTqFKjOWIF8Zu7k4buKO2zRFIQi10wv5PmuIT2zCH6KNiWGu2Mgu8lRzBPo9kVM-ackRZSd6Wwx1Ys2vB9XAjF4tcAMjm1a98xgtctOwpXuDMHHgkWgNNFGCOOI37KBpAzAoHUk0hWbj9yWT82xX24_u9LH_G5NIgXaUSnhXtMO6zC6ivNrf9a1l8twMRnI3hfHooRkyLyK35DptVaFtvq-z92PfYpCbycg9iec1nH1UJlJwqh9OEFOgunt8cc2MgvtOLp1HEU4_kk-Ad8kOtFYKz7zTpgLLzpBdYLEDMJrp4kVIBPZPP2mc_1SowvuaaupZR98uiA-h8cqLJxpoQ3T5FHBcqSz1oUMhLkoHfE6OIeDxBfeWAGyGMEhKHGuvnn7shkEIgvFYlY35wEsqgy8Pj8blPJQsvkeQbGYyKBkC2XC9CRA69np5K5RhU7It0d4kMiUx5QBSeanRFQkh85a0GXdrIpODYXYawO6EKv4zXGB0cE0_g889xDQ0kRZLmCFcpVd7kBBFzMXIHnopGC7I8K-IQjEpicJDjxFrlik8aDUXzq997iEt4mwnUwTwVYmouTda6dTFizUvxJnhaw2kUjqNMLiwGK5QttcsZ18U0em-nqWASkFI-tunZiBLyIc8BItoOO1RRFw7pyJxY6MbZq56PUKRTTdVc-8osCK4KQnbZG5gLAN3SmuaRDWw4_DERJCJR72vbVZdysbOgIgWS3a0R5zLt-i-sEUqzKWpL3AnQ2QqiwpZswdjwpxLWud2xTEYzyhg5J8kOW6vUvBTspuV_CefLFgtM9CqAl_AxlDPcUcsQ7H1X6utI48TzBPwqCjj2pydreuXEwgS0lk66yBMO_7TgzGoZqD7Xz5-sAT1dTZHFTl4rCHjh7ku1bpSyQuBVqkjtpCCMUFyKC-jRce5JEujsPrUhxrSN2DP2iUfX2AXz3H8SJWw8uILFI-W6Kjo7rGcnVwRud0FKwSUQyCAJgycGbeWD11hNlv2w8BVDl-6iYCxS-MBRetDYmE1Uf80Zl4E_bI_TXChokPdx8AXkvpxDh4qQ5YZqPJcOOcF8y-qGwICv68-bYCm9Gv7oL2onJm7S0RDwuvuaqjMxfwMYWv3lYS2B7yh0GuJ1fnDs-ObPqwdHSA1_YjYdRoG3YjAo1nhKTZc5t1EMtP15ORzC-Y5WvnvFL937Xf5kl8yz9MaYd97XNDjL_X4g-zr0u6m7zDRAgXHT5ErlGib4nPPaBNdDLFxYwoMp92Sdu2huvdf6t3rfqow-W-xrKsYf74_XJIxBcRgjUPTrhacP","DataProtected":true} \ No newline at end of file diff --git a/services/iam-service-net/src/IamService.Infrastructure/IdentityServer/Config.cs b/services/iam-service-net/src/IamService.Infrastructure/IdentityServer/Config.cs index a65894f9..b75142d4 100644 --- a/services/iam-service-net/src/IamService.Infrastructure/IdentityServer/Config.cs +++ b/services/iam-service-net/src/IamService.Infrastructure/IdentityServer/Config.cs @@ -166,6 +166,32 @@ public static class Config AccessTokenLifetime = 900, RefreshTokenExpiration = TokenExpiration.Sliding, SlidingRefreshTokenLifetime = 604800 + }, + + // EN: Swagger UI Client - for testing via Swagger + // VI: Swagger UI Client - để test qua Swagger + new Client + { + ClientId = "swagger-ui", + ClientName = "Swagger UI", + ClientSecrets = { new Secret("swagger-ui-secret".Sha256()) }, + + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + + AllowedScopes = + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.Email, + IdentityServerConstants.StandardScopes.OfflineAccess, + "roles", + "api" + }, + + AllowOfflineAccess = true, + AccessTokenLifetime = 3600, // 1 hour for testing convenience + RefreshTokenExpiration = TokenExpiration.Sliding, + SlidingRefreshTokenLifetime = 86400 // 1 day } ]; } diff --git a/services/storage-service-net/src/StorageService.API/Program.cs b/services/storage-service-net/src/StorageService.API/Program.cs index a315ab16..4b9135b3 100644 --- a/services/storage-service-net/src/StorageService.API/Program.cs +++ b/services/storage-service-net/src/StorageService.API/Program.cs @@ -57,6 +57,23 @@ try options.SubstituteApiVersionInUrl = true; }); + // EN: Add Authentication / VI: Thêm Authentication + builder.Services.AddAuthentication("Bearer") + .AddJwtBearer("Bearer", options => + { + // EN: Configure JWT Bearer for IAM Service integration + // VI: Cấu hình JWT Bearer để tích hợp với IAM Service + options.Authority = builder.Configuration["IamService:BaseUrl"] ?? "http://localhost:5001"; + options.RequireHttpsMetadata = false; // EN: Development only / VI: Chỉ dùng cho development + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateAudience = false, // EN: Validate in production / VI: Validate trong production + ValidateIssuer = false // EN: Validate in production / VI: Validate trong production + }; + }); + + builder.Services.AddAuthorization(); + // EN: Add controllers / VI: Thêm controllers builder.Services.AddControllers(); @@ -67,7 +84,6 @@ try builder.Environment.IsDevelopment(); }); - // EN: Add Swagger / VI: Thêm Swagger // EN: Add Swagger / VI: Thêm Swagger builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => @@ -81,6 +97,33 @@ try // EN: Enable annotations / VI: Bật annotations options.EnableAnnotations(); + + // EN: Add JWT Bearer Authentication to Swagger + // VI: Thêm JWT Bearer Authentication vào Swagger + options.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme.\r\n\r\nEnter your token (without 'Bearer ' prefix).\r\n\r\nExample: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...'", + Name = "Authorization", + In = Microsoft.OpenApi.Models.ParameterLocation.Header, + Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT" + }); + + options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement + { + { + new Microsoft.OpenApi.Models.OpenApiSecurityScheme + { + Reference = new Microsoft.OpenApi.Models.OpenApiReference + { + Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); }); // EN: Add health checks / VI: Thêm health checks @@ -122,6 +165,10 @@ try app.UseCors(); app.UseRouting(); + // EN: Authentication and Authorization / VI: Xác thực và phân quyền + app.UseAuthentication(); + app.UseAuthorization(); + // EN: Map health check endpoints / VI: Map health check endpoints app.MapHealthChecks("/health"); app.MapHealthChecks("/health/live", new() diff --git a/services/storage-service-net/src/StorageService.API/StorageService.API.csproj b/services/storage-service-net/src/StorageService.API/StorageService.API.csproj index fe5948ee..6d18bd58 100644 --- a/services/storage-service-net/src/StorageService.API/StorageService.API.csproj +++ b/services/storage-service-net/src/StorageService.API/StorageService.API.csproj @@ -16,6 +16,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all