Search code examples
azureoauth-2.0cypressazure-ad-b2ccypress-login

Azure B2C login with Cypress oauth 2.0 - {tenant}.b2clogin.com


I am currently trying to implement a Cypress e2e test which is involving Azure B2C AD as external identity provider. So first I tried to visit the login page just as a normal user would, with:

cy.get('#loginButton').click();
// this will visit: 
https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/oauth2/v2.0/authorize?...
cy.wait(2000);
cy.get('input').first().type(email).should('have.value', email);
cy.get('[id="password"]').type(password).should('have.value', password);
cy.get('#next').click();
// should now redirect me to my app with the access token in my local storage

But then i get the following error:

{statusCode: 401, message: "Unauthorized"}

While doing it myself inside the browser works perfectly fine. Cypress cannot do it. Then I learned, that Cypress cannot accept state from external websites and I need to get the login token with cy.request() and inject it in my application:

https://github.com/cypress-io/cypress/issues/1342

Tutorial: Azure AD Authentication in Cypress Tests with MSAL

Example Code

The Problem here is, that they used:

https://login.microsoftonline.com/${Cypress.env('AZURE_TENANT_ID')}/oauth2/v2.0/token

but my app uses:

https://${Cypress.env('AZURE_TENANT_NAME')}.b2clogin.com/${Cypress.env('AZURE_TENANT_NAME')}.onmicrosoft.com/${Cypress.env('AZURE_AD_LOGIN_POLICY_NAME')}/oauth2/v2.0/token

I tried to adapt the tutorial request, but it does not work for me:

  Cypress.Commands.add('login', () => {    
    cy.request({
      method: 'POST',
      url: `https://${Cypress.env('AZURE_AD_AUTH_TENANT')}.b2clogin.com/${Cypress.env('AZURE_AD_AUTH_TENANT')}.onmicrosoft.com/${Cypress.env('AZURE_AD_AUTH_LOGIN_POLICY_NAME')}/oauth2/v2.0/Token`,
      form: true,
      body: {
        grant_type: 'password',
        client_id: `${Cypress.env('AZURE_AD_AUTH_CLIENT_ID')}`,
        client_secret: Cypress.env('AZURE_AD_AUTH_CLIENT_SECRET'),
        scope: 'openid%20offline_access',
        username: Cypress.env('AZURE_USERNAME'),
        password: Cypress.env('AZURE_PASSWORD')
      },
    }).then((response) => {
      injectTokens(response);
    });

This request yields:

{
    "error": "invalid_request",
    "error_description": "AADB2C90055: The scope 'openid offline_access' provided in request must specify a resource, such as 'https://example.com/calendar.read'.\r\nCorrelation ID: 147e543e-7b12-4349-8917-ad7d97b4b7cd\r\nTimestamp: 2022-12-08 11:40:05Z\r\n"
}

Sadly I have no clue what this error means, which URL need i to provide? Which is the permission a user has to login to the website? I just want the token so i can visit the website as authentificated user with Cypress.

I know here are some posts with suggestions on how to approach this, but no a single one works for my url, with the ".b2clogin.com" inside, this works diffrent i guess.

Here are the 3 exposed urls i can call:

https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/oauth2/v2.0/authorize
https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{loginPolicy}/SelfAsserted
https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{loginPolicy}/oauth2/v2.0/token

Solution

  • I tried to reproduce the same in my environment via Postman and got below results:

    I registered one web application in my B2C tenant and added API permissions as below:

    enter image description here

    In Manifest, make sure to enable implicit flow like below:

    enter image description here

    Now, I created one resource owner user flow like below:

    enter image description here

    When I tried to get the token giving same parameters via Postman, I got same error as you like below:

    POST https://b2ctenantname.b2clogin.com/b2ctenantname.onmicrosoft.com/<ROPC_policyname>/oauth2/v2.0/token
    grant_type:password
    client_id:<App_ID>
    client_secret:<secret>
    scope:openid offline_access
    username:<UPN_of_B2C user> 
    password:xxxxxxxxxxxx
    

    Response:

    enter image description here

    To resolve the error, you need to include your Application ID in scope parameter.

    I generated the tokens successfully by changing scope like below:

    POST https://b2ctenantname.b2clogin.com/b2ctenantname.onmicrosoft.com/<ROPC_policyname>/oauth2/v2.0/token
    grant_type:password
    client_id:<App_ID>
    client_secret:<secret>
    scope:openid <App_ID> offline_access
    username:<UPN_of_B2C user> 
    password:xxxxxxxxxxxx
    

    Response:

    enter image description here

    If you want to get both access and id tokens, then include response_type parameter like below:

    POST https://b2ctenantname.b2clogin.com/b2ctenantname.onmicrosoft.com/<ROPC_policyname>/oauth2/v2.0/token
    grant_type:password
    client_id:<App_ID>
    client_secret:<secret>
    scope:openid <App_ID> offline_access
    response_type: token id_token
    username:<UPN_of_B2C user> 
    password:xxxxxxxxxxxx
    

    Response:

    enter image description here

    In your scenario, change value of scope parameter in your code by including your Application ID along with openid and offline_access.

    Reference:

    Set up a resource owner password credentials flow - Azure AD B2C | Microsoft