Keycloak 上下文中缺少访问令牌

Access token missing from Keycloak context

我正在尝试从 postman 向我的节点 apollo express 后端发出经过身份验证的请求。我收到一条错误消息,指出用户未经身份验证。当我查看上下文对象时,没有访问令牌并调用 context.kauth.isAuthenticated() returns false.

查看access token,accessToken确实是空白,但是请求头中确实存在Bearer Token。

所以我不确定为什么没有包含访问令牌。

我正在向 post 人发出请求,我在请求中包含令牌,如下所示:

为了获得这个访问令牌,我首先向 Keycloak 发出一个 postman 请求来生成这个令牌(请注意,我故意不显示我的用户名和密码 post

我在上面的 postman 请求中使用了上面的访问令牌。

这是我的 index.js 文件的样子:

require("dotenv").config();
import { ApolloServer } from "apollo-server-express";
import { ApolloServerPluginDrainHttpServer } from "apollo-server-core";
const { makeExecutableSchema } = require('@graphql-tools/schema');
import { configureKeycloak } from "./auth/config"
import {
  KeycloakContext,
  KeycloakTypeDefs,
  KeycloakSchemaDirectives,
} from "keycloak-connect-graphql";
import { applyDirectiveTransformers } from "./auth/transformers";
import express from "express";
import http from "http";
import typeDefs from "./graphql/typeDefs";
import resolvers from "./graphql/resolvers";
import { MongoClient } from "mongodb";
import MongoHelpers from "./dataSources/MongoHelpers";

async function startApolloServer(typeDefs, resolvers) {

  const client = new MongoClient(process.env.MONGO_URI);
  client.connect();

  let schema = makeExecutableSchema({
    typeDefs: [KeycloakTypeDefs, typeDefs],
    resolvers
  });

  schema = applyDirectiveTransformers(schema);

  const app = express();
  const httpServer = http.createServer(app);

  const { keycloak } = configureKeycloak(app, '/graphql')    

  const server = new ApolloServer({
    schema,
    schemaDirectives: KeycloakSchemaDirectives,
    resolvers,
    context: ({ req }) => {
      return {
        kauth: new KeycloakContext({ req }, keycloak) 
      }
      
    },
    plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
  });
  await server.start();
  server.applyMiddleware({ app });
  await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));
  console.log(` Server ready at http://localhost:4000${server.graphqlPath}`);
}

startApolloServer(typeDefs, resolvers);

这是我的 keycloak.json 文件:

我真的很困惑,我最初的想法是我没有正确地向 postman 提出请求。感谢您的指导

要求:

  • 使用node、apollo、express基于keycloak-connect中间件
  • 获取keycloak认证授权
  • 使用 Postman 通过 Bearer 令牌进行经过身份验证的调用。

index.js不是minimal, reproducible example,因为typeDefs./auth/transformers等部分缺失

https://github.com/aerogear/keycloak-connect-graphql 上有一个很酷的描述和很好的示例代码。

因此,如果仅稍微更改您的方法(例如,不需要 mongodb),然后相应地从 Github 页面的描述中添加同样略有更改的代码,则可以获得独立的运行 index.js.

例如,它可能看起来像这样:

"use strict";
const {ApolloServer, gql} = require("apollo-server-express")
const {ApolloServerPluginDrainHttpServer} = require("apollo-server-core")
const {makeExecutableSchema} = require('@graphql-tools/schema');
const {getDirective, MapperKind, mapSchema} = require('@graphql-tools/utils')
const {KeycloakContext, KeycloakTypeDefs, auth, hasRole, hasPermission} = require("keycloak-connect-graphql")
const {defaultFieldResolver} = require("graphql");
const express = require("express")
const http = require("http")
const fs = require('fs');
const path = require('path');
const session = require('express-session');
const Keycloak = require('keycloak-connect');

function configureKeycloak(app, graphqlPath) {
    const keycloakConfig = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'config/keycloak.json')));
    const memoryStore = new session.MemoryStore();
    app.use(session({
        secret: process.env.SESSION_SECRET_STRING || 'this should be a long secret',
        resave: false,
        saveUninitialized: true,
        store: memoryStore
    }));
    const keycloak = new Keycloak({
        store: memoryStore
    }, keycloakConfig);
    // Install general keycloak middleware
    app.use(keycloak.middleware({
        admin: graphqlPath
    }));
    // Protect the main route for all graphql services
    // Disable unauthenticated access
    app.use(graphqlPath, keycloak.middleware());
    return {keycloak};
}

const authDirectiveTransformer = (schema, directiveName = 'auth') => {
    return mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
            const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
            if (authDirective) {
                const {resolve = defaultFieldResolver} = fieldConfig;
                fieldConfig.resolve = auth(resolve);
            }
            return fieldConfig;
        }
    })
}

const directive = (keys, key, directive, directiveName) => {
    if (keys.length === 1 && keys[0] === key) {
        let dirs = directive[keys[0]];
        if (typeof dirs === 'string') dirs = [dirs];
        if (Array.isArray(dirs)) {
            return dirs.map((val) => String(val));
        } else {
            throw new Error(`invalid ${directiveName} args. ${key} must be a String or an Array of Strings`);
        }
    } else {
        throw Error(`invalid ${directiveName}  args. must contain only a ${key} argument`);
    }
}

const permissionDirectiveTransformer = (schema, directiveName = 'hasPermission') => {
    return mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
            const permissionDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
            if (permissionDirective) {
                const {resolve = defaultFieldResolver} = fieldConfig;
                const keys = Object.keys(permissionDirective);
                let resources = directive(keys, 'resources', permissionDirective, directiveName);
                fieldConfig.resolve = hasPermission(resources)(resolve);
            }
            return fieldConfig;
        }
    })
}

const roleDirectiveTransformer = (schema, directiveName = 'hasRole') => {
    return mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
            const roleDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
            if (roleDirective) {
                const {resolve = defaultFieldResolver} = fieldConfig;
                const keys = Object.keys(roleDirective);
                let role = directive(keys, 'role', roleDirective, directiveName);
                fieldConfig.resolve = hasRole(role)(resolve);
            }
            return fieldConfig;
        }
    })
}

const applyDirectiveTransformers = (schema) => {
    return authDirectiveTransformer(roleDirectiveTransformer(permissionDirectiveTransformer(schema)));
}

const typeDefs = gql`
  type Query {
    hello: String @hasRole(role: "developer")
  }
`

const resolvers = {
    Query: {
        hello: (obj, args, context, info) => {
            console.log(context.kauth)
            console.log(context.kauth.isAuthenticated())
            console.log(context.kauth.accessToken.content.preferred_username)

            const name = context.kauth.accessToken.content.preferred_username || 'world'
            return `Hello ${name}`
        }
    }
}

async function startApolloServer(typeDefs, resolvers) {

    let schema = makeExecutableSchema({
        typeDefs: [KeycloakTypeDefs, typeDefs],
        resolvers
    });

    schema = applyDirectiveTransformers(schema);

    const app = express();
    const httpServer = http.createServer(app);

    const {keycloak} = configureKeycloak(app, '/graphql')

    const server = new ApolloServer({
        schema,
        resolvers,
        context: ({req}) => {
            return {
                kauth: new KeycloakContext({req}, keycloak)
            }

        },
        plugins: [ApolloServerPluginDrainHttpServer({httpServer})],
    });
    await server.start();
    server.applyMiddleware({app});
    await new Promise((resolve) => httpServer.listen({port: 4000}));
    console.log(` Server ready at http://localhost:4000${server.graphqlPath}`);
}

startApolloServer(typeDefs, resolvers);

对应package.json:

{
  "dependencies": {
    "@graphql-tools/schema": "^8.3.10",
    "@graphql-tools/utils": "^8.6.9",
    "apollo-server-core": "^3.6.7",
    "apollo-server-express": "^3.6.7",
    "express": "^4.17.3",
    "express-session": "^1.17.2",
    "graphql": "^15.8.0",
    "graphql-tools": "^8.2.8",
    "http": "^0.0.1-security",
    "keycloak-connect": "^18.0.0",
    "keycloak-connect-graphql": "^0.7.0"
  }
}

与邮递员通话

可以看到,经过身份验证的调用成功了。此外,使用上面的代码,accessToken 被正确地记录到调试控制台:

这肯定不是完全符合您要求的功能。但是您可以根据您的要求逐渐对这个 运行 示例进行 desired/necessary 更改。