authts / oidc-client-ts

OpenID Connect (OIDC) and OAuth2 protocol support for browser-based JavaScript applications
https://authts.github.io/oidc-client-ts/
Apache License 2.0
1.35k stars 202 forks source link

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

Open Excel1 opened 3 months ago

Excel1 commented 3 months ago

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 commented 3 months ago

FYI, demo for integration with Capacitor was already made here : https://github.com/authts/oidc-client-ts/issues/537. (more specifically, this comment)

Excel1 commented 3 months ago

@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 commented 3 months ago

"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 commented 3 months ago

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 commented 3 months ago

@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 commented 3 months ago

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...

Excel1 commented 3 months ago
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