next.js 使用keycloak提供程序从下一个身份验证注销不起作用

o2gm4chl  于 2023-01-20  发布在  其他
关注(0)|答案(2)|浏览(160)

我有一个带有next-auth的nextjs应用程序来管理身份验证。
这是我的配置

....
export default NextAuth({
  // Configure one or more authentication providers
  providers: [
    KeycloakProvider({
      id: 'my-keycloack-2',
      name: 'my-keycloack-2',
      clientId: process.env.NEXTAUTH_CLIENT_ID,
      clientSecret: process.env.NEXTAUTH_CLIENT_SECRET,
      issuer: process.env.NEXTAUTH_CLIENT_ISSUER,
      profile: (profile) => ({
        ...profile,
        id: profile.sub
      })
    })
  ],
....

验证按预期工作,但当我尝试使用next-auth signOut函数注销时,它不起作用。Next-auth会话被破坏,但keycloak保留了他的会话。

xvw2m8pv

xvw2m8pv1#

经过一番研究,我发现一个reddit对话https://www.reddit.com/r/nextjs/comments/redv1r/nextauth_signout_does_not_end_keycloak_session/描述了同样的问题。
这是我的解决方案。
我编写了一个自定义函数来注销

const logout = async (): Promise<void> => {
    const {
      data: { path }
    } = await axios.get('/api/auth/logout');
    await signOut({ redirect: false });
    window.location.href = path;
  };

我还定义了一个api路径,以获取销毁keycloak /api/auth/logout上的会话的路径

export default (req, res) => {
  const path = `${process.env.NEXTAUTH_CLIENT_ISSUER}/protocol/openid-connect/logout? 
                redirect_uri=${encodeURIComponent(process.env.NEXTAUTH_URL)}`;

  res.status(200).json({ path });
};
    • 更新**

在keycloak的最新版本中(在本文更新时是19. *. *-〉https://github.com/keycloak/keycloak-documentation/blob/main/securing_apps/topics/oidc/java/logout.adoc),重定向uri变得有点复杂

export default (req, res) => {

  const session = await getSession({ req });

  let path = `${process.env.NEXTAUTH_CLIENT_ISSUER}/protocol/openid-connect/logout? 
                post_logout_redirect_uri=${encodeURIComponent(process.env.NEXTAUTH_URL)}`;

if(session?.id_token) {
  path = path + `&id_token_hint=${session.id_token}`
} else {
  path = path + `&client_id=${process.env.NEXTAUTH_CLIENT_ID}`
}

  res.status(200).json({ path });
};

请注意,如果包含post_logout_redirect_uri,则需要包含client_id或id_token_hint参数。

6tdlim6h

6tdlim6h2#

所以,我有一个稍微不同的方法建立在这个线索上。
我并不喜欢应用程序中发生的所有重定向,也不喜欢仅仅为了处理“注销后握手”而在应用程序中添加新端点
相反,我将id_token直接添加到生成的初始JWT标记中,然后将一个名为doFinalSignoutHandshake的方法附加到events.signOut,该方法自动执行对keycloak服务端点的GET请求,并代表用户终止会话。
这种技术允许我维护应用程序中的所有当前流,并且仍然使用next-auth公开的标准signOut方法,而无需在前端进行任何特殊的定制。
这是用typescript编写的,因此我扩展了JWT定义以包含新值(在普通JS中不需要

// exists under /types/next-auth.d.ts in your project
// Typescript will merge the definitions in most
// editors
declare module "next-auth/jwt" {
    interface JWT {
        provider: string;
        id_token: string;
    }
}

下面是我对/pages/api/[...nextauth.ts]的实现

import axios, { AxiosError } from "axios";
import NextAuth from "next-auth";
import { JWT } from "next-auth/jwt";
import KeycloakProvider from "next-auth/providers/keycloak";

// I defined this outside of the initial setup so
// that I wouldn't need to keep copying the
// process.env.KEYCLOAK_* values everywhere
const keycloak = KeycloakProvider({
    clientId: process.env.KEYCLOAK_CLIENT_ID,
    clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
    issuer: process.env.KEYCLOAK_ISSUER,
});

// this performs the final handshake for the keycloak
// provider, the way it's written could also potentially
// perform the action for other providers as well
async function doFinalSignoutHandshake(jwt: JWT) {
    const { provider, id_token } = jwt;

    if (provider == keycloak.id) {
        try {
            // Add the id_token_hint to the query string
            const params = new URLSearchParams();
            params.append('id_token_hint', id_token);
            const { status, statusText } = await axios.get(`${keycloak.options.issuer}/protocol/openid-connect/logout?${params.toString()}`);

            // The response body should contain a confirmation that the user has been logged out
            console.log("Completed post-logout handshake", status, statusText);
        }
        catch (e: any) {
            console.error("Unable to perform post-logout handshake", (e as AxiosError)?.code || e)
        }
    }
}

export default NextAuth({
    secret: process.env.NEXTAUTH_SECRET,
    providers: [
        keycloak
    ],
    callbacks: {
        jwt: async ({ token, user, account, profile, isNewUser }) => {
            if (account) {
                // copy the expiry from the original keycloak token
                // overrides the settings in NextAuth.session
                token.exp = account.expires_at;
                token.id_token = account.id_token;
            }

            return token;
        }
    },
    events: {
        signOut: ({ session, token }) => doFinalSignoutHandshake(token)
    }
});

相关问题