0

I have a Single Page App (SPA) written in ReactJS. I am attempting to query the Graph API. I am using the msal.js library to handle my authentication. I am using Azure AD B2C to manage my accounts. What I have so far:

I am able to login to Google and get my profile information back to my SPA. However, when an accessToken is requested, the result is empty. I am positive I'm doing something wrong and I'm guessing it's around my app registration and scopes. When I registered my app, it had a default scope called "user_impersonation". As I type this, I realize I registered my client (SPA) and not the API. Do I need to register both? I'm not sure how I would register Graph.

My code:

App.js

import AuthService from '../services/auth.service';
import GraphService from '../services/graph.service';

class App extends Component {
  constructor() {
  super();
  this.authService = new AuthService();
  this.graphService = new GraphService();
  this.state = {
    user: null,
    userInfo: null,
    apiCallFailed: false,
    loginFailed: false
  };
}
  componentWillMount() {}

  callAPI = () => {
    this.setState({
    apiCallFailed: false
  });
  this.authService.getToken().then(
    token => {
      this.graphService.getUserInfo(token).then(
        data => {
          this.setState({
            userInfo: data
          });
        },
        error => {
          console.error(error);
          this.setState({
            apiCallFailed: true
          });
        }
      );
    },
    error => {
      console.error(error);
      this.setState({
        apiCallFailed: true
      });
    }
  );
};

logout = () => {
  this.authService.logout();
};

login = () => {
  this.setState({
    loginFailed: false
  });
  this.authService.login().then(
    user => {
      if (user) {
        this.setState({
          user: user
        });
      } else {
        this.setState({
          loginFailed: true
        });
      }
    },
    () => {
      this.setState({
       loginFailed: true
      });
    }
  );
};

render() {
  let templates = [];
  if (this.state.user) {
    templates.push(
      <div key="loggedIn">
        <button onClick={this.callAPI} type="button">
          Call Graph's /me API
        </button>
        <button onClick={this.logout} type="button">
          Logout
        </button>
        <h3>Hello {this.state.user.name}</h3>
      </div>
    );
  } else {
    templates.push(
      <div key="loggedIn">
        <button onClick={this.login} type="button">
          Login with Google
        </button>
      </div>
    );
  }
  if (this.state.userInfo) {
    templates.push(
      <pre key="userInfo">{JSON.stringify(this.state.userInfo, null, 4)}</pre>
    );
  }
  if (this.state.loginFailed) {
    templates.push(<strong key="loginFailed">Login unsuccessful</strong>);
  }
  if (this.state.apiCallFailed) {
    templates.push(
      <strong key="apiCallFailed">Graph API call unsuccessful</strong>
    );
  }
  return (
    <div className="App">
      <Header />
      <Main />
        {templates}
    </div>
  );
 }
}

export default App

auth.service.js:

import * as Msal from 'msal';

export default class AuthService {
constructor() {
// let PROD_REDIRECT_URI = 'https://sunilbandla.github.io/react-msal-sample/';
// let redirectUri = window.location.origin;
// let redirectUri = 'http://localhost:3000/auth/openid/return'
// if (window.location.hostname !== '127.0.0.1') {
//   redirectUri = PROD_REDIRECT_URI;
// }
this.applicationConfig = {
  clientID: 'my_client_id',
  authority: "https://login.microsoftonline.com/tfp/my_app_name.onmicrosoft.com/b2c_1_google-sisu",
  b2cScopes: ['https://my_app_name.onmicrosoft.com/my_api_name/user_impersonation email openid profile']
  // b2cScopes: ['graph.microsoft.com user.read']
};
this.app = new Msal.UserAgentApplication(this.applicationConfig.clientID, this.applicationConfig.authority, function (errorDesc, token, error, tokenType) {});
// this.logger = new Msal.logger(loggerCallback, { level: Msal.LogLevel.Verbose, correlationId:'12345' });
}


login = () => {
    return this.app.loginPopup(this.applicationConfig.b2cScopes).then(
    idToken => {
    const user = this.app.getUser();
    if (user) {
      return user;
    } else {
      return null;
    }
  },
   () => {
      return null;
    }
  );
};


logout = () => {
  this.app.logout();
};


getToken = () => {
  return this.app.acquireTokenSilent(this.applicationConfig.b2cScopes).then(
    accessToken => {
      return accessToken;
    },
    error => {
      return this.app
        .acquireTokenPopup(this.applicationConfig.b2cScopes)
        .then(
          accessToken => {
            return accessToken;
          },
          err => {
            console.error(err);
          }
        );
      }
    );
  };
}

graph.service.js

export default class GraphService {
constructor() {
  // this.graphUrl = 'https://graph.microsoft.com/v1.0';
  this.graphUrl = 'http://httpbin.org/headers';
}

getUserInfo = token => {
  const headers = new Headers({ Authorization: `Bearer ${token}` });
  const options = {
    headers
  };
  return fetch(`${this.graphUrl}`, options)
    .then(response => response.json())
    .catch(response => {
      throw new Error(response.text());
    });
  };
}

I was getting an access denied error when trying to query the Graph API so I replaced graph.microsoft.com/1.0 with httpbin so I could actually see what was being passed in. This is where I saw that the token was null. This is the exact same code I pulled from Microsoft's example project, which works when I use their code. What they don't show is how AAD B2C and app registrations are configured which is where I believe my problem is.

Progger
  • 2,266
  • 4
  • 27
  • 51
  • post your **code**! and especially how you try to get an access_token. – astaykov Jun 21 '18 at 07:30
  • Can you structure your one concrete question. At the beginning you talk about Azure functions, at the end you try call Microsoft Graph. Two things - first, using Azure AD B2C you cannot use MS Graph. [B2C is not about MS Graph](https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-overview). Second - go and read the [docs about WebAPIs and Scopes in B2C](https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-access-tokens) – astaykov Jun 21 '18 at 19:24
  • This seems to say you can (and should) use the Graph API: https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-devquickstarts-graph-dotnet – Progger Jun 21 '18 at 20:05

3 Answers3

3

My problem was I needed to register 2 applications: 1 for my client, 1 for the API. Then, in the Azure AD B2C portal, I had to grant the client access to the API. Once I did that, an Access Token was given.

Progger
  • 2,266
  • 4
  • 27
  • 51
  • I have same issue and this fix my problem. For step by step to do this, please see official Microsoft doc: https://learn.microsoft.com/en-us/azure/active-directory-b2c/add-web-api-application?tabs=app-reg-ga – Quad Coders Dec 31 '20 at 07:25
  • 1
    And don't forget to request the scopes you want access to when requesting a token because if you don't request at least 1 scope, no token, as per https://stackoverflow.com/questions/63628333/b2c-authentication-not-returning-access-token – Guy Lowe Oct 28 '21 at 03:12
1

Sorry, it may be a bit confusing for users, but there are several different services here.

There is Azure AD Graph API - it is same for B2C

There is Microsoft Graph.

Then, within Azure AD B2C itself, you have your Application Registration for the B2C part. Using this application registration you cannot use any of the Graphs.

There is also a normal Application Integration in Azure Active Directory which is valid for the Graphs. However this type of application integrations (also as described here) cannot be used for B2C flows.

So, to make it clear again:

You cannot use any of the Graph APIs (neither Azure AD Graph API nor Microsoft Graph) from the context of an B2C application.

AND

You cannot use any B2C functionality (like login with google, etc.) from the context of "normal" app registered for use with Graph.

So, in context of B2C registered application you can only request and get an access_token for an API which is also registered for use with B2C.

astaykov
  • 30,768
  • 3
  • 70
  • 86
0

First log into your azure portal using link https://portal.azure.com and click the filter button and change the directory into created azure b2c tenant

enter image description here

Then click on All Services and search Azure AD B2C Click Applications and then click Add and then fill the form with a unique name as the below figure

enter image description here

Using this URI, you will allow the permission to your application to access certain features in your directory. As an example, this could be reading user profile information. After creating the Application you have to make sufficient permission for that pls follow below steps Click on the Applications label Click on the application you just created. Click on API access label Click + Add Select the application in step 4 from the Select API drop down. Select “Access this app on behalf of the signed-in user…” Click OK Then click create button In the left pane of Azure AD B2C you can find the label named User Flows click on that . Then click on the + New

User flow

enter image description here

Then click on the Sign up and Sign In and then select Recommended under the version and click Create after that give a name for your user flow and tick email sign up also tick the details you want in the return token under User attributes and token claims like the below images

enter image description here

enter image description here

Now you can see the created user flows and even you can run user flow using the Run user flow

enter image description here

Now let’s get started by creating a new ReactJS Project

Go to your directory you wish to create a reactjs application and type the below command to create a react in cmd opened.

$ npx create-react-app b2c-react

Now navigate to the newly created project directory by typing

$ cd b2c-react

to run the react app

$ npm start

We can go with two ways to achieve the target of get create login into your reactJS application using Azure Active Directory B2C.

Method I

1st - Go to access the b2c user flow created using Library provided by Microsoft that is using msal library. For that create a new file named B2c.js and add the code provided note that you have to configure the msalAppConfig as per your created instance that is,

var msalAppConfig = {
auth: {
clientId: '<client Id>',
authority:'https://<TenantSubDomain>.b2clogin.com/<Tenant>/<signInPolicy>',
redirectUri: '<Redirect Url>',
validateAuthority: false,
postLogoutRedirectUri: 'window.location.origin'},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: isIE()
}

Now need to replace the <> items from values in Azure AD B2C Application. To grab the value for the tenant, go back to your Azure AD B2C directory. Under overview, copy the value in “Domain name” field. tenant sub domain is first part of the tenant before before onmicrosoft.com.

enter image description here

Now, to grab the clientId, click on the Applications label, and copy the id from the newly created application and replace the value at applicatoinId field.

enter image description here

Now click on the User flows (polices) label and copy the name of the policy and replace the value at signInPolicy field.

enter image description here

The complete B2c.js will look like this

import React from 'react'; import * as msal from 'msal'

const state = {
    stopLoopingRedirect: false,
    config: {
      scopes: [],
      cacheLocation: null,
    },
    launchApp: null,
    accessToken: null,
    msalObj: null,   }

const LOCAL_STORAGE = 'localStorage' const SESSION_STORAGE = 'sessionStorage' const AUTHORIZATION_KEY = 'Authorization'    var isIE
= function isIE() {
    var ua = window.navigator.userAgent;
    var msie = ua.indexOf("MSIE ") > -1;
    var msie11 = ua.indexOf("Trident/") > -1;
    return msie || msie11;   };

  var B2C_SCOPES = {
    API_ACCESS: {
      scopes: ['https://b2cunicorn.onmicrosoft.com/api/user_impersonation']
    }   };
     var msalAppConfig = {
    auth: {
      clientId: '',
      authority: 'https://b2cunicorn.b2clogin.com/b2cunicorn.onmicrosoft.com/B2C_1_signup',
      redirectUri: 'http://localhost:3000',
      validateAuthority: false,
      postLogoutRedirectUri: 'window.location.origin'
    },
    cache: {
      cacheLocation: 'sessionStorage',
      storeAuthStateInCookie: isIE()
    }   };

  function acquireToken (successCallback) {
    const account = msalApp.getAccount()
  
    if (!account) {
      msalApp.loginRedirect(B2C_SCOPES.API_ACCESS)
    } else {
      msalApp.acquireTokenSilent(B2C_SCOPES.API_ACCESS).then(accessToken => {  
        if (msalAppConfig.cache.cacheLocation === LOCAL_STORAGE) {
            window.localStorage.setItem(AUTHORIZATION_KEY, 'Bearer ' + accessToken)
          } else {
            window.sessionStorage.setItem(AUTHORIZATION_KEY, 'Bearer ' + accessToken)
          }
          
        state.accessToken = accessToken
        if (state.launchApp) {
          state.launchApp()
        }
        if (successCallback) {
          successCallback()
        }
      }, error => {
        if (error) {
          msalApp.acquireTokenRedirect(B2C_SCOPES.API_ACCESS)
        }
      })
    }   }

  let msalApp;

  var authentication = {
    initialize: () => {
        console.log("aaa");
        msalApp = new msal.UserAgentApplication(msalAppConfig)
      },
    run: (launchApp) => {
        state.launchApp = launchApp
        msalApp.handleRedirectCallback(error => {
          if (error) {
            const errorMessage = error.errorMessage ? error.errorMessage : "Unable to acquire access token."
            console.log(errorMessage)
          }
        });
      acquireToken();
    },
    required: (WrappedComponent, renderLoading) => {
        return class extends React.Component {
          constructor (props) {
            super(props)
            this.state = {
              signedIn: false,
              error: null
            }
          }
    
          render () {
            if (this.state.signedIn) {
              return (<WrappedComponent {...this.props} />)
            }
            return typeof renderLoading === 'function' ? renderLoading() : null
          }
        }
      },

    signOut: function signOut() {
      return msalApp.logout();
    },
    getAccessToken: function getAccessToken() {
      return state.accessToken;
    }   };

  export default authentication;

Then edit index.js as follows

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import authentication from './B2C';

authentication.initialize();

authentication.run(()=>{
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  );
  serviceWorker.unregister();
});

Method II

This is using a third party library developed to sign In to azure active directory b2c. This library is well written for this task and I recommended to use it if you does not need special configuration as per your requirement. For that please install library using below command.

$ npm install react-azure-b2c --save

then configure the below object as per above information gained form Azure active directory.

authentication.initialize({
  instance: 'https://<Tenant_Sub_Domain>.b2clogin.com/', 
  tenant: '<YOUR-B2C-TENANT>',
  signInPolicy: '<SIGNIN-POLICY>',
  clientId: '<APPLICATION_ID>',
  cacheLocation: 'sessionStorage',
  scopes: [<SCOPE_ARRAY>],
  redirectUri: 'http://localhost:3000',
  postLogoutRedirectUri: window.location.origin,
});

Then your index.js file should be looked as below

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import authentication from 'react-azure-b2c';


authentication.initialize({
  instance: 'https://b2cunicorn.b2clogin.com/', 
  tenant: 'b2cunicorn.onmicrosoft.com',
  signInPolicy: 'B2C_1_signup',
  clientId:'<application id>',
  cacheLocation: 'sessionStorage',
  scopes: ['https://b2cunicorn.onmicrosoft.com/api/user_impersonation'],
  redirectUri: 'http://localhost:3000',
  postLogoutRedirectUri: window.location.origin,
  
});

authentication.run(()=>{
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  );
  serviceWorker.unregister();
});

After Method I or Method II you able make your login by using secured Azure active directory. The redirected portal will shown as below.

You will be able to customize the interface as you wish I hope to write about it in future.

enter image description here

Fezal halai
  • 756
  • 7
  • 14