Create an Angular todo list with azure static web apps Part 4

In this series about Azure static web apps, we created an Angular Todo list app, an Azure function using JavaScript and linked both together. If you want to have a look at the previous parts, here are the links

In this post, we will store the todos in Cosmos DB and read it from our Azure function.

Create Cosmos DB

Azure Cosmos DB gives you a free account with upto 400 RU/s. You can use it for this demo. Go to Azure Portal, Click New Resource -> Under Databases select Azure Cosmos DB, choose the API as SQL API and add the account name, resource group and region and leave everything else with the default value then click Review + Create then Create.

Once the Azure Cosmos DB account is created, open it and go to Data Explorer and from the toolbar click New Database and give it a name

Once the database is created, click the three dots near the database name and click New Container. Provide a name for the container and type /id as the partition key then click ok

From the left menu click Keys and copy the PRIMARY CONNECTION STRING.

Azure Function Settings

To connect to the Azure Cosmos DB, we we will need to store the connection string somewhere. For this purpose, we will use the file local.settings.json under api folder.

Add a new key under the values key and name it CosmosConnectionString and paste the Cosmos DB connection string copied earlier.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "CosmosConnectionString":""
  },"Host": {
    "CORS": "*"
  }
}

The local.settings.json file is not added to Git and should not be. This should include the connection strings and any secrets used locally.

Connect to Cosmos from the APIs

We have the database ready now and the connection string is set in our api. To read data from Cosmos, we need to install the package @azure/cosmos. Open command prompt and navigate to the api folder that has the function project and run the following command

npm i @azure/cosmos

Now open the index.js file inside the api/GetTodoList folder and paste the following code

const CosmosClient = require('@azure/cosmos').CosmosClient;

module.exports = async function (context, req) {
    context.log('JavaScript HTTP trigger function processed a request.');
    let cosmosClient = new CosmosClient(process.env['CosmosConnectionString']);
    let database = cosmosClient.database('todos');
    let container = database.container('todolist');
    let {resources} = (await container.items.query('SELECT c.id,c.title,c.isCompleted FROM c').fetchAll());

    context.res = {
        // status: 200, /* Defaults to 200 */
        body: resources
    };
}

The code is straight forward, in line 1, we imported the CosmosClient class which will be used to connect to Cosmos DB. This is like an EntityFramework DBContext. Line 5,6, and 7 we create a client instance, use it to get a reference to the database and the container that has our data.

In line 8, we query the container to return only the id, title and isCompleted fields and return the results.

You can see that the field names are now pascal named (title instead of Title), you will have to update the binding in the TodoList component

 <td><input type="checkbox" [checked]="todo.isCompleted" disabled></td>

Test

Run your api project with func start and your Angular app with ng serve. You should see an empty list. Go back to your Cosmos DB instance in Azure Portal, click Data Explorer, select the db then the container and choose Items. You can add a new item by clicking the New Item button on the toolbar and then click Save. Repeat the process to add few items

Now try to refresh your app and you should see the data displayed in the Angular app

Deployment

Commit and push your changes and wait for the workflow action to finish. If you tried your site now it will not work as the function has no idea about the connection string. To add a new connection string, open your Azure Static Web App, click on Configuration and then click Add. Type CosmosConnectionString as the name and paste the connection string for the Cosmos DB in the value field. Save and refresh your app and now you should see the application displaying the data you saved in Cosmos DB

Source code: https://github.com/haitham-shaddad/ng-todo-list-az-static-webapp

Create an Angular todo list with azure static web apps Part 1

In this post, I will create a To Do List app using Angular and we will host it on the newly introduced Azure service called Static Web Apps.

Project Setup

Azure static web app is connected to GitHub repo, and the first thing to do is to create a new repo under your github account. Once you create it, clone it to your local machine and run the following commands. Make sure you have angular-cli tools installed

ng new AzTodoList

Choose yes for routing and select your preferred css processor and hit enter to create the project and install all needed packages. Once all packages are installed, run ng serve to make sure the app works fine. You should have something like this running on port 4200

Make sure all files exist in the root folder where you cloned the GitHub repo. If you cloned the repo in a folder called ToDoList and then ran the command ng new inside it, it will create another subfolder. If this happened, make sure to move the files in the inner folder to the ToDoList folder. Otherwise, the GitHub action won’t work since it will fail to determine the programing language used. Your folder structure should look like this:

Now commit the source code with git add . to add all files followed by git commit -m "Initial commit" and finally git push origin master to push the files to GitHub.

Create Azure Static Web App

Open Azure portal and create a new static web app, log in to your GitHub account and choose the ToDoList app

Click on Next: Build to move to the next screen and configure it as below. The artifact location is dist/AzTodoList but it can be different in your case depending on your angular project name. You can get the exact path from your angular.json file or run ng build and check your folder structure.

When you create the app, it will automatically created the necessary CI/CD on GitHub using GitHub Actions and you can view that by clicking on the link “Thank you for using Azure Static Web App! We have not received any content for your site yet. Click here to check the status of your GitHub Action runs.” . You can view the build status by going to your GitHub repo and click Actions

As you can see above, it failed few times because I put the Angular app in a folder inside the repo. Once I moved all the files from within the inner folder to the root folder, it worked.

Test the app

Now go back to your Azure Portal and in the overview tab for the static web app, click Browse and this is what you should see

In the next post, we will add the APIs that will be used to retrieve, add and delete ToDos

Source code: https://github.com/haitham-shaddad/ng-todo-list-az-static-webapp

Download Attachments in Single Page App and Asp.Net Core

Introduction

If you have a SPA built with any JavaScript framework and it has an attachment feature, you must hit the part where you need to allow the user to download an attachment and the App is authenticating users using Tokens.

The problem

With normal forms authentication based on cookies the browser will simply send the authentication cookie with each request to your web server without you doing anything. If you have a link that will allow the user to download a file from your server, the browse will automatically send the authentication cookie when the user clicks the link. This makes it so easy for you. But if you are using token based authentication, it is your responsibility to send a token for each request sent to the server via Ajax by using the Authorization header.

Unfortunately, you cannot control the headers sent to the server if the user is opening a link in a new browser window and the user will end up with unauthorized request.

The solution

Download the file using Ajax Request

In this solution, you have to request the endpoint that downloads the file using Ajax Request which will include the authorization header and then get all the file content in a variable and push the content to the user. This works fine if the file size is very small. But imagine the case when you are downloading a 500MB file. This is not going to work since the file is stored in a JavaScript variable before the download takes place.

Make the API that download the attachment anonymous

If the endpoint that downloads the file doesn’t require authentication then we are good. But now the file will be available for every one to download. So, we have to find a way to secure the file even when the endpoint is anonymous.

If you have some experience with Azure Storage, you may have heard of Azure Storage Shared Access Signature. The idea is simple. When the user requests a file, generate a token, save it to a temporary storage  and append it to the URL of the download file endpoint. When the user clicks the link, the endpoint will be called and the token will be validated against the temporary storage and if it matches then send the file contents. This way we will be sure that the link was generated by the application to that user. Still, if the link was shared to another user, he will be able to download the file. But this is another issue that we can worry about later.

Implementation

We will create a new asp.net core site with an endpoint to download files but I will not create a SPA in this article. That will be left for the reader. I will test the idea though using Postman.

Open Visual Studio, Create a new project of type “Asp.NET Core Web Application” then Choose “API” in the next dialog. You can still choose “Web Application (Model-View-Controller)”. I will leave authentication to the default “No Authentication”.

Right Click on the Controllers folder and choose “New Controller”, choose “API Controlller – Empty” and name it AttachmentsController. You should end up with the following

[Produces("application/json")]
[Route("api/Attachments")]
 //[Authorize]
public class AttachmentsController : Controller
{
}

Notice that I have commented the [Authorize] attribute since I didn’t setup authentication in this demo. In real life scenario, you will setup authentication and authorization using Token based Authentication.

Create a folder named Services and then create a new interface called ISecureUrlGenerator. The content should look like the following:

   public interface ISecureUrlGenerator
    {
        string GenerateSecureAttachmentUrl(string id, string url);
        bool ValidateUrl(string url, string id);
        bool ValidateToken(string id, string token);
    }

Now, add class to implement the previous interface

using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;

namespace SecureAttachmentsDownload.Services
{
    public class SecureUrlGenerator : ISecureUrlGenerator
    {
        private readonly IMemoryCache memoryCache;

        public SecureUrlGenerator(IMemoryCache memoryCache)
        {
            this.memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
        }

        public string GenerateSecureAttachmentUrl(string id, string url)
        {
            var token = Guid.NewGuid().ToString().ToLower();
            StoreToken(id, token);
            var separator = url.Contains("?") ? "&" : "?";
            return $"{url}{separator}token={token}";
        }

        public bool ValidateToken(string id, string token)
        {
            var tokens = memoryCache.Get(id);
            if (tokens != null && tokens.Contains(token))
                return true;

            return false;
        }

        public bool ValidateUrl(string url, string id)
        {
            var uri = new Uri(url);
            var queryStringParams = uri.Query.Split("&");
            foreach (var param in queryStringParams)
            {
                var values = param.Split("=");
                if (values[0].ToLower() == "token")
                {
                    return ValidateToken(id, values[1]);
                }
            }

            return false;
        }

        private bool IsTokenValid(string id, string token)
        {
            var tokens = memoryCache.Get(id);
            if (tokens != null && tokens.Contains(token))
                return true;

            return false;
        }

        private void StoreToken(string id, string token)
        {
            var tokens = memoryCache.Get(id);
            if (tokens == null)
                tokens = new List();
            tokens.Add(token);

            memoryCache.Set(id, tokens);
        }
    }
}

In this implementation, I am storing the tokens in asp.net core memory cache. To enable this feature, you have to add the caching service in Starup.cs file

public void ConfigureServices(IServiceCollection services)
        {
            services.AddMemoryCache();
            services.AddMvc();
        }

You can replace the memory cache with a database if you want the tokens to be permanent and in this case you have to add an expiration date.

Before we utilize the secure URL genrator, we need a class to hold the attachments metadata since the user will request the list of attachments first and then download it.
Create a folder called Models and put the following class in it.

namespace SecureAttachmentsDownload.Models
{
    public class AttachmentMetadata
    {
        public int Id { get; set; }

        public string DownloadUrl { get; set; }

        public string Name { get; set; }

        public int FileSize { get; set; }
    }
}

Now, lets get to the part where we utilise our secure URL generator.
The flow will be as below:

  1. The user requests endpoint to return a list of attachments to be displayed to the user. Here, the DownloadUrl will have the token already. This will be secured by tokens
  2. The SPA will display this list to the user as links or buttons that the user can click to download the file. The href for the anchor tag will be the DownloadURl property
  3. The user will click the link to download the attachment
  4. The AttachmentController will be called and the endpoint will validate the token and return the file or else a 401

Open the AttachmentsController file and add the following 2 action methods

  using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using SecureAttachmentsDownload.Models;
using SecureAttachmentsDownload.Services;

namespace SecureAttachmentsDownload.Controllers
{
    [Produces("application/json")]
    [Route("api/Attachments")]
    //[Authorize]
    public class AttachmentsController : Controller
    {
        private readonly ISecureUrlGenerator _secureUrlGenerator;
        private readonly IHostingEnvironment _hostingEnvironment;

        private readonly List Attachments = new List()
            {
                new AttachmentMetadata
                {
                    Id = 1,
                    Name = "bitcoin.pdf",
                    ContentType = "application/pdf",
                    FileSize = 1024
                },
                  new AttachmentMetadata
                {
                    Id = 2,
                    Name = "report 1.pdf",
                    FileSize = 3024
                },
                  new AttachmentMetadata
                {
                    Id = 3,
                    Name = "report 2.pdf",
                    FileSize = 2024
                }
            };

        public AttachmentsController(ISecureUrlGenerator secureUrlGenerator, IHostingEnvironment hostingEnvironment)
        {
            _secureUrlGenerator = secureUrlGenerator;
            _hostingEnvironment = hostingEnvironment;
        }

        [HttpGet]
        [Route("")]
        public IActionResult Get()
        {
            foreach (var attachment in Attachments)
            {
                var url = Url.Action(nameof(AttachmentsController.Get), "Attachments", new { attachment.Id }, Url.ActionContext.HttpContext.Request.Scheme);
                attachment.DownloadUrl = _secureUrlGenerator.GenerateSecureAttachmentUrl(attachment.Id.ToString(), url);
            }

            return Ok(Attachments);
        }

        [HttpGet]
        [Route("{id}")]
        [AllowAnonymous]
        public IActionResult Get(int id, string token)
        {
            if (!_secureUrlGenerator.ValidateToken(id.ToString(), token))
                return Forbid();

            var attachment = Attachments.FirstOrDefault(a => a.Id == id);
            if (attachment == null)
                return NotFound();

            var stream = new FileStream($"{_hostingEnvironment.WebRootPath}\\Files\\{attachment.Name}", FileMode.Open);

            return File(stream, attachment.ContentType);
        }
    }
}

Now run the application and open the URL /api/Attachments. You will get the following excepttion:

InvalidOperationException: Unable to resolve service for type ‘SecureAttachmentsDownload.Services.ISecureUrlGenerator’ while attempting to activate ‘SecureAttachmentsDownload.Controllers.AttachmentsController’.

To fix it, open the startup.cs file and add the following line to the ConfigureServices method

  public void ConfigureServices(IServiceCollection services)
        {
            services.AddMemoryCache();
            services.AddScoped ();
            services.AddMvc();
        }

Now open the URL api/attachments again and you should see the following JSON response

[
{
"id": 1,
"downloadUrl": "http://localhost:53098/api/Attachments/1?token=b78763c2-0109-4c12-b771-5f5cc5d19017",
"name": "bitcoin.pdf",
"fileSize": 1024,
"contentType": "application/pdf"
},
{
"id": 2,
"downloadUrl": "http://localhost:53098/api/Attachments/2?token=12497a4a-8f08-44ba-b9f6-914c4b484cc5",
"name": "report 1.pdf",
"fileSize": 3024,
"contentType": null
},
{
"id": 3,
"downloadUrl": "http://localhost:53098/api/Attachments/3?token=8647bb52-e47f-4580-8149-0b1d238ab0e2",
"name": "report 2.pdf",
"fileSize": 2024,
"contentType": null
}
]

As you can see, the downloadUrl property has the absolute URL for the file and the `token` query string parameter is appended. If you open the first link in a new browser window, the Action Get(id) will be called and the token will be bound to the parameter token.
In my implementation, I have put some files in a folder called Files under the wwwroot folder. But in actual projects, you may retrieve the files from a Database, FTP or any Document Management System.

If you want to make sure that it is really working, just try to change any character in the token query string and you should get a forbid response from the server. In this example you will get an exception: InvalidOperationException: No authenticationScheme was specified, and there was no DefaultForbidScheme found.
This is because I didn’t configure the authentication middleware.

You can find the source code for this article on GitHub.

This implementation has as flaw. The list of attachments are returned with the download URL and the tokens are saved in memory. If the user didn’t click the link but after sometime, the tokens may have been already expired. So, either you save the tokens in a DB or you before clicking the link, fire an Ajax request to an endpoint that gets the metadata for a single attachment. This way, the downloadUrl will be always fresh and working.

If you have any questions or suggestions, please leave a comment below.