I’m somewhat new to frontend web development, so some things that are obvious to everyone that comes from that world aren’t obvious to me, with primarily mobile and desktop development experience. This is probably one of those instances, so please bear with me.
I now have a logged in user, but when I refresh the page, the actor, which is stored in the React context so it works across my app, gets blown and the user must go through the login process again. It seems the simplest way to persist state through browser refreshes is by using localStorage, but that requires me to be able to stringify everything I want to save.
Is there a way to stringify a logged-in authClient/agent/actor so that when it’s pulled back out of localStorage, the user is already logged in and doesn’t need to go through that flow again?
Many frontend frameworks have stores with libraries built specifically for persisting data to local storage and rehydrating the store with that data from local storage. Here’s a library that provides the Redux equivalent of this tooling.
Likewise. When a page is rendered - when the app starts - you can check the auth state and save the information - the identity - either in a global variable, a global store if you have one, or in the context, if you have no store.
e.g. in one of my app in Svelte, either on the app scale or in the global layout, I sync the auth status by calling of function of my store (source).
in the global store (source) I initialize agent-js which will - per default - read the information potentially saved in local storage (< v0.12.0) or indexeddb (>= v0.13.0) to initialize the identity - to create a session if these information are present and still valid. I then save the outcome - the identity - in my store.
Using above identity, I can then detect if user is already signed in or not. I used a derived store but the idea is that if identity exists in my store, it means that the user is signed in (source)
Oh wow, maybe I’ve been doing this all wrong. I’ve been storing the authClient and calling getIdentity() whenever I need to use the identity, like to show the principal, or to create the Actor, which I only do once and then cache in my React Context to use throughout the app (I don’t use Redux at the moment since App Context suffices).
I thought I’d need to keep the logged-in Actor around between page refreshes, or at least keep the authClient around. From what you’re saying, it sounds like all I need to keep is the identity, and from that I can easily re-create the Actor when I need:
const actor = createActor(canisterId as string, {
agentOptions: {
identity: myStoredIdentity,
},
});
So does this mean that Identity is serializable? I know that the Identity can be represented by your Principal, which can be displayed as a String, but certainly that string is not all I need to store. Shouldn’t I need all the cookie-like stuff that proves I’m logged in?
Your identity can be serialized to JSON - DelegationChain.toJSON. However, serializing it to and from IndexedDb is already handled for you automatically.
Goes to show you how little web dev experience I have; this is the first I’ve heard of IndexedDb. Seems like that’s exactly what I’m looking for: a way to store non-serializable objects in [something like] localStorage.
I assume that by “already handled”, you mean that, assuming I want to avoid persistence libraries, I can use the standard methods to interact with IndexedDb myself (like I would otherwise interact with localStorage), and just pass it an Identity object, not that there’s an easier way to just say “I want this object persisted across refreshes in IndexedDb”?
If I understand correctly, my workflow will be:
Check if Identity is in IndexedDb
If so, AuthClient.create(identity)
Check if authenticated
If so, create Actor using identity
If not, proceed as though identity wasn’t in IndexedDb
If not:
AuthClient.create()
authClient.login(…)
Store authClient.getIdentity() in IndexedDb
create Actor using identity
In writing this post, I noticed that there’s a “storage” parameter for AuthClient.create that defaults to localStorage. If AuthClient can be persisted in localStorage, maybe my first step above can be simplified to not even require the identity, and to just use whatever was persisted.
Yes, i guess doing this on a page initialization would also work
async function checkAuthentication() {
const client = authClient.create();
client.isAuthenticated();
}
The reason why i used the getIdentity in the earlier post is because React.Context isn’t available on regular ts files. With this method i am able to pass it along to the actor like so;
Ok, I think I see what’s going on here. AuthClient.create() pulls from localStorage (or wherever) and sometimes gets back an already-logged-in authClient that will resolve true for isAuthenticated().
Testing this out, when I try to use an Actor initialized with this AuthClient’s identity, I get back
Failed to authenticate request
Invalid signature: Invalid basic signature: Ed25519 signature could not be verified: public key
So I must still be doing something wrong. I’m still playing, but at least now I have a better idea of how this should work. Thank you for all your help! If you have any insight on this error, or if I’m way off base, let me know.
Are you testing with a local canister but with the online internet identity url? That won’t work. You’ll need to have a locally running Internet Identity canister for the login flow to work.
I was able to resolve the error and everything is working as expected now, with login state automatically persisted across refreshes without me needing to do anything special except follow Kyle’s flow, not my needlessly overcomplicated one.
My failure to get this to work originally was because I didn’t understand that AuthClient.create() is not idempotent. I thought it always returned a new “empty” (non-logged-in) AuthClient. Now I know that it fetches a logged in identity from (by default) local storage if there is one (I didn’t even know that it ever put one there in the first place). Pretty slick, if somewhat magical. I guess I missed that part of the explanation (and the code that made it work) when I was following @kpeacock’s IC Avatar videos when I originally set up my auth flow. Is there documentation that clearly explains this flow, including the magic?
Btw, the resolution to the “failed to authenticate” error above was to clear the cache for the page before going through the flow from scratch.