Skip to content

Instantly share code, notes, and snippets.

@degitgitagitya
Last active April 28, 2025 06:34
Show Gist options
  • Save degitgitagitya/db5c4385fc549f317eac64d8e5702f74 to your computer and use it in GitHub Desktop.
Save degitgitagitya/db5c4385fc549f317eac64d8e5702f74 to your computer and use it in GitHub Desktop.
Next JS + Next Auth + Keycloak + AutoRefreshToken
# KEYCLOAK BASE URL
KEYCLOAK_BASE_URL=
# KEYCLOAK CLIENT SECRET
KEYCLOAK_CLIENT_SECRET=
# KEYCLOAK CLIENT ID
KEYCLOAK_CLIENT_ID=
# BASE URL FOR NEXT AUTH
NEXTAUTH_URL=
# JWT SECRET KEY
JWT_SECRET=
# NEXT AUTH SECRET KEY
SECRET=
# JWT SIGNING PRIVATE KEY
JWT_SIGNING_PRIVATE_KEY=
import NextAuth from 'next-auth';
import KeycloakProvider from 'next-auth/providers/keycloak'
import type { JWT } from 'next-auth/jwt';
/**
* Takes a token, and returns a new token with updated
* `accessToken` and `accessTokenExpires`. If an error occurs,
* returns the old token and an error property
*/
/**
* @param {JWT} token
*/
const refreshAccessToken = async (token: JWT) => {
try {
if (Date.now() > token.refreshTokenExpired) throw Error;
const details = {
client_id: process.env.KEYCLOAK_CLIENT_ID,
client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
grant_type: ['refresh_token'],
refresh_token: token.refreshToken,
};
const formBody: string[] = [];
Object.entries(details).forEach(([key, value]: [string, any]) => {
const encodedKey = encodeURIComponent(key);
const encodedValue = encodeURIComponent(value);
formBody.push(encodedKey + '=' + encodedValue);
});
const formData = formBody.join('&');
const url = `${process.env.KEYCLOAK_BASE_URL}/token`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
body: formData,
});
const refreshedTokens = await response.json();
if (!response.ok) throw refreshedTokens;
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpired: Date.now() + (refreshedTokens.expires_in - 15) * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
refreshTokenExpired:
Date.now() + (refreshedTokens.refresh_expires_in - 15) * 1000,
};
} catch (error) {
return {
...token,
error: 'RefreshAccessTokenError',
};
}
};
// If you have the latest version of next-auth
// Please use this next auth provider instead of my custom provider https://next-auth.js.org/providers/keycloak
// const keycloakProvider = KeycloakProvider({
// clientId: process.env.KEYCLOAK_CLIENT_ID,
// clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
// issuer: process.env.KEYCLOAK_ISSUER,
// authorization: {
// params: {
// grant_type: 'authorization_code',
// scope:
// 'openid tts-saas-user-attribute email speech-api profile console-prosa payment-service',
// response_type: 'code'
// }
// },
// httpOptions: {
// timeout: 30000
// }
// })
export default NextAuth({
// providers: [keycloakProvider],
providers: [
{
id: 'keycloak',
name: 'Keycloak',
type: 'oauth',
version: '2.0', // Double check your keycloak version
params: { grant_type: 'authorization_code' },
scope: 'openid email profile console-prosa basic-user-attribute',
accessTokenUrl: `${process.env.KEYCLOAK_BASE_URL}/token`,
requestTokenUrl: `${process.env.KEYCLOAK_BASE_URL}/auth`,
authorizationUrl: `${process.env.KEYCLOAK_BASE_URL}/auth`,
clientId: process.env.KEYCLOAK_CLIENT_ID,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
profileUrl: `${process.env.KEYCLOAK_BASE_URL}/userinfo`,
profile: (profile) => {
return {
...profile,
id: profile.sub,
};
},
authorizationParams: {
response_type: 'code',
},
},
],
session: {
jwt: true,
},
jwt: {
secret: process.env.JWT_SECRET,
signingKey: process.env.JWT_SIGNING_PRIVATE_KEY,
},
secret: process.env.SECRET,
callbacks: {
/**
* @param {object} user User object
* @param {object} account Provider account
* @param {object} profile Provider profile
* @return {boolean|string} Return `true` to allow sign in
* Return `false` to deny access
* Return `string` to redirect to (eg.: "/unauthorized")
*/
async signIn(user, account) {
if (account && user) {
return true;
} else {
// TODO : Add unauthorized page
return '/unauthorized';
}
},
/**
* @param {string} url URL provided as callback URL by the client
* @param {string} baseUrl Default base URL of site (can be used as fallback)
* @return {string} URL the client will be redirect to
*/
async redirect(url, baseUrl) {
return url.startsWith(baseUrl) ? url : baseUrl;
},
/**
* @param {object} session Session object
* @param {object} token User object (if using database sessions)
* JSON Web Token (if not using database sessions)
* @return {object} Session that will be returned to the client
*/
async session(session, token: JWT) {
if (token) {
session.user = token.user;
session.error = token.error;
session.accessToken = token.accessToken;
}
return session;
},
/**
* @param {object} token Decrypted JSON Web Token
* @param {object} user User object (only available on sign in)
* @param {object} account Provider account (only available on sign in)
* @param {object} profile Provider profile (only available on sign in)
* @param {boolean} isNewUser True if new user (only available on sign in)
* @return {object} JSON Web Token that will be saved
*/
async jwt(token, user, account) {
// Initial sign in
if (account && user) {
// Add access_token, refresh_token and expirations to the token right after signin
token.accessToken = account.accessToken;
token.refreshToken = account.refreshToken;
token.accessTokenExpired =
Date.now() + (account.expires_in - 15) * 1000;
token.refreshTokenExpired =
Date.now() + (account.refresh_expires_in - 15) * 1000;
token.user = user;
return token;
}
// Return previous token if the access token has not expired yet
if (Date.now() < token.accessTokenExpired) return token;
// Access token has expired, try to update it
return refreshAccessToken(token);
},
},
});
// Client example
import { signIn, useSession } from "next-auth/client";
import { useEffect } from "react";
const HomePage() {
const [session] = useSession();
useEffect(() => {
if (session?.error === "RefreshAccessTokenError") {
signIn('keycloak', {
callbackUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/application`,
}); // Force sign in to hopefully resolve error
}
}, [session]);
return (...)
}
import type { User } from 'next-auth';
declare module 'next-auth' {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context
*/
interface Session {
user: {
sub: string;
email_verified: boolean;
name: string;
preferred_username: string;
given_name: string;
family_name: string;
email: string;
id: string;
org_name?: string;
telephone?: string;
};
error: string;
}
/**
* The shape of the user object returned in the OAuth providers' `profile` callback,
* or the second parameter of the `session` callback, when using a database.
*/
interface User {
sub: string;
email_verified: boolean;
name: string;
telephone: string;
preferred_username: string;
org_name: string;
given_name: string;
family_name: string;
email: string;
id: string;
}
/**
* Usually contains information about the provider being used
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
*/
interface Account {
provider: string;
type: string;
id: string;
accessToken: string;
accessTokenExpires?: any;
refreshToken: string;
idToken: string;
access_token: string;
expires_in: number;
refresh_expires_in: number;
refresh_token: string;
token_type: string;
id_token: string;
'not-before-policy': number;
session_state: string;
scope: string;
}
/** The OAuth profile returned from your provider */
interface Profile {
sub: string;
email_verified: boolean;
name: string;
telephone: string;
preferred_username: string;
org_name: string;
given_name: string;
family_name: string;
email: string;
}
}
declare module 'next-auth/jwt' {
/** Returned by the `jwt` callback and `getToken`, when using JWT sessions */
interface JWT {
name: string;
email: string;
sub: string;
name: string;
email: string;
sub: string;
accessToken: string;
refreshToken: string;
accessTokenExpired: number;
refreshTokenExpired: number;
user: User;
error: string;
}
}
@snehapltt
Copy link

snehapltt commented Mar 17, 2025

no , used next auth and keycloak provider mechanism for login already. in multiple apps, but in that Single Sign-On not working

@degitgitagitya
Copy link
Author

no , used next auth and keycloak provider mechanism for login already. in multiple apps, but in that Single Sign-On not working

oh, i see. personally, i'm never do that, each app need to invoke the auth flow independently (user click the sign in button)

but in theory you can do shared state / session across your apps, assuming all of the apps using the same next auth for handling auth state, you need to put the next auth cookie in top level domain, and then, the sub domain pick up that state. you can take a look at this discussion for further info nextauthjs/next-auth#405

@snehapltt
Copy link

sure

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment