├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ ├── dotnet-format.yml
│ ├── dotnetcore.yml
│ └── release.yaml
├── .gitignore
├── .vscode
├── launch.json
└── tasks.json
├── Directory.Build.props
├── Directory.Packages.props
├── LICENSE
├── README.md
├── WeihanLi.Web.Extensions.sln.DotSettings
├── WeihanLi.Web.Extensions.slnx
├── azure-pipelines.yml
├── build.ps1
├── build.sh
├── build
├── build.cs
├── version.props
└── weihanli.snk
├── docfx.json
├── docs
└── ReleaseNotes.md
├── icon.jpg
├── nuget.config
├── samples
└── WeihanLi.Web.Extensions.Samples
│ ├── AuthTestController.cs
│ ├── Filters
│ └── TestAuthFilter.cs
│ ├── LogInterceptor.cs
│ ├── McpToolExtension.cs
│ ├── Program.cs
│ ├── ValuesController.cs
│ ├── WeihanLi.Web.Extensions.Samples.csproj
│ ├── appsettings.Development.json
│ └── appsettings.json
├── src
├── Directory.Build.props
└── WeihanLi.Web.Extensions
│ ├── AccessControlHelper
│ ├── AccessControlAttribute.cs
│ ├── AccessControlAuthorizationHandler.cs
│ ├── AccessControlHelperBuilder.cs
│ ├── AccessControlHelperConstants.cs
│ ├── AccessControlHelperExtension.cs
│ ├── AccessControlHelperMiddleware.cs
│ ├── AccessControlOptions.cs
│ ├── AccessControlRequirement.cs
│ ├── AccessControlTagHelper.cs
│ ├── HtmlHelperExtension.cs
│ ├── IControlAccessStrategy.cs
│ ├── IResourceAccessStrategy.cs
│ ├── NoAccessControlAttribute.cs
│ └── SparkContainer.cs
│ ├── Authentication
│ ├── ApiKeyAuthentication
│ │ ├── ApiKeyAuthenticationDefaults.cs
│ │ ├── ApiKeyAuthenticationHandler.cs
│ │ └── ApiKeyAuthenticationOptions.cs
│ ├── AuthenticationBuilderExtension.cs
│ ├── BasicAuthentication
│ │ ├── BasicAuthenticationDefaults.cs
│ │ ├── BasicAuthenticationHandler.cs
│ │ └── BasicAuthenticationOptions.cs
│ ├── DelegateAuthentication
│ │ ├── DelegateAuthenticationDefaults.cs
│ │ ├── DelegateAuthenticationHandler.cs
│ │ └── DelegateAuthenticationOptions.cs
│ ├── HeaderAuthentication
│ │ ├── HeaderAuthenticationDefaults.cs
│ │ ├── HeaderAuthenticationHandler.cs
│ │ └── HeaderAuthenticationOptions.cs
│ └── QueryAuthentication
│ │ ├── QueryAuthenticationDefaults.cs
│ │ ├── QueryAuthenticationHandler.cs
│ │ └── QueryAuthenticationOptions.cs
│ ├── Authorization
│ ├── Jwt
│ │ ├── DependencyInjectionExtensions.cs
│ │ ├── JsonWebTokenOptions.cs
│ │ ├── JsonWebTokenOptionsSetup.cs
│ │ ├── JsonWebTokenService.cs
│ │ └── JwtBearerOptionsPostSetup.cs
│ └── Token
│ │ ├── ITokenService.cs
│ │ └── TokenEntity.cs
│ ├── DataProtection
│ ├── DataProtectionBuilderExtensions.cs
│ └── ParamsProtection
│ │ ├── ParamsProtectionHelper.cs
│ │ ├── ParamsProtectionOptions.cs
│ │ ├── ParamsProtectionResourceFilter.cs
│ │ └── ParamsProtectionResultFilter.cs
│ ├── Extensions
│ ├── DependenceResolverExtension.cs
│ ├── EndpointExtensions.cs
│ ├── FluentAspectServiceProviderFactory.cs
│ ├── HealthCheckExtension.cs
│ ├── HttpContextExtension.cs
│ ├── HttpContextTenantProviderExtension.cs
│ ├── HttpContextUserIdProviderExtension.cs
│ ├── MiddlewareExtension.cs
│ └── ResultExtensions.cs
│ ├── Filters
│ ├── ApiResultFilter.cs
│ ├── AuthorizationFilterAttribute.cs
│ ├── ConditionalFilter.cs
│ ├── EnvironmentFilter.cs
│ └── FeatureFlagFilter.cs
│ ├── Formatters
│ └── PlainTextInputFormatter.cs
│ ├── Middleware
│ ├── ConfigInspectorMiddleware.cs
│ └── CustomExceptionHandlerMiddleware.cs
│ ├── Pager
│ ├── IPagedListModel.cs
│ ├── IPagerModel.cs
│ ├── PagedListModel.cs
│ ├── PagedListModelExtension.cs
│ ├── PagerHelper.cs
│ └── PagerModel.cs
│ ├── Services
│ ├── HttpContextCancellationTokenProvider.cs
│ ├── HttpContextLoggingEnricher.cs
│ ├── HttpContextTenantProvider.cs
│ └── HttpContextUserIdProvider.cs
│ └── WeihanLi.Web.Extensions.csproj
└── toc.yml
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome:http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Don't use tabs for indentation.
7 | [*]
8 | indent_style = space
9 | # (Please don't specify an indent_size here; that has too many unintended consequences.)
10 |
11 | # Code files
12 | [*.{cs,csx,vb,vbx}]
13 | indent_size = 4
14 | insert_final_newline = true
15 | charset = utf-8-bom
16 |
17 | # Xml project files
18 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
19 | indent_size = 2
20 |
21 | # Xml config files
22 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
23 | indent_size = 2
24 |
25 | # JSON files
26 | [*.json]
27 | indent_size = 2
28 |
29 | # Dotnet code style settings:
30 | [*.{cs,vb}]
31 | # File header
32 | file_header_template = Copyright (c) Weihan Li. All rights reserved.\nLicensed under the MIT license.
33 |
34 | # Sort using and Import directives with System.* appearing first
35 | dotnet_sort_system_directives_first = false
36 | # Avoid "this." and "Me." if not necessary
37 | dotnet_style_qualification_for_field = false:suggestion
38 | dotnet_style_qualification_for_property = false:suggestion
39 | dotnet_style_qualification_for_method = false:suggestion
40 | dotnet_style_qualification_for_event = false:suggestion
41 |
42 | # Use language keywords instead of framework type names for type references
43 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
44 | dotnet_style_predefined_type_for_member_access = true:suggestion
45 |
46 | # Suggest more modern language features when available
47 | dotnet_style_object_initializer = true:suggestion
48 | dotnet_style_collection_initializer = true:suggestion
49 | dotnet_style_coalesce_expression = true:suggestion
50 | dotnet_style_null_propagation = true:suggestion
51 | dotnet_style_explicit_tuple_names = true:suggestion
52 |
53 | # CSharp code style settings:
54 | [*.cs]
55 | # namespace style
56 | csharp_style_namespace_declarations=file_scoped:warning
57 |
58 | # Prefer "var" everywhere
59 | csharp_style_var_for_built_in_types = true:suggestion
60 | csharp_style_var_when_type_is_apparent = true:suggestion
61 | csharp_style_var_elsewhere = true:suggestion
62 |
63 | # Prefer method-like constructs to have a block body
64 | csharp_style_expression_bodied_methods = false:none
65 | csharp_style_expression_bodied_constructors = false:none
66 | csharp_style_expression_bodied_operators = false:none
67 |
68 | # Prefer property-like constructs to have an expression-body
69 | csharp_style_expression_bodied_properties = true:none
70 | csharp_style_expression_bodied_indexers = true:none
71 | csharp_style_expression_bodied_accessors = true:none
72 |
73 | # Suggest more modern language features when available
74 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
75 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
76 | csharp_style_inlined_variable_declaration = true:suggestion
77 | csharp_style_throw_expression = true:suggestion
78 | csharp_style_conditional_delegate_call = true:suggestion
79 |
80 | # Newline settings
81 | csharp_new_line_before_open_brace = all
82 | csharp_new_line_before_else = true
83 | csharp_new_line_before_catch = true
84 | csharp_new_line_before_finally = true
85 | csharp_new_line_before_members_in_object_initializers = true
86 | csharp_new_line_before_members_in_anonymous_types = true
87 |
88 | # Fix formatting, https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/formatting-rules#rule-id-ide0055-fix-formatting
89 | dotnet_diagnostic.IDE00055.severity = warning
90 | # File header template
91 | dotnet_diagnostic.IDE0073.severity = warning
92 |
93 | # https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/unnecessary-code-rules
94 | # Remove unnecessary import
95 | dotnet_diagnostic.IDE0005.severity = warning
96 | # Private member is unused
97 | dotnet_diagnostic.IDE0051.severity = warning
98 | # Private member is unread
99 | dotnet_diagnostic.IDE0052.severity = warning
100 |
101 | # https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/
102 | # Avoid unused private fields
103 | dotnet_diagnostic.CA1823.severity = warning
104 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet-format.yml:
--------------------------------------------------------------------------------
1 | name: dotnet-format
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 | - "dev"
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Setup .NET SDK
15 | uses: actions/setup-dotnet@v4
16 | with:
17 | dotnet-version: |
18 | 8.0.x
19 | 9.0.x
20 | 10.0.x
21 | - name: format
22 | run: dotnet format
23 | - name: check for changes
24 | run: |
25 | if git diff --exit-code; then
26 | echo "has_changes=false" >> $GITHUB_ENV
27 | else
28 | echo "has_changes=true" >> $GITHUB_ENV
29 | fi
30 | - name: Commit and Push
31 | if: ${{ env.has_changes == 'true' }}
32 | shell: bash
33 | run: |
34 | # echo $GITHUB_REF_NAME
35 | # echo $GITHUB_SHA
36 | git config --local user.name "github-actions[bot]"
37 | git config --local user.email "weihanli@outlook.com"
38 | git add -u
39 | git commit -m "Automated dotnet-format update from commit ${GITHUB_SHA} on ${GITHUB_REF}"
40 | git log -1
41 | remote_repo="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git"
42 | git push "${remote_repo}" HEAD:${GITHUB_REF}
43 |
--------------------------------------------------------------------------------
/.github/workflows/dotnetcore.yml:
--------------------------------------------------------------------------------
1 | name: dotnetcore
2 |
3 | on: [push]
4 |
5 | jobs:
6 | # Label of the container job
7 | default:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | - name: Setup .NET SDK
12 | uses: actions/setup-dotnet@v4
13 | with:
14 | dotnet-version: |
15 | 8.0.x
16 | 9.0.x
17 | 10.0.x
18 | - name: dotnet info
19 | run: dotnet --info
20 | - name: build
21 | run: dotnet build
22 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches: [ master ]
6 | jobs:
7 | build:
8 | name: Release
9 | runs-on: windows-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Setup .NET SDK
13 | uses: actions/setup-dotnet@v4
14 | with:
15 | dotnet-version: |
16 | 8.0.x
17 | 9.0.x
18 | 10.0.x
19 | - name: Build
20 | shell: pwsh
21 | run: .\build.ps1 --stable=true
22 | - name: Get Release Version
23 | shell: pwsh
24 | run: dotnet-exec https://github.com/OpenReservation/scripts/blob/main/build/export-gh-release-version.cs
25 | - name: create release
26 | shell: pwsh
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | run: |
30 | gh release create ${{ env.ReleaseVersion }} --generate-notes --target master (Get-Item ./artifacts/packages/*.nupkg)
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.[Pp]ublish.xml
150 | *.azurePubxml
151 | # TODO: Comment the next line if you want to checkin your web deploy settings
152 | # but database connection strings (with potential passwords) will be unencrypted
153 | *.pubxml
154 | *.publishproj
155 |
156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
157 | # checkin your Azure Web App publish settings, but sensitive information contained
158 | # in these scripts will be unencrypted
159 | PublishScripts/
160 |
161 | # NuGet Packages
162 | *.nupkg
163 | # The packages folder can be ignored because of Package Restore
164 | **/packages/*
165 | # except build/, which is used as an MSBuild target.
166 | !**/packages/build/
167 | # Uncomment if necessary however generally it will be regenerated when needed
168 | #!**/packages/repositories.config
169 | # NuGet v3's project.json files produces more ignorable files
170 | *.nuget.props
171 | *.nuget.targets
172 |
173 | # Microsoft Azure Build Output
174 | csx/
175 | *.build.csdef
176 |
177 | # Microsoft Azure Emulator
178 | ecf/
179 | rcf/
180 |
181 | # Windows Store app package directories and files
182 | AppPackages/
183 | BundleArtifacts/
184 | Package.StoreAssociation.xml
185 | _pkginfo.txt
186 |
187 | # Visual Studio cache files
188 | # files ending in .cache can be ignored
189 | *.[Cc]ache
190 | # but keep track of directories ending in .cache
191 | !*.[Cc]ache/
192 |
193 | # Others
194 | ClientBin/
195 | ~$*
196 | *~
197 | *.dbmdl
198 | *.dbproj.schemaview
199 | *.jfm
200 | *.pfx
201 | *.publishsettings
202 | orleans.codegen.cs
203 |
204 | # Since there are multiple workflows, uncomment next line to ignore bower_components
205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
206 | #bower_components/
207 |
208 | # RIA/Silverlight projects
209 | Generated_Code/
210 |
211 | # Backup & report files from converting an old project file
212 | # to a newer Visual Studio version. Backup files are not needed,
213 | # because we have git ;-)
214 | _UpgradeReport_Files/
215 | Backup*/
216 | UpgradeLog*.XML
217 | UpgradeLog*.htm
218 |
219 | # SQL Server files
220 | *.mdf
221 | *.ldf
222 | *.ndf
223 |
224 | # Business Intelligence projects
225 | *.rdl.data
226 | *.bim.layout
227 | *.bim_*.settings
228 |
229 | # Microsoft Fakes
230 | FakesAssemblies/
231 |
232 | # GhostDoc plugin setting file
233 | *.GhostDoc.xml
234 |
235 | # Node.js Tools for Visual Studio
236 | .ntvs_analysis.dat
237 | node_modules/
238 |
239 | # Typescript v1 declaration files
240 | typings/
241 |
242 | # Visual Studio 6 build log
243 | *.plg
244 |
245 | # Visual Studio 6 workspace options file
246 | *.opt
247 |
248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
249 | *.vbw
250 |
251 | # Visual Studio LightSwitch build output
252 | **/*.HTMLClient/GeneratedArtifacts
253 | **/*.DesktopClient/GeneratedArtifacts
254 | **/*.DesktopClient/ModelManifest.xml
255 | **/*.Server/GeneratedArtifacts
256 | **/*.Server/ModelManifest.xml
257 | _Pvt_Extensions
258 |
259 | # Paket dependency manager
260 | .paket/paket.exe
261 | paket-files/
262 |
263 | # FAKE - F# Make
264 | .fake/
265 |
266 | # JetBrains Rider
267 | .idea/
268 | *.sln.iml
269 |
270 | # CodeRush
271 | .cr/
272 |
273 | # Python Tools for Visual Studio (PTVS)
274 | __pycache__/
275 | *.pyc
276 |
277 | # Cake - Uncomment if you are using it
278 | tools/**
279 | build/tools/**
280 |
281 | # Telerik's JustMock configuration file
282 | *.jmconfig
283 |
284 | # BizTalk build output
285 | *.btp.cs
286 | *.btm.cs
287 | *.odx.cs
288 | *.xsd.cs
289 |
290 | # docfx
291 | _site
292 | /**/DROP/
293 | /**/TEMP/
294 | docs/api/*
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (web)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | "program": "${workspaceFolder}/samples/WeihanLi.Web.Extensions.Samples/bin/Debug/net7.0/WeihanLi.Web.Extensions.Samples.dll",
13 | "args": [],
14 | "cwd": "${workspaceFolder}/samples/WeihanLi.Web.Extensions.Samples",
15 | "stopAtEntry": false,
16 | "serverReadyAction": {
17 | "action": "openExternally",
18 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
19 | },
20 | "env": {
21 | "ASPNETCORE_ENVIRONMENT": "Development"
22 | },
23 | "sourceFileMap": {
24 | "/Views": "${workspaceFolder}/Views"
25 | }
26 | },
27 | {
28 | "name": ".NET Core Attach",
29 | "type": "coreclr",
30 | "request": "attach"
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}/samples/WeihanLi.Web.Extensions.Samples/WeihanLi.Web.Extensions.Samples.csproj",
11 | "/property:GenerateFullPaths=true",
12 | "/consoleloggerparameters:NoSummary"
13 | ],
14 | "problemMatcher": "$msCompile"
15 | },
16 | {
17 | "label": "publish",
18 | "command": "dotnet",
19 | "type": "process",
20 | "args": [
21 | "publish",
22 | "${workspaceFolder}/samples/WeihanLi.Web.Extensions.Samples/WeihanLi.Web.Extensions.Samples.csproj",
23 | "/property:GenerateFullPaths=true",
24 | "/consoleloggerparameters:NoSummary"
25 | ],
26 | "problemMatcher": "$msCompile"
27 | },
28 | {
29 | "label": "watch",
30 | "command": "dotnet",
31 | "type": "process",
32 | "args": [
33 | "watch",
34 | "run",
35 | "--project",
36 | "${workspaceFolder}/samples/WeihanLi.Web.Extensions.Samples/WeihanLi.Web.Extensions.Samples.csproj"
37 | ],
38 | "problemMatcher": "$msCompile"
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | latest
5 | enable
6 | enable
7 | git
8 | https://github.com/WeihanLi/WeihanLi.Web.Extensions
9 | WeihanLi
10 | Copyright 2016-$([System.DateTime]::Now.Year) (c) WeihanLi
11 | $(NoWarn);NU5048;CS1591
12 | net10.0
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | true
5 |
6 | true
7 | all
8 |
9 | NU1901;NU1902;NU1903;NU1904
10 | 8.0.16
11 | 9.0.5
12 | 10.0.0-preview.4.25258.110
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2017-2024 [WeihanLi]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeihanLi.Web.Extensions
2 |
3 | ASP.NET Core Web extensions
4 |
5 | [](https://www.nuget.org/packages/WeihanLi.Web.Extensions/)
6 |
7 | [](https://www.nuget.org/packages/WeihanLi.Web.Extensions/absoluteLatest)
8 |
9 | [](https://weihanli.visualstudio.com/Pipelines/_build/latest?definitionId=19&branchName=dev)
10 |
11 | [](https://github.com/WeihanLi/WeihanLi.Web.Extensions/actions?query=workflow%3Adotnetcore)
12 |
13 | ## Features
14 |
15 | - MVC Pager
16 | - Data Protection
17 | - Authorization
18 | - Access Control Helper
19 | - JwtTokenService
20 | - Authentication
21 | - Query
22 | - Header
23 | - ApiKey
24 | - Basic
25 | - Delegate
26 | - Feature Flag
27 | - `HttpContext` extensions
28 |
--------------------------------------------------------------------------------
/WeihanLi.Web.Extensions.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
--------------------------------------------------------------------------------
/WeihanLi.Web.Extensions.slnx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | branches:
3 | include:
4 | - '*' # must quote since "*" is a YAML reserved character; we want a string
5 |
6 | pool:
7 | vmImage: 'windows-latest'
8 |
9 | steps:
10 | - task: UseDotNet@2
11 | displayName: 'Use .NET 8 sdk'
12 | inputs:
13 | packageType: sdk
14 | version: 8.0.x
15 |
16 | - task: UseDotNet@2
17 | displayName: 'Use .NET 9 sdk'
18 | inputs:
19 | packageType: sdk
20 | version: 9.0.x
21 |
22 | - task: UseDotNet@2
23 | displayName: 'Use .NET 10 sdk'
24 | inputs:
25 | packageType: sdk
26 | version: 10.0.x
27 | includePreviewVersions: true
28 |
29 | - script: dotnet --info
30 | displayName: 'dotnet info'
31 |
32 | - powershell: ./build.ps1
33 | displayName: 'Powershell Script'
34 | env:
35 | NuGet__ApiKey: $(nugetApiKey)
36 | NuGet__SourceUrl: $(nugetSourceUrl)
37 |
--------------------------------------------------------------------------------
/build.ps1:
--------------------------------------------------------------------------------
1 | [string]$SCRIPT = '.\build\build.cs'
2 |
3 | # Install dotnet tool
4 | dotnet tool install --global dotnet-execute
5 |
6 | Write-Host "dotnet-exec $SCRIPT --args $ARGS" -ForegroundColor GREEN
7 |
8 | dotnet-exec $SCRIPT --args $ARGS
9 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | SCRIPT='./build/build.cs'
3 |
4 | # Install tool
5 | dotnet tool install --global dotnet-execute
6 | export PATH="$PATH:$HOME/.dotnet/tools"
7 |
8 | echo "dotnet-exec $SCRIPT --args=$@"
9 |
10 | dotnet-exec $SCRIPT --args="$@"
11 |
--------------------------------------------------------------------------------
/build/build.cs:
--------------------------------------------------------------------------------
1 | var target = CommandLineParser.Val("target", args, "Default");
2 | var apiKey = CommandLineParser.Val("apiKey", args);
3 | var stable = CommandLineParser.BooleanVal("stable", args);
4 | var noPush = CommandLineParser.BooleanVal("noPush", args);
5 | var branchName = EnvHelper.Val("BUILD_SOURCEBRANCHNAME", "local");
6 |
7 | var solutionPath = "./WeihanLi.Web.Extensions.slnx";
8 | string[] srcProjects = [
9 | "./src/WeihanLi.Web.Extensions/WeihanLi.Web.Extensions.csproj"
10 | ];
11 | string[] testProjects = [ "./test/WeihanLi.Web.Extensions.Test/WeihanLi.Web.Extensions.Test.csproj" ];
12 |
13 | await new BuildProcessBuilder()
14 | .WithSetup(() =>
15 | {
16 | // cleanup artifacts
17 | if (Directory.Exists("./artifacts/packages"))
18 | Directory.Delete("./artifacts/packages", true);
19 |
20 | // args
21 | Console.WriteLine("Arguments");
22 | Console.WriteLine($" {args.StringJoin(" ")}");
23 | })
24 | .WithTaskExecuting(task => Console.WriteLine($@"===== Task {task.Name} {task.Description} executing ======"))
25 | .WithTaskExecuted(task => Console.WriteLine($@"===== Task {task.Name} {task.Description} executed ======"))
26 | .WithTask("hello", b => b.WithExecution(() => Console.WriteLine("Hello dotnet-exec build")))
27 | .WithTask("build", b =>
28 | {
29 | b.WithDescription("dotnet build")
30 | .WithExecution(() => ExecuteCommandAsync($"dotnet build {solutionPath}"))
31 | ;
32 | })
33 | .WithTask("test", b =>
34 | {
35 | b.WithDescription("dotnet test")
36 | .WithDependency("build")
37 | .WithExecution(async () =>
38 | {
39 | foreach (var project in testProjects)
40 | {
41 | await ExecuteCommandAsync($"dotnet test --collect:\"XPlat Code Coverage;Format=cobertura,opencover;ExcludeByAttribute=ExcludeFromCodeCoverage,Obsolete,GeneratedCode,CompilerGeneratedAttribute\" {project}");
42 | }
43 | })
44 | ;
45 | })
46 | .WithTask("pack", b => b.WithDescription("dotnet pack")
47 | .WithDependency("build")
48 | .WithExecution(async () =>
49 | {
50 | if (stable || branchName == "master")
51 | {
52 | foreach (var project in srcProjects)
53 | {
54 | await ExecuteCommandAsync($"dotnet pack {project} -o ./artifacts/packages");
55 | }
56 | }
57 | else
58 | {
59 | var suffix = $"preview-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
60 | foreach (var project in srcProjects)
61 | {
62 | await ExecuteCommandAsync($"dotnet pack {project} -o ./artifacts/packages --version-suffix {suffix}");
63 | }
64 | }
65 |
66 | if (noPush)
67 | {
68 | Console.WriteLine("Skip push there's noPush specified");
69 | return;
70 | }
71 |
72 | if (string.IsNullOrEmpty(apiKey))
73 | {
74 | // try to get apiKey from environment variable
75 | apiKey = Environment.GetEnvironmentVariable("NuGet__ApiKey");
76 |
77 | if (string.IsNullOrEmpty(apiKey))
78 | {
79 | Console.WriteLine("Skip push since there's no apiKey found");
80 | return;
81 | }
82 | }
83 |
84 | if (branchName != "master" && branchName != "preview")
85 | {
86 | Console.WriteLine($"Skip push since branch name {branchName} not support push packages");
87 | return;
88 | }
89 |
90 | // push nuget packages
91 | var source = Environment.GetEnvironmentVariable("NuGet__SourceUrl");
92 | var sourceConfig = string.IsNullOrEmpty(source) ? "" : $"-s {source}";
93 | foreach (var file in Directory.GetFiles("./artifacts/packages/", "*.nupkg"))
94 | {
95 | await ExecuteCommandAsync($"dotnet nuget push {file} -k {apiKey} --skip-duplicate {sourceConfig}", [new("$NuGet__ApiKey", apiKey)]);
96 | }
97 | }))
98 | .WithTask("Default", b => b.WithDependency("hello").WithDependency("pack"))
99 | .Build()
100 | .ExecuteAsync(target, ApplicationHelper.ExitToken);
101 |
102 | async Task ExecuteCommandAsync(string commandText, KeyValuePair[]? replacements = null)
103 | {
104 | var commandTextWithReplacements = commandText;
105 | if (replacements is { Length: > 0})
106 | {
107 | foreach (var item in replacements)
108 | {
109 | commandTextWithReplacements = commandTextWithReplacements.Replace(item.Value, item.Key);
110 | }
111 | }
112 | Console.WriteLine($"Executing command: \n {commandTextWithReplacements}");
113 | Console.WriteLine();
114 | var result = await CommandExecutor.ExecuteCommandAndOutputAsync(commandText);
115 | result.EnsureSuccessExitCode();
116 | Console.WriteLine();
117 | }
118 |
--------------------------------------------------------------------------------
/build/version.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | 2
4 | 2
5 | 0
6 | $(VersionMajor).$(VersionMinor).$(VersionPatch)
7 | develop
8 | $(PackageVersion)
9 |
10 |
11 |
--------------------------------------------------------------------------------
/build/weihanli.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeihanLi/WeihanLi.Web.Extensions/6b2a2bec9004785f68682275d552ee91ac4652bf/build/weihanli.snk
--------------------------------------------------------------------------------
/docfx.json:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": [
3 | {
4 | "src": [
5 | {
6 | "src": "./src",
7 | "files": [
8 | "**/*.csproj"
9 | ]
10 | }
11 | ],
12 | "dest": "docs/api"
13 | }
14 | ],
15 | "build": {
16 | "content": [
17 | {
18 | "files": [
19 | "**/*.{md,yml}"
20 | ],
21 | "exclude": [
22 | "_site/**"
23 | ]
24 | }
25 | ],
26 | "resource": [
27 | {
28 | "files": [
29 | "images/**"
30 | ]
31 | }
32 | ],
33 | "output": "_site",
34 | "template": [
35 | "default",
36 | "modern"
37 | ],
38 | "globalMetadata": {
39 | "_appName": "WeihanLi.Web.Extensions",
40 | "_appTitle": "WeihanLi.Web.Extensions",
41 | "_enableSearch": true,
42 | "pdf": false
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/docs/ReleaseNotes.md:
--------------------------------------------------------------------------------
1 | # Release Notes
2 |
3 | see pull requests: https://github.com/WeihanLi/WeihanLi.Web.Extensions/pulls?q=is%3Apr+is%3Aclosed+is%3Amerged
4 |
5 | ## 1.4.0
6 |
7 | - Add Basic auth support
8 | - Add `net7.0` target
9 | - Support `IEndpointFilter` for `ApiResultFilter`
10 | - Add `ConditionalFilter`/`EnvironmentFilter`/`NonProductionEnvironmentFilter`
11 |
12 | ## 1.3.0
13 |
14 | - Add `ApiResultFilter`
15 |
16 | ## 1.2.1
17 |
18 | - add FluentAspectServiceProviderFactory
19 | - Add FeatureFlag extensions
20 | - Update HealthCheckExtension
21 | - Add Pager/DataProtection/AccessControlHelper
22 | - Add HttpContextTenantProvider
23 | - Add ApiKeyAuthentication Support, fixes Add API-Key based authentication #15
24 | - Add AuthorizationFilterAttribute
25 | - Implement JwtTokenService, fixes Simplify JWT usage #16
26 |
27 | ## 1.1.0
28 |
29 | - Header/Quey Auth
30 | - more web extensions
31 |
32 | ## [1.0.0](https://www.nuget.org/packages/WeihanLi.Web.Extensions/1.0.0)
33 |
34 | Init
35 |
--------------------------------------------------------------------------------
/icon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeihanLi/WeihanLi.Web.Extensions/6b2a2bec9004785f68682275d552ee91ac4652bf/icon.jpg
--------------------------------------------------------------------------------
/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/samples/WeihanLi.Web.Extensions.Samples/AuthTestController.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Mvc;
5 | using WeihanLi.Web.Extensions.Samples.Filters;
6 |
7 | namespace WeihanLi.Web.Extensions.Samples;
8 |
9 | [TestAuthFilter(Role = "Admin")]
10 | [Route("api/authTest")]
11 | public class AuthTestController : ControllerBase
12 | {
13 | [TestAuthFilter(Role = "User")]
14 | [HttpGet]
15 | public IActionResult Index()
16 | {
17 | return Ok();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/samples/WeihanLi.Web.Extensions.Samples/Filters/TestAuthFilter.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Mvc.Filters;
5 | using WeihanLi.Web.Filters;
6 |
7 | namespace WeihanLi.Web.Extensions.Samples.Filters;
8 |
9 | public class TestAuthFilter : AuthorizationFilterAttribute
10 | {
11 | public required string Role { get; set; }
12 |
13 | public override void OnAuthorization(AuthorizationFilterContext context)
14 | {
15 | if (!context.IsEffectivePolicy(this)) return;
16 | Console.WriteLine($"{nameof(TestAuthFilter)}({Role}) is executing");
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/samples/WeihanLi.Web.Extensions.Samples/LogInterceptor.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using System.Diagnostics;
5 | using WeihanLi.Common.Aspect;
6 | using WeihanLi.Extensions;
7 |
8 | namespace WeihanLi.Web.Extensions.Samples;
9 |
10 | public class EventPublishLogInterceptor : AbstractInterceptor
11 | {
12 | public override async Task Invoke(IInvocation invocation, Func next)
13 | {
14 | Console.WriteLine("-------------------------------");
15 | Console.WriteLine($"Method {invocation.Method?.Name} invoke begin, eventData:{invocation.Arguments.ToJson()}");
16 | var watch = Stopwatch.StartNew();
17 | try
18 | {
19 | await next();
20 | }
21 | catch (Exception ex)
22 | {
23 | Console.WriteLine($"Method {invocation.Method?.Name} invoke exception({ex})");
24 | }
25 | finally
26 | {
27 | watch.Stop();
28 | Console.WriteLine($"Method {invocation.Method?.Name} invoke complete, elasped:{watch.ElapsedMilliseconds} ms");
29 | }
30 | Console.WriteLine("-------------------------------");
31 | }
32 | }
33 |
34 | public class EventHandleLogInterceptor : IInterceptor
35 | {
36 | public async Task Invoke(IInvocation invocation, Func next)
37 | {
38 | Console.WriteLine("-------------------------------");
39 | Console.WriteLine($"Event handle begin, eventData:{invocation.Arguments.ToJson()}");
40 | var watch = Stopwatch.StartNew();
41 | try
42 | {
43 | await next();
44 | }
45 | catch (Exception ex)
46 | {
47 | Console.WriteLine($"Event handle exception({ex})");
48 | }
49 | finally
50 | {
51 | watch.Stop();
52 | Console.WriteLine($"Event handle complete, elasped:{watch.ElapsedMilliseconds} ms");
53 | }
54 | Console.WriteLine("-------------------------------");
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/samples/WeihanLi.Web.Extensions.Samples/McpToolExtension.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.Extensions.DependencyInjection.Extensions;
5 | using ModelContextProtocol.Server;
6 | using System.Diagnostics;
7 |
8 | namespace WeihanLi.Web.Extensions.Samples;
9 |
10 | public interface IMcpToolEndpointMetadata
11 | {
12 | string Name { get; set; }
13 | string Description { get; set; }
14 | }
15 |
16 | public class McpToolEndpointMetadata : IMcpToolEndpointMetadata
17 | {
18 | public string Name { get; set; } = string.Empty;
19 | public string Description { get; set; } = string.Empty;
20 | }
21 |
22 | public class McpServerEndpointConfigureOptions(EndpointDataSource endpointDataSource, IServiceProvider services) : IConfigureOptions
23 | {
24 | public void Configure(McpServerOptions options)
25 | {
26 | options.Capabilities ??= new();
27 | options.Capabilities.Tools.ToolCollection ??= new();
28 |
29 | foreach (var endpoint in endpointDataSource.Endpoints)
30 | {
31 | if (!endpoint.Metadata.Any(m => m is IMcpToolEndpointMetadata))
32 | continue;
33 |
34 | Debug.Assert(endpoint.RequestDelegate is not null);
35 |
36 | var tool = McpServerTool.Create(endpoint.RequestDelegate.Method, typeof(HttpContext), new McpServerToolCreateOptions
37 | {
38 | Services = services
39 | });
40 | options.Capabilities.Tools.ToolCollection.Add(tool);
41 | }
42 | }
43 | }
44 |
45 | public static class McpToolExtension
46 | {
47 | public static IMcpServerBuilder WithEndpointTools(this IMcpServerBuilder builder)
48 | {
49 | ArgumentNullException.ThrowIfNull(builder);
50 |
51 | builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, McpServerEndpointConfigureOptions>());
52 | return builder;
53 | }
54 |
55 | public static IEndpointConventionBuilder AsMcpTool(this TBuilder builder, Action? toolConfigure = null)
56 | where TBuilder : IEndpointConventionBuilder
57 | {
58 | ArgumentNullException.ThrowIfNull(builder);
59 | var metadata = new McpToolEndpointMetadata();
60 | toolConfigure?.Invoke(metadata);
61 | builder.Add(c =>
62 | {
63 | if (string.IsNullOrEmpty(metadata.Name))
64 | {
65 | metadata.Name = c.DisplayName!;
66 | }
67 | if (string.IsNullOrEmpty(metadata.Description))
68 | {
69 | metadata.Description = c.DisplayName!;
70 | }
71 | c.Metadata.Add(metadata);
72 | });
73 | return builder;
74 | }
75 | }
76 |
77 | internal sealed class McpInvocation(IHttpContextAccessor contextAccessor)
78 | {
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/samples/WeihanLi.Web.Extensions.Samples/Program.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.IdentityModel.Tokens;
5 | using Scalar.AspNetCore;
6 | using System.Security.Claims;
7 | using System.Text.Json.Serialization;
8 | using WeihanLi.Common.Aspect;
9 | using WeihanLi.Common.Models;
10 | using WeihanLi.Extensions;
11 | using WeihanLi.Web.Authentication;
12 | using WeihanLi.Web.Authentication.ApiKeyAuthentication;
13 | using WeihanLi.Web.Authentication.HeaderAuthentication;
14 | using WeihanLi.Web.Authorization.Jwt;
15 | using WeihanLi.Web.Extensions;
16 | using WeihanLi.Web.Extensions.Samples;
17 | using WeihanLi.Web.Filters;
18 | using WeihanLi.Web.Formatters;
19 |
20 | var builder = WebApplication.CreateBuilder(args);
21 |
22 | builder.Services.AddAuthentication(HeaderAuthenticationDefaults.AuthenticationScheme)
23 | .AddJwtBearer()
24 | .AddBasic(options =>
25 | {
26 | options.UserName = "test";
27 | options.Password = "test";
28 | })
29 | .AddQuery(options => { options.UserIdQueryKey = "uid"; })
30 | .AddHeader(options =>
31 | {
32 | options.UserIdHeaderName = "X-UserId";
33 | options.UserNameHeaderName = "X-UserName";
34 | options.UserRolesHeaderName = "X-UserRoles";
35 | })
36 | .AddApiKey(options =>
37 | {
38 | options.ClaimsIssuer = "https://id.weihanli.xyz";
39 | options.ApiKey = "123456";
40 | options.ApiKeyName = "X-ApiKey";
41 | options.KeyLocation = KeyLocation.HeaderOrQuery;
42 | })
43 | .AddDelegate(options =>
44 | {
45 | options.Validator = c => (c.Request.Headers.TryGetValue("x-delegate-key", out var values) && values.ToString().Equals("test"))
46 | .WrapTask();
47 | options.ClaimsGenerator = c =>
48 | {
49 | Claim[] claims =
50 | [
51 | new (ClaimTypes.Name, "test")
52 | ];
53 | return Task.FromResult>(claims);
54 | };
55 | })
56 | ;
57 | builder.Services.AddJwtServiceWithJwtBearerAuth(options =>
58 | {
59 | options.SecretKey = Guid.NewGuid().ToString();
60 | options.Issuer = "https://id.weihanli.xyz";
61 | options.Audience = "SparkTodo";
62 | // EnableRefreshToken, disabled by default
63 | options.EnableRefreshToken = true;
64 | // Renew refresh token always
65 | // options.RenewRefreshTokenPredicate = _ => true;
66 | options.RefreshTokenSigningCredentialsFactory = () =>
67 | new SigningCredentials(
68 | new SymmetricSecurityKey(WeihanLi.Common.Services.GuidIdGenerator.Instance.NewId().GetBytes()),
69 | SecurityAlgorithms.HmacSha256
70 | );
71 | });
72 | builder.Services.AddAuthorization(options =>
73 | {
74 | options.AddPolicy("Basic",
75 | policyBuilder => policyBuilder.AddAuthenticationSchemes("Basic").RequireAuthenticatedUser());
76 | });
77 |
78 | builder.Services.AddControllers(options =>
79 | {
80 | options.InputFormatters.Add(new PlainTextInputFormatter());
81 | }).AddJsonOptions(options =>
82 | {
83 | options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
84 | });
85 | builder.Services.AddHttpContextUserIdProvider(options =>
86 | {
87 | options.UserIdFactory = static context => $"{context.GetUserIP()}";
88 | });
89 |
90 | builder.Services.AddEndpointsApiExplorer();
91 | builder.Services.AddOpenApi();
92 |
93 | builder.Host.UseFluentAspectsServiceProviderFactory(options =>
94 | {
95 | options.InterceptAll()
96 | .With();
97 | }, ignoreTypesPredict: t => t.HasNamespace() && (
98 | t.Namespace!.StartsWith("Microsoft.")
99 | || t.Namespace.StartsWith("System.")
100 | || t.Namespace.StartsWith("Swashbuckle.")
101 | )
102 | );
103 |
104 | builder.Services.AddMcpServer()
105 | .WithEndpointTools()
106 | .WithToolsFromAssembly()
107 | .WithHttpTransport()
108 | ;
109 |
110 | var app = builder.Build();
111 |
112 | app.MapRuntimeInfo().ShortCircuit();
113 | app.Map("/Hello", () => "Hello Minimal API!").AddEndpointFilter();
114 | app.Map("/HelloV2", Hello).AddEndpointFilter();
115 | app.Map("/HelloV3", () => Results.Ok(new { Name = "test" })).AddEndpointFilter();
116 | app.Map("/HelloV4", () => Results.Ok(Result.Success(new { Name = "test" }))).AddEndpointFilter();
117 | app.Map("/BadRequest", BadRequest).AddEndpointFilter();
118 | app.Map("/basic-auth-test", () => "Hello").RequireAuthorization("Basic");
119 |
120 | // conditional filter
121 | var conditionalTest = app.MapGroup("/conditional");
122 | conditionalTest.Map("/NotFound", () => "Not Found")
123 | .AddEndpointFilter(new EnvironmentFilter("Production"));
124 | conditionalTest.Map("/Dynamic", () => "You get it")
125 | .AddEndpointFilter(new ConditionalFilter()
126 | {
127 | ConditionFunc = c => c.Request.Query.TryGetValue("enable", out _),
128 | ResultFactory = c => Results.NotFound(new { c.Request.QueryString })
129 | });
130 |
131 | var testGroup1 = app.MapGroup("/test1");
132 | testGroup1.Map("/hello", () => "Hello");
133 | testGroup1.Map("/world", () => "World");
134 | testGroup1.AddEndpointFilter();
135 |
136 |
137 | var envGroup = app.MapGroup("/env-test");
138 | envGroup.Map("/dev", () => "env-test")
139 | .AddEndpointFilter(new EnvironmentFilter(Environments.Development));
140 | envGroup.Map("/prod", () => "env-test")
141 | .AddEndpointFilter(new EnvironmentFilter(Environments.Production));
142 | // attribute endpoint filter not supported for now, https://github.com/dotnet/aspnetcore/issues/43421
143 | // envGroup.Map("/stage", [EnvironmentFilter("Staging")]() => "env-test");
144 |
145 | app.UseHealthCheck();
146 |
147 | app.MapOpenApi();
148 | app.MapScalarApiReference();
149 |
150 | app.UseAuthentication();
151 | app.UseAuthorization();
152 |
153 | // app.MapConfigInspector(optionsConfigure: options =>
154 | // {
155 | // options.ConfigRenderer = async (context, configs) =>
156 | // {
157 | // var htmlStart = """
158 | //
159 | //
160 | // Config Inspector
161 | //
162 | //
163 | //
164 | //
165 | //
166 | // Provider |
167 | // Key |
168 | // Value |
169 | // Active |
170 | //
171 | //
172 | //
173 | // """;
174 | // var htmlEnd = "
";
175 | // var tbody = new StringBuilder();
176 | // foreach (var config in configs)
177 | // {
178 | // tbody.Append($"{config.Provider} | ");
179 | // foreach (var item in config.Items)
180 | // {
181 | // tbody.Append(
182 | // $$"""{{item.Key}} | {{item.Value}} | | """);
183 | // }
184 | //
185 | // tbody.AppendLine("
");
186 | // }
187 | //
188 | // var responseText = $"{htmlStart}{tbody}{htmlEnd}";
189 | // await context.Response.WriteAsync(responseText);
190 | // };
191 | // });
192 |
193 | app.MapConfigInspector()
194 | .AsMcpTool(tool =>
195 | {
196 | tool.Description = "Get configurations";
197 | })
198 | // .RequireAuthorization(x => x
199 | // .AddAuthenticationSchemes("ApiKey")
200 | // .RequireAuthenticatedUser()
201 | // )
202 | ;
203 | app.MapControllers();
204 |
205 | app.MapGet("/endpoints", (EndpointDataSource endpointDataSource) =>
206 | {
207 | var tools = endpointDataSource.Endpoints.Where(x => x.Metadata.Any(m => m is McpToolEndpointMetadata))
208 | .Select(x => x.Metadata.OfType().First().Name)
209 | .ToArray();
210 | return new
211 | {
212 | endpoints = endpointDataSource.Endpoints.Select(x => x.DisplayName),
213 | tools
214 | };
215 | });
216 | app.MapMcp();
217 |
218 | await app.RunAsync();
219 |
220 |
221 | static string Hello() => "Hello Minimal API!";
222 |
223 | static IResult BadRequest() => Results.BadRequest();
224 |
--------------------------------------------------------------------------------
/samples/WeihanLi.Web.Extensions.Samples/ValuesController.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Authentication.JwtBearer;
5 | using Microsoft.AspNetCore.Authorization;
6 | using Microsoft.AspNetCore.Mvc;
7 | using System.ComponentModel.DataAnnotations;
8 | using System.Security.Claims;
9 | using WeihanLi.Common.Models;
10 | using WeihanLi.Common.Services;
11 | using WeihanLi.Web.Authentication.ApiKeyAuthentication;
12 | using WeihanLi.Web.Authentication.DelegateAuthentication;
13 | using WeihanLi.Web.Authentication.HeaderAuthentication;
14 | using WeihanLi.Web.Authentication.QueryAuthentication;
15 | using WeihanLi.Web.Authorization.Token;
16 | using WeihanLi.Web.Filters;
17 |
18 | namespace WeihanLi.Web.Extensions.Samples;
19 |
20 | [Route("api/values")]
21 | [ApiController]
22 | [ApiResultFilter]
23 | public class ValuesController : ControllerBase
24 | {
25 | private readonly IServiceScopeFactory _serviceScopeFactory;
26 |
27 | public ValuesController(IServiceScopeFactory serviceScopeFactory) => _serviceScopeFactory = serviceScopeFactory;
28 |
29 | [HttpGet("[action]")]
30 | public IActionResult ServiceScopeTest()
31 | {
32 | Task.Run(() =>
33 | {
34 | using var scope = _serviceScopeFactory.CreateScope();
35 | var tokenService = scope.ServiceProvider.GetRequiredService();
36 | Console.WriteLine(tokenService.GetHashCode());
37 | });
38 | return Ok();
39 | }
40 |
41 | [HttpGet]
42 | public async Task Get([FromServices] IUserIdProvider userIdProvider)
43 | {
44 | var headerAuthResult = await HttpContext.AuthenticateAsync(HeaderAuthenticationDefaults.AuthenticationScheme);
45 | var queryAuthResult = await HttpContext.AuthenticateAsync(QueryAuthenticationDefaults.AuthenticationScheme);
46 | var apiKeyAuthResult = await HttpContext.AuthenticateAsync(ApiKeyAuthenticationDefaults.AuthenticationScheme);
47 | var delegateAuthResult = await HttpContext.AuthenticateAsync(DelegateAuthenticationDefaults.AuthenticationScheme);
48 | var bearerAuthResult = await HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
49 |
50 | return Ok(new
51 | {
52 | userId = userIdProvider.GetUserId(),
53 | defaultAuthResult = User.Identity,
54 | headerAuthResult = headerAuthResult.Principal?.Identity,
55 | queryAuthResult = queryAuthResult.Principal?.Identity,
56 | apiKeyAuthResult = apiKeyAuthResult.Principal?.Identity,
57 | bearerAuthResult = bearerAuthResult.Principal?.Identity,
58 | delegateAuthResult = delegateAuthResult.Principal?.Identity,
59 | });
60 | }
61 |
62 | [HttpGet("apiKeyTest")]
63 | [Authorize(AuthenticationSchemes = ApiKeyAuthenticationDefaults.AuthenticationScheme)]
64 | public IActionResult ApiKeyAuthTest()
65 | {
66 | return Ok(User.Identity);
67 | }
68 |
69 | [HttpGet("[action]")]
70 | [FeatureFlagFilter("Flag1", DefaultValue = true)]
71 | public IActionResult FeatureEnableTest()
72 | {
73 | return Ok(new
74 | {
75 | Time = DateTime.UtcNow
76 | });
77 | }
78 |
79 | [HttpGet("[action]")]
80 | [FeatureFlagFilter("Flag2", DefaultValue = false)]
81 | public IActionResult FeatureDisableTest()
82 | {
83 | return Ok(new
84 | {
85 | Time = DateTime.UtcNow
86 | });
87 | }
88 |
89 | [HttpGet("getToken")]
90 | public async Task GetToken([Required] string userName, [FromServices] ITokenService tokenService)
91 | {
92 | var token = await tokenService
93 | .GenerateToken(new Claim("name", userName));
94 | if (token is TokenEntityWithRefreshToken tokenEntityWithRefreshToken)
95 | {
96 | return tokenEntityWithRefreshToken.WrapResult().GetRestResult();
97 | }
98 | return token.WrapResult().GetRestResult();
99 | }
100 |
101 | [HttpGet("validateToken")]
102 | public async Task ValidateToken(string token, [FromServices] ITokenService tokenService)
103 | {
104 | return await tokenService
105 | .ValidateToken(token)
106 | .ContinueWith(r =>
107 | r.Result.WrapResult().GetRestResult()
108 | );
109 | }
110 |
111 | [HttpGet("RefreshToken")]
112 | public async Task RefreshToken(string refreshToken, [FromServices] ITokenService tokenService)
113 | {
114 | var token = await tokenService
115 | .RefreshToken(refreshToken);
116 | if (token is TokenEntityWithRefreshToken tokenEntityWithRefreshToken)
117 | {
118 | return tokenEntityWithRefreshToken.WrapResult().GetRestResult();
119 | }
120 | return token.WrapResult().GetRestResult();
121 | }
122 |
123 | [HttpGet("[action]")]
124 | [Authorize(AuthenticationSchemes = "Bearer")]
125 | public IActionResult BearerAuthTest()
126 | {
127 | return Ok();
128 | }
129 |
130 | [HttpGet("[action]")]
131 | public IActionResult Test()
132 | {
133 | return Ok(new { Name = "Amazing .NET" });
134 | }
135 |
136 | [HttpGet("[action]")]
137 | public IActionResult ExceptionTest()
138 | {
139 | throw new NotImplementedException();
140 | }
141 |
142 | [HttpGet("EnvironmentFilterTest/Dev")]
143 | [EnvironmentFilter("Development")]
144 | //[EnvironmentFilter("Production")]
145 | public IActionResult EnvironmentFilterDevTest()
146 | {
147 | return Ok(new { Title = ".NET is amazing!" });
148 | }
149 |
150 | [HttpGet("EnvironmentFilterTest/Prod")]
151 | [EnvironmentFilter("Production")]
152 | public IActionResult EnvironmentFilterProdTest()
153 | {
154 | return Ok(new { Title = ".NET is amazing!" });
155 | }
156 |
157 | [HttpPost("RawTextFormatterTest")]
158 | [Consumes("text/plain")]
159 | public Result RawTextFormatterTest([FromBody] string input)
160 | {
161 | return Result.Success(input);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/samples/WeihanLi.Web.Extensions.Samples/WeihanLi.Web.Extensions.Samples.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/samples/WeihanLi.Web.Extensions.Samples/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "FeatureFlags": {
3 | "ConfigInspector": true
4 | }
5 | }
--------------------------------------------------------------------------------
/samples/WeihanLi.Web.Extensions.Samples/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "FeatureFlags": {
3 | "ConfigInspector": false
4 | }
5 | }
--------------------------------------------------------------------------------
/src/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | direct
5 | false
6 | README.md
7 | icon.jpg
8 |
9 | true
10 |
11 | true
12 | snupkg
13 |
14 | https://github.com/WeihanLi/WeihanLi.Web.Extensions/blob/dev/docs/ReleaseNotes.md
15 |
16 | MIT
17 | false
18 | true
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Mvc.Controllers;
5 | using Microsoft.AspNetCore.Mvc.Filters;
6 | using System.Reflection;
7 | using WeihanLi.Web.Filters;
8 |
9 | namespace WeihanLi.Web.AccessControlHelper;
10 |
11 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
12 | public sealed class AccessControlAttribute : AuthorizationFilterAttribute
13 | {
14 | public string? AccessKey { get; set; }
15 |
16 | public override void OnAuthorization(AuthorizationFilterContext filterContext)
17 | {
18 | ArgumentNullException.ThrowIfNull(filterContext);
19 |
20 | var isDefinedNoControl = filterContext.ActionDescriptor.IsDefined(typeof(NoAccessControlAttribute), true);
21 |
22 | if (isDefinedNoControl) return;
23 |
24 | var accessStrategy = filterContext.HttpContext.RequestServices.GetService();
25 | if (accessStrategy is null)
26 | throw new InvalidOperationException("IResourceAccessStrategy not initialized,please register your ResourceAccessStrategy");
27 |
28 | if (!accessStrategy.IsCanAccess(AccessKey))
29 | {
30 | //if Ajax request
31 | filterContext.Result = filterContext.HttpContext.Request.IsAjaxRequest() ?
32 | accessStrategy.DisallowedAjaxResult :
33 | accessStrategy.DisallowedCommonResult;
34 | }
35 | }
36 | }
37 |
38 | internal static class AjaxRequestExtensions
39 | {
40 | public static bool IsAjaxRequest(this HttpRequest request)
41 | {
42 | return "XMLHttpRequest".Equals(request.Headers.XRequestedWith, StringComparison.OrdinalIgnoreCase);
43 | }
44 |
45 | public static bool IsDefined(this Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor actionDescriptor,
46 | Type attributeType, bool inherit)
47 | {
48 | if (actionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
49 | {
50 | if (controllerActionDescriptor.MethodInfo.GetCustomAttribute(attributeType) == null)
51 | {
52 | if (inherit && controllerActionDescriptor.ControllerTypeInfo.GetCustomAttribute(attributeType) != null)
53 | {
54 | return true;
55 | }
56 | }
57 | else
58 | {
59 | return true;
60 | }
61 | }
62 | return false;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlAuthorizationHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Authorization;
5 | using WeihanLi.Common;
6 |
7 | namespace WeihanLi.Web.AccessControlHelper;
8 |
9 | internal sealed class AccessControlAuthorizationHandler(
10 | IHttpContextAccessor contextAccessor,
11 | IOptions options)
12 | : AuthorizationHandler
13 | {
14 | private readonly AccessControlOptions _options = options.Value;
15 |
16 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AccessControlRequirement requirement)
17 | {
18 | var httpContext = contextAccessor.HttpContext;
19 | ArgumentNullException.ThrowIfNull(httpContext);
20 | var accessKey = _options.AccessKeyResolver.Invoke(httpContext);
21 | var resourceAccessStrategy = Guard.NotNull(httpContext).RequestServices.GetRequiredService();
22 | if (resourceAccessStrategy.IsCanAccess(accessKey))
23 | {
24 | context.Succeed(requirement);
25 | }
26 | else
27 | {
28 | context.Fail();
29 | }
30 | return Task.CompletedTask;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlHelperBuilder.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.AccessControlHelper;
5 |
6 | public interface IAccessControlHelperBuilder
7 | {
8 | IServiceCollection Services { get; }
9 | }
10 |
11 | internal sealed class AccessControlHelperBuilder(IServiceCollection services) : IAccessControlHelperBuilder
12 | {
13 | public IServiceCollection Services { get; } = services;
14 | }
15 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlHelperConstants.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.AccessControlHelper;
5 |
6 | public static class AccessControlHelperConstants
7 | {
8 | public const string PolicyName = "AccessControl";
9 | }
10 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlHelperExtension.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Authorization;
5 | using Microsoft.Extensions.DependencyInjection.Extensions;
6 | using WeihanLi.Web.AccessControlHelper;
7 |
8 | namespace Microsoft.AspNetCore.Builder
9 | {
10 | public static class ApplicationBuilderExtensions
11 | {
12 | public static IApplicationBuilder UseAccessControlHelper(this IApplicationBuilder app)
13 | {
14 | ArgumentNullException.ThrowIfNull(app);
15 | return app.UseMiddleware();
16 | }
17 | }
18 | }
19 |
20 | namespace Microsoft.Extensions.DependencyInjection
21 | {
22 | public static class ServiceCollectionExtensions
23 | {
24 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services)
25 | where TResourceAccessStrategy : class, IResourceAccessStrategy
26 | where TControlStrategy : class, IControlAccessStrategy
27 | {
28 | ArgumentNullException.ThrowIfNull(services);
29 |
30 | services.TryAddSingleton();
31 | services.TryAddSingleton();
32 |
33 | return services.AddAccessControlHelper();
34 | }
35 |
36 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services, ServiceLifetime resourceAccessStrategyLifetime, ServiceLifetime controlAccessStrategyLifetime)
37 | where TResourceAccessStrategy : class, IResourceAccessStrategy
38 | where TControlStrategy : class, IControlAccessStrategy
39 | {
40 | ArgumentNullException.ThrowIfNull(services);
41 |
42 | services.TryAdd(new ServiceDescriptor(typeof(IResourceAccessStrategy), typeof(TResourceAccessStrategy), resourceAccessStrategyLifetime));
43 | services.TryAdd(new ServiceDescriptor(typeof(IControlAccessStrategy), typeof(TControlStrategy), controlAccessStrategyLifetime));
44 |
45 | return services.AddAccessControlHelper();
46 | }
47 |
48 | public static IAccessControlHelperBuilder AddAccessControlHelper(
49 | this IServiceCollection services, Action configAction)
50 | where TResourceAccessStrategy : class, IResourceAccessStrategy
51 | where TControlStrategy : class, IControlAccessStrategy
52 | {
53 | ArgumentNullException.ThrowIfNull(services);
54 | ArgumentNullException.ThrowIfNull(configAction);
55 |
56 | services.Configure(configAction);
57 | return services.AddAccessControlHelper();
58 | }
59 |
60 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services, Action configAction, ServiceLifetime resourceAccessStrategyLifetime, ServiceLifetime controlAccessStrategyLifetime)
61 | where TResourceAccessStrategy : class, IResourceAccessStrategy
62 | where TControlStrategy : class, IControlAccessStrategy
63 | {
64 | ArgumentNullException.ThrowIfNull(services);
65 | ArgumentNullException.ThrowIfNull(configAction);
66 |
67 | services.Configure(configAction);
68 | return services.AddAccessControlHelper(resourceAccessStrategyLifetime, controlAccessStrategyLifetime);
69 | }
70 |
71 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services, bool useAsDefaultPolicy)
72 | {
73 | ArgumentNullException.ThrowIfNull(services);
74 |
75 | if (useAsDefaultPolicy)
76 | {
77 | services.AddAuthorization(options =>
78 | {
79 | var accessControlPolicy = new AuthorizationPolicyBuilder()
80 | .AddRequirements(new AccessControlRequirement())
81 | .Build();
82 | options.AddPolicy(AccessControlHelperConstants.PolicyName, accessControlPolicy);
83 | options.DefaultPolicy = accessControlPolicy;
84 | });
85 | }
86 | else
87 | {
88 | services.AddAuthorization(options =>
89 | {
90 | var accessControlPolicy = new AuthorizationPolicyBuilder()
91 | .AddRequirements(new AccessControlRequirement())
92 | .Build();
93 | options.AddPolicy(AccessControlHelperConstants.PolicyName, accessControlPolicy);
94 | });
95 | }
96 |
97 | services.AddSingleton();
98 |
99 | services.TryAddSingleton();
100 |
101 | return new AccessControlHelperBuilder(services);
102 | }
103 |
104 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services)
105 | {
106 | return AddAccessControlHelper(services, false);
107 | }
108 |
109 | public static IAccessControlHelperBuilder AddAccessControlHelper(this IServiceCollection services, Action configAction)
110 | {
111 | ArgumentNullException.ThrowIfNull(services);
112 | ArgumentNullException.ThrowIfNull(configAction);
113 |
114 | var option = new AccessControlOptions();
115 | configAction.Invoke(option);
116 | services.Configure(configAction);
117 | return services.AddAccessControlHelper(option.UseAsDefaultPolicy);
118 | }
119 |
120 | public static IAccessControlHelperBuilder AddResourceAccessStrategy(this IAccessControlHelperBuilder builder) where TResourceAccessStrategy : IResourceAccessStrategy
121 | {
122 | return AddResourceAccessStrategy(builder, ServiceLifetime.Singleton);
123 | }
124 |
125 | public static IAccessControlHelperBuilder AddResourceAccessStrategy(this IAccessControlHelperBuilder builder, ServiceLifetime serviceLifetime) where TResourceAccessStrategy : IResourceAccessStrategy
126 | {
127 | if (null == builder)
128 | {
129 | throw new ArgumentNullException(nameof(builder));
130 | }
131 |
132 | builder.Services.Add(
133 | new ServiceDescriptor(typeof(IResourceAccessStrategy), typeof(TResourceAccessStrategy), serviceLifetime));
134 | return builder;
135 | }
136 |
137 | public static IAccessControlHelperBuilder AddControlAccessStrategy(this IAccessControlHelperBuilder builder) where TControlAccessStrategy : IControlAccessStrategy
138 | {
139 | return AddControlAccessStrategy(builder, ServiceLifetime.Singleton);
140 | }
141 |
142 | public static IAccessControlHelperBuilder AddControlAccessStrategy(this IAccessControlHelperBuilder builder, ServiceLifetime serviceLifetime) where TControlAccessStrategy : IControlAccessStrategy
143 | {
144 | if (null == builder)
145 | {
146 | throw new ArgumentNullException(nameof(builder));
147 | }
148 |
149 | builder.Services.Add(new ServiceDescriptor(typeof(IControlAccessStrategy), typeof(TControlAccessStrategy), serviceLifetime));
150 | return builder;
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlHelperMiddleware.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.AccessControlHelper;
5 |
6 | ///
7 | /// AccessControlHelperMiddleware
8 | ///
9 | internal sealed class AccessControlHelperMiddleware
10 | {
11 | private readonly RequestDelegate _next;
12 | private readonly AccessControlOptions _option;
13 |
14 | ///
15 | /// Creates a new instance of
16 | ///
17 | /// The delegate representing the next middleware in the request pipeline.
18 | ///
19 | public AccessControlHelperMiddleware(
20 | RequestDelegate next,
21 | IOptions options
22 | )
23 | {
24 | ArgumentNullException.ThrowIfNull(next);
25 | _next = next;
26 | _option = options.Value;
27 | }
28 |
29 | ///
30 | /// Executes the middleware.
31 | ///
32 | /// The for the current request.
33 | /// A task that represents the execution of this middleware.
34 | public Task Invoke(HttpContext context)
35 | {
36 | var accessKey = _option.AccessKeyResolver.Invoke(context);
37 | var accessStrategy = context.RequestServices.GetRequiredService();
38 | if (accessStrategy.IsCanAccess(accessKey))
39 | {
40 | return _next(context);
41 | }
42 |
43 | context.Response.StatusCode = context.User is { Identity.IsAuthenticated: true } ? 403 : 401;
44 | return _option.DefaultUnauthorizedOperation?.Invoke(context) ?? Task.CompletedTask;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.AccessControlHelper;
5 |
6 | public sealed class AccessControlOptions
7 | {
8 | private Func _accessKeyResolver = context =>
9 | context.Request.Headers.TryGetValue("X-Access-Key", out var val) ? val.ToString() : null;
10 | public bool UseAsDefaultPolicy { get; set; }
11 |
12 | public Func AccessKeyResolver
13 | {
14 | get => _accessKeyResolver;
15 | set => _accessKeyResolver = value ?? throw new ArgumentNullException(nameof(AccessKeyResolver));
16 | }
17 |
18 | public Func? DefaultUnauthorizedOperation { get; set; }
19 | }
20 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlRequirement.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Authorization;
5 |
6 | namespace WeihanLi.Web.AccessControlHelper;
7 |
8 | internal sealed class AccessControlRequirement : IAuthorizationRequirement
9 | {
10 | }
11 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/AccessControlTagHelper.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Razor.TagHelpers;
5 |
6 | namespace WeihanLi.Web.AccessControlHelper;
7 |
8 | ///
9 | /// AccessControlTagHelper
10 | /// add support for tagHelper
11 | /// https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/authoring?view=aspnetcore-2.1#condition-tag-helper
12 | ///
13 | [HtmlTargetElement(Attributes = "asp-access")]
14 | public sealed class AccessControlTagHelper(IControlAccessStrategy controlAccessStrategy) : TagHelper
15 | {
16 | public override void Process(TagHelperContext context, TagHelperOutput output)
17 | {
18 | context.AllAttributes.TryGetAttribute("asp-access-key", out var accessKey);
19 | if (!controlAccessStrategy.IsControlCanAccess(accessKey?.Value.ToString()))
20 | {
21 | output.SuppressOutput();
22 | }
23 | else
24 | {
25 | if (accessKey != null)
26 | {
27 | output.Attributes.Remove(accessKey);
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/HtmlHelperExtension.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Html;
5 | using Microsoft.AspNetCore.Mvc.Rendering;
6 | using Microsoft.AspNetCore.Mvc.ViewFeatures;
7 |
8 | namespace WeihanLi.Web.AccessControlHelper;
9 |
10 | public static class HtmlHelperExtension
11 | {
12 | ///
13 | /// SparkActionLink
14 | ///
15 | /// HtmlHelper
16 | /// linkText
17 | /// actionName
18 | /// controllerName
19 | /// routeValues
20 | /// htmlAttributes
21 | /// accessKey
22 | ///
23 | public static IHtmlContent SparkActionLink(
24 | this IHtmlHelper helper, string linkText, string actionName, string? controllerName = null,
25 | object? routeValues = null, object? htmlAttributes = null, string? accessKey = null)
26 | {
27 | if (helper.ViewContext.HttpContext.RequestServices.GetRequiredService()
28 | .IsControlCanAccess(accessKey))
29 | {
30 | if (string.IsNullOrEmpty(controllerName))
31 | {
32 | return helper.ActionLink(linkText, actionName, routeValues, htmlAttributes);
33 | }
34 | else
35 | {
36 | return helper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes);
37 | }
38 | }
39 | return HtmlString.Empty;
40 | }
41 |
42 | ///
43 | /// SparkContainer
44 | ///
45 | /// HtmlHelper
46 | /// tagName
47 | /// htmlAttributes
48 | /// accessKey
49 | ///
50 | public static SparkContainer SparkContainer(this IHtmlHelper helper, string tagName, object? attributes = null, string? accessKey = null)
51 | => SparkContainerHelper(helper, tagName, HtmlHelper.AnonymousObjectToHtmlAttributes(attributes), accessKey);
52 |
53 | private static SparkContainer SparkContainerHelper(IHtmlHelper helper, string tagName,
54 | IDictionary? attributes = null, string? accessKey = null)
55 | {
56 | var tagBuilder = new TagBuilder(tagName);
57 | var canAccess = helper.ViewContext.HttpContext.RequestServices.GetRequiredService()
58 | .IsControlCanAccess(accessKey);
59 | if (canAccess)
60 | {
61 | if (attributes is not null)
62 | {
63 | tagBuilder.MergeAttributes(attributes);
64 | }
65 |
66 | tagBuilder.TagRenderMode = TagRenderMode.StartTag;
67 | helper.ViewContext.Writer.Write(tagBuilder);
68 | }
69 | return new SparkContainer(helper.ViewContext, tagName, canAccess);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/IControlAccessStrategy.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.AccessControlHelper;
5 |
6 | ///
7 | /// view component access strategy
8 | ///
9 | public interface IControlAccessStrategy
10 | {
11 | ///
12 | /// view component access strategy
13 | ///
14 | bool IsControlCanAccess(string? accessKey);
15 | }
16 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/IResourceAccessStrategy.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | namespace WeihanLi.Web.AccessControlHelper;
7 |
8 | public interface IResourceAccessStrategy
9 | {
10 | ///
11 | /// Is resource can be accessed
12 | ///
13 | /// accessKey
14 | ///
15 | bool IsCanAccess(string? accessKey);
16 |
17 | ///
18 | /// AccessStrategyName
19 | ///
20 | //string StrategyName { get; }
21 |
22 | IActionResult DisallowedCommonResult { get; }
23 |
24 | IActionResult DisallowedAjaxResult { get; }
25 | }
26 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/NoAccessControlAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Mvc.Filters;
5 |
6 | namespace WeihanLi.Web.AccessControlHelper;
7 |
8 | ///
9 | /// NoAccessControl
10 | ///
11 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
12 | public sealed class NoAccessControlAttribute : Attribute, IAuthorizationFilter
13 | {
14 | public void OnAuthorization(AuthorizationFilterContext context)
15 | {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/AccessControlHelper/SparkContainer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Mvc.Rendering;
5 |
6 | namespace WeihanLi.Web.AccessControlHelper;
7 |
8 | public sealed class SparkContainer : IDisposable
9 | {
10 | private readonly string _tagName;
11 | private readonly ViewContext _viewContext;
12 | private bool _disposed;
13 | private TextWriter? _writer;
14 |
15 | public SparkContainer(ViewContext viewContext, string tagName, bool canAccess = true)
16 | {
17 | _viewContext = viewContext;
18 | _tagName = tagName;
19 | if (!canAccess)
20 | {
21 | _writer = viewContext.Writer;
22 | viewContext.Writer = TextWriter.Null;
23 | }
24 | }
25 |
26 | public void Dispose()
27 | {
28 | if (!_disposed)
29 | {
30 | _disposed = true;
31 | EndContainer();
32 | }
33 | }
34 |
35 | public void EndContainer()
36 | {
37 | if (_writer is not null)
38 | {
39 | _viewContext.Writer = _writer;
40 | _writer = null;
41 | }
42 | else
43 | {
44 | _viewContext.Writer.Write("{0}>", _tagName);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/ApiKeyAuthentication/ApiKeyAuthenticationDefaults.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.Authentication.ApiKeyAuthentication;
5 |
6 | public static class ApiKeyAuthenticationDefaults
7 | {
8 | public const string AuthenticationScheme = "ApiKey";
9 | }
10 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/ApiKeyAuthentication/ApiKeyAuthenticationHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.Extensions.Logging;
5 | using Microsoft.Extensions.Primitives;
6 | using System.Security.Claims;
7 | using System.Text.Encodings.Web;
8 |
9 | namespace WeihanLi.Web.Authentication.ApiKeyAuthentication;
10 |
11 | public sealed class ApiKeyAuthenticationHandler : AuthenticationHandler
12 | {
13 | public ApiKeyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder)
14 | : base(options, logger, encoder)
15 | {
16 | }
17 |
18 | protected override async Task HandleAuthenticateAsync()
19 | {
20 | StringValues keyValues;
21 | var keyExists = Options.KeyLocation switch
22 | {
23 | KeyLocation.Query => Request.Query.TryGetValue(Options.ApiKeyName, out keyValues),
24 | KeyLocation.HeaderOrQuery => Request.Headers.TryGetValue(Options.ApiKeyName, out keyValues) || Request.Query.TryGetValue(Options.ApiKeyName, out keyValues),
25 | KeyLocation.QueryOrHeader => Request.Query.TryGetValue(Options.ApiKeyName, out keyValues) || Request.Headers.TryGetValue(Options.ApiKeyName, out keyValues),
26 | _ => Request.Headers.TryGetValue(Options.ApiKeyName, out keyValues),
27 | };
28 | if (!keyExists)
29 | return AuthenticateResult.NoResult();
30 |
31 | var validator = Options.ApiKeyValidator ?? ((_, keyValue) => Task.FromResult(string.Equals(Options.ApiKey, keyValue)));
32 | if (!await validator.Invoke(Context, keyValues.ToString()))
33 | return AuthenticateResult.Fail("Invalid ApiKey");
34 |
35 | var claims = new[]
36 | {
37 | new Claim("issuer", ClaimsIssuer)
38 | };
39 |
40 | if (Options.ClaimsGenerator != null)
41 | {
42 | var generatedClaims = await Options.ClaimsGenerator.Invoke(Context, Options);
43 | if (generatedClaims is { Count: > 0 })
44 | {
45 | claims = claims.Union(generatedClaims).ToArray();
46 | }
47 | }
48 |
49 | return AuthenticateResult.Success(
50 | new AuthenticationTicket(
51 | new ClaimsPrincipal([
52 | new ClaimsIdentity(claims, Scheme.Name)
53 | ]), Scheme.Name)
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/ApiKeyAuthentication/ApiKeyAuthenticationOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using System.Security.Claims;
5 |
6 | namespace WeihanLi.Web.Authentication.ApiKeyAuthentication;
7 |
8 | public sealed class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
9 | {
10 | public string ApiKey { get; set; } = default!;
11 | public string ApiKeyName { get; set; } = "X-ApiKey";
12 | public KeyLocation KeyLocation { get; set; }
13 | public Func>? ApiKeyValidator { get; set; }
14 | public Func>>? ClaimsGenerator { get; set; }
15 |
16 | public override void Validate()
17 | {
18 | ArgumentException.ThrowIfNullOrWhiteSpace(ApiKey);
19 | }
20 | }
21 |
22 | public enum KeyLocation
23 | {
24 | Header = 0,
25 | Query = 1,
26 | HeaderOrQuery = 2,
27 | QueryOrHeader = 3,
28 | }
29 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/AuthenticationBuilderExtension.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using WeihanLi.Web.Authentication.ApiKeyAuthentication;
5 | using WeihanLi.Web.Authentication.BasicAuthentication;
6 | using WeihanLi.Web.Authentication.DelegateAuthentication;
7 | using WeihanLi.Web.Authentication.HeaderAuthentication;
8 | using WeihanLi.Web.Authentication.QueryAuthentication;
9 |
10 | namespace WeihanLi.Web.Authentication;
11 |
12 | public static class AuthenticationBuilderExtension
13 | {
14 | #region AddApiKey
15 |
16 | public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder)
17 | {
18 | return builder.AddApiKey(ApiKeyAuthenticationDefaults.AuthenticationScheme);
19 | }
20 |
21 | public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, string schema)
22 | {
23 | return builder.AddApiKey(schema, _ => { });
24 | }
25 |
26 | public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder,
27 | Action configureOptions)
28 | {
29 | return builder.AddApiKey(ApiKeyAuthenticationDefaults.AuthenticationScheme,
30 | configureOptions);
31 | }
32 |
33 | public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, string schema,
34 | Action configureOptions)
35 | {
36 | ArgumentNullException.ThrowIfNull(builder);
37 | ArgumentNullException.ThrowIfNull(configureOptions);
38 |
39 | builder.Services.Configure(configureOptions);
40 | return builder.AddScheme(schema,
41 | configureOptions);
42 | }
43 |
44 | #endregion AddApiKey
45 |
46 | #region AddBasic
47 |
48 | public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder)
49 | {
50 | return builder.AddBasic(BasicAuthenticationDefaults.AuthenticationScheme);
51 | }
52 |
53 | public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string schema)
54 | {
55 | return builder.AddBasic(schema, _ => { });
56 | }
57 |
58 | public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder,
59 | Action configureOptions)
60 | {
61 | return builder.AddBasic(BasicAuthenticationDefaults.AuthenticationScheme,
62 | configureOptions);
63 | }
64 |
65 | public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string schema,
66 | Action configureOptions)
67 | {
68 | ArgumentNullException.ThrowIfNull(builder);
69 | ArgumentNullException.ThrowIfNull(configureOptions);
70 |
71 | builder.Services.Configure(configureOptions);
72 | return builder.AddScheme(schema,
73 | configureOptions);
74 | }
75 |
76 | #endregion AddApiKey
77 |
78 | #region AddHeader
79 |
80 | public static AuthenticationBuilder AddHeader(this AuthenticationBuilder builder)
81 | {
82 | return builder.AddHeader(HeaderAuthenticationDefaults.AuthenticationScheme);
83 | }
84 |
85 | public static AuthenticationBuilder AddHeader(this AuthenticationBuilder builder, string schema)
86 | {
87 | return builder.AddHeader(schema, _ => { });
88 | }
89 |
90 | public static AuthenticationBuilder AddHeader(this AuthenticationBuilder builder,
91 | Action configureOptions)
92 | {
93 | return builder.AddHeader(HeaderAuthenticationDefaults.AuthenticationScheme,
94 | configureOptions);
95 | }
96 |
97 | public static AuthenticationBuilder AddHeader(this AuthenticationBuilder builder, string schema,
98 | Action configureOptions)
99 | {
100 | ArgumentNullException.ThrowIfNull(builder);
101 | ArgumentNullException.ThrowIfNull(configureOptions);
102 |
103 | builder.Services.Configure(configureOptions);
104 | return builder.AddScheme(schema,
105 | configureOptions);
106 | }
107 |
108 | #endregion AddHeader
109 |
110 | #region AddQuery
111 |
112 | public static AuthenticationBuilder AddQuery(this AuthenticationBuilder builder)
113 | {
114 | return builder.AddQuery(QueryAuthenticationDefaults.AuthenticationScheme);
115 | }
116 |
117 | public static AuthenticationBuilder AddQuery(this AuthenticationBuilder builder, string schema)
118 | {
119 | return builder.AddQuery(schema, _ => { });
120 | }
121 |
122 | public static AuthenticationBuilder AddQuery(this AuthenticationBuilder builder,
123 | Action configureOptions)
124 | {
125 | return builder.AddQuery(QueryAuthenticationDefaults.AuthenticationScheme,
126 | configureOptions);
127 | }
128 |
129 | public static AuthenticationBuilder AddQuery(this AuthenticationBuilder builder, string schema,
130 | Action configureOptions)
131 | {
132 | ArgumentNullException.ThrowIfNull(builder);
133 | ArgumentNullException.ThrowIfNull(configureOptions);
134 |
135 | builder.Services.Configure(configureOptions);
136 | return builder.AddScheme(schema,
137 | configureOptions);
138 | }
139 |
140 | #endregion AddQuery
141 |
142 | #region AddDelegate
143 |
144 | public static AuthenticationBuilder AddDelegate(this AuthenticationBuilder builder,
145 | Action configureOptions) =>
146 | AddDelegate(builder, DelegateAuthenticationDefaults.AuthenticationScheme, configureOptions);
147 |
148 | public static AuthenticationBuilder AddDelegate(this AuthenticationBuilder builder, string schema,
149 | Action configureOptions)
150 | {
151 | ArgumentNullException.ThrowIfNull(builder);
152 | ArgumentNullException.ThrowIfNull(configureOptions);
153 |
154 | builder.Services.Configure(configureOptions);
155 | return builder.AddScheme(schema,
156 | configureOptions);
157 | }
158 |
159 | #endregion AddDelegate
160 | }
161 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/BasicAuthentication/BasicAuthenticationDefaults.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.Authentication.BasicAuthentication;
5 |
6 | public static class BasicAuthenticationDefaults
7 | {
8 | public const string AuthenticationScheme = "Basic";
9 | }
10 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/BasicAuthentication/BasicAuthenticationHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.Extensions.Logging;
5 | using System.Security.Claims;
6 | using System.Text.Encodings.Web;
7 | using WeihanLi.Extensions;
8 | using AuthenticateResult = Microsoft.AspNetCore.Authentication.AuthenticateResult;
9 |
10 | namespace WeihanLi.Web.Authentication.BasicAuthentication;
11 |
12 | public sealed class BasicAuthenticationHandler : AuthenticationHandler
13 | {
14 | public BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder)
15 | : base(options, logger, encoder)
16 | {
17 | }
18 |
19 | protected override async Task HandleAuthenticateAsync()
20 | {
21 | if (!Request.Headers.TryGetValue("Authorization", out var authHeaderValues))
22 | {
23 | return AuthenticateResult.NoResult();
24 | }
25 |
26 | var authHeader = authHeaderValues.ToString();
27 | if (authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
28 | {
29 | var token = authHeader[("Basic ".Length)..];
30 | if (string.IsNullOrEmpty(token))
31 | {
32 | return AuthenticateResult.Fail("Invalid Authorization header");
33 | }
34 |
35 | string userName, password;
36 | try
37 | {
38 | var array = Convert.FromBase64String(token).GetString().Split(':');
39 | if (array.Length != 2)
40 | {
41 | return AuthenticateResult.Fail("Invalid Authorization header");
42 | }
43 |
44 | userName = array[0];
45 | password = array[1];
46 | }
47 | catch
48 | {
49 | return AuthenticateResult.Fail("Invalid Authorization header");
50 | }
51 |
52 |
53 | var valid = await Options.UserCredentialValidator.Invoke(Request.HttpContext, userName, password);
54 | if (valid)
55 | {
56 | var claims = new[]
57 | {
58 | new Claim(ClaimTypes.Name, userName),
59 | new Claim("issuer", ClaimsIssuer),
60 | };
61 | return AuthenticateResult.Success(new AuthenticationTicket(
62 | new ClaimsPrincipal([
63 | new ClaimsIdentity(claims, Scheme.Name)
64 | ]), Scheme.Name));
65 | }
66 | return AuthenticateResult.Fail("Invalid user credential");
67 | }
68 | return AuthenticateResult.NoResult();
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/BasicAuthentication/BasicAuthenticationOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.Authentication.BasicAuthentication;
5 |
6 | public sealed class BasicAuthenticationOptions : AuthenticationSchemeOptions
7 | {
8 | public string? UserName { get; set; }
9 | public string? Password { get; set; }
10 |
11 | public Func> UserCredentialValidator { get; set; }
12 | = (context, user, pass) =>
13 | {
14 | var options = context.RequestServices.GetRequiredService>()
15 | .Value;
16 | return Task.FromResult(user == options.UserName && pass == options.Password);
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/DelegateAuthentication/DelegateAuthenticationDefaults.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.Authentication.DelegateAuthentication;
5 |
6 | public static class DelegateAuthenticationDefaults
7 | {
8 | public const string AuthenticationScheme = "Delegate";
9 | }
10 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/DelegateAuthentication/DelegateAuthenticationHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.Extensions.Logging;
5 | using System.Security.Claims;
6 | using System.Text.Encodings.Web;
7 |
8 | namespace WeihanLi.Web.Authentication.DelegateAuthentication;
9 |
10 | public sealed class DelegateAuthenticationHandler(
11 | IOptionsMonitor options,
12 | ILoggerFactory logger,
13 | UrlEncoder encoder)
14 | : AuthenticationHandler(options, logger, encoder)
15 | {
16 | protected override async Task HandleAuthenticateAsync()
17 | {
18 | ArgumentNullException.ThrowIfNull(Options.Validator);
19 |
20 | var authenticated = await Options.Validator.Invoke(Context);
21 | if (!authenticated)
22 | return AuthenticateResult.Fail($"Delegate authentication({Scheme.DisplayName ?? Scheme.Name}) failed.");
23 |
24 | List claims =
25 | [
26 | new("issuer", ClaimsIssuer)
27 | ];
28 |
29 | if (Options.ClaimsGenerator != null)
30 | {
31 | var generatedClaims = await Options.ClaimsGenerator.Invoke(Context);
32 | if (generatedClaims is { Count: > 0 })
33 | {
34 | claims = [.. generatedClaims, .. claims];
35 | }
36 | }
37 |
38 | return AuthenticateResult.Success(
39 | new AuthenticationTicket(
40 | new ClaimsPrincipal([
41 | new ClaimsIdentity(claims, Scheme.Name)
42 | ]), Scheme.Name)
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/DelegateAuthentication/DelegateAuthenticationOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using System.Security.Claims;
5 |
6 | namespace WeihanLi.Web.Authentication.DelegateAuthentication;
7 |
8 | public sealed class DelegateAuthenticationOptions : AuthenticationSchemeOptions
9 | {
10 | public Func> Validator { get; set; } = _ => Task.FromResult(false);
11 | public Func>>? ClaimsGenerator { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/HeaderAuthentication/HeaderAuthenticationDefaults.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.Authentication.HeaderAuthentication;
5 |
6 | public static class HeaderAuthenticationDefaults
7 | {
8 | public const string AuthenticationScheme = "Header";
9 | }
10 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/HeaderAuthentication/HeaderAuthenticationHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.Extensions.Logging;
5 | using System.Security.Claims;
6 | using System.Text.Encodings.Web;
7 |
8 | namespace WeihanLi.Web.Authentication.HeaderAuthentication;
9 |
10 | public sealed class HeaderAuthenticationHandler : AuthenticationHandler
11 | {
12 | public HeaderAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder)
13 | : base(options, logger, encoder)
14 | {
15 | }
16 |
17 | protected override async Task HandleAuthenticateAsync()
18 | {
19 | if (await Options.AuthenticationValidator(Context))
20 | {
21 | var claims = new List();
22 | if (Request.Headers.TryGetValue(Options.UserIdHeaderName, out var userIdValues))
23 | {
24 | claims.Add(new Claim(ClaimTypes.NameIdentifier, userIdValues.ToString()));
25 | }
26 | if (Request.Headers.TryGetValue(Options.UserNameHeaderName, out var userNameValues))
27 | {
28 | claims.Add(new Claim(ClaimTypes.Name, userNameValues.ToString()));
29 | }
30 | if (Request.Headers.TryGetValue(Options.UserRolesHeaderName, out var userRolesValues))
31 | {
32 | var userRoles = userRolesValues.ToString()
33 | .Split([Options.Delimiter], StringSplitOptions.RemoveEmptyEntries);
34 | claims.AddRange(userRoles.Select(r => new Claim(ClaimTypes.Role, r)));
35 | }
36 |
37 | if (Options.AdditionalHeaderToClaims.Count > 0)
38 | {
39 | foreach (var headerToClaim in Options.AdditionalHeaderToClaims)
40 | {
41 | if (Request.Headers.TryGetValue(headerToClaim.Key, out var headerValues))
42 | {
43 | foreach (var val in headerValues.ToString().Split([Options.Delimiter], StringSplitOptions.RemoveEmptyEntries))
44 | {
45 | claims.Add(new Claim(headerToClaim.Value, val));
46 | }
47 | }
48 | }
49 | }
50 |
51 | // claims identity 's authentication type can not be null https://stackoverflow.com/questions/45261732/user-identity-isauthenticated-always-false-in-net-core-custom-authentication
52 | var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name));
53 | var ticket = new AuthenticationTicket(
54 | principal,
55 | Scheme.Name
56 | );
57 | return AuthenticateResult.Success(ticket);
58 | }
59 |
60 | return AuthenticateResult.NoResult();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/HeaderAuthentication/HeaderAuthenticationOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.Authentication.HeaderAuthentication;
5 |
6 | public sealed class HeaderAuthenticationOptions : AuthenticationSchemeOptions
7 | {
8 | private string _userRolesHeaderName = "UserRoles";
9 | private string _userNameHeaderName = "UserName";
10 | private string _userIdHeaderName = "UserId";
11 | private string _delimiter = ",";
12 |
13 | public string UserIdHeaderName
14 | {
15 | get => _userIdHeaderName;
16 | set
17 | {
18 | if (!string.IsNullOrWhiteSpace(value))
19 | {
20 | _userIdHeaderName = value;
21 | }
22 | }
23 | }
24 |
25 | public string UserNameHeaderName
26 | {
27 | get => _userNameHeaderName;
28 | set
29 | {
30 | if (!string.IsNullOrWhiteSpace(value))
31 | {
32 | _userNameHeaderName = value;
33 | }
34 | }
35 | }
36 |
37 | public string UserRolesHeaderName
38 | {
39 | get => _userRolesHeaderName;
40 | set
41 | {
42 | if (!string.IsNullOrWhiteSpace(value))
43 | {
44 | _userRolesHeaderName = value;
45 | }
46 | }
47 | }
48 |
49 | ///
50 | /// AdditionalHeaderToClaims
51 | /// key: headerName
52 | /// value: claimType
53 | ///
54 | public Dictionary AdditionalHeaderToClaims { get; } = new(StringComparer.OrdinalIgnoreCase);
55 |
56 | private Func> _authenticationValidator = context =>
57 | {
58 | var userIdHeader = context.RequestServices.GetRequiredService>().Value.UserIdHeaderName;
59 | return Task.FromResult(context.Request.Headers.ContainsKey(userIdHeader));
60 | };
61 |
62 | public Func> AuthenticationValidator
63 | {
64 | get => _authenticationValidator;
65 | set => _authenticationValidator = value ?? throw new ArgumentNullException(nameof(AuthenticationValidator));
66 | }
67 |
68 | public string Delimiter
69 | {
70 | get => _delimiter;
71 | set => _delimiter = string.IsNullOrEmpty(value) ? "," : value;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/QueryAuthentication/QueryAuthenticationDefaults.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.Authentication.QueryAuthentication;
5 |
6 | public static class QueryAuthenticationDefaults
7 | {
8 | public const string AuthenticationScheme = "Query";
9 | }
10 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/QueryAuthentication/QueryAuthenticationHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.Extensions.Logging;
5 | using System.Security.Claims;
6 | using System.Text.Encodings.Web;
7 |
8 | namespace WeihanLi.Web.Authentication.QueryAuthentication;
9 |
10 | public sealed class QueryAuthenticationHandler : AuthenticationHandler
11 | {
12 | public QueryAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder)
13 | : base(options, logger, encoder)
14 | {
15 | }
16 |
17 | protected override async Task HandleAuthenticateAsync()
18 | {
19 | if (Request.Query.Count > 0 && await Options.AuthenticationValidator(Context))
20 | {
21 | var claims = new List();
22 | if (Request.Query.TryGetValue(Options.UserIdQueryKey, out var userIdValues))
23 | {
24 | claims.Add(new Claim(ClaimTypes.NameIdentifier, userIdValues.ToString()));
25 | }
26 | if (Request.Query.TryGetValue(Options.UserNameQueryKey, out var userNameValues))
27 | {
28 | claims.Add(new Claim(ClaimTypes.Name, userNameValues.ToString()));
29 | }
30 | if (Request.Query.TryGetValue(Options.UserRolesQueryKey, out var userRolesValues))
31 | {
32 | var userRoles = userRolesValues.ToString()
33 | .Split([Options.Delimiter], StringSplitOptions.RemoveEmptyEntries);
34 | claims.AddRange(userRoles.Select(r => new Claim(ClaimTypes.Role, r)));
35 | }
36 | if (Options.AdditionalQueryToClaims.Count > 0)
37 | {
38 | foreach (var queryToClaim in Options.AdditionalQueryToClaims)
39 | {
40 | if (Request.Query.TryGetValue(queryToClaim.Key, out var queryValues))
41 | {
42 | foreach (var val in queryValues.ToString().Split([Options.Delimiter], StringSplitOptions.RemoveEmptyEntries))
43 | {
44 | claims.Add(new Claim(queryToClaim.Value, val));
45 | }
46 | }
47 | }
48 | }
49 | // claims identity 's authentication type can not be null https://stackoverflow.com/questions/45261732/user-identity-isauthenticated-always-false-in-net-core-custom-authentication
50 | var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, Scheme.Name));
51 | var ticket = new AuthenticationTicket(principal, Scheme.Name);
52 | return AuthenticateResult.Success(ticket);
53 | }
54 |
55 | return AuthenticateResult.NoResult();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authentication/QueryAuthentication/QueryAuthenticationOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.Authentication.QueryAuthentication;
5 |
6 | public sealed class QueryAuthenticationOptions : AuthenticationSchemeOptions
7 | {
8 | private string _userRolesQueryKey = "UserRoles";
9 | private string _userNameQueryKey = "UserName";
10 | private string _userIdQueryKey = "UserId";
11 | private string _delimiter = ",";
12 |
13 | public string UserIdQueryKey
14 | {
15 | get => _userIdQueryKey;
16 | set
17 | {
18 | if (!string.IsNullOrWhiteSpace(value))
19 | {
20 | _userIdQueryKey = value;
21 | }
22 | }
23 | }
24 |
25 | public string UserNameQueryKey
26 | {
27 | get => _userNameQueryKey;
28 | set
29 | {
30 | if (!string.IsNullOrWhiteSpace(value))
31 | {
32 | _userNameQueryKey = value;
33 | }
34 | }
35 | }
36 |
37 | public string UserRolesQueryKey
38 | {
39 | get => _userRolesQueryKey;
40 | set
41 | {
42 | if (!string.IsNullOrWhiteSpace(value))
43 | {
44 | _userRolesQueryKey = value;
45 | }
46 | }
47 | }
48 |
49 | ///
50 | /// 自定义其他的 header
51 | /// key: QueryKey
52 | /// value: claimType
53 | ///
54 | public Dictionary AdditionalQueryToClaims { get; } = new(StringComparer.OrdinalIgnoreCase);
55 |
56 | public string Delimiter
57 | {
58 | get => _delimiter;
59 | set
60 | {
61 | if (string.IsNullOrEmpty(value))
62 | {
63 | _delimiter = value;
64 | }
65 | }
66 | }
67 |
68 | private Func> _authenticationValidator = context =>
69 | {
70 | var userIdKey = context.RequestServices.GetRequiredService>().Value.UserIdQueryKey;
71 | return Task.FromResult(context.Request.Query.ContainsKey(userIdKey));
72 | };
73 |
74 | public Func> AuthenticationValidator
75 | {
76 | get => _authenticationValidator;
77 | set => _authenticationValidator = value ?? throw new ArgumentNullException(nameof(AuthenticationValidator));
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authorization/Jwt/DependencyInjectionExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Authentication.JwtBearer;
5 | using Microsoft.Extensions.DependencyInjection.Extensions;
6 | using WeihanLi.Web.Authorization.Token;
7 |
8 | namespace WeihanLi.Web.Authorization.Jwt;
9 |
10 | public static class DependencyInjectionExtensions
11 | {
12 | public static IServiceCollection AddJwtService(this IServiceCollection serviceCollection, Action optionsAction)
13 | {
14 | ArgumentNullException.ThrowIfNull(serviceCollection);
15 | ArgumentNullException.ThrowIfNull(optionsAction);
16 |
17 | serviceCollection.Configure(optionsAction);
18 | serviceCollection.TryAddSingleton();
19 | serviceCollection.ConfigureOptions();
20 | return serviceCollection;
21 | }
22 |
23 | public static IServiceCollection AddJwtServiceWithJwtBearerAuth(this IServiceCollection serviceCollection, Action optionsAction, Action? jwtBearerOptionsSetup = null)
24 | {
25 | ArgumentNullException.ThrowIfNull(serviceCollection);
26 | ArgumentNullException.ThrowIfNull(optionsAction);
27 |
28 | if (jwtBearerOptionsSetup is not null)
29 | {
30 | serviceCollection.Configure(jwtBearerOptionsSetup);
31 | }
32 |
33 | serviceCollection.ConfigureOptions();
34 | return serviceCollection.AddJwtService(optionsAction);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authorization/Jwt/JsonWebTokenOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.IdentityModel.Tokens;
5 | using WeihanLi.Common.Services;
6 |
7 | namespace WeihanLi.Web.Authorization.Jwt;
8 |
9 | public sealed class JsonWebTokenOptions
10 | {
11 | ///
12 | /// "iss" (Issuer) Claim
13 | ///
14 | /// The "iss" (issuer) claim identifies the principal that issued the
15 | /// JWT. The processing of this claim is generally application specific.
16 | /// The "iss" value is a case-sensitive string containing a StringOrURI
17 | /// value. Use of this claim is OPTIONAL.
18 | public string? Issuer { get; set; }
19 |
20 | ///
21 | /// "aud" (Audience) Claim
22 | ///
23 | /// The "aud" (audience) claim identifies the recipients that the JWT is
24 | /// intended for. Each principal intended to process the JWT MUST
25 | /// identify itself with a value in the audience claim. If the principal
26 | /// processing the claim does not identify itself with a value in the
27 | /// "aud" claim when this claim is present, then the JWT MUST be
28 | /// rejected. In the general case, the "aud" value is an array of case-sensitive strings, each containing a StringOrURI value.
29 | /// In the special case when the JWT has one audience, the "aud" value MAY be a
30 | /// single case-sensitive string containing a StringOrURI value. The
31 | /// interpretation of audience values is generally application specific.
32 | /// Use of this claim is OPTIONAL.
33 | public string? Audience { get; set; }
34 |
35 | ///
36 | /// SecretKey used for generate and validate token
37 | ///
38 | public string? SecretKey { get; set; }
39 |
40 | ///
41 | /// Set the timespan the token will be valid for (default is 1 hour/3600 seconds)
42 | ///
43 | public TimeSpan ValidFor { get; set; } = TimeSpan.FromHours(1);
44 |
45 | ///
46 | /// Gets or sets the clock skew to apply when validating a time.
47 | ///
48 | public TimeSpan ClockSkew { get; set; } = TimeSpan.FromMinutes(5);
49 |
50 | ///
51 | /// "jti" (JWT ID) Claim (default ID is a GUID)
52 | ///
53 | ///
54 | /// The "jti" (JWT ID) claim provides a unique identifier for the JWT.
55 | /// The identifier value MUST be assigned in a manner that ensures that
56 | /// there is a negligible probability that the same value will be
57 | /// accidentally assigned to a different data object; if the application
58 | /// uses multiple issuers, collisions MUST be prevented among values
59 | /// produced by different issuers as well. The "jti" claim can be used
60 | /// to prevent the JWT from being replayed.
61 | ///
62 | public Func? JtiGenerator { get; set; } = () => GuidIdGenerator.Instance.NewId();
63 |
64 | public Func? SigningCredentialsFactory { get; set; }
65 |
66 | public bool EnableRefreshToken { get; set; }
67 |
68 | public Func? RenewRefreshTokenPredicate { get; set; }
69 |
70 | public TimeSpan RefreshTokenValidFor { get; set; } = TimeSpan.FromHours(8);
71 |
72 | public Func? RefreshTokenSigningCredentialsFactory { get; set; }
73 |
74 | public string? NameClaimType { get; set; }
75 | public string? RoleClaimType { get; set; }
76 |
77 | public string RefreshTokenOwnerClaimType { get; set; } = "x-rt-owner";
78 |
79 | public Func? RefreshTokenValidator { get; set; }
80 |
81 | public Action? TokenValidationConfigure { get; set; }
82 |
83 | internal SigningCredentials? SigningCredentials { get; set; }
84 | internal SigningCredentials? RefreshTokenSigningCredentials { get; set; }
85 |
86 | internal TokenValidationParameters GetTokenValidationParameters(Action? parametersAction = null)
87 | {
88 | var parameters = new TokenValidationParameters
89 | {
90 | // The signing key must match!
91 | ValidateIssuerSigningKey = true,
92 | IssuerSigningKey = SigningCredentials?.Key,
93 | // Validate the JWT Issuer (iss) claim
94 | ValidateIssuer = true,
95 | ValidIssuer = Issuer,
96 | // Validate the JWT Audience (aud) claim
97 | ValidateAudience = true,
98 | ValidAudience = Audience,
99 | // Validate the token expiry
100 | ValidateLifetime = true,
101 | // If you want to allow a certain amount of clock drift, set that here:
102 | ClockSkew = ClockSkew
103 | };
104 |
105 | if (!string.IsNullOrEmpty(NameClaimType))
106 | {
107 | parameters.NameClaimType = NameClaimType;
108 | }
109 | if (!string.IsNullOrEmpty(RoleClaimType))
110 | {
111 | parameters.RoleClaimType = RoleClaimType;
112 | }
113 |
114 | parametersAction?.Invoke(parameters);
115 | TokenValidationConfigure?.Invoke(parameters);
116 | return parameters;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authorization/Jwt/JsonWebTokenOptionsSetup.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.IdentityModel.Tokens;
5 | using WeihanLi.Extensions;
6 |
7 | namespace WeihanLi.Web.Authorization.Jwt;
8 |
9 | internal sealed class JsonWebTokenOptionsSetup : IPostConfigureOptions
10 | {
11 | public void PostConfigure(string? name, JsonWebTokenOptions options)
12 | {
13 | if (options.SigningCredentialsFactory is null)
14 | {
15 | if (options.SecretKey.IsNotNullOrWhiteSpace())
16 | {
17 | options.SigningCredentialsFactory = () => new SigningCredentials(new SymmetricSecurityKey(options.SecretKey.GetBytes()), SecurityAlgorithms.HmacSha256);
18 | }
19 | }
20 | ArgumentNullException.ThrowIfNull(options.SigningCredentialsFactory);
21 | options.SigningCredentials = options.SigningCredentialsFactory.Invoke();
22 | options.RefreshTokenSigningCredentials = options.RefreshTokenSigningCredentials is null
23 | ? options.SigningCredentials
24 | : options.RefreshTokenSigningCredentialsFactory?.Invoke();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authorization/Jwt/JsonWebTokenService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.IdentityModel.Tokens;
5 | using System.IdentityModel.Tokens.Jwt;
6 | using System.Security.Claims;
7 | using WeihanLi.Common.Services;
8 | using WeihanLi.Extensions;
9 | using WeihanLi.Web.Authorization.Token;
10 |
11 | namespace WeihanLi.Web.Authorization.Jwt;
12 |
13 | public class JsonWebTokenService : ITokenService
14 | {
15 | private readonly IHttpContextAccessor _httpContextAccessor;
16 | private readonly JwtSecurityTokenHandler _tokenHandler = new();
17 | private readonly JsonWebTokenOptions _tokenOptions;
18 |
19 | private readonly Lazy
20 | _lazyTokenValidationParameters,
21 | _lazyRefreshTokenValidationParameters;
22 |
23 | public JsonWebTokenService(IHttpContextAccessor httpContextAccessor, IOptions tokenOptions)
24 | {
25 | _httpContextAccessor = httpContextAccessor;
26 | _tokenOptions = tokenOptions.Value;
27 | _lazyTokenValidationParameters = new(() =>
28 | _tokenOptions.GetTokenValidationParameters());
29 | _lazyRefreshTokenValidationParameters = new(() =>
30 | _tokenOptions.GetTokenValidationParameters(parameters =>
31 | {
32 | parameters.ValidAudience = GetRefreshTokenAudience();
33 | parameters.IssuerSigningKey = _tokenOptions.RefreshTokenSigningCredentials?.Key;
34 | })
35 | );
36 | }
37 |
38 | public virtual Task GenerateToken(params Claim[] claims)
39 | => GenerateTokenInternal(_tokenOptions.EnableRefreshToken, claims);
40 |
41 | public virtual Task ValidateToken(string token)
42 | {
43 | return _tokenHandler.ValidateTokenAsync(token, _lazyTokenValidationParameters.Value);
44 | }
45 |
46 | public virtual async Task RefreshToken(string refreshToken)
47 | {
48 | ArgumentNullException.ThrowIfNull(_httpContextAccessor.HttpContext);
49 | var refreshTokenValidateResult = await _tokenHandler.ValidateTokenAsync(refreshToken, _lazyRefreshTokenValidationParameters.Value);
50 | if (!refreshTokenValidateResult.IsValid || _tokenOptions.RefreshTokenValidator?.Invoke(refreshTokenValidateResult, _httpContextAccessor.HttpContext) == false)
51 | {
52 | throw new InvalidOperationException("Invalid RefreshToken", refreshTokenValidateResult.Exception);
53 | }
54 | var renewRefreshToken = _tokenOptions.RenewRefreshTokenPredicate?.Invoke(refreshTokenValidateResult);
55 | return await GenerateTokenInternal(renewRefreshToken.GetValueOrDefault(),
56 | refreshTokenValidateResult.Claims
57 | .Where(x => x.Key != JwtRegisteredClaimNames.Jti)
58 | .Select(c => new Claim(c.Key, c.Value.ToString() ?? string.Empty)).ToArray()
59 | );
60 | }
61 |
62 | protected virtual Task GetRefreshToken(Claim[] claims, string jti)
63 | {
64 | var claimList = new List((claims)
65 | .Where(c => c.Type != _tokenOptions.RefreshTokenOwnerClaimType)
66 | .Union([new Claim(_tokenOptions.RefreshTokenOwnerClaimType, jti)])
67 | );
68 |
69 | claimList.RemoveAll(c =>
70 | JwtInternalClaimTypes.Contains(c.Type)
71 | || c.Type == JwtRegisteredClaimNames.Jti);
72 | var jtiNew = _tokenOptions.JtiGenerator?.Invoke() ?? GuidIdGenerator.Instance.NewId();
73 | claimList.Add(new(JwtRegisteredClaimNames.Jti, jtiNew));
74 | var now = DateTimeOffset.UtcNow;
75 | claimList.Add(new Claim(JwtRegisteredClaimNames.Iat, now.ToUnixTimeMilliseconds().ToString(), ClaimValueTypes.Integer64));
76 | var jwt = new JwtSecurityToken(
77 | issuer: _tokenOptions.Issuer,
78 | audience: GetRefreshTokenAudience(),
79 | claims: claimList,
80 | notBefore: now.UtcDateTime,
81 | expires: now.Add(_tokenOptions.RefreshTokenValidFor).UtcDateTime,
82 | signingCredentials: _tokenOptions.RefreshTokenSigningCredentials);
83 | var encodedJwt = _tokenHandler.WriteToken(jwt);
84 | return encodedJwt.WrapTask();
85 | }
86 |
87 | private static readonly HashSet JwtInternalClaimTypes =
88 | [
89 | "iss",
90 | "exp",
91 | "aud",
92 | "nbf",
93 | "iat"
94 | ];
95 |
96 | private async Task GenerateTokenInternal(bool refreshToken, Claim[]? claims)
97 | {
98 | var now = DateTimeOffset.UtcNow;
99 | var claimList = new List()
100 | {
101 | new (JwtRegisteredClaimNames.Iat, now.ToUnixTimeMilliseconds().ToString(), ClaimValueTypes.Integer64)
102 | };
103 | if (claims is { Length: > 0 })
104 | {
105 | claimList.AddRange(
106 | claims.Where(x => !JwtInternalClaimTypes.Contains(x.Type))
107 | );
108 | }
109 | var jti = claimList.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Jti)?.Value;
110 | if (jti.IsNullOrEmpty())
111 | {
112 | jti = _tokenOptions.JtiGenerator?.Invoke() ?? GuidIdGenerator.Instance.NewId();
113 | claimList.Add(new(JwtRegisteredClaimNames.Jti, jti));
114 | }
115 | var jwt = new JwtSecurityToken(
116 | issuer: _tokenOptions.Issuer,
117 | audience: _tokenOptions.Audience,
118 | claims: claimList,
119 | notBefore: now.UtcDateTime,
120 | expires: now.Add(_tokenOptions.ValidFor).UtcDateTime,
121 | signingCredentials: _tokenOptions.SigningCredentials);
122 | var encodedJwt = _tokenHandler.WriteToken(jwt);
123 |
124 | var response = refreshToken ? new TokenEntityWithRefreshToken()
125 | {
126 | AccessToken = encodedJwt,
127 | ExpiresIn = (int)_tokenOptions.ValidFor.TotalSeconds,
128 | RefreshToken = await GetRefreshToken(claims ?? [], jti)
129 | } : new TokenEntity()
130 | {
131 | AccessToken = encodedJwt,
132 | ExpiresIn = (int)_tokenOptions.ValidFor.TotalSeconds
133 | };
134 | return response;
135 | }
136 |
137 | private string GetRefreshTokenAudience() => $"{_tokenOptions.Audience}_RefreshToken";
138 | }
139 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authorization/Jwt/JwtBearerOptionsPostSetup.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Authentication.JwtBearer;
5 |
6 | namespace WeihanLi.Web.Authorization.Jwt;
7 |
8 | internal sealed class JwtBearerOptionsPostSetup(IOptions options) :
9 | IPostConfigureOptions
10 | {
11 | private readonly IOptions _options = options;
12 |
13 | public void PostConfigure(string? name, JwtBearerOptions options)
14 | {
15 | options.Audience = _options.Value.Audience;
16 | options.ClaimsIssuer = _options.Value.Issuer;
17 | options.TokenValidationParameters = _options.Value.GetTokenValidationParameters();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authorization/Token/ITokenService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.IdentityModel.Tokens;
5 | using System.Security.Claims;
6 |
7 | namespace WeihanLi.Web.Authorization.Token;
8 |
9 | public interface ITokenService
10 | {
11 | Task GenerateToken(params Claim[] claims);
12 |
13 | Task ValidateToken(string token);
14 |
15 | Task RefreshToken(string refreshToken);
16 | }
17 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Authorization/Token/TokenEntity.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | namespace WeihanLi.Web.Authorization.Token;
5 |
6 | public class TokenEntity
7 | {
8 | public required string AccessToken { get; set; }
9 | public int ExpiresIn { get; set; }
10 | }
11 |
12 | public class TokenEntityWithRefreshToken : TokenEntity
13 | {
14 | public string? RefreshToken { get; set; }
15 | }
16 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/DataProtection/DataProtectionBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.DataProtection;
5 | using Microsoft.AspNetCore.Mvc;
6 | using WeihanLi.Web.DataProtection.ParamsProtection;
7 |
8 | namespace WeihanLi.Web.DataProtection;
9 |
10 | public static class DataProtectionBuilderExtensions
11 | {
12 | ///
13 | /// AddParamsProtection
14 | ///
15 | /// dataProtectionBuilder
16 | ///
17 | public static IDataProtectionBuilder AddParamsProtection(this IDataProtectionBuilder builder)
18 | {
19 | ArgumentNullException.ThrowIfNull(builder);
20 |
21 | builder.Services.Configure(action =>
22 | {
23 | action.Filters.Add();
24 | action.Filters.Add();
25 | });
26 |
27 | return builder;
28 | }
29 |
30 | ///
31 | /// AddParamsProtection
32 | ///
33 | /// dataProtectionBuilder
34 | /// options config action
35 | ///
36 | public static IDataProtectionBuilder AddParamsProtection(this IDataProtectionBuilder builder, Action optionsAction)
37 | {
38 | ArgumentNullException.ThrowIfNull(builder);
39 | ArgumentNullException.ThrowIfNull(optionsAction);
40 |
41 | builder.Services.Configure(optionsAction);
42 | builder.AddParamsProtection();
43 | return builder;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/DataProtection/ParamsProtection/ParamsProtectionHelper.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.DataProtection;
5 | using Newtonsoft.Json.Linq;
6 | using System.Diagnostics;
7 | using System.Diagnostics.CodeAnalysis;
8 | using System.Security.Cryptography;
9 |
10 | namespace WeihanLi.Web.DataProtection.ParamsProtection;
11 |
12 | internal static class ParamsProtectionHelper
13 | {
14 | public const string DefaultPurpose = "ParamsProtector";
15 |
16 | private static bool IsParamNeedProtect(this ParamsProtectionOptions option, string propName, string? value)
17 | {
18 | if (option.ProtectParams.Any(p => p.Equals(propName, StringComparison.OrdinalIgnoreCase)))
19 | {
20 | return (!option.ParamValueProtectFuncEnabled || option.ParamValueNeedProtectFunc(value));
21 | }
22 | return false;
23 | }
24 |
25 | private static bool IsParamNeedUnprotect(this ParamsProtectionOptions option, string propName, string? value)
26 | {
27 | if (option.ProtectParams.Any(p => p.Equals(propName, StringComparison.OrdinalIgnoreCase)))
28 | {
29 | return !option.ParamValueProtectFuncEnabled || !option.ParamValueNeedProtectFunc(value);
30 | }
31 | return false;
32 | }
33 |
34 | private static void ProtectParams(JToken token, ITimeLimitedDataProtector protector, ParamsProtectionOptions option)
35 | {
36 | if (token is JArray array)
37 | {
38 | foreach (var j in array)
39 | {
40 | if (array.Parent is JProperty property && j is JValue val)
41 | {
42 | var strJ = val.Value?.ToString();
43 | if (option.IsParamNeedProtect(property.Name, strJ))
44 | {
45 | val.Value = protector.Protect(strJ!, TimeSpan.FromMinutes(option.ExpiresIn.GetValueOrDefault(10)));
46 | }
47 | }
48 | else
49 | {
50 | ProtectParams(j, protector, option);
51 | }
52 | }
53 | }
54 | else if (token is JObject obj)
55 | {
56 | foreach (var property in obj.Children())
57 | {
58 | var val = property.Value.ToString();
59 | if (option.IsParamNeedProtect(property.Name, val))
60 | {
61 | property.Value = protector.Protect(val, TimeSpan.FromMinutes(option.ExpiresIn.GetValueOrDefault(10)));
62 | }
63 | else
64 | {
65 | if (property.Value.HasValues)
66 | {
67 | ProtectParams(property.Value, protector, option);
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
74 | public static bool TryGetUnprotectedValue(this IDataProtector protector, ParamsProtectionOptions option,
75 | string? value, [MaybeNullWhen(false)] out string unprotectedValue)
76 | {
77 | unprotectedValue = value;
78 | if (string.IsNullOrEmpty(value))
79 | return false;
80 |
81 | if (option is { AllowUnprotectedParams: true, ParamValueProtectFuncEnabled: true } && option.ParamValueNeedProtectFunc(value)
82 | )
83 | {
84 | unprotectedValue = value;
85 | }
86 | else
87 | {
88 | try
89 | {
90 | unprotectedValue = protector.Unprotect(value);
91 | }
92 | catch (Exception e)
93 | {
94 | Debug.WriteLine(e, $"Error in unprotect value:{value}");
95 | unprotectedValue = value;
96 | if (option.AllowUnprotectedParams && e is CryptographicException && !e.Message.Contains("expired"))
97 | {
98 | return true;
99 | }
100 | return false;
101 | }
102 | }
103 | return true;
104 | }
105 |
106 | public static void ProtectParams(JToken token, IDataProtector protector, ParamsProtectionOptions option)
107 | {
108 | if (option is { Enabled: true, ProtectParams.Length: > 0 })
109 | {
110 | if (protector is ITimeLimitedDataProtector timeLimitedDataProtector)
111 | {
112 | ProtectParams(token, timeLimitedDataProtector, option);
113 | return;
114 | }
115 | if (token is JArray array)
116 | {
117 | foreach (var j in array)
118 | {
119 | if (array.Parent is JProperty property && j is JValue val)
120 | {
121 | var strJ = val.Value?.ToString();
122 | if (option.IsParamNeedProtect(property.Name, strJ))
123 | {
124 | val.Value = protector.Protect(strJ!);
125 | }
126 | }
127 | else
128 | {
129 | ProtectParams(j, protector, option);
130 | }
131 | }
132 | }
133 | else if (token is JObject obj)
134 | {
135 | foreach (var property in obj.Children())
136 | {
137 | var val = property.Value.ToString();
138 | if (option.IsParamNeedProtect(property.Name, val))
139 | {
140 | property.Value = protector.Protect(val);
141 | }
142 | else
143 | {
144 | if (property.Value.HasValues)
145 | {
146 | ProtectParams(property.Value, protector, option);
147 | }
148 | }
149 | }
150 | }
151 | }
152 | }
153 |
154 | public static void UnProtectParams(JToken token, IDataProtector protector, ParamsProtectionOptions option)
155 | {
156 | if (option is { Enabled: true, ProtectParams.Length: > 0 })
157 | {
158 | if (token is JArray array)
159 | {
160 | foreach (var j in array)
161 | {
162 | if (j is JValue val)
163 | {
164 | var strJ = val.Value?.ToString();
165 | if (array.Parent is JProperty property && option.IsParamNeedUnprotect(property.Name, strJ))
166 | {
167 | try
168 | {
169 | val.Value = protector.Unprotect(strJ!);
170 | }
171 | catch (Exception e)
172 | {
173 | Debug.WriteLine(e);
174 | if (option.AllowUnprotectedParams && e is CryptographicException && !e.Message.Contains("expired"))
175 | {
176 | val.Value = strJ;
177 | }
178 | else
179 | {
180 | throw;
181 | }
182 | }
183 | }
184 | }
185 | else
186 | {
187 | UnProtectParams(j, protector, option);
188 | }
189 | }
190 | }
191 | else if (token is JObject obj)
192 | {
193 | foreach (var property in obj.Children())
194 | {
195 | if (property.Value is JArray)
196 | {
197 | UnProtectParams(property.Value, protector, option);
198 | }
199 | else
200 | {
201 | var val = property.Value.ToString();
202 | if (option.IsParamNeedUnprotect(property.Name, val))
203 | {
204 | try
205 | {
206 | property.Value = protector.Unprotect(val);
207 | }
208 | catch (Exception e)
209 | {
210 | Debug.WriteLine(e);
211 | if (option.AllowUnprotectedParams && e is CryptographicException && !e.Message.Contains("expired"))
212 | {
213 | property.Value = val;
214 | }
215 | else
216 | {
217 | throw;
218 | }
219 | }
220 | }
221 | else
222 | {
223 | if (property.Value.HasValues)
224 | {
225 | UnProtectParams(property.Value, protector, option);
226 | }
227 | }
228 | }
229 | }
230 | }
231 | }
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/DataProtection/ParamsProtection/ParamsProtectionOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Mvc;
5 | using System.Linq.Expressions;
6 | using WeihanLi.Extensions;
7 |
8 | namespace WeihanLi.Web.DataProtection.ParamsProtection;
9 |
10 | public sealed class ParamsProtectionOptions
11 | {
12 | private string[] _protectParams = [];
13 |
14 | ///
15 | /// ProtectorPurpose
16 | ///
17 | public string? ProtectorPurpose { get; set; } = "ParamsProtection";
18 |
19 | ///
20 | /// ExpiresIn, minutes
21 | ///
22 | public int? ExpiresIn { get; set; }
23 |
24 | ///
25 | /// Enabled for paramsProtection
26 | ///
27 | public bool Enabled { get; set; } = true;
28 |
29 | ///
30 | /// Allow unprotected params
31 | ///
32 | public bool AllowUnprotectedParams { get; set; }
33 |
34 | ///
35 | /// Invalid request response http status code
36 | /// refer to https://restfulapi.net/http-status-codes/
37 | ///
38 | public int InvalidRequestStatusCode { get; set; } = 412;
39 |
40 | ///
41 | /// the params to protect
42 | ///
43 | public string[] ProtectParams
44 | {
45 | get => _protectParams;
46 | set
47 | {
48 | // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
49 | if (value is not null)
50 | {
51 | _protectParams = value;
52 | }
53 | }
54 | }
55 |
56 | ///
57 | /// whether the NeedProtectFunc is enabled
58 | ///
59 | public bool ParamValueProtectFuncEnabled { get; set; }
60 |
61 | ///
62 | /// whether the parameter should be protected
63 | ///
64 | public Func ParamValueNeedProtectFunc { get; set; } = str => long.TryParse(str, out _);
65 |
66 | ///
67 | /// whether the response should be protected
68 | ///
69 | internal IDictionary NeedProtectResponseValues { get; } = new Dictionary()
70 | {
71 | { typeof(ObjectResult), "Value"}
72 | };
73 |
74 | ///
75 | /// Add type and value ToProtectValues
76 | ///
77 | /// TResult
78 | /// the value of the type to protect
79 | public void AddProtectValue(Expression> valueExpression) where TResult : class, IActionResult
80 | {
81 | NeedProtectResponseValues[typeof(TResult)] = valueExpression.GetMemberInfo().Name;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/DataProtection/ParamsProtection/ParamsProtectionResourceFilter.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.DataProtection;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.AspNetCore.Mvc.Filters;
7 | using Microsoft.Extensions.Logging;
8 | using Microsoft.Extensions.Primitives;
9 | using Newtonsoft.Json.Linq;
10 | using System.Text;
11 | using WeihanLi.Common.Helpers;
12 | using WeihanLi.Extensions;
13 |
14 | namespace WeihanLi.Web.DataProtection.ParamsProtection;
15 |
16 | public sealed class ParamsProtectionResourceFilter : IResourceFilter
17 | {
18 | private static readonly Lazy XmlDataSerializer = new(() => new XmlDataSerializer());
19 | private readonly IDataProtector _protector;
20 | private readonly ParamsProtectionOptions _option;
21 |
22 | private readonly ILogger _logger;
23 |
24 | public ParamsProtectionResourceFilter(IDataProtectionProvider protectionProvider, ILogger logger, IOptions options)
25 | {
26 | _option = options.Value;
27 | _protector = protectionProvider.CreateProtector(_option.ProtectorPurpose ?? ParamsProtectionHelper.DefaultPurpose);
28 | if (_option.ExpiresIn.GetValueOrDefault(0) > 0)
29 | {
30 | _protector = _protector.ToTimeLimitedDataProtector();
31 | }
32 |
33 | _logger = logger;
34 | }
35 |
36 | public void OnResourceExecuting(ResourceExecutingContext context)
37 | {
38 | if (_option is { Enabled: true, ProtectParams.Length: > 0 })
39 | {
40 | var request = context.HttpContext.Request;
41 |
42 | // QueryString
43 | if (request.Query.Count > 0)
44 | {
45 | var queryDic = request.Query.ToDictionary(query => query.Key, query => query.Value);
46 | foreach (var param in _option.ProtectParams)
47 | {
48 | if (queryDic.ContainsKey(param))
49 | {
50 | var values = new List(queryDic[param].Count);
51 | for (var i = 0; i < queryDic[param].Count; i++)
52 | {
53 | if (_protector.TryGetUnprotectedValue(_option, queryDic[param][i]!, out var val))
54 | {
55 | values.Add(val);
56 | }
57 | else
58 | {
59 | _logger.LogWarning($"Error in unprotect query value: param:{param}");
60 | context.Result = new StatusCodeResult(_option.InvalidRequestStatusCode);
61 |
62 | return;
63 | }
64 | }
65 | queryDic[param] = new StringValues(values.ToArray());
66 | }
67 | context.HttpContext.Request.Query = new QueryCollection(queryDic);
68 | }
69 | }
70 | // route value
71 | if (context.RouteData.Values.Count > 0)
72 | {
73 | foreach (var param in _option.ProtectParams)
74 | {
75 | if (context.RouteData.Values.ContainsKey(param))
76 | {
77 | if (_protector.TryGetUnprotectedValue(_option, context.RouteData.Values[param]?.ToString(), out var val))
78 | {
79 | context.RouteData.Values[param] = val;
80 | }
81 | else
82 | {
83 | _logger.LogWarning($"Error in un-protect routeValue:{param}");
84 |
85 | context.Result = new StatusCodeResult(_option.InvalidRequestStatusCode);
86 |
87 | return;
88 | }
89 | }
90 | }
91 | }
92 |
93 | if (request.Method.EqualsIgnoreCase("POST") || request.Method.EqualsIgnoreCase("PUT"))
94 | {
95 | var contentType = request.ContentType ?? string.Empty;
96 | if (contentType.Contains("json"))
97 | {
98 | using var reader = new StreamReader(request.Body, Encoding.UTF8);
99 | var content = reader.ReadToEnd();
100 | var obj = content.JsonToObject();
101 | try
102 | {
103 | ParamsProtectionHelper.UnProtectParams(obj, _protector, _option);
104 | }
105 | catch (Exception e)
106 | {
107 | _logger.LogWarning(e, "Error in unprotect request body");
108 |
109 | context.Result = new StatusCodeResult(_option.InvalidRequestStatusCode);
110 |
111 | return;
112 | }
113 |
114 | context.HttpContext.Request.Body = obj.ToJson().GetBytes().ToMemoryStream();
115 | } // json body
116 | else if (contentType.Contains("xml"))
117 | {
118 | // TODO: need test
119 | var obj = XmlDataSerializer.Value.Deserialize(request.Body.ToByteArray());
120 | try
121 | {
122 | ParamsProtectionHelper.UnProtectParams(obj, _protector, _option);
123 | }
124 | catch (Exception e)
125 | {
126 | _logger.LogWarning(e, "Error in unprotect request body");
127 |
128 | context.Result = new StatusCodeResult(_option.InvalidRequestStatusCode);
129 |
130 | return;
131 | }
132 | context.HttpContext.Request.Body = XmlDataSerializer.Value.Serialize(obj).ToMemoryStream();
133 | } // xml body
134 |
135 | // form data
136 | if (request is { HasFormContentType: true, Form.Count: > 0 })
137 | {
138 | var formDic = request.Form.ToDictionary(p => p.Key, p => p.Value);
139 | foreach (var param in _option.ProtectParams)
140 | {
141 | if (formDic.TryGetValue(param, out var values))
142 | {
143 | var vals = new List();
144 | foreach (var item in values)
145 | {
146 | if (_protector.TryGetUnprotectedValue(_option, item, out var val))
147 | {
148 | vals.Add(val);
149 | }
150 | else
151 | {
152 | _logger.LogWarning($"Error in unprotect form data: param:{param}");
153 | context.Result = new StatusCodeResult(_option.InvalidRequestStatusCode);
154 |
155 | return;
156 | }
157 | }
158 | formDic[param] = new StringValues(vals.ToArray());
159 | }
160 | }
161 | request.Form = new FormCollection(formDic);
162 | }
163 | }
164 | }
165 | }
166 |
167 | public void OnResourceExecuted(ResourceExecutedContext context)
168 | {
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/DataProtection/ParamsProtection/ParamsProtectionResultFilter.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.DataProtection;
5 | using Microsoft.AspNetCore.Mvc.Filters;
6 | using Microsoft.Extensions.Logging;
7 | using Newtonsoft.Json.Linq;
8 | using WeihanLi.Common;
9 | using WeihanLi.Extensions;
10 |
11 | namespace WeihanLi.Web.DataProtection.ParamsProtection;
12 |
13 | public sealed class ParamsProtectionResultFilter : IResultFilter
14 | {
15 | private readonly IDataProtector _protector;
16 | private readonly ParamsProtectionOptions _option;
17 | private readonly ILogger _logger;
18 |
19 | public ParamsProtectionResultFilter(IDataProtectionProvider protectionProvider, IOptions options, ILogger logger)
20 | {
21 | _logger = logger;
22 | _option = options.Value;
23 |
24 | _protector = protectionProvider.CreateProtector(_option.ProtectorPurpose ?? ParamsProtectionHelper.DefaultPurpose);
25 |
26 | if (_option.ExpiresIn.GetValueOrDefault(0) > 0)
27 | {
28 | _protector = _protector.ToTimeLimitedDataProtector();
29 | }
30 | }
31 |
32 | public void OnResultExecuting(ResultExecutingContext context)
33 | {
34 | if (_option is { Enabled: true, ProtectParams.Length: > 0 })
35 | {
36 | foreach (var pair in _option.NeedProtectResponseValues)
37 | {
38 | if (pair.Key.IsInstanceOfType(context.Result))
39 | {
40 | var prop = CacheUtil.GetTypeProperties(pair.Key).FirstOrDefault(p => p.Name == pair.Value);
41 | var val = prop?.GetValueGetter()?.Invoke(context.Result);
42 | if (val is not null)
43 | {
44 | _logger.LogDebug($"ParamsProtector is protecting {pair.Key.FullName} Value");
45 |
46 | var obj = JToken.FromObject(val);
47 | ParamsProtectionHelper.ProtectParams(obj, _protector, _option);
48 | prop!.GetValueSetter()?.Invoke(context.Result, obj);
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | public void OnResultExecuted(ResultExecutedContext context)
56 | {
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Extensions/DependenceResolverExtension.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using WeihanLi.Common;
5 |
6 | namespace WeihanLi.Web.Extensions;
7 |
8 | public static class DependenceResolverExtension
9 | {
10 | ///
11 | /// try get service from HttpContext.RequestServices
12 | ///
13 | /// TService
14 | /// dependencyResolver
15 | /// service instance
16 | public static TService ResolveCurrentService(this IDependencyResolver dependencyResolver)
17 | where TService : class
18 | {
19 | var contextAccessor = dependencyResolver.GetRequiredService();
20 | return Guard.NotNull(contextAccessor.HttpContext).RequestServices.GetRequiredService();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Extensions/EndpointExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using WeihanLi.Common.Helpers;
5 | using WeihanLi.Web.Middleware;
6 |
7 | namespace WeihanLi.Web.Extensions;
8 |
9 | public static class EndpointExtensions
10 | {
11 | public static IEndpointConventionBuilder MapRuntimeInfo(this IEndpointRouteBuilder endpointRouteBuilder, string path = "/runtime-info")
12 | {
13 | ArgumentNullException.ThrowIfNull(endpointRouteBuilder);
14 | return endpointRouteBuilder.MapGet(path, () => ApplicationHelper.RuntimeInfo);
15 | }
16 |
17 | public static IEndpointConventionBuilder MapConfigInspector(this IEndpointRouteBuilder endpointRouteBuilder,
18 | string path = "/config-inspector",
19 | Action? optionsConfigure = null
20 | )
21 | {
22 | ArgumentNullException.ThrowIfNull(endpointRouteBuilder);
23 |
24 | if (optionsConfigure is not null)
25 | {
26 | var options = endpointRouteBuilder.ServiceProvider.GetRequiredService>();
27 | optionsConfigure(options.Value);
28 | }
29 |
30 | return endpointRouteBuilder.MapGet($"{path}/{{configKey?}}", async (context) =>
31 | {
32 | var options = context.RequestServices.GetRequiredService>();
33 | await ConfigInspectorMiddleware.InvokeAsync(context, options);
34 | });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Extensions/FluentAspectServiceProviderFactory.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.Extensions.Hosting;
5 | using System.Linq.Expressions;
6 | using WeihanLi.Common.Aspect;
7 | using WeihanLi.Extensions;
8 |
9 | namespace WeihanLi.Web.Extensions;
10 |
11 | internal sealed class FluentAspectsServiceProviderFactory(
12 | Action optionsAction,
13 | Action? aspectBuildAction,
14 | Expression> ignoreTypesPredict)
15 | : IServiceProviderFactory
16 | {
17 | public IServiceCollection CreateBuilder(IServiceCollection services)
18 | {
19 | return services;
20 | }
21 |
22 | public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder)
23 | {
24 | return containerBuilder.BuildFluentAspectsProvider(optionsAction, aspectBuildAction, ignoreTypesPredict);
25 | }
26 | }
27 |
28 | public static class FluentAspectServiceProviderFactoryExtensions
29 | {
30 | public static IHostBuilder UseFluentAspectsServiceProviderFactory(this IHostBuilder hostBuilder,
31 | Action optionsAction,
32 | Action? aspectBuildAction = null,
33 | Expression>? ignoreTypesPredict = null)
34 | {
35 | if (ignoreTypesPredict == null)
36 | {
37 | ignoreTypesPredict = t =>
38 | t.HasNamespace()
39 | && (t.Namespace!.StartsWith("Microsoft.") == true
40 | || t.Namespace.StartsWith("System.") == true)
41 | ;
42 | }
43 | hostBuilder.UseServiceProviderFactory(
44 | new FluentAspectsServiceProviderFactory(optionsAction, aspectBuildAction, ignoreTypesPredict)
45 | );
46 | return hostBuilder;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Extensions/HealthCheckExtension.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.Extensions.Logging;
5 |
6 | namespace WeihanLi.Web.Extensions;
7 |
8 | public static class HealthCheckExtensions
9 | {
10 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder)
11 | {
12 | return UseHealthCheck(applicationBuilder, new PathString("/api/health"));
13 | }
14 |
15 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, string path)
16 | {
17 | return UseHealthCheck(applicationBuilder, new PathString(path));
18 | }
19 |
20 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, PathString path)
21 | {
22 | static Task DefaultHealthCheck(HttpContext context, Func next)
23 | {
24 | context.Response.StatusCode = 200;
25 | return context.Response.WriteAsync("healthy");
26 | }
27 | applicationBuilder.Map(path, builder => builder.Use(DefaultHealthCheck));
28 | return applicationBuilder;
29 | }
30 |
31 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, string path, Func checkFunc)
32 | {
33 | return UseHealthCheck(applicationBuilder, new PathString(path), serviceProvider => Task.FromResult(checkFunc(serviceProvider)));
34 | }
35 |
36 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, string path,
37 | Func> checkFunc)
38 | {
39 | return UseHealthCheck(applicationBuilder, new PathString(path), checkFunc);
40 | }
41 |
42 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, PathString path, Func? checkFunc)
43 | {
44 | if (checkFunc == null)
45 | {
46 | checkFunc = _ => true;
47 | }
48 | return UseHealthCheck(applicationBuilder, path, serviceProvider => Task.FromResult(checkFunc(serviceProvider)));
49 | }
50 |
51 | public static IApplicationBuilder UseHealthCheck(this IApplicationBuilder applicationBuilder, PathString path, Func>? checkFunc)
52 | {
53 | if (checkFunc == null)
54 | {
55 | checkFunc = _ => Task.FromResult(true);
56 | }
57 |
58 | async Task CheckFunc(HttpContext context, Func next)
59 | {
60 | try
61 | {
62 | var healthy = await checkFunc.Invoke(context.RequestServices);
63 | if (healthy)
64 | {
65 | context.Response.StatusCode = StatusCodes.Status200OK;
66 | await context.Response.WriteAsync("healthy");
67 | }
68 | else
69 | {
70 | context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
71 | await context.Response.WriteAsync("unhealthy");
72 | }
73 | }
74 | catch (Exception ex)
75 | {
76 | context.RequestServices.GetRequiredService()
77 | .CreateLogger(typeof(HealthCheckExtensions)).LogError(ex, "HealthCheck Exception");
78 | context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
79 | await context.Response.WriteAsync("unhealthy");
80 | }
81 | }
82 | applicationBuilder.Map(path, builder => builder.Use(CheckFunc));
83 | return applicationBuilder;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Extensions/HttpContextExtension.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using System.IdentityModel.Tokens.Jwt;
5 | using System.Security.Claims;
6 | using WeihanLi.Extensions;
7 |
8 | namespace WeihanLi.Web.Extensions;
9 |
10 | public static class HttpContextExtension
11 | {
12 | ///
13 | /// GetUserIP
14 | ///
15 | /// httpContext
16 | /// realIPHeader, default `X-Forwarded-For`
17 | /// user ip
18 | // ReSharper disable InconsistentNaming
19 | public static string? GetUserIP(this HttpContext httpContext, string realIPHeader = "X-Forwarded-For")
20 | {
21 | ArgumentNullException.ThrowIfNull(httpContext);
22 |
23 | return httpContext.Request.Headers.TryGetValue(realIPHeader, out var ip)
24 | ? ip.ToString()
25 | : httpContext.Connection.RemoteIpAddress?.MapToIPv4().ToString();
26 | }
27 |
28 | ///
29 | /// GetUserId from claims, get from `ClaimTypes.NameIdentifier/`nameid`/`sub` by default
30 | ///
31 | /// principal
32 | ///
33 | public static string? GetUserId(this ClaimsPrincipal principal)
34 | {
35 | var userId = GetUserId(principal, ClaimTypes.NameIdentifier);
36 | if (!string.IsNullOrEmpty(userId))
37 | {
38 | return userId;
39 | }
40 | userId = GetUserId(principal, JwtRegisteredClaimNames.NameId);
41 | if (!string.IsNullOrEmpty(userId))
42 | {
43 | return userId;
44 | }
45 | return GetUserId(principal, JwtRegisteredClaimNames.Sub);
46 | }
47 |
48 | ///
49 | /// GetUserId from claims, get from ClaimTypes.NameIdentifier/`nameid`/`sub` by default
50 | ///
51 | /// userId type
52 | /// principal
53 | ///
54 | public static T GetUserId(this ClaimsPrincipal principal) => GetUserId(principal).ToOrDefault()!;
55 |
56 | public static T GetUserId(this ClaimsPrincipal principal, string claimType)
57 | => GetUserId(principal, claimType).ToOrDefault()!;
58 |
59 | public static string? GetUserId(this ClaimsPrincipal principal, string claimType)
60 | => principal.FindFirst(claimType)?.Value;
61 | }
62 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Extensions/HttpContextTenantProviderExtension.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.Extensions.DependencyInjection.Extensions;
5 | using WeihanLi.Common;
6 | using WeihanLi.Common.Services;
7 | using WeihanLi.Web.Services;
8 |
9 | namespace WeihanLi.Web.Extensions;
10 |
11 | public static class HttpContextTenantProviderExtension
12 | {
13 | public static IServiceCollection AddHttpContextTenantProvider(this IServiceCollection serviceCollection)
14 | {
15 | Guard.NotNull(serviceCollection);
16 | serviceCollection.TryAddSingleton();
17 | serviceCollection.TryAddSingleton();
18 | return serviceCollection;
19 | }
20 |
21 | public static IServiceCollection AddHttpContextTenantProvider(this IServiceCollection serviceCollection, Action optionsAction)
22 | {
23 | Guard.NotNull(serviceCollection);
24 | Guard.NotNull(optionsAction);
25 |
26 | serviceCollection.Configure(optionsAction);
27 | return serviceCollection.AddHttpContextTenantProvider();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Extensions/HttpContextUserIdProviderExtension.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.Extensions.DependencyInjection.Extensions;
5 | using WeihanLi.Common;
6 | using WeihanLi.Common.Services;
7 | using WeihanLi.Web.Services;
8 |
9 | namespace WeihanLi.Web.Extensions;
10 |
11 | public static class HttpContextUserIdProviderExtension
12 | {
13 | public static IServiceCollection AddHttpContextUserIdProvider(this IServiceCollection serviceCollection)
14 | {
15 | Guard.NotNull(serviceCollection);
16 | serviceCollection.TryAddSingleton();
17 | serviceCollection.TryAddSingleton();
18 | return serviceCollection;
19 | }
20 |
21 | public static IServiceCollection AddHttpContextUserIdProvider(this IServiceCollection serviceCollection, Action optionsAction)
22 | {
23 | Guard.NotNull(serviceCollection);
24 | Guard.NotNull(optionsAction);
25 |
26 | serviceCollection.Configure(optionsAction);
27 | return serviceCollection.AddHttpContextUserIdProvider();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Extensions/MiddlewareExtension.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using WeihanLi.Web.Middleware;
5 |
6 | namespace WeihanLi.Web.Extensions;
7 |
8 | public static class MiddlewareExtension
9 | {
10 | ///
11 | /// UseCustomExceptionHandler
12 | ///
13 | /// applicationBuilder
14 | ///
15 | public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder applicationBuilder)
16 | {
17 | applicationBuilder.UseMiddleware();
18 | return applicationBuilder;
19 | }
20 |
21 | ///
22 | /// Use middleware if feature is enabled
23 | ///
24 | public static IApplicationBuilder UseIfFeatureEnabled(this IApplicationBuilder app, Func middleware, string featureFlagName, bool defaultValue = false)
25 | {
26 | var configuration = app.ApplicationServices.GetRequiredService();
27 | if (configuration.IsFeatureEnabled(featureFlagName, defaultValue))
28 | {
29 | app.Use(middleware);
30 | }
31 | return app;
32 | }
33 |
34 | ///
35 | /// Use middleware if feature is enabled
36 | ///
37 | public static IApplicationBuilder UseIfFeatureEnabled(this IApplicationBuilder app, Func middleware, string featureFlagName, bool defaultValue = false)
38 | {
39 | var configuration = app.ApplicationServices.GetRequiredService();
40 | if (configuration.IsFeatureEnabled(featureFlagName, defaultValue))
41 | {
42 | app.Use(middleware);
43 | }
44 | return app;
45 | }
46 |
47 | ///
48 | /// Use middleware if feature is enabled
49 | ///
50 | public static IApplicationBuilder UseIfFeatureEnabled(this IApplicationBuilder app, Func, Task> middleware, string featureFlagName, bool defaultValue = false)
51 | {
52 | var configuration = app.ApplicationServices.GetRequiredService();
53 | if (configuration.IsFeatureEnabled(featureFlagName, defaultValue))
54 | {
55 | app.Use(middleware);
56 | }
57 | return app;
58 | }
59 |
60 | public static IApplicationBuilder UseIfFeatureEnabled(this IApplicationBuilder app, string featureFlagName, bool defaultValue = false)
61 | {
62 | var configuration = app.ApplicationServices.GetRequiredService();
63 | if (configuration.IsFeatureEnabled(featureFlagName, defaultValue))
64 | {
65 | app.UseMiddleware();
66 | }
67 | return app;
68 | }
69 |
70 | ///
71 | /// Use middleware when feature is enabled, based on UseWhen
72 | ///
73 | public static IApplicationBuilder UseWhenFeatureEnabled(
74 | this IApplicationBuilder app,
75 | Action configure,
76 | string featureFlagName,
77 | bool defaultValue = false)
78 | {
79 | return app.UseWhen(context => context.RequestServices.GetRequiredService()
80 | .IsFeatureEnabled(featureFlagName, defaultValue), configure);
81 | }
82 |
83 | ///
84 | /// Use ConfigInspector to inspect config when necessary
85 | ///
86 | internal static IApplicationBuilder UseConfigInspector(this IApplicationBuilder app,
87 | Action? optionsConfigure = null)
88 | {
89 | ArgumentNullException.ThrowIfNull(app);
90 |
91 | return app.UseMiddleware();
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Extensions/ResultExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Mvc;
5 | using System.Net;
6 | using WeihanLi.Common.Models;
7 |
8 | namespace WeihanLi.Web.Extensions;
9 |
10 | public static class ResultModelExtensions
11 | {
12 | public static IActionResult GetOkResult(this T result)
13 | {
14 | return result is null
15 | ? new OkResult()
16 | : new OkObjectResult(result);
17 | }
18 |
19 | public static IActionResult GetRestResult(this T result, ResultStatus status = ResultStatus.Success)
20 | {
21 | if (result is null)
22 | return new NoContentResult();
23 |
24 | return status switch
25 | {
26 | ResultStatus.BadRequest => new BadRequestObjectResult(result),
27 | ResultStatus.NotFound => new NotFoundObjectResult(result),
28 | ResultStatus.MethodNotAllowed => new ObjectResult(result)
29 | {
30 | StatusCode = (int)HttpStatusCode.MethodNotAllowed
31 | },
32 | ResultStatus.Unauthorized => new ObjectResult(result) { StatusCode = (int)HttpStatusCode.Unauthorized },
33 | ResultStatus.Forbidden => new ObjectResult(result) { StatusCode = (int)HttpStatusCode.Forbidden },
34 | _ => new OkObjectResult(result)
35 | };
36 | }
37 |
38 | public static IActionResult GetRestResult(this ResultStatus status)
39 | {
40 | return status switch
41 | {
42 | ResultStatus.BadRequest => new BadRequestResult(),
43 | ResultStatus.NotFound => new NotFoundResult(),
44 | ResultStatus.MethodNotAllowed => new StatusCodeResult((int)HttpStatusCode.MethodNotAllowed),
45 | ResultStatus.Unauthorized => new StatusCodeResult((int)HttpStatusCode.Unauthorized),
46 | ResultStatus.Forbidden => new StatusCodeResult((int)HttpStatusCode.Forbidden),
47 | ResultStatus.Success => new OkResult(),
48 | _ => new StatusCodeResult((int)status)
49 | };
50 | }
51 |
52 | public static IActionResult GetRestResult(this Result result)
53 | {
54 | return GetRestResult(result, result.Status);
55 | }
56 |
57 | public static IActionResult GetRestResult(this Result result)
58 | {
59 | return GetRestResult(result, result.Status);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/WeihanLi.Web.Extensions/Filters/ApiResultFilter.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Weihan Li. All rights reserved.
2 | // Licensed under the MIT license.
3 |
4 | using Microsoft.AspNetCore.Mvc;
5 | using Microsoft.AspNetCore.Mvc.Filters;
6 | using WeihanLi.Common.Models;
7 |
8 | namespace WeihanLi.Web.Filters;
9 |
10 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
11 | public sealed class ApiResultFilter : Attribute
12 | , IResultFilter, IExceptionFilter
13 | , IEndpointFilter
14 |
15 | {
16 | public void OnResultExecuting(ResultExecutingContext context)
17 | {
18 | if (context.Result is ObjectResult { Value: not Result } objectResult)
19 | {
20 | var result = new Result