Sapir’s failed research blog

I didn’t REALLY understand FOCI & BroCI… until now (Kinda)

FOCI and BroCI appear in many great research papers, conference talks, and attack write-ups. Even after reading most of them, I realized I couldn’t confidently explain how either mechanism actually worked. Every time I tried, I found another gap in my understanding.
This post is my attempt to fix that.

Instead of summarizing existing research, I wanted to build the mental model myself and answering the questions that came up along the way. Some of those questions led to small side quests, while others are still unanswered.

As I said, many great pieces of research have already been made on these topics. Here is a list:

Step #1: FOCI

FOCI (Family of Client IDs) is a concept where, by getting a token to one client, you can exchange it for a token to a different client that belongs to the same family. This way, the user doesn’t need to authenticate again to get an access token for another app.

All of the currently known FOCI applications are documented in entrascopes.

My first question was: How do you know an application belongs to FOCI? The answer is that Microsoft includes the foci claim in the token response when you authenticate to the application.

Here is an example: This is an ROPC request to Microsoft Office:

This ROPC authentication command results in the following response:

That’s really good to know. Now, it is also easy to understand how to detect all of the clients inside FOCI.

Side Quest: The secaud Claim

These extra claims that only exist for FOCI clients made me wonder if there are more interesting claims we only see for specific apps. This quest led me to the secaud claim.

This claim exists in some app+resource combinations, and it contains two sub-claims:

  1. aud
  2. scp

Let’s see an example from a token I got when I authenticated to Teams with the Skype resource:

As you can see, the scp inside the secaud is different and wider than the scp of the main token.

The Plain-English Version of this claim

Think of an access token as a visitor badge. The aud is the building the badge lets you into.

When you get a Teams token, the badge says: “Front desk: this is the user. Let them into Teams.” That’s aud

But inside the same badge is a smaller note: “P.S. Teams, if you need to walk over to the Microsoft Graph building on this person’s behalf, you’re pre-approved to do these specific things there: read/write their mail, read/write their groups…” That’s secaud

So, secaud indicates delegated permissions the receiving service may use when acquiring downstream tokens. It lets the service you’re talking to (Teams) turn around and call another service (Graph) for you, without going back to the login server to ask for a fresh token.

I don’t think you can use it yourself, I tried. The aud of my token was Skype, and when I tried to pass it to Microsoft Graph, I got an invalid token error. Maybe there is a specific method to use these tokens to access Microsoft Graph, but I haven’t found it yet.

After collecting tokens from several application/resource combinations, I ended up with the following Graph secaud values.

Combosecaud.scp
Office / Outlook + ExchangeMail.ReadWrite, Files.Read, Group.ReadWrite.All, User.Invite.All, Policy.Read.All, LicenseAssignment.Read.All, DataLossPreventionPolicy.Evaluate, SensitiveInfoType.Detect/Read.All, SensitivityLabel.Evaluate, User.Read
Teams + Exchange


Group.Read.All, Group.ReadWrite.All, User.Invite.All, Policy.Read.All, LicenseAssignment.Read.All, User.Read
Teams + Skype
Mail.Read, Mail.ReadWrite, MailboxSettings.ReadWrite, Sites.ReadWrite.All, Group.ReadWrite.All, Directory.Read.All, People.Read, Place.Read.All, ProfilePhoto.Read.All, User.ReadWrite
Teams + Substrate
Group.Read.All, Sites.Read.All, LicenseAssignment.Read.All, User.Read
OneDrive + SharePoint
Directory.Read.All, User.Read.All, User.Invite.All, User.RevokeSessions.All, Organization.Read.All, GroupMember.Read.All, GroupSettings.Read.All

Exchanging FOCI Tokens

Now, back to FOCI. Now that I know how to find these apps, I wanted to see what it looks like to exchange a FOCI access token for another FOCI access token.

I used ROPC for authentication since I wanted to stick to a case study where attackers only have a password. After the first authentication (which was to Microsoft Office with the Microsoft Graph resource),I tried to ask a token for Microsoft Teams (also FOCI), using my refresh token from the previous request.

And I indeed got a new access token, again with the FOCI claim. So, it’s basically using the standard refresh token process but refreshing it to a different client ID.

As a sanity check, I tried to use it to access the Microsoft Azure CLI (which is not a FOCI client), and, as expected, I got an invalid_grant error.

Lets summarize this part with a cute FOCI flow:

Office (FOCI client)
│ authenticate
use refresh token
Teams (FOCI client)
new access token

Step #2: BroCI (Broker / Hosted Apps)

Now that I understood FOCI, the next obvious thing was to understand BroCI.

Now, I’ll be honest with you: I read the blog posts, and I watched the talks. But every time I think I’ve got it, I try to explain it to someone else and discover unanswered questions I’m not sure how to resolve. That’s why I’m doing this.

Certain Microsoft applications embed other applications inside them. Rather than every embedded application performing its own authentication flow, Microsoft allows the host (“broker”) to obtain tokens on behalf of the hosted application.

I’m going to explain it in a way that makes me understand it better. We have 3 identities here:

  • User
  • Broker application
  • Hosted application

The concept is that some applications are nested (hosted) inside other applications. The most common example that everyone uses is the Azure Portal and Ibiza. We all know Ibiza is hosted inside the Azure Portal, up to that part, everything makes sense.

The goal of this flow is simple: if I’m currently authenticated to the Azure Portal and want to access information that Ibiza provides (like Users for example), Ibiza asks the broker to get a token for the user on its behalf. The Azure Portal will then use the refresh token it holds to request this new token for Ibiza.

Let’s see an example using Burp Suite. First, I got a completely normal token for the Azure Portal (notice the client ID):

  • Azure Portal = c44b4083-3bb0-49c1-b47d-974e53cbdf3c

The response yields a refresh token, an access token, and an OpenID token.

Now, I am using that refresh token and performing this request:

And a cute BroCI flow:

           User
            │
            │ authenticate
            ▼
      Azure Portal
     (Broker App)
            │
            │ refresh token
            ▼
      Entra ID
            │
            | access token
            ▼
         Ibiza
      (Hosted App)

Let’s understand what we see here. I’m asking for a token using some new fields:

  1. brk_client_id -> In our case, the Azure Portal
  2. brk_redirect_uri -> Will be explained later in the post
  3. client_id -> Ibiza (the app that needs the Azure Portal to ask for a token)
  4. refresh_token -> The RT we got when we authenticated to the Azure Portal

The response from this process is an access token for Azure Ibiza. I can see why Microsoft implemented this idea, it is for the same reason they implemented SSO, minimal user interactions for maximum user comfort.

Finding the Brokers

Now that I understood the concept, I had some questions.

1. How can I find the brokers? For a given app, how can I tell if it uses NAA (Nested App Authentication)?

The answer is: using the reply URL! These broker redirect URIs are exposed through application objects (often visible on the corresponding Service Principal), and you can find them using the Graph API. For example, if we run this query:

https://graph.microsoft.com/beta/servicePrincipals?$filter=appId eq 'bb2a2e3a-c5e7-4f0a-88e0-8e01fd3fc1f4'

We will get information about the “cpim service,” a random first-party application. In its reply URLs, we can see the following:

"replyUrls": [
"https://*.cpim.windows.net/*",
"brk-c44b4083-3bb0-49c1-b47d-974e53cbdf3c://rc.portal.azure.com",
"brk-c44b4083-3bb0-49c1-b47d-974e53cbdf3c://canary-ms.portal.azure.com",
"brk-c44b4083-3bb0-49c1-b47d-974e53cbdf3c://ms.portal.azure.com",
"brk-c44b4083-3bb0-49c1-b47d-974e53cbdf3c://preview.portal.azure.com",
"brk-c44b4083-3bb0-49c1-b47d-974e53cbdf3c://canary.portal.azure.com",
"brk-c44b4083-3bb0-49c1-b47d-974e53cbdf3c://portal.azure.com",
"https://proxy.b2clogin.com/tenantredirect/authresp",
"https://login.microsoftonline.com/te/tenantredirect/authresp",
"https://te.cpim.windows.net/tenantredirect/authresp"
]

This means the cpim service is an NAA that can be brokered by the Azure Portal (if you translate the GUID).

We can see it can be brokered by azure portal with multiple URIs. Another question I had is: What is the importance of the host? Why we need 6 different broker URIs for Azure Portal?

The answer: Anatomy of the URI

brk-c44b4083-3bb0-49c1-b47d-974e53cbdf3c://ms.portal.azure.com
└┬┘ └──────────────┬──────────────────┘ └────────┬────────┘
│ │ │
"broker" the BROKER's client ID the web page that
(Azure Portal) actually started the login

So one URI packs two different identities:

  • Who brokers the token → the client ID in the scheme (Azure Portal).
  • Which web page is allowed to receive it → the host.

NAA exists so a web app running inside a host (like an add-in or component loaded inside the Azure Portal) can get tokens through that host, instead of doing its own login.

  1. The nested app says “get me a token.”
  2. The broker (Azure Portal) does the actual auth with Entra.
  3. Entra validates the registered redirect URI, and browsers treat each origin separately, so each portal hostname requires its own registered broker redirect URI.

That last step is something i needed to understand: the broker doesn’t live at one single URL. Azure Portal is served from many different hostnames:

HostWhat it is
portal.azure.comnormal production portal
ms.portal.azure.comMicrosoft-internal
canary.portal.azure.comNot sure actually
preview.portal.azure.compreview
rc.portal.azure.comNot sure actually

Each of those is a separate browser origin. The browser treats ms.portal.azure.com and canary.portal.azure.com as completely different websites, a token meant for one is not allowed to be delivered to the other.

So Entra has to know, ahead of time, the exact origin it’s allowed to hand the token back to. That’s why every host needs its own registered redirect URI. The client ID alone isn’t enough, it tells Entra which broker, but not which of the broker’s many front doors the response is allowed to come back through.

Digging Into Unknown Brokers

Something I noticed in entrascopes and in my own validations is that some of the broker URIs point to an unknown application or are structured a bit differently. I downloaded all the apps from entrascopes and ended up with these unknown client IDs acting as brokers:

#Unknown Broker Client IDRelsAppsNested Apps That Trust It
brk-multihub (no client ID → Unknown app)21437Spread across Office, Copilot Studio, Yammer, M365Chat, etc.
1c836cbdb-7a5b-44cc-a54f-564b4b486fc6139Azure Service Linker, CPIM Portal Ext, AppInsights Ext, Graph Data Connect, ActivityLog, Monitoring Alerts, EMM ModernWorkplace, OperationsManagementSuite, Verifiable Credentials Admin
22821b473-fe24-4c86-ba16-62834d6e80c3254M365ChatClient, Office Online Add-in SSO, Office Online Core SSO, OfficeHome
3865367a6-9c28-4844-88ce-259d34dbabae102Office Online Add-in SSO, Office Online Core SSO
46c162bf8-dda4-4ec3-8c3c-b816baadab88102Office Online Add-in SSO, Office Online Core SSO
514e3d5b3-8888-4eb1-9343-e4aca764b965102Office Online Add-in SSO, Office Online Core SSO
603b184b5-8cb6-45d1-bef1-10db52790f06121Windows 365 Portal
796f7e131-7b0f-4947-8a62-9cd027b94147121Windows 365 Portal
81c65024a-5992-4a60-b9be-c40b388283e081Microsoft Engage Hub
900000000-0000-0000-0000-00004017045511Copilot App (FOCI, public client)
100b1df6d3-2deb-44d4-b44b-7101937e072611M365ChatClient

I assume these are undocumented applications? Based on their hosts, it looks like most of them are mostly office-related apps: *-oauth.officeapps.live.com.

I tried to authenticate to some of these using the device code flow to learn more about them, but the authentication failed because these are confidential apps. However, I did discover some information about them based on the errors I received:

App ID Verdict Name / Origin
--------------------------------------------------------------------------------------------------------------
0b1df6d3-2deb-44d4-b44b-7101937e0726 EXISTS Origin: outlook.office.com
c836cbdb-7a5b-44cc-a54f-564b4b486fc6 EXISTS Origin: *.azure.us / entra.microsoft.us (Gov)
2821b473-fe24-4c86-ba16-62834d6e80c3 EXISTS Origin: *.officeapps.live.com
865367a6-9c28-4844-88ce-259d34dbabae EXISTS - MSA (consumer) only OneDrive Web Consumer / Origin: *.officeapps.live.com
6c162bf8-dda4-4ec3-8c3c-b816baadab88 EXISTS Origin: *.officeapps.live.com
14e3d5b3-8888-4eb1-9343-e4aca764b965 EXISTS - DISABLED DEVApp Home Pages / Origin: *.officeapps.live.com
03b184b5-8cb6-45d1-bef1-10db52790f06 EXISTS Origin: windows365 / deschutes
96f7e131-7b0f-4947-8a62-9cd027b94147 EXISTS - blocked by evaluation policy Origin: windows365 / deschutes
1c65024a-5992-4a60-b9be-c40b388283e0 EXISTS Origin: engagehub / portal.azure.com

Then, I remembered I could use the interactiveauth module in roadrtx, which opens a Selenium-based browser and performs the auth code flow for you. This is an amazing capability, when you need to authenticate to apps the requires PKCE.

But even with the correct reply URI and the interactiveauth module, I didn’t succeed in authenticating to them. Based on the errors, I think I used the wrong scope (which is a bit surprising because I used Microsoft Graph).

  • I ended up discovering most of these applications, and I’m currently working on a PR to entrascopes to update them.

The Conditional Access Policy Bypass – By NetSPI

One very interesting thing is the Conditional Access Policy (CAP) bypass found by NetSPI. It’s actually one of the reasons I wanted to do this small quest. I read it, and it’s written extremely well, yet I felt like I lacked some basics to truly understand it. Now that I feel like I understand this concept pretty well, I can happily send you to read the blog post they wrote!

Some spoilers: NetSPI found several BroCI flows where Conditional Access evaluation could be bypassed under specific conditions.

So, if you stole a refresh token for the Azure Portal (which is a very realistic real-world scenario), you could use the BroCI flow to get an access token for Ibiza, specifically with the Microsoft Graph resource, while bypassing any CAP. They also found this vulnerability in other combinations, which are documented in their blog post!

Conclusion

One thing I didn’t expect when I started writing this post was how many new questions would appear once I finally understood the basics.

At first, my goal was simply to understand how FOCI and BroCI actually worked. But once I could reproduce the authentication flows myself, I found myself paying attention to things I had ignored before: undocumented brokers, unusual redirect URIs, claims like secaud, and applications that don’t quite fit the patterns described in previous research.

I don’t have answers to all of those questions yet, and that’s probably my favorite part. Building the mental model was only the beginning. Now I have a much better idea of where to look next.

Leave a comment