Microsoft Entra Enterprise App

Last updated: July 5, 2025

My First Encounter with Enterprise Apps: From Confusion to Clarity

I still remember my early days as an IAM practitioner when I encountered my first real enterprise identity challenge. A colleague showed me their company portal where they could click on various application tiles and instantly access CRM systems, project management tools, HR portals, and custom internal applications without entering credentials multiple times. I was fascinated by what seemed like magic - how did all these different applications "know" who the user was?

That was my introduction to Microsoft Entra Enterprise Apps (formerly Azure AD Enterprise Applications), though I didn't fully understand the architecture at the time. What appeared to be simple user experience was actually a sophisticated identity and access management system that had been carefully designed to provide seamless single sign-on (SSO) across multiple applications.

As I dove deeper into IAM implementation and became responsible for integrating various Node.js applications with Microsoft Entra, I realized how much complexity was hidden behind that elegant user experience. Through trial, error, and countless configuration sessions, I've learned the ins and outs of enterprise apps. Let me share what I've discovered about enterprise apps, the various ways users can be onboarded, and how to build applications that integrate seamlessly with Microsoft's identity ecosystem.

Understanding Microsoft Entra Enterprise Apps: The Corporate Identity Hub

Microsoft Entra Enterprise Apps are essentially applications that have been registered and configured within a Microsoft Entra ID tenant to provide identity and access management capabilities. Think of them as the "corporate versions" of applications that understand how to authenticate users through your organization's identity provider.

When I explain this to developers, I use this analogy: if your application is a nightclub, then registering it as an Enterprise App is like hiring a professional bouncer who already knows all the VIPs and has a direct line to the membership office. Instead of checking IDs individually, the bouncer trusts the membership office's stamp of approval.

Here's what makes an Enterprise App special:

1. Centralized Identity Management

Instead of managing user accounts separately, the application trusts Microsoft Entra ID as the authoritative source of user identity and attributes.

2. Single Sign-On (SSO) Capabilities

Users authenticate once with their corporate credentials and gain access to all authorized applications without additional login prompts.

3. Centralized Access Control

IT administrators can manage who has access to what applications from a single control plane, including conditional access policies and multi-factor authentication requirements.

4. Compliance and Auditing

All authentication events and access patterns are logged centrally, providing comprehensive audit trails for compliance purposes.

The Four Primary Ways Users Can Be Onboarded to Microsoft Entra Enterprise Apps

Through my experience implementing various integration patterns, I've identified four primary methods for user onboarding to Enterprise Apps. Each method serves different scenarios and has unique implementation considerations.

Let me walk you through each method with real-world examples from my implementations.

Method 1: SAML-based Authentication

This is the method I covered extensively in my previous SAML blog post, but let me provide the enterprise app perspective:

When to Use: When you have existing applications that need to integrate with corporate identity without major code changes, or when working with third-party SaaS applications.

User Experience: Users either start at your application (SP-initiated) or click on your app from the Microsoft 365 portal (IdP-initiated).

Here's the enterprise app configuration flow I typically follow:

Method 2: OAuth 2.0/OpenID Connect Integration

This became my preferred method for modern applications due to its flexibility and security features.

When to Use: For new applications, mobile apps, SPAs, or when you need programmatic access to Microsoft Graph APIs.

User Experience: Users see a familiar Microsoft login screen and consent to permissions, then return to your application.

Here's how I implement this in Node.js applications:

// oauth-enterprise-app.js - OAuth 2.0/OIDC Enterprise App Integration
const express = require('express');
const { ConfidentialClientApplication } = require('@azure/msal-node');
const { Client } = require('@microsoft/microsoft-graph-client');
const { TokenCredentialAuthenticationProvider } = require('@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials');

const app = express();

// Enterprise App Configuration from Entra Admin Center
const clientConfig = {
    auth: {
        clientId: process.env.ENTRA_CLIENT_ID, // Application (client) ID
        clientSecret: process.env.ENTRA_CLIENT_SECRET, // Client secret
        authority: `https://login.microsoftonline.com/${process.env.ENTRA_TENANT_ID}` // Tenant ID
    },
    system: {
        loggerOptions: {
            loggerCallback: (loglevel, message, containsPii) => {
                console.log(message);
            },
            piiLoggingEnabled: false,
            logLevel: 'Info',
        }
    }
};

const pca = new ConfidentialClientApplication(clientConfig);

// Define scopes - these must be configured in the Enterprise App
const scopes = [
    'openid',
    'profile', 
    'User.Read',
    'Group.Read.All', // For reading user groups
    'UserAuthenticationMethod.ReadWrite.All' // For MFA management
];

// Route to initiate OAuth flow
app.get('/login', async (req, res) => {
    const authCodeUrlParameters = {
        scopes: scopes,
        redirectUri: process.env.REDIRECT_URI || 'http://localhost:3000/auth/callback',
        state: generateStateValue(), // CSRF protection
    };

    try {
        const authUrl = await pca.getAuthCodeUrl(authCodeUrlParameters);
        res.redirect(authUrl);
    } catch (error) {
        console.error('Error generating auth URL:', error);
        res.status(500).send('Authentication initialization failed');
    }
});

// OAuth callback handler
app.get('/auth/callback', async (req, res) => {
    const tokenRequest = {
        code: req.query.code,
        scopes: scopes,
        redirectUri: process.env.REDIRECT_URI || 'http://localhost:3000/auth/callback',
        state: req.query.state
    };

    try {
        const response = await pca.acquireTokenByCode(tokenRequest);
        
        // Store tokens securely (in production, use proper session management)
        req.session.accessToken = response.accessToken;
        req.session.idToken = response.idToken;
        req.session.account = response.account;
        
        // Initialize Microsoft Graph client
        const graphClient = await initializeGraphClient(response.accessToken);
        
        // Get user information and groups
        const userInfo = await getUserProfileAndGroups(graphClient);
        req.session.userProfile = userInfo;
        
        res.redirect('/dashboard');
    } catch (error) {
        console.error('Token acquisition failed:', error);
        res.status(500).send('Authentication failed');
    }
});

// Function to initialize Microsoft Graph client
async function initializeGraphClient(accessToken) {
    const authProvider = {
        getAccessToken: async () => {
            return accessToken;
        }
    };
    
    return Client.initWithMiddleware({ authProvider });
}

// Function to get user profile and groups using Microsoft Graph
async function getUserProfileAndGroups(graphClient) {
    try {
        // Get user profile
        const user = await graphClient.api('/me').get();
        
        // Get user's group memberships
        const groups = await graphClient.api('/me/memberOf').get();
        
        // Get user's MFA methods
        const authMethods = await graphClient.api('/me/authentication/methods').get();
        
        return {
            profile: {
                id: user.id,
                email: user.mail || user.userPrincipalName,
                displayName: user.displayName,
                jobTitle: user.jobTitle,
                department: user.department,
                officeLocation: user.officeLocation
            },
            groups: groups.value.map(group => ({
                id: group.id,
                displayName: group.displayName,
                groupType: group.groupTypes
            })),
            authenticationMethods: authMethods.value.map(method => ({
                id: method.id,
                type: method['@odata.type'],
                // Additional method-specific properties
            }))
        };
    } catch (error) {
        console.error('Error fetching user information:', error);
        throw error;
    }
}

function generateStateValue() {
    return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}

module.exports = app;

Method 3: Application Proxy Integration

This method surprised me with its power when I needed to integrate legacy applications that couldn't be easily modified.

When to Use: For on-premises applications that need cloud identity integration without code changes, or legacy apps that support header-based authentication.

User Experience: Users access the application through a cloud URL, but the application runs on-premises with SSO provided through HTTP headers.

Method 4: Direct Microsoft Graph API Integration

This is my go-to method for applications that need deep integration with Microsoft 365 services and user management.

When to Use: When building custom applications that need to manage users, groups, or integrate deeply with Microsoft 365 services.

User Experience: Usually combined with other authentication methods, but provides rich integration capabilities.

Here's an example of how I use Graph API for user and group management:

// graph-integration.js - Advanced Microsoft Graph Integration
const { Client } = require('@microsoft/microsoft-graph-client');
const { ClientSecretCredential } = require('@azure/identity');

class EntraUserManager {
    constructor(tenantId, clientId, clientSecret) {
        this.credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
        this.graphClient = Client.initWithMiddleware({
            authProvider: {
                getAccessToken: async () => {
                    const tokenResponse = await this.credential.getToken('https://graph.microsoft.com/.default');
                    return tokenResponse.token;
                }
            }
        });
    }

    // Query user groups for authorization
    async getUserGroups(userId) {
        try {
            const groups = await this.graphClient
                .api(`/users/${userId}/memberOf`)
                .select('id,displayName,groupTypes')
                .get();
            
            return groups.value.map(group => ({
                id: group.id,
                name: group.displayName,
                type: group.groupTypes?.includes('DynamicMembership') ? 'dynamic' : 'assigned'
            }));
        } catch (error) {
            console.error('Error fetching user groups:', error);
            throw new Error('Failed to fetch user groups');
        }
    }

    // Reset user's MFA methods
    async resetUserMFA(userId) {
        try {
            // Get current authentication methods
            const currentMethods = await this.graphClient
                .api(`/users/${userId}/authentication/methods`)
                .get();
            
            const results = [];
            
            // Remove existing MFA methods (except password)
            for (const method of currentMethods.value) {
                if (method['@odata.type'] !== '#microsoft.graph.passwordAuthenticationMethod') {
                    try {
                        await this.graphClient
                            .api(`/users/${userId}/authentication/methods/${method.id}`)
                            .delete();
                        
                        results.push({
                            methodId: method.id,
                            type: method['@odata.type'],
                            status: 'removed'
                        });
                    } catch (deleteError) {
                        results.push({
                            methodId: method.id,
                            type: method['@odata.type'],
                            status: 'error',
                            error: deleteError.message
                        });
                    }
                }
            }
            
            // Force the user to set up MFA on next login
            await this.graphClient
                .api(`/users/${userId}`)
                .patch({
                    'passwordProfile': {
                        'forceChangePasswordNextSignIn': false
                    }
                    // Note: Forcing MFA setup requires specific admin permissions
                });
            
            return {
                userId: userId,
                resetResults: results,
                status: 'MFA reset completed'
            };
            
        } catch (error) {
            console.error('Error resetting user MFA:', error);
            throw new Error('Failed to reset user MFA');
        }
    }

    // Create security group for application access
    async createApplicationGroup(appName, description) {
        try {
            const group = {
                displayName: `${appName} Users`,
                description: description || `Security group for ${appName} application access`,
                mailEnabled: false,
                securityEnabled: true,
                mailNickname: `${appName.toLowerCase().replace(/\s+/g, '')}users`
            };
            
            const createdGroup = await this.graphClient
                .api('/groups')
                .post(group);
            
            return {
                groupId: createdGroup.id,
                displayName: createdGroup.displayName,
                description: createdGroup.description
            };
        } catch (error) {
            console.error('Error creating application group:', error);
            throw new Error('Failed to create application group');
        }
    }

    // Add user to application group
    async addUserToGroup(userId, groupId) {
        try {
            await this.graphClient
                .api(`/groups/${groupId}/members/$ref`)
                .post({
                    '@odata.id': `https://graph.microsoft.com/v1.0/users/${userId}`
                });
            
            return { status: 'success', message: 'User added to group' };
        } catch (error) {
            console.error('Error adding user to group:', error);
            throw new Error('Failed to add user to group');
        }
    }

    // Get comprehensive user profile
    async getUserProfile(userId) {
        try {
            const user = await this.graphClient
                .api(`/users/${userId}`)
                .select('id,displayName,mail,userPrincipalName,jobTitle,department,officeLocation,manager')
                .expand('manager($select=displayName,mail)')
                .get();
            
            return {
                id: user.id,
                displayName: user.displayName,
                email: user.mail || user.userPrincipalName,
                jobTitle: user.jobTitle,
                department: user.department,
                office: user.officeLocation,
                manager: user.manager ? {
                    name: user.manager.displayName,
                    email: user.manager.mail
                } : null
            };
        } catch (error) {
            console.error('Error fetching user profile:', error);
            throw new Error('Failed to fetch user profile');
        }
    }
}

// Express.js route examples using the EntraUserManager
app.get('/api/user/groups/:userId', async (req, res) => {
    try {
        const userManager = new EntraUserManager(
            process.env.ENTRA_TENANT_ID,
            process.env.ENTRA_CLIENT_ID,
            process.env.ENTRA_CLIENT_SECRET
        );
        
        const groups = await userManager.getUserGroups(req.params.userId);
        res.json({ groups });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

app.post('/api/admin/reset-mfa/:userId', async (req, res) => {
    try {
        // Check if current user has admin permissions
        if (!req.user || !req.user.isAdmin) {
            return res.status(403).json({ error: 'Admin access required' });
        }
        
        const userManager = new EntraUserManager(
            process.env.ENTRA_TENANT_ID,
            process.env.ENTRA_CLIENT_ID,
            process.env.ENTRA_CLIENT_SECRET
        );
        
        const result = await userManager.resetUserMFA(req.params.userId);
        res.json(result);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

module.exports = { EntraUserManager };

The Complete Enterprise App Onboarding Flow

Based on my experience, here's the comprehensive flow I follow when onboarding a new Node.js application as an Enterprise App:

Key Lessons I've Learned About Enterprise App Integration

After implementing dozens of Enterprise App integrations, here are the critical insights I've gained:

1. Plan Your Permission Strategy Early

Microsoft Graph API permissions can be complex. I always start by mapping out exactly what data my application needs to access and request only the minimum required permissions.

  • User Consent: Individual users can consent to permissions

  • Admin Consent: Administrators can consent on behalf of all users

  • Application Permissions: Background services that don't require user interaction

3. Design for Multiple Tenants

Even if you're building for a single organization, designing your application to be multi-tenant from the start makes it much easier to scale or migrate later.

4. Implement Proper Token Management

Access tokens expire, refresh tokens can be revoked, and applications need to handle these scenarios gracefully. I always implement robust token refresh logic.

5. Security is Paramount

Enterprise environments have strict security requirements. Always use HTTPS, implement CSRF protection, validate all tokens, and follow the principle of least privilege.

Conclusion: The Power of Integrated Enterprise Identity

My journey from confusion about enterprise apps to successfully implementing complex integrations has taught me that Microsoft Entra Enterprise Apps are much more than just authentication systems—they're the foundation of modern enterprise security and user experience.

The four onboarding methods I've covered each serve different scenarios:

  • SAML for broad compatibility and existing applications

  • OAuth/OIDC for modern applications and API access

  • Application Proxy for legacy system integration

  • Graph API for deep Microsoft 365 integration

What started as a simple need to "log users in" evolved into building applications that can manage user lifecycle, enforce security policies, provide rich user experiences, and integrate seamlessly with the broader Microsoft ecosystem.

Whether you're building your first enterprise application or architecting a complex multi-tenant system, understanding these integration patterns will help you create applications that not only authenticate users but truly participate in the enterprise identity ecosystem.

The key is to start simple—pick the integration method that best fits your immediate needs—and then expand your capabilities as your application grows. The flexibility of Microsoft Entra's platform means you can evolve your integration approach as your requirements become more sophisticated.

Remember: enterprise identity isn't just about who can log in—it's about creating a seamless, secure, and manageable experience for both users and administrators in complex organizational environments.

Additional Resources


Have questions about Enterprise App integration or want to share your own experiences? Feel free to reach out through the comments below. Let's build better enterprise applications together!

Last updated