Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V6 code copy for V9 #33045

Merged
merged 1 commit into from
Jul 11, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace StaticFilesAuth;

public class JwtAuthenticationOptions
{
public const string Options = "JwtAuthenticationOptions";

public string ValidIssuer { get; set; }
public string ValidAudience { get; set; }
public string SymmetricSecurityKey { get; set; }
public bool ValidateIssuer { get; set; }
public bool ValidateAudience { get; set; }
public bool ValidateLifetime { get; set; }
public bool ValidateIssuerSigningKey { get; set; }
public int TokenExpirationInMinutes { get; set; }
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using StaticFilesAuth;

var securityScheme = new OpenApiSecurityScheme() {
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JSON Web Token based security",
};

var securityReq = new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
};

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(o => {
o.AddSecurityDefinition("Bearer", securityScheme);
o.AddSecurityRequirement(securityReq);
});

var jwtAuthOpts = new JwtAuthenticationOptions();
builder.Configuration.GetSection(JwtAuthenticationOptions.Options).Bind(jwtAuthOpts);

builder.Services.AddAuthentication(o => {
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o => {
o.TokenValidationParameters = new TokenValidationParameters {
ValidIssuer = jwtAuthOpts.ValidIssuer,
ValidAudience = jwtAuthOpts.ValidAudience,
IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(jwtAuthOpts.SymmetricSecurityKey)),
ValidateIssuer = jwtAuthOpts.ValidateIssuer,
ValidateAudience = jwtAuthOpts.ValidateAudience,
ValidateLifetime = jwtAuthOpts.ValidateLifetime,
ValidateIssuerSigningKey = jwtAuthOpts.ValidateIssuerSigningKey
};
});

builder.Services.AddAuthorization(o =>
{
o.AddPolicy("AuthenticatedUsers", b => b.RequireAuthenticatedUser());
o.AddPolicy("AdminsOnly", b => b.RequireRole("admin"));
});

var app = builder.Build();

if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseFileServer();

app.UseAuthentication();
app.UseAuthorization();

var securityKey = new SymmetricSecurityKey(Convert.FromBase64String(jwtAuthOpts.SymmetricSecurityKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var tokenDescriptor = new SecurityTokenDescriptor {
Issuer = jwtAuthOpts.ValidIssuer,
Audience = jwtAuthOpts.ValidAudience,
SigningCredentials = credentials
};

app.MapGet("/token", (HttpContext context) => {
var username = context.Request.Headers["username"].ToString();
var password = context.Request.Headers["password"].ToString();

var claimsIdentity = new ClaimsIdentity(new[]
{
new Claim(JwtRegisteredClaimNames.Sub, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
});

if(username.Equals("admin") && password.Equals("admin"))
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "admin"));
}

tokenDescriptor.Subject = claimsIdentity;
tokenDescriptor.Expires = DateTime.UtcNow.AddMinutes(jwtAuthOpts.TokenExpirationInMinutes);

var jwtTokenHandler = new JwtSecurityTokenHandler();

return Results.Ok(new {
access_token = jwtTokenHandler.WriteToken(jwtTokenHandler.CreateToken(tokenDescriptor))
});
}).AllowAnonymous();

string GetOrCreateFilePath(string fileName, string filesDirectory = "PrivateFiles")
{
var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);

if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}

return Path.Combine(app.Environment.ContentRootPath, directoryPath, fileName);
}

async Task SaveFileWithCustomFileName(IFormFile file, string fileSaveName)
{
var filePath = GetOrCreateFilePath(fileSaveName);
await using var fileStream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(fileStream);
}
// <snippet_1>
app.MapGet("/files/{fileName}", IResult (string fileName) =>
{
var filePath = GetOrCreateFilePath(fileName);

if (File.Exists(filePath))
{
return TypedResults.PhysicalFile(filePath, fileDownloadName: $"{fileName}");
}

return TypedResults.NotFound("No file found with the supplied file name");
})
.WithName("GetFileByName")
.RequireAuthorization("AuthenticatedUsers");

// IFormFile uses memory buffer for uploading. For handling large file use streaming instead.
// https://learn.microsoft.com/aspnet/core/mvc/models/file-uploads#upload-large-files-with-streaming
app.MapPost("/files", async (IFormFile file, LinkGenerator linker, HttpContext context) =>
{
// Don't rely on the file.FileName as it is only metadata that can be manipulated by the end-user
// Take a look at the `Utilities.IsFileValid` method that takes an IFormFile and validates its signature within the AllowedFileSignatures

var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
await SaveFileWithCustomFileName(file, fileSaveName);

context.Response.Headers.Append("Location", linker.GetPathByName(context, "GetFileByName", new { fileName = fileSaveName}));
return TypedResults.Ok("File Uploaded Successfully!");
})
.RequireAuthorization("AdminsOnly");

app.Run();
// </snippet_1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>43a14276-5819-40b0-aee3-17bfbedadcf6</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0-preview.2.23153.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0-preview.2.23153.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

<ItemGroup>
<Folder Include="PrivateFiles" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace StaticFilesAuth;

public static class Utilities
{
public static Dictionary<string, List<byte[]>> AllowedFileSignatures = new()
{
{
".jpeg", new List<byte[]>
{
new byte[] {0xFF, 0xD8, 0xFF, 0xE0},
new byte[] {0xFF, 0xD8, 0xFF, 0xE2},
new byte[] {0xFF, 0xD8, 0xFF, 0xE3}
}
},
{
".png", new List<byte[]>
{
new byte[] {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
}
}
};

public static bool IsFileValid(IFormFile file)
{
using var reader = new BinaryReader(file.OpenReadStream());

var signatures = AllowedFileSignatures
.Values
.SelectMany(x => x)
.ToList();

var headerBytes = reader.ReadBytes(AllowedFileSignatures
.Max(m => m.Value.Max(n => n.Length)));

return signatures
.Any(signature => headerBytes
.Take(signature.Length)
.SequenceEqual(signature));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Authentication": {
"Schemes": {
"Bearer": {
"ValidAudiences": [
"https://localhost:57923",
"http://localhost:57924"
],
"ValidIssuer": "dotnet-user-jwts"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"JwtAuthenticationOptions": {
"ValidIssuer": "https://localhost:5001",
"ValidAudience": "https://localhost:5001",
"SymmetricSecurityKey": "g1WoF+SA1EerA2o0ZbUqOQ==",
"ValidateIssuer": true,
"ValidateAudience": true,
"ValidateLifetime": false,
"ValidateIssuerSigningKey": true,
"TokenExpirationInMinutes": 2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using StaticFilesSample.Models;

namespace StaticFilesSample.Controllers
{
public class Home2Controller : Controller
{
private readonly ILogger<Home2Controller> _logger;

public Home2Controller(ILogger<Home2Controller> logger)
{
_logger = logger;
}

public IActionResult Index()
{
return View();
}

public IActionResult Privacy()
{
return View();
}

public IActionResult MyStaticFilesRR()
{
// Only returns image when calling
/*
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
RequestPath = "/StaticFiles"
});
*/
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace StaticFilesSample.Models
{
public class ErrorViewModel
{
public string? RequestId { get; set; }

public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>NotDefault</title>
</head>
<body>
<h1>This is the NotDefault page in /MyStaticFiles.</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>MyStaticFiles</title>
</head>
<body>
<h1>This is the Test3 page of /MyStaticFiles.</h1>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}

<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
Loading
Loading