This article demonstrates how to set up a Web API 2 excel file download using OAuth2 Implicit Flow. The application requires an Authorization Server and Identity Server V2 from Thinkteckture and also the excel Media Formatter from WebApiContrib. leastprivilege.com provided a lot of blogs which helped complete this article. Thanks for those blogs. The article should help as a simple Howto for this scenario.
Code: https://github.com/damienbod/ExcelFileExportWithOAuth2ImplicitFlow
OAuth2 Implicit Flow
The application uses the OAuth2 Implicit flow. This flow is defined here:
http://tools.ietf.org/id/draft-ietf-oauth-v2-31.html#rfc.section.4.2
Resource Server
The resource server is a simple MVC application which hosts a Web API 2 service. The api has one single method for exporting excel data. This export uses the WebApiContrib.Formatting.Xlsx library from Jordan Gray. The api method forces that excel is always returned no matter what is set in the Accept Header. This is not usually good practice as the client should decide in which format the data should be returned. I have forced this, so that it’s easy to use in the browser.
The method has 3 different attributes for security. These are not all required, it’s just to demonstrate what is possible. The scope attribute from Thinkteckture causes the AuthorizationManager.CheckAccess invocation. The ScopeAuthorize checks that the User has export scope rights and authorize just checks that the user is authorized.
using System; using System.Collections.Generic; using System.Net.Http.Headers; using System.Web.Http; using ExcelFileExportWithOAuth2ImplicitFlow.Models; using Thinktecture.IdentityModel.WebApi; namespace ExcelFileExportWithOAuth2ImplicitFlow.Controllers { public class ExcelExportController : ApiController { /// <summary> /// ResourceActionAuthorize: the AuthorizationManager CheckAccess method is called /// ScopeAuthorize: Checks that the user has the scope /// </summary> [ResourceActionAuthorize("export")] [ScopeAuthorize("export")] [Authorize] [HttpGet] [Route("api/ExcelExport/complete/{id}")] public IHttpActionResult GetAnimalsExportExcel(string id) { // force that this method always returns an excel document. Request.Headers.Accept.Clear(); Request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.ms-excel")); return Ok(books); } /// <summary> /// Use some demo data /// </summary> List<Book> books = new List<Book> { new Book { Id = 1, Name = "A way to Galway", Author = "Tom Moore", Rating = 5 }, new Book { Id = 2, Name = "As fast as a Berner", Author = "Bob Nobody", Rating = 4 }, new Book { Id = 3, Name = "Singing Loud", Author = "Tim Mooney", Rating = 3 }, new Book { Id = 4, Name = "Lookup", Author = "Philip Jones", Rating = 2 }, new Book { Id = 5, Name = "Fighting Molly", Author = "Patrick Host", Rating = 1 }, new Book { Id = 6, Name = "Calling home", Author = "Oliver McNearney", Rating = 6 }, }; } }
The Web API config adds the excel media formatter to the Formatters collection.
using System.Web.Http; using WebApiContrib.Formatting.Xlsx; namespace ExcelFileExportWithOAuth2ImplicitFlow { public static class WebApiConfig { public static HttpConfiguration Register() { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.Formatters.Add(new XlsxMediaTypeFormatter()); return config; } } }
The QueryStringOAuthBearerProvider is used to get the bearer token from the query string. The token cannot be provided as a header because the client requires a file download and cannot use an ajax method.
using Microsoft.Owin.Security.OAuth; using System.Threading.Tasks; namespace ExcelFileExportWithOAuth2ImplicitFlow.Security { /// <summary> /// This class is required if standard header bearer authentifaction cannot be used. This helper method helps get the resource from a different location. /// From http://leastprivilege.com/2013/10/31/retrieving-bearer-tokens-from-alternative-locations-in-katanaowin/ /// </summary> public class QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider { readonly string _name; public QueryStringOAuthBearerProvider(string name) { _name = name; } public override Task RequestToken(OAuthRequestTokenContext context) { var value = context.Request.Query.Get(_name); if (!string.IsNullOrEmpty(value)) { context.Token = value; } return Task.FromResult<object>(null); } } }
The AuthorizationManager would usually authorize the user/client context. This is not implemented in this demo.
using System.Security.Claims; namespace ExcelFileExportWithOAuth2ImplicitFlow.Security { public class AuthorizationManager : ClaimsAuthorizationManager { public override bool CheckAccess(AuthorizationContext context) { // This is where you can authorise the user if you // have special roles etc, database checks or whatever return true; } } }
The Startup class is used for the OWIN initialization and used for defining OWIN Middleware. The security middleware for Web API 2 is defined here. The const and definitions MUST match the Authorization and Identity server configurations. For example the SigningKey is defined on the Authorization.Server and copied here. If the client request does not match this or the Authorization.Server does not match, you will have authorization exceptions/problems.
Important is also that the query provider is defined in the jwtBearerAuthenticationOptions. Otherwise the default Provider is used and this only checks in the header for the Bearer token.
using System; using System.Collections.Generic; using System.IdentityModel.Tokens; using ExcelFileExportWithOAuth2ImplicitFlow.Security; using Microsoft.Owin; using Owin; using Thinktecture.IdentityModel; using Thinktecture.IdentityModel.Tokens; using Microsoft.Owin.Security.Jwt; [assembly: OwinStartup(typeof(ExcelFileExportWithOAuth2ImplicitFlow.Startup))] namespace ExcelFileExportWithOAuth2ImplicitFlow { public class Startup { public const string SigningKey = "Bp3sKtrLxjqjkqxulWMr32m6Hsfkwq49KD6KVHeWdvY="; public List<string> SecurityAudiences = new List<string>() { "ExcelFileExportWithOAuth2ImplicitFlow" }; public const string Issuer = "AS"; public const string Token = "token"; public void Configuration(IAppBuilder app) { // authorization manager ClaimsAuthorization.CustomAuthorizationManager = new AuthorizationManager(); // no mapping of incoming claims to Microsoft types JwtSecurityTokenHandler.InboundClaimTypeMap = ClaimMappings.None; // validate JWT tokens from AuthorizationServer var jwtBearerAuthenticationOptions = new JwtBearerAuthenticationOptions { AllowedAudiences = SecurityAudiences, IssuerSecurityTokenProviders = new List<IIssuerSecurityTokenProvider>() { new SymmetricKeyIssuerSecurityTokenProvider(Issuer, SigningKey) }, Provider = new QueryStringOAuthBearerProvider(Token) }; app.UseJwtBearerAuthentication(jwtBearerAuthenticationOptions); app.UseWebApi(WebApiConfig.Register()); } } }
The following NuGet packages are required for this demo:
<?xml version="1.0" encoding="utf-8"?> <packages> <package id="Antlr" version="3.5.0.2" targetFramework="net45" /> <package id="bootstrap" version="3.1.1" targetFramework="net45" /> <package id="EPPlus" version="3.1.3.3" targetFramework="net45" /> <package id="jQuery" version="2.1.0" targetFramework="net45" /> <package id="Microsoft.AspNet.Identity.Core" version="2.0.1" targetFramework="net45" /> <package id="Microsoft.AspNet.Identity.Owin" version="2.0.1" targetFramework="net45" /> <package id="Microsoft.AspNet.Mvc" version="5.1.2" targetFramework="net45" /> <package id="Microsoft.AspNet.Razor" version="3.1.2" targetFramework="net45" /> <package id="Microsoft.AspNet.Web.Optimization" version="1.1.3" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi" version="5.1.2" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.Client" version="5.1.2" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.Core" version="5.1.2" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.HelpPage" version="5.1.2" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.Owin" version="5.1.2" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.WebHost" version="5.1.2" targetFramework="net45" /> <package id="Microsoft.AspNet.WebPages" version="3.1.2" targetFramework="net45" /> <package id="Microsoft.Owin" version="2.1.0" targetFramework="net45" /> <package id="Microsoft.Owin.Host.SystemWeb" version="2.1.0" targetFramework="net45" /> <package id="Microsoft.Owin.Security" version="2.1.0" targetFramework="net45" /> <package id="Microsoft.Owin.Security.Cookies" version="2.1.0" targetFramework="net45" /> <package id="Microsoft.Owin.Security.Jwt" version="2.1.0" targetFramework="net45" /> <package id="Microsoft.Owin.Security.OAuth" version="2.1.0" targetFramework="net45" /> <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net45" /> <package id="Modernizr" version="2.7.2" targetFramework="net45" /> <package id="Newtonsoft.Json" version="6.0.3" targetFramework="net45" /> <package id="Owin" version="1.0" targetFramework="net45" /> <package id="Respond" version="1.4.2" targetFramework="net45" /> <package id="System.IdentityModel.Tokens.Jwt" version="3.0.2" targetFramework="net45" /> <package id="Thinktecture.IdentityModel.Core" version="1.1.0" targetFramework="net45" /> <package id="Thinktecture.IdentityModel.WebApi" version="1.1.0" targetFramework="net45" /> <package id="WebGrease" version="1.6.0" targetFramework="net45" /> </packages>
Setting up the Https
The application requires an ExcelFileExportWithOAuth2ImplicitFlow certificate to run. See this Link for help. Then host your application with the same name as the cert to test. If you use a different URL, the example code and server configuration need to be changed.
Identity Server
The application uses Thinktecture.IdentityServer.v2. An exportuser needs to be configured as also the Authorisation.Server. Thinktecture provide good documentation on how to set up this system. If it’s your first time setting up the system, configure exactly like the help videos with the demo configuration.
For the example, the Relying Party is configured to point to the Authorization.Server found at https://root/authz.
The exportuser is also required for the example.
Authorization Server
The Authorization.Server from Thinkteckture is also used. This is used to configure the OAuth2 Flow. The configuration must match the client and the resource server code.
The Server is configuration as follows:
A key like this is required for the demo. (If you create a new key yourself, then copy it to the SigningKey value in your Resource server; Startup class)
A client configuration is then required for the exportuser. The exportuser needs to be defined and also the callback URL for this client:
The application is configured as follows:
The scope must include an export scope as this is what is used by the client and also the resource server.
The client exportuser also requires rights for this scope in the application.
Client Application
The client application is included in the resource server application. If it is a different application, CORS should be enabled.
The client application uses the resource. The authorization URL contains the Authorization Audience defined in the Startup class and also the AuthorizationServer. (ExcelFileExportWithOAuth2ImplicitFlow for this demo.) The callback method must also match the resource server URL and also the Authorization.Server definition. You can see, that if anything is badly configured on the server or the Resource Server, or the Authorization Server, or the Identity Server, the OAuth2 will not authorize…
<div class="row"> <br /> <p> <button id="authButton">Download Excel file using OAuth Implicit Flow</button> </p> </div> @section scripts { <script> $(function () { var authorizationUrl = 'https://root/authz/ExcelFileExportWithOAuth2ImplicitFlow/oauth/authorize'; var client_id = 'exportuser'; var redirect_uri = 'https://ExcelFileExportWithOAuth2ImplicitFlow/home/download'; var response_type = "token"; var scope = "export"; var state = Date.now() + "" + Math.random(); $("#authButton").click(function () { var url = authorizationUrl + "?" + "client_id=" + encodeURI(client_id) + "&" + "redirect_uri=" + encodeURI(redirect_uri) + "&" + "response_type=" + encodeURI(response_type) + "&" + "scope=" + encodeURI(scope) + "&" + "state=" + encodeURI(state); sessionStorage["state"] = state; window.location = url; }); }); </script> }
Once the user has logged in, the Authorization server returns the client to the download page with a bearer token. The client then sends a download request with the token as a parameter. Now the user can download the file. Easy…
<!DOCTYPE html> <html> <head> <title>title</title> <script src="~/Scripts/jquery-2.1.0.js"></script> <script> $(function () { var params = {}, queryString = location.hash.substring(1), regex = /([^&=]+)=([^&]*)/g, m; while (m = regex.exec(queryString)) { params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); } var token = params.access_token; document.getElementById("token").value = token; }); </script> </head> <body> <br/> <div> <form action="https://ExcelFileExportWithOAuth2ImplicitFlow/api/ExcelExport/complete/completeExportId" method="get"> <input type="hidden" name="token" id="token" value="noneset" /> <input type="submit" value="download file" /> </form> </div> </body> </html>
Notes:
- This is just a demo, example project for reference; OAuth2 with file downloads.
- The Application has no proper Authenication.
- If your resource server delivers json or xml, you can use ajax and add the Authorization header to the request. Lots of example exist already for this.
- Identity Server V3 will be released soon and includes better security features.
- It would be easy to use a different OAuth2 server like google. Lots of examples exist for this.
- If you use OAuth2, you must use at least HTTPS for security reasons.
- This example is open to impersonation attacks
- Should add OpenId support and secure tokens
Thanks to the Thinkteckture for the great work in this area.
Thanks to Jordan Gray for providing WebApiContrib.Formatting.Xlsx
Links:
https://github.com/thinktecture/Thinktecture.IdentityServer.v2
https://github.com/thinktecture/Thinktecture.AuthorizationServer
https://github.com/thinktecture
http://www.beabigrockstar.com/
http://www.thread-safe.com/2012/02/more-on-oauth-implicit-flow-application.html
https://drupal.org/node/1958718
http://dotnetcodr.com/2014/01/30/introduction-to-oauth2-part-4-the-implicit-flow/
https://auth0.com/blog/2014/01/27/ten-things-you-should-know-about-tokens-and-cookies/
Reblogged this on leastprivilege.com.