Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Azure B2C authentication from SPFX App Teams #2669

Open
Ben-Oops opened this issue Jan 7, 2025 · 19 comments
Open

Azure B2C authentication from SPFX App Teams #2669

Ben-Oops opened this issue Jan 7, 2025 · 19 comments

Comments

@Ben-Oops
Copy link

Ben-Oops commented Jan 7, 2025

Context:

In the context of a POC, I need to make calls to APIs from an SPFX App Teams hosted in my tenant using an Azure AD B2C to authenticate. I create an IDP (Identity Provider) to add my tenant in Azure B2C. To authenticate we use OAuth 2.0 Open ID Connect with Authorization Code flow.

Repro Step

Solution 1: Use @azure/msal-browser SDK for authentication

It works in Teams Web but not in Teams Desktop because it does not allow to open pop-up. See below.

Teams Web Teams Desktop
TeamsWebMSAL TeamsDesktopMSAL

I think there is no way to authorize po-up in Teams Desktop so i tried Solution 2 below.

Solution 2: Use @microsoft/teams-js SDK for authentication

This time the pop-up is now displayed correctly in Teams Desktop but I can't complete authentication in both Teams Desktop and Web. I do receive the authorisation code but it is not intercepted by the Teams connection SDK and is therefore not sent to the /token endpoint to retrieve the authentication token. I use the same redirect URL as the one used with @azure/msal-browser SDK

Redirect URL: https://[tenantname].sharepoint.com/_layouts/15/teamslogon.aspx

Step 1 - Login Pop-Up Step2 - Authorization Code
TeamsWebTeamsJS TeamsWebTeamsJS2
  const authenticateWithTeams = (): Promise<void> => {

    const parameters: authentication.AuthenticatePopUpParameters = {
      url: `https://[AzureADB2CDomain]/[MyTenantEntraId]/oauth2/v2.0/authorize?p=[AzureADB2CAuthorizationNamePolicy]&client_id=[AppId]&nonce=defaultNonce&scope=[Scopes]&response_type=code&prompt=login`,
      width: 600,
      height: 535,
      isExternal: true,
    };

    return new Promise<void>((resolve, reject) => {
      app
        .initialize()
        .then(() => {
          authentication
            .authenticate(parameters)
            .then((result) => {
              console.log("success" + result);
            })
            .catch((reason) => {
              console.log("failed" + reason);
              reject(reason);
            });
        })
        .catch((error) => {
          console.log("failed" + error);
        });
    });
  };

As you can see in Step 2 I'm stuck on this screen with no way of recovering my access token. If anyone can help me with this it would be greatly appreciated. Thank you very much.

@Nivedipa-MSFT
Copy link

@Ben-Oops - Thank you for bringing this issue to our attention. We will look into it and get back to you shortly.

@AE-MS
Copy link
Contributor

AE-MS commented Jan 8, 2025

@Ben-Oops thanks for providing a great level of detail in your question, including a code snippet!

You're right that solution1 won't work for pop-up blocking reasons, which is why the authentication.authenticate function exists, like you are using in solution2.

In solution 2, is the page that you're directing the pop-up to (url parameter in the AuthenticatePopUpParameters object) a page that runs teamsjs, redirects through the identity provider (IdP), then back to the teamsjs page? The way this typically works is that when the IdP redirects back to your page in the authentication pop-up, your page would then call authentication.notifySuccess to communicate the result to the original tab page.

You can see a visual overview of this expected flow here:
https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/authentication/auth-flow-tab#use-oauth-idp-to-enable-authentication

From the code that I see, it looks like the page that is getting launched in the authentication pop-up might not be making the appropriate teamsjs calls to communicate the result back to the original tab page. What do you think?

@Ben-Oops
Copy link
Author

Ben-Oops commented Jan 8, 2025

Yes, you're absolutely right, that's the source of my problem i guess. My redirection page as you can see don't call authentication.notifySuccess. Why ? Because it's a native page which is stored inside /_layouts/15/teamslogon.aspx and i can't include custom code in it.

I don't know which let's say "native redirection address" I can set up to be able to call this authentication.notifySuccess on my pop-up. Does it mean that i need to create a proxy somewhere for example a sharepoint page in order to be able to call authentication.notifySuccess ?

@AE-MS
Copy link
Contributor

AE-MS commented Jan 8, 2025

Yes, you'll need to have some page where you can write custom code that calls authentication.notifySuccess() to serve as the IdP redirect initiator and receiver. Some developers have a route/path on their existing tab app site for this. I don't know which option might work best for you.

@Ben-Oops
Copy link
Author

Ben-Oops commented Jan 9, 2025

Thanks for your answer.

Unfortunately as it's an SPFx project based on this article i don't really know what route/path i can provide to be able to do this.

https://learn.microsoft.com/en-us/microsoftteams/platform/sbs-gs-spfx?tabs=vscode%2Cviscode

I don't really have a path of my application as it's handle by Teams. If i take a look to my Teams Manifest app, my content url is like this.

TeamsManifest

@Ben-Oops
Copy link
Author

Ben-Oops commented Jan 9, 2025

Thanks again for your help. I was able to get Teams Desktop to work, but now I can't get it to work in Teams Web mode. I have the following error after the execution of authentication.notifySuccess.

IssueCanceledUser

I saw this post https://github.com/OfficeDev/microsoft-teams-library-js/issues/1779 related to this issue but it doesn't help me. You can find below the related code i used to called authentication.notifySuccess.

export const OAuthWp = (): JSX.Element => {
  const [accessToken, setAccessToken] = React.useState<string | null>(null);

  const confirmAuthentification = async ():Promise<void> => {
    const hash = window.location.hash.replace(/^#/, '');
    const params = new URLSearchParams(hash);
    await app.initialize();
    setAccessToken(params.get('access_token'));
    authentication.notifySuccess(JSON.stringify({
      idToken: params.get('id_token'),
      accessToken: params.get('access_token'),
      tokenType: params.get('token_type'),
      expiresIn: params.get('expires_in')
    }));
  };

  React.useEffect(() => {
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    confirmAuthentification();
  }, []);


  return (
    <div>
      <div>ACCESS TOKEN: {accessToken}</div>
      <div>IsNotifySuccessCall: {isNotifyCalled}</div>
    </div>
  );
};

@AE-MS
Copy link
Contributor

AE-MS commented Jan 10, 2025

Hmm, I'm not sure what might be causing that. Is that "TEAMS AUTH Error" string somewhere in your code or is it coming from Teams? Is authentication.js a Teams file or one of yours? I did a quick search of the Teams codebase and I didn't see the string "TEAMS AUTH" anywhere but that doesn't necessarily mean its not constructed somewhere.

Enabling TeamsJS client logging may help track down what's going on after you call authentication.notifySuccess. Can you try enabling logging and share the logs from the failure case here (redact any sensitive information, e.g., tokens)

BTW you may only be doing this for testing but you shouldn't send tokens back via authentication.notifySuccess since an attacker may have triggered your authentication window to be launched (instead of your own app) and now the tokens will go to the attacker. We don't do a good job of documenting that so I will get the documentation updated (although we do sort of call it out on this page by telling people to use a random number).

@Ben-Oops
Copy link
Author

TEAMS AUTH comes from a catch of mine. Authentication.js comes from TeamsJS. Search for CancelledByUser.

https://github.com/OfficeDev/microsoft-teams-library-js/blob/27fcd1335993151c387c4ef2b32e05dd8e126610/packages/teams-js/src/public/authentication.ts#L417

I will try to send you more information by enabling Teams JS Client logging.

Thanks again.

@Ben-Oops
Copy link
Author

As promise you can find below Teams JS Client logging.

LogTeams

The token is sent as you can see at the bottom but never received by my parent window. I've got a CancelledByUser on the following line:

const accessToken = await authentication.authenticate(parameters);

@AE-MS
Copy link
Contributor

AE-MS commented Jan 13, 2025

Thank you for sending the log screenshot, these were very helpful! We spent some time looking at these logs this morning and there are definitely some suspicious statements that we're trying to understand.

  1. Would it be possible to get the full teamsjs logs as a text dump (e.g., right click in the logs window and choose "Save As") and send the file? With the text dump we should be able to see the values of some of the parameters that can't be seen in the screenshot. You should still remove the token from the text file and feel free to filter the logs to only teamsJs if you'd like.

  2. Also, are there teamsjs logs from before where the screenshot starts? We were expecting to see some other log statements from before the authentication window started that could help provide some environmental information.

  3. In the code you showed where you call authentication.authenticate, are you providing a AuthenticatePopUpParameters object or a AuthenticateParameters object?

  4. What browser/OS is this running on?

  5. Do you know what version(s) of teamsjs are being used here? From the logs it seems like there are two different versions being used, one in the main window (common.js) and a different one in the authentication window (chunk.spfx-core-team...js). Are both of those SPFx apps or just the one in the authentication window (with SPFx in the JS file name)? I think SPFx apps get teamsjs 2.24.0 by default right now.

Thanks!

@Ben-Oops
Copy link
Author

Ben-Oops commented Jan 14, 2025

Thanks again for your help.

1 & 2: Log in PJ
teams.microsoft.com-1736847560192.log

3: AuthenticatePopUpParameters

4: Chrome Version 131.0.6778.265

5: Version 2.32.

@TrevorJoelHarris
Copy link
Contributor

I have a couple of questions @Ben-Oops! Logs very helpful.

How are you opening the authentication window? From your logs it seems like the auth window is running at chunk.spfx-core-teamsenv-teamsJs-v2_none_1f10a83c0c1dc6ce702c.js (I see that it is calling getAuthToken). It also looks like the authentication window teamsjs logs are appearing before any logs coming from common.js (which I assume is your main app window).

I believe that the supported way to open the auth window is by calling authentication.authenticate() from your main app window and passing it the url of your auth window. Is that how you're opening your auth window, and if so do you have any idea why the auth window logs are showing up before the ones from common.js?

@Ben-Oops
Copy link
Author

I have an SPFx project which i deploy to Teams by following this tutorial:

https://learn.microsoft.com/en-us/microsoftteams/platform/sbs-gs-spfx

In my project i call TeamsJS authenticate directly by clicking on a login button:

  const authenticateWithTeams = async (): Promise<void> => {
    await app.initialize();
    const redirectUri = "https://mytenantname.sharepoint.com/sites/OAuthLogin";
    const parameters: authentication.AuthenticatePopUpParameters = {
      url: `https://[AzureADB2CDomain]/[TenantEntraId]/oauth2/v2.0/authorize?p=[AzureADB2CAuthorizationNamePolicy]&redirect_uri=${redirectUri}&client_id=[AppId]&nonce=defaultNonce&scope=[Scopes]&response_type=token&prompt=login`,
      width: 600,
      height: 535,
    };

    const accessToken = await authentication.authenticate(parameters);
    console.log(accessToken);

  };

I'm redirected to my Azure B2C authority login page inside the pop-up. See in the above discussion. So far so good.

I set my redirect page to a sharepoint page in order to be able to capture the accessToken. I set it to https://.sharepoint.com/sites/OAuthLogin. Inside this page, i have another SPFx webpart which run this code to get the access token and close the pop-up.

    await app.initialize();

    const urlParams = new URLSearchParams(window.location.hash.substring(1));
    const accessToken = urlParams.get('access_token');
    console.log('Access Token:', accessToken);
    if(!accessToken) {
      authentication.notifyFailure("Authentication failed.");
    } else {
      authentication.notifySuccess(accessToken);
    }

This code works on Teams Desktop not on Teams Web. If you have an example of a similar project, I'd love to get it, because I don't know how to solve my problem.

@AE-MS
Copy link
Contributor

AE-MS commented Jan 22, 2025

We are going to reach out to the SPFx team to better understand their intended design for using teamsjs in these scenarios and report back.

@lucabandMSFT
Copy link

hey @Ben-Oops, thanks for bringing this to our attention. I work in the SharePoint Framework team and I hope I can give you some directions here.

in short, what you are experiencing is.. expected: as you are an SPFx app, you are supposed to use the auth provided by the SPFx client APIs. The APIs have logic inside to understand the best auth for the host. As you are in Teams, we will use OBO or NAA (whatever your version of Teams in your tenant supports).

The rational behind this choise is because teams.JS library is not present in all the places where you can host an SPFx application. In fact, it is not available on a SharePoint page in a browser. Here are a couple of pointers to help you here:
AadHttpClient class | Microsoft Learn
Connect to Azure AD-secured APIs in SharePoint Framework solutions | Microsoft Learn

Now: how this does fit with 3rd party IdPs? We currently support only Azure AD as IdP. That means that federation between identity providers is the only way for you to integrate with other IdPs.

@Ben-Oops
Copy link
Author

Hi @lucabandMSFT,

Thanks for your help.

If i understand correctly your comments it's not possible to achieve what i tried to do with SPFx because it only supports Entra ID (Azure AD). So my only way is to use Azure B2C federation with NAA. Right ?

Reminder of my goal: Connect in SSO if possible from a Teams App deployed in multiple tenants to an Azure B2C through user Entra ID identity of each tenants.

I'm a bit lost on how to achieve this goal.

What i've done so far: In Azure B2C i declared an Entra Id (Single Tenant for now to test). This way i'm able to connect with my personal tenant account to Azure B2C using MSAL library for example. But the problem comes when i tried to implement this authentication in Teams App. As Teams App don't authorize loginPopUp or loginRedirect, i decided to use TeamsJS for authentication. I don't succeed to make it work properly as you can see above.

I someone can give me some leads on how to achieve this goal, it will be greatly appreciate as i'm a bit stuck with SPFx. Thanks.

@AE-MS
Copy link
Contributor

AE-MS commented Jan 27, 2025

I just want to check to ensure we are answering the right question. Is it required for your solution to use SPFx? Or are you also open to building a Teams app using just teamsjs without the use of SPFx? I didn't know if being part of SharePoint was a requirement for your app or if it was just a choice you made because you thought it was the best/easiest way to achieve your goal.

@Ben-Oops
Copy link
Author

It's not mandatory to use SPFx. I use it because it's already integrated with native M365 environment. I can also use Fluent to create an experience equivalent to the native M365 experience.

@lucabandMSFT
Copy link

lucabandMSFT commented Jan 31, 2025

@Ben-Oops , let's try with simple steps (first.. then we can add more complexity later). Assuming you can federate (AzureAD federation with the other IdP). Does using AADHttpClient withing the SPFx application works for you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants