Grafbase supports using OpenID Connect as your auth strategy. You can configure your schema with rules based on your signed-in and user group statuses. Grafbase also enables you to restrict allowed operations per rule.
Clerk is a popular choice when it comes to connecting an OIDC provider with Grafbase. In this guide we'll explore using some of the @clerk/nextjs
package functions.
Clerk uses short-lived tokens for added security, and requires you to call getToken()
before every request to get a new token (if required) to pass with requests.
You should read this guide on how to configure Clerk as your identity provider.
This guide assumes you have an existing project with Clerk and Next.js setup. You can use the nextjs-clerk example to follow along if you haven't already got a project.
We'll configure the barebones of Apollo Client to get data flowing. We won't be making any optimizations for SSR, or caching in this guide.
npm install @apollo/client graphql
Then inside of pages/_app.tsx
we'll configure our HttpLink
and pass it our Grafbase API URL:
import { HttpLink } from '@apollo/client'
const httpLink = new HttpLink({
uri: 'YOUR_GRAFBASE_API_URL',
})
Now create a new <ApolloProviderWrapper />
functional component so we can invoke setContext
on our Apollo Client HttpLink
. Inside of this function we'll create and memoize a new ApolloClient
instance and call it client
:
import type { PropsWithChildren } from 'react'
import { useMemo } from 'react'
import {
ApolloClient,
ApolloProvider,
InMemoryCache,
from,
} from '@apollo/client'
export const ApolloProviderWrapper = ({ children }: PropsWithChildren) => {
const client = useMemo(() => {
return new ApolloClient({
link: from([httpLink]),
cache: new InMemoryCache(),
})
}, [])
return <ApolloProvider client={client}>{children}</ApolloProvider>
}
You'll also notice above that we're passing an array to from
for our ApolloClient link
. We need to do this so we can create a single ApolloLink that is built up of the context we'll pass it next.
We'll next import setContext
from @apollo/client
. With this we can create authMiddleware
that we then pass to from
. The last link httpLink
is what executes the GraphQL request to Grafbase.
import { setContext } from '@apollo/client/link/context'
export const ApolloProviderWrapper = ({ children }: PropsWithChildren) => {
const client = useMemo(() => {
const authMiddleware = setContext(async (operation, { headers }) => {
return {
headers: {
...headers,
authorization: `Bearer TOKEN_HERE`,
},
}
})
return new ApolloClient({
link: from([authMiddleware, httpLink]),
cache: new InMemoryCache(),
})
}, [])
return <ApolloProvider client={client}>{children}</ApolloProvider>
}
Finally we'll return from inside of authMiddleware
our new request headers with our token
from Clerk! You'll need to pass it a "template" so it can create the correct JWT that includes any groups you belong to for authorization purposes.
Clerk manages caching and refreshing the state of getToken
. We'll want to memoize getToken
to prevent unnecessary HTTP requests to Clerk when the token is still valid.
import { useAuth } from '@clerk/nextjs'
export const ApolloProviderWrapper = ({ children }: PropsWithChildren) => {
const { getToken } = useAuth()
const client = useMemo(() => {
const authMiddleware = setContext(async (operation, { headers }) => {
const token = await getToken({ template: 'grafbase' })
return {
headers: {
...headers,
authorization: `Bearer ${token}`,
},
}
})
return new ApolloClient({
link: from([authMiddleware, httpLink]),
cache: new InMemoryCache(),
})
}, [getToken])
return <ApolloProvider client={client}>{children}</ApolloProvider>
}
Don't forget to add getToken
to the dependencies of useMemo
.
Now where you would normally use <ApolloProvider />
to wrap your application... Just use <ApolloProviderWrapper />
and it will take care of passing the correct token from Clerk to Grafbase!
Let's now look at what this looks like with a custom _app.tsx
component for a typical Next.js application.
We'll make sure the following scenarios are covered:
- Public pages are accessible without logging in, and have GraphQL context
- Signed in users can access protected pages
- Signed out users that attempt to access protected pages are redirected to sign-in
import {
ClerkLoaded,
ClerkProvider,
RedirectToSignIn,
SignedIn,
SignedOut,
} from '@clerk/nextjs'
const publicPages = [
'/',
'/about-us',
'/privacy-policy',
'/sign-in/[[...index]]',
'/sign-up/[[...index]]',
]
const MyApp = ({ Component, pageProps, router }) => {
return (
<ClerkProvider {...pageProps}>
<ClerkLoaded>
<ApolloProviderWrapper>
{publicPages.includes(router.pathname) ? (
<Component {...pageProps} />
) : (
<Fragment>
<SignedIn>
<Component {...pageProps} />
</SignedIn>
<SignedOut>
<RedirectToSignIn />
</SignedOut>
</Fragment>
)}
</ApolloProviderWrapper>
</ClerkLoaded>
</ClerkProvider>
)
}
This next step will depend on your schema, and rules. For the purposes of this next step, we'll use the following schema and rules:
schema
@auth(
providers: [{ type: oidc, issuer: "{{ env.CLERK_ISSUER_URL }}" }]
rules: [
{ allow: private, operations: [read] }
{ allow: groups, groups: ["admin"], operations: [delete] }
]
) {
query: Query
}
type Post @model {
title: String!
comments: [Comment!]!
}
type Comment @model {
message: String!
post: Post!
}
The above schema allows any signed-in user to read
data, this includes fetching a collection, by id or unique field.
We'll now import gql
and useQuery
from @apollo/client
to execute our first GraphQL operation. We'll write a query in this example, but this could also be a mutation.
import { gql, useQuery } from '@apollo/client'
const QUERY = gql`
{
postCollection(first: 10) {
edges {
node {
id
title
}
}
}
}
`
const MyPage = () => {
const { data, loading } = useQuery(QUERY)
if (loading) return <p>Loading!</p>
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
export default MyPage
You'll notice the request to Grafbase is unauthorized if you aren't signed in. However, if you sign in to your application, the query to read
data will be allowed!
It doesn't matter if you have any data in your project as the request will be allowed or denied based on the token rules.