authenticationoauth-2.0oauthcors

Microsoft OAuth authorization code flow CORS


I have an SPA, where the user will login using Microsoft Entra using authorization code flow. Client is public, so I registered my app as SPA client, following Microsoft oath code flow documentation. Let's assume I have no API in this project, because I want to use public flow and the API should not be needed since we are using PKCE instead of client secret.

Client registered as SPA in my Entra

To obtain the token:

I'm generating PKCE (challenge and verifier) and then requesting authorization code from

https://login.microsoftonline.com/[my tenant id]/oauth2/v2.0/authorize

sending

Which gives me GET request URL:

https://login.microsoftonline.com/[my tenant id]/oauth2/v2.0/authorize?client_id=[my client id]&response_type=code&redirect_uri=[url-encoded redirect uri]&response_mode=query&scope=.default&code_challenge=[generated challenge]&code_challenge_method=S256

So far so good. Im receiving auth code at the given URL. Now I would like to exchange obtained auth code for the token, sending POST request at token endpoint:

this.http.post(
    'https://login.microsoftonline.com/[my tenant id]/oauth2/v2.0/token',
    {
      client_id: '[my client id]',
      scope: '.default',
      code: '[received auth code]',
      redirect_uri: 'https://[my domain]/ms-auth-response',
      grant_type: 'authorization_code',
      code_verifier: '[code verifier matching the challenge],
    },
    {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      }
    });

Now the issue I'm facing is that login endpoint suprisingly returns CORS error, stating cross-origin requests are

cors error

There is a notice about this kind of errors in the docs:

cors notice

But i have my client registered as SPA and manifest is correct:

app manifest

Now, if I send the code along with verifier from POSTMAN, setting manually "origin" header to my domain - microsoft endpoint is returning token.

Without origin header, as expected "Tokens issued for the 'Single-Page Application' client-type may only be redeemed via cross-origin requests." error is returned.

So MS is telling me - you cant use cross origin request but you have to use cross origin request?

I was trying to check out some samples from microsoft github or even MSAL library, but npm wont even install the modules since they're all either deprecated or working with some old node versions (14-15 or so).

I was integrating many OAuth2 SSO's including google, using implicit grands and code flows, buth never have I faced the issue like here. Why is it working from postman with origin set to my domain but not from the domain itself in browser? I've checked in chrome tools and all the headers sent to the token endpoint are set correctly (origin, referer etc.)

Have anyone faced simmilar issue? I dont want to change the flow or register confidentail web api client to be intermediate party in the whole proces as it should not be needed.

Some advice how to deal with those CORS errors, or maybe there is anything else to know about going with oauth using Microsoft login?


Solution

  • As pointed out by CBroe in the comments, the error was caused by misencoding the request. I assumed that Angular's http client will do it, looking at the headers (that's what Postman was doing - which is why it was working just fine there). Explicitly encoding the object using URLSearchParams solved the problem immidiately:

    const encodedBody = new URLSearchParams({
      client_id: '[my client id]',
      scope: '.default',
      code: '[received auth code]',
      redirect_uri: 'https://[my domain]/ms-auth-response',
      grant_type: 'authorization_code',
      code_verifier: '[code verifier matching the challenge],
    });
    
    this.http.post(
        'https://login.microsoftonline.com/[my tenant id]/oauth2/v2.0/token',
        encodedBody.toString(),
        {
            headers: {
                'Cache-Control': 'no-cache',
                'Content-Type': 'application/x-www-form-urlencoded',
            }
        }
    );