Skip to content

StefanH114/Documentation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 

Repository files navigation

Fixing v1/v2 Token Version Issues with Entra ID Authentication

This document outlines the steps to fix the "invalid signature" error you're encountering due to token version mismatches between your SPA and API application registrations.

Problem Diagnosis

Based on the token information provided, we've identified these issues:

  1. Your access token has ver: "1.0" but your API is likely configured for v2.0 tokens
  2. The audience (aud) in your access token is Microsoft Graph (00000003-0000-0000-c000-000000000000) instead of your API
  3. The scope in your token is for Microsoft Graph (User.Read), not for your custom API scope (ToolBox.API)

Solution Steps

1. Update the API Application Manifest

Your API app registration needs to be explicitly configured to accept v2.0 tokens:

  1. Navigate to your API app registration (ToolBox.API) in Entra portal

    • Go to Azure Portal → Microsoft Entra ID → App Registrations → Your API App
    • Click on "Manifest" in the left navigation
  2. Update the manifest to explicitly set token version:

    Look for or add the following setting:

    "accessTokenAcceptedVersion": 2

    Make sure it's set to 2 (not null or 1).

  3. Verify Application ID URI:

    Check that the identifierUris section contains your API's identifier URI:

    "identifierUris": [
      "api://your-api-client-id"
    ]
  4. Save the manifest changes

2. Update the SPA Authorization Request

The issue in your frontend is that it's requesting tokens for Microsoft Graph, not for your API.

  1. Update the authConfig.js in your SPA:
// Keep the login request focused on user sign-in
const loginRequest = {
    scopes: ["User.Read"]  // Only request Microsoft Graph scopes for login
};

// Create a separate request object specifically for your API
const apiRequest = {
    scopes: ["api://your-api-client-id/Greeting.Read"]  // Your API scope
};
  1. Update your apiClient.js to use the correct scope request:
// API configuration
const apiConfig = {
    uri: 'http://localhost:8080',
    // Make sure this matches EXACTLY with what's configured in your API app registration
    scopes: ['api://your-api-client-id/Greeting.Read'] 
};

/**
 * Makes an authenticated API call
 */
async function callApi() {
    const account = myMSALObj.getActiveAccount();
    if (!account) {
        throw Error("No active account! Verify a user has been signed in and setActiveAccount has been called.");
    }

    // Create a request specifically for the API scope
    const request = {
        scopes: apiConfig.scopes,
        account: account
    };

    // Silently acquires an access token which is then attached to a request
    return myMSALObj.acquireTokenSilent(request)
        .then((response) => {
            // Log the token for debugging (remove in production)
            console.log("Token type:", response.tokenType);
            console.log("For scopes:", response.scopes);
            
            const headers = new Headers();
            headers.append("Authorization", `Bearer ${response.accessToken}`);

            const options = {
                method: "GET",
                headers: headers
            };

            return fetch(apiConfig.uri, options)
                .then(response => response.text())
                .catch(error => {
                    console.error('API call failed:', error);
                    throw error;
                });
        }).catch((error) => {
            console.error('Token acquisition failed:', error);
            if (error instanceof msal.InteractionRequiredAuthError) {
                // Fallback to interactive method when silent call fails
                return myMSALObj.acquireTokenRedirect(request);
            }
            throw error;
        });
}

3. Update Your API Server Configuration

Ensure your API is properly configured to validate v2.0 tokens:

const config = {
  auth: {
    // Your tenant ID
    tenant: 'your-tenant-id-here',
    
    // The Application (client) ID of your API application
    audience: 'your-api-client-id-here'
  }
}

// Add proper CORS handling
const express = require('express')
const cors = require('cors');
const {expressjwt: jwt } = require("express-jwt");
const jwks = require('jwks-rsa')
const jwtAuthz = require('express-jwt-authz')

// Initialize Express
const app = express()

// Add CORS middleware - critical for browser-based API calls
app.use(cors({
  origin: 'http://localhost:3000', // Your SPA URL
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// JWT validation middleware - make sure it's configured for v2.0 tokens
app.use(jwt({
  secret: jwks.expressJwtSecret({
    jwksUri: 'https://login.microsoftonline.com/' + config.auth.tenant + '/discovery/v2.0/keys'
  }),
  audience: config.auth.audience,  // This should match your API client ID
  issuer: 'https://login.microsoftonline.com/' + config.auth.tenant + '/v2.0', // v2.0 endpoint
  algorithms: ['RS256']
}));

Testing and Verification

After making these changes:

  1. Get a new token by logging in to your SPA

  2. Check the token using jwt.ms and verify:

    • It has ver: "2.0" (token version)
    • The audience (aud) is your API's client ID, not Microsoft Graph
    • It contains your API's scope (Greeting.Read)
  3. Debug API calls:

    • In the browser dev tools, inspect network requests when calling your API
    • Verify the Authorization header contains the token
    • Check server logs for any validation errors

Common Errors and Solutions

Error: "Invalid signature" or "Unable to validate signature"

  • Cause: Token was signed with a key from v1.0 endpoint but being validated against v2.0 keys (or vice versa)
  • Solution: Ensure accessTokenAcceptedVersion is consistently set to 2 and both client and API use v2.0 endpoints

Error: "Invalid audience"

  • Cause: Token was issued for Microsoft Graph but being used for your API
  • Solution: Ensure your SPA is requesting a token specifically for your API by using the correct scope format

Error: "Scope not found" or "Insufficient scope"

  • Cause: Token doesn't contain the required scope (Greeting.Read)
  • Solution: Make sure your SPA is requesting the correct scope and you've granted API permissions in the app registration

Additional Resources

Setup Documentation: Vanilla JavaScript SPA with Protected API using Entra ID

This documentation explains how to configure and integrate the vanilla JavaScript single-page application (SPA) with the protected Node.js API using Microsoft Entra ID (formerly Azure AD) for authentication and authorization.

Overview

This setup involves:

  • Frontend: Vanilla JavaScript SPA that authenticates users using MSAL.js
  • Backend: Node.js Express API that validates JWT tokens and enforces scope-based access control
  • Authentication Provider: Microsoft Entra ID with OAuth 2.0 / OpenID Connect

Architecture

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Vanilla JS    │    │   Entra ID      │    │   Protected     │
│   SPA Client    │◄──►│   Authority     │    │   Node.js API   │
│   (Frontend)    │    │                 │    │   (Backend)     │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                                              ▲
         │            Access Token with Scopes          │
         └──────────────────────────────────────────────┘

Prerequisites

  • Node.js (version 16 or higher)
  • Microsoft Entra tenant with administrative permissions
  • Visual Studio Code (recommended)

Part 1: Entra ID App Registrations

1.1 Register the Backend API Application

  1. Navigate to the Microsoft Entra admin center

  2. Create the API App Registration

    • Navigate to Applications > App registrations
    • Click New registration
    • Configure the following:
      • Name: ToolBox.API (or your preferred API name)
      • Supported account types: Accounts in this organizational directory only (Single tenant)
      • Redirect URI: Leave blank (not needed for API)
    • Click Register
  3. Note the Application Details

    • Copy the Application (client) ID - you'll need this for the API configuration
    • Copy the Directory (tenant) ID - you'll need this for both frontend and API
  4. Expose an API Scope

    • In your API app registration, go to Expose an API
    • Click Set next to Application ID URI and accept the default (api://{client-id})
    • Click Add a scope and configure:
      • Scope name: Greeting.Read
      • Who can consent?: Admins and users
      • Admin consent display name: Read API Greetings
      • Admin consent description: Allows the user to see greetings from the API.
      • User consent display name: Read API Greetings
      • User consent description: Allows you to see greetings from the API.
      • State: Enabled
    • Click Add scope

1.2 Register the Frontend SPA Application

  1. Create the SPA App Registration

    • Still in App registrations, click New registration
    • Configure the following:
      • Name: ToolBox.SPA (or your preferred SPA name)
      • Supported account types: Accounts in this organizational directory only (Single tenant)
      • Redirect URI:
        • Platform: Single-page application (SPA)
        • URI: http://localhost:3000
    • Click Register
  2. Note the Application Details

    • Copy the Application (client) ID - you'll need this for the SPA configuration
  3. Configure API Permissions

    • In your SPA app registration, go to API permissions
    • Click Add a permission
    • Select My APIs tab
    • Choose your ToolBox.API application
    • Select Delegated permissions
    • Check the Greeting.Read scope you created earlier
    • Click Add permissions
    • (Optional) Click Grant admin consent if you want to pre-consent for all users
  4. Configure Authentication Settings

    • Go to Authentication
    • Under Single-page application, ensure http://localhost:3000 is listed
    • Add additional redirect URIs if needed:
      • http://localhost:3000/redirect
      • http://localhost:3000/ (with trailing slash)
    • Under Advanced settings:
      • Allow public client flows: No
      • Live SDK support: No
    • Save the configuration

Part 2: Configure the Protected API (Backend)

2.1 Update API Configuration

  1. Navigate to the protect-api folder

    cd protect-api
  2. Install dependencies

    npm install
  3. Update the configuration in app.js

    Replace the placeholder values in the config object:

    const config = {
      auth: {
        // Replace with your tenant ID from Part 1.1 step 3
        tenant: 'your-tenant-id-here',
        
        // Replace with your API's Application (client) ID from Part 1.1 step 3
        audience: 'your-api-client-id-here'
      }
    }

2.2 Test the API

  1. Start the API server

    node app.js
  2. Verify the API is running

    • You should see: Listening here: http://localhost:8080
    • The API endpoint is now protected and requires a valid JWT token with the Greeting.Read scope

Part 3: Configure the Vanilla JavaScript SPA (Frontend)

3.1 Update SPA Configuration

  1. Navigate to the vanillajs-spa/App folder

    cd ../vanillajs-spa/App
  2. Install dependencies

    npm install
  3. Update public/authConfig.js

    Replace the placeholder values:

    const msalConfig = {
        auth: {
             // Replace with your SPA's Application (client) ID from Part 1.2 step 2
             clientId: "your-spa-client-id-here",
             
             // Replace with your tenant ID from Part 1.1 step 3
             authority: "https://login.microsoftonline.com/your-tenant-id-here",
             
             redirectUri: 'http://localhost:3000', // Must match registered redirect URI
             navigateToLoginRequestUrl: true,
        },
        cache: {
            cacheLocation: 'sessionStorage',
            storeAuthStateInCookie: false,
        },
        // ... rest of configuration remains the same
    };
    
    // Update the login request to include your API scope
    const loginRequest = {
        scopes: ["User.Read", "api://your-api-client-id-here/Greeting.Read"],
    };

3.2 Add API Integration Code

  1. Create a new file public/apiClient.js to handle API calls:

    // API configuration
    const apiConfig = {
        uri: 'http://localhost:8080',
        scopes: ['api://your-api-client-id-here/Greeting.Read'] // Replace with your API client ID
    };
    
    /**
     * Makes an authenticated API call
     */
    async function callApi() {
        const account = myMSALObj.getActiveAccount();
        if (!account) {
            throw Error("No active account! Verify a user has been signed in and setActiveAccount has been called.");
        }
    
        const request = {
            scopes: apiConfig.scopes,
            account: account
        };
    
        // Silently acquires an access token which is then attached to a request for the Microsoft Graph API
        return myMSALObj.acquireTokenSilent(request)
            .then((response) => {
                const headers = new Headers();
                headers.append("Authorization", `Bearer ${response.accessToken}`);
    
                const options = {
                    method: "GET",
                    headers: headers
                };
    
                return fetch(apiConfig.uri, options)
                    .then(response => response.text())
                    .catch(error => {
                        console.error('API call failed:', error);
                        throw error;
                    });
            }).catch((error) => {
                console.error('Token acquisition failed:', error);
                if (error instanceof msal.InteractionRequiredAuthError) {
                    // Fallback to interactive method when silent call fails
                    return myMSALObj.acquireTokenRedirect(request);
                }
                throw error;
            });
    }
  2. Update public/index.html to include the new API client and add a button to test the API:

    Add this script tag after the existing script includes:

    <script type="text/javascript" src="./apiClient.js"></script>

    Add this button in the welcome section:

    <button type="button" id="callApiBtn" class="btn btn-success ml-auto" onclick="testApi()">Call Protected API</button>
    <div id="api-response" class="mt-3"></div>
  3. Update public/ui.js to add the API test function:

    Add this function to handle the API call:

    async function testApi() {
        try {
            const responseDiv = document.getElementById('api-response');
            responseDiv.innerHTML = '<p>Calling API...</p>';
            
            const apiResponse = await callApi();
            responseDiv.innerHTML = `<div class="alert alert-success"><strong>API Response:</strong><br>${apiResponse}</div>`;
        } catch (error) {
            console.error('API call failed:', error);
            const responseDiv = document.getElementById('api-response');
            responseDiv.innerHTML = `<div class="alert alert-danger"><strong>API Error:</strong><br>${error.message}</div>`;
        }
    }
    
    // Show/hide API button based on sign-in status
    function updateUI(account) {
        const callApiBtn = document.getElementById('callApiBtn');
        if (account) {
            callApiBtn.style.display = 'block';
        } else {
            callApiBtn.style.display = 'none';
        }
    }

3.3 Start the SPA

  1. Start the development server

    npm start
  2. Open your browser

    • Navigate to http://localhost:3000

Part 4: Testing the Integration

4.1 Complete Flow Test

  1. Start both applications:

    • API: http://localhost:8080 (should be running from Part 2.2)
    • SPA: http://localhost:3000 (should be running from Part 3.3)
  2. Test the authentication flow:

    • Open http://localhost:3000 in your browser
    • Click Sign In
    • Complete the authentication with your Entra ID account
    • You should see a welcome message with your user information
  3. Test the API integration:

    • After signing in, click Call Protected API
    • You should see a success message from the API: "Hello, world. You were able to access this because you provided a valid access token with the Greeting.Read scope as a claim."

4.2 Troubleshooting Common Issues

Issue: "CORS Error" when calling API

Solution: Add CORS middleware to your API:

cd protect-api
npm install cors

Update app.js to include CORS:

const cors = require('cors');

// Add after initializing Express
app.use(cors({
  origin: 'http://localhost:3000' // Your SPA URL
}));

Issue: "Invalid audience" error

Solution: Verify that:

  • The audience in the API config matches your API's Application (client) ID
  • The scope in the SPA includes your API's Application ID URI

Issue: "Scope not found" error

Solution: Ensure:

  • The scope Greeting.Read is properly exposed in your API app registration
  • The SPA app registration has permissions to access this scope
  • The scope format in the SPA is api://your-api-client-id/Greeting.Read

Issue: SPA redirect issues

Solution: Verify:

  • Redirect URIs are properly registered in the SPA app registration
  • URIs match exactly (including trailing slashes)
  • The redirectUri in authConfig.js matches a registered URI

Part 5: Production Considerations

5.1 Security Best Practices

  1. Use HTTPS in production

    • Update redirect URIs to use HTTPS
    • Ensure API endpoints use HTTPS
    • Use secure cookie settings
  2. Token validation

    • The API already validates JWT signatures and expiration
    • Consider implementing additional claims validation as needed
  3. CORS configuration

    • Restrict CORS origins to your specific domains
    • Don't use wildcards in production

5.2 Environment Configuration

  1. Use environment variables for sensitive data:

    API configuration (protect-api/.env):

    TENANT_ID=your-tenant-id
    CLIENT_ID=your-api-client-id
    

    SPA configuration (use build-time replacement):

    const msalConfig = {
        auth: {
            clientId: process.env.REACT_APP_CLIENT_ID,
            authority: `https://login.microsoftonline.com/${process.env.REACT_APP_TENANT_ID}`,
            // ...
        }
    };

5.3 Deployment

  1. API Deployment:

    • Deploy to Azure App Service, Azure Container Instances, or your preferred hosting platform
    • Update CORS settings to include your production SPA URL
    • Ensure environment variables are properly configured
  2. SPA Deployment:

    • Build the application for production
    • Deploy static files to Azure Static Web Apps, CDN, or web server
    • Update app registration redirect URIs to include production URLs

Summary

You now have a complete setup with:

  1. Two Entra ID app registrations:

    • Backend API (ToolBox.API) with exposed scope Greeting.Read
    • Frontend SPA (ToolBox.SPA) with delegated permissions to the API
  2. Protected Node.js API that:

    • Validates JWT tokens from Entra ID
    • Enforces scope-based authorization
    • Returns data only to authenticated users with the correct scope
  3. Vanilla JavaScript SPA that:

    • Authenticates users with Entra ID using MSAL.js
    • Acquires access tokens with the appropriate scopes
    • Makes authenticated API calls to the protected backend

The integration provides secure, token-based authentication and authorization using industry-standard OAuth 2.0 and OpenID Connect protocols through Microsoft Entra ID.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published