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:
- OAuth2 Implicit Flow with Angular and ASP.NET Core 1.0 IdentityServer4
- Authorization Policies and Data Protection with IdentityServer4 in ASP.NET Core
- Angular OpenID Connect Implicit Flow with IdentityServer4
- Angular2 OpenID Connect Implicit Flow with IdentityServer4
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.
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.
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
