Closed jsfeutz closed 2 years ago
Thank you for logging issue.
Do you have more info what doesnt work? We could start with updating example and add support for it.
My take is that we should be good when using graphql-tools to build schema and then pass schema to apollo server (no matter version)
Let me know if that works for you.
Thanks for responding :) Let me know what I'm doing wrong.
It seems like apollo-connect-express completely ignores the directives @auth, @hasRole when I upgrade to apollo v3
To replicate
what works:
1) Take the vanilla instructions from the keycloak-connect-graphql readme. install apollo-connect-express@2.25.2 or earlier
adding a @auth, @hasRole, to a query/mutation results in "user authentication required" message when authorization header not provided.
what doesnt work:
2) Changing only the version of apollo-connect-express@3.0.0+ few new modifications on how apollo server 3 needs to be started. keycloak-connect-graphql seems to be ignored and you can access the query/mutations that have @auth/@hasRole set.
` //apollo v 2.25.2, cant move to v3 until we fix keycloak integrations.
const server = new ApolloServer({
// plugins: [ApolloServerPluginInlineTrace()], //v3 option
typeDefs: mergeTypeDefs([...Globals.TypeDefs, ServiceModule.typeDefs]),
schemaDirectives: KeycloakSchemaDirectives,
resolvers: ServiceModule.resolvers,
context: async({ req, connection }) => {
return {
kauth: new KeycloakContext({ req }, keycloak) //add the KeycloakContext to `kauth`
}
},
});
//await server.start() //v3 start
server.applyMiddleware({ app, path: URI_GQL })
const httpServer = http.createServer(app)
//server.installSubscriptionHandlers(httpServer);
// ⚠️ Pay attention to the fact that we are calling `listen` on the http server variable, and not on `app`.
httpServer.listen(HTTP_PORT, () => {
log.info(`🚀 LOG_LEVEL=${process.env.LOG_LEVEL}: Server ready at http://0.0.0.0:${HTTP_PORT}${server.graphqlPath}`)
log.info(`🚀 LOG_LEVEL=${process.env.LOG_LEVEL}: Subscriptions ready at ws://0.0.0.0:${HTTP_PORT}${server.subscriptionsPath}`)
})`
Yes. Because Apollo 3.0 has it is own ways of building schema and support directives (to support federation etc). We are using graphql-tools that is vendor agnositc so it works with graphql-express and apollo - assuming that we pass schema to the apollo server.
Actually this is issue with readme that doesn't seem to be using GraphQL tools. All we recommend is graphql server express that stopped be compatible with tools from 3.x
https://github.com/aerogear/keycloak-connect-graphql
We should update docs how to use graphql tools.
Overal we could update project to work with graphql express (and stop supporting graphql-tools).
Place that causes issues: https://github.com/aerogear/keycloak-connect-graphql/blob/228ec2bfca66644b5168a07a6d2db025701afc1f/src/directives/index.ts#L3
Format is different for the 3.0
So new config using makeExecutableSchema and passing just a schema to apollo 3.1.2 or 2.25.2.
I added console.log markers in keycloak-connect-graphql at KeyContext and KeyContextBase. From the log, Keycontext nor KeyContextBase ever get the token from the authorization header. Not sure if I'm not setting up the Keycloak context correctly.
It allows access to the query/mutation.
code
``
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { mergeTypeDefs } = require('@graphql-tools/merge');
const Keycloak = require('keycloak-connect')
const { KeycloakTypeDefs, KeycloakContext, KeycloakSchemaDirectives } = require('keycloak-connect-graphql')
let schema = makeExecutableSchema({
typeDefs: mergeTypeDefs([KeycloakTypeDefs, ServiceModule.typeDefs, ...Globals.TypeDefs]),
resolvers: [ServiceModule.resolvers], // optional
allowUndefinedInResolve: false, // optional
resolverValidationOptions: {}, // optional
schemaDirectives: KeycloakSchemaDirectives, // optiona
parseOptions: {}, // optional
inheritResolversFromInterfaces: false // optional
});
//apollo v 3.1.2 config
const server = new ApolloServer({
schema: schema,
context: async({ req, connection }) => {
console.log("Apollo Server Context: Calling Context.")
console.log("Authorization Header::")
console.log(req.headers.authorization)
return {
kauth: new KeycloakContext({ req }, keycloak) //or new KeycloakContext({ req }) same result
}
},
});
``
log:: `Apollo Server Context: Calling Context.
Authorization Header::
Bearer eyJhbGciOiJSU#####
Calling Keycloak Context Constructor
Token Not Found:
calling KeycloakBase
undefined
Keycloak Object found`
This issue is definitely something that needs to be addressed by example and code. I have no capacity to check it this week, however i think you are close to success without it
As for error this happens when code is missing: https://github.com/aerogear/keycloak-connect-graphql/blob/master/examples/lib/common.js#L24
for blocking unauthenticated access
https://github.com/aerogear/keycloak-connect-graphql/blob/master/examples/basic.js#L19
Here's my solution using Apollo 3 with graphql-modules
:
KeycloakTypeDefs
into AST: gpl(KeycloakTypeDefs)
.@graphql-tools/utils
instead of using KeycloakSchemaDirectives
.Authorization: Bearer xxx
header in client side.The main part of this solution is replacing KeycloakSchemaDirectives
with our own transformers, we can found some clue from the example in @graphql-tools/utils
:
const transformer = (schema, directiveName = 'auth') =>
mapSchema(schema, {
[MapperKind.FIELD]: (fieldConfig) => {
// Check whether this field has the specified directive
const [directive] =
getDirective(schema, fieldConfig, directiveName) || [];
if (directive) {
// Get this field's original resolver
const { resolve = defaultFieldResolver } = fieldConfig;
// Replace the original resolver with a function that *first* calls
// the original resolver, then converts its result
fieldConfig.resolve = async (source, args, context, info) => {
const result = await resolve(source, args, context, info);
// TODO: Convert the result here
return result;
};
}
},
});
Full example below:
In graphql/Keycloak.mjs
:
I copied some content of the schemaDirectiveVisitors.ts
import { gql, createModule } from 'graphql-modules';
import { defaultFieldResolver } from 'graphql';
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
import {
KeycloakTypeDefs,
auth as resolveAuth,
hasRole as resolveHasRole,
hasPermission as resolveHasPermission,
} from 'keycloak-connect-graphql';
export const authDirectiveTransformer = (schema, directiveName = 'auth') =>
mapSchema(schema, {
[MapperKind.FIELD]: (fieldConfig) => {
const [directive] =
getDirective(schema, fieldConfig, directiveName) || [];
if (directive) {
// AuthDirective.visitFieldDefinition
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = resolveAuth(resolve);
}
},
});
// HasRoleDirective.parseAndValidateArgs
function parseAndValidateRoles(args) {
const keys = Object.keys(args);
if (keys.length === 1 && keys[0] === 'role') {
const role = args[keys[0]];
if (typeof role === 'string') return [role];
if (Array.isArray(role)) return role.map((val) => String(val));
throw new Error(
`invalid hasRole args. role must be a String or an Array of Strings`
);
}
throw Error("invalid hasRole args. must contain only a 'role argument");
}
export const hasRoleDirectiveTransformer = (
schema,
directiveName = 'hasRole'
) =>
mapSchema(schema, {
[MapperKind.FIELD]: (fieldConfig) => {
const [directive] =
getDirective(schema, fieldConfig, directiveName) || [];
if (directive) {
// HasRoleDirective.visitFieldDefinition
const { resolve = defaultFieldResolver } = fieldConfig;
const roles = parseAndValidateRoles(directive);
fieldConfig.resolve = resolveHasRole(roles)(resolve);
}
},
});
// HasPermissionDirective.parseAndValidateArgs
function parseAndValidateResources(args) {
const keys = Object.keys(args);
if (keys.length === 1 && keys[0] === 'resources') {
const resources = args[keys[0]];
if (typeof resources === 'string') return [resources];
if (Array.isArray(resources)) return resources.map((val) => String(val));
throw new Error(
`invalid hasPermission args. resources must be a String or an Array of Strings`
);
}
throw Error(
"invalid hasPermission args. must contain only a 'resources argument"
);
}
export const hasPermissionDirectiveTransformer = (
schema,
directiveName = 'hasPermission'
) =>
mapSchema(schema, {
[MapperKind.FIELD]: (fieldConfig) => {
const [directive] =
getDirective(schema, fieldConfig, directiveName) || [];
if (directive) {
// HasPermissionDirective.visitFieldDefinition
const { resolve = defaultFieldResolver } = fieldConfig;
const resources = parseAndValidateResources(directive);
fieldConfig.resolve = resolveHasPermission(resources)(resolve);
}
},
});
export default createModule({
id: 'keycloak',
dirname,
typeDefs: gql(KeycloakTypeDefs), // Transform string to AST
});
The graphql/Application.mjs
:
import { createApplication } from 'graphql-modules';
import keycloak from './Keycloak.mjs';
const application = createApplication({
modules: [
// ...other modules
keycloak,
],
});
export default application;
and the apollo.mjs
which provide the main server functionality:
import { ApolloServer } from 'apollo-server-express';
import { KeycloakContext } from 'keycloak-connect-graphql';
import { compose } from './utils.mjs';
import keycloak from './keycloak.mjs';
import graphqlApp from './graphql/Application.mjs';
import {
authDirectiveTransformer,
hasRoleDirectiveTransformer,
hasPermissionDirectiveTransformer,
} from './graphql/Keycloak.mjs';
const schema = graphqlApp.createSchemaForApollo();
const context = ({ req }) => ({
kauth: new KeycloakContext({ req }, keycloak),
});
// Here I use a compose utility to avoid nesting functions
// authDirectiveTransformer(hasRoleDirectiveTransformer(hasPermissionDirectiveTransformer(schema)))
const composedSchema = compose(
authDirectiveTransformer,
hasRoleDirectiveTransformer,
hasPermissionDirectiveTransformer
)(schema);
export default async function start({ app }) {
const server = new ApolloServer({
schema: composedSchema,
context,
});
await server.start();
server.applyMiddleware({ app });
}
The compose utility is a simple reduce function:
export const compose =
(...fns) =>
(x) =>
fns.reduceRight((v, f) => f(v), x);
Hope it can help, thanks.
Would be great if you guys could add a full example. I unfortuanetly do not understand the sketched solution but cope with the same problem.
We are about two months since any activity and this is still an issue with no realistic examples that doesn't require copy pasta and workarounds. Are there going to be any changes or documentation updates for Apollo 3?
Looking for volunteers to help with maintenance. Reviewed @fynncfchen solution here and it looks perfect! I guess we need to create example for that https://github.com/aerogear/keycloak-connect-graphql/issues/127#issuecomment-907741906
Ya I can probably help out a bit. I pulled last night and actually started creating transformers and was going to do a PR lol
@wtrocki is it ok to create a PR without unit tests and get some advice on how to implement them for my additions? I have not done unit testing in js/ts before...
No unit tests needed for examples. Anything that will let me to test and review change will be great.
Well doesn't look like the transformers will work with this library since apollo-server-express v3 doesnt have SchemaDirectiveVisitor
class. Should have figured that before hand, so no PR. Will have to be a workaround like above.
@wtrocki added PR
@wtrocki I think we can close this issue now that we have documentation on getting things to work with v3. In the meantime, what are your thoughts on moving this library to provide v3 support out of the lib? I would definitely volunteer to be a maintainer as well.
Has anyone gotten keycloak-connect-graphql working with apollo-server-express 3.0.0. It seems to only work up to apollo-server-express@2.25.2