1 Feb 2023

New application types

Our developer portal has recently updated the UI for creating new APS applications. After hitting the Create application button, you're now presented with 3 choices of an application type to create:

Create Application Dialog

Let's take a quick look at each option:

Traditional Web App

As indicated in the UI, this is the most flexible option, and it is also the app type that has been used for all applications created before this UI change. Traditional web apps support 2-legged as well as 3-legged OAuth workflows.

If this is the first time you see these terms, 2-legged workflows are intended for server-side applications with no end user, and 3-legged workflows are typically used in scenarios where your application acts on behalf of a specific user after they log in with their Autodesk account.

To learn more about this application type, please refer to https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/App-types/traditionalweb/.

Desktop, Mobile, Single-Page App

This option is recommended for scenarios where your application is running natively on a desktop or a mobile device, in other words, for scenarios where you cannot protect your app's credentials. This application type uses Proof Key for Code Exchange (PKCE) for increased security.

To demonstrate this new workflow, here's a super-simple HMTL page that lets the user login with their Autodesk credentials, and displays an access token that could potentially be used to load models into the viewer:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<body>
    <h1>PKCE Authorization Code Grant Example</h1>
    <script>
        const APS_CLIENT_ID = '...';
        const APS_CALLBACK_URL = '...';

        /**
         * Generates random string of specified length.
         * @param {number} len Length of the output string.
         * @param {string} chars Characters allowed in the output string.
         * @returns {string}
         */
        function generateRandomString(len, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
            const arr = new Array(len);
            for (let i = 0; i < len; i++) {
                arr[i] = chars[Math.floor(Math.random() * chars.length)];
            }
            return arr.join('');
        }

        /**
         * Generates a PKCE code challenge for given code verifier (see https://aps.autodesk.com/en/docs/oauth/v2/tutorials/code-challenge/).
         * @async
         * @param {string} Code verifier.
         * @returns {Promise<string>}
         */
        async function generateCodeChallenge(str) {
            const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
            return window.btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
        }

        /**
         * Generates URL for initiating the PKCE authorization workflow (see https://aps.autodesk.com/en/docs/oauth/v2/tutorials/get-3-legged-token-pkce/#step-1-direct-the-user-to-the-authorization-web-flow-with-pkce).
         * @param {string} clientId Application client ID.
         * @param {string} callbackUrl Callback URL as configured on the dev. portal.
         * @param {array} scopes List of authentication scopes (see https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/scopes/).
         * @param {string} nonce Arbitrary string used to associate a client session with an ID token and to mitigate replay attacks.
         * @param {string} challenge Code challenge generated from code verifier (see `generateCodeChallenge`).
         * @returns {string}
         */
        function generateLoginUrl(clientId, callbackUrl, scopes, nonce, challenge) {
            const url = new URL('https://developer.api.autodesk.com/authentication/v2/authorize');
            url.searchParams.append('response_type', 'code');
            url.searchParams.append('client_id', APS_CLIENT_ID);
            url.searchParams.append('redirect_uri', APS_CALLBACK_URL);
            url.searchParams.append('scope', scopes.join(' '));
            url.searchParams.append('nonce', nonce);
            url.searchParams.append('prompt', 'login');
            url.searchParams.append('code_challenge', challenge);
            url.searchParams.append('code_challenge_method', 'S256');
            return url.toString();
        }

        /**
         * Exchanges temporary code from the PKCE authorization workflow for access token.
         * See https://aps.autodesk.com/en/docs/oauth/v2/tutorials/get-3-legged-token-pkce/#step-3-exchange-the-authorization-code-with-pkce-for-an-access-token.
         * @async
         * @param {string} clientId Application client ID.
         * @param {string} codeVerifier PKCE code verifier.
         * @param {string} code Temporary code received from the PKCE authorization workflow.
         * @param {string} callbackUrl Callback URL as configured on the dev. portal.
         * @returns {Promise<object>}
         */
        async function exchangeToken(clientId, codeVerifier, code, callbackUrl) {
            const payload = {
                'grant_type': 'authorization_code',
                'client_id': clientId,
                'code_verifier': codeVerifier,
                'code': code,
                'redirect_uri': callbackUrl
            };
            const resp = await fetch('https://developer.api.autodesk.com/authentication/v2/token', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Accept': 'application/json'
                },
                body: Object.keys(payload).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(payload[key])).join('&')
            });
            if (!resp.ok) {
                throw new Error(await resp.text());
            }
            const credentials = await resp.json();
            return credentials;
        }

        addEventListener('DOMContentLoaded', async function () {
            const params = new URLSearchParams(window.location.search);
            if (params.has('code')) { // If the URL contains a `code` query parameter, exchange it for an access token, and display the token on the page
                try {
                    const codeVerifier = localStorage.getItem('code-verifier'); // Retrieve code verifier from local storage
                    const credentials = await exchangeToken(APS_CLIENT_ID, codeVerifier, params.get('code'), APS_CALLBACK_URL);
                    document.write('<code>' + JSON.stringify(credentials) + '</code>');
                } catch (err) {
                    document.write('<p>' + err + '</p>');
                }
            } else if (params.has('error')) { // If the URL contains an `error` query parameter, display the error description on the page
                document.write('<p>' + params.get('error') + '</p>');
                document.write('<p>' + params.get('error_description') + '</p>');
            } else { // Otherwise display a link to initiate the PKCE authorization workflow
                try {
                    const codeVerifier = generateRandomString(64); // Length must be between 43 and 128
                    localStorage.setItem('code-verifier', codeVerifier); // Store code verifier for later use
                    const codeChallenge = await generateCodeChallenge(codeVerifier);
                    const login = document.createElement('a');
                    login.href = generateLoginUrl(APS_CLIENT_ID, APS_CALLBACK_URL, ['data:read'], '123456', codeChallenge);
                    login.innerText = 'Login';
                    document.body.appendChild(login);
                } catch (err) {
                    document.write('<p>' + err + '</p>');
                }
            }
        });
    </script>
</body>

</html>

To test this sample locally, specify your own APS_CLIENT_ID and APS_CALLBACK_URL values at the beginning of the JavaScript code, and serve the page using a static server (if you're not sure how, this article explains some options). Note that the exact callback URL will depend on the static server you use, and the same URL needs to be set both in code and on the dev. portal.

To learn more about this option, please refer to https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/App-types/native/.

Server-to-Server App

As the name suggests, this option is suggested for server-side applications with no end users. In this case only the 2-legged OAuth workflow is supported.

To learn more about this app type, please refer to https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/App-types/Machine-to-machine/.

Related Article