okta / okta-vue

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

Okta Auth plugin internally fails with an undefined instance of this.oktaAuth which leads to our Web SPA failing on startup and each route change. #14

Open CVANCGCG opened 4 years ago

CVANCGCG commented 4 years ago

I'm submitting a:

Current behavior

Our Vue single page application fails on startup and displays a blank page stemming from an error that originates from within the Okta Plugin within @okta/okta-vue/Auth.js that indicates the internal instance of await oktaAuth is undefined and fails to establish a user identity from an accessToken/idToken.

Uncaught (in promise) TypeError: Cannot read property 'oktaAuth' of undefined
    at _callee$ (webpack-internal:///./src/router/index.js:94)
    at tryCatch (webpack-internal:///./node_modules/regenerator-runtime/runtime.js:62)
    at Generator.invoke [as _invoke] (webpack-internal:///./node_modules/regenerator-runtime/runtime.js:296)
    at Generator.prototype.<computed> [as next] (webpack-internal:///./node_modules/regenerator-runtime/runtime.js:114)
    at step (webpack-internal:///./node_modules/@vue/babel-preset-app/node_modules/@babel/runtime/helpers/builtin/es6/asyncToGenerator.js:12)
    at _next (webpack-internal:///./node_modules/@vue/babel-preset-app/node_modules/@babel/runtime/helpers/builtin/es6/asyncToGenerator.js:27)

Expected behavior

We expect the normal behavior where the redirect to Okta to result in either in a successful login or refresh of the existing access and id token. In either case we expect the library to always internally resolve to a defined (non-null and non-undefined) instance in all of the references to: "this.oktaAuth"

Minimal reproduction of the problem with instructions

  1. Create new Vue application
  2. Add this route initializer Note: In the code snippet below, you see that our initialization of the Auth plugin uses defaults that result in autoRenew: true and the use of local storage. And because our application displays confidential data, we take a vigilant approach to checking the user on each route change that uses the router::beforeEach() and router::beforeResolve() event hooks. The intent is to ensure we authenticate users and renew session tokens via router::beforeEach() event and subsequently invalidate/synchronize the user in the router::beforeResolve() event handler.
    
    import Auth from '@okta/okta-vue'
    import store from '@state/store'

Vue.use(Auth, { issuer: process.env.VUE_APP_OKTA_ISSUER_URL, clientId: process.env.VUE_APP_OKTA_CLIENT_ID, redirectUri: ${window.location.origin}/implicit/callback, scopes: ['openid', 'profile', 'email'], pkce: true, })

Vue.use(VueRouter) Vue.use(VueMeta, { // The component option name that vue-meta looks for meta info on. keyName: 'page', })

const router = new VueRouter({ routes: allRoutes, scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition } else { return { x: 0, y: 0 } } }, })

3. Add these pre and post route guards to ensure only authenticated users are granted access

router.beforeEach(Vue.prototype.$auth.authRedirectGuard())

router.beforeResolve(async (routeTo, routeFrom, next) => { // Fetch actively logged in use from IDP and conditionally update store const authenticatedIDPUser = await Vue.prototype.$auth.getUser() if (!authenticatedIDPUser) { debugger alert('[router.beforeResolve] "$auth.getUser()" returned null/undefined') } const isAuthorizedUserLoggedIn = store.getters['auth/isLoggedIn'] const synchronizeUserStoreRequired = authenticatedIDPUser && !isAuthorizedUserLoggedIn if (synchronizeUserStoreRequired) { await store.dispatch('auth/onLoggedIn', Vue.prototype.$auth) }

// If we reach this point, continue resolving the route. next() })


4. start application via ``npm run serve``

## Extra information about the use case/user story you are trying to implement
<!-- Describe the motivation or the concrete use case. -->
We need to ensure each user is authenticated and has a valid access token before access is granted to the application and data is displayed via APIs.

## Environment

- Package version: 1.1.1
- Vue version: 2.6.10
- Browser: Chrome
- OS: MacOS Mojave, i.e. OSX 10.14.5
- Node version (`node -v`): 12.0.0
- Other:
CVANCGCG commented 4 years ago

image

@ca

CVANCGCG commented 4 years ago

Also an update: as we employ workarounds, we are noticing this logged error:

Error in [router.beforeResolve] maintaining profile: OAuthError: The client specified not to prompt, but the user is not logged in.

When searching for this topic, I come across this lively Github issue here. Is there a known solution for this problem that has yet to be officially documented?

aarongranick-okta commented 4 years ago

@CVANCGCG Based on the error "Cannot read property 'oktaAuth' of undefined", It looks like Vue.prototype.$auth has not been initialized. This is done in the install() method defined here: https://github.com/okta/okta-oidc-js/blob/master/packages/okta-vue/src/okta-vue.js#L6

It could be that the install method has not been called yet. It may also be possible that the constructor is throwing an exception. If you are able to set a breakpoint in the debugger in this function you should be able to see what is happening. okta-vue is distributed with external source maps, you can load these into your app using the source-map-loader plugin for webpack.

This error: "OAuthError: The client specified not to prompt, but the user is not logged in." will occur during token renew or any call to token.getWithoutPrompt when there is not a currently active Okta session. It is probably not related to this issue, which I suspect is either about timing or configuration.

CVANCGCG commented 4 years ago

@aarongranick-okta, I've enabled source maps in vue.config.js as follows.

const { sslConfig, registerMiddleware } = require('./server/util')

module.exports = {
  transpileDependencies: ['vuetify'],
  css: {
    // Enable CSS source maps.
    sourceMap: true,
    // Note!: We use 'The easy solution' outlined at https://joshuatz.com/posts/2019/vue-mixing-sass-with-scss-with-vuetify-as-an-example/
    //        It is critical to use the 'scss' instead of 'sass' key to use SCSS flavor of SASS
    loaderOptions: {
      scss: {
        data: `@import "src/styles/base/_variables.scss";`
      }
    }
  },
  // Configure Webpack's dev server.
  // https://cli.vuejs.org/guide/cli-service.html
  devServer: {
    before: registerMiddleware,
    host: 'localhost',
    // We don't have the ssl certs in Codebuild
    https: process.env.CODEBUILD_BUILD_ID ? false : sslConfig(),
    progress: false,
    allowedHosts: ['localhost', '127.0.0.1', '.amazonaws.com', '.capgroup.com', '.oktapreview.com', '.okta.com']
  },
  configureWebpack: {
    devtool: 'eval-source-map',
    entry: './src/main.ts',
    // stats: 'errors-warnings',
    plugins: [],
  },
}

Unfortunately the source code I'm debugging is a tokenized version and not what I expect. Here's a screenshot. image

Do you see anything wrong that is causing that esoteric transformed JS code? Suppose we fix the source map for the okta distribution, what would we walk it back to in our application code? Do you see anything unexpected with my initialization of the Auth plugin and use of route guards? It appears to be standard and consistent with the Okta developer guidance?

aarongranick-okta commented 4 years ago

To include sourcemaps, the source-map-loader should be added to the webpack config, under the "module / rules" section, as described here: https://webpack.js.org/loaders/source-map-loader/

Having the sourcemaps will make it easier to debug the code. This line in your code:

Vue.use(Auth, {
  issuer: process.env.VUE_APP_OKTA_ISSUER_URL,
  clientId: process.env.VUE_APP_OKTA_CLIENT_ID,
  redirectUri: `${window.location.origin}/implicit/callback`,
  scopes: ['openid', 'profile', 'email'],
  pkce: true,
})

should create an instance of the auth service and attach it to the Vue prototype as Vue.prototype.$auth. This may not happen if an exception is thrown during this step. (The exception might be caught by Vue, try enabling "Break on all exceptions" in your Chrome developer tools (by default it breaks only on UNCAUGHT exceptions).

The most common reasons why an error would occur during construction are invalid configuration or insufficient browser capabilities (such as on IE, which requires a polyfill).

The error trace shows the error being thrown at ./src/router/index.js:94 Definitely put a breakpoint or try/catch at or before this location. Hopefully debugging will be able to reveal why this variable is undefined.

aarongranick-okta commented 4 years ago

The problem may be caused by the logic in router.beforeResolve running on the callback route. The callback route should not contain any logic, including reading tokens or retrieving user info. It is in the middle of an auth flow and should complete before any questions are asked. If you had a block if (routeTo === '/implicit/callback') return it might fix it for you.

CVANCGCG commented 4 years ago

Hi @aarongranick-okta, I appreciate the suggestion and will test this approach.