Open Excel1 opened 3 days ago
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:
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).
developers are often not security experts, so the lib takes care of doing what's best in current security recommendations
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
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):
Regarding your questions:
- 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
- Where do I have to start with the migration and what do I have to consider?
Have a look at: https://github.com/Badisi/auth-js/issues/30#issuecomment-1436074818
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
- 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
@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?
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',
})
}
}
Quasar uses localhost#/ for ios and localhost/#/ for android. How can i set it?
currentUser = await userManager.signinRedirectCallback(window.location.href.replace('#/', '/'));
I get the usermanager if existing: userManager = getUserManagerInstance(); How do i achieve this in the lib or does it work automatically?
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?
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();
}
});
});
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.
@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 :-)
@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
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: https://github.com/quasarframework/quasar/issues/12818 https://github.com/quasarframework/quasar/releases/tag/%40quasar%2Fapp-vite-v2.0.0-rc.1
@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
@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!
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 a the guard and injector part ;-)
@Badisi 🔝 - feel free to contact me, when you are ready :)
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