I was building an internal application using Nuxt 2, with a separate PHP API handling the backend.
Unlike most websites I’d worked on, the application needed proper user authentication. Different users needed different levels of access, and the API couldn’t simply trust that somebody had logged into the frontend.
I didn’t want to build an authentication system from scratch, particularly when it came to managing passwords, multi-factor authentication and sessions.
I decided to use Okta, which provides identity management and authentication using OpenID Connect and OAuth 2.0.
Getting users logged in was fairly straightforward. Keeping everything authenticated as tokens expired turned out to be much more interesting.
Setting up Okta in Nuxt
The application used Nuxt 2 and Vue 2, with client-side rendering.
I used the @okta/okta-auth-js and @okta/okta-vue packages to integrate Okta with the application.
The configuration lived in a Nuxt plugin.
1import Vue from 'vue' 2import OktaVue from '@okta/okta-vue' 3import { OktaAuth } from '@okta/okta-auth-js' 4 5export default (context, inject) => { 6 const oktaAuth = new OktaAuth({ 7 issuer: context.$config.OKTA_ISSUER + '/oauth2/default', 8 clientId: context.$config.OKTA_CLIENT_ID, 9 redirectUri: window.location.origin + '/implicit/callback',10 scopes: ['openid', 'profile', 'email'],11 pkce: true12 })13 14 oktaAuth.start()15 16 Vue.use(OktaVue, { oktaAuth })17 18 inject('auth', oktaAuth)19}
The issuer identifies the Okta authorisation server, while the client ID identifies the application.
Both can be configured through Nuxt’s runtime configuration.
The important setting here is pkce: true.
PKCE, or Proof Key for Code Exchange, is designed for applications which can’t securely store a client secret, such as a JavaScript application running in the browser.
It adds protection to the authorisation code flow without requiring a secret to be embedded in the frontend.
Creating the login page
Rather than building the entire login form myself, I used Okta’s Sign-In Widget.
The widget handles the authentication interface, including the parts which would otherwise require a lot of additional code.
I loaded it dynamically on the login page.
1const { default: OktaSignIn } = await import( 2 '@okta/okta-signin-widget' 3) 4 5const widget = new OktaSignIn({ 6 baseUrl: this.$config.OKTA_ISSUER, 7 clientId: this.$config.OKTA_CLIENT_ID, 8 redirectUri: window.location.origin + '/implicit/callback', 9 authClient: this.$auth10})11 12widget.showSignInToGetTokens({13 el: '#okta-signin-container'14}).then(async (tokens) => {15 await this.$auth.tokenManager.setTokens(tokens)16 widget.remove()17})
Once the user authenticated, the tokens were stored by Okta’s token manager.
I could then load the user information and the data needed by the application.
This also meant I didn’t need to handle passwords directly in my own code.
Authenticating API requests
The frontend communicated with a separate API, using Axios.
Logging into Okta wasn’t enough on its own. Every API request also needed to include an access token so the backend could verify who was making the request.
Rather than manually adding the token every time I called the API, I created an Axios interceptor.
1export default function ({ $axios, $auth }) {2 $axios.interceptors.request.use(async (config) => {3 const token = await $auth.getAccessToken()4 5 config.headers.Authorization = `Bearer ${token}`6 7 return config8 })9}
Now, whenever the application made a request, the interceptor retrieved the current access token and attached it to the Authorization header.
The API could then validate the token and decide whether the user had permission to access the requested resource.
I also used the user’s group information to control access to parts of the frontend.
Of course, hiding something in the UI isn’t security. The API still needs to perform its own authorisation checks.
The problem with expiring access tokens
Access tokens don’t last forever.
That’s a good thing for security, but it creates an interesting problem in a single-page application.
A user might log in, open the dashboard and leave it running for several hours.
Eventually, their access token expires.
If the application tries to make another API request using that expired token, the request fails.
Okta provides automatic token renewal, but I ended up taking more control over the process because I wanted to coordinate renewal with the rest of the application.
I needed to know when tokens were being renewed so that other requests could wait rather than being sent with outdated credentials.
Renewing tokens before they expire
I disabled Okta’s automatic renewal and added my own scheduling logic.
The access token contains an expiry timestamp, which can be used to work out when it needs renewing.
I scheduled renewal thirty seconds before the token expired.
1const accessToken = await auth.tokenManager.get( 2 'accessToken' 3) 4 5const expiresAt = accessToken.expiresAt * 1000 6const now = Date.now() 7 8const delay = expiresAt - now - 30000 9 10if (delay <= 0) {11 await renewTokens()12} else {13 setTimeout(renewTokens, delay)14}
When the timer fired, the application requested new tokens and stored them using Okta’s token manager.
I also kept a flag in Vuex indicating whether token renewal was in progress.
That turned out to be useful for the next problem.
Pausing API requests during renewal
Imagine an access token is about to expire.
The application starts renewing it, but at the same time several components make API requests.
Without any coordination, those requests might be sent with the old token before renewal finishes.
I added another Axios interceptor which checks whether token renewal is in progress.
If it is, the request waits.
1function pauseWhilstRenewing(config, store) { 2 if (!store.state.isRenewingTokens) { 3 return config 4 } 5 6 return new Promise((resolve) => { 7 const unwatch = store.watch( 8 state => state.isRenewingTokens, 9 (renewing) => {10 if (!renewing) {11 unwatch()12 resolve(config)13 }14 }15 )16 })17}
Once the renewal finishes, the waiting requests can continue.
This works alongside the interceptor responsible for attaching the access token.
The order of those interceptors matters. Requests need to wait until renewal has completed before the new token is attached.
Handling logout
I also added a logout function which signed the user out of Okta and cleared locally stored application data.
1await auth.signOut({2 postLogoutRedirectUri: window.location.origin3})
The application then returned to its unauthenticated state.
It’s important to clear application state as well as ending the identity provider’s session, particularly if the frontend has cached information about the previous user.
What I learned
The main thing I took away from this project is that authentication isn’t just about checking a username and password.
A real application needs to consider token expiry, API authorisation, refreshing sessions, multiple simultaneous requests, long-running connections and logout behaviour.
Using an identity provider removes the need to implement the most sensitive parts of authentication yourself, but you still need to integrate it properly with the rest of your application.
And in a single-page application, keeping everything authenticated over several hours can be more complicated than getting somebody logged in in the first place.