Quantcast
Channel: ASP.NET Core – Software Engineering
Viewing all articles
Browse latest Browse all 269

Secure file download using IdentityServer4, Angular2 and ASP.NET Core

$
0
0

This article shows how a secure file download can be implemented using Angular 2 with an OpenID Connect Implicit Flow using IdentityServer4. The resource server needs to process the access token in the query string and the NuGet package IdentityServer4.AccessTokenValidation makes it very easy to support this. The default security implementation jwtBearerHandler reads the token from the header.

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

Other posts in this series:

The Secure File Resource Server

The required packages for the resource server are defined in the project.json file in the dependencies. The authorization packages and the IdentityServer4.AccessTokenValidation package need to be added.

"dependencies": {
	"Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
	"Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
	"Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
	"Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
	"Microsoft.Extensions.Configuration.FileProviderExtensions": "1.0.0-rc1-final",
	"Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
	"Microsoft.Extensions.Logging": "1.0.0-rc1-final",
	"Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final",
	"Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final",

	"Microsoft.AspNet.Authorization": "1.0.0-rc1-final",
	"Microsoft.AspNet.Authentication.JwtBearer": "1.0.0-rc1-final",
	"Microsoft.AspNet.Cors": "6.0.0-rc1-final",
	"Microsoft.AspNet.Diagnostics": "1.0.0-rc1-final",
	"IdentityServer4.AccessTokenValidation": "1.0.0-beta3"
},

The UseIdentityServerAuthentication extension from the NuGet IdentityServer4.AccessTokenValidation package can be used to read the access token from the query string. Normally this is done in the HTTP headers, but for file upload, this is not so easy, if your not using cookies. As the application uses OpenID Connect Implicit Flow, tokens are being used. The options.TokenRetriever = TokenRetrieval.FromQueryString() is used to configure the ASP.NET Core middleware to authenticate and authorize using the access token in the query string.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	loggerFactory.AddConsole(Configuration.GetSection("Logging"));
	loggerFactory.AddDebug();

	app.UseIISPlatformHandler();

	app.UseCors("corsGlobalPolicy");

	app.UseStaticFiles();

	JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
	app.UseIdentityServerAuthentication(options =>
	{
		options.Authority = "https://localhost:44345/";
		options.ScopeName = "securedFiles";
		options.ScopeSecret = "securedFilesSecret";

		options.AutomaticAuthenticate = true;
		// required if you want to return a 403 and not a 401 for forbidden responses
		options.AutomaticChallenge = true;
		options.TokenRetriever = TokenRetrieval.FromQueryString();
	});
	app.UseMvc();
}

An AuthorizeFilter is used to validate if the requesting access token has the scope “securedFiles”. The “securedFilesUser” policy is used to validate that the requesting token has the role “securedFiles.user”. The two policies are used in the MVC6 controllers as attributes.

var securedFilesPolicy = new AuthorizationPolicyBuilder()
	.RequireAuthenticatedUser()
	.RequireClaim("scope", "securedFiles")
	.Build();

services.AddAuthorization(options =>
{
	options.AddPolicy("securedFilesUser", policyUser =>
	{
		policyUser.RequireClaim("role", "securedFiles.user");
	});
});

The FileExplorerController is used to return the possible secure files which can be downloaded. The authorization policies which were defined in the startup class are used here. If the requesting token has the claim “securedFiles.admin”, all files will be returned in the payload of the HTTP GET.

using System.Linq;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Authorization;
using Microsoft.Extensions.PlatformAbstractions;
using ResourceFileServer.Providers;

namespace ResourceFileServer.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    public class FileExplorerController : Controller
    {
        private readonly IApplicationEnvironment _appEnvironment;
        private readonly ISecuredFileProvider _securedFileProvider;

        public FileExplorerController(ISecuredFileProvider securedFileProvider, IApplicationEnvironment appEnvironment)
        {
            _securedFileProvider = securedFileProvider;
            _appEnvironment = appEnvironment;
        }

        [Authorize("securedFilesUser")]
        [HttpGet]
        public IActionResult Get()
        {
            var adminClaim = User.Claims.FirstOrDefault(x => x.Type == "role" && x.Value == "securedFiles.admin");
            var files = _securedFileProvider.GetFilesForUser(adminClaim != null);

            return Ok(files);
        }
    }
}

The DownloadController is used for file download requests. The requesting token must have the claim of type scope and value “securedFiles” and also the claim of type role and the value “securedFiles.user”. In the demo application, one file requires the claim with type role and value “securedFiles.admin”.

The GET method checks if the file exists. If it does not exist, a 400 response is returned. It then checks, if the requesting token has the authorization to access the file. If this is ok, the file is returned as an “application/octet-stream” response.

using System.Linq;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Authorization;
using Microsoft.Extensions.PlatformAbstractions;
using ResourceFileServer.Providers;

namespace ResourceFileServer.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    public class DownloadController : Controller
    {
        private readonly IApplicationEnvironment _appEnvironment;
        private readonly ISecuredFileProvider _securedFileProvider;

        public DownloadController(ISecuredFileProvider securedFileProvider, IApplicationEnvironment appEnvironment)
        {
            _securedFileProvider = securedFileProvider;
            _appEnvironment = appEnvironment;
        }

        [Authorize("securedFilesUser")]
        [HttpGet("{id}")]
        public IActionResult Get(string id)
        {
            var filePath = $"{_appEnvironment.ApplicationBasePath}/SecuredFileShare/{id}";
            if(!System.IO.File.Exists(filePath))
            {
                return HttpBadRequest($"File does not exist: {id}");
            }

            var adminClaim = User.Claims.FirstOrDefault(x => x.Type == "role" && x.Value == "securedFiles.admin");
            if(_securedFileProvider.HasUserClaimToAccessFile(id, adminClaim != null))
            {
                var fileContents = System.IO.File.ReadAllBytes(filePath);
                return new FileContentResult(fileContents, "application/octet-stream");
            }

            return HttpUnauthorized();
        }
    }
}

Angular 2 client

The Angular 2 application uses the SecureFileService to access the server APIs. The access_token parameter is added to the query string for the file download resource server. This is different to the standard way of adding the access token to the header. The GetDownloadfileUrl method is used to create the URL for the download link.

import { Injectable } from 'angular2/core';
import { Http, Response, Headers } from 'angular2/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;

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

    public GetDownloadfileUrl(id: string): string {
        var token = this._securityService.GetToken();
        return this.actionUrl + id + "?access_token=" + token;
    }

    public GetListOfFiles = (): Observable<string[]> => {
        var token = this._securityService.GetToken();

        return this._http.get(this.fileExplorerUrl + "?access_token=" + token, {
        }).map(res => res.json());
    }

}

The SecureFilesComponent is used to open a new window and get the secure file from the server using the URL created in the SecureFileService GetDownloadfileUrl method.

import { Component, OnInit } from 'angular2/core';
import { CORE_DIRECTIVES } from 'angular2/common';
import { SecureFileService } from '../services/SecureFileService';
import { SecurityService } from '../services/SecurityService';
import { Observable }       from 'rxjs/Observable';
import { Router } from 'angular2/router';

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

export class SecureFilesComponent implements OnInit {

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

    ngOnInit() {
      this.getData();
    }

    public GetFileById(id: any) {
        window.open(this._secureFileService.GetDownloadfileUrl(id));
    }

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

After a successful login, the available files are displayed in a HTML table.
secureFile_download_01

The file can be downloaded using the access token. If a non-authorized user tries to download a file, a 403 will be returned or if an incorrect access token or no access token is used in the HTTP request, a 401 will be returned.

secureFile_download_02
Notes:

Using IdentityServer4.AccessTokenValidation, support for access tokens in the query string is very easy to implement in an ASP.NET Core application. One problem, is when support for tokens in both the request header and the query string needs to be supported in one web application.

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

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



Viewing all articles
Browse latest Browse all 269

Trending Articles