-
-
Save degitgitagitya/db5c4385fc549f317eac64d8e5702f74 to your computer and use it in GitHub Desktop.
# 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; | |
} | |
} |
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.
hi Is there a public repo with next with keycloak installed?
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.
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 ?
the token on the client cookie that you revoked will be
invaliduseless (but still a valid token), meaning that token can't be used for authorization