Angular2 secure file download without using an access token in URL or cookies

This article shows how an Angular 2 SPA client can download files using an access token without passing it to the resource server in the URL. The access token is only used in the HTTP Header.

If the access token is sent in the URL, this will be saved in server logs, routing logs, browser history, or copy/pasted by users and sent to other users in emails etc. If the user does not log out after using the application, the access token will still be valid until a token timeout. Due to this, it is better to not send an access token in the URL.

The article shows how this could be implemented without using cookies and without sending the access token in the URL. The application is implemented using OpenID Connect Implicit Flow with IdentityServer4 with ASP.NET Core.

Code: https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow

2016.12.04: Updated to IdentityServer4 rc4
2016.11.18: Updated to Angular 2.2.0, IdentityServer4 rc3
2016.11.03: Updated to Angular 2.1.1, Webpack 2, AoT, treeshaking
2016.10.08: Updated to IdentityServer4 rc2
2016.09.18: Updated to IdentityServer4 RC1, Angular 2 release, ASP.NET Core 1.0.1
2016.08.11: Updated to IdentityServer4 1.0.0-beta5
2016.07.03: Updated to ASP.NET Core RTM
2016.06.26: Updated Angular 2 to rc3, new Angular 2 routing, IdentityServer4 beta 3, connect/endsession implemented
2016.05.22: Updated to ASP.NET Core RC2 dotnet
2016.05.07: Updated to Angular 2 rc1
2016.06.21: Updated to Angular 2 rc2

Posts in this series:

Angular 2 Client

The Angular 2 application uses the SecureFileService to download the files securely. The SecurityService is injected in this service using dependency injection and the access token can be accessed through this service. The DownloadFile function implements the download service. The access token is added to the HTTP request headers using the Headers from the ‘angular2/http’ component. This is then added in the setHeaders function.

The service calls GenerateOneTimeAccessToken with the file id. The HTTP request returns an access id which can be used once within 30 seconds. This access id is then used in a second HTTP request in the URL which downloads the required file. It does not matter if this is copied as it cannot be reused.

import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
import { Configuration } from '../app.constants';
import { SecurityService } from '../services/SecurityService';

@Injectable()
export class SecureFileService {

    private actionUrl: string;
    private fileExplorerUrl: string;
    private headers: Headers;

    constructor(private _http: Http, private _configuration: Configuration, private _securityService: SecurityService) {
        this.actionUrl = `${_configuration.FileServer}api/Download/`; 
        this.fileExplorerUrl = `${_configuration.FileServer }api/FileExplorer/`;    
    }

    public DownloadFile(id: string) {
        this.setHeaders();
        let oneTimeAccessToken = "";
        this._http.get(`${this.actionUrl}GenerateOneTimeAccessToken/${id}`, {
            headers: this.headers
        }).map(
            res => res.text()
            ).subscribe(
            data => {
                oneTimeAccessToken = data;
                
            },
            error => this._securityService.HandleError(error),
            () => {
                console.log(`open DownloadFile for file ${id}: ${this.actionUrl}${oneTimeAccessToken}`);
                window.open(`${this.actionUrl}${oneTimeAccessToken}`);
            });
    }

    public GetListOfFiles = (): Observable<string[]> => {
        this.setHeaders();
        return this._http.get(this.fileExplorerUrl, {
            headers: this.headers
        }).map(res => res.json());
    }

    private setHeaders() {
        this.headers = new Headers();
        this.headers.append('Content-Type', 'application/json');
        this.headers.append('Accept', 'application/json');

        var token = this._securityService.GetToken();

        if (token !== "") {
            this.headers.append('Authorization', 'Bearer ' + token);
        }
    }
}

The DownloadFileById function in the SecureFilesComponent component is used to call the service DownloadFile(id) function which downloads the file as explained above.

import { Component, OnInit } from '@angular/core';
import { SecureFileService } from './SecureFileService';
import { SecurityService } from '../services/SecurityService';
import { Observable }       from 'rxjs/Observable';

@Component({
    selector: 'securefiles',
    templateUrl: 'securefiles.component.html',
    providers: [SecureFileService]
})

export class SecureFilesComponent implements OnInit {

    public message: string;
    public Files: string[];
   
    constructor(private _secureFileService: SecureFileService, public securityService: SecurityService) {
        this.message = "Secure Files download";
    }

    ngOnInit() {
      this.getData();
    }

    public DownloadFileById(id: any) {
        this._secureFileService.DownloadFile(id);
    }

    private getData() {
        this._secureFileService.GetListOfFiles()
            .subscribe(data => this.Files = data,
            error => this.securityService.HandleError(error),
            () => console.log('Get all completed'));
    }
}

The component HTML template creates a list of files which can be download by the current authorized user and adds these in the browser as links.

<div class="col-md-12" *ngIf="securityService.IsAuthorized" >
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">{{message}}</h3>
        </div>
        <div class="panel-body">
            <table class="table">
                <thead>
                    <tr>
                        <th>Name</th>
                    </tr>
                </thead>
                <tbody>
                    <tr style="height:20px;" *ngFor="#file of Files" >
                        <td><a (click)="DownloadFileById(file)">Download {{file}}</a></td>
                    </tr>
                </tbody>
            </table>

        </div>
    </div>
</div>

The SecurityService is used to login and get the access token and the token id from Identity Server 4. This is explained in the previous post Angular2 OpenID Connect Implicit Flow with IdentityServer4.

Implementing the File Server

The server API implements a GenerateOneTimeAccessToken method to start the download. This method authorizes using policies and checks if the file id exists. A HttpNotFound is returned, if the file id does not exist. It then validates, if the file exists on the file server. The method also checks, if the user is an administrator, and uses this to validate that the user is authorized to access the file. The AddFileIdForUseOnceAccessId method is then used to generate a use once access id for this file.

[Authorize("securedFilesUser")]
[HttpGet("GenerateOneTimeAccessToken/{id}")]
public IActionResult GenerateOneTimeAccessToken(string id)
{
	if (!_securedFileProvider.FileIdExists(id))
	{
		return HttpNotFound($"File id does not exist: {id}");
	}

	var filePath = $"{_appEnvironment.ApplicationBasePath}/SecuredFileShare/{id}";
	if (!System.IO.File.Exists(filePath))
	{
		return HttpNotFound($"File does not exist: {id}");
	}

	var adminClaim = User.Claims.FirstOrDefault(x => x.Type == "role" && x.Value == "securedFiles.admin");
	if (_securedFileProvider.HasUserClaimToAccessFile(id, adminClaim != null))
	{
		// TODO generate a one time access token
		var oneTimeToken = _securedFileProvider.AddFileIdForUseOnceAccessId(filePath);
		return Ok(oneTimeToken);
	}

	// returning a HTTP Forbidden result.
	return new HttpStatusCodeResult(403);
}

The download file API can be used with the use once access id parameter. This method uses the access id to retrieve the file path using the GetFileIdForUseOnceAccessId method. If the access id is valid, the file can be downloaded using the FileContentResult.

[AllowAnonymous]
[HttpGet("{accessId}")]
public IActionResult Get(string accessId)
{
	var filePath = _securedFileProvider.GetFileIdForUseOnceAccessId(accessId);
	if(!string.IsNullOrEmpty(filePath))
	{
		var fileContents = System.IO.File.ReadAllBytes(filePath);
		return new FileContentResult(fileContents, "application/octet-stream");
	}

	// returning a HTTP Forbidden result.
	return new HttpStatusCodeResult(401);
}

The UseOnceAccessIdService is responsible for generating the access id and validating it when using it. The AddFileIdForUseOnceAccessId method creates a new UseOnceAccessId object which creates a random string which is used for the download access id. The object saves the time stamp as a property and also the file path which will be available for download for 30 seconds. This object is then saved to a in-memory list. The UseOnceAccessIdService service is registered as a singleton in the startup class.

The GetFileIdForUseOnceAccessId removes any objects which are older than 30 seconds. It then retrieves the UseOnceAccessId object if it still exists and returns the file path from this. The object is then deleted. This prevents that the file can be downloaded twice using the same key.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ResourceFileServer.Providers
{
    public class UseOnceAccessIdService
    {
        /// <summary>
        /// One time tokens live for a max of 30 seconds
        /// </summary>
        private double _timeToLive = 30.0;
        private static object lockObject = new object();

        private List<UseOnceAccessId> _useOnceAccessIds = new List<UseOnceAccessId>();

        public string GetFileIdForUseOnceAccessId(string useOnceAccessId)
        {
            var fileId = string.Empty;

            lock(lockObject) {

                // Max 30 seconds to start download after requesting one time token.
                _useOnceAccessIds.RemoveAll(t => t.Created < DateTime.UtcNow.AddSeconds(-_timeToLive));

                var item = _useOnceAccessIds.FirstOrDefault(t => t.AccessId == useOnceAccessId);
                if (item != null)
                {
                    fileId = item.FileId;
                    _useOnceAccessIds.Remove(item);
                }
            }

            return fileId;
        }

        public string AddFileIdForUseOnceAccessId(string filePath)
        {
            var useOnceAccessId = new UseOnceAccessId(filePath);
            lock (lockObject)
            {
                _useOnceAccessIds.Add(useOnceAccessId);
            }
            return useOnceAccessId.AccessId;
        }
    }
}

The UseOnceAccessId object is used to save a request for a download and generates the random access id string in the constructor.

using System;

namespace ResourceFileServer.Providers
{
    internal class UseOnceAccessId
    {
        public UseOnceAccessId(string fileId)
        {
            Created = DateTime.UtcNow;
            AccessId = CreateAccessId();
            FileId = fileId;
        }

        public DateTime Created { get; }

        public string AccessId { get; }

        public string FileId { get; }

        private string CreateAccessId()
        {
            SecureRandom secureRandom = new SecureRandom();
            return secureRandom.Next() + Guid.NewGuid().ToString();
        }
    }
}

Now the files can be downloaded from the resource file server without using the access token in the URL or without using cookies.

secureNoAccessInUrl_01

Notes: Thanks for Alistair for pointing this out in the comments of the previous post. Maybe it would be nice, if Identity Server 4 could support this using ‘use once tokens’, then the standard authorization middleware could be used on the resource server.

Links

http://openid.net/specs/openid-connect-core-1_0.html

http://openid.net/specs/openid-connect-implicit-1_0.html

https://github.com/aspnet/Security

https://github.com/IdentityServer/IdentityServer4.AccessTokenValidation

http://www.filipekberg.se/2013/07/12/are-you-serving-files-insecurely-in-asp-net/

Announcing IdentityServer for ASP.NET 5 and .NET Core

https://github.com/IdentityServer/IdentityServer4

https://github.com/IdentityServer/IdentityServer4.Samples

The State of Security in ASP.NET 5 and MVC 6: OAuth 2.0, OpenID Connect and IdentityServer

http://connect2id.com/learn/openid-connect

https://github.com/FabianGosebrink/Angular2-ASPNETCore-SignalR-Demo

Getting Started with ASP NET Core 1 and Angular 2 in Visual Studio 2015

http://benjii.me/2016/01/angular2-routing-with-asp-net-core-1/

http://tattoocoder.azurewebsites.net/angular2-aspnet5-spa-template/

Cross-platform Single Page Applications with ASP.NET Core 1.0, Angular 2 & TypeScript

3 comments

  1. […] Angular2 secure file download without using an access token in URL or cookies – Damian Bowden takes a look at how you can perform secure file downloads using Angular2 […]

  2. As always Damien, you are the MAN!

    Keep up the amazing work. Every-time I see your blog it just gets better and better!

    Cheers

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: