Skip to content

Commit ef9169c

Browse files
justinyooDoHoon Kim
authored andcommitted
Add authorisation feature to OpenApiHttpTriggerContext (Azure#255)
1 parent 39820ca commit ef9169c

File tree

26 files changed

+696
-205
lines changed

26 files changed

+696
-205
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// using System.Globalization;
2+
// using System.Linq;
3+
// using System.Net;
4+
// using System.Threading.Tasks;
5+
6+
// using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions;
7+
// using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Configurations;
8+
// using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Extensions;
9+
10+
// namespace Microsoft.Azure.WebJobs.Extensions.OpenApi.FunctionApp.V3Static.Configurations
11+
// {
12+
// public class OpenApiHttpTriggerAuthorization : DefaultOpenApiHttpTriggerAuthorization
13+
// {
14+
// public override async Task<OpenApiAuthorizationResult> AuthorizeAsync(IHttpRequestDataObject req)
15+
// {
16+
// var result = default(OpenApiAuthorizationResult);
17+
// var authtoken = (string) req.Headers["Authorization"];
18+
// if (authtoken.IsNullOrWhiteSpace())
19+
// {
20+
// result = new OpenApiAuthorizationResult()
21+
// {
22+
// StatusCode = HttpStatusCode.Unauthorized,
23+
// ContentType = "text/plain",
24+
// Payload = "Unauthorized",
25+
// };
26+
27+
// return await Task.FromResult(result).ConfigureAwait(false);
28+
// }
29+
30+
// if (authtoken.StartsWith("Bearer", ignoreCase: true, CultureInfo.InvariantCulture) == false)
31+
// {
32+
// result = new OpenApiAuthorizationResult()
33+
// {
34+
// StatusCode = HttpStatusCode.Unauthorized,
35+
// ContentType = "text/plain",
36+
// Payload = "Invalid auth format",
37+
// };
38+
39+
// return await Task.FromResult(result).ConfigureAwait(false);
40+
// }
41+
42+
// var token = authtoken.Split(' ').Last();
43+
// if (token != "secret")
44+
// {
45+
// result = new OpenApiAuthorizationResult()
46+
// {
47+
// StatusCode = HttpStatusCode.Forbidden,
48+
// ContentType = "text/plain",
49+
// Payload = "Invalid auth token",
50+
// };
51+
52+
// return await Task.FromResult(result).ConfigureAwait(false);
53+
// }
54+
55+
// return await Task.FromResult(result).ConfigureAwait(false);
56+
// }
57+
// }
58+
// }

src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/Extensions/OpenApiHostBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Functions;
2+
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions;
23
using Microsoft.Extensions.DependencyInjection;
34
using Microsoft.Extensions.Hosting;
45

src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/Extensions/OpenApiHttpRequestDataExtensions.cs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.Linq;
23

34
using Microsoft.AspNetCore.Http;
45
using Microsoft.AspNetCore.Http.Internal;
@@ -15,10 +16,44 @@ namespace Microsoft.Azure.Functions.Worker.Extensions.OpenApi.Extensions
1516
public static class OpenApiHttpRequestDataExtensions
1617
{
1718
/// <summary>
18-
/// Gets the <see cref="QueryCollection"/> instance from the <see cref="HttpRequestData"/>.
19+
/// Gets the <see cref="IHeaderDictionary"/> instance from the <see cref="HttpRequestData"/>.
1920
/// </summary>
2021
/// <param name="req"><see cref="HttpRequestData"/> instance.</param>
21-
/// <returns>Returns <see cref="QueryCollection"/> instance.</returns>
22+
/// <returns>Returns <see cref="IHeaderDictionary"/> instance.</returns>
23+
public static IHeaderDictionary Headers(this HttpRequestData req)
24+
{
25+
req.ThrowIfNullOrDefault();
26+
27+
var headers = req.Headers.ToDictionary(p => p.Key, p => new StringValues(p.Value.ToArray()));
28+
if (headers.IsNullOrDefault() || headers.Any() == false)
29+
{
30+
headers = new Dictionary<string, StringValues>();
31+
}
32+
33+
return new HeaderDictionary(headers);
34+
}
35+
36+
/// <summary>
37+
/// Gets the <see cref="StringValues"/> object from the header of <see cref="HttpRequestData"/>.
38+
/// </summary>
39+
/// <param name="req"><see cref="HttpRequestData"/> instance.</param>
40+
/// <param name="key">Header key.</param>
41+
/// <returns>Returns <see cref="StringValues"/> object.</returns>
42+
public static StringValues Header(this HttpRequestData req, string key)
43+
{
44+
req.ThrowIfNullOrDefault();
45+
46+
var headers = Headers(req);
47+
var value = headers.ContainsKey(key) ? headers[key] : new StringValues(default(string));
48+
49+
return value;
50+
}
51+
52+
/// <summary>
53+
/// Gets the <see cref="IQueryCollection"/> instance from the <see cref="HttpRequestData"/>.
54+
/// </summary>
55+
/// <param name="req"><see cref="HttpRequestData"/> instance.</param>
56+
/// <returns>Returns <see cref="IQueryCollection"/> instance.</returns>
2257
public static IQueryCollection Queries(this HttpRequestData req)
2358
{
2459
req.ThrowIfNullOrDefault();
@@ -33,7 +68,7 @@ public static IQueryCollection Queries(this HttpRequestData req)
3368
}
3469

3570
/// <summary>
36-
/// Gets the <see cref="StringValues"/> object from the <see cref="HttpRequestData"/>.
71+
/// Gets the <see cref="StringValues"/> object from the querystring of <see cref="HttpRequestData"/>.
3772
/// </summary>
3873
/// <param name="req"><see cref="HttpRequestData"/> instance.</param>
3974
/// <param name="key">Querystring key.</param>

src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/Functions/OpenApiTriggerFunction.cs

Lines changed: 81 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Threading.Tasks;
55

66
using Microsoft.Azure.Functions.Worker.Http;
7+
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions;
78
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Extensions;
89
using Microsoft.Extensions.Logging;
910

@@ -37,20 +38,34 @@ public async Task<HttpResponseData> RenderSwaggerDocument(HttpRequestData req, s
3738
log.LogInformation($"swagger.{extension} was requested.");
3839

3940
var fi = new FileInfo(ctx.FunctionDefinition.PathToAssembly);
41+
var request = new HttpRequestObject(req);
4042
var result = default(string);
4143
var response = default(HttpResponseData);
4244
try
4345
{
44-
result = await (await this._context.SetApplicationAssemblyAsync(fi.Directory.FullName, appendBin: false))
45-
.Document
46-
.InitialiseDocument()
47-
.AddMetadata(this._context.OpenApiConfigurationOptions.Info)
48-
.AddServer(new HttpRequestObject(req), this._context.HttpSettings.RoutePrefix, this._context.OpenApiConfigurationOptions)
49-
.AddNamingStrategy(this._context.NamingStrategy)
50-
.AddVisitors(this._context.GetVisitorCollection())
51-
.Build(this._context.ApplicationAssembly, this._context.OpenApiConfigurationOptions.OpenApiVersion)
52-
.RenderAsync(this._context.GetOpenApiSpecVersion(this._context.OpenApiConfigurationOptions.OpenApiVersion), this._context.GetOpenApiFormat(extension))
53-
.ConfigureAwait(false);
46+
var auth = await this._context
47+
.SetApplicationAssemblyAsync(fi.Directory.FullName, appendBin: false)
48+
.AuthorizeAsync(request)
49+
.ConfigureAwait(false);
50+
if (!auth.IsNullOrDefault())
51+
{
52+
response = req.CreateResponse(auth.StatusCode);
53+
response.Headers.Add("Content-Type", auth.ContentType);
54+
await response.WriteStringAsync(auth.Payload).ConfigureAwait(false);
55+
56+
return response;
57+
}
58+
59+
result = await this._context
60+
.Document
61+
.InitialiseDocument()
62+
.AddMetadata(this._context.OpenApiConfigurationOptions.Info)
63+
.AddServer(request, this._context.HttpSettings.RoutePrefix, this._context.OpenApiConfigurationOptions)
64+
.AddNamingStrategy(this._context.NamingStrategy)
65+
.AddVisitors(this._context.GetVisitorCollection())
66+
.Build(this._context.ApplicationAssembly, this._context.OpenApiConfigurationOptions.OpenApiVersion)
67+
.RenderAsync(this._context.GetOpenApiSpecVersion(this._context.OpenApiConfigurationOptions.OpenApiVersion), this._context.GetOpenApiFormat(extension))
68+
.ConfigureAwait(false);
5469

5570
response = req.CreateResponse(HttpStatusCode.OK);
5671
response.Headers.Add("Content-Type", this._context.GetOpenApiFormat(extension).GetContentType());
@@ -82,20 +97,34 @@ public async Task<HttpResponseData> RenderOpenApiDocument(HttpRequestData req, s
8297
log.LogInformation($"{version}.{extension} was requested.");
8398

8499
var fi = new FileInfo(ctx.FunctionDefinition.PathToAssembly);
100+
var request = new HttpRequestObject(req);
85101
var result = default(string);
86102
var response = default(HttpResponseData);
87103
try
88104
{
89-
result = await (await this._context.SetApplicationAssemblyAsync(fi.Directory.FullName, appendBin: false))
90-
.Document
91-
.InitialiseDocument()
92-
.AddMetadata(this._context.OpenApiConfigurationOptions.Info)
93-
.AddServer(new HttpRequestObject(req), this._context.HttpSettings.RoutePrefix, this._context.OpenApiConfigurationOptions)
94-
.AddNamingStrategy(this._context.NamingStrategy)
95-
.AddVisitors(this._context.GetVisitorCollection())
96-
.Build(this._context.ApplicationAssembly, this._context.GetOpenApiVersionType(version))
97-
.RenderAsync(this._context.GetOpenApiSpecVersion(version), this._context.GetOpenApiFormat(extension))
98-
.ConfigureAwait(false);
105+
var auth = await this._context
106+
.SetApplicationAssemblyAsync(fi.Directory.FullName, appendBin: false)
107+
.AuthorizeAsync(request)
108+
.ConfigureAwait(false);
109+
if (!auth.IsNullOrDefault())
110+
{
111+
response = req.CreateResponse(auth.StatusCode);
112+
response.Headers.Add("Content-Type", auth.ContentType);
113+
await response.WriteStringAsync(auth.Payload).ConfigureAwait(false);
114+
115+
return response;
116+
}
117+
118+
result = await this._context
119+
.Document
120+
.InitialiseDocument()
121+
.AddMetadata(this._context.OpenApiConfigurationOptions.Info)
122+
.AddServer(request, this._context.HttpSettings.RoutePrefix, this._context.OpenApiConfigurationOptions)
123+
.AddNamingStrategy(this._context.NamingStrategy)
124+
.AddVisitors(this._context.GetVisitorCollection())
125+
.Build(this._context.ApplicationAssembly, this._context.GetOpenApiVersionType(version))
126+
.RenderAsync(this._context.GetOpenApiSpecVersion(version), this._context.GetOpenApiFormat(extension))
127+
.ConfigureAwait(false);
99128

100129
response = req.CreateResponse(HttpStatusCode.OK);
101130
response.Headers.Add("Content-Type", this._context.GetOpenApiFormat(extension).GetContentType());
@@ -126,17 +155,31 @@ public async Task<HttpResponseData> RenderSwaggerUI(HttpRequestData req, Functio
126155
log.LogInformation("SwaggerUI page was requested.");
127156

128157
var fi = new FileInfo(ctx.FunctionDefinition.PathToAssembly);
158+
var request = new HttpRequestObject(req);
129159
var result = default(string);
130160
var response = default(HttpResponseData);
131161
try
132162
{
133-
result = await (await this._context.SetApplicationAssemblyAsync(fi.Directory.FullName, appendBin: false))
134-
.SwaggerUI
135-
.AddMetadata(this._context.OpenApiConfigurationOptions.Info)
136-
.AddServer(new HttpRequestObject(req), this._context.HttpSettings.RoutePrefix, this._context.OpenApiConfigurationOptions)
137-
.BuildAsync(this._context.PackageAssembly, this._context.OpenApiCustomUIOptions)
138-
.RenderAsync("swagger.json", this._context.GetDocumentAuthLevel(), this._context.GetSwaggerAuthKey())
139-
.ConfigureAwait(false);
163+
var auth = await this._context
164+
.SetApplicationAssemblyAsync(fi.Directory.FullName, appendBin: false)
165+
.AuthorizeAsync(request)
166+
.ConfigureAwait(false);
167+
if (!auth.IsNullOrDefault())
168+
{
169+
response = req.CreateResponse(auth.StatusCode);
170+
response.Headers.Add("Content-Type", auth.ContentType);
171+
await response.WriteStringAsync(auth.Payload).ConfigureAwait(false);
172+
173+
return response;
174+
}
175+
176+
result = await this._context
177+
.SwaggerUI
178+
.AddMetadata(this._context.OpenApiConfigurationOptions.Info)
179+
.AddServer(request, this._context.HttpSettings.RoutePrefix, this._context.OpenApiConfigurationOptions)
180+
.BuildAsync(this._context.PackageAssembly, this._context.OpenApiCustomUIOptions)
181+
.RenderAsync("swagger.json", this._context.GetDocumentAuthLevel(), this._context.GetSwaggerAuthKey())
182+
.ConfigureAwait(false);
140183

141184
response = req.CreateResponse(HttpStatusCode.OK);
142185
response.Headers.Add("Content-Type", ContentTypeHtml);
@@ -167,16 +210,21 @@ public async Task<HttpResponseData> RenderOAuth2Redirect(HttpRequestData req, Fu
167210
log.LogInformation("The oauth2-redirect.html page was requested.");
168211

169212
var fi = new FileInfo(ctx.FunctionDefinition.PathToAssembly);
213+
var request = new HttpRequestObject(req);
170214
var result = default(string);
171215
var response = default(HttpResponseData);
172216
try
173217
{
174-
result = await (await this._context.SetApplicationAssemblyAsync(fi.Directory.FullName, appendBin: false))
175-
.SwaggerUI
176-
.AddServer(new HttpRequestObject(req), this._context.HttpSettings.RoutePrefix, this._context.OpenApiConfigurationOptions)
177-
.BuildOAuth2RedirectAsync(this._context.PackageAssembly)
178-
.RenderOAuth2RedirectAsync("oauth2-redirect.html", this._context.GetDocumentAuthLevel(), this._context.GetSwaggerAuthKey())
179-
.ConfigureAwait(false);
218+
await this._context
219+
.SetApplicationAssemblyAsync(fi.Directory.FullName, appendBin: false)
220+
.ConfigureAwait(false);
221+
222+
result = await this._context
223+
.SwaggerUI
224+
.AddServer(request, this._context.HttpSettings.RoutePrefix, this._context.OpenApiConfigurationOptions)
225+
.BuildOAuth2RedirectAsync(this._context.PackageAssembly)
226+
.RenderOAuth2RedirectAsync("oauth2-redirect.html", this._context.GetDocumentAuthLevel(), this._context.GetSwaggerAuthKey())
227+
.ConfigureAwait(false);
180228

181229
response = req.CreateResponse(HttpStatusCode.OK);
182230
response.Headers.Add("Content-Type", ContentTypeHtml);

src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/HttpRequestObject.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public HttpRequestObject(HttpRequestData req)
2727
? new HostString(req.Url.Authority)
2828
: new HostString(req.Url.Host, req.Url.Port);
2929

30+
this.Headers = req.Headers();
3031
this.Query = req.Queries();
3132
this.Body = req.Body;
3233
}
@@ -37,6 +38,9 @@ public HttpRequestObject(HttpRequestData req)
3738
/// <inheritdoc/>
3839
public virtual HostString Host { get; }
3940

41+
/// <inheritdoc/>
42+
public virtual IHeaderDictionary Headers { get; }
43+
4044
/// <inheritdoc/>
4145
public virtual IQueryCollection Query { get;}
4246

src/Microsoft.Azure.Functions.Worker.Extensions.OpenApi/OpenApiHttpTriggerContext.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,24 @@ public virtual async Task<IOpenApiHttpTriggerContext> SetApplicationAssemblyAsyn
140140
return this;
141141
}
142142

143+
/// <inheritdoc />
144+
public virtual async Task<OpenApiAuthorizationResult> AuthorizeAsync(IHttpRequestDataObject req)
145+
{
146+
var result = default(OpenApiAuthorizationResult);
147+
var type = this.ApplicationAssembly
148+
.GetLoadableTypes()
149+
.SingleOrDefault(p => p.HasInterface<IOpenApiHttpTriggerAuthorization>());
150+
if (type.IsNullOrDefault())
151+
{
152+
return result;
153+
}
154+
155+
var auth = Activator.CreateInstance(type) as IOpenApiHttpTriggerAuthorization;
156+
result = await auth.AuthorizeAsync(req).ConfigureAwait(false);
157+
158+
return result;
159+
}
160+
143161
/// <inheritdoc />
144162
public virtual VisitorCollection GetVisitorCollection()
145163
{

src/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core/Abstractions/IHttpRequestDataObject.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@ public interface IHttpRequestDataObject
1919
/// </summary>
2020
HostString Host { get; }
2121

22+
/// <summary>
23+
/// Gets the header collection.
24+
/// </summary>
25+
IHeaderDictionary Headers { get; }
26+
2227
/// <summary>
2328
/// Gets the query collection.
2429
/// </summary>
25-
IQueryCollection Query { get;}
30+
IQueryCollection Query { get; }
2631

2732
/// <summary>
2833
/// Gets the request payload stream.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Threading.Tasks;
2+
3+
using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Configurations;
4+
5+
namespace Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions
6+
{
7+
/// <summary>
8+
/// This provides interfaces to HTTP trigger authorisation for OpenAPI endpoints.
9+
/// </summary>
10+
public interface IOpenApiHttpTriggerAuthorization
11+
{
12+
/// <summary>
13+
/// Authorizes the endpoint.
14+
/// </summary>
15+
/// <param name="req"><see cref="IHttpRequestDataObject"/> instance.</param>
16+
/// <returns>Returns <see cref="OpenApiAuthorizationResult"/> instance.</returns>
17+
Task<OpenApiAuthorizationResult> AuthorizeAsync(IHttpRequestDataObject req);
18+
}
19+
}

0 commit comments

Comments
 (0)