okta / okta-vue

OIDC SDK for Vue
https://github.com/okta/okta-vue
Other
46 stars 25 forks source link

AuthSdkError: Unable to retrieve OAuth redirect params from storage #134

Closed sergiomartindev closed 1 year ago

sergiomartindev commented 1 year ago

Describe the bug

Description: After integrating the @okta/okta-vue library into my Vue.js application and configuring the necessary components and routes, I encountered an error message when attempting to log in with Okta. The specific error message is "AuthSdkError: Unable to retrieve OAuth redirect params from storage."

Reproduction Steps:

Expected Behavior: I expected the Okta login process to successfully authenticate the user and redirect them back to my application. The access token should be retrieved and set as the authToken for subsequent requests, and the application should function without any errors.

Actual Behavior: Instead of successfully logging in and redirecting back to the application, I encountered the "AuthSdkError: Unable to retrieve OAuth redirect params from storage" error message. This error occurs when attempting to retrieve the OAuth redirect parameters from the storage mechanism used by the Okta SDK.

This bug prevented successful authentication with Okta and hindered the overall functionality of my application.

To resolve the issue, I performed several troubleshooting steps, including clearing the browser cache and cookies, verifying the Okta configuration, checking browser support, and enabling Okta debugging. However, the issue persisted, leading me to believe it is a bug within the @okta/okta-vue library that requires further investigation and resolution by the library maintainers.

Reproduction Steps?

Import the necessary dependencies in your main app entry file):

import { createApp } from 'vue';
import App from './App.vue';
import OktaVue from '@okta/okta-vue';
import okta from '@app/okta';

Within an immediately invoked async function, create the Vue app, configure it to use the Okta plugin, and mount it:

(async () => {
  const app = createApp(App);
  app.use(OktaVue, okta);

  app.mount('#app');
})();

In your router configuration file, import the LoginCallback component from @okta/okta-vue and define the / path to use it:

import { LoginCallback } from '@okta/okta-vue';

export const routes: AppRoute[] = [
  {
    path: '/',
    component: LoginCallback,
    meta: {
      requiredRoles: [],
    },
  },
  // Other routes...
];

Trigger the login process in your global route guard if the user is not authenticated:

if (!authStore.isAuthenticated) {
  await authStore.login();
}

In the login method of authStore, use the useOktaAuth hook to retrieve the access token and set it as the authToken. Additionally, set the authentication header using clientService:

async login() {
  const oktaAuth = useOktaAuth();
  const oktaAuthToken = oktaAuth.getAccessToken();
  this.authToken = oktaAuthToken;
  clientService.setAuthHeader(httpClient.getHttpClient(), oktaAuthToken as string);
}

SDK Versions

System: OS: Windows 10 10.0.19044 CPU: (8) x64 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz Memory: 938.77 MB / 7.74 GB Binaries: Node: 16.17.0 - C:\Program Files\nodejs\node.EXE npm: 8.15.0 - C:\Program Files\nodejs\npm.CMD Browsers: Edge: Spartan (44.19041.1266.0), Chromium (114.0.1823.51)
Internet Explorer: 11.0.19041.1566 npmPackages: @okta/okta-vue: ^5.6.0 => 5.6.0 @vue/eslint-config-prettier: 7.1.0 => 7.1.0 @vue/eslint-config-typescript: 11.0.3 => 11.0.3 @vue/test-utils: 2.3.2 => 2.3.2 @vue/tsconfig: 0.4.0 => 0.4.0 vue: 3.3.4 => 3.3.4 vue-i18n: ^9.2.2 => 9.2.2 vue-router: 4.2.1 => 4.2.1 vue-tsc: 1.4.2 => 1.4.2 vuetify: 3.3.2 => 3.3.2

Additional Information

No response

jaredperreault-okta commented 1 year ago

@sergiomartindev Can you paste snippets of your usage of @okta/okta-auth-js? The error your mentioned is thrown by auth-js, not okta-vue.

I'd also recommend mounting your LoginCallback component on a path other than '/'. Our samples usually use '/login/callback'

sergiomartindev commented 1 year ago

@jaredperreault-okta Sure thing. Here it goes:


const oktaAuth = new OktaAuth({
  issuer: 'https://xxxxxxxxxxx',
  clientId: 'xxxxxxxxxxxxxx',
  scopes: ['openid', 'email', 'profile'],
  redirectUri: 'https://localhost:3000/',
});

export default {
  oktaAuth,
};

I am not redirecting to /login/callback due to some policies of my company. So for the moment I am redirecting to /. Is that relevant?

jaredperreault-okta commented 1 year ago

The LoginCallback is not the source of your bug, just a recommendation.

Do you call oktaAuth.signInWithRedirect() somewhere in your app? I'm not sure what authStore.login() actually does

sergiomartindev commented 1 year ago

@jaredperreault-okta I am not calling oktaAuth.signInWithRedirect() anywhere in the app.

The development flow is as follows:

I'm showing you the content of authStore.login() at the end of the following flow, which follows the application's authentication process.

Inside the root file, I instantiate the Vue application and associate it with the Okta-vue plugin:

import { createApp } from 'vue';
import App from './App.vue';

import OktaVue from '@okta/okta-vue';
import okta from '@app/okta';

const app = createApp(App);
app.use(OktaVue, okta);

app.mount('#app');

The content of @app/okta is as follows:

import { OktaAuth } from '@okta/okta-auth-js';

const oktaAuth = new OktaAuth({
  issuer: 'https://xxxxxxxxx',
  clientId: 'xxxxxxxxxxxxx',
  scopes: ['openid', 'email', 'profile'],
  redirectUri: 'https://localhost:3000/',
});

export default {
  oktaAuth,
};

The redirectUri redirects to /. This is my router configuration for managing that:

import { LoginCallback } from '@okta/okta-vue';

export const routes: AppRoute[] = [
  {
    path: '/',
    component: LoginCallback,
  },
];

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
});

Furthermore, within a global application guard, I have defined the following condition:

import { navigationGuard as oktaNavigationGuard } from '@okta/okta-vue';

async function beforeEach(to: AppRoute) {
  const routeRequiresAuthentication: boolean = Boolean(to.meta.requiresAuth);

  if (routeRequiresAuthentication) {
    const authStore = useAuthStore();

    await oktaNavigationGuard(to);

    if (!authStore.isAuthenticated) {
      await authStore.login();
    }
  }

  return true;
}

This condition indicates that if the user is not authenticated, it should call the authStore.login() method, which has the following content:

import { useAuth as useOktaAuth } from '@okta/okta-vue';

export default defineStore('auth', {
  actions: {
    async login() {
      const oktaAuth = useOktaAuth();
      this.authToken = oktaAuth.getAccessToken();
    }
}

It is worth mentioning that SOMETIMES it manages to generate the token and redirects me to the previous page, being authenticated. But some others it takes me to / and shows me the AuthSdkError: Unable to retrieve OAuth redirect params from storage message.

jaredperreault-okta commented 1 year ago

getAccessToken does not request an access token from the server, it reads an existing token from storage. This method will only return a token if an auth flow has already completed (and the user is currently authenticated). I believe the issue your experiencing is due to navigating to the LoginCallback component independent of an auth flow redirect.

The LoginCallback component is designed for Redirect Model (more info). The auth flow goes as follows:

  1. Unauthenticated user navigates to my.app
  2. Route Guard checks the user's auth state, determines they are unauthenticated and initiates redirect to Okta (this is usually done via signInWithRedirect())
  3. User enters their credentials into the SIW at {{yourokta}}.okta.com
  4. Once authentication is successful, the user redirected back to my.app (Okta will redirect to the configured redirectUri, this should be the same path the LoginCallback component is mounted at)
  5. The LoginCallback component will process the resulting authorization code, exchange it for tokens, and write them to storage
  6. At this point, the user is now authenticated and tokens have been written to storage

The reason we recommend mounting LoginCallback component on a dedicated route is it's not meant to be rendered outside of the context of a redirect from Okta

Edit: This sample app might help: https://github.com/okta/samples-js-vue/tree/master/okta-hosted-login

jaredperreault-okta commented 1 year ago

@sergiomartindev I am not seeing how/where a request is made to Okta for tokens. If you're not using signInWithRedirect, how are you requesting tokens from Okta?

sergiomartindev commented 1 year ago

@jaredperreault-okta It's because of the OktaNavigationGuard imported from okta-vue:

import { navigationGuard as oktaNavigationGuard } from '@okta/okta-vue';

Once I add it to the router, it redirects to the Okta login page. I thought it did the same job as signInWithRedirect.

jaredperreault-okta commented 1 year ago

@sergiomartindev My mistake, that is exactly what navigationGuard does. Not sure how I missed that in your snippets.

A difference I noticed between our sample and tests app and the code snippets your provided

const app = createApp(App);
app.use(router);        // samples/test app mount router before OktaVue
app.use(OktaVue, okta);

app.mount('#app');

Secondly, can you initiate a login from you app, then check your browser devtools to see if there is a entry in localStorage? It's key should be something like okta-transaction

jaredperreault-okta commented 1 year ago

@sergiomartindev Has this been resolved?

sergiomartindev commented 1 year ago

Hey @jaredperreault-okta, I apologize for jumping to other tasks and forgetting to close this matter. The error doesn't seem to be appearing anymore, which is kind of weird since I haven't made many changes since the original implementation.

As a summary:

Entry:

import { createApp } from 'vue';
import App from './App.vue';
import router from '@app/router';
import httpClient from '@app/httpClient';
import OktaVue from '@okta/okta-vue';
import okta from '@app/okta';

(async () => {
  await httpClient.initHttpClient();

  const oktaInstance = await okta.getOktaInstance();
  const app = createApp(App);

  app.use(router);
  app.use(OktaVue, { oktaAuth: oktaInstance });

  app.mount('#app');
})();

@app/okta:

import appConfigService from '@app/services/appConfig.service';
import { OktaAuth } from '@okta/okta-auth-js';

async function getOktaInstance(): Promise<OktaAuth> {
  const appConfig = await appConfigService.fetchAppConfig();

  return new OktaAuth({
    issuer: appConfig.oktaTenantId,
    clientId: appConfig.oktaClientId,
    redirectUri: appConfig.oktaRedirectUri,
    postLogoutRedirectUri: appConfig.oktaLogoutURL,
    scopes: ['openid', 'email', 'profile'],
    state: 'test',
    responseMode: 'fragment',
    responseType: ['token', 'id_token'],
    tokenManager: {
      storage: 'localStorage',
    },
  });
}

export default {
  getOktaInstance,
};

@app/router:

import { createRouter, createWebHistory } from 'vue-router';
import AppRoute from '@app/interfaces/AppRoute.interface';
import appGuard from '@app/guards/app.guard';
import { LoginCallback } from '@okta/okta-vue';
import { navigationGuard as oktaNavigationGuard } from '@okta/okta-vue';

export const routes: AppRoute[] = [
  {
    path: '/',
    component: LoginCallback,
    meta: {
      requiredRoles: [],
    },
  },
];

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  // @ts-ignore - Can't parse AppRoute to RouteRecordRaw
  routes,
});

router.beforeEach(oktaNavigationGuard);
// @ts-ignore - OurRoute doesn't match with NormalizedRoute
router.beforeEach(appGuard.beforeEach);

export default router;

@app/guards/app.guard:

import AppRoute from '@app/interfaces/AppRoute.interface';
import useAuthStore from '@app/stores/auth.store';
import { useAuth as useOktaAuth } from '@okta/okta-vue';
import httpClient from '@app/httpClient';
import clientService from '@app/httpClient/services/client.service';

async function beforeEach(to: AppRoute) {
  const routeRequiresAuthentication: boolean = Boolean(to.meta.requiresAuth);

  if (routeRequiresAuthentication) {
    const authStore = useAuthStore();

    if (!authStore.isAuthenticated) {
      const oktaAuth = useOktaAuth();
      const oktaAuthToken = oktaAuth.getIdToken();
      authStore.setAuthToken(oktaAuthToken as string);
      clientService.setAuthHeader(httpClient.getHttpClient(), oktaAuthToken as string);
    }
  }
}

export default {
  beforeEach,
};

And that would be it.

Regarding the okta-transaction key, there is indeed one called okta-shared-transaction-storage. In total, there are four keys: okta-original-uri-storage, okta-cache-storage, okta-shared-transaction-storage, and okta-token-storage."

jaredperreault-okta commented 1 year ago

I'll close this issue for now, feel free to reopen if the issuer occurs again