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

Questions for Vue3 - Pinia Store - Keycloak - Capacitor Example #1548

Open
Excel1 opened this issue Jun 20, 2024 · 7 comments
Open

Questions for Vue3 - Pinia Store - Keycloak - Capacitor Example #1548

Excel1 opened this issue Jun 20, 2024 · 7 comments
Labels
Capacitor Capacitor is an open source native runtime for building Web Native apps. Keycloak Identity question Further information is requested

Comments

@Excel1
Copy link

Excel1 commented Jun 20, 2024

Hello!
I try to connect Keycloak with my WebApplication. My Goal is to use oidc-client.ts for web and (capacitor) app and also to make this here available for inspiration (copy&paste) how to do it.

Still now i got several problems/questions
Web:
W1. SSO doesnt work: On reloading the page (F5) i get "OIDC initialization error: Error: No state in response" - How can i log in silently too or login with the saved token?
W2. Is the current code well implemented or there are some issues in general?

Capacitor
C1. By login (signInRedirect) i get the "OIDC login error: Error: Crypto.subtle is available only in secure contexts (HTTPS)." How can i avoid this?
C2. How can store the token (offline_access) to achieve that you dont need to regularly log in?

I already know, that in the current state the AppListener and redirect to/from mayap:// is missing. Would be awesome if we can create a full example.

AuthStore

import { User } from 'oidc-client-ts';
import AuthService from 'src/services/auth.service';
import { defineStore } from 'pinia';

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;
    },
    getEmail(): string | undefined {
      return this.user?.profile?.preferred_username;
    },
  },
  actions: {
    async initOidcClient() {
      try {
        this.user = await AuthService.initOidcClient();
      } catch (error) {
        this.user = null;
      }
    },
    async login(redirectUri?: string) {
      try {
        await AuthService.login(redirectUri);
      } catch (error) {
        console.error(error);
      }
    },
    async logout() {
      try {
        await AuthService.logout();
      } catch (error) {
        console.error(error);
      }
    }
  }
});

AuthService

import { api } from 'boot/axios';
import { UserManager, WebStorageStateStore, User } from 'oidc-client-ts';

let userManager: UserManager | null = null;
let refreshTokenInterval: string | number | NodeJS.Timeout | undefined;

export default {
  async initOidcClient() {
    userManager = getUserManagerInstance();
    try {
      console.log(userManager)

      const user = await userManager.signinRedirectCallback()
      console.log('OIDC user:', user);
      if (user && !user.expired) {
        setTokenInterval(user);
        registerTokenInterceptor();
        return user;
      }
      return null;
    } catch (error) {
      console.error('OIDC initialization error:', error);
      throw error;
    }
  },
  async login(redirectUri?: string) {
    try {
      await userManager?.signinRedirect({ redirect_uri: redirectUri });
    } catch (error) {
      console.error('OIDC login error:', error);
    }
  },
  async logout() {
    clearInterval(refreshTokenInterval);
    try {
      await userManager?.signoutRedirect();
    } catch (error) {
      console.error('OIDC logout error:', error);
    }
  }
};

function getUserManagerInstance() {
  if (!userManager) {
    userManager = new UserManager({
      authority: 'http://<ip>:8080/auth/realms/master',
      client_id: '<clientId>',
      redirect_uri: window.location.origin,
      post_logout_redirect_uri: window.location.origin,
      response_type: 'code',
      scope: 'openid profile email offline_access',
      filterProtocolClaims: true,
      loadUserInfo: true,
      userStore: new WebStorageStateStore({ store: window.localStorage }),
    });
  }
  return userManager;
}

function registerTokenInterceptor() {
  api.interceptors.request.use(async (config) => {
    const user = await userManager?.getUser();
    if (user && !user.expired) {
      config.headers.Authorization = `Bearer ${user.access_token}`;
    }
    return config;
  });
}

function setTokenInterval(user: User) {
  if (user && !user.expired) {
    refreshTokenInterval = setInterval(async () => {
      try {
        const refreshedUser = await userManager?.signinSilent();
        if (refreshedUser) {
          console.log('Token refreshed');
        }
      } catch (error) {
        console.log('Failed to refresh token: ', error);
      }
    }, 60000);
  }
}
@Badisi
Copy link
Contributor

Badisi commented Jun 20, 2024

FYI, demo for integration with Capacitor was already made here : #537.
(more specifically, this comment)

@Excel1
Copy link
Author

Excel1 commented Jun 20, 2024

@Badisi thank you for the fast replay. But i dont like its again another wrapper tbh. But it seems that there is currently no solution to achive that without a wrapper right? i appreciate your work - does your solution work with exactly my prerequisites? And how do i implement it?

@Excel1
Copy link
Author

Excel1 commented Jun 20, 2024

"Crypto.subtle is available only in secure contexts (HTTPS)." This error makes it impossible to test oidc in by using local ips in a private network (without setting up certificates and more).

@Badisi
Copy link
Contributor

Badisi commented Jun 21, 2024

I was not intended to make you use my wrapper, but rather direct you towards a concrete example.
So that it can save you time to develop your Capacitor side 😉


But if you do choose to use my wrapper, please let me know and I will be happy to help

@Excel1
Copy link
Author

Excel1 commented Jun 21, 2024

@Badisi

I try two different approaches

  1. Continue with only oidc-client.ts but the usermanager didnt recieve the token from App.Listener(url) everything works okay but some different error happens

  2. Starting with your lib - i installed it already and try to see how to implement it in my code. But without documentation its very hard to implement it in my vue3 application :)

@Excel1
Copy link
Author

Excel1 commented Jun 21, 2024

I already created a hybrid app by using angular. I used this lib https://github.com/manfredsteyer/angular-oauth2-oidc and it works perfectly - but it seems this lib and vue seems to work different...

@pamapa pamapa added question Further information is requested Keycloak Identity Capacitor Capacitor is an open source native runtime for building Web Native apps. labels Jun 21, 2024
@Excel1
Copy link
Author

Excel1 commented Jun 21, 2024

import { boot } from 'quasar/wrappers';
import { useAuthStore } from 'stores/auth.store';
import { App, URLOpenListenerEvent } from '@capacitor/app';

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

  async function initializeOidcAfterRouting() {
    console.log("OIDC client initialized")
    try {
      await authStore.initOidcClient(true); 
    } catch (error) {
      console.error('Failed to initialize OIDC client:', error);
      // Handle error (e.g., redirect to an error page)
    }
  }

  App.addListener('appUrlOpen', function (event: URLOpenListenerEvent) {
    // Example url: https://beerswift.app/tabs/tabs2
    // slug = /tabs/tabs2
    const slug = event.url.split('myapp://login').pop();

    // We only push to the route if there is a slug present
    if (slug) {
      router.push(slug).then(() => {
        initializeOidcAfterRouting();
      });
    }
  });

  const authStore = useAuthStore();

  function sleep(ms: number) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }

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

    // wait till oidc is initiated
    while (authStore.getUser === undefined) {
      await sleep(100)
    }

    // clear url from keycloak state
    if (authStore.isAuthenticated && to.fullPath.includes('/login')) {
      next('/')
    }

    console.log(authStore.isAuthenticated)

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

I think the only thing i need from now is, to get the token from keycloak to the usermanager. Everything works but on redirect the usermanager cant retrieve the token. This App uses history mode but however the url from android deeplink is converted to localhodst:5200/#/login?state...

It seems the usermanager cant work with the hash and i dont know how to solve it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Capacitor Capacitor is an open source native runtime for building Web Native apps. Keycloak Identity question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants