Access token missing from Keycloak context - javascript
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.
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:
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
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:
I am really quite stummped, my initial thought is that I am not making the reqest from postman correctly. Am grateful for any guidance
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
As one can see, the authenticated call is then successful. Also, with the above code, the accessToken is logged correctly to the debug console:
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.
Related
Insert data from javascript file into mongodb using graphql
I am new to using Graphql and MongoDB. I am trying to insert data from an existing javascript file where the data has been defined. I was trying to use a mutation in order to achieve this but I have no clue what I'm really doing. Any help would be nice. const dotenv = require('dotenv'); dotenv.config(); const { ApolloServer, gql } = require('apollo-server'); const { MongoClient } = require('mongodb'); const items = require('./itemsListData'); const typeDefs = gql` type Query { items:[Item!]! } type Item{ id:ID!, name:String!, aisle:String!, bay:String!, price:Float!, xVal:Int!, yVal:Int! } type Mutation { createItem(name: String!, aisle: String!): Item! } `; console.log(items) const resolvers = { Query: { items:() => items, }, Item:{ id: ( { _id, id }) => _id || id, }, Mutation: { createItem: async(_, { name }, { db }) => { // name:String!, bays:[Bay!]!, xStartVal:Int!, xEndVal:Int!, yStartVal:Int!, yEndVal:Int! const newItem = { items } // insert Item object into database const result = await db.collection('Items').insert(newItem); console.log("This is the result " + result); return result.ops[0]; // first item in array is the item we just added } } }; const start = async () => { const client = new MongoClient("mongodb+srv://admin:admin#quickkartcluster.o0bsfej.mongodb.net/test", { useNewUrlParser: true, useUnifiedTopology: true }); await client.connect(); const db = client.db("QuickKart"); const context = { db, } const server = new ApolloServer({ typeDefs, resolvers, context, introspection: true }); // The `listen` method launches a web server. server.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`); }); } start(); here is my javascript data file https://pastebin.com/wvGANBgR
Strapi backend broken when enable custom middleware
I created a custom middleware that makes the api & admin unreachable when I enable it. The middleware is pretty simple, it adds a request id to incoming request on the server: const { createNamespace } = require('cls-hooked'); const { v4: uuidv4 } = require('uuid'); const loggerNamespace = createNamespace('logger'); module.exports = (strapi) => { return { initialize() { strapi.app.use((ctx, next) => { const reqId = ctx.request.get('X-Request-Id') || uuidv4(); ctx.response.set('X-Request-Id', reqId); loggerNamespace.run(() => { loggerNamespace.set('requestId', reqId); next(); }); }); }, }; }; It's enable using the config file ./config/middleware.json: module.exports = { settings: { addRequestId: { enabled: true, }, }, }; Then, when enabled, calling an API endpoint or trying to connect to admin results in 404 Not Found. I use strapi 3.6.8 with node 14.18.1. Any idea why? PS: I suspected cls-hooked to be the culprit but removing it for testing with an anemic middleware doesn't work either.
#Salvino put me on the right track suggesting that I should await the execution of next. Digging in the code of cls-hooked, I found the method runPromise that is similar to run and returns a Promise that can be awaited. It solved the issue. Fixed code: const { createNamespace } = require('cls-hooked'); const { v4: uuidv4 } = require('uuid'); const loggerNamespace = createNamespace('logger'); module.exports = (strapi) => { return { initialize() { strapi.app.use(async (ctx, next) => { const reqId = ctx.request.get('X-Request-Id') || uuidv4(); ctx.response.set('X-Request-Id', reqId); await loggerNamespace.runPromise(async () => { loggerNamespace.set('requestId', reqId); await next(); }); }); }, }; };
Async export of redis client in nodejs
The following code constructs a redis client and exports. I am fetching the redis password from vault secret management service and that call is a promise/async. The code doesnt wait for that call and it exports the redis client before async call completes. I am not sure what I am doing wrong here. Any idea? import redis from 'redis'; import bluebird from 'bluebird'; import logger from '../logger'; import srvconf from '../srvconf'; import { getVaultSecret } from '../services/vault.service'; const vaultConfig = srvconf.get('vault'); bluebird.promisifyAll(redis); let redisUrl = ''; const maskRedisUrl = (url) => url.replace(/password=.*/, 'password=*****'); const setRedisUrl = (host, port, pw) => { const pwstring = pw ? `?password=${pw}` : ''; const url = `redis://${host}:${port}${pwstring}`; console.log(`Setting redis_url to '${maskRedisUrl(url)}'`); return url; } if (vaultConfig.use_vault) { (async () => { const secret = await getVaultSecret(`${vaultConfig.redis.secrets_path + vaultConfig.redis.key}`) redisUrl = setRedisUrl(srvconf.get('redis_host'), srvconf.get('redis_port'), secret.PASSWORD); })().catch(err => console.log(err)); } else { if (!srvconf.get('redis_url')) { redisUrl = setRedisUrl(srvconf.get('redis_host'), srvconf.get('redis_port'), srvconf.get('redis_password'));; } else { redisUrl = srvconf.get('redis_url'); console.log(`Found redis_url ${maskRedisUrl(redisUrl)}`); } } const options = redisUrl ? { url: redisUrl } : {}; const redisClient = redis.createClient(options); redisClient.on('error', err => { logger.error(err); }); export default redisClient;
The problem is that (async () => {...})() returns a Promise and you are not awaiting it at the top-level, so the script continues to run past that line, sets options = {} and returns the redisClient. What you need is a top-level await which is enabled by default in Node versions >= 14.8.0. However, if your project uses a version older than that, there is a workaround as shown below. Please note that the below code is NOT tested since I do not have the same project setup locally. Module import redis from "redis"; import bluebird from "bluebird"; import logger from "../logger"; import srvconf from "../srvconf"; import { getVaultSecret } from "../services/vault.service"; const vaultConfig = srvconf.get("vault"); bluebird.promisifyAll(redis); let redisUrl = ""; let redisClient = null; const initRedisClient = () => { const options = redisUrl ? { url: redisUrl } : {}; redisClient = redis.createClient(options); redisClient.on("error", (err) => { logger.error(err); }); }; const maskRedisUrl = (url) => url.replace(/password=.*/, "password=*****"); const setRedisUrl = (host, port, pw) => { const pwstring = pw ? `?password=${pw}` : ""; const url = `redis://${host}:${port}${pwstring}`; console.log(`Setting redis_url to '${maskRedisUrl(url)}'`); return url; }; (async () => { if (vaultConfig.use_vault) { try { const secret = await getVaultSecret( `${vaultConfig.redis.secrets_path + vaultConfig.redis.key}` ); redisUrl = setRedisUrl( srvconf.get("redis_host"), srvconf.get("redis_port"), secret.PASSWORD ); } catch (err) { console.log(err); } } else { if (!srvconf.get("redis_url")) { redisUrl = setRedisUrl( srvconf.get("redis_host"), srvconf.get("redis_port"), srvconf.get("redis_password") ); } else { redisUrl = srvconf.get("redis_url"); console.log(`Found redis_url ${maskRedisUrl(redisUrl)}`); } } // Initialize Redis client after vault secrets are loaded initRedisClient(); })(); export default redisClient; Usage At all places where you import and use the client, you always need to check if it is actually initialized successfully, and throw (and catch) a well defined error if it is not. const redisClient = require("path/to/module"); ... if (redisClient) { // Use it } else { throw new RedisClientNotInitializedError(); } ...
Apollo GraphQL - TypeError: Cannot read property 'findOrCreateUser' of undefined
I'm following this tutorial https://www.apollographql.com/docs/tutorial/mutation-resolvers/#book-trips and have ended up getting this error: "TypeError: Cannot read property 'findOrCreateUser' of undefined", " at login Below is the function I use to call login: mutation LoginUser { login(email: "daisy#apollographql.com") { token } } The file below is where findOrCreateUser is called: src/resolvers.js module.exports = { Mutation: { login: async (_, {email}, {dataSources}) => { const user = await dataSources.userAPI.findOrCreateUser({ email }); if (user) { user.token = Buffer.from(email).toString('base64'); return user; } }, }, This is where dataSources is defined: src/index.js require('dotenv').config(); const { ApolloServer } = require('apollo-server'); const typeDefs = require('./schema'); const { createStore } = require('./utils'); const resolvers = require('./resolvers'); const isEmail = require('isemail'); const LaunchAPI = require('./datasources/launch'); const UserAPI = require('./datasources/user'); const store = createStore(); const server = new ApolloServer({ context: async ({req}) => { const auth = req.headers && req.headers.authorization || ''; const email = Buffer.from(auth, 'base64').toString('ascii'); if (!isEmail.validate(email)) return {user: null}; // find a user by their email const users = await store.user.findOrCreate({ where: { email } }); const user = users && users[0] || null; return { user: {...user.dataValues } }; }, dataSources: () => ({ launchAPI: new LaunchAPI(), UserAPI: new UserAPI({store}) }), typeDefs, resolvers, }); server.listen().then(() => { console.log(` Server is running! Listening on port 4000 Explore at https://studio.apollographql.com/dev `) });
As xadm mentioned in the comment the issues is your usage of userAPI vs UserAPI. the datasource object is created in index.js in the following block dataSources: () => ({ launchAPI: new LaunchAPI(), UserAPI: new UserAPI({store}) }), here you defined datasources.UserAPI however in resolvers.js you refer to it as datasources.userAPI (note the difference in capitalization of userAPI). Your problem can be resolved by changing the above code block to dataSources: () => ({ launchAPI: new LaunchAPI(), userAPI: new UserAPI({store}) }),
GraphQL server with Deno
It works just once for the below code import { graphql, GraphQLSchema, GraphQLObjectType, GraphQLString, buildSchema, } from "https://cdn.pika.dev/graphql/^15.0.0"; import { serve } from "https://deno.land/std#0.50.0/http/server.ts"; var schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "RootQueryType", fields: { hello: { type: GraphQLString, resolve() { return "world"; }, }, }, }), }); var query = "{ hello }"; graphql(schema, query).then((result) => { console.log(result); }); How to keep it listening, just like express Something like this var express = require('express'); var graphqlHTTP = require('express-graphql'); var { buildSchema } = require('graphql'); // Construct a schema, using GraphQL schema language var schema = buildSchema(` type Query { hello: String } `); // The root provides a resolver function for each API endpoint var root = { hello: () => { return 'Hello world!'; }, }; var app = express(); app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: root, graphiql: true, })); app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql');
import { graphql, buildSchema, } from "https://cdn.pika.dev/graphql/^15.0.0"; import {Application, Router} from "https://deno.land/x/oak/mod.ts"; var schema = buildSchema(` type Query { hello: String } `); var resolver = {hello: () => 'Hello world!'} const executeSchema = async (query:any) => { const result = await graphql(schema, query, resolver); return result; } var router = new Router(); router.post("/graph", async ({request, response}) => { if(request.hasBody) { const body = await request.body(); const result = await executeSchema(body.value); response.body = result; } else { response.body = "Query Unknown"; } }) let app = new Application(); app.use(router.routes()); app.use(router.allowedMethods()); console.log("Server running"); app.listen({port: 5000})
You can now use https://deno.land/x/deno_graphql to achieve this goal. It provides everything needed out-of-the-box and works with multiple Deno frameworks (oak, abc, attain, etc). This is how you code looks like (with oak for example): import { Application, Context, Router } from "https://deno.land/x/oak/mod.ts"; import { gql, graphqlHttp, makeExecutableSchema, } from "https://deno.land/x/deno_graphql/oak.ts"; const typeDefs = gql` type Query { hello: String } `; const resolvers = { Query: { hello: () => "Hello world!", }, }; const context = (context: Context) => ({ request: context.request, }); const schema = makeExecutableSchema({ typeDefs, resolvers }); const app = new Application(); const router = new Router(); router.post("/graphql", graphqlHttp({ schema, context })); app.use(router.routes()); await app.listen({ port: 4000 }); PS : i'm the author of the package, so you can ask me anything. Hope this helps!
Here is an example using oak working with your GraphQL code. First let's say you have a repository graphRepository.ts with your graph schema: import { graphql, GraphQLSchema, GraphQLObjectType, GraphQLString } from "https://cdn.pika.dev/graphql/^15.0.0"; var schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: "RootQueryType", fields: { hello: { type: GraphQLString, resolve() { return "world"; }, }, }, }), }); export async function querySchema(query: any) { return await graphql(schema, query) .then(async (result) => { return result; }); } Now start your app.ts listener with the routes, and use the following URL to call the endpoint: http://localhost:8000/graph/query/hello import { Application, Router } from "https://deno.land/x/oak/mod.ts"; import { querySchema } from "./graphRepository.ts"; const router = new Router(); router .get("/graph/query/:value", async (context) => { const queryValue: any = context.params.value; const query = `{ ${queryValue}}` const result = await querySchema(query); console.log(result) context.response.body = result; }) const app = new Application(); app.use(router.routes()); app.use(router.allowedMethods()); await app.listen({ port: 8000 });
here is a code example using oak and middleware. You also can enjoy the playground GUI like an apollo one. import { Application } from "https://deno.land/x/oak/mod.ts"; import { applyGraphQL, gql } from "https://deno.land/x/oak_graphql/mod.ts"; const app = new Application(); app.use(async (ctx, next) => { await next(); const rt = ctx.response.headers.get("X-Response-Time"); console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`); }); app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; ctx.response.headers.set("X-Response-Time", `${ms}ms`); }); const types = gql` type User { firstName: String lastName: String } input UserInput { firstName: String lastName: String } type ResolveType { done: Boolean } type Query { getUser(id: String): User } type Mutation { setUser(input: UserInput!): ResolveType! } `; const resolvers = { Query: { getUser: (parent: any, {id}: any, context: any, info: any) => { console.log("id", id, context); return { firstName: "wooseok", lastName: "lee", }; }, }, Mutation: { setUser: (parent: any, {firstName, lastName}: any, context: any, info: any) => { console.log("input:", firstName, lastName); return { done: true, }; }, }, }; const GraphQLService = applyGraphQL({ typeDefs: types, resolvers: resolvers }) app.use(GraphQLService.routes(), GraphQLService.allowedMethods()); console.log("Server start at http://localhost:8080"); await app.listen({ port: 8080 });
I have created gql for making GraphQL servers that aren't tied to a web framework. All of the responses above show Oak integration but you don't really have to use it to have a GraphQL server. You can go with std/http instead: import { serve } from 'https://deno.land/std#0.90.0/http/server.ts' import { GraphQLHTTP } from 'https://deno.land/x/gql/mod.ts' import { makeExecutableSchema } from 'https://deno.land/x/graphql_tools/mod.ts' import { gql } from 'https://deno.land/x/graphql_tag/mod.ts' const typeDefs = gql` type Query { hello: String } ` const resolvers = { Query: { hello: () => `Hello World!` } } const schema = makeExecutableSchema({ resolvers, typeDefs }) const s = serve({ port: 3000 }) for await (const req of s) { req.url.startsWith('/graphql') ? await GraphQLHTTP({ schema, graphiql: true })(req) : req.respond({ status: 404 }) }