Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Support for vue / quasar applications #47

Open
Excel1 opened this issue Nov 19, 2024 · 24 comments
Open

[FEATURE] Support for vue / quasar applications #47

Excel1 opened this issue Nov 19, 2024 · 24 comments
Assignees
Labels
enhancement New feature or request

Comments

@Excel1
Copy link

Excel1 commented Nov 19, 2024

Introduction

Currently, there is very little documentation available on using or migrating to badisi auth-js, which makes it challenging to customize my code. For this reason, I am seeking help here. If the implementation is successful, I plan to extend the existing documentation to make the library more accessible.

Current Setup

My project is built with Quasar and Vue 3. I have developed a hybrid application powered by Capacitor, using Keycloak in combination with oidc-client.ts for authentication. While everything works as expected, Apple’s App Store Guideline 4.0 requires using the Safari In-App Browser (via the Capacitor Browser Plugin) for redirection handling instead of the default browser.

Project Details

  • Boot Files: Executed immediately when the app starts or the webpage loads in a browser.
  • Service & Store Pattern: The app follows this pattern for state management.
  • Offline Authentication: A refresh token is stored to allow the user to stay offline for up to 30 days without needing to log in again.
  • Token Storage: Tokens are saved using WebStorageStateStore combined with Shared Preferences for offline compatibility.
  • Platform-Specific Redirection: iOS uses localhost#/, while Android uses localhost/#/ due to a Quasar-specific behavior.

Migration Goal

The aim is to adapt the authentication flow to comply with Apple's requirements by switching to badisi auth-js while maintaining the existing functionality and platform-specific quirks.

Current Code Base

auth.service.ts

let userManager: UserManager | null = null;
let currentUser: User | null | undefined = undefined;
let refreshTokenInterval: string | number | NodeJS.Timeout | undefined;
let lastLoginTime: string | null = localStorage.getItem('lastLoginTime');
const userStore = useUserStore();
const teamStore = useTeamStore();

export default {
  async initOidcClient(isRedirect?: boolean) {
    userManager = getUserManagerInstance();
    try {
      if (isRedirect) {
        if (AppService.isMobile()) {
          if (AppService.isAndroid()) {
            currentUser = await userManager.signinRedirectCallback(window.location.href.replace('/#/', '/'));
          } else {
            currentUser = await userManager.signinRedirectCallback(window.location.href.replace('#/', '/'));
          }
        } else {
          currentUser = await userManager.signinRedirectCallback();
        }
      } else {
        currentUser = await userManager.getUser();
      }

      if (currentUser && !currentUser.expired) {
        setTokenInterval();
        registerTokenInterceptor();
        return currentUser;
      }

      setTokenInterval();
      registerTokenInterceptor();

      return currentUser;
    } catch (error) {
      throw error;
    }
  },
  async login(redirectUri?: string) {
    try {
      await this.initOidcClient(false);
      await userManager?.signinRedirect({ redirect_uri: redirectUri });
    } catch (error) {
      throw error;
    }
  },
  async logout() {
    await logoutDeviceSpecific();
  },
  async onResume() {
    if (currentUser && currentUser.expired) {
      await refreshAccessToken();
    }
  }
};

async function logoutDeviceSpecific() {
  clearInterval(refreshTokenInterval);
  try {
    if (Platform.is.mobile) {
      userStore.clearCurrentUser();
      teamStore.clearCurrentTeam();
      if (AppService.isAndroid()) {
        await userManager?.signoutSilent();
      } else {
        await userManager?.signoutRedirect({post_logout_redirect_uri: 'myapp://logout'})
      }
    } else {
      const cookiesValue = localStorage.getItem('cookies');

      localStorage.clear();

      if (cookiesValue !== null) {
        localStorage.setItem('cookies', cookiesValue);
      }

      await userManager?.signoutRedirect();
    }
  } catch (error) {
    console.error('OIDC logout error:', error);
  }
}

function getUserManagerInstance() {
  if (!userManager) {
    if (AppService.isMobile()) {
      userManager = new UserManager({
        authority: 'keycloakurl',
        client_id: 'client',
        redirect_uri: 'myapp://login',
        post_logout_redirect_uri: 'myapp:/' + '/logout',
        response_type: 'code',
        scope: 'openid profile email offline_access',
        filterProtocolClaims: true,
        loadUserInfo: true,
        automaticSilentRenew: false,
        userStore: new WebStorageStateStore({ store: new MobileStorage() })
      });
    } else {
      userManager = new UserManager({
        authority: 'keycloakurl',
        client_id: 'client',
        redirect_uri: window.location.origin + '/login',
        post_logout_redirect_uri: window.location.origin,
        response_type: 'code',
        scope: 'openid profile email offline_access',
        filterProtocolClaims: true,
        loadUserInfo: true,
        automaticSilentRenew: false,
        userStore: new WebStorageStateStore({ store: window.localStorage })
      });
    }
  }
  return userManager;
}

function registerTokenInterceptor() {
  api.interceptors.request.use(async (config) => {

    const status = await Network.getStatus();
    console.log('Network status:', status.connected);
    if (!status.connected) {
      return Promise.reject(new axios.Cancel('No internet connection'));
    }

    console.log('Request interceptor:', config);

    let user = await userManager?.getUser();
    if (user && !user.expired) {
      config.headers.Authorization = `Bearer ${user.access_token}`;
    }
    if (user?.expired) {
      user = await userManager?.signinSilent();
      config.headers.Authorization = `Bearer ${user?.access_token}`;
    }
    return config;
  });
}

function setTokenInterval() {
  refreshTokenInterval = setInterval(refreshAccessToken, 1000000);
}

async function refreshAccessToken() {
  if (currentUser) {
    try {
      currentUser = await userManager?.signinSilent();
      if (currentUser && !currentUser.expired) {
        console.log('Access token refreshed');
        lastLoginTime = String(Date.now());
      } else {
        console.log('Access token refresh failed');
      }
    } catch (error) {
      console.error('Error refreshing access token:', error);
      if (error instanceof Error && error.message === 'Stale token') {
        console.log('Stale token, signing out');
        await logoutDeviceSpecific();
      }

      // 28 days
      if (lastLoginTime && Date.now() - Number(lastLoginTime) > 2419200000) {
        console.log('Token expired, signing out');
        await logoutDeviceSpecific();
      }
    }

    if (lastLoginTime && Date.now() - Number(lastLoginTime) > 2419200000) {
      console.log('Token expired, signing out');
      await logoutDeviceSpecific();
    }
  }
}

authentication.ts - boot

export default boot(async ({ router }) => {
  const authStore = useAuthStore();

  try {
    await authStore.initOidcClient(isRedirect);
  } catch (error) {
    console.error('Failed to initialize OIDC client:', error);
    console.log(authStore.getUser);
  }
});

auth.store.ts

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: <User | null | undefined>undefined,
  }),
  getters: {
    getUser(): User | null | undefined {
      return this.user;
    },
    isAuthenticated(): boolean | undefined {
      return !!this.user && !this.user.expired;
    },
    isOfflineAuthenticated(): boolean | undefined {
      return !!this.user?.refresh_token;
    },
    getEmail(): string | undefined {
      return this.user?.profile?.preferred_username;
    },
  },
  actions: {
    async initOidcClient(isRedirect?: boolean) {
      try {
        this.user = await AuthService.initOidcClient(isRedirect);
      } catch (error) {
        console.log('Failed to initialize OIDC client:', error);
      }
    },
    async login(redirectUri?: string) {
      try {
        await AuthService.login(redirectUri);
      } catch (error) {
        throw error;
      }
    },
    async logout() {
      try {
        await AuthService.logout();
      } catch (error) {
        console.error(error);
      }
    }
  }
});

routerGuard.ts - boot

export default boot(({ router }) => {

  async function initializeOidcAfterRouting() {
    console.log('OIDC client initialized');
    try {
      await authStore.initOidcClient(true);
    } catch (error) {
      console.error('Failed to initialize OIDC client:', error);
    }
  }


  const authStore = useAuthStore();

  router.beforeEach(async (to, from, next) => {

    if (authStore.isOfflineAuthenticated && to.fullPath.includes('/login')) {
      next('/');
    }

    if (to.matched.some(record => record.meta?.requiresAuth)) {
      if (authStore.isOfflineAuthenticated) {
        next();
      } else {
        next('/home');
      }
    } else {
      next();
    }
  });
});

Questions

  • Can I use auth-js to implement exactly what I have already implemented? (different redirectUris, offline login)
  • Where do I have to start with the migration and what do I have to consider?
  • How extensive will the migration be and can I keep the current pattern?
@Badisi
Copy link
Owner

Badisi commented Nov 19, 2024

Hello @Excel1,

Thanks for the interest and sorry about the lack of documentation.
This is clearly something I would like to improve, but always find myself not having enough time to.


To give you some highlights:

  • this library was made with 3 things in mind:

    1. Provide one single library to do both desktop and mobile authentication

      when you are developing hybrid apps (ie. same app code running on both desktop and mobile) it is absurd to have to install 2 different libs (with different implementations) to do the authentication. To date, I still don't know of any lib that's doing that (or maybe ionic auth connect but it's a paid option).

    2. Make sure all the security standards and bests practices are respected so that the developer don't have to think about it

      developers are often not security experts, so the lib takes care of doing what's best in current security recommendations

    3. Make sure that a good level of security is kept

      like I said, developers are usually not security experts, so the lib should guarantee a good level of security at all time. For that, the access control and settings have to be limited to make sure no one is bringing security holes into his own app.

  • the core of this library is made in pure Javascript and is mostly a wrapper on top of oidc-client-ts

  • an Angular implementation was also made but only adds Angular's features, like guards, service, interceptor and a schematic to ease the installation process

    • I guess a Vue implementation could be easily made based on this same implementation
      • And if you are willing to give it a try, I would be happy to assist if needed 😉

Regarding the code your provided:

Based on what I see, most of this code could easily be removed as it is already managed by this library.


Regarding Apple's guidelines:

This library will never use the "system browser" as it is not considered a best practice.
Right now the library is expecting @capacitor/browser to be installed (which is an "in-app browser").
But I'm also planning to develop another library (later on) which will provide "custom tabs / browser tabs browsers", which are required for SSO to work.


Regarding offline mode:

The library will automatically renew the user's tokens, one minute prior to access_token expiration (this could be overridden by settings). At that time, it will uses the refresh_token to do the renewal.
So users can remain authenticated, with their tokens renewed silently, as long as their session is active.


Regarding tokens storage:

On desktop
As a good security practice, tokens are not stored on desktop - they are kept in the app memory.
So as long as you are logged in and the app is live, when your access token is going to expired, the refresh token will be used to renew the user access. But once the app is closed, the tokens are lost.
Reopening the app (or simply refreshing the page), means that the user will not be re-logged in automatically (even if his session is still active). So in case the lib was configured with retrieveUserSession: true -> an hidden iFrame will be used to contact the IDP and retrieve the tokens when the app starts.

On mobile
Tokens are stored in the device so that the user could be automatically re-logged in when reopening the app.
Depending on the plugins you have installed in your app, the library will recognize and use the following storage (by priority order):

  1. capacitor-secure-storage-plugin (recommended)
  2. @capacitor/preferences
  3. @capacitor/storage
  4. localStorage

Regarding your questions:

  1. Can I use auth-js to implement exactly what I have already implemented? (different redirectUris, offline login)

Anything that was working with oidc-client-ts should work with this library
I just have a doubt about your redirect uris point but this could be managed easily too if required

  1. Where do I have to start with the migration and what do I have to consider?

Have a look at: #30 (comment)
And also at the Angular's implementation and the demo apps
Live demo app is currently broken for VanillaJS, but you can play with the Angular one: here
You can play with the demo apps: here
Finally, reach to me if you need more help, as I'm really interested in making this library also available for Vue

  1. How extensive will the migration be and can I keep the current pattern?

Migration should be fairly easy as the idea behind this library is to do everything for you

@Excel1
Copy link
Author

Excel1 commented Nov 20, 2024

@Badisi At first thank you for the detailed answer. I tried to translate oidc-client.ts code into using your code. Can you take a look if the transformation should working?

Preparations

  1. installed @Badisi/auth-js
  2. capacitor browser plugin
  3. capacitor/preferences

Code Transformation

auth.service.ts

let userManager: UserManager | null = null;
let currentUser: User | null | undefined = undefined;
let refreshTokenInterval: string | number | NodeJS.Timeout | undefined;
let lastLoginTime: string | null = localStorage.getItem('lastLoginTime');
const userStore = useUserStore();
const teamStore = useTeamStore();

export default {
  async initOidcClient(isRedirect?: boolean) {
    userManager = getUserManagerInstance();
    try {
      if (isRedirect) {
        if (AppService.isMobile()) {
          if (AppService.isAndroid()) {
            currentUser = await userManager.signinRedirectCallback(window.location.href.replace('/#/', '/'));
          } else {
            currentUser = await userManager.signinRedirectCallback(window.location.href.replace('#/', '/'));
          }
        } else {
          currentUser = await userManager.signinRedirectCallback();
        }
      } else {
        currentUser = await userManager.getUser();
      }

      if (currentUser && !currentUser.expired) {
        setTokenInterval();
        registerTokenInterceptor();
        return currentUser;
      }

      setTokenInterval();
      registerTokenInterceptor();

      return currentUser;
    } catch (error) {
      throw error;
    }
  },
  async login(redirectUri?: string) {
    try {
      await this.initOidcClient(false);
      await userManager?.signinRedirect({ redirect_uri: redirectUri });
    } catch (error) {
      throw error;
    }
  },
  async logout() {
    await logoutDeviceSpecific();
  },
  async onResume() {
    if (currentUser && currentUser.expired) {
      await refreshAccessToken();
    }
  }
};

async function logoutDeviceSpecific() {
  clearInterval(refreshTokenInterval);
  try {
    if (Platform.is.mobile) {
      userStore.clearCurrentUser();
      teamStore.clearCurrentTeam();
      if (AppService.isAndroid()) {
        await userManager?.signoutSilent();
      } else {
        await userManager?.signoutRedirect({post_logout_redirect_uri: 'myapp://logout'})
      }
    } else {
      const cookiesValue = localStorage.getItem('cookies');

      localStorage.clear();

      if (cookiesValue !== null) {
        localStorage.setItem('cookies', cookiesValue);
      }

      await userManager?.signoutRedirect();
    }
  } catch (error) {
    console.error('OIDC logout error:', error);
  }
}

function getUserManagerInstance() {
  if (!userManager) {
    if (AppService.isMobile()) {
      userManager = new UserManager({
        authority: 'keycloakurl',
        client_id: 'client',
        redirect_uri: 'myapp://login',
        post_logout_redirect_uri: 'myapp:/' + '/logout',
        response_type: 'code',
        scope: 'openid profile email offline_access',
        filterProtocolClaims: true,
        loadUserInfo: true,
        automaticSilentRenew: false,
        userStore: new WebStorageStateStore({ store: new MobileStorage() })
      });
    } else {
      userManager = new UserManager({
        authority: 'keycloakurl',
        client_id: 'client',
        redirect_uri: window.location.origin + '/login',
        post_logout_redirect_uri: window.location.origin,
        response_type: 'code',
        scope: 'openid profile email offline_access',
        filterProtocolClaims: true,
        loadUserInfo: true,
        automaticSilentRenew: false,
        userStore: new WebStorageStateStore({ store: window.localStorage })
      });
    }
  }
  return userManager;
}

function registerTokenInterceptor() {
  api.interceptors.request.use(async (config) => {

    const status = await Network.getStatus();
    console.log('Network status:', status.connected);
    if (!status.connected) {
      return Promise.reject(new axios.Cancel('No internet connection'));
    }

    console.log('Request interceptor:', config);

    let user = await userManager?.getUser();
    if (user && !user.expired) {
      config.headers.Authorization = `Bearer ${user.access_token}`;
    }
    if (user?.expired) {
      user = await userManager?.signinSilent();
      config.headers.Authorization = `Bearer ${user?.access_token}`;
    }
    return config;
  });
}

function setTokenInterval() {
  refreshTokenInterval = setInterval(refreshAccessToken, 1000000);
}

async function refreshAccessToken() {
  if (currentUser) {
    try {
      currentUser = await userManager?.signinSilent();
      if (currentUser && !currentUser.expired) {
        console.log('Access token refreshed');
        lastLoginTime = String(Date.now());
      } else {
        console.log('Access token refresh failed');
      }
    } catch (error) {
      console.error('Error refreshing access token:', error);
      if (error instanceof Error && error.message === 'Stale token') {
        console.log('Stale token, signing out');
        await logoutDeviceSpecific();
      }

      // 28 days
      if (lastLoginTime && Date.now() - Number(lastLoginTime) > 2419200000) {
        console.log('Token expired, signing out');
        await logoutDeviceSpecific();
      }
    }

    if (lastLoginTime && Date.now() - Number(lastLoginTime) > 2419200000) {
      console.log('Token expired, signing out');
      await logoutDeviceSpecific();
    }
  }
}

to

export default {
// init oidc client
async function initAuthJsOIDCClient() {
    await initOidc(<OIDCAuthSettings>{
      authorityUrl: 'myAuthority,
      clientId: 'myClient',
      mobileScheme: 'myApp',
    })
  }
}

Questions

  1. Quasar uses localhost#/ for ios and localhost/#/ for android. How can i set it?
    currentUser = await userManager.signinRedirectCallback(window.location.href.replace('#/', '/'));

  2. I get the usermanager if existing:
    userManager = getUserManagerInstance();
    How do i achieve this in the lib or does it work automatically?

  3. How do i register the token interceptor?

authentication.ts - boot

Questions

  1. Do i still need it? No right, cause i can use the NRouterGuard for it.

auth.store.ts

Questions

  1. I can use the same store right? If we take a look to get the user
actions: {
    async initOidcClient(isRedirect?: boolean) {
      try {
        this.user = await AuthService.initOidcClient(isRedirect);
      } catch (error) {
        console.log('Failed to initialize OIDC client:', error);
      }
    },

How can i do get the user?

routerGuard.ts - boot

export default boot(({ router }) => {

  async function initializeOidcAfterRouting() {
    console.log('OIDC client initialized');
    try {
      await authStore.initOidcClient(true);
    } catch (error) {
      console.error('Failed to initialize OIDC client:', error);
    }
  }


  const authStore = useAuthStore();

  router.beforeEach(async (to, from, next) => {

    if (authStore.isOfflineAuthenticated && to.fullPath.includes('/login')) {
      next('/');
    }

    if (to.matched.some(record => record.meta?.requiresAuth)) {
      if (authStore.isOfflineAuthenticated) {
        next();
      } else {
        next('/home');
      }
    } else {
      next();
    }
  });
});

Questions

  1. In the past i got problems, that my OIDC was not reachable. What happens if the oidc is not available? Does the page still load when I initialise the oidc in routerguard?
  2. Here i just need to use isAuthenticated right?
  3. Is this the right place to init the oidc?

Vue Lib

I am very grateful for your help! If I get it working and understand your library better, I will try to convert it into a Vuelib in a study project.

@Badisi
Copy link
Owner

Badisi commented Nov 21, 2024

@Excel1, thanks for the follow-up.

Hard for me to tell based only on those code snippets...
Would it be possible for you to set-up a simple project with quasar, capacitor, vue and my lib ?
We could then work on it together more easily.
Thanks


auth.service.ts

The idea of this library is to start before anything else (i.e. to start even before the bootstrap of your app).
It will act as a guard to prevent your app from loading in case the user is not logged-in.
And it will also avoid your app context (i.e. Angular, Vue, etc) to be loaded twice due to redirects in case of authentication.
So it should be the first thing to run (ex: in Angular it starts in the main.ts which is basically like the first script tag in your index.html). So I don't think an auth.service.ts is the right place in your case.

Quasar uses localhost#/ for ios and localhost/#/ for android. How can i set it?

Not sure you really needs to do it with my lib. But for info you can override anything that's oidc-client-ts related, with initOidc({ internal: X })

I get the usermanager if existing

You don't have to do anything at all. Just call initOidc() and the lib will managed everything for you.

How do i register the token interceptor?

It's actually only available in the Angular implementation. But I have plan to port it to the VanillaJS one. In your case you would have to keep your current Vue implementation. If you can provide a sample project as I said, I would be able to provide a Vue interceptor.

In the past i got problems, that my OIDC was not reachable. What happens if the oidc is not available? Does the page still load when I initialise the oidc in routerguard?

Like I said before, the lib should be initialized even before your app. So in case your IDP is not reachable, your app won't load at all and you can simply present to the user a fallback page with a login button so he can retry the authentication (I can also provide this scenario in the sample project).

Here i just need to use isAuthenticated right?

Depends on your needs

Is this the right place to init the oidc?

Already answered it :-)

@Excel1
Copy link
Author

Excel1 commented Nov 21, 2024

@Badisi

Hard for me to tell based only on those code snippets...
Would it be possible for you to set-up a simple project with quasar, capacitor, vue and my lib ?
We could then work on it together more easily.
Thanks

Yes ofc. https://github.com/Excel1/quasar-authjs

I have created a simple Quasar Project with a protected and unprotected page (HomePage). Additionally I added a RouterGuard and the auth.service.ts to abstract this, if it makes sense. I changed a few things and tried to map the login process according to suspicion.

I have tried to follow most of the instructions and orientated myself on the projects, but so far I have not been able to log in because initOidc does not seem to be a function. I hope you can recognise the approach and you can do something with it :) If you would like to explain it, so that I can understand it and possibly write documentation - that would be super helpful!

As already mentioned, the boot files are executed at the beginning (boot/routerGuard). If you need more information or the code is not enough for you, contact me and I will try to adapt the code

@Badisi
Copy link
Owner

Badisi commented Nov 22, 2024

initOidc does not seem to be a function

Was not an easy one, but this was due to the fact that quasar was not supporting esm yet.
Luckily they now have a release candidate that does:
quasarframework/quasar#12818
https://github.com/quasarframework/quasar/releases/tag/%40quasar%2Fapp-vite-v2.0.0-rc.1

@Badisi
Copy link
Owner

Badisi commented Nov 22, 2024

@Excel1, I've already managed to make a working version of your project with login and logout.
Only guard and injector remains.
Please add me as a contributor to your repo so that I can push my modifications directly to it.
Thanks

@Excel1
Copy link
Author

Excel1 commented Nov 22, 2024

@Badisi Thank you for the fast help! - I added you as a contributor and will update my main project to the release candidate and try to take over the implementation from the project. You are welcome to copy the entire quasar-authjs and put it into your demo folder if you like!

@Badisi
Copy link
Owner

Badisi commented Nov 22, 2024

Thanks, I have pushed a pre-version of my modifications.
It's still in progress and might not completely work as expected, so please wait for it to be stable.
I'm currently having a look at the guard and interceptor part ;-)

@Excel1
Copy link
Author

Excel1 commented Nov 22, 2024

@Badisi 🔝 - feel free to contact me, when you are ready :)

@Badisi
Copy link
Owner

Badisi commented Nov 23, 2024

@Excel1, only the interceptor remains now (will do it in the upcoming days).
In the meantime, do not hesitate to start playing with the project and make a review at what I did.
I'm no Vue expert (never used it before) so I do hope I respected the standards.

Will write a full detailed explanation of what I did, once it will be finished.
Thanks!

@Badisi
Copy link
Owner

Badisi commented Nov 26, 2024

@Excel1, everything is ready 🥳

I left a few examples in the init of the lib and the routes so you can see what optons are available to you.
I'll try to make another comment later this week to give you more details about what I did.
Enjoy :-)

PS: I didn't tried it on mobile because I don't know how I'm supposed to use Capacitor in Quasar. So if you manage to have Capacitor in the project I could also have a look to see if it works properly. Thanks

@Badisi Badisi self-assigned this Nov 26, 2024
@Badisi Badisi added the enhancement New feature or request label Nov 26, 2024
@Badisi Badisi changed the title Need help for implementing / migration in a vue / quasar application [FEAT] Support for vue / quasar applications Nov 26, 2024
@Badisi Badisi changed the title [FEAT] Support for vue / quasar applications [FEATURE] Support for vue / quasar applications Nov 26, 2024
@Excel1
Copy link
Author

Excel1 commented Nov 27, 2024

@Badisi Thank you very much. I try to implement it now in a production app. This will take a while (it will take a long while) cause it seems like i got many errors due the change to "@quasar/app-vite": "^2.0.0-rc.3" with causes many import problems...

e.g. import { useAuthStore } from 'stores/auth.store'; --> import { useAuthStore } from '../stores/auth.store'; (for each import)

I also got my first question:

Do i need "vite-plugin-static-copy": "^2.2.0" cause this causes dependency issues. with cue i18n and many more vite plugins.

//quasar config
...viteStaticCopy({
          targets: [
            {
              src: 'node_modules/@badisi/auth-js/oidc/assets/*',
              dest: 'oidc/callback/'
            }
          ]
        })

@Excel1
Copy link
Author

Excel1 commented Nov 28, 2024

Also i mentioned that you make some configuration changes like in

  • tsconfig.json
  • quasar.config.json ( require('quasar/wrappers') to from 'quasar/wrappers' and setting an alias.
  • package.json "postinstall"

Are these steps necessary?

@Badisi
Copy link
Owner

Badisi commented Nov 28, 2024

What I have done in the demo project

Temporary modifications

  • src/@auth-vue

    • this will become a separated package@badisi/auth-vue (that I will release later on)
    • until this package is actually released, I have already configured the demo project to recognize it, using:
      • paths in tsconfig.json (to make Typescript knows about @badisi/auth-vue during imports)
      • alias in quasar.config.js (to make Vite knows about @badisi/auth-vue during compilation)
    • => you shouldn't have to touch anything inside that folder
    • => once the package is released, you will just have to remove the configs in tsconfig and quasar.config (nothing more)
    • => the idea behind that is to already use imports like "import X from '@badisi/auth-vue'" instead of "import X from './src/@auth-vue" and have to rename all the imports when the package will be released
  • patch-package and ./patches folder

    • some modifications where required in the actual core of @badisi/auth-js
    • as I don't have time currently to make the modifications and release a new version, I have patched the library directly in the demo project
    • patch-package is a library that will apply modifications found in the ./patches folder directly inside the node_modules folder
    • for this to work patch-package is actually used during post install of dependencies (cf. package.json#scripts#postinstall)
    • => once @badisi/auth-vue is released, this script could be removed

Library usage

  • init

    • the library is initialized in src/boot/auth.ts which is the main entry point in a Quasar application
    • there, you will find an existing configuration that uses an Auth0 demo account (demo / Pa55w0rd!)
    • the part with extraQueryParams is purely specific to Auth0 so that the access_token is in JWT format (so not required) (it is used later on in the guard example so that a role could be read from the token - otherwise token is opaque)
  • guard

    • you will find some examples in src/router/routes.ts
    • guards can be simply activated (using true/false) or be more advanced using validators functions (ex: to check roles)
  • interceptor

    • my library will now uses its own fetch/xhr interceptors (no need to use any interceptor in Quasar/Vue)
    • examples can be found in src/boot/auth.ts (ex: automaticLoginOn401 and automaticInjectToken)
    • you can include/exclude url patterns (string/regexp) where you want the access_token to be injected or not

@Badisi
Copy link
Owner

Badisi commented Nov 28, 2024

If migrating to Quasar v2 is too much effort for you right now (even if it will be required one day), I've pushed a branch quasar-v1.x where it should be working with Quasar v1.

What was required:

  • remove quasar prepare in package.json and rollback tsconfig.json to use Quasar preset

    • => those were required by Quasar v2
  • use dynamic import in quasar.config.js to import vite-static-plugin-copy (as it is an esm package)

    • the use of this plugin is required to copy required assets from @badisi/auth-js into your dist folder
    • => what's actually needed is the copy of the assets, but another plugin or another method could be used
  • use aliases in quasar.config.js to make Quasar load the proper esm version of packages

    • @badisi/auth-js
    • oidc-client-ts
    • crypto-js -> this one also required some shims (that could be found in ./patches folder), because crypto-js does not have default exports

@Excel1
Copy link
Author

Excel1 commented Dec 1, 2024

Thank you for the work. I tried to implement it into my production project.

My changes:

// tsconfig.json
{
  "extends": "@quasar/app-vite/tsconfig-preset",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@badisi/auth-vue": ["./src/@auth-vue"]
    }
  }
}
// quasar.config.js

module.exports = configure(async function (/* ctx */) {
  const { viteStaticCopy } = await import("vite-plugin-static-copy");
  ...

      alias: {
        "@badisi/auth-vue": path.resolve(__dirname, "./src/@auth-vue"),
        "@badisi/auth-js": path.resolve(__dirname, "./node_modules/@badisi/auth-js/esm"),
        "oidc-client-ts": path.resolve(__dirname, "./node_modules/oidc-client-ts/dist/esm/oidc-client-ts.js"),
        "crypto-js/core.js": path.resolve(__dirname, "./patches/crypto-js-core.shim.js"),
        "crypto-js/enc-base64.js": path.resolve(__dirname, "./patches/crypto-js-enc-base64.shim.js"),
        "crypto-js/enc-utf8.js": path.resolve(__dirname, "./patches/crypto-js-enc-utf8.shim.js"),
        "crypto-js/sha256.js": path.resolve(__dirname, "./patches/crypto-js-sha256.shim.js"),
      },

i copied the @auth-vue folder into my /src.

Is this still here correct? I have now tried to add vite-plugin-static-copy and got dependency problems.

// my dev dependencies
  "devDependencies": {
    "@intlify/vite-plugin-vue-i18n": "^3.3.1",
    "@quasar/app-vite": "^1.9.3",
    "@types/node": "^12.20.21",
    "@types/vue-writer": "^1.2.0",
    "@typescript-eslint/eslint-plugin": "^5.10.0",
    "@typescript-eslint/parser": "^5.10.0",
    "autoprefixer": "^10.4.2",
    "eslint": "^8.10.0",
    "eslint-config-prettier": "^8.1.0",
    "eslint-plugin-vue": "^9.0.0",
    "prettier": "^2.5.1",
    "typescript": "^4.5.4"
  },
  
  // also tried the updated version
    "devDependencies": {
    "@intlify/vite-plugin-vue-i18n": "^3.4.0",
    "@quasar/app-vite": "^1.0.0",
    "@types/node": "^22.9.1",
    "@types/vue-writer": "^1.2.0",
    "@typescript-eslint/eslint-plugin": "^5.10.0",
    "@typescript-eslint/parser": "^5.10.0",
    "autoprefixer": "^10.4.2",
    "eslint": "^8.10.0",
    "eslint-config-prettier": "^8.1.0",
    "eslint-plugin-vue": "^9.0.0",
    "prettier": "^2.5.1",
    "typescript": "^5.4",
    "vite-plugin-static-copy": "^2.1.0"
  },
//quasar-auth-project dependencies
  "devDependencies": {
    "@quasar/app-vite": "^1.0.0",
    "@types/node": "^22.9.1",
    "autoprefixer": "^10.4.2",
    "typescript": "^5.4",
    "vite-plugin-static-copy": "^2.1.0"
  },
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR! 
npm ERR! While resolving: @intlify/[email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/vite
npm ERR!   peer vite@"^5.0.0" from [email protected]
npm ERR!   node_modules/vite-plugin-static-copy
npm ERR!     dev vite-plugin-static-copy@"^1.0.6" from the root project
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! peer vite@"^2.0.0" from @intlify/[email protected]
npm ERR! node_modules/@intlify/vite-plugin-vue-i18n
npm ERR!   dev @intlify/vite-plugin-vue-i18n@"^3.3.1" from the root project
npm ERR! 
npm ERR! Conflicting peer dependency: [email protected]
npm ERR! node_modules/vite
npm ERR!   peer vite@"^2.0.0" from @intlify/[email protected]
npm ERR!   node_modules/@intlify/vite-plugin-vue-i18n
npm ERR!     dev @intlify/vite-plugin-vue-i18n@"^3.3.1" from the root project
npm ERR! 
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.

My Questions:

  • Do i need Typescript 5?
  • For me "vite-plugin-static-copy": "^2.1.0" doesnt work with the quasar vite version. Did you install it by using "--legacy-peer-deps"?

@Excel1
Copy link
Author

Excel1 commented Dec 1, 2024

By installing with "--legacy-peer-deps" it was possible to make it work.

But eslint throw the next error:
"@typescript-eslint/ no-empty-interface"
for file @auth-vue/auth.ts line 9

By ignoring the folder "/src/@auth-vue"
i got the error: "Error: AuthService is not provided" by using it like this:

import {useAuthService} from '@badisi/auth-vue';
const authService = useAuthService();

const login = () => {
  authService.login()

}

@Badisi
Copy link
Owner

Badisi commented Dec 1, 2024

Never use --legacy-peer-deps, all it's gonna do is mess up your dependencies.

Regarding the error, all the info are in the npm log:

  • vite-plugin-static-copy requires vite@^5.0.0
  • whereas @intlify/vite-plugin-vue-i18n requires vite@^2.0.0
  • => thus creating a conflict

Looking at https://github.com/intlify/vite-plugin-vue-i18n you can easily see that this package is deprecated.
And they now advice to use: @intlify/unplugin-vue-i18n.
So just use this package instead and you should be good.

@Excel1
Copy link
Author

Excel1 commented Dec 2, 2024

I got very good news! Its working (but still i have to fine tuning).

One problem i found was:
Cookie “AUTH_SESSION_ID” has been rejected because a non-HTTPS cookie can’t be set as “secure”.
when testing it with a http backend (local - development).

With https it works fine.

I try out to check how to fine tune and will comment my questions (like always) :) Currently i still didnt check mobile but i hope i can finish up this week.

But thanks again for the support so far

@Excel1
Copy link
Author

Excel1 commented Dec 2, 2024

First Question

When automaticLoginOn401: false, and authGuardFallbackUrl: '/home' is set and i click reload. I got redirected to /home. Without authGuardFallbackUrl i got redirected to login and back. How can i set authGuardFallbackUrl: '/home' and dont redirect to auth provider on refresh?

@Excel1
Copy link
Author

Excel1 commented Dec 5, 2024

@Badisi

The new i18n version is unfortunately not compatible with Quasar V1. Since static copy requires a higher Vite version, which is only available via v2, I tried to upgrade the project. Unfortunately, there are serious bugs in v2, which was released yesterday, e.g. i18n is no longer recognised in the project and/or capacitor imports are no longer recognised. Therefore I have to wait until the bugs are fixed in Quasar which will take a few weeks. I will get back to you as soon as I know more!

@Excel1
Copy link
Author

Excel1 commented Dec 6, 2024

After some trials i got it working by using workarounds (recommended by the quasar developers).

Webpage

If i refresh the secured page i got logged off/ redirected as told.

Android

Yay! The capacitor browser opens - i see the log in page but didnt got redirected back to app. I stuck into the redirection process. Do i have to redirect on special url endings?

@Excel1
Copy link
Author

Excel1 commented Dec 18, 2024

@Badisi i also tried it for quasar version 1 (cause some bugs are still in v2) and it works. This is the current error on refreshing the page.

console:
[OIDCAuthManager] User's session cannot be retrieved: login_required [oidc-auth-manager.ts:171:27](http://localhost:9000/node_modules/projects/auth-js/oidc/oidc-auth-manager.ts)
    signinSilent oidc-auth-manager.ts:171

@Badisi
Copy link
Owner

Badisi commented Dec 22, 2024

@Excel1. sorry for the long wait.

What's your current status ? Do you still need help ?


Routing issue

  • authGuardFallbackUrl : string is the url the user should be redirected to if he's not authenticated.
  • Refreshing the page while the user is authenticated, should bring him back authenticated - if it is not the case I suspect there is something wrong with the sign-in silent (see the last point).
    You can enable more logs with logLevel: Log.DEBUG.

Android

On mobile (both iOS and Android) you have to specify the mobileScheme: string option.
And you also have to enable/configure that custom url scheme in your native mobile project.
This custom url will be used for the redirect to your app.

User's session cannot be retrieved: login_required

This isn't an error per se, more of a warning to tell you that the user's session couldn't be retrieved.

When using retrieveUserSession: true the library will try to authenticate back the user at startup (this is called sign-in silent), if he still has an opened session (meaning user won't have to login).

  • On mobile, tokens are saved in the device secure storage. So during initialization the library will simply retrieve the tokens from there.

  • On desktop, for security reason, tokens are not saved on disk, they are kept in memory. So in case the user closes the browser and re-opens it (or simply refresh the page), the library will silently try to retrieve the tokens from the IDP so that the user is authenticated back. For that the library will create an hidden iframe to contact the IDP and try to retrieve the user's tokens.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants