2022 W12


It’s been nearly a whole month since the last edition of my weeknotes were published. This New Year’s Resolution clearly isn’t going so well… 🤷‍♂️ I’m only human. I’ll persist!

My list of “things I’d like to write about on the blog” is growing more rapidly every week and the articles aren’t getting completed. This weeknote will highlight an outline of some of the top ones so that I can at least get something out in the open.

Sociocracy Leadership Training

I’m very excited that my place on this season’s Sociocracy Leadership Training has been confirmed. It’s a program run by Sociocracy For All and headed up by the wonderful Jerry Koch-Gonzalez; whom I had a chance to catch up with briefly this week.

The more I think about sociocracy and the practical applications of it (especially relating these to my work at Equal Care Coop), the more I’m excited about getting stuck into this course.

There’ll be more updates as I make my through it over the next few months.

next-auth and refreshing tokens

Much to my consternation, next-auth doesn’t have an easy way to configure refreshing tokens without writing a bunch of boilerplate yourself. On top of this, I found the callback methods that allow for inserting your own logic were named confusingly. Here’s a summary of what I’ve learnt when trying to implement OpenIDConnect (OIDC) authentication flows with Keycloak and next-auth.

In this scenario, I want to redirect users to Keycloak to perform all of the authentication, grab the access token, and then use that with other services from within my front-end application.

Callbacks are control

Understanding the callbacks provided by next-auth is key to leveraging its flexibility. They’re not named very well and the docs consist of “reference material” and not “guides”. In other words, they’re good to refer to once you get the gist of it but terrible for helping you understand the concepts in the first place.

jwt

This callback controls what will be stored in the cookie that is saved in the user’s browser (and consequentially, what we’ll be able to retrieve later inside our application).

For our use case, this means that we need, at a minimum, the access token, the access token expiry and the refresh token. We probably want things like the user ID etc. but we’ll skip past that as it will be different for everyone.

Here’s what our callback looks like

async jwt({ token, account, user }) {
	// Initial sign in
	if (account && user) {
		return {
			...token,
			accessToken: account.access_token,
			exp: Date.now() + account.expires_at! * 1000,
			refreshToken: account.refresh_token,
		}
	}
	// Return previous token if the access token has not expired yet
	if (Date.now() < token.exp) {
		return token
	}

	// Access token has expired, try to update it
	try {
		const refreshedTokenSet = await refreshAccessToken(token)
		return {
			...token,
			accessToken: refreshedTokenSet.access_token,
			exp: refreshedTokenSet.expires_at!,
			refreshToken: refreshedTokenSet.refresh_token,
		}
	} catch (error) {
		if (typeof error === 'string' && error.startsWith("RefreshAccessTokenError")) {
			return {
				...token,
				error,
			}
		}
		throw error
	}
}

// ...

async function refreshAccessToken(token: JWT): Promise<TokenSet> {
	if (!token.refreshToken) throw "RefreshAccessTokenError::NoToken"

	const client = await openIdClient() // from openid-client, configured for keycloak
	try {
		return client.refresh(token.refreshToken)
	} catch (error) {
		console.log(error)
		throw "RefreshAccessTokenError"
	}
}

This callback will be called both server-side and client-side, whenever the session is accessed. This means refresh logic here is re-used across server and client environments, which is the key to us being able to refresh tokens that can be used for both data fetching on the server and calls from the client.

session

This method configures what is available to the getSession and useSession methods. The docs state that it returns a subset of the token returned from the jwt callback, but in practice I found that distinction not to hold.

My session callback contains everything in the token as well as a couple of computed properties.

redirect

Keycloak redirects users back to the front-end after they complete an action (sign [in|out] etc.).

To log out properly, there are two steps with OIDC:

  1. Remove the cookie that stores the authentication tokens in your application
  2. Send the user to Keycloak to end sessions in the OIDC authentication server
  3. Redirect them back to our application on completion

In next-auth, we get step 1 and 3 for free, but we need to implement step 2 manually. To achieve this in next-auth, we define a special “fake” logout route that can be intercepted and pointed towards Keycloak.

The callback code looks like this:

async redirect({ url, baseUrl }) {
	if (url.startsWith(baseUrl)) return url;
	if (url.match(/^/logout$/)) {
		const client = await openIdClient() // using openid-client, configured to point at keycloak
		const redirectUrl = process.env.NEXTAUTH_URL || 'http://localhost:3001';
		const endSessionUrl = client.endSessionUrl({ post_logout_redirect_uri: redirectUrl })
		return endSessionUrl;
	}

	// Allows relative callback URLs
	if (url.startsWith('/')) return new URL(url, baseUrl).toString();
	return baseUrl;
},

Which we can activate by redirecting people to this special “fake” logout route when signing them out:

() => signOut({ callbackUrl: '/logout' })

Big props to this (rather hidden) discussion for prompting with this solution and helping me get my head around the callbacks.

Typescript Bug

I found a bug in Typescript and I got a strange sense of satisfaction from it. Maybe it’s nostalgia from my days as an automation tester?

remix-auth-oidc

I released my first package on npm!

It’s a small wrapper for remix-auth that makes handling OIDC auth flows a bit easier.

Gotta start somewhere!

© neverstew 2022
GitHub logo