├── src
├── host.json
├── local.settings.json
├── Constants.cs
├── AzFunctionsJwtAuth.csproj
├── AppStartup.cs
├── SampleAuthorizedService.cs
├── AzFunctionsJwtAuth.sln
├── TokenIssuer.cs
├── AuthorizedServiceBase.cs
├── AuthenticationInfo.cs
└── AuthenticationService.cs
├── README.md
├── LICENSE
└── .gitignore
/src/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0"
3 | }
--------------------------------------------------------------------------------
/src/local.settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "IsEncrypted": false,
3 | "Values": {
4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true",
5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet"
6 | }
7 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AzFunctionsJwtAuth
2 | Skeleton code for performing authentication and authorization with custom schemes and JWT in Azure Functions
3 |
4 | Check out https://www.charliedigital.com/2020/05/24/azure-functions-with-jwt-authentication
5 |
--------------------------------------------------------------------------------
/src/Constants.cs:
--------------------------------------------------------------------------------
1 | namespace AzFunctionsJwtAuth
2 | {
3 | public static class Constants
4 | {
5 | ///
6 | /// You can put this secret key in your config instead or read it from Key Vault
7 | ///
8 | public static readonly string SECRET_KEY = "YOUR_SECRETY_KEY_JUST_A_LONG_STRING";
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/AzFunctionsJwtAuth.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp3.0
4 | v3
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | PreserveNewest
14 |
15 |
16 | PreserveNewest
17 | Never
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/AppStartup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Functions.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | [assembly: FunctionsStartup(typeof(AzFunctionsJwtAuth.AppStartup))]
5 |
6 | namespace AzFunctionsJwtAuth
7 | {
8 | ///
9 | /// Startup class used to initialize the dependency injection.
10 | ///
11 | ///
12 | /// See: https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection
13 | ///
14 | public class AppStartup : FunctionsStartup
15 | {
16 | ///
17 | /// Configure the DI container.
18 | ///
19 | public override void Configure(IFunctionsHostBuilder builder)
20 | {
21 | // Intject the token service.
22 | builder.Services.AddSingleton();
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/SampleAuthorizedService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Microsoft.Azure.WebJobs;
5 | using Microsoft.Azure.WebJobs.Extensions.Http;
6 | using Microsoft.Extensions.Logging;
7 |
8 | namespace AzFunctionsJwtAuth
9 | {
10 | ///
11 | /// Just a sample service which is fully authorized.
12 | ///
13 | public class SampleAuthorizedService : AuthorizedServiceBase
14 | {
15 | [FunctionName("AuthorizedEcho")]
16 | public async Task AuthorizedEcho(
17 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "authorizedecho/{message}")]
18 | HttpRequest req,
19 | string message,
20 | ILogger log)
21 | {
22 | return new OkObjectResult($"{Auth.Username} sent message {message}");
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 CharlieDigital
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/AzFunctionsJwtAuth.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30002.166
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzFunctionsJwtAuth", "AzFunctionsJwtAuth.csproj", "{B36078ED-4386-48A4-B78F-230AAA86984F}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {B36078ED-4386-48A4-B78F-230AAA86984F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {B36078ED-4386-48A4-B78F-230AAA86984F}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {B36078ED-4386-48A4-B78F-230AAA86984F}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {B36078ED-4386-48A4-B78F-230AAA86984F}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {8D1F7E35-E369-4DF1-82DD-F3007003C81B}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/src/TokenIssuer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using JWT;
3 | using JWT.Algorithms;
4 | using JWT.Serializers;
5 |
6 | namespace AzFunctionsJwtAuth
7 | {
8 | ///
9 | /// Wrapper class for encapsulating the token issuance logic.
10 | ///
11 | public class TokenIssuer
12 | {
13 | private readonly IJwtAlgorithm _algorithm;
14 | private readonly IJsonSerializer _serializer;
15 | private readonly IBase64UrlEncoder _base64Encoder;
16 | private readonly IJwtEncoder _jwtEncoder;
17 |
18 | public TokenIssuer()
19 | {
20 | // JWT specific initialization.
21 | // https://github.com/jwt-dotnet/jwt
22 | _algorithm = new HMACSHA256Algorithm();
23 | _serializer = new JsonNetSerializer();
24 | _base64Encoder = new JwtBase64UrlEncoder();
25 | _jwtEncoder = new JwtEncoder(_algorithm, _serializer, _base64Encoder);
26 | }
27 |
28 | ///
29 | /// This method is intended to be the main entry point for generation of the JWT.
30 | ///
31 | /// The user that the token is being issued for.
32 | /// A JWT token which can be returned to the user.
33 | public string IssueTokenForUser(Credentials credentials)
34 | {
35 | // Instead of returning a string, we'll return the JWT with a set of claims about the user
36 | Dictionary claims = new Dictionary
37 | {
38 | // JSON representation of the user Reference with ID and display name
39 | { "username", credentials.User },
40 |
41 | // TODO: Add other claims here as necessary; maybe from a user database
42 | { "role", "admin"}
43 | };
44 |
45 | string token = _jwtEncoder.Encode(claims, Constants.SECRET_KEY);
46 |
47 | return token;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/AuthorizedServiceBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Security.Authentication;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.Http;
8 | using Microsoft.Azure.WebJobs.Host;
9 |
10 | namespace AzFunctionsJwtAuth
11 | {
12 | ///
13 | /// Base class for authenticated service which checks the incoming JWT token.
14 | ///
15 | public abstract class AuthorizedServiceBase : IFunctionInvocationFilter
16 | {
17 | private const string AuthenticationHeaderName = "Authorization";
18 |
19 | // Access the authentication info.
20 | protected AuthenticationInfo Auth { get; private set; }
21 |
22 | ///
23 | /// Pre-execution filter.
24 | ///
25 | ///
26 | /// This mechanism can be used to extract the authentication information. Unfortunately, the binding in SignalRConnectionInfoAttribute
27 | /// does not pick this up from the headers even when bound.
28 | ///
29 | public Task OnExecutingAsync(FunctionExecutingContext executingContext, CancellationToken cancellationToken)
30 | {
31 | HttpRequest message = executingContext.Arguments.First().Value as HttpRequest;
32 |
33 | if (message == null || !message.Headers.ContainsKey(AuthenticationHeaderName))
34 | {
35 | return Task.FromException(new AuthenticationException("No Authorization header was present"));
36 | }
37 |
38 | try
39 | {
40 | Auth = new AuthenticationInfo(message);
41 | }
42 | catch (Exception exception)
43 | {
44 | return Task.FromException(exception);
45 | }
46 |
47 | if (!Auth.IsValid)
48 | {
49 | return Task.FromException(new KeyNotFoundException("No identity key was found in the claims."));
50 | }
51 |
52 | return Task.CompletedTask;
53 | }
54 |
55 | ///
56 | /// Post-execution filter.
57 | ///
58 | public Task OnExecutedAsync(FunctionExecutedContext executedContext, CancellationToken cancellationToken)
59 | {
60 | // Nothing.
61 | return Task.CompletedTask;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/AuthenticationInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using JWT.Algorithms;
4 | using JWT.Builder;
5 | using Microsoft.AspNetCore.Http;
6 |
7 | namespace AzFunctionsJwtAuth
8 | {
9 | ///
10 | /// Wrapper class for encapsulating claims parsing.
11 | ///
12 | public class AuthenticationInfo
13 | {
14 | public bool IsValid { get; }
15 | public string Username { get; }
16 | public string Role { get; }
17 |
18 | public AuthenticationInfo(HttpRequest request)
19 | {
20 | // Check if we have a header.
21 | if (!request.Headers.ContainsKey("Authorization"))
22 | {
23 | IsValid = false;
24 |
25 | return;
26 | }
27 |
28 | string authorizationHeader = request.Headers["Authorization"];
29 |
30 | // Check if the value is empty.
31 | if (string.IsNullOrEmpty(authorizationHeader))
32 | {
33 | IsValid = false;
34 |
35 | return;
36 | }
37 |
38 | // Check if we can decode the header.
39 | IDictionary claims = null;
40 |
41 | try
42 | {
43 | if (authorizationHeader.StartsWith("Bearer"))
44 | {
45 | authorizationHeader = authorizationHeader.Substring(7);
46 | }
47 |
48 | // Validate the token and decode the claims.
49 | claims = new JwtBuilder()
50 | .WithAlgorithm(new HMACSHA256Algorithm())
51 | .WithSecret(Constants.SECRET_KEY)
52 | .MustVerifySignature()
53 | .Decode>(authorizationHeader);
54 | }
55 | catch (Exception exception)
56 | {
57 | IsValid = false;
58 |
59 | return;
60 | }
61 |
62 | // Check if we have user claim.
63 | if (!claims.ContainsKey("username"))
64 | {
65 | IsValid = false;
66 |
67 | return;
68 | }
69 |
70 | IsValid = true;
71 | Username = Convert.ToString(claims["username"]);
72 | Role = Convert.ToString(claims["role"]);
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/AuthenticationService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Http;
4 | using Microsoft.AspNetCore.Mvc;
5 | using Microsoft.Azure.WebJobs;
6 | using Microsoft.Azure.WebJobs.Extensions.Http;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace AzFunctionsJwtAuth
10 | {
11 | ///
12 | /// Service class for performing authentication.
13 | ///
14 | public class AuthenticationService
15 | {
16 | private readonly TokenIssuer _tokenIssuer;
17 |
18 | ///
19 | /// Injection constructor.
20 | ///
21 | /// DI injected token issuer singleton.
22 | public AuthenticationService(TokenIssuer tokenIssuer)
23 | {
24 | _tokenIssuer = tokenIssuer;
25 | }
26 |
27 | [FunctionName("Authenticate")]
28 | public async Task Authenticate(
29 | // https://stackoverflow.com/a/52748884/116051
30 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth")]
31 | Credentials credentials,
32 | ILogger log)
33 | {
34 | // Perform custom authentication here
35 | bool authenticated = credentials?.User.Equals("charles", StringComparison.InvariantCultureIgnoreCase) ?? false;
36 |
37 | if (!authenticated)
38 | {
39 | return new UnauthorizedResult();
40 | }
41 |
42 | return new OkObjectResult(_tokenIssuer.IssueTokenForUser(credentials));
43 | }
44 |
45 | [FunctionName("ChangePassword")]
46 | public async Task ChangePassword(
47 | // https://stackoverflow.com/a/52748884/116051
48 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "changepassword")]
49 | HttpRequest req, // Note: we need the underlying request to get the header
50 | ILogger log)
51 | {
52 | // Check if we have authentication info.
53 | AuthenticationInfo auth = new AuthenticationInfo(req);
54 |
55 | if (!auth.IsValid)
56 | {
57 | return new UnauthorizedResult(); // No authentication info.
58 | }
59 |
60 | string newPassword = await req.ReadAsStringAsync();
61 |
62 | return new OkObjectResult($"{auth.Username} changed password to {newPassword}");
63 | }
64 | }
65 |
66 | ///
67 | /// DTO for transferring the auth info.
68 | ///
69 | public class Credentials
70 | {
71 | public string User { get; set; }
72 |
73 | public string Password { get; set; }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # Azure Functions localsettings file
5 | local.settings.json
6 |
7 | # User-specific files
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 | bld/
24 | [Bb]in/
25 | [Oo]bj/
26 | [Ll]og/
27 |
28 | # Visual Studio 2015 cache/options directory
29 | .vs/
30 | # Uncomment if you have tasks that create the project's static files in wwwroot
31 | #wwwroot/
32 |
33 | # MSTest test Results
34 | [Tt]est[Rr]esult*/
35 | [Bb]uild[Ll]og.*
36 |
37 | # NUNIT
38 | *.VisualState.xml
39 | TestResult.xml
40 |
41 | # Build Results of an ATL Project
42 | [Dd]ebugPS/
43 | [Rr]eleasePS/
44 | dlldata.c
45 |
46 | # DNX
47 | project.lock.json
48 | project.fragment.lock.json
49 | artifacts/
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # NCrunch
117 | _NCrunch_*
118 | .*crunch*.local.xml
119 | nCrunchTemp_*
120 |
121 | # MightyMoose
122 | *.mm.*
123 | AutoTest.Net/
124 |
125 | # Web workbench (sass)
126 | .sass-cache/
127 |
128 | # Installshield output folder
129 | [Ee]xpress/
130 |
131 | # DocProject is a documentation generator add-in
132 | DocProject/buildhelp/
133 | DocProject/Help/*.HxT
134 | DocProject/Help/*.HxC
135 | DocProject/Help/*.hhc
136 | DocProject/Help/*.hhk
137 | DocProject/Help/*.hhp
138 | DocProject/Help/Html2
139 | DocProject/Help/html
140 |
141 | # Click-Once directory
142 | publish/
143 |
144 | # Publish Web Output
145 | *.[Pp]ublish.xml
146 | *.azurePubxml
147 | # TODO: Comment the next line if you want to checkin your web deploy settings
148 | # but database connection strings (with potential passwords) will be unencrypted
149 | #*.pubxml
150 | *.publishproj
151 |
152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
153 | # checkin your Azure Web App publish settings, but sensitive information contained
154 | # in these scripts will be unencrypted
155 | PublishScripts/
156 |
157 | # NuGet Packages
158 | *.nupkg
159 | # The packages folder can be ignored because of Package Restore
160 | **/packages/*
161 | # except build/, which is used as an MSBuild target.
162 | !**/packages/build/
163 | # Uncomment if necessary however generally it will be regenerated when needed
164 | #!**/packages/repositories.config
165 | # NuGet v3's project.json files produces more ignoreable files
166 | *.nuget.props
167 | *.nuget.targets
168 |
169 | # Microsoft Azure Build Output
170 | csx/
171 | *.build.csdef
172 |
173 | # Microsoft Azure Emulator
174 | ecf/
175 | rcf/
176 |
177 | # Windows Store app package directories and files
178 | AppPackages/
179 | BundleArtifacts/
180 | Package.StoreAssociation.xml
181 | _pkginfo.txt
182 |
183 | # Visual Studio cache files
184 | # files ending in .cache can be ignored
185 | *.[Cc]ache
186 | # but keep track of directories ending in .cache
187 | !*.[Cc]ache/
188 |
189 | # Others
190 | ClientBin/
191 | ~$*
192 | *~
193 | *.dbmdl
194 | *.dbproj.schemaview
195 | *.jfm
196 | *.pfx
197 | *.publishsettings
198 | node_modules/
199 | orleans.codegen.cs
200 |
201 | # Since there are multiple workflows, uncomment next line to ignore bower_components
202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
203 | #bower_components/
204 |
205 | # RIA/Silverlight projects
206 | Generated_Code/
207 |
208 | # Backup & report files from converting an old project file
209 | # to a newer Visual Studio version. Backup files are not needed,
210 | # because we have git ;-)
211 | _UpgradeReport_Files/
212 | Backup*/
213 | UpgradeLog*.XML
214 | UpgradeLog*.htm
215 |
216 | # SQL Server files
217 | *.mdf
218 | *.ldf
219 |
220 | # Business Intelligence projects
221 | *.rdl.data
222 | *.bim.layout
223 | *.bim_*.settings
224 |
225 | # Microsoft Fakes
226 | FakesAssemblies/
227 |
228 | # GhostDoc plugin setting file
229 | *.GhostDoc.xml
230 |
231 | # Node.js Tools for Visual Studio
232 | .ntvs_analysis.dat
233 |
234 | # Visual Studio 6 build log
235 | *.plg
236 |
237 | # Visual Studio 6 workspace options file
238 | *.opt
239 |
240 | # Visual Studio LightSwitch build output
241 | **/*.HTMLClient/GeneratedArtifacts
242 | **/*.DesktopClient/GeneratedArtifacts
243 | **/*.DesktopClient/ModelManifest.xml
244 | **/*.Server/GeneratedArtifacts
245 | **/*.Server/ModelManifest.xml
246 | _Pvt_Extensions
247 |
248 | # Paket dependency manager
249 | .paket/paket.exe
250 | paket-files/
251 |
252 | # FAKE - F# Make
253 | .fake/
254 |
255 | # JetBrains Rider
256 | .idea/
257 | *.sln.iml
258 |
259 | # CodeRush
260 | .cr/
261 |
262 | # Python Tools for Visual Studio (PTVS)
263 | __pycache__/
264 | *.pyc
--------------------------------------------------------------------------------