Universal Auth - Clerk Plugin
Install the with/auth-clerk plugin branch and Pull-Request:
npx @fullproduct/universal-app install with/auth-clerkThis enables universal authentication by adding the @auth/clerk workspace package to your monorepo:
- <ClerkProvider />
- <SignUp />
- <SignIn />
- <UserButton />
- <OrganizationSwitcher />
- useClerk()
- useAuth()
- useUser()
- useSession()
- useOrganization()
- useSocialAuthFlow()
- useSignUpFlow()
- useSignInFlow()
- getMobileAuthHeaders()
Requirements - Clerk Setup
This universal auth plugin requires you to have a Clerk account for your project.
You can create one for free at clerk.com/signup.
At the moment of writing these docs, they have a generous free tier:
- Up to 10k monthly active users
- Won’t charge you for users who sign up but never return
Clerk Env Vars
To connect your app to Clerk, you’ll need to add some environment variables to your .env files:
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_XXXXXXX...On the Next.js side, we need both the publishable key (for the client) and the secret key (for server-side usage):
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_XXXXXXX...
CLERK_SECRET_KEY=sk_test_XXXXXXX... # Only for server-side usageMake sure to never publish or expose your CLERK_SECRET_KEY in any way, such as accidentally including it in the client-side bundle. Luckily, next.js and expo both prevent this by only exposing environment variables prefixed with NEXT_PUBLIC_ or EXPO_PUBLIC_
Unversal Auth - with Clerk
If you merged the plugin from our CLI, then everything should already be set up for you.
But, just to be sure, here’s a quick intro to what that plugin branch does.
Usage of Clerk packages
Our custom @auth/clerk package closes the gaps between Clerk’s own packages for Web, Mobile and the Server:
@clerk/nextjsfor our Next.js Web App + SSR & API routes@clerk/clerk-expofor our React Native Mobile App@clerk/backendfor our Server-side auth checks and middleware
Specifically, the gap between @clerk/nextjs and mobile is closed by recreating similar UI components for Expo, as they’re missing in the @clerk/clerk-expo package:
<SignIn /><SignUp /><UserButton /><OrganizationSwitcher />
Differences on Web vs Mobile
We did have to reduce the api surface of these components a bit on mobile, but the core functionality is there. Some options will just be more configurable on web than on mobile (for now). Better this way, than not at all.
In the near future, we already have plans to close the gap even further and keep up with Clerk’s own evolving APIs.
For more details on what works universally, and what doesn’t, check the individual component docs in the sidebar.
Ultimately, you’ll likely want to craft your own custom auth flows and UIs, for which Clerk has it’s own guides:
Next — /sign-in, /sign-up
In Next.js, merging this plugin branch adds two new pages to @app/next:
- (auth) / sign-in / [[...rest]] / page.tsx
- (auth) / sign-up / [[...rest]] / page.tsx
While both do import from @auth/clerk, the actual SignIn & SignUp implementation on web is exactly the same as in @clerk/nextjs. Meaning there are no restrictions or differences compared to using Clerk directly, e.g:
Expo — /sign-in, /sign-up
As mentioned, the @clerk/clerk-expo package doesn’t provide any pre-built auth screens or UI, so we had to create our own with a smaller API surface.
- (generated) / sign-in / index.tsx
- (generated) / sign-up / index.tsx
- SignInScreen.tsx
- SignUpScreen.tsx
- sign-in / index.native.tsx
- sign-up / index.native.tsx
These screens were built by us using Clerk’s guide for creating custom auth flows:
If you’re wondering about the (generated) and index.native.tsx parts in our fs routing setup:
index.native.tsxmakes sure our scripts only re-export the route to@app/expo(generated)is where our build script re-exports the actual route file to
SignInScreen.tsx and SignUpScreen.tsx hold the actual implementations of the screens for mobile.
- use our custom
<SignIn />and<SignUp />components from@auth/clerk - … which in turn use the hooks from
@clerk/clerk-expo
Cross-platform secure API calls
With this setup, we’re good to log in on all Web, iOS and Android versions of our app.
But signing in alone is not enough. We need to make sure all our requests to our (next.js) back-end are properly authenticated as well.
Next.js Middleware
On the web, Clerk’s auth token is stored through cookies, which we can then read in our next.js middleware:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { createMiddlewareContext } from '@green-stack/utils/apiUtils'
/* --- Route protection ----------------- */
const isProtectedRoute = createRouteMatcher([
// Screen routes
'/dashboard(.*)',
// ...
// API routes
'/api/users(.*)',
'/api/plans(.*)',
// ...
])
/* --- Middleware ----------------------- */
// -i- https://nextjs.org/docs/app/api-reference/functions/next-request
export const middleware = clerkMiddleware(async (auth, req) => {
// Protect routes
if (isProtectedRoute(req)) await auth.protect()
// Get auth context
const authCtx = await auth()
const authToken = await authCtx.getToken()
// Create the request context header (to pass things to API resolvers)
const headerContext: Partial<RequestContext> = {
// ...
userId: authCtx.userId || undefined,
orgId: authCtx.orgId || undefined,
sessionId: authCtx.sessionId || undefined,
authToken: authToken || undefined,
}
// Execute request handler (and pass the request context header)
const res = NextResponse.next({
request: {
headers: await createMiddlewareContext(req, headerContext),
},
})
// Continue by resolving the request
return res
})
/* --- Config --------------------------- */
// -i- https://nextjs.org/docs/14/app/building-your-application/routing/middleware
export const config = {
matcher: [
'/((?!.+\\.[\\w]+$|_next).*)',
'/',
'/(api|graphql|trpc)(.*)',
],
}Mobile Auth Headers
On mobile, we don’t have cookies to rely on, so we should include the auth token in the headers of our API calls.
When merging the with/auth-clerk plugin branch, we already added a utility function to our graphqlQuery() helper to do just that:
- Calls
getMobileAuthHeaders()from@auth/clerkto get the auth token from Clerk - Includes the auth token in the headers of our request to the next.js back-end
Meaning that all API’s, hooks and fetchers crafted through our recommended way of resolving data are already properly authenticated on both Web and Mobile.
If you need to make custom authenticated requests (e.g. A REST endpoint) from mobile, feel free to use getMobileAuthHeaders() as well:
import { getMobileAuthHeaders } from '@auth/clerk'
const authHeaders = await getMobileAuthHeaders()
// -> { Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' }
// -> { } -- if not signed in, or if used in next.js
fetch('https://your-api-endpoint.com/protected', {
method: 'GET',
headers: {
...authHeaders,
'Content-Type': 'application/json',
},
})Serverside auth checks & utils
You’ll typically use @clerk/backend server-side to check the auth token.
For our way of writing data resolvers, we added some helper functions to simplify.
From @auth/clerk/utils/server/clerk, import:
getResolverClerkClient()to lazy importclerkClientso it doesn’t error during SSRgetUser()retrieves a user by their ClerkuserIdgetUserByEmail()retrieves a user by their email addressgetUserOrganisations()takes auserIdand returns their organisationsgetAdminOrganisations()takes auserIdand returns the organisations where they’re an admincreateOrganisation()creates a new organisation for the givenuserId+ (org)namegetOrganisation()retrieves an organisation by itsorganizationIdupdateOrganisation()updates an organisation by itsorganizationId+ (obj)updates
Here’s an example of how to extract auth context in a resolver:
import { getResolverClerkClient } from '@auth/clerk/utils/server/clerk'
/** --- getAuthState() ------------------------------------- */
/** -i- Retrieve the current authentication state */
export const getAuthState = createResolver(async ({ args, context, ... }) => {
// Extract the request context (populated & verified from middleware)
const { userId, orgId, sessionId, authToken } = context
// If anything is missing, middleware kept the context empty (= not authenticated)
if (!userId || !sessionId || !authToken) return { isSignedIn: false }
// Get the Clerk client
const clerkClient = await getResolverClerkClient(context)
// Retrieve the session?
const session = await clerkClient.sessions.verifySession(sessionId)
// Retrieve the user object from clerk?
const user = await clerkClient.users.getUser(userId)
// ...
}, getAuthStateBridge)