Repo
All the code in this article is available open-source in our GitHub repo.
Introduction
This is an example of using next.js, nextauth.js and typescript for both anonymous sessions and authenticated sessions. It’s not a primer on how to use Next.js or Nextauth.js, but the focus instead is how to resolve a specific problem.
The basis of this demo is a simple OAuth login using Github as a provider. It’s from an app that allows users to update notes stored in GitHub. Users can’t do that without a GitHub user. However, I wanted to enable users to test the app before logging in. To do that we need to manage information before having an authenticate user account.
To achieve that anonymous user experience we need a session, as the user hasn’t logged in yet. This demo creates an anonymous session using a second provider
. It’s not polished. It simply shows the session information, specifically how:
- It starts with an empty state (no data)
- It’s replaced by an anonymous session automatically, typically within a few seconds
- When the user clicks
Sign in
, they’re redirected to GitHub - After they’re redirected back, selected GitHub user details are visible in the session
- When the user clicks
Sign out
, the session is wiped (no data). - Again, within a few seconds, a new anonymous session is created and displayed.
Getting started
- First install the repo
npm install npm run gen-secret
- Set up
.env.local
using placeholders from.env.local.template
- Include the
NEXTAUTH_SECRET
generated above - Transpose credentials from your (already created) GitHub App
- Include the
- Run locally
npm run dev
- Open a browser to http://localhost:3000
- Click
Sign in
button- View logs to see
GitHub signIn
event
- View logs to see
- Click
Sign out
button- View logs to see
GitHub signOut
andAnonymous signIn
events
- View logs to see
Highlights
The /editor
route is wrapped in a NextAuthProvider:
<NextAuthProvider>
<section>
<nav>
<Account/>
</nav>
{children}
</section>
</NextAuthProvider>
That auth provider is a session provider, but contains an AnonymousSessionProvider
that kicks in when there’s no session.
<SessionProvider>
<AnonymousSessionProvider>
{children}
</AnonymousSessionProvider>
</SessionProvider>
In AnonymousSessionProvider
one hook pulls the session information, then a second does an anonymous ‘sign-in’ if there’s no session.
const {data: session, status} = useSession();
useEffect(() => {
if (status === "unauthenticated") {
// login as anonymous
signIn("credentials")
.then((data) => {});
}
}, [status]);
In the back-end, the /api/auth/[...nextauth]/route
has two providers:
export const authOptions: AuthOptions = {
providers: [
CredentialsProvider({
name: "anonymous",
credentials: {},
async authorize(credentials, req) {
return createAnonymousUser();
},
}),
GithubProvider({
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
}),
],
callbacks: {
async jwt({token, account, profile}: {token: JWT, account: Account | null, profile?: Profile}): Promise<JWT> {
if (account && account?.expires_at && account?.type === 'oauth') {
// at sign-in, persist in the JWT the GitHub account details to enable brokered requests in the future
token.access_token = account.access_token;
token.expires_at = account.expires_at;
token.refresh_token = account.refresh_token;
token.refresh_token_expires_in = account.refresh_token_expires_in;
token.provider = 'github';
}
if (!token.provider) token.provider = 'anonymous';
return token;
},
async session({session, token, user}: {session: Session, token: JWT, user: AdapterUser}): Promise<Session> {
// don't make the token (JWT) contents available to the client session (JWT), but flag that they're server-side
if (token.provider) {
session.token_provider = token.provider;
}
return session;
},
},
events: {
async signIn({user, account, profile}: {user: User, account: Account | null, profile?: Profile}): Promise<void> {
debug(`signIn of ${user.name} from ${user?.provider || account?.provider}`);
},
async signOut({session, token}: {session: Session, token: JWT}): Promise<void> {
debug(`signOut of ${token.name} from ${token.provider}`);
},
},
session: {
// use default, an encrypted JWT (JWE) store in the session cookie
strategy: "jwt" as SessionStrategy,
},
}
const handler = NextAuth(authOptions);
and a helper function for creating a nice anonymous user:
const createAnonymousUser = (): User => {
// generate a random name and email for this anonymous user
const customConfig: Config = {
dictionaries: [adjectives, colors, animals],
separator: '-',
length: 3,
style: 'capital'
};
// handle is simple-red-aardvark
const unique_handle: string = uniqueNamesGenerator(customConfig).replaceAll(' ','');
// real name is Red Aardvark
const unique_realname: string = unique_handle.split('-').slice(1).join(' ');
const unique_uuid: string = randomUUID();
return {
id: unique_uuid,
email: `${unique_handle.toLowerCase()}@example.com`,
name: unique_realname,
image: "",
provider: "anonymous"
};
};
This took a while to puzzle out from various blog posts and GitHub issues (1, 2). I hope that it helps anyone trying to do the same thing. Please start with the GitHub repo, because the highlights shown in this blog post are illustrative only and not comprehensive, whereas the repo is complete and works at the time of writing.
Leave a comment