K
Kinde4mo ago
Colin

Permissions and roles not updating after a change using the Nuxt module

Following https://discord.com/channels/1070212618549219328/1205374262878412810 and https://discord.com/channels/1070212618549219328/1204727161794338849 I've been having a hard time getting up to date data from the nuxt module when using the same methods as in the docs. Here is a composition I've got to handle those operations:
export async function useUser() {
const { user } = useAuth()
const client = useKindeClient()

const { data: permissions } = await useAsyncData(`permissions-${user?.id ?? 'guest'}`, async () => {
const { permissions } = (await client?.getPermissions()) ?? {}
return permissions as string[]
})

const getHasPermission = (permission: string) => {
return permissions.value?.includes(permission) ?? false
}

const { data: roles, refresh: refreshRoles } = await useAsyncData(`roles-${user?.id ?? 'guest'}`, async () => {
const { value } = (await client?.getClaim('roles')) ?? {}
return value as { id: string, key: string, name: string }[]
})

const getHasRole = (role: string) => {
return roles.value?.some(r => r.key === role) ?? false
}

const { data: organization } = await useAsyncData(`organization-${user?.id ?? 'guest'}`, async () => {
const { orgCode } = (await client?.getOrganization()) ?? {}
return orgCode as string
})

return {
permissions,
roles,
organization,
getHasPermission,
getHasRole,
refreshRoles,
}
}
export async function useUser() {
const { user } = useAuth()
const client = useKindeClient()

const { data: permissions } = await useAsyncData(`permissions-${user?.id ?? 'guest'}`, async () => {
const { permissions } = (await client?.getPermissions()) ?? {}
return permissions as string[]
})

const getHasPermission = (permission: string) => {
return permissions.value?.includes(permission) ?? false
}

const { data: roles, refresh: refreshRoles } = await useAsyncData(`roles-${user?.id ?? 'guest'}`, async () => {
const { value } = (await client?.getClaim('roles')) ?? {}
return value as { id: string, key: string, name: string }[]
})

const getHasRole = (role: string) => {
return roles.value?.some(r => r.key === role) ?? false
}

const { data: organization } = await useAsyncData(`organization-${user?.id ?? 'guest'}`, async () => {
const { orgCode } = (await client?.getOrganization()) ?? {}
return orgCode as string
})

return {
permissions,
roles,
organization,
getHasPermission,
getHasRole,
refreshRoles,
}
}
At first I thought it was something to do with the tokens not being refreshed after the update but that does not make much sense as the data for permissions should not be in the token right? Anyway, I've not had any success in updating the user data. Am I doing something wrong or missing a piece?
9 Replies
onderay
onderay4mo ago
Hey @Colin , @Daniel_Kinde will be online in a few hrs to help answer this in more depth as he is one of the maintainers of the SDK. In general, it seems like your approach to fetching and using user data, permissions, and roles within a Nuxt application using the Kinde Nuxt module is generally correct. However, there are a few considerations and potential steps you can take to troubleshoot the issue of not getting up-to-date data: Token Refresh: While you mentioned that permissions data should not be in the token, it's important to ensure that the user's session is still valid. Kinde handles authentication and session management, including token refresh, but it's worth verifying that this process is working as expected in your application. If the session or token is expired and not properly refreshed, it could affect the ability to fetch updated data. Caching with useAsyncData: The useAsyncData function you're using is designed to fetch data asynchronously and cache it. Ensure that the cache is not preventing the retrieval of updated data. You might want to experiment with manually triggering a refresh of the data in certain scenarios or after specific user actions to see if that updates the data as expected. Review Kinde Permissions and Roles Configuration: Double-check the configuration of permissions and roles within your Kinde admin area to ensure that they are set up as expected. It's also worth verifying that the permissions and roles are being correctly assigned to users.
Colin
Colin4mo ago
Thanks for your detailled answer, for now I have covered your second and third point. I've check the permissions and roles in the dashboard and they all work as expected. I've also tried refreshing manually the data using the refresh method that is returned from the useAsyncData composable and it does not seem like this is the issue. For the token point, I am not doing anything with tokens so I don't see why it would not be valid (do you have any docs about what operations could lead to token invalidation?). I simply depend on the token refresh from the kinde client. For more context after testing it seems that logging out and in again works most of the time (but this just can't be a viable solution). Note that this does not work every time and sometime even logging out and in into another account does not provide up to date data. I will unfortunately not be able to test anything till Thursday as I am in business trip.
Daniel_Kinde
Daniel_Kinde4mo ago
Thanks for your efforts looking at this @Colin , I will look into it deeper while you're away.
Colin
Colin4mo ago
Hey @Daniel_Kinde any news on this?
Daniel_Kinde
Daniel_Kinde4mo ago
Hi @Colin , I have not recreated your setup, but I am aware that some work was done so that we invalidate this cache and refresh the claims whenever a user is updated in the UI or via API. This includes updates to profile information, organization changes, roles, permissions, properties and user. Are you able to retry?
Colin
Colin4mo ago
After some more testing, this is still not working, when changing a role or a permission (or any data on the user it seems) even after a clean browser refresh the data does not change. This is a kinda huge security concern as this means users could access roles and permissions that they do not have anymore, on top of being annoying to work with.
dave_kinde
dave_kinde4mo ago
Hey @Colin there are a few different ways of checking roles / permissions 1. Using the Management API - this will be real time. You could poll it and keep an internal store but obviously that incurs additional network requests. 2. Using the SDK helper methods which read from the access token. This is super convenient and saves on network requests but does suffer from the eventual consistency problem where there will be a window that updates in the Kinde admin UI are not yet reflected in the token depending on the access token lifetime you have set, 1+2. You could use a hybrid of a short access token lifetime combined with using the management API to protect highly sensitive areas of your product. Coming soon 3. Webhooks are right around the corner and give you the best of both worlds. This would allow you to subscribe to a user update event and based on that either request a new access token or update an internal store. We should have Beta access available for that any day now if you're keen to give them a go.
Colin
Colin4mo ago
Hey @dave_kinde thanks for your answer. I see so the kinde client methods in the nuxt module are based on just reading the access token. I did not know that, I will try to get a solution working and post it here once it is done. Seems like I found a solution. I created a /api/kinde/me route that gets the data from the management api and a useUser composable that fetches the data from that route. This was pretty easy thanks to the util @Daniel_Kinde helped me create in #Best way to access the management API from Nuxt server routes? Here is the server route:
const { kinde } = useRuntimeConfig()

export default defineEventHandler(async (event) => {
const { client } = useKindeServerClient(event)
const clientUser = await client.getUserProfile()
const managementApi = await useKindeManagementApi()

try {
const user = await managementApi.users.getUserData({ id: clientUser.id })

const roles = (await managementApi.organizations.getOrganizationUserRoles({
orgCode: kinde.defaultOrg,
userId: clientUser.id,
})).roles || []

const permissions = (await managementApi.organizations.getOrganizationUserPermissions({
orgCode: kinde.defaultOrg,
userId: clientUser.id,
})).permissions || []

return {
profile: user,
roles,
permissions,
}
}
catch (error) {
console.error(error)
throw createError({ statusMessage: 'Failed to fetch user', statusCode: 500 })
}
})
const { kinde } = useRuntimeConfig()

export default defineEventHandler(async (event) => {
const { client } = useKindeServerClient(event)
const clientUser = await client.getUserProfile()
const managementApi = await useKindeManagementApi()

try {
const user = await managementApi.users.getUserData({ id: clientUser.id })

const roles = (await managementApi.organizations.getOrganizationUserRoles({
orgCode: kinde.defaultOrg,
userId: clientUser.id,
})).roles || []

const permissions = (await managementApi.organizations.getOrganizationUserPermissions({
orgCode: kinde.defaultOrg,
userId: clientUser.id,
})).permissions || []

return {
profile: user,
roles,
permissions,
}
}
catch (error) {
console.error(error)
throw createError({ statusMessage: 'Failed to fetch user', statusCode: 500 })
}
})
And here is the composable:
export async function useUser() {
const { data: user, refresh: refreshUser } = await useFetch('/api/kinde/me')

const getHasPermission = async (permission: string) => {
await refreshUser()
if (!user.value)
return false
return user.value.permissions.some(p => p.key === permission) ?? false
}

const getHasRole = async (role: string) => {
await refreshUser()
if (!user.value)
return false
return user.value.roles.some(r => r.key === role) ?? false
}

return {
user,
refreshUser,
getHasPermission,
getHasRole,
}
}
export async function useUser() {
const { data: user, refresh: refreshUser } = await useFetch('/api/kinde/me')

const getHasPermission = async (permission: string) => {
await refreshUser()
if (!user.value)
return false
return user.value.permissions.some(p => p.key === permission) ?? false
}

const getHasRole = async (role: string) => {
await refreshUser()
if (!user.value)
return false
return user.value.roles.some(r => r.key === role) ?? false
}

return {
user,
refreshUser,
getHasPermission,
getHasRole,
}
}
This may be something that could be implemented into the module as it seems to me that this use case is pretty standard, I would be happy to contribute if this is something you want to consider.
Daniel_Kinde
Daniel_Kinde4mo ago
Great stuff @Colin , It is high on my list to get the management API access in the core module, we have a few PRs to get cleaned up and can look into this deeper.