Microsoft’s Azure Active Directory B2C service allows cloud administrators to define custom policies, which orchestrates trust between principals using standard authentication protocols. One such custom policy that B2C defines by default is the Resource Owner Password Credentials (ROPC) flow, which implements the OAuth standard authentication flow of the same name and allows users to simply supply their credentials to the authorization server in order to obtain a valid access token. Due to its simplicity, the Internet-Draft that defines the current best practices for OAuth 2.0 explicitly states that the “resource owner password credentials grant MUST NOT be used.” For its part, Microsoft’s own documentation warns against using this custom flow, as follows:

[i]n most scenarios, more secure alternatives are available and recommended. This flow requires a very high degree of trust in the application, and carries risks that are not present in other flows. You should only use this flow when other more secure flows aren’t viable.

However, both the Internet-Draft and the Microsoft documentation are only concerned with how the authentication process may be impacted (i.e., they recommend against using ROPC flow as it exposes user credentials to the client) and do not consider how the authorization process is impacted. This could leave cloud administrators under-informed when weighing the risks of implementing an ROPC flow and unintentionally exposing their applications to unauthorized attacks if they ultimately deploy the default B2C ROPC flow. In this blog, we will discuss how the B2C ROPC custom flow implementation by Microsoft is inherently vulnerable, provide an example of how you can exploit this weakness, and outline steps you can take to protect yourself against this vulnerability.

Why is the ROPC custom flow implementation inherently vulnerable?

The Azure documentation encourages an application or user to send a request to the authorization server a claim that contains the scope of the identity token the application or user would require. By default, the configuration of an ROPC flow deployed in Azure B2C will accept and process scopes claims. In contrast, according to the OAuth ROPC definition in OAuth 2.0 RFC 6749, section 4.3, this processing is purely optional (see Figure 1).

Figure 1. The Azure documentation recommends the use of the scope claim.

These scopes, along with all the other information provided to the B2C flow, are subsequently encoded in a JSON Web Token (JWT). The token is then returned to the authenticating application or user who can use the JWT as an access token for another web application, as Figure 2 illustrates.

Figure 2. The ROPC authentication flow as depicted by the Azure documentation.

At this point, once the web application receives the previously-generated access token in a request, the web application will check the list of encoded scopes to validate whether the request is authorized. The web application also has no reason not to trust the JWT, as it was generated by Azure B2C.

Herein lies the problem: since Azure B2C has no control over what scopes a user or application can request, and the web application has no reason not to trust a JWT generated by Azure B2C, an attacker can submit arbitrary scopes to a publicly-exposed authorization server (in this case, the B2C ROPC flow). Doing this enables the attacker to obtain valid access tokens that provide them with unauthorized scopes, augmenting an attacker’s impact in the event that they obtain valid credentials for an application that relies on B2C as its identity provider.

Example Attack Leveraging ROPC to Escalate Privileges

To demonstrate this inherent vulnerability in the ROPC custom flow, we first created a web application that relies on Azure B2C as its authorization server. We based this application on the Azure sample web API found here, and modified it so that the Passport.js authentication middleware required the bearer token to include a specific scope for each API endpoint. Below is an example of this authorization check, in which the application verifies that the JWT presented to it has the “demo.read“ scope.

Next, we registered the application within the Azure AD B2C tenant (see Figures 3-5).

Figure 3. An application named “ropc_flow“ was registered to the Azure AD B2C tenant. Note that the client ID ends in “d85“.

Figure 4. The application was configured to specify that the application is a public client.

Figure 5. The application also had a read and write permission for its associated application.

Then, we created a resource owner user flow to authenticate using ROPC within the Azure B2C tenant. We configured the application to allow the OAuth v2.0 implicit flow (see Figures 6 and 7).

Figure 6. The application registration was configured to allow OAuth 2.0 Implicit Flow.

Figure 7. An ROPC user flow called “B2C_1_ropc_test“ was created within the Azure AD B2C tenant.

Finally, we used the authentication flow to receive a valid session token from the B2C tenant. The following terminal command sends a request to authenticate a user using the ROPC flow:

Access Token Scope Considerations

Note that the scopes submitted by the request only include “openid“ and the B2C app registration ID, which is not encoded within a scope section of the JSON Web Token (JWT), as we can see in Figure 8.

Figure 8. The data encoded within the JWT indicates that it was issued by the B2C tenant and authorized the web application’s client ID (the “azp“ value) to make valid requests to itself.

If we used this token to call the “/api/todolist/ endpoint“, we would receive an “Unauthorized“ error response like the one below, because the JWT lacked authorization to make valid requests to the back-end (the “aud“ value). Additionally, it did not have the required “demo.read“ and “demo.write“ claims within the JWT.

By default, when the application front-end requests an access token on behalf of the user (such as the Azure sample single-page web app found here), it only requests that the B2C flow return a JWT with a hard-coded scope. However, since the authorization server is exposed to the public Internet, an attacker with the user’s credentials can simply request a new access token with the “read” and “write” scope as we have done below:

Azure B2C will now return a valid JWT with expanded privileges, thus allowing an attacker to escalate their privileges, which we see in Figure 9 and the code snippet that follows it.

Figure 9. The decoded malicious JWT indicates that it was issued by the B2C tenant and authorized the registered application’s front end client ID (the “azp“ value) to make valid requests to the backend API (the “aud“ value).

What can you do to prevent this from happening to your applications?

In order to prevent attackers from exploiting this vulnerability in your environment, we advise you to avoid using the Azure B2C ROPC custom flows in general. Instead, cloud administrators should use the OAuth 2.0 code grant if possible.

If your organization must use ROPC, we recommend implementing one of two workarounds. The first workaround is to use a separate JWT claim (similar to the Roles claim) to store permissions, and then make sure that application authorization checks are implemented to validate permissions at the time of use. Encode this claim into the JWT without relying on any user-controlled input. Instead, rely on the ground truth permissions database. In our example, this would mean that once the user presents their credentials in a request to the authorization server, the B2C flow itself will first authenticate the user, then look up the user’s role in another database and encode it into the access token it returns to the requester.

The other workaround is to block access to the default B2C domain name according to the instructions in the Azure B2C documentation. In doing so, you can limit network access to the ROPC custom flow to only hosts that need to use it. In the case of our example, only the web application would be able to communicate with the authorization server, blocking attackers from injecting arbitrary scopes into their access token.