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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user