Monday, June 21, 2021

Springboot + Vue.js + Azure Active Directory Authentication

Springboot + VueJS + Azure AD Authentication

 

This is a configuration guide on how to setup a SpringBoot application running as a backend API with a VueJS frontend, authenticated using Microsoft’s Azure Active Directory (AD).

 In this case, we are using Azure only for authentication, whereby staff and students of the university, can use their own credentials to access our custom-built platforms.

The process by which authentication occurs step-by-step can be summarised in the diagram below:



In the above diagram we can link the following:

Client: VueJS client application

Resource Server: Springboot API

Authorization server: Azure Active Directory

 

This approach relies on Azure configuration in both the front-end VueJS app and Springboot API app

1)      When a user first visits your secured VueJS application, the user needs to be identified and is redirected to the Azure authentication server for login. Once the user has been identified, the access token can be requrested from the Authorization server (in this case Azure).

2)      The access token is stored internally in the VueJS app and used later in subsequent calls to the API as part of the ‘Bearer’ header. The API (Resource server), accepts the token and forwards it to Azure for validation of the access Token.

3)      Azure responds to the API confirming the access token is valid, including what roles the user has.

4)      The API checks the user roles have access to the specific resource (API) being requested.

5)      Once access has been confirmed, the API returns the result to the VueJS client.

In this configuration, the API is no longer responsible for generating tokens. All tokens are managed by Azure. The API simply passes tokens around for validation.

Azure

https://portal.azure.com
https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade

Reference: https://devblogs.microsoft.com/azure-sdk/vue-js-user-authentication/

Goto “Active Directory”  


Before we can configure our VueJS and Springboot app, we need to create a new App in Azure. There are already many online instructions on how to do this, so I will cut to the chase:

Registration



App IDs

Once the new App has been registered we need to take note of the Client ID and Tenant ID:



 

 

Redirect URIs

Add the redirect URIs hosting your VueJS app.



The URIs should be for your VueJS app. In my case, localhost:3001 was for VueJS.

API Permissions

Make sure the App has the following API permissions set:

 

App Roles

For each app we can create our own roles, and assign users to those roles.



Here, we’ve created 2 roles with values ROLE_ADMIN and ROLE_RESEARCHER. The allowed member types should be set to ‘Users/Groups’.

Manifest (Might be optional)

In the manifest file set the accessTokenAcceptedVersion to 2

Enterprise Application Mode

Switch to Enterprise Application mode by searching for your App

Add users and assign roles



 



Pick a user and pick a role, and save.

 

And that’s it for the Azure setup.


 

VueJS + MSAL 2.0

Reference: https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser

Reference: https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-spa-app-registration

Reference: https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens

Microsoft has released a javascript library for developers to authenticate users with Azure Active Directory called MSAL.js

In your existing VueJS application, from the command line, import the following libraries.

npm I @azure/core-http

npm I @azure/msal-browser

 

At the time of writing this document, the versions were:

    "@azure/core-http""^1.2.6",

    "@azure/msal-browser""^2.14.2",

 

Under services folder, create a file called auth-azure.service.js from:

https://github.com/Philip-Wu/VueJS-MSAL/blob/main/auth-azure.service.js

Modify the file to set the clientId, tenantId and authority url in the config. The authority URL may be different depending on how your organization is setup. For more information on authority: https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-client-application-configuration#authority

In my case the authority was configured as:

authority: 'https://login.microsoftonline.com/<tenantId>,

 

Where the URL is suffixed with the tenantId.

Please take note of the defaultScope used for acquiring tokens.

This is super important. Otherwise, we get an error about 'Invalid signature' due to receiving an access token in v1.0 format.

The backend API will attempt to validate the token using a 2.0 endpoint, which is not suited for an v1.0 access token. So to force v2.0 accessToken, we use the ./default. This was not mentioned in any formal documentation

 

The defaultScope should be <your_client_id>/.default in order to get the v2.0 format for access tokens. This should already be coded in the function from the github file. This was not well documented by Azure.

The .env file should contain the redirect URI that should be the same as what was configured in Azure earlier:

VUE_APP_AZURE_AUTH_REDIRECT_URI='http://localhost:3001/'

It must be prefixed with VUE_APP_ in order for VueJS to make it available to the application. Otherwise, it will be undefined.


In the router/index.js file, when the user clicks on the login button, we can invoke the auth-azure script:

        {
            path: "/login",
            name: "login",
            component: Login,
            beforeEnter: async (tofromnext=> {                
                console.log('Login route, user: '+authAzure.user());
                if(! authAzure.user()) {
                    if (app != undefined) {
                        authAzure.appSignIn();
                    } 
                } else {
                    console.log('already authenticated user:');
                }

                next();
            }                     }, 

Further down in the router/index.js file, we want to stored a redirectPath in the session if a secured page was requested:

router.beforeEach((tofromnext=> {
  const publicPages = ['/login''/register''/','/home'];
  const authRequired = !publicPages.includes(to.path);
  const loggedIn = authAzure.isLoggedIn();
  // trying to access a restricted page + not logged in
  // redirect to login page
  if (authRequired && !loggedIn) {
    sessionStorage.setItem('redirectPath'to.path);
    
    next('/login');
  } else {
    next();
  }
});

 

In main.js, we handle the created() event by checking if the user is already logged in:

var app = new Vue({
    router,
    store,
    created() {
      console.log('app created')
      authAzure.init()
    },    
    async mounted() {
      console.log('app mounted')

      let redirectPath = sessionStorage.getItem('redirectPath');      
      if (authAzure.isLoggedIn() && redirectPath) {

        // Define function to be used as callback
        let redirectFunc = function() {
          console.log('redirecting to 'redirectPath)
          sessionStorage.removeItem('redirectPath')
          router.push(redirectPath)  
        }

        if (! authAzure.waitingOnAccessToken) {
          redirectFunc()
        } else {
          // Register callback to be executed when accessToken has been assigned
          authAzure.accessTokenCallbacks.push(redirectFunc)
        }
      }

    },
    render: h => h(App),
    renderError(herr) {
      return h('pre', { style: { color: 'red' }}, err.stack)
    } 
  });

 

 

Create a file called auth-header.js that will be used as a function for supplying the Bearer Authorization header with the acquired accessToken:

function authHeaderAzure() {
    let accessToken = authAzure.accessToken;
    console.log(app+'using token: '+accessToken);

    if (accessToken) {
        return { Authorization: 'Bearer ' + accessToken };
    } else {
        return {};
    }

}

 

Create a file auth.module.js to handle the dispatches from auth-azure.service.js. If the user was attempting to access a secured page directly, then they are redirected the requested page immediately after authentication.

import authAzure from '../services/auth-azure.service';
import app from '../main'

/**
 * Used for rendering the navigation componenet, Nav.vue, to manage the state of being Logged in vs Logged out.
 * Using the Vuex.store we can trigger Nav.vue to re-render upon logging in or out.
 */

const user = authAzure?.user()

const initialState = user
    ? { loggedIn: true  }
    : { loggedIn: false };

export const auth = {
    namespaced: true,
    state: initialState,

    mutations: {
        loginSuccess(state) {
            console.log('mutation loginSuccess')
            state.loggedIn = true;            
        },
        loginFailure(state) {
            state.loggedIn = false;
        },
        logout(state) {
            console.log('mutation logoutSuccess')
            state.loggedIn = false;
        },
        
    },

    actions: {
        loginSuccess({commit}) {
            commit('loginSuccess')

            // Redirect user if a page was loaded directly in the browser
            let redirectPath = sessionStorage.getItem('redirectPath');
            console.log('redirectPath: '+redirectPath)
            if (redirectPath) {
                sessionStorage.removeItem('redirectPath');
                app.$router.push(redirectPath)
            }

        },
        logout({commit}) {
            commit('logout')
        },
        loginFailure({commit}) {
            commit('loginFailure')
        }
    }
};

 

 

Troubleshooting

Can use the following tool to decode an access token. Take carefully notice of the token version number:

https://jwt.ms/


 

Springboot + Azure authentication

Reference: https://docs.microsoft.com/en-us/java/api/overview/azure/spring-boot-starter-active-directory-readme?view=azure-java-stable (accessing a Resource server section)

Reference: https://github.com/Azure/azure-sdk-for-java/tree/azure-spring-boot-starter-active-directory_3.5.0/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-active-directory-resource-server

Reference: https://developer.okta.com/blog/2019/06/20/spring-preauthorize

 

To setup our Springboot app as a Resource server, we add the following Azure libraries to our dependency to our pom.xml file:

<dependency>
   <groupId>
com.azure.spring</groupId>
   <artifactId>
azure-spring-boot-starter-active-directory</artifactId>
   <version>
3.5.0</version>
</dependency>
<dependency>
   <groupId>
org.springframework.boot</groupId>
   <artifactId>
spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

 

Then we need to configure our springboot security by creating the following file:

@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity
(prePostEnabled = true)
class WebSecurityConfig extends AADResourceServerWebSecurityConfigurerAdapter{

   
@Autowired
   
UserDetailsServiceImpl userDetailsService

   
@Autowired
   
private AuthEntryPointJwt unauthorizedHandler

   
@Override
   
protected void configure(HttpSecurity http) throws Exception{
       
super.configure(http)

               
.antMatchers("/","/v1/var/**","/home","/signin","/login","/hash", "/signup", "/explorer/**").permitAll()
               
.anyRequest().authenticated()     } } }} 

Next we need to configure our Azure app by providing the tenant-id and client-id in the application.yml file:

azure:
 
activedirectory:
   
tenant-id: <azure app tentant id>
   
client-id: <azure app client id>

 

If you want to debug an azure related error, I would also highly suggest setting the root logging level to DEBUG in the application.yml. This helped me to resolve a misleading error message:

logging:
 
file:
   
path: logs
 
level:
   
root: DEBUG

 

Before we can define the role access for our API in the controller, we need to work out how the roles are renamed by Azure (or the library). This is where I spent most of my time as none of this was mentioned in any of the official documentation or any forums. This is where the logging level of DEBUG was handy.

If the roles do not match exactly between Azure and the Springboot API, then on the browser we may see network errors when debugging using the “Developer tools” of the browser:



Here we can see the error says “insufficient_scope” and “The request requires higher privileges than provided by the access token”. For me this was misleading, suggesting that the issue was related to the Azure API Permissions, but that was not the case. The API permission ‘User.Read’, should be sufficient privileges for authentication and accessing our API. Rather than an API permission issue, it was really a ROLE configuration issue.

On the API server, we can see the following logs:

2021-06-21 11:42:38,284 {HH:mm:ss.SSS} [http-nio-8081-exec-1] DEBUG o.s.s.o.s.r.w.BearerTokenAuthenticationFilter - Set SecurityContextHolder to BearerTokenAuthentication [Principal=com.azure.spring.aad.webapi.AADOAuth2AuthenticatedPrincipal@4581efe6, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[SCOPE_User.Read, APPROLE_ROLE_ADMIN]]

We can see that Azure is prefixing my configured roles with APPROLE_

This means, in my controllers, I need to specify my role access using APPROLE_ROLE_ADMIN as follows:

@Slf4j
@RestController
@RequestMapping
(path="/v1/var")
@PreAuthorize("hasAnyAuthority('APPROLE_ROLE_ADMIN', 'APPROLE_ROLE_RESEARCHER')")
class VariantAnnotationController {

Here we use the @PreAuthorize annotation and the hasAnyAuthority method to control access to the API based on the Azure roles previously configured.


 

Other notes:

Initially I had tried to use the VueJS plugin as a wrapper for the MSAL.js client library, but I had trouble getting that to work, perhaps because it was initially designed for an older version of the MSAL library and hasn’t been updated for a while. Just for reference the vueJs plugin is the ‘vue-msal’ plugin: https://github.com/mvertopoulos/vue-msal