Working with Box OpenAPI Clients
In May of this year, Box announced an OpenAPI specification for their API endpoints. The specification is available on their Github site, but in implementing it, I found few practical examples and no examples for C#/.NET.
To start with, you will need to get your hands on the swagger code generation tools. The repository is available on Github, but you’re better off just getting the .jar and be done with it:
1 |
http://central.maven.org/maven2/io/swagger/swagger-codegen-cli/2.2.3/swagger-codegen-cli-2.2.3.jar |
Once you’ve got the .jar and the .json files from the Box OpenAPI specification, place them all in the same folder and create another config.json file:
The config.json file contains the parameters for the code generator. You can specify the options at the command line as well, but here is the configuration format:
1 2 3 4 |
{ "packageName": "IC.He.Api.Box", "targetFramework" : "v3.5" } |
Note that I’m targeting .NET 3.5. The packageName parameter defines the namespace.
Because of the way the swagger codegen tools work, it will generate a bunch of duplicate class names in each of the namespaces. The easiest way to deal with this is to add a --model-name-prefix parameter for each of the libraries.
As an example
1 2 3 4 5 6 |
java -jar swagger-codegen-cli.jar generate ^ -i token.openapi-v2.json -l csharp ^ -o dotnet/token ^ -c config.json ^ --model-name-prefix Tk |
Once you’ve generated the code, you’ll need to make some minor corrections in the ApiClient class by replacing the code that attaches the file parameters to the outgoing request:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// add file parameter, if any foreach(var param in fileParams) { byte[] bytes; using (MemoryStream stream = new MemoryStream()) { param.Value.Writer.Invoke(stream); stream.Flush(); bytes = stream.GetBuffer(); } request.AddFile(param.Value.Name, bytes, param.Value.FileName, param.Value.ContentType); } |
Without this change, the code will send an empty HTTP request body and no content will be sent. You will receive the response error (in Fiddler or your HTTP trace tool of choice) of:
1 2 3 4 5 6 7 8 |
HTTP/1.1 408 Request body incomplete Date: Tue, 12 Sep 2017 20:51:31 GMT Content-Type: text/html; charset=UTF-8 Connection: close Cache-Control: no-cache, must-revalidate Timestamp: 16:51:31.782 The request body did not contain the specified number of bytes. Got 155, expected 206 |
Once you’ve got your libraries all ready, you’re good to go. The problem is that the documentation is somewhat sparse and incomplete. When you generate the client code, a bunch of samples are also generated under a docs directory in Markdown format. Here is the example for uploading a file to Box:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
using System; using System.Diagnostics; using IC.He.Sdk.Box.Upload.Api; using IC.He.Sdk.Box.Upload.Client; using IC.He.Sdk.Box.Upload.Model; namespace Example { public class UploadFileExample { public void main() { // Configure OAuth2 access token for authorization: OAuth2Security Configuration.Default.AccessToken = "YOUR_ACCESS_TOKEN"; var apiInstance = new FileUploadApi(); var file = new System.IO.Stream(); // System.IO.Stream | File to upload var attributes = attributes_example; // string | File attributes try { // Upload File FileList result = apiInstance.UploadFile(file, attributes); Debug.WriteLine(result); } catch (Exception e) { Debug.Print("Exception when calling FileUploadApi.UploadFile: " + e.Message ); } } } } |
The problem is that there is no example of attributes , which is a key property without which the API call will fail. Fortunately, we can find an example in the online documentation of the API.
Note that the parent ID should be 0 for the root folder.
Before we can get it all to work, we have to provide an access token and for that, we will need to create an app in Box. From the developer console, click Create New App. In this case, I’ll be selecting an Enterprise Integration since I intend to have a standalone service managing the content in a Box tenant. Once the app is created, there are a few key pieces of information that we will need:
- The Enterprise ID on the General page for the app.
- The Client ID and Client Secret on the Configuration page for the app.
- The Key ID on the Configuration page for the app.
For testing purposes, you can generate a developer token, however, this expires in 60 minutes:
For initial testing, this is fine. However, for the app to work as a service, we will need to use the Token API to create a JWT token to exchange for an access token. First is to switch the Authentication Model into OAuth 2.0 with JWT (Server Authentication). Next is to generate a public/private key pair by clicking on Generate a Public/Private Keypair. Once the key is generated, you’ll have the Key ID that I listed above.
This is where things start to get hairy! I suppose if you are using .NET 4.6 or .NET Core, your life will be peachy since there are plenty of great libraries on Nuget that you can use to create a JWT token and manage all of this complexity. For those of us still working on .NET 3.5, you’ll need to do a bit of work before you can generate a token!
First is that we will need to convert the PEM format private key obtained from Box into a format that can be consumed by the .NET RSA crypto provider which wants an XML string. The best way I found to do this was a piece of code by Michel Gallant. Paste your private key into a file and run the tool to create a file which will have the key in XML format. Next we need to create a valid JWT token. There’s lots of info on the web including the official specification so let’s just take a look at the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
static string MakeToken() { // Header Dictionary<string, string> headerTokens = new Dictionary<string, string> { {"alg", "RS256"}, {"typ", "JWT"}, {"kid", "[YOUR_KEY_ID_HERE]"} }; string header = headerTokens.ToJson(); Console.Out.WriteLine(header); string header64 = Base64UrlEncode(Encoding.UTF8.GetBytes(header)); // Payload Int32 exp = (Int32) (DateTime.UtcNow.AddSeconds(60).Subtract(new DateTime(1970, 1, 1))).TotalSeconds; Dictionary<string, object> payloadTokens = new Dictionary<string, object> { {"iss", "[YOUR_CLIENT_ID_HERE]"}, {"sub", "[YOUR_ENTERPRISE_ID_HERE]"}, {"box_sub_type", "enterprise"}, {"aud", "https://api.box.com/oauth2/token"}, {"jti", Guid.NewGuid().ToString("N")}, {"exp", exp} }; string payload = payloadTokens.ToJson(); Console.Out.WriteLine(payload); string payload64 = Base64UrlEncode(Encoding.UTF8.GetBytes(payload)); string keyXml = File.ReadAllText("box_key.xml"); RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(); rsa.FromXmlString(keyXml); string baseValue = string.Format("{0}.{1}", header64, payload64); byte[] encrypted = rsa.SignData(Encoding.UTF8.GetBytes(baseValue), "SHA256"); return string.Format("{0}.{1}.{2}", header64, payload64, Base64UrlEncode(encrypted)); } private static string Base64UrlEncode(byte[] input) { var output = Convert.ToBase64String(input); output = output.Split('=')[0]; // Remove any trailing '='s output = output.Replace('+', '-'); // 62nd char of encoding output = output.Replace('/', '_'); // 63rd char of encoding return output; } |
I’m using ServiceStack.Text to convert the dictionary to JSON. Bottom line: you don’t really need a fancy library to create a JWT token. If you have any doubts about the token, use the JWT.io debugger to check the validity of the token.
Once you have the token, we need to exchange it for an access token using the Token API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
string jwtToken = MakeToken(); string clientId = "[YOUR_CLIENT_ID]"; string clientSecret = "[YOUR_CLIENT_SECRET]"; string query = string.Format("grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={0}&client_id={1}&client_secret={2}", jwtToken, clientId, clientSecret); WebRequest request = HttpWebRequest.Create("https://api.box.com/oauth2/token"); request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; using (Stream stream = request.GetRequestStream()) { byte[] contents = Encoding.ASCII.GetBytes(query); stream.Write(contents, 0, contents.Length); } string accessToken; using (WebResponse response = request.GetResponse()) using(Stream stream = response.GetResponseStream()) { byte[] bytes = stream.ReadFully(); string responseBody = Encoding.UTF8.GetString(bytes); accessToken = responseBody.ToStringDictionary()["access_token"]; } |
Finally! You have an access token which can be used to use the API!
This will work fine if you are using it in a console application, but if you try to use it in a web application, you’ll run into two issues. The first is a cryptic error with a message “The system cannot find the file specified”. This is an issue with the permissions model of the application pool account configuration which can be fixed by loading the profile for the account. The second issue is an error message along the lines of “Invalid algorithm specified“. There are a lot of articles discussing this with various complicated fixes, but I found the easiest fix on a 9 year old Microsoft blog post (Anders Abel has a good explanation if you’re interested). The solution is to add the following to your web.config:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<configuration> <!-- ... other configuration data ... --> <mscorlib> <!-- ... other configuration data ... --> <cryptographySettings> <cryptoNameMapping> <cryptoClasses> <cryptoClass SHA256CSP="System.Security.Cryptography.SHA256CryptoServiceProvider, System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> </cryptoClasses> <nameEntry name="SHA256" class="SHA256CSP" /> <nameEntry name="SHA256CryptoServiceProvider" class="SHA256CSP" /> <nameEntry name="System.Security.Cryptography.SHA256CryptoServiceProvider" class="SHA256CSP" /> </cryptoNameMapping> </cryptographySettings> </mscorlib> </configuration> |
And with that, you should be able to use the OpenAPI generated clients! If you log in with your developer account, you won’t find the files under your files; you have to go into the admin console and access the files there under the app: