├── .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 | [![Latest version](https://img.shields.io/nuget/v/AspNetCore.Authentication.ApiToken.svg)](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 | ![](assets/op.gif) 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 | [![Latest version](https://img.shields.io/nuget/v/AspNetCore.Authentication.ApiToken.svg)](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 | ![](assets/op.gif) 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 | ![image-run](assets/swagger.png) 53 | 54 | Gif: 55 | 56 | ![image-op](../../assets/op.gif) -------------------------------------------------------------------------------- /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 CreateEventsAsync() => Task.FromResult(new ApiTokenEvents()); 47 | 48 | protected virtual string ParseToken() 49 | { 50 | string token; 51 | var type = Options.ParseType; 52 | 53 | var attr = Context.GetEndpoint()?.Metadata.GetMetadata(); 54 | if (attr != null) 55 | { 56 | type = attr.Type; 57 | } 58 | 59 | switch (type) 60 | { 61 | case ApiTokenParseType.Header: 62 | token = ParseTokenFromHeader(); 63 | break; 64 | case ApiTokenParseType.QueryString: 65 | token = ParseTokenFromQueryString(); 66 | break; 67 | default: 68 | { 69 | token = ParseTokenFromQueryString(); 70 | if (string.IsNullOrEmpty(token)) 71 | { 72 | token = ParseTokenFromHeader(); 73 | } 74 | 75 | break; 76 | } 77 | } 78 | 79 | if (token != null && token.Length != 64) 80 | { 81 | return null; 82 | } 83 | 84 | return token; 85 | } 86 | 87 | private string ParseTokenFromHeader() 88 | { 89 | string authorization = Request.Headers[Options.HeaderKey]; 90 | 91 | if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) 92 | { 93 | return authorization.Substring("Bearer ".Length).Trim(); 94 | 95 | } 96 | 97 | return null; 98 | } 99 | 100 | private string ParseTokenFromQueryString() 101 | { 102 | if (Request.Query.ContainsKey(Options.QueryStringKey)) 103 | { 104 | return Request.Query[Options.QueryStringKey]; 105 | } 106 | 107 | return null; 108 | } 109 | 110 | /// 111 | /// Searches the 'Authorization' header for a 'Bearer' token. 112 | /// 113 | /// 114 | protected override async Task HandleAuthenticateAsync() 115 | { 116 | try 117 | { 118 | // Give application opportunity to find from a different location, adjust, or reject token 119 | var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options); 120 | 121 | // event can set the token 122 | await Events.MessageReceived(messageReceivedContext); 123 | if (messageReceivedContext.Result != null) 124 | { 125 | return messageReceivedContext.Result; 126 | } 127 | 128 | // If application retrieved token from somewhere else, use that. 129 | var token = messageReceivedContext.Token; 130 | 131 | if (string.IsNullOrEmpty(token)) 132 | { 133 | token = ParseToken(); 134 | } 135 | 136 | if (string.IsNullOrEmpty(token)) 137 | { 138 | return AuthenticateResult.NoResult(); 139 | } 140 | 141 | Exception validationFailure = null; 142 | ClaimsPrincipal principal = null; 143 | try 144 | { 145 | principal = await _tokenValidator.ValidateTokenAsync(Options, token, Scheme.Name); 146 | } 147 | catch (Exception ex) 148 | { 149 | Logger.TokenValidationFailed(ex); 150 | validationFailure = ex; 151 | } 152 | 153 | if (validationFailure != null) 154 | { 155 | var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) 156 | { 157 | Exception = validationFailure 158 | }; 159 | 160 | await Events.AuthenticationFailed(authenticationFailedContext); 161 | 162 | if (authenticationFailedContext.Result != null) 163 | { 164 | return authenticationFailedContext.Result; 165 | } 166 | 167 | return AuthenticateResult.Fail(authenticationFailedContext.Exception); 168 | } 169 | 170 | Logger.TokenValidationSucceeded(); 171 | var tokenValidatedContext = new ApiTokenValidatedContext(Context, Scheme, Options) 172 | { 173 | Principal = principal, 174 | Token = token 175 | }; 176 | 177 | await Events.TokenValidated(tokenValidatedContext); 178 | if (tokenValidatedContext.Result != null) 179 | { 180 | return tokenValidatedContext.Result; 181 | } 182 | 183 | tokenValidatedContext.Properties.StoreTokens(new[] 184 | { 185 | new AuthenticationToken { Name = ApiTokenDefaults.ApiTokenName, Value = token } 186 | }); 187 | 188 | tokenValidatedContext.Success(); 189 | return tokenValidatedContext.Result; 190 | } 191 | catch (Exception ex) 192 | { 193 | Logger.ErrorProcessingMessage(ex); 194 | 195 | var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options) 196 | { 197 | Exception = ex 198 | }; 199 | 200 | await Events.AuthenticationFailed(authenticationFailedContext); 201 | 202 | if (authenticationFailedContext.Result != null) 203 | { 204 | return authenticationFailedContext.Result; 205 | } 206 | 207 | throw; 208 | } 209 | } 210 | 211 | protected override async Task HandleChallengeAsync(AuthenticationProperties properties) 212 | { 213 | var authResult = await HandleAuthenticateOnceSafeAsync(); 214 | var eventContext = new ApiTokenChallengeContext(Context, Scheme, Options, properties) 215 | { 216 | AuthenticateFailure = authResult?.Failure 217 | }; 218 | 219 | // Avoid returning error=invalid_token if the error is not caused by an authentication failure (e.g missing token). 220 | if (Options.IncludeErrorDetails && eventContext.AuthenticateFailure != null) 221 | { 222 | eventContext.Error = "invalid_token"; 223 | eventContext.ErrorDescription = CreateErrorDescription(eventContext.AuthenticateFailure); 224 | } 225 | 226 | await Events.Challenge(eventContext); 227 | if (eventContext.Handled) 228 | { 229 | return; 230 | } 231 | 232 | Response.StatusCode = 401; 233 | 234 | if (string.IsNullOrEmpty(eventContext.Error) && 235 | string.IsNullOrEmpty(eventContext.ErrorDescription) && 236 | string.IsNullOrEmpty(eventContext.ErrorUri)) 237 | { 238 | Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge); 239 | } 240 | else 241 | { 242 | // https://tools.ietf.org/html/rfc6750#section-3.1 243 | // WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired" 244 | var builder = new StringBuilder(Options.Challenge); 245 | if (Options.Challenge.IndexOf(' ') > 0) 246 | { 247 | // Only add a comma after the first param, if any 248 | builder.Append(','); 249 | } 250 | if (!string.IsNullOrEmpty(eventContext.Error)) 251 | { 252 | builder.Append(" error=\""); 253 | builder.Append(eventContext.Error); 254 | builder.Append("\""); 255 | } 256 | if (!string.IsNullOrEmpty(eventContext.ErrorDescription)) 257 | { 258 | if (!string.IsNullOrEmpty(eventContext.Error)) 259 | { 260 | builder.Append(","); 261 | } 262 | 263 | builder.Append(" error_description=\""); 264 | builder.Append(eventContext.ErrorDescription); 265 | builder.Append('\"'); 266 | } 267 | if (!string.IsNullOrEmpty(eventContext.ErrorUri)) 268 | { 269 | if (!string.IsNullOrEmpty(eventContext.Error) || 270 | !string.IsNullOrEmpty(eventContext.ErrorDescription)) 271 | { 272 | builder.Append(","); 273 | } 274 | 275 | builder.Append(" error_uri=\""); 276 | builder.Append(eventContext.ErrorUri); 277 | builder.Append('\"'); 278 | } 279 | 280 | Response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString()); 281 | } 282 | } 283 | 284 | protected override Task HandleForbiddenAsync(AuthenticationProperties properties) 285 | { 286 | var forbiddenContext = new ForbiddenContext(Context, Scheme, Options); 287 | Response.StatusCode = 403; 288 | return Events.Forbidden(forbiddenContext); 289 | } 290 | 291 | private static string CreateErrorDescription(Exception authFailure) 292 | { 293 | string message = authFailure switch 294 | { 295 | TokenExpiredException ee => 296 | $"The token expired at '{ee.ExpireAt.LocalDateTime.ToString(CultureInfo.InvariantCulture)}'", 297 | _ => authFailure.Message 298 | }; 299 | 300 | return message; 301 | } 302 | 303 | 304 | } 305 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/ApiTokenInitializeService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using AspNetCore.Authentication.ApiToken.Abstractions; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace AspNetCore.Authentication.ApiToken 8 | { 9 | public class ApiTokenInitializeService: IHostedService 10 | { 11 | private readonly IApiTokenCacheService _cacheService; 12 | private readonly ILogger _logger; 13 | 14 | public ApiTokenInitializeService(IApiTokenCacheService cacheService,ILogger logger) 15 | { 16 | _cacheService = cacheService; 17 | _logger = logger; 18 | } 19 | public async Task StartAsync(CancellationToken cancellationToken) 20 | { 21 | await _cacheService.InitializeAsync(); 22 | } 23 | 24 | public Task StopAsync(CancellationToken cancellationToken) 25 | { 26 | return Task.CompletedTask; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/ApiTokenOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using AspNetCore.Authentication.ApiToken.Abstractions; 3 | using Microsoft.AspNetCore.Authentication; 4 | 5 | namespace AspNetCore.Authentication.ApiToken 6 | { 7 | public class ApiTokenOptions : AuthenticationSchemeOptions 8 | { 9 | /// 10 | /// Defines whether the token validation errors should be returned to the caller. 11 | /// Enabled by default, this option can be disabled to prevent the JWT handler 12 | /// from returning an error and an error_description in the WWW-Authenticate header. 13 | /// 14 | public bool IncludeErrorDetails { get; set; } = true; 15 | 16 | /// 17 | /// Gets or sets the challenge to put in the "WWW-Authenticate" header. 18 | /// 19 | public string Challenge { get; set; } = ApiTokenDefaults.AuthenticationScheme; 20 | 21 | /// 22 | /// The object provided by the application to process events raised by the api key authentication middleware. 23 | /// The application may implement the interface fully, or it may create an instance of 24 | /// and assign delegates only to the events it wants to process. 25 | /// 26 | public new ApiTokenEvents Events 27 | { 28 | get => (ApiTokenEvents)base.Events; 29 | set => base.Events = value; 30 | } 31 | 32 | /// 33 | /// Set where to parse the token. Default 34 | /// 35 | public ApiTokenParseType ParseType { get; set; } = ApiTokenParseType.Both; 36 | 37 | /// 38 | /// Token parse from request header key. Default 39 | /// 40 | public string HeaderKey { get; set; } = ApiTokenDefaults.TokenParseHeaderKey; 41 | 42 | /// 43 | /// Token parse from request querystring key. Default 44 | /// 45 | public string QueryStringKey { get; set; } = ApiTokenDefaults.TokenParseQueryStringKey; 46 | 47 | /// 48 | /// ApiToken expire time. Default: 1 hour. 49 | /// 50 | public TimeSpan TokenExpire { get; set; } = TimeSpan.FromHours(1); 51 | 52 | /// 53 | /// Refresh token expire time. Default: 24 hour. 54 | /// 55 | public TimeSpan RefreshTokenExpire { get; set; } = TimeSpan.FromHours(24); 56 | 57 | /// 58 | /// If set up to false,Repeated creation of token () will invalidate the old token for user. 59 | /// 60 | public bool AllowMultiTokenActive { get; set; } = true; 61 | 62 | /// 63 | /// Use caching to improve performance, the cache service implementation interface . 64 | /// 65 | public bool UseCache { get; set; } = false; 66 | 67 | public string RoleClaimType { get; set; } = ApiTokenClaimTypes.Role; 68 | 69 | public string NameClaimType { get; set; } = ApiTokenClaimTypes.Name; 70 | 71 | /// 72 | /// The time when the old bearer token is still in effect when the token is refreshed. This property only valid when = true. 73 | /// 74 | public TimeSpan? KeepTokenValidTimeSpanOnRefresh { get; set; } 75 | 76 | } 77 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/ApiTokenPostConfigureOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace AspNetCore.Authentication.ApiToken 5 | { 6 | /// 7 | /// Used to setup defaults for all . 8 | /// 9 | public class ApiTokenPostConfigureOptions : IPostConfigureOptions 10 | { 11 | /// 12 | /// Invoked to post configure a JwtBearerOptions instance. 13 | /// 14 | /// The name of the options instance being configured. 15 | /// The options instance to configure. 16 | public void PostConfigure(string name, ApiTokenOptions options) 17 | { 18 | switch (options.ParseType) 19 | { 20 | case ApiTokenParseType.Header: 21 | ValidateHeaderKey(options); 22 | break; 23 | case ApiTokenParseType.QueryString: 24 | ValidateQueryStringKey(options); 25 | break; 26 | default: 27 | ValidateHeaderKey(options); 28 | ValidateQueryStringKey(options); 29 | break; 30 | } 31 | } 32 | 33 | private static void ValidateHeaderKey(ApiTokenOptions options) 34 | { 35 | if (options.ParseType == ApiTokenParseType.Header && string.IsNullOrEmpty(options.HeaderKey)) 36 | { 37 | throw new InvalidOperationException($"{nameof(ApiTokenOptions.HeaderKey)} must be set."); 38 | } 39 | } 40 | 41 | private static void ValidateQueryStringKey(ApiTokenOptions options) 42 | { 43 | if (options.ParseType == ApiTokenParseType.QueryString && string.IsNullOrEmpty(options.QueryStringKey)) 44 | { 45 | throw new InvalidOperationException($"{nameof(ApiTokenOptions.QueryStringKey)} must be set."); 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/ApiTokenTools.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace AspNetCore.Authentication.ApiToken 6 | { 7 | /// 8 | /// Token Generating Algorithm 9 | /// 10 | /// UserId + DateTime.Now(MillSecond) + RandBytes(Length 8) 11 | /// 12 | /// SHA256 + HEX 13 | /// 14 | public class ApiTokenTools 15 | { 16 | public static string CreateToken(string userId) 17 | { 18 | var bytes = new Span(new byte[8]); 19 | var random = new Random(DateTime.Now.Millisecond); 20 | random.NextBytes(bytes); 21 | return CreateToken(userId, DateTime.Now, bytes); 22 | } 23 | 24 | public static string CreateToken(string userId, DateTimeOffset now, Span randBytes) 25 | { 26 | var data = Encoding.UTF8.GetBytes($"{userId}{now.ToUnixTimeMilliseconds()}").AsSpan(); 27 | var resultBytes = new Span(new byte[data.Length + randBytes.Length]); 28 | data.CopyTo(resultBytes); 29 | randBytes.CopyTo(resultBytes.Slice(data.Length)); 30 | 31 | using var sha = SHA256.Create(); 32 | var result = sha.ComputeHash(resultBytes.ToArray()); 33 | 34 | var hex = new StringBuilder(); 35 | foreach (byte b in result) 36 | { 37 | hex.AppendFormat("{0:x2}", b); 38 | } 39 | return hex.ToString().ToUpper(); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/AspNetCore.Authentication.ApiToken.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1;net5.0; 5 | $(LibraryFrameworks) 6 | disable 7 | A ASP.NET Core Reference Token authentication open source library. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/AspNetCore.Authentication.ApiToken.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | False 7 | True -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Cache/NullApiTokenCacheService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AspNetCore.Authentication.ApiToken.Abstractions; 4 | 5 | namespace AspNetCore.Authentication.ApiToken 6 | { 7 | public class NullApiTokenCacheService : IApiTokenCacheService 8 | { 9 | public Task GetAsync(string token, string scheme) 10 | { 11 | throw new NotImplementedException(); 12 | } 13 | 14 | public Task SetAsync(TokenModel token) 15 | { 16 | return Task.CompletedTask; 17 | } 18 | 19 | public Task SetNullAsync(string invalidToken, string scheme) 20 | { 21 | return Task.CompletedTask; 22 | } 23 | 24 | public Task RemoveAsync(string token, string scheme) 25 | { 26 | return Task.CompletedTask; 27 | } 28 | 29 | public Task LockTakeAsync(string key, string value, TimeSpan timeOut) 30 | { 31 | return Task.FromResult(true); 32 | } 33 | 34 | public Task LockReleaseAsync(string key, string value) 35 | { 36 | return Task.FromResult(true); 37 | } 38 | 39 | public Task InitializeAsync() 40 | { 41 | return Task.CompletedTask; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Cache/TokenModelCache.cs: -------------------------------------------------------------------------------- 1 | namespace AspNetCore.Authentication.ApiToken 2 | { 3 | public class TokenModelCache 4 | { 5 | public TokenModel Token { get; set; } 6 | public bool Available => Token != null; 7 | } 8 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Events/ApiTokenChallengeContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace AspNetCore.Authentication.ApiToken.Events 6 | { 7 | /// 8 | /// A when access to a resource authenticated using ApiToken Token bearer is challenged. 9 | /// 10 | public class ApiTokenChallengeContext : PropertiesContext 11 | { 12 | /// 13 | /// Initializes a new instance of . 14 | /// 15 | /// 16 | public ApiTokenChallengeContext( 17 | HttpContext context, 18 | AuthenticationScheme scheme, 19 | ApiTokenOptions options, 20 | AuthenticationProperties properties) 21 | : base(context, scheme, options, properties) { } 22 | 23 | /// 24 | /// Any failures encountered during the authentication process. 25 | /// 26 | public Exception AuthenticateFailure { get; set; } 27 | 28 | /// 29 | /// Gets or sets the "error" value returned to the caller as part 30 | /// of the WWW-Authenticate header. This property may be null when 31 | /// is set to false. 32 | /// 33 | public string Error { get; set; } 34 | 35 | /// 36 | /// Gets or sets the "error_description" value returned to the caller as part 37 | /// of the WWW-Authenticate header. This property may be null when 38 | /// is set to false. 39 | /// 40 | public string ErrorDescription { get; set; } 41 | 42 | /// 43 | /// Gets or sets the "error_uri" value returned to the caller as part of the 44 | /// WWW-Authenticate header. This property is always null unless explicitly set. 45 | /// 46 | public string ErrorUri { get; set; } 47 | 48 | /// 49 | /// If true, will skip any default logic for this challenge. 50 | /// 51 | public bool Handled { get; private set; } 52 | 53 | /// 54 | /// Skips any default logic for this challenge. 55 | /// 56 | public void HandleResponse() => Handled = true; 57 | } 58 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Events/ApiTokenValidatedContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace AspNetCore.Authentication.ApiToken.Events 5 | { 6 | /// 7 | /// A context for . 8 | /// 9 | public class ApiTokenValidatedContext : ResultContext 10 | { 11 | /// 12 | /// Initializes a new instance of . 13 | /// 14 | /// 15 | public ApiTokenValidatedContext( 16 | HttpContext context, 17 | AuthenticationScheme scheme, 18 | ApiTokenOptions options) 19 | : base(context, scheme, options) { } 20 | 21 | public string Token { get; set; } 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Events/AuthenticationFailedContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace AspNetCore.Authentication.ApiToken.Events 6 | { 7 | /// 8 | /// A when authentication has failed. 9 | /// 10 | public class AuthenticationFailedContext : ResultContext 11 | { 12 | /// 13 | /// Initializes a new instance of . 14 | /// 15 | /// 16 | public AuthenticationFailedContext( 17 | HttpContext context, 18 | AuthenticationScheme scheme, 19 | ApiTokenOptions options) 20 | : base(context, scheme, options) { } 21 | 22 | /// 23 | /// Gets or sets the exception associated with the authentication failure. 24 | /// 25 | public Exception Exception { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Events/ForbiddenContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace AspNetCore.Authentication.ApiToken.Events 5 | { 6 | /// 7 | /// A when access to a resource is forbidden. 8 | /// 9 | public class ForbiddenContext : ResultContext 10 | { 11 | /// 12 | /// Initializes a new instance of . 13 | /// 14 | /// 15 | public ForbiddenContext( 16 | HttpContext context, 17 | AuthenticationScheme scheme, 18 | ApiTokenOptions options) 19 | : base(context, scheme, options) { } 20 | 21 | 22 | } 23 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Events/MessageReceivedContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace AspNetCore.Authentication.ApiToken.Events 5 | { 6 | /// 7 | /// A context for . 8 | /// 9 | public class MessageReceivedContext : ResultContext 10 | { 11 | /// 12 | /// Initializes a new instance of . 13 | /// 14 | /// 15 | public MessageReceivedContext( 16 | HttpContext context, 17 | AuthenticationScheme scheme, 18 | ApiTokenOptions options) 19 | : base(context, scheme, options) { } 20 | 21 | /// 22 | /// Bearer Token. This will give the application an opportunity to retrieve a token from an alternative location. 23 | /// 24 | public string Token { get; set; } 25 | 26 | } 27 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Exceptions/TokenExpiredException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AspNetCore.Authentication.ApiToken.Exceptions 4 | { 5 | public class TokenExpiredException : Exception 6 | { 7 | public DateTimeOffset ExpireAt { get; } 8 | 9 | public TokenExpiredException(string message, DateTime expireAt) : base(message) 10 | { 11 | ExpireAt = expireAt; 12 | } 13 | 14 | 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Exceptions/TokenInvalidException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AspNetCore.Authentication.ApiToken.Exceptions 4 | { 5 | public class TokenInvalidException : Exception 6 | { 7 | public TokenInvalidException(string message) : base(message) 8 | { 9 | 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/HttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using AspNetCore.Authentication.ApiToken; 3 | using Microsoft.AspNetCore.Authentication; 4 | 5 | // ReSharper disable once CheckNamespace 6 | namespace Microsoft.AspNetCore.Http 7 | { 8 | public static class HttpContextExtensions 9 | { 10 | public static Task GetApiTokenAsync(this HttpContext context) 11 | { 12 | return context.GetTokenAsync(ApiTokenDefaults.ApiTokenName); 13 | } 14 | 15 | public static Task GetApiTokenAsync(this HttpContext context, string scheme) 16 | { 17 | return context.GetTokenAsync(scheme, ApiTokenDefaults.ApiTokenName); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/LoggingExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // ReSharper disable once CheckNamespace 4 | namespace Microsoft.Extensions.Logging 5 | { 6 | internal static class LoggingExtensions 7 | { 8 | private static Action _tokenValidationFailed; 9 | private static Action _tokenValidationSucceeded; 10 | private static Action _errorProcessingMessage; 11 | 12 | static LoggingExtensions() 13 | { 14 | _tokenValidationFailed = LoggerMessage.Define( 15 | eventId: new EventId(1, "TokenValidationFailed"), 16 | logLevel: LogLevel.Information, 17 | formatString: "Failed to validate the token."); 18 | _tokenValidationSucceeded = LoggerMessage.Define( 19 | eventId: new EventId(2, "TokenValidationSucceeded"), 20 | logLevel: LogLevel.Information, 21 | formatString: "Successfully validated the token."); 22 | _errorProcessingMessage = LoggerMessage.Define( 23 | eventId: new EventId(3, "ProcessingMessageFailed"), 24 | logLevel: LogLevel.Error, 25 | formatString: "Exception occurred while processing message."); 26 | } 27 | 28 | public static void TokenValidationFailed(this ILogger logger, Exception ex) 29 | => _tokenValidationFailed(logger, ex); 30 | 31 | public static void TokenValidationSucceeded(this ILogger logger) 32 | => _tokenValidationSucceeded(logger, null); 33 | 34 | public static void ErrorProcessingMessage(this ILogger logger, Exception ex) 35 | => _errorProcessingMessage(logger, ex); 36 | } 37 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Parse/ApiTokenParseAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AspNetCore.Authentication.ApiToken 4 | { 5 | [AttributeUsage(AttributeTargets.Method)] 6 | public class ApiTokenParseAttribute : Attribute 7 | { 8 | public ApiTokenParseType Type { get; } 9 | 10 | public ApiTokenParseAttribute(ApiTokenParseType type = ApiTokenParseType.Both) 11 | { 12 | Type = type; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Parse/ApiTokenParseType.cs: -------------------------------------------------------------------------------- 1 | namespace AspNetCore.Authentication.ApiToken 2 | { 3 | public enum ApiTokenParseType 4 | { 5 | Header=0, 6 | QueryString, 7 | Both 8 | } 9 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Store/DefaultApiTokenOperator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Security.Claims; 6 | using System.Threading.Tasks; 7 | using AspNetCore.Authentication.ApiToken.Abstractions; 8 | using AspNetCore.Authentication.ApiToken.Results; 9 | using Microsoft.AspNetCore.Authentication; 10 | using Microsoft.Extensions.Options; 11 | 12 | namespace AspNetCore.Authentication.ApiToken 13 | { 14 | public class DefaultApiTokenOperator : IApiTokenOperator 15 | { 16 | private readonly IOptionsMonitor _optionsMonitor; 17 | private readonly IApiTokenProfileService _profileService; 18 | private readonly IApiTokenStore _tokenStore; 19 | private readonly IApiTokenCacheService _cacheService; 20 | private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; 21 | 22 | private ApiTokenOptions _options; 23 | private string _innerScheme; 24 | 25 | public DefaultApiTokenOperator( 26 | IOptionsMonitor optionsMonitor, 27 | IApiTokenProfileService profileService, 28 | IApiTokenStore tokenStore, 29 | IApiTokenCacheService cacheService, 30 | IAuthenticationSchemeProvider authenticationSchemeProvider) 31 | { 32 | _optionsMonitor = optionsMonitor; 33 | _profileService = profileService; 34 | _tokenStore = tokenStore; 35 | _cacheService = cacheService; 36 | _authenticationSchemeProvider = authenticationSchemeProvider; 37 | } 38 | 39 | private async Task InitializeAsync(string scheme) 40 | { 41 | if (string.IsNullOrEmpty(scheme)) 42 | { 43 | var defaultScheme = await _authenticationSchemeProvider.GetDefaultAuthenticateSchemeAsync(); 44 | if (defaultScheme != null && defaultScheme.HandlerType == typeof(ApiTokenHandler)) 45 | { 46 | _innerScheme = defaultScheme.Name; 47 | } 48 | else 49 | { 50 | throw new InvalidOperationException($"Default authenticate scheme type is not {typeof(ApiTokenHandler).FullName}, you must be to set scheme."); 51 | } 52 | } 53 | 54 | _options = _optionsMonitor.Get(_innerScheme); 55 | } 56 | 57 | public virtual async Task CreateAsync(string userId, string scheme = null) 58 | { 59 | await InitializeAsync(scheme); 60 | var claims = await GetUserClaimsAsync(userId); 61 | 62 | var result = CreateToken(userId, claims, _innerScheme); 63 | 64 | await RemoveAllOldTokenAsync(userId, _innerScheme); 65 | 66 | await _tokenStore.StoreAsync(new List() { result.Bearer, result.Refresh }); 67 | 68 | if (_options.UseCache) 69 | { 70 | await _cacheService.SetAsync(result.Bearer); 71 | } 72 | 73 | return result; 74 | } 75 | 76 | public virtual async Task RefreshAsync(string refreshToken, string scheme = null) 77 | { 78 | await InitializeAsync(scheme); 79 | var token = await _tokenStore.GetAsync(refreshToken, _innerScheme); 80 | if (token == null || token.Type != TokenType.Refresh) 81 | { 82 | return TokenCreateResult.Failed("invalid refresh_token"); 83 | } 84 | 85 | if (!token.IsValid) 86 | { 87 | return TokenCreateResult.Failed($"The refresh_token expired at '{token.Expiration.LocalDateTime.ToString(CultureInfo.InvariantCulture)}'"); 88 | } 89 | 90 | var claims = await GetUserClaimsAsync(token.UserId); 91 | var result = CreateToken(token.UserId, claims, _innerScheme); 92 | 93 | await _tokenStore.RemoveAsync(refreshToken, _innerScheme); 94 | 95 | if (_options.AllowMultiTokenActive && _options.KeepTokenValidTimeSpanOnRefresh != null) 96 | { 97 | var tokenList = await _tokenStore.GetListAsync(token.UserId, _innerScheme, TokenType.Bearer); 98 | var validTokenList = tokenList 99 | .Where(a => a.Expiration - DateTime.Now > _options.KeepTokenValidTimeSpanOnRefresh).ToList(); 100 | validTokenList.ForEach(a => a.Expiration = DateTime.Now + _options.KeepTokenValidTimeSpanOnRefresh.Value); 101 | foreach (var item in validTokenList) 102 | { 103 | await _cacheService.SetAsync(item); 104 | } 105 | 106 | await _tokenStore.UpdateListAsync(validTokenList); 107 | } 108 | 109 | await RemoveAllOldTokenAsync(token.UserId, _innerScheme); 110 | 111 | await _tokenStore.StoreAsync(new List() { result.Bearer, result.Refresh }); 112 | 113 | if (_options.UseCache) 114 | { 115 | await _cacheService.SetAsync(result.Bearer); 116 | } 117 | 118 | return result; 119 | } 120 | 121 | public virtual async Task RefreshClaimsAsync(string token, string scheme = null) 122 | { 123 | await InitializeAsync(scheme); 124 | var tokenModel = await _tokenStore.GetAsync(token, _innerScheme); 125 | if (tokenModel == null || tokenModel.Type != TokenType.Bearer) 126 | { 127 | return RefreshClaimsResult.Failed("invalid token"); 128 | } 129 | 130 | var claims = await GetUserClaimsAsync(tokenModel.UserId); 131 | tokenModel.Claims = claims; 132 | //Refresh db 133 | await _tokenStore.UpdateAsync(tokenModel); 134 | 135 | //Refresh cache 136 | if (_options.UseCache) 137 | { 138 | tokenModel.Claims = claims; 139 | await _cacheService.SetAsync(tokenModel); 140 | } 141 | 142 | return RefreshClaimsResult.Success(); 143 | } 144 | 145 | public virtual async Task RemoveAsync(string token, string scheme = null) 146 | { 147 | await InitializeAsync(scheme); 148 | var tokenModel = await _tokenStore.GetAsync(token, _innerScheme); 149 | if (tokenModel == null) 150 | { 151 | return; 152 | } 153 | 154 | //Remove from cache 155 | if (_options.UseCache) 156 | { 157 | await _cacheService.RemoveAsync(tokenModel.Value, _innerScheme); 158 | } 159 | 160 | //Remove from db 161 | await _tokenStore.RemoveAsync(token, _innerScheme); 162 | } 163 | 164 | private async Task> GetUserClaimsAsync(string userId) 165 | { 166 | var claims = await _profileService.GetUserClaimsAsync(userId); 167 | if (claims.All(a => a.Type != ApiTokenClaimTypes.Subject)) 168 | { 169 | claims.Add(new Claim(ApiTokenClaimTypes.Subject, userId)); 170 | } 171 | 172 | return claims; 173 | } 174 | 175 | private TokenCreateResult CreateToken(string userId, List claims, string scheme) 176 | { 177 | var bearerParam = $"{userId}_{scheme}_bearer"; 178 | var refreshParam = $"{userId}_{scheme}_refresh"; 179 | var now = DateTime.Now; 180 | var token = new TokenModel() 181 | { 182 | Value = ApiTokenTools.CreateToken(bearerParam), 183 | CreateTime = now, 184 | Type = TokenType.Bearer, 185 | UserId = userId, 186 | Claims = claims, 187 | Expiration = now + _options.TokenExpire, 188 | Scheme = scheme 189 | }; 190 | 191 | var refreshToken = new TokenModel() 192 | { 193 | Value = ApiTokenTools.CreateToken(refreshParam), 194 | CreateTime = now, 195 | Type = TokenType.Refresh, 196 | UserId = userId, 197 | Claims = null, 198 | Expiration = now + _options.RefreshTokenExpire, 199 | Scheme = scheme 200 | }; 201 | 202 | return TokenCreateResult.Success(token, refreshToken); 203 | } 204 | 205 | private async Task RemoveAllOldTokenAsync(string userId, string scheme) 206 | { 207 | if (!_options.AllowMultiTokenActive) 208 | { 209 | //Remove old token from cache 210 | if (_options.UseCache) 211 | { 212 | var tokenList = await _tokenStore.GetListAsync(userId, scheme, TokenType.Bearer); 213 | foreach (var token in tokenList) 214 | { 215 | await _cacheService.RemoveAsync(token.Value, scheme); 216 | } 217 | } 218 | 219 | //Remove old token from db 220 | await _tokenStore.RemoveListAsync(userId, scheme); 221 | } 222 | } 223 | } 224 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Store/Results/RefreshClaimsResult.cs: -------------------------------------------------------------------------------- 1 | namespace AspNetCore.Authentication.ApiToken.Results 2 | { 3 | public class RefreshClaimsResult : ResultBase 4 | { 5 | public static RefreshClaimsResult Success() => new RefreshClaimsResult() { Error = false }; 6 | public static RefreshClaimsResult Failed(string errorDescription) => new RefreshClaimsResult() { Error = true, ErrorDescription = errorDescription }; 7 | } 8 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Store/Results/ResultBase.cs: -------------------------------------------------------------------------------- 1 | namespace AspNetCore.Authentication.ApiToken.Results 2 | { 3 | public class ResultBase 4 | { 5 | public bool Error { get; set; } 6 | 7 | public string ErrorDescription { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Store/Results/TokenCreateResult.cs: -------------------------------------------------------------------------------- 1 | namespace AspNetCore.Authentication.ApiToken.Results 2 | { 3 | public class TokenCreateResult : ResultBase 4 | { 5 | public TokenModel Bearer { get; set; } 6 | public TokenModel Refresh { get; set; } 7 | 8 | public static TokenCreateResult Success(TokenModel token, TokenModel refreshToken) => new TokenCreateResult() { Error = false, Bearer = token, Refresh = refreshToken }; 9 | public static TokenCreateResult Failed(string errorDescription) => new TokenCreateResult() { Error = true, ErrorDescription = errorDescription }; 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Store/TokenModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Claims; 4 | 5 | namespace AspNetCore.Authentication.ApiToken 6 | { 7 | public class TokenModel 8 | { 9 | public string Value { get; set; } 10 | 11 | /// 12 | /// Bearer or Refresh 13 | /// 14 | public TokenType Type { get; set; } 15 | 16 | public string UserId { get; set; } 17 | 18 | public string Scheme { get; set; } 19 | 20 | public List Claims { get; set; } 21 | 22 | public DateTimeOffset CreateTime { get; set; } 23 | 24 | public DateTimeOffset Expiration { get; set; } 25 | 26 | public bool IsValid => (Expiration.UtcDateTime - DateTimeOffset.UtcNow).TotalMilliseconds > 0; 27 | 28 | public TimeSpan LifeTime=> Expiration.UtcDateTime - DateTimeOffset.UtcNow; 29 | 30 | } 31 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Store/TokenType.cs: -------------------------------------------------------------------------------- 1 | namespace AspNetCore.Authentication.ApiToken 2 | { 3 | public enum TokenType 4 | { 5 | Bearer=0, 6 | Refresh 7 | } 8 | } -------------------------------------------------------------------------------- /src/AspNetCore.AuthenticationApiToken/Validate/DefaultApiTokenValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Security.Claims; 3 | using System.Threading.Tasks; 4 | using AspNetCore.Authentication.ApiToken.Abstractions; 5 | using AspNetCore.Authentication.ApiToken.Exceptions; 6 | 7 | namespace AspNetCore.Authentication.ApiToken 8 | { 9 | public class DefaultApiTokenValidator : IApiTokenValidator 10 | { 11 | private readonly IApiTokenStore _store; 12 | private readonly IApiTokenCacheService _cacheService; 13 | 14 | public DefaultApiTokenValidator(IApiTokenStore store, IApiTokenCacheService cacheService) 15 | { 16 | _store = store; 17 | _cacheService = cacheService; 18 | } 19 | 20 | public virtual async Task ValidateTokenAsync(ApiTokenOptions options, [NotNull] string token, string schemeName) 21 | { 22 | TokenModel tokenModel = null; 23 | 24 | //Get from cache 25 | if (options.UseCache) 26 | { 27 | var tokenModelCache = await _cacheService.GetAsync(token, schemeName); 28 | if (tokenModelCache != null) 29 | { 30 | if (tokenModelCache.Available) 31 | { 32 | tokenModel = tokenModelCache.Token; 33 | } 34 | else 35 | { 36 | throw new TokenInvalidException("matching token could not be found from cache"); 37 | } 38 | } 39 | } 40 | 41 | //If cache return null, then get from db 42 | if (tokenModel == null) 43 | { 44 | var queryTokenModel = await _store.GetAsync(token, schemeName); 45 | if (queryTokenModel != null && queryTokenModel.IsValid) 46 | tokenModel = queryTokenModel; 47 | 48 | //set cache 49 | if (tokenModel != null && options.UseCache) 50 | { 51 | await _cacheService.SetAsync(tokenModel); 52 | } 53 | } 54 | 55 | if (tokenModel == null) 56 | { 57 | if (options.UseCache) 58 | { 59 | await _cacheService.SetNullAsync(token, schemeName); 60 | } 61 | 62 | throw new TokenInvalidException("matching token could not be found"); 63 | } 64 | 65 | //Check expiration 66 | if (!tokenModel.IsValid) 67 | { 68 | throw new TokenExpiredException("Token expired at " + tokenModel.Expiration, tokenModel.Expiration.DateTime); 69 | } 70 | 71 | //Generate ClaimsPrincipal 72 | var claims = tokenModel.Claims; 73 | 74 | var result = new ClaimsPrincipal(); 75 | result.AddIdentity(new ClaimsIdentity(claims, schemeName, options.NameClaimType, options.RoleClaimType)); 76 | 77 | return result; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /tests/AspNetCore.ApiToken.UnitTests/AspNetCore.ApiToken.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/AspNetCore.ApiToken.UnitTests/TokenTests.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.Authentication.ApiToken; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace AspNetCore.ApiToken.UnitTests 6 | { 7 | public class TokenTests 8 | { 9 | private readonly ITestOutputHelper _outputHelper; 10 | 11 | public TokenTests(ITestOutputHelper outputHelper) 12 | { 13 | _outputHelper = outputHelper; 14 | } 15 | 16 | [Fact] 17 | public void CreateToken() 18 | { 19 | var token = ApiTokenTools.CreateToken("1"); 20 | Assert.Equal(64, token.Length); 21 | _outputHelper.WriteLine(token); 22 | } 23 | } 24 | } 25 | --------------------------------------------------------------------------------