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 (to, from, next) => {
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((to, from, next) => {
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(h, err) {
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