├── .gitattributes
├── .gitignore
├── AspNetCore.Authentication.ApiToken.sln
├── AspNetCore.Authentication.ApiToken.sln.DotSettings
├── LICENSE
├── README.md
├── README_zh-CN.md
├── assets
└── op.gif
├── build
├── common.props
└── version.props
├── sample
└── AspNetCore.ApiToken.SampleApp
│ ├── ApiTokenDbContext.cs
│ ├── AspNetCore.ApiToken.SampleApp.csproj
│ ├── Controllers
│ └── TokenController.cs
│ ├── Entities
│ ├── ApiToken.cs
│ └── User.cs
│ ├── Migrations
│ ├── 20201229074003_init.Designer.cs
│ ├── 20201229074003_init.cs
│ └── ApiTokenDbContextModelSnapshot.cs
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── README.md
│ ├── Startup.cs
│ ├── Store
│ ├── ClaimConverter.cs
│ ├── ClaimLite.cs
│ ├── MyApiTokenProfileService.cs
│ └── MyApiTokenStore.cs
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ └── assets
│ └── swagger.png
├── src
├── AspNetCore.AuthenticationApiToken.Redis
│ ├── AspNetCore.Authentication.ApiToken.Redis.csproj
│ ├── RedisTokenCacheExtensions.cs
│ ├── RedisTokenCacheOptions.cs
│ ├── RedisTokenCachePostConfigureOptions.cs
│ └── RedisTokenCacheService.cs
└── AspNetCore.AuthenticationApiToken
│ ├── Abstractions
│ ├── IApiTokenCacheService.cs
│ ├── IApiTokenOperator .cs
│ ├── IApiTokenProfileService.cs
│ ├── IApiTokenStore.cs
│ └── IApiTokenValidator.cs
│ ├── ApiTokenAuthenticationBuilder.cs
│ ├── ApiTokenCacheOptions.cs
│ ├── ApiTokenClaimTypes.cs
│ ├── ApiTokenCleanHostedService.cs
│ ├── ApiTokenCleanOptions.cs
│ ├── ApiTokenDefaults.cs
│ ├── ApiTokenEvents.cs
│ ├── ApiTokenExtensions.cs
│ ├── ApiTokenHandler.cs
│ ├── ApiTokenInitializeService.cs
│ ├── ApiTokenOptions.cs
│ ├── ApiTokenPostConfigureOptions.cs
│ ├── ApiTokenTools.cs
│ ├── AspNetCore.Authentication.ApiToken.csproj
│ ├── AspNetCore.Authentication.ApiToken.csproj.DotSettings
│ ├── Cache
│ ├── NullApiTokenCacheService.cs
│ └── TokenModelCache.cs
│ ├── Events
│ ├── ApiTokenChallengeContext.cs
│ ├── ApiTokenValidatedContext.cs
│ ├── AuthenticationFailedContext.cs
│ ├── ForbiddenContext.cs
│ └── MessageReceivedContext.cs
│ ├── Exceptions
│ ├── TokenExpiredException.cs
│ └── TokenInvalidException.cs
│ ├── HttpContextExtensions.cs
│ ├── LoggingExtensions.cs
│ ├── Parse
│ ├── ApiTokenParseAttribute.cs
│ └── ApiTokenParseType.cs
│ ├── Store
│ ├── DefaultApiTokenOperator.cs
│ ├── Results
│ │ ├── RefreshClaimsResult.cs
│ │ ├── ResultBase.cs
│ │ └── TokenCreateResult.cs
│ ├── TokenModel.cs
│ └── TokenType.cs
│ └── Validate
│ └── DefaultApiTokenValidator.cs
└── tests
└── AspNetCore.ApiToken.UnitTests
├── AspNetCore.ApiToken.UnitTests.csproj
└── TokenTests.cs
/.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 |
--------------------------------------------------------------------------------
/.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 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015/2017 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # Visual Studio 2017 auto generated files
36 | Generated\ Files/
37 |
38 | # MSTest test Results
39 | [Tt]est[Rr]esult*/
40 | [Bb]uild[Ll]og.*
41 |
42 | # NUNIT
43 | *.VisualState.xml
44 | TestResult.xml
45 |
46 | # Build Results of an ATL Project
47 | [Dd]ebugPS/
48 | [Rr]eleasePS/
49 | dlldata.c
50 |
51 | # Benchmark Results
52 | BenchmarkDotNet.Artifacts/
53 |
54 | # .NET Core
55 | project.lock.json
56 | project.fragment.lock.json
57 | artifacts/
58 |
59 | # StyleCop
60 | StyleCopReport.xml
61 |
62 | # Files built by Visual Studio
63 | *_i.c
64 | *_p.c
65 | *_h.h
66 | *.ilk
67 | *.meta
68 | *.obj
69 | *.iobj
70 | *.pch
71 | *.pdb
72 | *.ipdb
73 | *.pgc
74 | *.pgd
75 | *.rsp
76 | *.sbr
77 | *.tlb
78 | *.tli
79 | *.tlh
80 | *.tmp
81 | *.tmp_proj
82 | *_wpftmp.csproj
83 | *.log
84 | *.vspscc
85 | *.vssscc
86 | .builds
87 | *.pidb
88 | *.svclog
89 | *.scc
90 |
91 | # Chutzpah Test files
92 | _Chutzpah*
93 |
94 | # Visual C++ cache files
95 | ipch/
96 | *.aps
97 | *.ncb
98 | *.opendb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 | *.VC.db
103 | *.VC.VC.opendb
104 |
105 | # Visual Studio profiler
106 | *.psess
107 | *.vsp
108 | *.vspx
109 | *.sap
110 |
111 | # Visual Studio Trace Files
112 | *.e2e
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # AxoCover is a Code Coverage Tool
135 | .axoCover/*
136 | !.axoCover/settings.json
137 |
138 | # Visual Studio code coverage results
139 | *.coverage
140 | *.coveragexml
141 |
142 | # NCrunch
143 | _NCrunch_*
144 | .*crunch*.local.xml
145 | nCrunchTemp_*
146 |
147 | # MightyMoose
148 | *.mm.*
149 | AutoTest.Net/
150 |
151 | # Web workbench (sass)
152 | .sass-cache/
153 |
154 | # Installshield output folder
155 | [Ee]xpress/
156 |
157 | # DocProject is a documentation generator add-in
158 | DocProject/buildhelp/
159 | DocProject/Help/*.HxT
160 | DocProject/Help/*.HxC
161 | DocProject/Help/*.hhc
162 | DocProject/Help/*.hhk
163 | DocProject/Help/*.hhp
164 | DocProject/Help/Html2
165 | DocProject/Help/html
166 |
167 | # Click-Once directory
168 | publish/
169 |
170 | # Publish Web Output
171 | *.[Pp]ublish.xml
172 | *.azurePubxml
173 | # Note: Comment the next line if you want to checkin your web deploy settings,
174 | # but database connection strings (with potential passwords) will be unencrypted
175 | *.pubxml
176 | *.publishproj
177 |
178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
179 | # checkin your Azure Web App publish settings, but sensitive information contained
180 | # in these scripts will be unencrypted
181 | PublishScripts/
182 |
183 | # NuGet Packages
184 | *.nupkg
185 | # The packages folder can be ignored because of Package Restore
186 | **/[Pp]ackages/*
187 | # except build/, which is used as an MSBuild target.
188 | !**/[Pp]ackages/build/
189 | # Uncomment if necessary however generally it will be regenerated when needed
190 | #!**/[Pp]ackages/repositories.config
191 | # NuGet v3's project.json files produces more ignorable files
192 | *.nuget.props
193 | *.nuget.targets
194 |
195 | # Microsoft Azure Build Output
196 | csx/
197 | *.build.csdef
198 |
199 | # Microsoft Azure Emulator
200 | ecf/
201 | rcf/
202 |
203 | # Windows Store app package directories and files
204 | AppPackages/
205 | BundleArtifacts/
206 | Package.StoreAssociation.xml
207 | _pkginfo.txt
208 | *.appx
209 |
210 | # Visual Studio cache files
211 | # files ending in .cache can be ignored
212 | *.[Cc]ache
213 | # but keep track of directories ending in .cache
214 | !?*.[Cc]ache/
215 |
216 | # Others
217 | ClientBin/
218 | ~$*
219 | *~
220 | *.dbmdl
221 | *.dbproj.schemaview
222 | *.jfm
223 | *.pfx
224 | *.publishsettings
225 | orleans.codegen.cs
226 |
227 | # Including strong name files can present a security risk
228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
229 | #*.snk
230 |
231 | # Since there are multiple workflows, uncomment next line to ignore bower_components
232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
233 | #bower_components/
234 |
235 | # RIA/Silverlight projects
236 | Generated_Code/
237 |
238 | # Backup & report files from converting an old project file
239 | # to a newer Visual Studio version. Backup files are not needed,
240 | # because we have git ;-)
241 | _UpgradeReport_Files/
242 | Backup*/
243 | UpgradeLog*.XML
244 | UpgradeLog*.htm
245 | ServiceFabricBackup/
246 | *.rptproj.bak
247 |
248 | # SQL Server files
249 | *.mdf
250 | *.ldf
251 | *.ndf
252 |
253 | # Business Intelligence projects
254 | *.rdl.data
255 | *.bim.layout
256 | *.bim_*.settings
257 | *.rptproj.rsuser
258 | *- Backup*.rdl
259 |
260 | # Microsoft Fakes
261 | FakesAssemblies/
262 |
263 | # GhostDoc plugin setting file
264 | *.GhostDoc.xml
265 |
266 | # Node.js Tools for Visual Studio
267 | .ntvs_analysis.dat
268 | node_modules/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush personal settings
299 | .cr/personal
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Tabs Studio
310 | *.tss
311 |
312 | # Telerik's JustMock configuration file
313 | *.jmconfig
314 |
315 | # BizTalk build output
316 | *.btp.cs
317 | *.btm.cs
318 | *.odx.cs
319 | *.xsd.cs
320 |
321 | # OpenCover UI analysis results
322 | OpenCover/
323 |
324 | # Azure Stream Analytics local run output
325 | ASALocalRun/
326 |
327 | # MSBuild Binary and Structured Log
328 | *.binlog
329 |
330 | # NVidia Nsight GPU debugger configuration file
331 | *.nvuser
332 |
333 | # MFractors (Xamarin productivity tool) working folder
334 | .mfractor/
335 |
336 | # Local History for Visual Studio
337 | .localhistory/
338 |
339 | # BeatPulse healthcheck temp database
340 | healthchecksdb
--------------------------------------------------------------------------------
/AspNetCore.Authentication.ApiToken.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30711.63
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7201702E-C795-4DA9-8427-0C8693D26181}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{89759E97-FE62-4E3A-9423-0E79ADACB7AF}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FB67236B-9E22-4A96-A785-DBA279775D14}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.ApiToken.UnitTests", "tests\AspNetCore.ApiToken.UnitTests\AspNetCore.ApiToken.UnitTests.csproj", "{D5237AEC-2F8F-488C-8B1D-C7B0662C3CF8}"
13 | EndProject
14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.Authentication.ApiToken", "src\AspNetCore.AuthenticationApiToken\AspNetCore.Authentication.ApiToken.csproj", "{99A8EC97-A5BD-4AF9-B186-82BE148AA53B}"
15 | EndProject
16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.Authentication.ApiToken.Redis", "src\AspNetCore.AuthenticationApiToken.Redis\AspNetCore.Authentication.ApiToken.Redis.csproj", "{633A4CD1-9A41-4514-AD0E-1C957024E919}"
17 | EndProject
18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.ApiToken.SampleApp", "sample\AspNetCore.ApiToken.SampleApp\AspNetCore.ApiToken.SampleApp.csproj", "{15A4AD86-7676-447F-AED3-46462AD93657}"
19 | EndProject
20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{05F612D3-C4F7-455F-ABBE-B2462AA1FB93}"
21 | ProjectSection(SolutionItems) = preProject
22 | .gitignore = .gitignore
23 | build\common.props = build\common.props
24 | README.md = README.md
25 | build\version.props = build\version.props
26 | EndProjectSection
27 | EndProject
28 | Global
29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
30 | Debug|Any CPU = Debug|Any CPU
31 | Release|Any CPU = Release|Any CPU
32 | EndGlobalSection
33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
34 | {D5237AEC-2F8F-488C-8B1D-C7B0662C3CF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {D5237AEC-2F8F-488C-8B1D-C7B0662C3CF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {D5237AEC-2F8F-488C-8B1D-C7B0662C3CF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {D5237AEC-2F8F-488C-8B1D-C7B0662C3CF8}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {99A8EC97-A5BD-4AF9-B186-82BE148AA53B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {99A8EC97-A5BD-4AF9-B186-82BE148AA53B}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {99A8EC97-A5BD-4AF9-B186-82BE148AA53B}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {99A8EC97-A5BD-4AF9-B186-82BE148AA53B}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {633A4CD1-9A41-4514-AD0E-1C957024E919}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43 | {633A4CD1-9A41-4514-AD0E-1C957024E919}.Debug|Any CPU.Build.0 = Debug|Any CPU
44 | {633A4CD1-9A41-4514-AD0E-1C957024E919}.Release|Any CPU.ActiveCfg = Release|Any CPU
45 | {633A4CD1-9A41-4514-AD0E-1C957024E919}.Release|Any CPU.Build.0 = Release|Any CPU
46 | {15A4AD86-7676-447F-AED3-46462AD93657}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47 | {15A4AD86-7676-447F-AED3-46462AD93657}.Debug|Any CPU.Build.0 = Debug|Any CPU
48 | {15A4AD86-7676-447F-AED3-46462AD93657}.Release|Any CPU.ActiveCfg = Release|Any CPU
49 | {15A4AD86-7676-447F-AED3-46462AD93657}.Release|Any CPU.Build.0 = Release|Any CPU
50 | EndGlobalSection
51 | GlobalSection(SolutionProperties) = preSolution
52 | HideSolutionNode = FALSE
53 | EndGlobalSection
54 | GlobalSection(NestedProjects) = preSolution
55 | {D5237AEC-2F8F-488C-8B1D-C7B0662C3CF8} = {FB67236B-9E22-4A96-A785-DBA279775D14}
56 | {99A8EC97-A5BD-4AF9-B186-82BE148AA53B} = {7201702E-C795-4DA9-8427-0C8693D26181}
57 | {633A4CD1-9A41-4514-AD0E-1C957024E919} = {7201702E-C795-4DA9-8427-0C8693D26181}
58 | {15A4AD86-7676-447F-AED3-46462AD93657} = {89759E97-FE62-4E3A-9423-0E79ADACB7AF}
59 | EndGlobalSection
60 | GlobalSection(ExtensibilityGlobals) = postSolution
61 | SolutionGuid = {FD33B376-A654-476E-A3AB-0843EE5719C1}
62 | EndGlobalSection
63 | EndGlobal
64 |
--------------------------------------------------------------------------------
/AspNetCore.Authentication.ApiToken.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | TTL
--------------------------------------------------------------------------------
/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 [yyyy] [name of copyright owner]
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 | # AspNetCore.Authentication.ApiToken
2 |
3 | English | [中文](README_zh-CN.md)
4 |
5 | [](https://www.nuget.org/packages/AspNetCore.Authentication.ApiToken/)
6 |
7 | AspNetCore.Authentication.ApiToken is an authentication component for ASP.NET Core, following the design specification of ASP.NET Core authentication framework. It is mainly used in the WebApi project to provide **issuance** and **verification** Token capabilities. The Token issued by this component is not a Json Web Token (JWT), which is similar to the Reference Token in IdentityServer4 and needs to be queried on the server to verify the validity. If there is a need for Reference Token in IdentityServer4 in your project, then IdentityServer4 is recommended for medium and large projects. If it is a small and medium-sized project, then you can consider AspNetCore.Authentication.ApiToken, which is more portable than IdentityServer4. Maintenance costs are lower. The advantage of this Token over JWT is that it can completely control the life cycle of the Token. The disadvantage is that to verify the Token, you need to query the storage every time to compare and verify (the performance can be improved by caching).
8 |
9 | ## Features
10 |
11 | - Simple access, only need to implement two interfaces
12 | - Integrated issuance, refresh, cancellation and verification of Token
13 | - Support caching, Redis is implemented by default, and other caches can be easily extended
14 | - Support regular cleaning of expired Token background tasks
15 | - Support to update the user claim (role) to take effect immediately without logging in again
16 | - Only one Token can be valid for the same user at the same time (if a new Token is issued, all old Tokens will become invalid)
17 | - Support smooth transition when refreshing Token, old Token will not be invalid immediately
18 | - Support authentication events
19 |
20 |
21 | ## Quick start
22 |
23 | ### 1.Install
24 |
25 | Install via Nuget in your WebApi project
26 |
27 | ````shell
28 | dotnet add package AspNetCore.Authentication.ApiToken
29 | ````
30 |
31 | ### 2.Implementation interface IApiTokenProfileService
32 |
33 | The main function of this interface is to query the user's Claims according to the user Id when **creating** and **refreshing** Tokens, such as commonly used: Name, Id, and Role.
34 |
35 | The Claims provided here can be accessed in the `HttpContext.User.Claims` property after **authentication is successful**. Role Claim can be used on `[Authorize]`, such as `[Authorize(Roles = "Admin")]`
36 |
37 | Example(Entity Framework core):
38 |
39 | MyApiTokenProfileService.cs
40 |
41 | ````csharp
42 | public class MyApiTokenProfileService : IApiTokenProfileService
43 | {
44 | private readonly EfDbContext _dbContext;
45 |
46 | public MyApiTokenProfileService(EfDbContext dbContext)
47 | {
48 | _dbContext = dbContext;
49 | }
50 | public async Task> GetUserClaimsAsync(string userId)
51 | {
52 | var user = await _dbContext.Users.FirstAsync(a => a.Id == userId);
53 | return new List()
54 | {
55 | new Claim(ApiTokenClaimTypes.Subject,userId),
56 | new Claim(ApiTokenClaimTypes.Name,user.Name),
57 | new Claim(ApiTokenClaimTypes.Role,user.Role),
58 | };
59 | }
60 | }
61 | ````
62 |
63 | ### 3.Implementation interface IApiTokenStore
64 |
65 | This interface is used to store, query, and delete tokens. Because the Token provided by this component needs to be checked and compared for validity verification.
66 |
67 | The example uses the database as a storage implementation (Entity Framework core):
68 |
69 | MqApiTokenStore.cs
70 | ````csharp
71 | public class MqApiTokenStore : IApiTokenStore
72 | {
73 | //Store token
74 | public async Task StoreAsync(TokenModel token)
75 | {
76 | //...
77 | }
78 |
79 | //Store Token list
80 | public async Task StoreAsync(List token)
81 | {
82 | //...
83 | }
84 |
85 | //Get token
86 | public async Task GetAsync(string token, string scheme)
87 | {
88 | //...
89 | }
90 |
91 | //Get the token list
92 | public async Task> GetListAsync(string userId, string scheme)
93 | {
94 | //...
95 | }
96 |
97 | //Get a list of tokens of the specified type
98 | public async Task> GetListAsync(string userId, string scheme, TokenType type)
99 | {
100 | //...
101 | }
102 |
103 | //Update token
104 | public async Task UpdateAsync(TokenModel token)
105 | {
106 | //...
107 | }
108 |
109 | //Update token list
110 | public async Task UpdateListAsync(List token)
111 | {
112 | //...
113 | }
114 |
115 | //Delete token
116 | public async Task RemoveAsync(string token, string scheme)
117 | {
118 | //...
119 | }
120 |
121 | //Delete list
122 | public async Task RemoveListAsync(string userId, string scheme)
123 | {
124 | //...
125 | }
126 |
127 | //Delete the Token list of the specified type
128 | public async Task RemoveListAsync(string userId, string scheme, TokenType type)
129 | {
130 | //...
131 | }
132 |
133 | //Remove expiration token
134 | public async Task RemoveExpirationAsync()
135 | {
136 | //...
137 | }
138 | }
139 | ````
140 |
141 | ### 4.Configuration
142 |
143 | Startup.cs
144 |
145 | ````csharp
146 | public void ConfigureServices(IServiceCollection services)
147 | {
148 | services.AddAuthentication(ApiTokenDefaults.AuthenticationScheme)
149 | .AddApiToken()
150 | .AddProfileService()
151 | .AddTokenStore();
152 | //Other services...
153 | }
154 | ````
155 |
156 | ### 5.Issue token
157 |
158 | You need to write an API for issuing tokens yourself.
159 |
160 | Inject `IApiTokenOperator tokenOperator`
161 |
162 | ````csharp
163 | var createResult = await tokenOperator.CreateAsync("");
164 | ````
165 |
166 | The returned result contains Bearer Token and Refresh Token. Bearer Token is used for interface verification, and Refresh Token is used for Token refresh.
167 |
168 | ### 6.Use Token
169 |
170 | Similar to the way of using JWT, add Header to the request
171 |
172 | ````
173 | Authorization: Bearer
174 | ````
175 |
176 | ### 7.Demo
177 |
178 | **Please refer to the complete implementation [SampleApp](./sample/AspNetCore.ApiToken.SampleApp/README.md)**
179 |
180 | 
181 |
182 | ## Advance
183 |
184 | ### 1.Use cache
185 |
186 | Install Nuget package:`AspNetCore.Authentication.ApiToken.Redis`
187 |
188 | Add service on Startup.ConfigureServices `AddRedisCache(op => op.ConnectionString = "")`
189 |
190 | Example:
191 |
192 | ```csharp
193 | services.AddAuthentication(ApiTokenDefaults.AuthenticationScheme)
194 | .AddApiToken(op => op.UseCache = false)
195 | .AddRedisCache(op => op.ConnectionString = "127.0.0.1:6379")
196 | .AddProfileService()
197 | .AddTokenStore();
198 | ```
199 |
200 | The cache validity period can be customized, generally the cache validity period is the same as the token expiration time.
201 |
202 | ### 2.Custom cache
203 |
204 | To implement the `IApiTokenCacheService` interface, please refer to the implementation of [Redis](src/AspNetCore.AuthenticationApiToken.Redis/RedisTokenCacheService.cs).
205 |
206 | ### 3.Clean Token Background Service
207 |
208 | Periodic cleaning service refers to running to clean up expired tokens in the database at regular intervals, adding `AddCleanService()` to the registration service
209 |
210 | Example:
211 |
212 | ````csharp
213 | services.AddAuthentication(ApiTokenDefaults.AuthenticationScheme)
214 | .AddApiToken(op => op.UseCache = false)
215 | .AddProfileService()
216 | .AddTokenStore()
217 | .AddCleanService();
218 | ````
219 |
220 | Can customize the interval time.
221 |
222 | ### 4.Refresh Token
223 |
224 | Inject `IApiTokenOperator` and call the `RefreshAsync(string refreshToken, string scheme)` method, it will automatically refresh and return the result.
225 |
226 | The `ApiTokenOptions.KeepTokenValidTimeSpanOnRefresh` property can be used to set how long the old Token can be valid after refreshing.
227 |
228 | ### 5.Update claim
229 |
230 | Inject `IApiTokenOperator` and call `RefreshClaimsAsync(string token, string scheme)` method. Mainly used for users to update information, such as name or role, if you do not need to login again, it will take effect immediately, you can call this method.
231 |
232 | ### 6.Revoke token
233 |
234 | Inject `IApiTokenOperator` and call `RemoveAsync(string token, string scheme)` method.
235 |
236 | ### Tips
237 |
238 | The scheme in the above method can not be passed, but it needs to be passed in when multiple ApiToken authentication services are registered, or the ApiToken authentication is not the default scheme. This is because of the design of the authentication framework of ASP.NET Core. If you need to know the details, you can see the official documentation of ASP.NET Core.
239 |
240 | ## Thanks
241 |
242 | The following items are referred to in the design and compilation of this project:
243 |
244 | - [aspnetcore-authentication-apikey](https://github.com/mihirdilip/aspnetcore-authentication-apikey)
245 | - [Microsoft.AspNetCore.Authentication.JwtBearer](https://github.com/dotnet/aspnetcore/tree/master/src/Security/Authentication/JwtBearer/src)
246 | - [IdentityServer4](https://github.com/identityserver/identityserver4)
247 |
248 |
249 |
250 |
--------------------------------------------------------------------------------
/README_zh-CN.md:
--------------------------------------------------------------------------------
1 | # AspNetCore.Authentication.ApiToken
2 |
3 | 中文 | [English](README.md)
4 |
5 | [](https://www.nuget.org/packages/AspNetCore.Authentication.ApiToken/)
6 |
7 | AspNetCore.Authentication.ApiToken 是一个用于 ASP.NET Core 的认证组件,遵循 ASP.NET Core 的认证框架设计规范。它主要用于 WebApi 项目,提供**签发**和**校验** Token 的能力。本组件签发的 Token 非 Json Web Token(JWT),类似于 IdentityServer4 中的 Reference Token,需要在服务端查询来进行有效性的验证。如果在你的项目中有 IdentityServer4 中的 Reference Token 需求,那么在中大型项目中推荐使用 IdentityServer4,如果是中小型项目,那么你可以考虑 AspNetCore.Authentication.ApiToken,它比 IdentityServer4 更加的轻便,接入和维护成本更低。此 Token 比 JWT 带来的优势是可以完全控制 Token 的生命周期,缺点是验证 Token 需要每次查询存储来比对验证(可以通过缓存来提升性能)。
8 |
9 | ## 功能
10 |
11 | - 接入简单,只需要实现两个接口
12 | - 一体化签发、刷新、注销和验证 Token
13 | - 支持缓存,默认已实现 Redis,可轻松扩展其它缓存
14 | - 支持定期清理过期 Token 后台任务
15 | - 支持更新用户 Claim (角色)立即生效,无需重新登录
16 | - 支持同一用户同一时间只能有一个 Token 生效(签发新Token,所有旧 Token 都会失效)
17 | - 支持刷新 Token 时平滑过渡,旧Token不会立即失效
18 | - 支持认证事件
19 |
20 |
21 | ## 快速入门
22 |
23 | ### 1.安装
24 |
25 | 在你的 WebApi 项目中通过 Nuget 安装
26 |
27 | ````shell
28 | dotnet add package AspNetCore.Authentication.ApiToken
29 | ````
30 |
31 | ### 2.实现接口 IApiTokenProfileService
32 |
33 | 此接口的主要功能为,在**创建**和**刷新** Token 时,根据用户Id查询用户的 Claims,如常用的:Name、Id、Role。
34 |
35 | 这里提供的 Claims,将可以在**认证成功**后的 `HttpContext.User.Claims` 属性中被访问。Role Claim 将可以用在 `[Authorize]`上,如 `[Authorize(Roles = "Admin")]`
36 |
37 | 示例(Entity Framework core):
38 |
39 | MyApiTokenProfileService.cs
40 |
41 | ````csharp
42 | public class MyApiTokenProfileService : IApiTokenProfileService
43 | {
44 | private readonly EfDbContext _dbContext;
45 |
46 | public MyApiTokenProfileService(EfDbContext dbContext)
47 | {
48 | _dbContext = dbContext;
49 | }
50 | public async Task> GetUserClaimsAsync(string userId)
51 | {
52 | var user = await _dbContext.Users.FirstAsync(a => a.Id == userId);
53 | return new List()
54 | {
55 | new Claim(ApiTokenClaimTypes.Subject,userId),
56 | new Claim(ApiTokenClaimTypes.Name,user.Name),
57 | new Claim(ApiTokenClaimTypes.Role,user.Role),
58 | };
59 | }
60 | }
61 | ````
62 |
63 | ### 3.实现接口 IApiTokenStore
64 |
65 | 此接口用于存储和查询、删除 Token。因为本组件提供的 Token 需要查询比对进行有效性验证。
66 |
67 | 示例以数据库作为存储实现(Entity Framework core):
68 |
69 | MqApiTokenStore.cs
70 | ````csharp
71 | public class MqApiTokenStore : IApiTokenStore
72 | {
73 | //存储Token
74 | public async Task StoreAsync(TokenModel token)
75 | {
76 | //...
77 | }
78 |
79 | //存储Token列表
80 | public async Task StoreAsync(List token)
81 | {
82 | //...
83 | }
84 |
85 | //获取Token
86 | public async Task GetAsync(string token, string scheme)
87 | {
88 | //...
89 | }
90 |
91 | //获取Token列表
92 | public async Task> GetListAsync(string userId, string scheme)
93 | {
94 | //...
95 | }
96 |
97 | //获取指定类型的Token列表
98 | public async Task> GetListAsync(string userId, string scheme, TokenType type)
99 | {
100 | //...
101 | }
102 |
103 | //更新
104 | public async Task UpdateAsync(TokenModel token)
105 | {
106 | //...
107 | }
108 |
109 | //更新列表
110 | public async Task UpdateListAsync(List token)
111 | {
112 | //...
113 | }
114 |
115 | //删除
116 | public async Task RemoveAsync(string token, string scheme)
117 | {
118 | //...
119 | }
120 |
121 | //删除列表
122 | public async Task RemoveListAsync(string userId, string scheme)
123 | {
124 | //...
125 | }
126 |
127 | //删除指定类型的Token列表
128 | public async Task RemoveListAsync(string userId, string scheme, TokenType type)
129 | {
130 | //...
131 | }
132 |
133 | //删除过期Token
134 | public async Task RemoveExpirationAsync()
135 | {
136 | //...
137 | }
138 | }
139 | ````
140 |
141 | ### 4.配置
142 |
143 | Startup.cs
144 |
145 | ````csharp
146 | public void ConfigureServices(IServiceCollection services)
147 | {
148 | services.AddAuthentication(ApiTokenDefaults.AuthenticationScheme)
149 | .AddApiToken()
150 | .AddProfileService()
151 | .AddTokenStore();
152 | //Other services...
153 | }
154 | ````
155 |
156 | ### 5.签发Token
157 |
158 | 你需要自己编写一个签发 Token 的 Api
159 |
160 | 注入 `IApiTokenOperator tokenOperator`
161 |
162 | 调用
163 |
164 | ````csharp
165 | var createResult = await tokenOperator.CreateAsync("<用户Id>");
166 | ````
167 |
168 | 返回的结果中包含了 Bearer Token 和 Refresh Token。Bearer Token 用于接口验证,Refresh Token 用于 Token 的刷新。
169 |
170 | ### 6.使用 Token
171 |
172 | 类似于JWT的使用方式,在请求中加入 Header
173 |
174 | ````
175 | Authorization: Bearer
176 | ````
177 |
178 | ### 7.Demo
179 |
180 | **完整的实现请参阅 [SampleApp](./sample/AspNetCore.ApiToken.SampleApp/README.md)**
181 |
182 | 
183 |
184 | ## 进阶
185 |
186 | ### 1.使用缓存
187 |
188 | 安装包:`AspNetCore.Authentication.ApiToken.Redis`
189 |
190 | 在注册服务中添加 `AddRedisCache(op => op.ConnectionString = "")`
191 |
192 | 示例:
193 |
194 | ```csharp
195 | services.AddAuthentication(ApiTokenDefaults.AuthenticationScheme)
196 | .AddApiToken(op => op.UseCache = true)
197 | .AddRedisCache(op => op.ConnectionString = "127.0.0.1:6379")
198 | .AddProfileService()
199 | .AddTokenStore();
200 | ```
201 |
202 | 可以自定义缓存有效期,一般缓存有效期与token过期时间相同
203 |
204 | ### 2.实现自定义缓存
205 |
206 | 实现 `IApiTokenCacheService` 接口,可以参考 [Redis](src/AspNetCore.AuthenticationApiToken.Redis/RedisTokenCacheService.cs) 的实现。
207 |
208 | ### 3.定期清理 Token 服务
209 |
210 | 定期清理服务指在固定的时间间隔运行清理数据库中已过期的Token,在注册服务中添加 `AddCleanService()`
211 |
212 | 示例:
213 |
214 | ````csharp
215 | services.AddAuthentication(ApiTokenDefaults.AuthenticationScheme)
216 | .AddApiToken(op => op.UseCache = false)
217 | .AddProfileService()
218 | .AddTokenStore()
219 | .AddCleanService();
220 | ````
221 |
222 | 可以自定义间隔时间
223 |
224 | ### 4.使用刷新token
225 |
226 | 注入 `IApiTokenOperator ` 并调用 `RefreshAsync(string refreshToken, string scheme)`方法即可,会自动刷新并返回结果。
227 |
228 | `ApiTokenOptions.KeepTokenValidTimeSpanOnRefresh` 属性可以设置刷新后,旧 Token 仍可以生效多久。
229 |
230 | ### 5.更新 claim
231 |
232 | 注入 `IApiTokenOperator ` 并调用 `RefreshClaimsAsync(string token, string scheme)` 方法即可。主要用于用户更新了资料,比如姓名或者角色,如果不需要重新登录,立即生效可以调用此方法。
233 |
234 | ### 6.注销token
235 |
236 | 注入 `IApiTokenOperator ` 并调用 `RemoveAsync(string token, string scheme)` 方法即可。
237 |
238 | ### 提示
239 |
240 | 以上方法中的 scheme 可不传,但是在注册了多个 ApiToken 认证服务,或者是 ApiToken 认证不是默认 scheme 的情况下,需要传入。这是因为 ASP.NET Core 的认证框架设计,需要了解详情的可以去看 ASP.NET Core官方文档。
241 |
242 | ## 感谢
243 |
244 | 本项目在设计和编写时参考了以下项目:
245 |
246 | - [aspnetcore-authentication-apikey](https://github.com/mihirdilip/aspnetcore-authentication-apikey)
247 | - [Microsoft.AspNetCore.Authentication.JwtBearer](https://github.com/dotnet/aspnetcore/tree/master/src/Security/Authentication/JwtBearer/src)
248 | - [IdentityServer4](https://github.com/identityserver/identityserver4)
249 |
250 |
251 |
252 |
--------------------------------------------------------------------------------
/assets/op.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stulzq/AspNetCore.Authentication.ApiToken/3eb55b4e890afffadd35015ce2c2ba4b68e35634/assets/op.gif
--------------------------------------------------------------------------------
/build/common.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Zhiqiang Li
7 | https://github.com/stulzq/AspNetCore.Authentication.ApiToken.git
8 | git
9 |
10 | https://github.com/stulzq/AspNetCore.Authentication.ApiToken
11 | LICENSE
12 | aspnetcore,authentication,apitoken
13 | true
14 | snupkg
15 |
16 |
17 |
18 | NU1605;NU1701
19 | NU1701;1701;1702;1705;1591;CS1591
20 |
21 |
22 |
23 |
24 |
25 |
26 | True
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/build/version.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0
4 | 3
5 | 2
6 |
7 | $(VersionMajor).$(VersionMinor).$(VersionPatch)
8 |
9 |
10 |
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/ApiTokenDbContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AspNetCore.ApiToken.SampleApp.Entities;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace AspNetCore.ApiToken.SampleApp
6 | {
7 | public class ApiTokenDbContext : DbContext
8 | {
9 | public ApiTokenDbContext(DbContextOptions options)
10 | : base(options)
11 | { }
12 |
13 | public DbSet Users { get; set; }
14 | public DbSet Token { get; set; }
15 |
16 | protected override void OnModelCreating(ModelBuilder modelBuilder)
17 | {
18 | modelBuilder.Entity().HasData(new User { Id = 1, Name = "Allen", Password = "123456", Role = "Admin" });
19 |
20 | base.OnModelCreating(modelBuilder);
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/AspNetCore.ApiToken.SampleApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp5.0
5 |
6 |
7 |
8 | NU1605;NU1701
9 | NU1701;1701;1702;1705;1591;CS1591
10 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
11 |
12 |
13 |
14 |
15 | all
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 |
18 |
19 | all
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Controllers/TokenController.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Threading.Tasks;
3 | using AspNetCore.Authentication.ApiToken;
4 | using AspNetCore.Authentication.ApiToken.Abstractions;
5 | using Microsoft.AspNetCore.Authorization;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.AspNetCore.Mvc;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace AspNetCore.ApiToken.SampleApp.Controllers
11 | {
12 | [ApiController]
13 | [Route("[controller]")]
14 | public class TokenController : ControllerBase
15 | {
16 | private readonly ILogger _logger;
17 | private readonly IApiTokenOperator _tokenOperator;
18 |
19 | public TokenController(ILogger logger, IApiTokenOperator tokenOperator)
20 | {
21 | _logger = logger;
22 | _tokenOperator = tokenOperator;
23 | }
24 |
25 | [Authorize(Roles = "Admin")]
26 | [HttpGet("[action]")]
27 | public async Task Validate()
28 | {
29 | return Ok(
30 | new
31 | {
32 | Token = await HttpContext.GetApiTokenAsync(),
33 | Id = HttpContext.User.Claims.First(a => a.Type == ApiTokenClaimTypes.Subject).Value,
34 | Name = HttpContext.User.Claims.First(a => a.Type == ApiTokenClaimTypes.Name).Value,
35 | Role = HttpContext.User.Claims.First(a => a.Type == ApiTokenClaimTypes.Role).Value,
36 | });
37 | }
38 |
39 |
40 | [HttpGet("[action]")]
41 | public async Task Create()
42 | {
43 | var createResult = await _tokenOperator.CreateAsync("1");
44 | return Ok(createResult);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Entities/ApiToken.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel.DataAnnotations;
3 | using System.ComponentModel.DataAnnotations.Schema;
4 |
5 | namespace AspNetCore.ApiToken.SampleApp.Entities
6 | {
7 | public class ApiToken
8 | {
9 | [Key]
10 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
11 | public int Id { get; set; }
12 |
13 | [Required]
14 | [StringLength(64)]
15 | public string Token { get; set; }
16 |
17 | [Required]
18 | [StringLength(50)]
19 | public string Scheme { get; set; }
20 |
21 | [Required]
22 | [StringLength(50)]
23 | public string Type { get; set; }
24 |
25 | [Required]
26 | public int UserId { get; set; }
27 |
28 | [Required]
29 | public string Claims { get; set; }
30 |
31 | [Required]
32 | public DateTime CreateTime { get; set; }
33 |
34 | [Required]
35 | public DateTime Expiration { get; set; }
36 | }
37 | }
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Entities/User.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.ComponentModel.DataAnnotations.Schema;
3 |
4 | namespace AspNetCore.ApiToken.SampleApp.Entities
5 | {
6 | public class User
7 | {
8 | [Key]
9 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
10 | public int Id { get; set; }
11 |
12 | [Required]
13 | [StringLength(50)]
14 | public string Password { get; set; }
15 |
16 | [Required]
17 | [StringLength(50)]
18 | public string Name { get; set; }
19 |
20 | [Required]
21 | [StringLength(50)]
22 | public string Role { get; set; }
23 | }
24 | }
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Migrations/20201229074003_init.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using AspNetCore.ApiToken.SampleApp;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.Infrastructure;
6 | using Microsoft.EntityFrameworkCore.Migrations;
7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
8 |
9 | namespace AspNetCore.ApiToken.SampleApp.Migrations
10 | {
11 | [DbContext(typeof(ApiTokenDbContext))]
12 | [Migration("20201229074003_init")]
13 | partial class init
14 | {
15 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
16 | {
17 | #pragma warning disable 612, 618
18 | modelBuilder
19 | .HasAnnotation("Relational:MaxIdentifierLength", 64)
20 | .HasAnnotation("ProductVersion", "5.0.1");
21 |
22 | modelBuilder.Entity("AspNetCore.ApiToken.SampleApp.Entities.ApiToken", b =>
23 | {
24 | b.Property("Id")
25 | .ValueGeneratedOnAdd()
26 | .HasColumnType("int");
27 |
28 | b.Property("Claims")
29 | .IsRequired()
30 | .HasColumnType("longtext");
31 |
32 | b.Property("CreateTime")
33 | .HasColumnType("datetime(6)");
34 |
35 | b.Property("Expiration")
36 | .HasColumnType("datetime(6)");
37 |
38 | b.Property("Scheme")
39 | .IsRequired()
40 | .HasMaxLength(50)
41 | .HasColumnType("varchar(50)");
42 |
43 | b.Property("Token")
44 | .IsRequired()
45 | .HasMaxLength(64)
46 | .HasColumnType("varchar(64)");
47 |
48 | b.Property("Type")
49 | .IsRequired()
50 | .HasMaxLength(50)
51 | .HasColumnType("varchar(50)");
52 |
53 | b.Property("UserId")
54 | .HasColumnType("int");
55 |
56 | b.HasKey("Id");
57 |
58 | b.ToTable("Token");
59 | });
60 |
61 | modelBuilder.Entity("AspNetCore.ApiToken.SampleApp.Entities.User", b =>
62 | {
63 | b.Property("Id")
64 | .ValueGeneratedOnAdd()
65 | .HasColumnType("int");
66 |
67 | b.Property("Name")
68 | .IsRequired()
69 | .HasMaxLength(50)
70 | .HasColumnType("varchar(50)");
71 |
72 | b.Property("Password")
73 | .IsRequired()
74 | .HasMaxLength(50)
75 | .HasColumnType("varchar(50)");
76 |
77 | b.Property("Role")
78 | .IsRequired()
79 | .HasMaxLength(50)
80 | .HasColumnType("varchar(50)");
81 |
82 | b.HasKey("Id");
83 |
84 | b.ToTable("Users");
85 |
86 | b.HasData(
87 | new
88 | {
89 | Id = 1,
90 | Name = "Allen",
91 | Password = "123456",
92 | Role = "Admin"
93 | });
94 | });
95 | #pragma warning restore 612, 618
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Migrations/20201229074003_init.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Metadata;
3 | using Microsoft.EntityFrameworkCore.Migrations;
4 |
5 | namespace AspNetCore.ApiToken.SampleApp.Migrations
6 | {
7 | public partial class init : Migration
8 | {
9 | protected override void Up(MigrationBuilder migrationBuilder)
10 | {
11 | migrationBuilder.CreateTable(
12 | name: "Token",
13 | columns: table => new
14 | {
15 | Id = table.Column(type: "int", nullable: false)
16 | .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
17 | Token = table.Column(type: "varchar(64)", maxLength: 64, nullable: false),
18 | Scheme = table.Column(type: "varchar(50)", maxLength: 50, nullable: false),
19 | Type = table.Column(type: "varchar(50)", maxLength: 50, nullable: false),
20 | UserId = table.Column(type: "int", nullable: false),
21 | Claims = table.Column(type: "longtext", nullable: false),
22 | CreateTime = table.Column(type: "datetime(6)", nullable: false),
23 | Expiration = table.Column(type: "datetime(6)", nullable: false)
24 | },
25 | constraints: table =>
26 | {
27 | table.PrimaryKey("PK_Token", x => x.Id);
28 | });
29 |
30 | migrationBuilder.CreateTable(
31 | name: "Users",
32 | columns: table => new
33 | {
34 | Id = table.Column(type: "int", nullable: false)
35 | .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
36 | Password = table.Column(type: "varchar(50)", maxLength: 50, nullable: false),
37 | Name = table.Column(type: "varchar(50)", maxLength: 50, nullable: false),
38 | Role = table.Column(type: "varchar(50)", maxLength: 50, nullable: false)
39 | },
40 | constraints: table =>
41 | {
42 | table.PrimaryKey("PK_Users", x => x.Id);
43 | });
44 |
45 | migrationBuilder.InsertData(
46 | table: "Users",
47 | columns: new[] { "Id", "Name", "Password", "Role" },
48 | values: new object[] { 1, "Allen", "123456", "Admin" });
49 | }
50 |
51 | protected override void Down(MigrationBuilder migrationBuilder)
52 | {
53 | migrationBuilder.DropTable(
54 | name: "Token");
55 |
56 | migrationBuilder.DropTable(
57 | name: "Users");
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Migrations/ApiTokenDbContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using AspNetCore.ApiToken.SampleApp;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.Infrastructure;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 |
8 | namespace AspNetCore.ApiToken.SampleApp.Migrations
9 | {
10 | [DbContext(typeof(ApiTokenDbContext))]
11 | partial class ApiTokenDbContextModelSnapshot : ModelSnapshot
12 | {
13 | protected override void BuildModel(ModelBuilder modelBuilder)
14 | {
15 | #pragma warning disable 612, 618
16 | modelBuilder
17 | .HasAnnotation("Relational:MaxIdentifierLength", 64)
18 | .HasAnnotation("ProductVersion", "5.0.1");
19 |
20 | modelBuilder.Entity("AspNetCore.ApiToken.SampleApp.Entities.ApiToken", b =>
21 | {
22 | b.Property("Id")
23 | .ValueGeneratedOnAdd()
24 | .HasColumnType("int");
25 |
26 | b.Property("Claims")
27 | .IsRequired()
28 | .HasColumnType("longtext");
29 |
30 | b.Property("CreateTime")
31 | .HasColumnType("datetime(6)");
32 |
33 | b.Property("Expiration")
34 | .HasColumnType("datetime(6)");
35 |
36 | b.Property("Scheme")
37 | .IsRequired()
38 | .HasMaxLength(50)
39 | .HasColumnType("varchar(50)");
40 |
41 | b.Property("Token")
42 | .IsRequired()
43 | .HasMaxLength(64)
44 | .HasColumnType("varchar(64)");
45 |
46 | b.Property("Type")
47 | .IsRequired()
48 | .HasMaxLength(50)
49 | .HasColumnType("varchar(50)");
50 |
51 | b.Property("UserId")
52 | .HasColumnType("int");
53 |
54 | b.HasKey("Id");
55 |
56 | b.ToTable("Token");
57 | });
58 |
59 | modelBuilder.Entity("AspNetCore.ApiToken.SampleApp.Entities.User", b =>
60 | {
61 | b.Property("Id")
62 | .ValueGeneratedOnAdd()
63 | .HasColumnType("int");
64 |
65 | b.Property("Name")
66 | .IsRequired()
67 | .HasMaxLength(50)
68 | .HasColumnType("varchar(50)");
69 |
70 | b.Property("Password")
71 | .IsRequired()
72 | .HasMaxLength(50)
73 | .HasColumnType("varchar(50)");
74 |
75 | b.Property("Role")
76 | .IsRequired()
77 | .HasMaxLength(50)
78 | .HasColumnType("varchar(50)");
79 |
80 | b.HasKey("Id");
81 |
82 | b.ToTable("Users");
83 |
84 | b.HasData(
85 | new
86 | {
87 | Id = 1,
88 | Name = "Allen",
89 | Password = "123456",
90 | Role = "Admin"
91 | });
92 | });
93 | #pragma warning restore 612, 618
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Hosting;
2 | using Microsoft.Extensions.Hosting;
3 |
4 | namespace AspNetCore.ApiToken.SampleApp
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | CreateHostBuilder(args).Build().Run();
11 | }
12 |
13 | public static IHostBuilder CreateHostBuilder(string[] args) =>
14 | Host.CreateDefaultBuilder(args)
15 | .ConfigureWebHostDefaults(webBuilder =>
16 | {
17 | webBuilder.UseStartup();
18 | });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:5000",
8 | "sslPort": 0
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "token",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "AspNetCore.ApiToken.SampleApp": {
21 | "commandName": "Project",
22 | "launchBrowser": true,
23 | "launchUrl": "swagger",
24 | "applicationUrl": "http://localhost:5000",
25 | "environmentVariables": {
26 | "ASPNETCORE_ENVIRONMENT": "Development"
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/README.md:
--------------------------------------------------------------------------------
1 | # Run steps
2 |
3 | 1.Set **MySql** database connection string
4 |
5 | appsettings.Development.json
6 |
7 | ````json
8 | "ConnectionStrings": {
9 | "DefaultConnection": ""
10 | }
11 | ````
12 |
13 | Tips: If your database is not MySQL,you can do this by following these steps:
14 |
15 | (1) Delete `Migrations` folder
16 |
17 | (2) Remove nuget package `Pomelo.EntityFrameworkCore.MySql`
18 |
19 | (3) Install new EF database driver package from nuget
20 |
21 | (4) Update registration EF Code in `Startup.cs`
22 |
23 | (5) Add Migrations
24 |
25 |
26 | ````shell
27 | Add-Migration init
28 | ````
29 |
30 | Termnial:
31 |
32 | ````shell
33 | dotnet ef migrations add init
34 | ````
35 |
36 | 2.Execute EntityFramewok Code Migrations
37 |
38 | Package Manager Console:
39 |
40 | ````shell
41 | Update-Database
42 | ````
43 |
44 | Termnial:
45 |
46 | ````shell
47 | dotnet ef database update
48 | ````
49 |
50 | 3.Run
51 |
52 | 
53 |
54 | Gif:
55 |
56 | 
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using AspNetCore.ApiToken.SampleApp.Store;
4 | using AspNetCore.Authentication.ApiToken;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.EntityFrameworkCore;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.DependencyInjection;
10 | using Microsoft.Extensions.Hosting;
11 | using Microsoft.OpenApi.Models;
12 | using Newtonsoft.Json;
13 | using Newtonsoft.Json.Serialization;
14 | using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
15 |
16 | namespace AspNetCore.ApiToken.SampleApp
17 | {
18 | public class Startup
19 | {
20 | public Startup(IConfiguration configuration)
21 | {
22 | Configuration = configuration;
23 | }
24 |
25 | public IConfiguration Configuration { get; }
26 |
27 | // This method gets called by the runtime. Use this method to add services to the container.
28 | public void ConfigureServices(IServiceCollection services)
29 | {
30 | services.AddAuthentication(ApiTokenDefaults.AuthenticationScheme)
31 | .AddApiToken(op => op.UseCache = false)
32 | // .AddRedisCache(op => op.ConnectionString = "192.168.3.57:6379")
33 | .AddProfileService()
34 | .AddTokenStore()
35 | .AddCleanService();
36 | // .AddRedisCache(op=>op.ConnectionString="xxx");
37 |
38 | services.AddControllers().AddNewtonsoftJson(op =>
39 | {
40 | op.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Local;
41 | op.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm";
42 | op.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
43 | });
44 |
45 | //Swagger
46 | services.AddSwaggerGen(op =>
47 | {
48 | op.UseInlineDefinitionsForEnums();
49 | op.SwaggerDoc("v1",
50 | new OpenApiInfo { Title = typeof(Startup).Namespace, Version = "v1" });
51 | op.DocInclusionPredicate((docName, description) => true);
52 |
53 | op.AddSecurityDefinition("ApiToken", new OpenApiSecurityScheme
54 | {
55 | Description = "Input Bearer {token}\"",
56 | Name = "Authorization",
57 | In = ParameterLocation.Header,
58 | Type = SecuritySchemeType.ApiKey,
59 | Scheme = "ApiToken"
60 | });
61 | op.AddSecurityRequirement(new OpenApiSecurityRequirement
62 | {
63 | {
64 | new OpenApiSecurityScheme
65 | {
66 | Reference = new OpenApiReference
67 | {
68 | Type = ReferenceType.SecurityScheme,
69 | Id = "ApiToken"
70 | }
71 | },
72 | new string[] { }
73 | }
74 | });
75 |
76 | });
77 | services.AddSwaggerGenNewtonsoftSupport();
78 |
79 | services.AddDbContext(options => options.UseMySql(
80 | Configuration.GetConnectionString("DefaultConnection"),
81 | ServerVersion.FromString("5.7-mysql"),
82 | mySqlOptions => mySqlOptions
83 | .CharSetBehavior(CharSetBehavior.NeverAppend)));
84 | }
85 |
86 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
87 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
88 | {
89 | if (env.IsDevelopment())
90 | {
91 | app.UseDeveloperExceptionPage();
92 |
93 | app.UseSwagger();
94 | app.UseSwaggerUI(op =>
95 | {
96 | op.SwaggerEndpoint($"/swagger/v1/swagger.json",
97 | $"{typeof(Startup).Namespace} v1");
98 | });
99 | }
100 |
101 | app.UseRouting();
102 |
103 | app.UseAuthentication();
104 | app.UseAuthorization();
105 |
106 | app.UseEndpoints(endpoints =>
107 | {
108 | endpoints.MapControllers();
109 | });
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Store/ClaimConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Security.Claims;
3 | using Newtonsoft.Json;
4 |
5 | namespace AspNetCore.ApiToken.SampleApp.Store
6 | {
7 | ///
8 | /// From https://github.com/IdentityServer/IdentityServer4
9 | ///
10 | public class ClaimConverter : JsonConverter
11 | {
12 | public override bool CanConvert(Type objectType)
13 | {
14 | return typeof(Claim) == objectType;
15 | }
16 |
17 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
18 | {
19 | var source = serializer.Deserialize(reader);
20 | var target = new Claim(source.Type, source.Value, source.ValueType);
21 | return target;
22 | }
23 |
24 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
25 | {
26 | var source = (Claim)value;
27 |
28 | var target = new ClaimLite
29 | {
30 | Type = source.Type,
31 | Value = source.Value,
32 | ValueType = source.ValueType
33 | };
34 |
35 | serializer.Serialize(writer, target);
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Store/ClaimLite.cs:
--------------------------------------------------------------------------------
1 | namespace AspNetCore.ApiToken.SampleApp.Store
2 | {
3 | ///
4 | /// From https://github.com/IdentityServer/IdentityServer4
5 | ///
6 | public class ClaimLite
7 | {
8 | public string Type { get; set; }
9 | public string Value { get; set; }
10 | public string ValueType { get; set; }
11 |
12 | }
13 | }
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Store/MyApiTokenProfileService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Security.Claims;
3 | using System.Threading.Tasks;
4 | using AspNetCore.Authentication.ApiToken;
5 | using AspNetCore.Authentication.ApiToken.Abstractions;
6 | using Microsoft.EntityFrameworkCore;
7 |
8 | namespace AspNetCore.ApiToken.SampleApp.Store
9 | {
10 | public class MyApiTokenProfileService : IApiTokenProfileService
11 | {
12 | private readonly ApiTokenDbContext _dbContext;
13 |
14 | public MyApiTokenProfileService(ApiTokenDbContext dbContext)
15 | {
16 | _dbContext = dbContext;
17 | }
18 |
19 | public async Task> GetUserClaimsAsync(string userId)
20 | {
21 | var realUserId = int.Parse(userId);
22 | var user = await _dbContext.Users.FirstAsync(a => a.Id == realUserId);
23 | return new List()
24 | {
25 | new Claim(ApiTokenClaimTypes.Subject,userId),
26 | new Claim(ApiTokenClaimTypes.Name,user.Name),
27 | new Claim(ApiTokenClaimTypes.Role,user.Role),
28 | };
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/Store/MyApiTokenStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Security.Claims;
5 | using System.Threading.Tasks;
6 | using AspNetCore.Authentication.ApiToken;
7 | using AspNetCore.Authentication.ApiToken.Abstractions;
8 | using Microsoft.EntityFrameworkCore;
9 | using Newtonsoft.Json;
10 |
11 | namespace AspNetCore.ApiToken.SampleApp.Store
12 | {
13 | public class MyApiTokenStore : IApiTokenStore
14 | {
15 | private readonly ApiTokenDbContext _dbContext;
16 |
17 | public MyApiTokenStore(ApiTokenDbContext dbContext)
18 | {
19 | _dbContext = dbContext;
20 | }
21 | public async Task StoreAsync(TokenModel token)
22 | {
23 | var entity = ConvertToApiToken(token);
24 | await _dbContext.Token.AddAsync(entity);
25 | await _dbContext.SaveChangesAsync();
26 | }
27 |
28 | public async Task StoreAsync(List token)
29 | {
30 | var entities = token.Select(ConvertToApiToken).ToList();
31 | await _dbContext.Token.AddRangeAsync(entities);
32 | await _dbContext.SaveChangesAsync();
33 | }
34 |
35 | public async Task GetAsync(string token, string scheme)
36 | {
37 | var entity = await _dbContext.Token.Where(a => a.Token == token && a.Scheme == scheme).FirstOrDefaultAsync();
38 | return entity == null ? null : ConvertToTokenModel(entity);
39 | }
40 |
41 | public async Task> GetListAsync(string userId, string scheme)
42 | {
43 | var queryResult = await _dbContext.Token.Where(a => a.UserId == int.Parse(userId) && a.Scheme == scheme).ToListAsync();
44 | var result = queryResult.Select(ConvertToTokenModel).ToList();
45 | return result;
46 | }
47 |
48 | public async Task> GetListAsync(string userId, string scheme, TokenType type)
49 | {
50 | var queryResult = await _dbContext.Token
51 | .Where(a => a.UserId == int.Parse(userId) && a.Scheme == scheme)
52 | .Where(a => a.Token == type.ToString())
53 | .ToListAsync();
54 | var result = queryResult.Select(ConvertToTokenModel).ToList();
55 | return result;
56 | }
57 |
58 | public async Task UpdateAsync(TokenModel token)
59 | {
60 | _dbContext.Token.Update(ConvertToApiToken(token));
61 | await _dbContext.SaveChangesAsync();
62 | }
63 |
64 | public async Task UpdateListAsync(List token)
65 | {
66 | _dbContext.Token.UpdateRange(token.Select(ConvertToApiToken));
67 | await _dbContext.SaveChangesAsync();
68 | }
69 |
70 | public async Task RemoveAsync(string token, string scheme)
71 | {
72 | var tokenEntity = await _dbContext.Token.Where(a => a.Token == token).FirstOrDefaultAsync();
73 | if (tokenEntity != null)
74 | {
75 | _dbContext.Token.Remove(tokenEntity);
76 | await _dbContext.SaveChangesAsync();
77 | }
78 | }
79 |
80 | public async Task RemoveListAsync(string userId, string scheme)
81 | {
82 | var tokenList = await _dbContext.Token
83 | .Where(a => a.UserId == int.Parse(userId) && a.Scheme == scheme)
84 | .ToListAsync();
85 | if (tokenList.Any())
86 | {
87 | _dbContext.Token.RemoveRange(tokenList);
88 | await _dbContext.SaveChangesAsync();
89 | }
90 | }
91 |
92 | public async Task RemoveListAsync(string userId, string scheme, TokenType type)
93 | {
94 | var tokenList = await _dbContext.Token
95 | .Where(a => a.UserId == int.Parse(userId) && a.Scheme == scheme)
96 | .Where(a => a.Type == type.ToString())
97 | .ToListAsync();
98 | if (tokenList.Any())
99 | {
100 | _dbContext.Token.RemoveRange(tokenList);
101 | await _dbContext.SaveChangesAsync();
102 | }
103 | }
104 |
105 | public async Task RemoveExpirationAsync()
106 | {
107 | var tokens = _dbContext.Token.Where(a => a.Expiration < DateTime.Now);
108 | var count = await tokens.CountAsync();
109 | _dbContext.Token.RemoveRange(tokens);
110 | await _dbContext.SaveChangesAsync();
111 | return count;
112 | }
113 |
114 | private TokenModel ConvertToTokenModel(Entities.ApiToken apiToken)
115 | {
116 | var result = new TokenModel()
117 | {
118 | CreateTime = apiToken.CreateTime,
119 | Expiration = apiToken.Expiration,
120 | Type = Enum.Parse(apiToken.Type),
121 | UserId = apiToken.UserId.ToString(),
122 | Value = apiToken.Token,
123 | Scheme = apiToken.Scheme,
124 | Claims = JsonConvert.DeserializeObject>(apiToken.Claims,new ClaimConverter())
125 | };
126 |
127 | return result;
128 |
129 | }
130 |
131 | private Entities.ApiToken ConvertToApiToken(TokenModel tokenModel)
132 | {
133 | var result = new Entities.ApiToken()
134 | {
135 | CreateTime = tokenModel.CreateTime.DateTime,
136 | Expiration = tokenModel.Expiration.DateTime,
137 | Type = tokenModel.Type.ToString(),
138 | UserId = int.Parse(tokenModel.UserId),
139 | Token = tokenModel.Value,
140 | Scheme = tokenModel.Scheme
141 | };
142 |
143 | if (tokenModel.Claims != null)
144 | {
145 | result.Claims = JsonConvert.SerializeObject(tokenModel.Claims, new ClaimConverter());
146 | }
147 | else
148 | {
149 | result.Claims = "[]";
150 | }
151 |
152 | return result;
153 | }
154 | }
155 | }
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "ConnectionStrings": {
10 | "DefaultConnection": "Server=localhost;port=3306;database=ApiTokenSample;uid=root;pwd=123123;"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/sample/AspNetCore.ApiToken.SampleApp/assets/swagger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stulzq/AspNetCore.Authentication.ApiToken/3eb55b4e890afffadd35015ce2c2ba4b68e35634/sample/AspNetCore.ApiToken.SampleApp/assets/swagger.png
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken.Redis/AspNetCore.Authentication.ApiToken.Redis.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1;net5.0;
5 | $(LibraryFrameworks)
6 | disable
7 | Api Token Redis Cache
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken.Redis/RedisTokenCacheExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AspNetCore.Authentication.ApiToken.Redis;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.DependencyInjection.Extensions;
5 | using Microsoft.Extensions.Options;
6 |
7 | // ReSharper disable once CheckNamespace
8 | namespace AspNetCore.Authentication.ApiToken
9 | {
10 | public static class RedisTokenCacheExtensions
11 | {
12 | public static ApiTokenAuthenticationBuilder AddRedisCache(this ApiTokenAuthenticationBuilder builder, Action configureOptions)
13 | {
14 | builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, RedisTokenCachePostConfigureOptions>());
15 | builder.AddCache(configureOptions);
16 |
17 | return builder;
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken.Redis/RedisTokenCacheOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace AspNetCore.Authentication.ApiToken.Redis
4 | {
5 | public class RedisTokenCacheOptions : ApiTokenCacheOptions
6 | {
7 |
8 | }
9 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken.Redis/RedisTokenCachePostConfigureOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.Options;
3 |
4 | namespace AspNetCore.Authentication.ApiToken.Redis
5 | {
6 | ///
7 | /// Used to setup defaults for all .
8 | ///
9 | public class RedisTokenCachePostConfigureOptions : IPostConfigureOptions
10 | {
11 | public void PostConfigure(string name, RedisTokenCacheOptions options)
12 | {
13 | if (string.IsNullOrEmpty(options.ConnectionString))
14 | {
15 | throw new InvalidOperationException($"{nameof(RedisTokenCacheOptions.ConnectionString)} must be not null.");
16 | }
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken.Redis/RedisTokenCacheService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using AspNetCore.Authentication.ApiToken.Abstractions;
4 | using MessagePack;
5 | using MessagePack.Resolvers;
6 | using Microsoft.Extensions.Logging;
7 | using Microsoft.Extensions.Options;
8 | using StackExchange.Redis;
9 |
10 | namespace AspNetCore.Authentication.ApiToken.Redis
11 | {
12 | public class RedisTokenCacheService : IApiTokenCacheService, IAsyncDisposable
13 | {
14 | private readonly ILogger _logger;
15 | private readonly RedisTokenCacheOptions _options;
16 | private IDatabase _cache;
17 | private ConnectionMultiplexer _connection;
18 | private readonly string _tokenCacheKeyPrefix;
19 | public RedisTokenCacheService(IOptions options, ILogger logger)
20 | {
21 | _logger = logger;
22 | _options = options.Value;
23 | _tokenCacheKeyPrefix = _options.CachePrefix + ":{0}:token:{1}";
24 | }
25 |
26 | public async Task InitializeAsync()
27 | {
28 | _connection = await ConnectionMultiplexer.ConnectAsync(_options.ConnectionString);
29 | _cache = _connection.GetDatabase();
30 |
31 | _logger.LogInformation("Redis token cache service init successful.");
32 | }
33 |
34 | private string GetKey(string token, string scheme)
35 | {
36 | return string.Format(_tokenCacheKeyPrefix, scheme, token);
37 | }
38 |
39 | public async Task GetAsync(string token, string scheme)
40 | {
41 | var key = GetKey(token, scheme);
42 | var cacheData = await _cache.StringGetAsync(key);
43 | if (!cacheData.HasValue)
44 | {
45 | return default;
46 | }
47 |
48 | return MessagePackSerializer.Deserialize(cacheData, ContractlessStandardResolver.Options);
49 | }
50 |
51 | public async Task SetAsync(TokenModel token)
52 | {
53 | var key = GetKey(token.Scheme, token.Value);
54 |
55 | await _cache.StringSetAsync(key, Serialize(new TokenModelCache() { Token = token }), token.LifeTime);
56 | }
57 |
58 | public async Task SetNullAsync(string invalidToken, string scheme)
59 | {
60 | var key = GetKey(scheme, invalidToken);
61 | if (_options.InvalidTokenNullCacheTimeSpan != null)
62 | {
63 | var ttl = _options.InvalidTokenNullCacheTimeSpan.Value;
64 | await _cache.StringSetAsync(key, Serialize(new TokenModelCache()), ttl);
65 | }
66 | }
67 |
68 | public async Task RemoveAsync(string token, string scheme)
69 | {
70 | var key = GetKey(scheme, token);
71 |
72 | await _cache.KeyDeleteAsync(key);
73 | }
74 |
75 | public async Task LockTakeAsync(string key, string value, TimeSpan timeOut)
76 | {
77 | var lockKey = string.Format(_tokenCacheKeyPrefix, key);
78 | return await _cache.LockTakeAsync(lockKey, value, timeOut);
79 | }
80 |
81 | public async Task LockReleaseAsync(string key, string value)
82 | {
83 | var lockKey = string.Format(_tokenCacheKeyPrefix, key);
84 | var result = await _cache.LockReleaseAsync(lockKey, value);
85 | if (!result)
86 | {
87 | _logger.LogError($"Lock release failed, Key: {lockKey}, Value: {value}");
88 | }
89 | }
90 |
91 | private static byte[] Serialize(TokenModelCache data)
92 | {
93 | return MessagePackSerializer.Serialize(data, ContractlessStandardResolver.Options);
94 | }
95 |
96 | public async ValueTask DisposeAsync()
97 | {
98 | if (_connection != null)
99 | {
100 | await _connection.CloseAsync();
101 | }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/Abstractions/IApiTokenCacheService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 |
4 | namespace AspNetCore.Authentication.ApiToken.Abstractions
5 | {
6 | public interface IApiTokenCacheService
7 | {
8 | Task InitializeAsync();
9 |
10 | Task GetAsync(string token, string scheme);
11 |
12 | Task SetAsync(TokenModel token);
13 |
14 | Task SetNullAsync(string invalidToken, string scheme);
15 |
16 | Task RemoveAsync(string token, string scheme);
17 |
18 | Task LockTakeAsync(string key, string value, TimeSpan timeOut);
19 |
20 | Task LockReleaseAsync(string key, string value);
21 | }
22 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/Abstractions/IApiTokenOperator .cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using AspNetCore.Authentication.ApiToken.Results;
3 |
4 | namespace AspNetCore.Authentication.ApiToken.Abstractions
5 | {
6 | public interface IApiTokenOperator
7 | {
8 | Task CreateAsync(string userId, string scheme = null);
9 |
10 | Task RefreshAsync(string refreshToken, string scheme = null);
11 |
12 | Task RefreshClaimsAsync(string token, string scheme = null);
13 |
14 | Task RemoveAsync(string token, string scheme = null);
15 | }
16 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/Abstractions/IApiTokenProfileService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Security.Claims;
3 | using System.Threading.Tasks;
4 |
5 | namespace AspNetCore.Authentication.ApiToken.Abstractions
6 | {
7 | public interface IApiTokenProfileService
8 | {
9 | Task> GetUserClaimsAsync(string userId);
10 | }
11 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/Abstractions/IApiTokenStore.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 |
4 | namespace AspNetCore.Authentication.ApiToken.Abstractions
5 | {
6 | public interface IApiTokenStore
7 | {
8 | Task StoreAsync(TokenModel token);
9 |
10 | Task StoreAsync(List token);
11 |
12 | Task GetAsync(string token, string scheme);
13 |
14 | Task> GetListAsync(string userId, string scheme);
15 |
16 | Task> GetListAsync(string userId, string scheme, TokenType type);
17 |
18 | Task UpdateAsync(TokenModel token);
19 |
20 | Task UpdateListAsync(List token);
21 |
22 | Task RemoveAsync(string token, string scheme);
23 |
24 | Task RemoveListAsync(string userId, string scheme);
25 |
26 | Task RemoveListAsync(string userId, string scheme, TokenType type);
27 |
28 | Task RemoveExpirationAsync();
29 | }
30 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/Abstractions/IApiTokenValidator.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Claims;
2 | using System.Threading.Tasks;
3 |
4 | namespace AspNetCore.Authentication.ApiToken.Abstractions
5 | {
6 | public interface IApiTokenValidator
7 | {
8 | Task ValidateTokenAsync(ApiTokenOptions options, string token, string schemeName);
9 | }
10 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/ApiTokenAuthenticationBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AspNetCore.Authentication.ApiToken.Abstractions;
3 | using Microsoft.AspNetCore.Authentication;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.DependencyInjection.Extensions;
6 |
7 | namespace AspNetCore.Authentication.ApiToken
8 | {
9 | public class ApiTokenAuthenticationBuilder
10 | {
11 | public IServiceCollection Services { get; }
12 |
13 | public ApiTokenAuthenticationBuilder(AuthenticationBuilder builder)
14 | {
15 | Services = builder.Services;
16 | }
17 |
18 | public ApiTokenAuthenticationBuilder AddProfileService() where TProfileService : class
19 | {
20 | Services.AddTransient(typeof(IApiTokenProfileService), typeof(TProfileService));
21 | return this;
22 | }
23 |
24 | public ApiTokenAuthenticationBuilder AddTokenStore() where TTokenStore : class
25 | {
26 | Services.AddScoped(typeof(IApiTokenStore), typeof(TTokenStore));
27 | return this;
28 | }
29 |
30 | public ApiTokenAuthenticationBuilder AddCache(Action configureOptions)
31 | where TCacheService : IApiTokenCacheService
32 | where TCacheOptions : ApiTokenCacheOptions
33 | {
34 | if (configureOptions == null)
35 | {
36 | throw new ArgumentNullException(nameof(configureOptions));
37 | }
38 |
39 | Services.Configure(configureOptions);
40 | Services.Replace(ServiceDescriptor.Singleton(typeof(IApiTokenCacheService), typeof(TCacheService)));
41 | return this;
42 | }
43 |
44 | public ApiTokenAuthenticationBuilder AddCleanService() => AddCleanService(_ => { });
45 |
46 | public ApiTokenAuthenticationBuilder AddCleanService(Action configureOptions)
47 | {
48 | if (configureOptions == null)
49 | {
50 | throw new ArgumentNullException(nameof(configureOptions));
51 | }
52 | Services.Configure(configureOptions);
53 | Services.AddHostedService();
54 | return this;
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/ApiTokenCacheOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace AspNetCore.Authentication.ApiToken
4 | {
5 | public class ApiTokenCacheOptions
6 | {
7 | ///
8 | /// Redis connection string. Help: https://stackexchange.github.io/StackExchange.Redis/Configuration.html
9 | ///
10 | public string ConnectionString { get; set; }
11 |
12 | ///
13 | /// Set an empty cache for invalid token to avoid penetrating the database
14 | ///
15 | /// * If set value to null, it will not take effect
16 | ///
17 | public TimeSpan? InvalidTokenNullCacheTimeSpan { get; set; } = TimeSpan.FromMinutes(10);
18 |
19 | ///
20 | /// Cache key prefix in redis.
21 | ///
22 | public string CachePrefix { get; set; } = "aspnetcore:authentication";
23 |
24 | }
25 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/ApiTokenClaimTypes.cs:
--------------------------------------------------------------------------------
1 | namespace AspNetCore.Authentication.ApiToken
2 | {
3 | ///
4 | /// From JwtClaimTypes https://github.com/IdentityModel/IdentityModel/blob/main/src/JwtClaimTypes.cs
5 | ///
6 | public class ApiTokenClaimTypes
7 | {
8 | /// Unique Identifier for the End-User at the Issuer.
9 | public const string Subject = "sub";
10 |
11 | /// End-User's full name in displayable form including all name parts, possibly including titles and suffixes, ordered according to the End-User's locale and preferences.
12 | public const string Name = "name";
13 |
14 | /// Given name(s) or first name(s) of the End-User. Note that in some cultures, people can have multiple given names; all can be present, with the names being separated by space characters.
15 | public const string GivenName = "given_name";
16 |
17 | /// Surname(s) or last name(s) of the End-User. Note that in some cultures, people can have multiple family names or no family name; all can be present, with the names being separated by space characters.
18 | public const string FamilyName = "family_name";
19 |
20 | /// Middle name(s) of the End-User. Note that in some cultures, people can have multiple middle names; all can be present, with the names being separated by space characters. Also note that in some cultures, middle names are not used.
21 | public const string MiddleName = "middle_name";
22 |
23 | /// Casual name of the End-User that may or may not be the same as the given_name. For instance, a nickname value of Mike might be returned alongside a given_name value of Michael.
24 | public const string NickName = "nickname";
25 |
26 | /// Shorthand name by which the End-User wishes to be referred to at the RP, such as janedoe or j.doe. This value MAY be any valid JSON string including special characters such as @, /, or whitespace. The relying party MUST NOT rely upon this value being unique
27 | /// The RP MUST NOT rely upon this value being unique, as discussed in http://openid.net/specs/openid-connect-basic-1_0-32.html#ClaimStability
28 | public const string PreferredUserName = "preferred_username";
29 |
30 | /// URL of the End-User's profile page. The contents of this Web page SHOULD be about the End-User.
31 | public const string Profile = "profile";
32 |
33 | /// URL of the End-User's profile picture. This URL MUST refer to an image file (for example, a PNG, JPEG, or GIF image file), rather than to a Web page containing an image.
34 | /// Note that this URL SHOULD specifically reference a profile photo of the End-User suitable for displaying when describing the End-User, rather than an arbitrary photo taken by the End-User.
35 | public const string Picture = "picture";
36 |
37 | /// URL of the End-User's Web page or blog. This Web page SHOULD contain information published by the End-User or an organization that the End-User is affiliated with.
38 | public const string WebSite = "website";
39 |
40 | /// End-User's preferred e-mail address. Its value MUST conform to the RFC 5322 [RFC5322] addr-spec syntax. The relying party MUST NOT rely upon this value being unique
41 | public const string Email = "email";
42 |
43 | /// "true" if the End-User's e-mail address has been verified; otherwise "false".
44 | /// When this Claim Value is "true", this means that the OP took affirmative steps to ensure that this e-mail address was controlled by the End-User at the time the verification was performed. The means by which an e-mail address is verified is context-specific, and dependent upon the trust framework or contractual agreements within which the parties are operating.
45 | public const string EmailVerified = "email_verified";
46 |
47 | /// End-User's gender. Values defined by this specification are "female" and "male". Other values MAY be used when neither of the defined values are applicable.
48 | public const string Gender = "gender";
49 |
50 | /// End-User's birthday, represented as an ISO 8601:2004 [ISO8601‑2004] YYYY-MM-DD format. The year MAY be 0000, indicating that it is omitted. To represent only the year, YYYY format is allowed. Note that depending on the underlying platform's date related function, providing just year can result in varying month and day, so the implementers need to take this factor into account to correctly process the dates.
51 | public const string BirthDate = "birthdate";
52 |
53 | /// String from the time zone database (http://www.twinsun.com/tz/tz-link.htm) representing the End-User's time zone. For example, Europe/Paris or America/Los_Angeles.
54 | public const string ZoneInfo = "zoneinfo";
55 |
56 | /// End-User's locale, represented as a BCP47 [RFC5646] language tag. This is typically an ISO 639-1 Alpha-2 [ISO639‑1] language code in lowercase and an ISO 3166-1 Alpha-2 [ISO3166‑1] country code in uppercase, separated by a dash. For example, en-US or fr-CA. As a compatibility note, some implementations have used an underscore as the separator rather than a dash, for example, en_US; Relying Parties MAY choose to accept this locale syntax as well.
57 | public const string Locale = "locale";
58 |
59 | /// End-User's preferred telephone number. E.164 (https://www.itu.int/rec/T-REC-E.164/e) is RECOMMENDED as the format of this Claim, for example, +1 (425) 555-1212 or +56 (2) 687 2400. If the phone number contains an extension, it is RECOMMENDED that the extension be represented using the RFC 3966 [RFC3966] extension syntax, for example, +1 (604) 555-1234;ext=5678.
60 | public const string PhoneNumber = "phone_number";
61 |
62 | /// True if the End-User's phone number has been verified; otherwise false. When this Claim Value is true, this means that the OP took affirmative steps to ensure that this phone number was controlled by the End-User at the time the verification was performed.
63 | /// The means by which a phone number is verified is context-specific, and dependent upon the trust framework or contractual agreements within which the parties are operating. When true, the phone_number Claim MUST be in E.164 format and any extensions MUST be represented in RFC 3966 format.
64 | public const string PhoneNumberVerified = "phone_number_verified";
65 |
66 | /// End-User's preferred postal address. The value of the address member is a JSON structure containing some or all of the members defined in http://openid.net/specs/openid-connect-basic-1_0-32.html#AddressClaim
67 | public const string Address = "address";
68 |
69 | /// Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. It MAY also contain identifiers for other audiences. In the general case, the aud value is an array of case sensitive strings. In the common special case when there is one audience, the aud value MAY be a single case sensitive string.
70 | public const string Audience = "aud";
71 |
72 | /// Issuer Identifier for the Issuer of the response. The iss value is a case sensitive URL using the https scheme that contains scheme, host, and optionally, port number and path components and no query or fragment components.
73 | public const string Issuer = "iss";
74 |
75 | /// The time before which the JWT MUST NOT be accepted for processing, specified as the number of seconds from 1970-01-01T0:0:0Z
76 | public const string NotBefore = "nbf";
77 |
78 | /// The exp (expiration time) claim identifies the expiration time on or after which the token MUST NOT be accepted for processing, specified as the number of seconds from 1970-01-01T0:0:0Z
79 | public const string Expiration = "exp";
80 |
81 | /// Time the End-User's information was last updated. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.
82 | public const string UpdatedAt = "updated_at";
83 |
84 | /// The iat (issued at) claim identifies the time at which the JWT was issued, , specified as the number of seconds from 1970-01-01T0:0:0Z
85 | public const string IssuedAt = "iat";
86 |
87 | /// Authentication Methods References. JSON array of strings that are identifiers for authentication methods used in the authentication.
88 | public const string AuthenticationMethod = "amr";
89 |
90 | /// Session identifier. This represents a Session of an OP at an RP to a User Agent or device for a logged-in End-User. Its contents are unique to the OP and opaque to the RP.
91 | public const string SessionId = "sid";
92 |
93 | ///
94 | /// Authentication Context Class Reference. String specifying an Authentication Context Class Reference value that identifies the Authentication Context Class that the authentication performed satisfied.
95 | /// The value "0" indicates the End-User authentication did not meet the requirements of ISO/IEC 29115 level 1.
96 | /// Authentication using a long-lived browser cookie, for instance, is one example where the use of "level 0" is appropriate.
97 | /// Authentications with level 0 SHOULD NOT be used to authorize access to any resource of any monetary value.
98 | /// (This corresponds to the OpenID 2.0 PAPE nist_auth_level 0.)
99 | /// An absolute URI or an RFC 6711 registered name SHOULD be used as the acr value; registered names MUST NOT be used with a different meaning than that which is registered.
100 | /// Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific.
101 | /// The acr value is a case sensitive string.
102 | ///
103 | public const string AuthenticationContextClassReference = "acr";
104 |
105 | /// Time when the End-User authentication occurred. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. When a max_age request is made or when auth_time is requested as an Essential Claim, then this Claim is REQUIRED; otherwise, its inclusion is OPTIONAL.
106 | public const string AuthenticationTime = "auth_time";
107 |
108 | /// The party to which the ID Token was issued. If present, it MUST contain the OAuth 2.0 Client ID of this party. This Claim is only needed when the ID Token has a single audience value and that audience is different than the authorized party. It MAY be included even when the authorized party is the same as the sole audience. The azp value is a case sensitive string containing a StringOrURI value.
109 | public const string AuthorizedParty = "azp";
110 |
111 | /// Access Token hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the access_token value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, hash the access_token value with SHA-256, then take the left-most 128 bits and base64url encode them. The at_hash value is a case sensitive string.
112 | public const string AccessTokenHash = "at_hash";
113 |
114 | /// Code hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the code value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is HS512, hash the code value with SHA-512, then take the left-most 256 bits and base64url encode them. The c_hash value is a case sensitive string.
115 | public const string AuthorizationCodeHash = "c_hash";
116 |
117 | /// State hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of the ASCII representation of the state value, where the hash algorithm used is the hash algorithm used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is HS512, hash the code value with SHA-512, then take the left-most 256 bits and base64url encode them. The c_hash value is a case sensitive string.
118 | public const string StateHash = "s_hash";
119 |
120 | /// String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the value of the nonce parameter sent in the Authentication Request. If present in the Authentication Request, Authorization Servers MUST include a nonce Claim in the ID Token with the Claim Value being the nonce value sent in the Authentication Request. Authorization Servers SHOULD perform no other processing on nonce values used. The nonce value is a case sensitive string.
121 | public const string Nonce = "nonce";
122 |
123 | /// JWT ID. A unique identifier for the token, which can be used to prevent reuse of the token. These tokens MUST only be used once, unless conditions for reuse were negotiated between the parties; any such negotiation is beyond the scope of this specification.
124 | public const string JwtId = "jti";
125 |
126 | /// Defines a set of event statements that each may add additional claims to fully describe a single logical event that has occurred.
127 | public const string Events = "events";
128 |
129 | /// OAuth 2.0 Client Identifier valid at the Authorization Server.
130 | public const string ClientId = "client_id";
131 |
132 | /// OpenID Connect requests MUST contain the "openid" scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. Scope values used that are not understood by an implementation SHOULD be ignored.
133 | public const string Scope = "scope";
134 |
135 | /// The "act" (actor) claim provides a means within a JWT to express that delegation has occurred and identify the acting party to whom authority has been delegated.The "act" claim value is a JSON object and members in the JSON object are claims that identify the actor. The claims that make up the "act" claim identify and possibly provide additional information about the actor.
136 | public const string Actor = "act";
137 |
138 | /// The "may_act" claim makes a statement that one party is authorized to become the actor and act on behalf of another party. The claim value is a JSON object and members in the JSON object are claims that identify the party that is asserted as being eligible to act for the party identified by the JWT containing the claim.
139 | public const string MayAct = "may_act";
140 |
141 | ///
142 | /// an identifier
143 | ///
144 | public const string Id = "id";
145 |
146 | ///
147 | /// The identity provider
148 | ///
149 | public const string IdentityProvider = "idp";
150 |
151 | ///
152 | /// The role
153 | ///
154 | public const string Role = "role";
155 |
156 | ///
157 | /// The reference token identifier
158 | ///
159 | public const string ReferenceTokenId = "reference_token_id";
160 |
161 | ///
162 | /// The confirmation
163 | ///
164 | public const string Confirmation = "cnf";
165 | }
166 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/ApiTokenCleanHostedService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using AspNetCore.Authentication.ApiToken.Abstractions;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Hosting;
7 | using Microsoft.Extensions.Logging;
8 | using Microsoft.Extensions.Options;
9 | using Timer = System.Threading.Timer;
10 |
11 | namespace AspNetCore.Authentication.ApiToken
12 | {
13 | public class ApiTokenCleanHostedService : IHostedService
14 | {
15 | private readonly ILogger _logger;
16 | private readonly IApiTokenCacheService _cache;
17 | private readonly IServiceProvider _serviceProvider;
18 | private readonly ApiTokenCleanOptions _options;
19 |
20 | private const string LockKey = "cleanlock";
21 |
22 | private Timer _timer;
23 |
24 | public ApiTokenCleanHostedService(IOptions options,
25 | ILogger logger,
26 | IApiTokenCacheService cache,
27 | IServiceProvider serviceProvider)
28 | {
29 | _logger = logger;
30 | _cache = cache;
31 | _serviceProvider = serviceProvider;
32 | _options = options.Value;
33 | }
34 |
35 | public Task StartAsync(CancellationToken cancellationToken)
36 | {
37 | if (_options.Interval <= 0)
38 | {
39 | _logger.LogInformation($"The value of Interval is {_options.Interval}, service will not start.");
40 | return Task.CompletedTask;
41 | }
42 |
43 | _timer = new Timer(async _ => await DoWorkAsync(), null, TimeSpan.Zero,
44 | TimeSpan.FromSeconds(_options.Interval));
45 | _logger.LogInformation("Start success.");
46 | return Task.CompletedTask;
47 | }
48 |
49 | private async Task DoWorkAsync()
50 | {
51 | var lockValue = Guid.NewGuid().ToString();
52 |
53 | //Request lock
54 | if (!await _cache.LockTakeAsync(LockKey, lockValue, TimeSpan.FromSeconds(60)))
55 | {
56 | _logger.LogInformation("Request lock failed, not run.");
57 | return;
58 | }
59 |
60 | using var scope = _serviceProvider.CreateScope();
61 | var tokenStore = scope.ServiceProvider.GetRequiredService();
62 | var cleanCount = await tokenStore.RemoveExpirationAsync();
63 |
64 | await _cache.LockReleaseAsync(LockKey, lockValue);
65 | _logger.LogInformation($"{cleanCount} expired token records have been deleted.");
66 | }
67 |
68 | public Task StopAsync(CancellationToken cancellationToken)
69 | {
70 | _timer?.Change(Timeout.Infinite, 0);
71 | _logger.LogInformation("Stop success.");
72 | return Task.CompletedTask;
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/ApiTokenCleanOptions.cs:
--------------------------------------------------------------------------------
1 | using AspNetCore.Authentication.ApiToken.Abstractions;
2 |
3 | namespace AspNetCore.Authentication.ApiToken
4 | {
5 | public class ApiTokenCleanOptions
6 | {
7 | ///
8 | /// Background Service periodically run clean stored expired token, will call . Unit: second.
9 | ///
10 | /// * If set value to 0, the service will not start.
11 | ///
12 | public int Interval { get; set; } = 86400;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/ApiTokenDefaults.cs:
--------------------------------------------------------------------------------
1 | namespace AspNetCore.Authentication.ApiToken
2 | {
3 | public static class ApiTokenDefaults
4 | {
5 | ///
6 | /// Default value for AuthenticationScheme
7 | ///
8 | public const string AuthenticationScheme = "ApiToken";
9 |
10 | ///
11 | /// Get token from request header key, eg. Authorization: Bearer xxx
12 | ///
13 | public const string TokenParseHeaderKey = "Authorization";
14 |
15 | ///
16 | /// Get token from request querystring key, eg. https://www.google.com/api/apple?token=xxxx
17 | ///
18 | public const string TokenParseQueryStringKey = "ApiToken";
19 |
20 | public const string ApiTokenName = "access_token";
21 | }
22 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/ApiTokenEvents.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using AspNetCore.Authentication.ApiToken.Events;
4 |
5 | namespace AspNetCore.Authentication.ApiToken
6 | {
7 | ///
8 | /// Specifies events which the invokes to enable developer control over the authentication process.
9 | ///
10 | public class ApiTokenEvents
11 | {
12 | ///
13 | /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
14 | ///
15 | public Func OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;
16 |
17 | ///
18 | /// Invoked if Authorization fails and results in a Forbidden response.
19 | ///
20 | public Func OnForbidden { get; set; } = context => Task.CompletedTask;
21 |
22 | ///
23 | /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
24 | ///
25 | public Func OnTokenValidated { get; set; } = context => Task.CompletedTask;
26 |
27 | ///
28 | /// Invoked before a challenge is sent back to the caller.
29 | ///
30 | public Func OnChallenge { get; set; } = context => Task.CompletedTask;
31 |
32 | ///
33 | /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
34 | ///
35 | public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context);
36 |
37 | ///
38 | /// Invoked if Authorization fails and results in a Forbidden response
39 | ///
40 | public virtual Task Forbidden(ForbiddenContext context) => OnForbidden(context);
41 |
42 |
43 | ///
44 | /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
45 | ///
46 | public virtual Task TokenValidated(ApiTokenValidatedContext context) => OnTokenValidated(context);
47 |
48 | ///
49 | /// Invoked before a challenge is sent back to the caller.
50 | ///
51 | public virtual Task Challenge(ApiTokenChallengeContext context) => OnChallenge(context);
52 |
53 | ///
54 | /// Invoked when a protocol message is first received.
55 | ///
56 | public Func OnMessageReceived { get; set; } = context => Task.CompletedTask;
57 |
58 | ///
59 | /// Invoked when a protocol message is first received.
60 | ///
61 | public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context);
62 |
63 | }
64 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/ApiTokenExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AspNetCore.Authentication.ApiToken.Abstractions;
3 | using Microsoft.AspNetCore.Authentication;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.DependencyInjection.Extensions;
6 | using Microsoft.Extensions.Options;
7 |
8 | namespace AspNetCore.Authentication.ApiToken
9 | {
10 | ///
11 | /// Extension methods to configure ApiToken Token bearer authentication.
12 | ///
13 | public static class ApiTokenExtensions
14 | {
15 | ///
16 | /// Enables ApiToken Token-bearer authentication using the default scheme .
17 | ///
18 | /// ApiToken Token bearer authentication performs authentication by extracting and validating a ApiToken Token token from the Authorization request header.
19 | ///
20 | ///
21 | /// The .
22 | /// A reference to after the operation has completed.
23 | public static ApiTokenAuthenticationBuilder AddApiToken(this AuthenticationBuilder builder)
24 | => builder.AddApiToken(ApiTokenDefaults.AuthenticationScheme, _ => { });
25 |
26 | ///
27 | /// Enables ApiToken Token-bearer authentication using the default scheme .
28 | ///
29 | /// ApiToken Token bearer authentication performs authentication by extracting and validating a ApiToken Token token from the Authorization request header.
30 | ///
31 | ///
32 | /// The .
33 | /// A delegate that allows configuring .
34 | /// A reference to after the operation has completed.
35 | public static ApiTokenAuthenticationBuilder AddApiToken(this AuthenticationBuilder builder, Action configureOptions)
36 | => builder.AddApiToken(ApiTokenDefaults.AuthenticationScheme, configureOptions);
37 |
38 | ///
39 | /// Enables ApiToken Token-bearer authentication using the specified scheme.
40 | ///
41 | /// ApiToken Token bearer authentication performs authentication by extracting and validating a ApiToken Token token from the Authorization request header.
42 | ///
43 | ///
44 | /// The .
45 | /// The authentication scheme.
46 | /// A delegate that allows configuring .
47 | /// A reference to after the operation has completed.
48 | public static ApiTokenAuthenticationBuilder AddApiToken(this AuthenticationBuilder builder, string authenticationScheme, Action configureOptions)
49 | => builder.AddApiToken(authenticationScheme, displayName: null, configureOptions: configureOptions);
50 |
51 | ///
52 | /// Enables ApiToken Token-bearer authentication using the specified scheme.
53 | ///
54 | /// ApiToken Token bearer authentication performs authentication by extracting and validating a ApiToken Token token from the Authorization request header.
55 | ///
56 | ///
57 | /// The .
58 | /// The authentication scheme.
59 | /// The display name for the authentication handler.
60 | /// A delegate that allows configuring .
61 | /// A reference to after the operation has completed.
62 | public static ApiTokenAuthenticationBuilder AddApiToken(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions)
63 | {
64 | if (configureOptions == null)
65 | {
66 | throw new ArgumentNullException(nameof(configureOptions));
67 | }
68 |
69 | builder.Services.AddSingleton();
70 | builder.Services.AddTransient();
71 | builder.Services.AddTransient();
72 | builder.Services.AddHostedService();
73 |
74 | builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ApiTokenPostConfigureOptions>());
75 | builder.AddScheme(authenticationScheme, displayName, configureOptions);
76 | return new ApiTokenAuthenticationBuilder(builder);
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/src/AspNetCore.AuthenticationApiToken/ApiTokenHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Security.Claims;
4 | using System.Text;
5 | using System.Text.Encodings.Web;
6 | using System.Threading.Tasks;
7 | using AspNetCore.Authentication.ApiToken.Abstractions;
8 | using AspNetCore.Authentication.ApiToken.Events;
9 | using AspNetCore.Authentication.ApiToken.Exceptions;
10 | using Microsoft.AspNetCore.Authentication;
11 | using Microsoft.AspNetCore.Http;
12 | using Microsoft.Extensions.Logging;
13 | using Microsoft.Extensions.Options;
14 | using Microsoft.Net.Http.Headers;
15 |
16 | namespace AspNetCore.Authentication.ApiToken
17 | {
18 | public class ApiTokenHandler : AuthenticationHandler
19 | {
20 | private readonly IApiTokenValidator _tokenValidator;
21 |
22 | ///
23 | /// Initializes a new instance of .
24 | ///
25 | ///
26 | public ApiTokenHandler(
27 | IOptionsMonitor options,
28 | ILoggerFactory logger,
29 | UrlEncoder encoder,
30 | ISystemClock clock,
31 | IApiTokenValidator tokenValidator) : base(options, logger, encoder, clock)
32 | {
33 | _tokenValidator = tokenValidator;
34 | }
35 |
36 | ///
37 | /// The handler calls methods on the events which give the application control at certain points where processing is occurring.
38 | /// If it is not provided a default instance is supplied which does nothing when the methods are called.
39 | ///
40 | protected new ApiTokenEvents Events
41 | {
42 | get => (ApiTokenEvents)base.Events;
43 | set => base.Events = value;
44 | }
45 |
46 | protected override Task