6/5/2022 Admin

Blazor File Upload using Web API and Swagger


 

Uploading a file using an API has been covered in many places. Uploading a file using a Swagger page has also been covered in the following Github project: dotnet-labs/HerokuContainer. Implementing authentication, using JWTs has also been covered in this project: StefanescuEduard/DotnetSwaggerDocumentation.

However the example in this Blog post combines all this and uses Blazor.

Note: You can download the completed project from here: Github: ADefWebserver/BlazorFileUploadSwagger.

Create The Project

image

The first step is to create a new Blazor Server project.

image

Next, we add the following NuGet packages:

  • Newtonsoft.Json
  • Swashbuckle.AspNetCore
  • Microsoft.IdentityModel.Tokens
  • System.IdentityModel.Tokens.Jwt
  • Microsoft.AspNetCore.Authentication.JwtBearer

 

image

We alter the contents of the appsettings.json file to the following:

 

{
  "AppSettings": {
    "EncryptionKey": "012389ABCDEFGHIJKLMN4567OPQRSTUVWXYZ",
    "UserName": "TestUser",
    "Password": "TestPassword"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

 

Note: In a real world app you would use a unique EncryptionKey and UserName and Password.

 

Add JSON Web Token (JWT) code

image

Now, we will add the code to create and validate JWTs.

Create a Services folder, and add the following files:

 

JWTAuthenticationService.cs

 

#pragma warning disable 1591
using BlazorFileUploadSwagger.Models;
using Microsoft.Extensions.Options;
namespace BlazorFileUploadSwagger.Services
{
    public class JWTAuthenticationService
    {
        private readonly AppSettings appSettings;
        private readonly TokenService tokenService;
        public JWTAuthenticationService(IOptions<AppSettings> options, 
            TokenService tokenService)
        {
            appSettings = options.Value;
            this.tokenService = tokenService;
        }
        
        public async Task<string> Authenticate(ApiToken userCredentials)
        {
            // **********************
            // authenticate the username and password 
            // **********************
            string securityToken = "";
            
            if (
                (appSettings.UserName.ToLower() == 
                userCredentials.UserName.ToLower())
                && 
                (appSettings.Password == userCredentials.Password)
                )
            {
                securityToken = await tokenService.GetToken();
            }
            return securityToken;
        }
    }
}

 

TokenService.cs (Code taken from here)

 

#pragma warning disable 1591
using BlazorFileUploadSwagger.Models;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
namespace BlazorFileUploadSwagger.Services
{
    public class TokenService
    {
        private readonly AppSettings appSettings;
        public TokenService(IOptions<AppSettings> options)
        {
            appSettings = options.Value;
        }
        public async Task<string> GetToken()
        {
            SecurityTokenDescriptor tokenDescriptor = 
                await GetTokenDescriptor();
            
            var tokenHandler = new JwtSecurityTokenHandler();
            SecurityToken securityToken = 
                tokenHandler.CreateToken(tokenDescriptor);
            
            string token = tokenHandler.WriteToken(securityToken);
            return token;
        }
        private async Task<SecurityTokenDescriptor> GetTokenDescriptor()
        {
            const int expiringHours = 24;
            byte[] securityKey = 
                await Task.Run(() => Encoding.UTF8.GetBytes(appSettings.EncryptionKey));
            
            var symmetricSecurityKey = new SymmetricSecurityKey(securityKey);
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Expires = DateTime.UtcNow.AddHours(expiringHours),
                SigningCredentials = 
                new SigningCredentials(symmetricSecurityKey, 
                SecurityAlgorithms.HmacSha256Signature)
            };
            return tokenDescriptor;
        }
    }
}

 

image

Create a Models folder, and add the following files:

 

ApiToken.cs

 

namespace BlazorFileUploadSwagger.Models
{
    public class ApiToken
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
}

 

AppSettings.cs

 

namespace BlazorFileUploadSwagger.Models
{
    public class AppSettings
    {
        public string EncryptionKey { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
    }
}

 

image

Create a Extensions folder, and add the following file:

 

AuthenticationExtensions.cs

 

#pragma warning disable 1591
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System;
namespace BlazorFileUploadSwagger
{
    public static class AuthenticationExtensions
    {
        public static IServiceCollection AddAuthentication(
            this IServiceCollection services,
            byte[] signingKey)
        {
            services.AddAuthentication(authOptions =>
                {
                    authOptions.DefaultAuthenticateScheme = 
                    JwtBearerDefaults.AuthenticationScheme;
                    
                    authOptions.DefaultChallengeScheme = 
                    JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(jwtOptions =>
                {
                    jwtOptions.SaveToken = true;
                    jwtOptions.TokenValidationParameters = 
                    new TokenValidationParameters
                    {
                        ValidateAudience = false,
                        ValidateIssuer = false,
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(signingKey),
                        ValidateLifetime = true,
                        LifetimeValidator = LifetimeValidator
                    };
                });
            return services;
        }
        private static bool LifetimeValidator(DateTime? notBefore,
            DateTime? expires,
            SecurityToken securityToken,
            TokenValidationParameters validationParameters)
        {
            return expires != null && expires > DateTime.Now;
        }
    }
}

 

image

Add the following using statements to Program.cs:

 

using System.Text;
using System.Reflection;
using System.Security.Claims;
using System.Security.Principal;
using Microsoft.AspNetCore.Server.IISIntegration;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using BlazorFileUploadSwagger;
using BlazorFileUploadSwagger.Services;
using BlazorFileUploadSwagger.Models;

 

Add the following code (below: var builder = WebApplication.CreateBuilder(args)):

 

// JWT Configuration
IConfigurationSection settingsSection = 
    builder.Configuration.GetSection("AppSettings");
AppSettings settings = settingsSection.Get<AppSettings>();
byte[] signingKey = Encoding.UTF8.GetBytes(settings.EncryptionKey);
builder.Services.AddAuthentication(signingKey);
builder.Services.AddScoped<JWTAuthenticationService>();
builder.Services.AddScoped<TokenService>();
builder.Services.Configure<AppSettings>(settingsSection);

 

Add Controllers

image

The Swagger API will document the controller methods we will create next.

Add the following file to the Models folder:

 

UploadForm.cs

 

namespace BlazorFileUploadSwagger.Models
{
    /// <summary>
    /// Upload Form
    /// </summary>
    public class UploadForm
    {
        /// <summary>
        /// File Directory
        /// </summary>
        public string FileDirectory { get; set; }
        /// <summary>
        /// File Attachment
        /// </summary>
        public IFormFile FileAttachment { get; set; }
        /// <summary>
        /// File Attachments
        /// </summary>
        public List<IFormFile> FileAttachments { get; set; }
    }
}

 

image

We will now create a Auth Controller that will accept the username and password specified in the appsettings.json file and issue a JWT.

We will then create an Upload Controller that will be protected by: [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] so that a user can only call it if they pass a valid JWT issued by the Auth Controller.

Create a Controllers folder, and add the following files:

 

AuthController.cs

 

using BlazorFileUploadSwagger.Models;
using BlazorFileUploadSwagger.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BlazorFileUploadSwagger.Controlers
{
    /// <summary>
    /// Auth Controller
    /// </summary>
    [ApiController]
    [Produces("application/json")]
    [Route("api/[controller]")]
    public class AuthController : ControllerBase
    {
        private readonly JWTAuthenticationService authenticationService;
        
        public AuthController(JWTAuthenticationService authenticationService)
        {
            this.authenticationService = authenticationService;
        }        
        #region public async Task<string> GetAuthToken([FromQuery] ApiToken objApiToken)
        /// <summary>
        /// Obtain a security token to use for subsequent calls. 
        /// Copy the output received and then click the Authorize button (above). 
        /// Paste the contents (between the quotes) into that box and then 
        /// click Authorize then close. Now the remaining methods will work.
        /// </summary>
        /// <param name="objApiToken"></param>
        /// <response code="200">JWT token created</response>
        [AllowAnonymous]
        [HttpGet("GetAuthToken")]
        [ProducesResponseType(typeof(string), 200)]
        public async Task<string> GetAuthToken([FromQuery] ApiToken objApiToken)
        {
            var dict = new Dictionary<string, string>();
            dict.Add("username", objApiToken.UserName);
            dict.Add("password", objApiToken.Password);
            string token = await authenticationService.Authenticate(objApiToken);
            
            return token;
        }
        #endregion
    }
}

 

UploadController.cs

 

using BlazorFileUploadSwagger.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BlazorFileUploadSwagger.Controllers
{
    /// <summary>
    /// An Upload controller for <code>multipart/form-data</code> submission
    /// </summary>
    [ApiController]
    [Produces("application/json")]
    [Route("api/[controller]")]
    public class UploadController : ControllerBase
    {
        private readonly IWebHostEnvironment _hostEnvironment;
        private string _SystemFiles;
        public UploadController(
            IWebHostEnvironment hostEnvironment)
        {
            _hostEnvironment = hostEnvironment;
            // Set _SystemFiles 
            _SystemFiles =
                System.IO.Path.Combine(
                    hostEnvironment.ContentRootPath,
                    "Files");
            // Create SystemFiles directory if needed
            if (!Directory.Exists(_SystemFiles))
            {
                DirectoryInfo di =
                    Directory.CreateDirectory(_SystemFiles);
            }
        }
        /// <summary>
        /// Upload
        /// </summary>
        /// <param name="form">A form</param>
        /// <returns></returns>
        //JwtBearerDefaults means this method will only work if a Jwt is being passed
        [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
        [HttpPost]
        public void UploadAPI([FromForm] UploadForm form)
        {
            // Get File Directory
            string FileDirectory = form.FileDirectory;
            // Set _SystemFiles
            _SystemFiles = System.IO.Path.Combine(_SystemFiles, FileDirectory);
            // Create SystemFiles directory if needed
            if (!Directory.Exists(_SystemFiles))
            {
                DirectoryInfo di =
                    Directory.CreateDirectory(_SystemFiles);
            }
            // Process File Attachment
            if (form.FileAttachment != null)
            {
                using (var readStream = form.FileAttachment.OpenReadStream())
                {
                    var filename = form.FileAttachment.FileName.Replace("\"", "").ToString();
                    filename = _SystemFiles + $@"\{filename}";
                    //Save file to harddrive
                    using (FileStream fs = System.IO.File.Create(filename))
                    {
                        form.FileAttachment.CopyTo(fs);
                        fs.Flush();
                    }
                }
            }
            // Process all File Attachments
            if (form.FileAttachments != null)
            {
                foreach (var file in form.FileAttachments)
                {
                    // Process file
                    using (var readStream = file.OpenReadStream())
                    {
                        var filename = file.FileName.Replace("\"", "").ToString();
                        filename = _SystemFiles + $@"\{filename}";
                        //Save file to harddrive
                        using (FileStream fs = System.IO.File.Create(filename))
                        {
                            file.CopyTo(fs);
                            fs.Flush();
                        }
                    }
                }
            }
        }    
    }
}

 

Add Swagger

image

First, we need to instruct the application to create the .xml file Swagger will need to create the API.

Right-click on the Project node and select Edit Project File and add the following line;

 

<GenerateDocumentationFile>true</GenerateDocumentationFile>

 

Add the following code to the Program.cs file:

 

// Swagger Configuration
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1",
        Title = "API",
        Description = "A simple example ASP.NET Core Web API"
    });
    options.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = "bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "JWT Authorization header using the Bearer scheme. " +
        "Example: \"Authorization: Bearer {token}\""
    });
    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
                new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "bearerAuth"
                    }
                },
                new string[] {}
        }
    });
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    options.IncludeXmlComments(xmlPath, true);
});

 

Also, add the following code under app.UseRouting():

 

app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});
app.UseSwagger();
app.UseSwaggerUI();

 

 

image

Finally, change all the code in the Index.razor page to the following;

 

Index.razor

@page "/"
<PageTitle>Index</PageTitle>
<h2><a href="swagger/index.html">Swagger UI</a></h2>

 

Download

Github: ADefWebserver/BlazorFileUploadSwagger

 

Links

dotnet-labs/HerokuContainer

StefanescuEduard/DotnetSwaggerDocumentation

Combining Bearer Token and Cookie Authentication in ASP.NET

File Upload via Swagger

Swagger

An error has occurred. This application may no longer respond until reloaded. Reload 🗙