DEV Community

loading...

Authorization Code Flow with PKCE in Azure AD with React

Cibrax
Pablo is an internationally recognized expert and entrepreneur with more than 22 years of experience in designing and implementing large distributed systems in different stacks.
Originally published at thecibrax.com ・4 min read

Authorization Code Flow with PKCE

The implicit authorization code flow was initially released for native/dumb and javascript applications running in a context of a browser that did not have a dedicated backend to negotiate an access token from an authorization server (due to a limitation in browsers that prevented JavaScript from making cross-domain calls) .

In this flow, access tokens were returned directly to the browser without requiring any client secret. These aspects made it naturally less secure, so additional practices were used to mitigate any potential vulnerability (e.g short lived tokens, pre-registered redirection URIs or a set of a unique nonce and state parameters in the URL)

Nowadays, most browsers support Cross-Origin Resource Sharing (CORS) so that cross-domain call limitation is not longer an issue. PCKE or Proof of Code Key Exchange leverages CORS in the browser to negotiate an access token in two steps.

A code is negotiated in the first step with the following request,

HTTP GET https://AzureADURL/authorize 
&response_type=code 
&redirect_uri=https://localhost:3000/login 
&scope=openid profile
&client_id=..... 
&code_challenge=......
&code_challenge_method=S256 
Enter fullscreen mode Exit fullscreen mode

And a second step is executed to get the actual access token,

HTTP POST https://AzureADURL/token
&code=.....
&client_id=.... 
&grant_type=authorization_code
&code_verifier=....
&redirect_uri=https://localhost:3000/login
Enter fullscreen mode Exit fullscreen mode

The code returned in the first call is the result of a cryptographic algorithm computation (hash) from the code challenge and code challenge method arguments passed in the first call. The code is later validated in the second call takes the code with the code verifier argument.

Authorization Code Flow with PKCE in Azure AD

This authorization code flow was recently enabled in Microsoft Azure AD.

Microsoft also released an update of the Microsoft Authentication Library (MSAL) for javascript to support this flow, which is now called msal-browser.

As this library is still in beta, documentation and samples are hard to find. I used one of the Vainila JS starter samples to implement the reusable content provider for React that is shared as part of this article.

The application must be registered as any other regular app in Azure AD under App Registrations, but the the reply url must be set with the type "spa" as it is shown below,

"replyUrlsWithType": [
        {
            "url": "http://localhost:3000/",
            "type": "Spa"
        }
    ],
Enter fullscreen mode Exit fullscreen mode

http://localhost:3000 is where the React application is running.

Context Provider Hook in React

In a typical React application, data/state is passed from top/parent to down/children components using properties, but this might not be ideal for cross-cutting corners that apply to all components or state that is shared between all of them. Security is one of those concerns, so it is a good candidate to be implemented as a context provider.

A context provider allows sharing state between components without explicitly passing it as properties through every level of the three. It would be equivalent to a global state that can be accessed on demand by the components.

React provides a hook "useContext" to inject a context with shared state in any component.

Context Provider for Azure AD authentication
The context provider I wrote for this article provides different accessors for state representing the current security context in the applications and functions for user authentication.

<MsalContext.Provider
            value={{
                isAuthenticated,
                user,
                token,
                loading,
                popupOpen,
                loginError,
                login,
                logout,
                getToken
            }}
        >
        {children}
</MsalContext.Provider>
Enter fullscreen mode Exit fullscreen mode

There is a semantic difference between login/getToken and user/token. While login performs user authentication with Open ID/Connect for getting an ID token (the user information), getToken returns an access token for consuming a backend API.

The configuration passed to these two functions (login and getToken) is slightly different. One uses the "openid profile" scopes, and the other uses one scope defined by the specific api to be consumed.

// ID token request
export const loginRequest = {
    scopes: ["openid", "profile", "User.Read"],
    forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token
};

// Access token request.
export const apiRequest = {
    scopes: ["api://{API ID}/ReadWrite"],
    forceRefresh: false 
}
};
Enter fullscreen mode Exit fullscreen mode

The entry point for using the new msal browser library is the PublicClientApplication object, which receives the configuration for connecting to Azure AD as part of the constructor.

export const config = {
    auth: {
        clientId: "{CLIENT ID}",
        authority: 'https://login.microsoftonline.com/{TENANT ID}',
        redirectUri: 'http://localhost:3000/',
    },
    cache: {
        cacheLocation: "localStorage", // This configures where your cache will be stored
        storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge


};
}

const pc = new msal.PublicClientApplication(config);
Enter fullscreen mode Exit fullscreen mode

The following component shows how the context provider is imported and used to log in/out the user with two different buttons.

const Welcome = () => {
  const { user, logout, getToken, token, loginError } = useMsal();


  return (
    <div>
      <h1>Welcome {user.userName}</h1>
      {token && (<span>Your token is {token}</span>)}
      <br></br>
      <button onClick={() => getToken(apiRequest, "loginPopup")}>Get Token</button>
      <br></br>
      <button onClick={() => logout()}>Log out</button>
      <br></br>
      {loginError && (<span>Error: {loginError.message}</span>)}
    </div>
  );
};


export default Welcome;
Enter fullscreen mode Exit fullscreen mode

Full source code for this sample

The full source code for this context provider and sample is available in Github.



Discussion (0)