Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Update static files sample to use AuthorizationMiddleware #65

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build/dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
<MicrosoftAspNetCoreAuthenticationGooglePackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAuthenticationGooglePackageVersion>
<MicrosoftAspNetCoreAuthenticationOpenIdConnectPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAuthenticationOpenIdConnectPackageVersion>
<MicrosoftAspNetCoreAuthenticationTwitterPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAuthenticationTwitterPackageVersion>
<MicrosoftAspNetCoreAuthorizationPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAuthorizationPackageVersion>
<MicrosoftAspNetCoreAuthorizationPackageVersion>3.0.0-a-alpha1-authz-middleware-16948</MicrosoftAspNetCoreAuthorizationPackageVersion>
<MicrosoftAspNetCoreAuthorizationPolicyPackageVersion>3.0.0-a-alpha1-authz-middleware-16948</MicrosoftAspNetCoreAuthorizationPolicyPackageVersion>
<MicrosoftAspNetCoreCryptographyKeyDerivationPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreCryptographyKeyDerivationPackageVersion>
<MicrosoftAspNetCoreDataProtectionExtensionsPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreDataProtectionExtensionsPackageVersion>
<MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options)

public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();

public Task<AuthorizationPolicy> GetRequiredPolicyAsync() => FallbackPolicyProvider.GetRequiredPolicyAsync();

// Policies are looked up by string name, so expect 'parameters' (like age)
// to be embedded in the policy names. This is abstracted away from developers
// by the more strongly-typed attributes derived from AuthorizeAttribute
Expand Down
1 change: 1 addition & 0 deletions samples/CustomPolicyProvider/CustomPolicyProvider.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<NoWarn>NU1605</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions samples/DynamicSchemes/DynamicSchemes.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.0;net461</TargetFrameworks>
<NoWarn>NU1605</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="$(MicrosoftAspNetCorePackageVersion)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netcoreapp3.0;net461</TargetFrameworks>
<UserSecretsId>aspnet-Identity.ExternalClaims-E95BE154-CB1B-4633-A2E0-B2DF12FE8BD3</UserSecretsId>
<NoWarn>NU1605</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="$(MicrosoftAspNetCorePackageVersion)" />
Expand Down
174 changes: 89 additions & 85 deletions samples/StaticFilesAuth/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -49,27 +53,36 @@ public void ConfigureServices(IServiceCollection services)
{
return false;
}
var userPath = Path.Combine(usersPath, userName);
if (context.Resource is IFileInfo file)
if (context.Resource is Endpoint endpoint)
{
var path = Path.GetDirectoryName(file.PhysicalPath);
return string.Equals(path, basePath, StringComparison.OrdinalIgnoreCase)
|| string.Equals(path, usersPath, StringComparison.OrdinalIgnoreCase)
|| string.Equals(path, userPath, StringComparison.OrdinalIgnoreCase)
|| path.StartsWith(userPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
}
else if (context.Resource is IDirectoryContents dir)
{
// https://github.com/aspnet/Home/issues/3073
// This won't work right if the directory is empty
var path = Path.GetDirectoryName(dir.First().PhysicalPath);
return string.Equals(path, basePath, StringComparison.OrdinalIgnoreCase)
|| string.Equals(path, usersPath, StringComparison.OrdinalIgnoreCase)
|| string.Equals(path, userPath, StringComparison.OrdinalIgnoreCase)
|| path.StartsWith(userPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
var userPath = Path.Combine(usersPath, userName);

var file = endpoint.Metadata.GetMetadata<IFileInfo>();
if (file != null)
{
var path = Path.GetDirectoryName(file.PhysicalPath);
return string.Equals(path, basePath, StringComparison.OrdinalIgnoreCase)
|| string.Equals(path, usersPath, StringComparison.OrdinalIgnoreCase)
|| string.Equals(path, userPath, StringComparison.OrdinalIgnoreCase)
|| path.StartsWith(userPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
}

var dir = endpoint.Metadata.GetMetadata<IDirectoryContents>();
if (dir != null)
{
// https://github.com/aspnet/Home/issues/3073
// This won't work right if the directory is empty
var path = Path.GetDirectoryName(dir.First().PhysicalPath);
return string.Equals(path, basePath, StringComparison.OrdinalIgnoreCase)
|| string.Equals(path, usersPath, StringComparison.OrdinalIgnoreCase)
|| string.Equals(path, userPath, StringComparison.OrdinalIgnoreCase)
|| path.StartsWith(userPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
}

throw new InvalidOperationException($"Missing file system metadata.");
}

throw new NotImplementedException($"Unknown resource type '{context.Resource.GetType()}'");
throw new InvalidOperationException($"Unknown resource type '{context.Resource.GetType()}'");
});
});
});
Expand All @@ -96,16 +109,27 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, IAuthori

var files = new PhysicalFileProvider(Path.Combine(env.ContentRootPath, "PrivateFiles"));

app.Map("/MapAuthenticatedFiles", branch =>
app.Use(async (context, next) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This two stage process seems unnecessary.

app.Map("/MapImperativeFiles", branch => {
  branch.Use((context, next) => { SetFileEndpoint(context, files, "files"); return next(); })
  branch.UseAuthorization();
  SetupFileServer(branch, files);
});
app.Map("/MapAuthenticatedFiles", branch => {
  branch.Use((context, next) => { SetFileEndpoint(context, files, null); return next(); })
  branch.UseAuthorization();
  SetupFileServer(branch, files);
});

Which could be consolidated to:

void MapFileAuth(IApplicationBuilder app, string path, IFileProvider files, string policy)
{
app.Map(path, branch => {
  branch.Use((context, next) => { SetFileEndpoint(context, files, policy); return next(); })
  branch.UseAuthorization();
  SetupFileServer(branch, files);
});
}

MapFileAuth(app, "/MapImperativeFiles", files, "files");
MapFileAuth(app, "/MapAuthenticatedFiles", files, null);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm, I prefer the current way it is working. There is a single point of authorization for all requests.

Do you not the process of calculating and setting an endpoint for a file? After I updated this sample I raised an issue about making this more of a first class citizen here - https://github.com/aspnet/Routing/issues/903

I'd like it to look something like:

// For files sitting in wwwroot
app.UseStaticFiles();

app.UseEndpointRouting(routes =>
{
    // MVC
    routes.MapController(name: "default", template: "{controller=Home}/{action=Index}/{id?}");

    // Static file endpoints for files in non-standard locations
    routes.MapStaticFiles("/MapImperativeFiles").RequireAuthorization("files");
    routes.MapStaticFiles("/MapAuthenticatedFiles").RequireAuthorization();
});

app.UseAuthorization();

I don't think this sample would be able to become quite this clean. The branching, and different URLs pointed at the same files makes things messy. Have you considered not having branching in this sample? I think it would make it more real world if there was a set of directories that required auth, and another set that required auth from a specific person.

// @rynowak

Copy link
Member

@Tratcher Tratcher Nov 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a single UseAuthorization per branch makes sense, but don't artificially bend the pipeline back on itself just to reduce the calls to UseAuthorization. You're effectively duplicating the Map code before and after UseAuthorization when it's a lot cleaner not to.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like where aspnet/Routing#903 is going, but we're not there yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the overlapping file structure is not realistic, but the branching is.

{
MapAuthenticatedFiles(branch, files);
});
// Set an endpoint for files inside PrivateFiles to run authorization
PathString remaining;
if (context.Request.Path.StartsWithSegments("/MapImperativeFiles", out remaining))
{
SetFileEndpoint(context, files, remaining, "files");
}
else if (context.Request.Path.StartsWithSegments("/MapAuthenticatedFiles", out remaining))
{
SetFileEndpoint(context, files, remaining, null);
}

app.Map("/MapImperativeFiles", branch =>
{
MapImperativeFiles(authorizationService, branch, files);
await next();
});

app.UseAuthorization();

app.Map("/MapAuthenticatedFiles", branch => SetupFileServer(branch, files));
app.Map("/MapImperativeFiles", branch => SetupFileServer(branch, files));

app.UseMvc(routes =>
{
routes.MapRoute(
Expand All @@ -114,81 +138,61 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, IAuthori
});
}

// Blanket authorization, any authenticated user is allowed access to these resources.
private static void MapAuthenticatedFiles(IApplicationBuilder branch, PhysicalFileProvider files)
private void SetupFileServer(IApplicationBuilder builder, IFileProvider files)
{
branch.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync(new AuthenticationProperties()
{
// https://github.com/aspnet/Security/issues/1730
// Return here after authenticating
RedirectUri = context.Request.PathBase + context.Request.Path + context.Request.QueryString
});
return;
}

await next();
});
branch.UseFileServer(new FileServerOptions()
builder.UseFileServer(new FileServerOptions()
{
EnableDirectoryBrowsing = true,
FileProvider = files
});
}

// Policy based authorization, requests must meet the policy criteria to be get access to the resources.
private static void MapImperativeFiles(IAuthorizationService authorizationService, IApplicationBuilder branch, PhysicalFileProvider files)
private static void SetFileEndpoint(HttpContext context, PhysicalFileProvider files, PathString filePath, string policy)
{
branch.Use(async (context, next) =>
var fileSystemInfo = GetFileSystemInfo(files, filePath);
if (fileSystemInfo != null)
{
var fileInfo = files.GetFileInfo(context.Request.Path);
AuthorizationResult result = null;
if (fileInfo.Exists)
{
result = await authorizationService.AuthorizeAsync(context.User, fileInfo, "files");
}
else
{
// https://github.com/aspnet/Home/issues/2537
var dir = files.GetDirectoryContents(context.Request.Path);
if (dir.Exists)
{
result = await authorizationService.AuthorizeAsync(context.User, dir, "files");
}
else
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
return;
}
}
var metadata = new List<object>();
metadata.Add(fileSystemInfo);
metadata.Add(new AuthorizeAttribute(policy));

var endpoint = new Endpoint(
c => Task.CompletedTask,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null? Otherwise this breaks 404.
It's a bit awkward to use Endpoints for auth without using them to actually handle the request.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't break 404 because an endpoint is only being created for a file, and files are always being return by UseFileSystem middleware.

The endpoint request delegate is never being called, but if it was then returning a null would NRE when it is awaited.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should never be awaited. If it matches a file then this would never get run. If it doesn't match a file then calling this would be wrong.
https://github.com/aspnet/HttpAbstractions/pull/1059/files#diff-3376870948b08e8c913217b7dab1bdcbR86

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better yet, since we don't expect it to get called it should throw InvalidOperationException.

new EndpointMetadataCollection(metadata),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Condense: new EndpointMetadataCollection(fileSystemInfo, new AuthorizeAttribute(policy)),

context.Request.Path);

context.Features.Set<IEndpointFeature>(new EndpointFeature(endpoint));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warrants a SetEndpoint extension method that creates the dummy feature for you internally. We do this for other features already. And while you're at it add the matching GetEndpoint extension.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion! How would SetEndpoint work if there is already an IEndpointFeature set? Would it update the Endpoint property and leave the same feature, or would it always set a new feature with endpoint.

I ask because the implementation that routing sets implements multiple feature interfaces, e.g. IEndpointFeature and IRouteValuesFeature and IRouterFeature (for legacy).

Copy link
Member

@Tratcher Tratcher Nov 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not expect it to replace the feature implementation, only the endpoint. The features need to function independently.

}
}

if (!result.Succeeded)
private static object GetFileSystemInfo(PhysicalFileProvider files, string path)
{
var fileInfo = files.GetFileInfo(path);
if (fileInfo.Exists)
{
return fileInfo;
}
else
{
// https://github.com/aspnet/Home/issues/2537
var dir = files.GetDirectoryContents(path);
if (dir.Exists)
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync(new AuthenticationProperties()
{
// https://github.com/aspnet/Security/issues/1730
// Return here after authenticating
RedirectUri = context.Request.PathBase + context.Request.Path + context.Request.QueryString
});
return;
}
// Authenticated but not authorized
await context.ForbidAsync();
return;
return dir;
}
}

await next();
});
branch.UseFileServer(new FileServerOptions()
return null;
}

private class EndpointFeature : IEndpointFeature
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Callers shouldn't have to create this dummy type. See the comments about Get/SetEndpoint() extensions.

{
public Endpoint Endpoint { get; set; }

public EndpointFeature(Endpoint endpoint)
{
EnableDirectoryBrowsing = true,
FileProvider = files
});
Endpoint = endpoint;
}
}
}
}
3 changes: 2 additions & 1 deletion samples/StaticFilesAuth/StaticFilesAuth.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>netcoreapp3.0;net461</TargetFrameworks>
<UserSecretsId>aspnet-StaticFilesAuth-AFE2BD9D-1575-4C3E-BE32-3F15C5BC9947</UserSecretsId>
<NoWarn>NU1605</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -35,7 +36,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="$(MicrosoftAspNetCorePackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="$(MicrosoftAspNetCoreAuthenticationCookiesPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="$(MicrosoftAspNetCoreAuthorizationPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authorization.Policy" Version="$(MicrosoftAspNetCoreAuthorizationPolicyPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
<PackageReference Include="Microsoft.NET.Sdk.Razor" Version="$(MicrosoftNETSdkRazorPackageVersion)" PrivateAssets="All" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<NoWarn>NU1605</NoWarn>
</PropertyGroup>

<!-- Workaround for https://github.com/dotnet/sdk/issues/1729 -->
Expand All @@ -21,6 +22,7 @@
<ProjectReference Include="..\..\samples\StaticFilesAuth\StaticFilesAuth.csproj" />

<PackageReference Include="Microsoft.AspNetCore" Version="$(MicrosoftAspNetCorePackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authorization.Policy" Version="$(MicrosoftAspNetCoreAuthorizationPolicyPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
Expand Down