1

I am trying to make an authenticated request from postman to my node, apollo, express backend. I am gettting an error saying that the user is unauthenticated. When I look at the context object, there is no access token and calling context.kauth.isAuthenticated() returns false.

Looking at the access token, I can see that accessToken is indeed blank, but there does exist the Bearer Token in the request header.

enter image description here enter image description here

So I am not sure why the access token is not being included.

I am making the request from postman, I am including the token in the request like so:

enter image description here

In order to get this access token, I am first making a postman request to Keycloak to generate this token like so (note that I am intentionally not showing my username and password for this post

enter image description here

I am using the above access token in my postman request above.

This is what my index.js file looks like:

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);

And this is my keycloak.json file:

enter image description here

I am really quite stummped, my initial thought is that I am not making the reqest from postman correctly. Am grateful for any guidance

Jan Garaj
  • 25,598
  • 3
  • 38
  • 59
Oamar Kanji
  • 1,824
  • 6
  • 24
  • 39
  • 1
    When you don't use the GUI authentication interface the `id_token` (and perhaps other stuff) of keycloak is not defined, maybe that is linked somehow to your acces token? Have you tried it outside postman in a regular website/app? – Casper Kuethe Apr 19 '22 at 19:58
  • 1
    Can you try manually? The Authorization Tab in Postman is for convenience, you could start by trying to set the corresponding Header yourself? Usually it would be "Authorization".' Also. can you enable some more logging on the backend? – Mario B Apr 23 '22 at 11:46
  • @CasperKuethe and Mairo B thanks for your suggestions. I did indeed try it from the front end and not just from postman and it works. Then, once one has logged into the front end it works from postman. This is because there are additional cookies that need to be included in the request. These cookies are set by keycloak in the browser and postman will actually extract them from the browser and include them in the request, but they have to already be there which is achieved by logging in with the front end. – Oamar Kanji Apr 23 '22 at 23:23

1 Answers1

5

Requirements:

  • use node, apollo, express to get keycloak Authentication and Authorization based on the keycloak-connect middleware
  • using Postman to make an authenticated call with a Bearer token.

index.js in the question is not a minimal, reproducible example because, for example, the parts in typeDefs, ./auth/transformers and so on are missing.

There is a cool description at https://github.com/aerogear/keycloak-connect-graphql with nice example code.

So if one changes your approach only slightly (e.g. mongodb is not needed) and then adds the also slightly changed code from the description of the Github page accordingly, one can get a standalone running index.js.

For example, it might look something like this:

"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);

The corresponding 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"
  }
}

Call With Postman

demo

As one can see, the authenticated call is then successful. Also, with the above code, the accessToken is logged correctly to the debug console:

debug output

This is certainly not the functionality that exactly meets your requirements. But you may be able to gradually make the desired/necessary changes from this running example depending on your requirements.

Stephan Schlecht
  • 26,556
  • 1
  • 33
  • 47