Share via

Token exchange fails specifically on Mobile Clients with AADSTS50013

Lienl 205 Reputation points
2026-02-06T08:00:01.1333333+00:00

I have developed a custom Microsoft Teams Tab application using the Teams Toolkit (v5.x) and Teams JS SDK v2.19. The Single Sign-On (SSO) flow works seamlessly on both the Teams Desktop and Web clients.

However, when the same Tab is accessed via the teams mobile the silent authentication fails during the On-Behalf-Of (OBO) token exchange. The client-side getAuthToken() successfully retrieves a bootstrap token, but our backend service rejects it when attempting to swap it for a Graph API token.

Is there a known difference in the Token Audience ?

Microsoft Teams | Development
Microsoft Teams | Development

Building, integrating, or customizing apps and workflows within Microsoft Teams using developer tools and APIs

0 comments No comments
{count} votes

Answer accepted by question author
  1. Steven-N 21,565 Reputation points Microsoft External Staff Moderator
    2026-02-06T09:09:16.52+00:00

    Hi Lienl

    Thank you for reaching out to Microsoft Q&A forum

    The behavior you are experiencing is related to a difference in how the Microsoft Teams mobile client handles Single Sign-On (SSO) tokens compared to the Desktop and Web clients. When your Tab application calls microsoftTeams.authentication.getAuthToken() on Desktop or Web, the returned token has an audience (aud claim) set to your application's custom API URI (e.g., api://<your-app-id>), which is the expected format for the On-Behalf-Of (OBO) token exchange flow.

    The official Microsoft documentation on Teams Tab SSO describes the standard flow as:

    1. getAuthToken() returns a token with audience = your app's Application ID URI
    2. You exchange this via OBO to get a Graph token

    So, the solution is you need to handle both token types in your backend. Here's the recommended approach:

    1. Detect the Token Type

    Decode the incoming JWT (without validation first) and inspect the **aud**claim:

    import jwt from 'jsonwebtoken';
    function getTokenAudience(token: string): string {
      const decoded = jwt.decode(token) as { aud: string };
      return decoded?.aud || '';
    }
    

    2. Branch Your Logic

    async function getGraphToken(clientToken: string): Promise<string> {
      const audience = getTokenAudience(clientToken);
      
      if (audience === 'https://graph.microsoft.com') {
        // Mobile: Token is already a Graph token - validate and use directly
        await validateGraphToken(clientToken); // Verify signature, issuer, etc.
        return clientToken;
      } else {
        // Desktop/Web: Perform standard OBO exchange
        return await performOboExchange(clientToken);
      }
    }
    

    And finally ensure your app registration includes:

    "api": {
      "requestedAccessTokenVersion": 2,
      "oauth2PermissionScopes": [...],
      "knownClientApplications": [
        "Teams Desktop client ID",  // Teams Desktop
        "Teams Mobile client ID "   // Teams Mobile
      ]
    }
    

    Finally, you have to validate mobile tokens properly by verifying the signature against Microsoft's public keys, confirming the issuer matches your tenant, and checking that the azp or appid claim matches your Teams application's client ID.


    If the answer is helpful, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".     

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    1 person found this answer helpful.
    0 comments No comments

0 additional answers

Sort by: Most helpful

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.