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?
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
localhost#/, while Android useslocalhost/#/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
authentication.ts - boot
auth.store.ts
routerGuard.ts - boot
Questions