Skip to content

Instantly share code, notes, and snippets.

@degitgitagitya
Last active June 12, 2024 09:18
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;
}
}
@Shaju06
Copy link

Shaju06 commented Mar 21, 2024

On client next auth session is being stored in cookies will it still validate if I've revoke refresh token?

@degitgitagitya
Copy link
Author

degitgitagitya commented Mar 21, 2024

On client next auth session is being stored in cookies will it still validate if I've revoke refresh token?

the token on the client cookie that you revoked will be invalid useless (but still a valid token), meaning that token can't be used for authorization

@Varinder06
Copy link

I tried with this api admin/realms/{realm}/users/{id}/consents/{client} but still on client side next auth doesn't logout that user.It only works when we delete cookies.

@Meguais
Copy link

Meguais commented May 19, 2024

hi Is there a public repo with next with keycloak installed?

@degitgitagitya
Copy link
Author

degitgitagitya commented Jun 12, 2024

I tried with this api admin/realms/{realm}/users/{id}/consents/{client} but still on client side next auth doesn't logout that user.It only works when we delete cookies.

you can't magically logout a user, you should verify the accessToken from keycloak every time user access the api. if the accessToken invalid (in a case of session invalidation from another source) we proceed to logout the user.

@degitgitagitya
Copy link
Author

hi Is there a public repo with next with keycloak installed?

i'm sorry, all of my keycloak related repositories are private, by any chance, have you seen this ?

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