feat(authentication): Integrate JWT Bearer authentication and Swagger enhancements

- Added JWT Bearer authentication configuration in `Program.cs` for IAM service integration.
- Updated Swagger setup to include JWT Bearer security definition and requirements.
- Introduced a new Swagger UI client for testing with resource owner password grant type in `Config.cs`.
- Included necessary package reference for `Microsoft.AspNetCore.Authentication.JwtBearer` in the project file.
This commit is contained in:
Ho Ngoc Hai
2026-01-13 12:24:41 +07:00
parent c7ca541c5f
commit afb756681e
6 changed files with 244 additions and 1 deletions

View File

@@ -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<TokenEndpointDocumentFilter>();
});
// EN: Add health checks / VI: Thêm health checks

View File

@@ -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;
/// <summary>
/// EN: Adds OAuth2 /connect/token endpoint to Swagger documentation.
/// VI: Thêm OAuth2 /connect/token endpoint vào Swagger documentation.
/// </summary>
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<OpenApiTag> { 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<string, OpenApiMediaType>
{
["application/x-www-form-urlencoded"] = new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = "object",
Required = new HashSet<string> { "grant_type", "client_id" },
Properties = new Dictionary<string, OpenApiSchema>
{
["grant_type"] = new OpenApiSchema
{
Type = "string",
Description = "OAuth2 grant type",
Enum = new List<IOpenApiAny>
{
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<string, OpenApiMediaType>
{
["application/json"] = new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>
{
["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);
}
}

View File

@@ -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}

View File

@@ -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
}
];
}

View File

@@ -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<string>()
}
});
});
// 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()

View File

@@ -16,6 +16,7 @@
<!-- EN: FluentValidation for request validation / VI: FluentValidation cho validation request -->
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>