DPoP #3: Implementing DPoP client side with javascript

In the two previous posts in my DPoP blog series i looked a little closer at the DPoP specification, and implemented the Authorization Server side of the spec using Duende IdentityServer.

In this post I will describe how things went when I implemented a javascript client that uses the DPoP mechanism.

How did it go, you ask?

Well, to quote Colonel Kurtz in Apocolypse Now: “The horror.. The horror..”

Source code and a demo

I’m not really sure that sharing my code for this blog post is a good idea for my future career.. But I’ll take my chances. Before you proceed, just a quick word of advice: Don’t use my code as a reference for any production system :). I know I wouldn’t.

With that said, you’ll find the source code for my client implementation here: https://github.com/udelt/dpop_js_test

And you’ll find a demo of the client here: https://dpoptest.z1.web.core.windows.net/index.htm.

Patience is a virtue

The Authorization Server and the API for the demo runs on “cold” Azure instances, and will take a moment to load if it hasn’t been accessed in a while. So, you might need to be patient while waiting for a response from the Duende IdentityServer and the API in the DPoP client demo.

Developing a DPoP enabled client

I haven’t coded any javascript in many years, so I decided that this was a great opportunity to catch up.

Initial attempts

  • I first looked at Filip Skokan’s POC implementation of DPoP - aaaaand.. I didn’t understand a thing…
  • I then looked at OIDC/OAuth client libraries - aaaaaand… I didn’t understand a thing

Bummer.. I quickly realized that I needed to focus on learning some javascript, so I started reading the Mozilla documentation pages to understand some of the new concepts in javascript.

I never ride on the same day that I saddle….

To make a long story short, I ended up spending a lot more time on learning javascript than I anticipated. I’m not convinced that this was a great investment since I really don’t do any work developing javascript applications. Well - I’m sure I’ll have no trouble quickly unlearning what I’ve just learned.

Keeping it simple

After my intense javascript crash-course I was running extremely low on self-esteem, and decided on the following:

  • Steal what I can from Filip Skokan
  • No frameworks - just pure javascript (ES6)
  • No existing OAuth/OIDC client side library
  • Only use a simple OAuth flow: client_credentials
  • There will be no client secret
  • There will be no PKCE
  • I’ll spend no time on responsive html
  • Using Node as development server
  • Deploy the client to Azure Storage (Static Web Site)
  • Using Visual Studio Code as IDE
  • Only test on chromium based browsers
  • All crypto mechanisms using the Web Crypto API in the browser

The demo

Welcome to the demo!

Herein lies the most central pieces of the client demo code.

Generating the key material

The first step in the DPoP flow is to generate the neccessary key material.

The key is used to sign the DPoP proof, and the public key will be transferred to the AS in the jose header in a jwk structure.

A friendly expert pointed out to me that there is no need to export the private key. I’ve now set the extractable parameter to false. Now, in case of an attack, the key material is kept safe.

This job was easily acheived with the SubtleCrypto interface. In this example the algorithm and curve is hard coded, but it is easy to extend with support for other algorithms.

export async function generateKey() {
    var key = await crypto.subtle.generateKey({
            name: "ECDSA",
            namedCurve: "P-384"
        }, false, ["sign", "verify"])
        .then(function(eckey) {           
            return eckey;
        })
        .catch(function(err) {
            console.error(err);
        });
    return key;
}

Creating the DPoP proof

When the key is generated we can use it to create the DPoP proof that the client presents to the Authorization Server.

This code example basically shows the final part of creating the DPoP proof, and I’ve more or less copied it directly from Filip Skokans DPoP POC.

export async function createDpopProof(privateKey, header, payload) {
    const p = JSON.stringify(payload);
    const h = JSON.stringify(header);

    const partialToken = [
        Base64Url.ToBase64Url(Base64Url.utf8ToUint8Array(h)),
        Base64Url.ToBase64Url(Base64Url.utf8ToUint8Array(p)),
    ].join(".");

    const messageAsUint8Array = Base64Url.utf8ToUint8Array(partialToken);

    var signatureAsBase64 = await Crypto.Sign(privateKey, messageAsUint8Array);

    var token = `${partialToken}.${signatureAsBase64}`;

    return token;        
}

The signature is generated by using the SubtleCrypto interface, which felt quite intuitive. The part that felt a bit weird was the Base64Url formatting, and the lack of conversion support in the browser.

export async function Sign(privateKey, messageAsUint8Array){
    var signature = await crypto.subtle.sign({
            name: "ECDSA",
            hash: { name: "SHA-256" },
            },
            privateKey,
            messageAsUint8Array)
        .then(function(signature) {
            const signatureAsBase64 = Base64.ToBase64Url(new Uint8Array(signature));
            return signatureAsBase64;
        })
        .catch(function(err){
            console.log(err);
            throw(err);
        });
    return signature;
};

Requesting the Access Token with the DPoP proof

The token request code is so simplified that it isn’t really useful for other things than showing how the DPoP proof is transferred via the http header.

export async function getAccessToken(url, dpopProof) {

    var result = await fetch(url, {
            method: 'POST',
            headers: {
                'DPOP': dpopProof,
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: 'client_id=client&grant_type=client_credentials'
        })
        .then(response => {
            return response.text();
        })
        .catch(function(error) {
            console.log(error);
        });

        var jsonResult = JSON.parse(result);
        var token = jsonResult.access_token;        
        return token;
}

Generating the Access Token hash for the new DPoP proof

When the client receives the AccessToken we can calculate it’s hash value, and include that in the new DPoP proof that the client will present to the API.

Still using the SubtleCrypto interface. Also, still quite easy.. And the spec says no padding, so i removed the padding.

export async function createHash(accessToken, noPadding = false){
    var encodedAT = new TextEncoder().encode(accessToken);
    var atHash = await crypto.subtle.digest('SHA-256', encodedAT)
    .then(function(hash) {        
        var base = Base64.ToBase64Url(new Uint8Array(hash));
        if (noPadding){
            base = base.replace(/\=+$/, '');
        }    
        return base;
    })
    .catch(function(err){
        console.log(err);
        throw err;
    });
    return atHash;
}

Generating the DPoP proof for the API

The next step is to generate the DPoP proof that the client will present to the API. Also quite easily acheived with javascript. Actually, I think that this is one of the times during coding where I felt that javascript had a certain shine to it..

export async function createDpopProof(atHash, jwk, key, resourceUrl) {
    var dpopProof = {
        key: undefined,
        jwk: undefined,
        thumbprint: undefined
    };

    var dpop_proof_payload = {
        jti: await uuid.generate,
        htm: "POST",
        htu: resourceUrl,
        iat: Math.floor(Date.now() / 1000)
    };

    if (atHash)
        dpop_proof_payload["ath"] = atHash;

    var header = {
        typ: "dpop+jwt",
        alg: "ES256",
        jwk: undefined
    };

    if (!key){
        key = await cryptoModule.generateKey();
    }

    if (!jwk){
        jwk = await cryptoModule.exportJwk(key.publicKey);
        delete jwk.ext;
        delete jwk.key_ops;
    }        

    header.jwk = jwk;

    var dpopProof = await Jwt.create(key.privateKey, header, dpop_proof_payload);

    return { dpopProof: dpopProof, key: key, jwk: jwk};

}

Requesting the API resource with the new DPoP proof and Access Token

The final part is where it’s all brought together — requesting the resource at the API.

export async function callAPI(accessToken, dpopProof) {
    var url = "https://dpoptestapi.azurewebsites.net/DPoP";
    var response = await fetch(url, {
            method: 'GET',
            headers: {
                'DPOP': dpopProof,
                'Authorization': `DPOP ${accessToken}`,
                'Content-Type': 'application/json'
            },            
        })
        .then(response => {
            var json = response.json();            
            return json;
        })
        .catch(function(err){
            console.error(err);
        });
        return response;
}

My conclusion

Regardless of my lacking experience and competence in Javascript I was able to implement the DPoP mechanism, although perhaps not without effort.. But, for a skilled javascript programmer I would assume that it is actually quite easy.

Not only for browser apps (public apps)?

As mentioned in my first blog post in this series, DPoP is an alternative to the existing mTLS mechanism.

Choosing between sender constraining token with mTLS or on the application layer by using DPoP (or similar) boils down on the ecosystems, environments and infrastructures that the apps run in. As mentioned in part 1 of this series, there are some pitfalls that could make mTLS challenging to use, and for these scenarios DPoP definitely is a viable option.

With that in mind, I really think that the DPoP mechanism is promising for other client types than browser based apps, e.g. mobile and desktop apps.

I would love to see sender constraining of tokens become a default behaviour in OAuth flows, where the choice of using mTLS or DPoP would be more or less transparent for the developer?

Securing the security mechanism

While sender constraining tokens with DPoP is a mechanism that protects the protocol flow itself, the threats for the client (browser) are unchanged. In other words: if the client is compromised, you are still in big trouble.

So, the difficult questions remain:

  • How do you keep secrets safe in the browser?
  • And, how do you protect your browser app against attacks?

There are, however, some obvious advantages of DPoP:

  • The secrets (key material) can have a short lifespan
  • DPoP proofs can have a short life span
  • The spec is relatively simple to implement

Wrapping it up

While previous attempts (like the token-binding specification) came to a halt, there is some reason to hope that the DPoP spec receives enough attention and that the IETF is able to finalize the specification.

I really hope that the IETF is able to keep the simplicity of the mechanism intact, and that they agree that it complements mTLS in a way that is beneficial to apps that aren’t easily able to use client certificates.

You’ll find the source code for my client implementation here: https://github.com/udelt/dpop_js_test

And you’ll find a demo of the client here: https://dpoptest.z1.web.core.windows.net/index.htm

Tilbake til notater