JWT without SDKs

JWT without SDKs

If you are not ready to use any of the official Box SDKs, or an SDK is not available in your language of choice, it is totally possible to use the Box APIs without them.

This guide will take you through user authentication using JWT without the use of the Box SDKs. JWT authentication is designed for working directly with the Box API without requiring a user to redirect through Box to authorize your application.

Overview

To complete a JWT authorization the following steps need to be completed.

  1. Read the configuration file
  2. Decrypt the private key
  3. Create the JWT assertion
  4. Request the Access Token

At the end of this flow, the application has an Access Token that can be used to make API calls on behalf of the application.

The access token acquired through JWT is inherently tied to the Service Account for the application. Any API call made with this token will seem to come from this application and will not have access to files and folders from other users without explicitly getting access them.

It is possible to act as another user using the as-user header or by requesting a User Access Token.

Prerequisites

Before we can get started, you will need to have completed the following steps.

  • Create a Box Application within the developer console
  • Create and download the private key configuration file for your application and save it as config.json
  • Ensure your Box Application is approved for usage within your enterprise

1. Read JSON configuration

After creating a Box Application there should be a config.json file containing the application's private key and other details. The following is an example.

config.json
{
  "boxAppSettings": {
    "clientID": "abc...123",
    "clientSecret": "def...234",
    "appAuth": {
      "publicKeyID": "abcd1234",
      "privateKey": "-----BEGIN ENCRYPTED PRIVATE KEY-----\n....\n-----END ENCRYPTED PRIVATE KEY-----\n",
      "passphrase": "ghi...345"
    }
  },
  "enterpriseID": "1234567"
}

To use this object in the application it needs to be read from file.

.Net
using System;
using System.IO;
using Newtonsoft.Json;

class Config
{
    public class BoxAppSettings {
        public class AppAuth {
            public string privateKey { get; set; }
            public string passphrase { get; set; }
            public string publicKeyID { get; set; }
        }
        public string clientID { get; set; }
        public string clientSecret { get; set; }
        public AppAuth appAuth { get; set; }

    }
    public string enterpriseID { get; set; }
    public BoxAppSettings boxAppSettings { get; set; }
}

var reader = new StreamReader("config.json");
var json = reader.ReadToEnd();

var config = JsonConvert.DeserializeObject<Config>(json);
Java
import java.io.FileReader;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

class Config {
  class BoxAppSettings {
    class AppAuth {
      String privateKey;
      String passphrase;
      String publicKeyID;
    }

    String clientID;
    String clientSecret;
    AppAuth appAuth;
  }

  BoxAppSettings boxAppSettings;
  String enterpriseID;
}

FileReader reader = new FileReader("config.json");

Gson gson = new GsonBuilder().create();
Config config = (Config) gson.fromJson(reader, Config.class);
Python
import json
import os

config = json.load(open('config.json'))
Node
const fs = require("fs");

const config = JSON.parse(fs.readFileSync("config.json"));
Ruby
require 'json'

config = JSON.parse(
  File.read('config.json')
)
PHP
$json = file_get_contents('config.json');
$config = json_decode($json);

Parsing JSON

In some programming languages there is more than one way to read and parse JSON from a file. Refer to guides on your preferred programming language for more complete guides, including error handling.

2. Decrypt private key

To create the JWT assertion the application needs the private key from the configuration object. This private key is encrypted and requires a passcode to unlock. Both the encrypted key and passcode are provided in the configuration object.

.Net
using System.Security.Cryptography;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;

// https://www.bouncycastle.org/csharp/index.html
class PasswordFinder : IPasswordFinder
{
  private string password;
  public PasswordFinder(string _password) { password = _password; }
  public char[] GetPassword() { return password.ToCharArray(); }
}

var appAuth = config.boxAppSettings.appAuth;
var stringReader = new StringReader(appAuth.privateKey);
var passwordFinder = new PasswordFinder(appAuth.passphrase);
var pemReader = new PemReader(stringReader, passwordFinder);
var keyParams = (RsaPrivateCrtKeyParameters) pemReader.ReadObject();

public RSA CreateRSAProvider(RSAParameters rp)
{
  var rsaCsp = RSA.Create();
  rsaCsp.ImportParameters(rp);
  return rsaCsp;
}

public RSAParameters ToRSAParameters(RsaPrivateCrtKeyParameters privKey)
{
  RSAParameters rp = new RSAParameters();
  rp.Modulus = privKey.Modulus.ToByteArrayUnsigned();
  rp.Exponent = privKey.PublicExponent.ToByteArrayUnsigned();
  rp.P = privKey.P.ToByteArrayUnsigned();
  rp.Q = privKey.Q.ToByteArrayUnsigned();
  rp.D = ConvertRSAParametersField(privKey.Exponent, rp.Modulus.Length);
  rp.DP = ConvertRSAParametersField(privKey.DP, rp.P.Length);
  rp.DQ = ConvertRSAParametersField(privKey.DQ, rp.Q.Length);
  rp.InverseQ = ConvertRSAParametersField(privKey.QInv, rp.Q.Length);
  return rp;
}

public byte[] ConvertRSAParametersField(BigInteger n, int size)
{
  byte[] bs = n.ToByteArrayUnsigned();
  if (bs.Length == size)
      return bs;
  if (bs.Length > size)
      throw new ArgumentException("Specified size too small", "size");
  byte[] padded = new byte[size];
  Array.Copy(bs, 0, padded, size - bs.Length, bs.Length);
  return padded;
}

var key = CreateRSAProvider(ToRSAParameters(keyParams));
Java
import java.io.StringReader;
import java.security.PrivateKey;
import java.security.Security;

import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;

// https://www.bouncycastle.org/java.html
Security.addProvider(new BouncyCastleProvider());

PEMParser pemParser = new PEMParser(
  new StringReader(config.boxAppSettings.appAuth.privateKey)
);
Object keyPair = pemParser.readObject();
pemParser.close();

char[] passphrase = config.boxAppSettings.appAuth.passphrase.toCharArray();
JceOpenSSLPKCS8DecryptorProviderBuilder decryptBuilder =
  new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider("BC");
InputDecryptorProvider decryptProvider
  = decryptBuilder.build(passphrase);
PrivateKeyInfo keyInfo
  = ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(decryptProvider);

PrivateKey key = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
Python
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key

appAuth = config["boxAppSettings"]["appAuth"]
privateKey = appAuth["privateKey"]
passphrase = appAuth["passphrase"]

# https://cryptography.io/en/latest/
key = load_pem_private_key(
  data=privateKey.encode('utf8'),
  password=passphrase.encode('utf8'),
  backend=default_backend(),
)
Node
let key = {
  key: config.boxAppSettings.appAuth.privateKey,
  passphrase: config.boxAppSettings.appAuth.passphrase
};
Ruby
require "openssl"

appAuth = config['boxAppSettings']['appAuth']
key = OpenSSL::PKey::RSA.new(
  appAuth['privateKey'],
  appAuth['passphrase']
)
PHP
$private_key = $config->boxAppSettings->appAuth->privateKey;
$passphrase = $config->boxAppSettings->appAuth->passphrase;
$key = openssl_pkey_get_private($private_key, $passphrase);

An alternative to loading private key from file

the application might not want to keep both the private key and password stored on disk. An alternative option would be to pass in the password as an environment variable, separating the private key from the token used to unlock the key.

3. Create JWT assertion

To authenticate to the Box API the application needs to create a signed JWT assertion that can be exchanged for a traditional OAuth 2.0 Access Token.

A JWT assertion is essentially an encrypted JSON object, consisting of a header, claims, and signature. Let's start by creating the claims, sometimes also referred to as the payload.

.Net
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Collections.Generic;

byte[] randomNumber = new byte[64];
RandomNumberGenerator.Create().GetBytes(randomNumber);
var jti = Convert.ToBase64String(randomNumber);

DateTime expirationTime = DateTime.UtcNow.AddSeconds(45);

var claims = new List<Claim>{
  new Claim("sub", config.enterpriseID),
  new Claim("box_sub_type", "enterprise"),
  new Claim("jti", jti),
};
Java
import org.jose4j.jwt.JwtClaims;

String authenticationUrl = "https://api.box.com/oauth2/token";

JwtClaims claims = new JwtClaims();
claims.setIssuer(config.boxAppSettings.clientID);
claims.setAudience(authenticationUrl);
claims.setSubject(config.enterpriseID);
claims.setClaim("box_sub_type", "enterprise");
claims.setGeneratedJwtId(64);
claims.setExpirationTimeMinutesInTheFuture(0.75f);
Python
import time
import secrets

authentication_url = 'https://api.box.com/oauth2/token'

claims = {
  'iss': config['boxAppSettings']['clientID'],
  'sub': config['enterpriseID'],
  'box_sub_type': 'enterprise',
  'aud': authentication_url,
  'jti': secrets.token_hex(64),
  'exp': round(time.time()) + 45
}
Node
const crypto = require("crypto");

const authenticationUrl = "https://api.box.com/oauth2/token";

let claims = {
  iss: config.boxAppSettings.clientID,
  sub: config.enterpriseID,
  box_sub_type: "enterprise",
  aud: authenticationUrl,
  jti: crypto.randomBytes(64).toString("hex"),
  exp: Math.floor(Date.now() / 1000) + 45
};
Ruby
require 'securerandom'

authentication_url = 'https://api.box.com/oauth2/token'

claims = {
  iss: config['boxAppSettings']['clientID'],
  sub: config['enterpriseID'],
  box_sub_type: 'enterprise',
  aud: authentication_url,
  jti: SecureRandom.hex(64),
  exp: Time.now.to_i + 45
}
PHP
$authenticationUrl = 'https://api.box.com/oauth2/token';

$claims = [
  'iss' => $config->boxAppSettings->clientID,
  'sub' => $config->enterpriseID,
  'box_sub_type' => 'enterprise',
  'aud' => $authenticationUrl,
  'jti' => base64_encode(random_bytes(64)),
  'exp' => time() + 45,
  'kid' => $config->boxAppSettings->appAuth->publicKeyID
];
ParameterTypeDescription
iss, requiredStringThe Box Application's OAuth client ID
sub, requiredStringThe Box Enterprise ID if this app is to act on behalf of the Service Account of that application, or the User ID if this app wants to act on behalf of another user.
box_sub_type, requiredStringenterprise or user depending on the type of token being requested in the sub claim
aud, requiredStringAlways https://api.box.com/oauth2/token
jti, requiredStringA universally unique identifier specified by the application for this JWT. A unique string of at least 16 characters and at most 128 characters.
exp, requiredIntegerThe Unix time when this JWT is to expire. Can be set to a maximum value of 60 seconds beyond the issue time. It is recommended to set this to less than the maximum allowed.
iat, optionalIntegerIssued at time. The token cannot be used before this time.
nbf, optionalIntegerNot before. Not Specifies when the token will start being valid.

Next, these claims need to be signed using the private key. Depending on the language and library used, the header of the JWT is configured by defining the encryption algorithm and the ID of the public key used to sign the claims.

.Net
using Microsoft.IdentityModel.Tokens;

String authenticationUrl = "https://api.box.com/oauth2/token";

var payload = new JwtPayload(
  config.boxAppSettings.clientID,
  authenticationUrl,
  claims,
  null,
  expirationTime
);

var credentials = new SigningCredentials(
  new RsaSecurityKey(key),
  SecurityAlgorithms.RsaSha512
);
var header = new JwtHeader(signingCredentials: credentials);

var jst = new JwtSecurityToken(header, payload);
var tokenHandler = new JwtSecurityTokenHandler();
string assertion = tokenHandler.WriteToken(jst);
Java
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;

JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
jws.setKey(key);

jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA512);
jws.setHeader("typ", "JWT");
jws.setHeader("kid", config.boxAppSettings.appAuth.publicKeyID);
String assertion = jws.getCompactSerialization();
Python
import jwt

keyId = config['boxAppSettings']['appAuth']['publicKeyID']

assertion = jwt.encode(
  claims,
  key,
  algorithm='RS512',
  headers={
    'kid': keyId
  }
)
Node
const jwt = require('jsonwebtoken')

let keyId = config.boxAppSettings.appAuth.publicKeyID

let headers = {
  'algorithm': 'RS512',
  'keyid': keyId,
}

let assertion = jwt.sign(claims, key, headers)
Ruby
require 'jwt'
keyId = appAuth['publicKeyID']
assertion = JWT.encode(claims, key, 'RS512', { kid: keyId })
PHP
use \Firebase\JWT\JWT;
$assertion = JWT::encode($claims, $key, 'RS512');

For the header the following parameters are supported.

ParameterTypeDescription
algorithm, requiredStringThe encryption algorithm used to sign the JWT claim. This can be one of RS256, RS384, or RS512.
keyid, requiredStringThe ID of the public key used to sign the JWT. Not required, though essential when multiple key pairs are defined for an application.

Using JWT libraries

Signing your own JWT can be a complicated and painful process. Luckily, the hard work has already been done for you and libraries exist in pretty much every language. Head over to JWT.io for an overview.

4. Request Access Token

The final step is to exchange the short lived JWT assertion for a more long lived OAuth 2.0 Access Token by calling the authentication endpoint with the assertion as a parameter.

.Net
using System.Net;
using System.Net.Http;

var content = new FormUrlEncodedContent(new[]
{
  new KeyValuePair<string, string>(
    "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
  new KeyValuePair<string, string>(
    "assertion", assertion),
  new KeyValuePair<string, string>(
    "client_id", config.boxAppSettings.clientID),
  new KeyValuePair<string, string>(
    "client_secret", config.boxAppSettings.clientSecret)
});

var client = new HttpClient();
var response = client.PostAsync(authenticationUrl, content).Result;

class Token
{
  public string access_token { get; set; }
}

var data = response.Content.ReadAsStringAsync().Result;
var token = JsonConvert.DeserializeObject<Token>(data);
var accessToken = token.access_token;
Java
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

List<NameValuePair> params = new ArrayList<NameValuePair>();

params.add(new BasicNameValuePair(
  "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"));
params.add(new BasicNameValuePair(
  "assertion", assertion));
params.add(new BasicNameValuePair(
  "client_id", config.boxAppSettings.clientID));
params.add(new BasicNameValuePair(
  "client_secret", config.boxAppSettings.clientSecret));

CloseableHttpClient httpClient =
  HttpClientBuilder.create().disableCookieManagement().build();
HttpPost request = new HttpPost(authenticationUrl);
request.setEntity(new UrlEncodedFormEntity(params));
CloseableHttpResponse httpResponse = httpClient.execute(request);
HttpEntity entity = httpResponse.getEntity();
String response = EntityUtils.toString(entity);
httpClient.close();

class Token {
  String access_token;
}

Token token = (Token) gson.fromJson(response, Token.class);
String accessToken = token.access_token;
Python
import json

from urllib.request import urlopen
from urllib.request import Request
from urllib.parse import urlencode

params = urlencode({
  'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
  'assertion': assertion,
  'client_id': config['boxAppSettings']['clientID'],
  'client_secret': config['boxAppSettings']['clientSecret']
}).encode()

request = Request(authentication_url, params)
response = urlopen(request).read()
access_token = json.loads(response)['access_token']
Node
const axios = require('axios')
const querystring = require('querystring');

let accessToken = await axios.post(
  authenticationUrl,
  querystring.stringify({
    grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
    assertion: assertion,
    client_id: config.boxAppSettings.clientID,
    client_secret: config.boxAppSettings.clientSecret
  })
)
.then(response => response.data.access_token)
Ruby
require 'json'
require 'uri'
require 'net/https'

params = URI.encode_www_form({
  grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
  assertion: assertion,
  client_id: config['boxAppSettings']['clientID'],
  client_secret: config['boxAppSettings']['clientSecret']
})

uri = URI.parse(authentication_url)
http = Net::HTTP.start(uri.host, uri.port, use_ssl: true)
request = Net::HTTP::Post.new(uri.request_uri)
request.body = params
response = http.request(request)

access_token = JSON.parse(response.body)['access_token']
PHP
use GuzzleHttp\Client;

$params = [
  'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
  'assertion' => $assertion,
  'client_id' => $config->boxAppSettings->clientID,
  'client_secret' => $config->boxAppSettings->clientSecret
];

$client = new Client();
$response = $client->request('POST', $authenticationUrl, [
  'form_params' => $params
]);

$data = $response->getBody()->getContents();
$access_token = json_decode($data)->access_token;

Summary

By now the application should be able to authorize an application using JWT without using any of the SDKs, by using the following steps.

  1. Read the configuration file
  2. Decrypt the private key
  3. Create the JWT assertion
  4. Request the Access Token

To learn how to use this token head over to the guide on Making API calls.

Code Samples

All of the code in this guide is available on GitHub.