This is a ported and improved version of Remix Auth, it is using Solid Sessions to manage the user sessions / cookies - based on Solid Start
It is highly recommended to start using next auth instead, it is more secured and has much more providers.
- @solid-auth/core - Core package where the logic, types and strategies are defined
- @solid-auth/oauth2 - OAuth2 strategy based on the
@solid-auth/core
strategy - @solid-auth/socials - OAuth2 socials providers (like discord, github, google, etc) based on the
@solid-auth/oauth2
strategy - @solid-auth/auth0 - Auth0 strategy based on the
@solid-auth/core
and@solid-auth/oauth2
strategy - @solid-auth/credentials - Use your own conditions, just return the user to the strategy and we will handle the rest for you.
- @solid-auth/client - Client side package that will allow you to get the current session from the client side.
It is recommended to use Create JD App to create your app, it will create a new app with all the necessary packages and configurations, so you won't need to worry about anything.
npm i @solid-auth/core
# if you want to use social providers
npm i @solid-auth/socials
As mentioned previously, @soid-auth/core
is based on Solid Sessions, so first you need to create a sessionStorage:
// src/utils/auth.ts
import { createCookieSessionStorage } from 'solid-start'
import { createSolidAuthClient } from '@solid-auth/core'
const getBaseUrl = () => {
if (typeof window !== 'undefined') return ''
return `http://localhost:${process.env.PORT ?? 3000}`
}
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: '_session',
secrets: ['your-secret'],
secure: true,
maxAge: 60 * 60 * 24 * 30,
},
})
// Create the Solid Auth Client So You Can Actually Manage The User From The Client Side
export const authClient = createSolidAuthClient(`${getBaseUrl()}/api/auth`)
Now that you have a session you can create the actual authenticator:
// src/server/auth.ts
import { type User } from '@prisma/client' // the type of your user
import { DiscordStrategy } from '@solid-auth/socials' // or any other provider
import { serverEnv } from '~/env/server' // type safed process.env, doesn't really matter
import { sessionStorage } from '~/utils/auth' // the sessionStorage we created before
import { prisma } from './db/client' // or any other orm you wish to use
import { Authenticator } from '@solid-auth/core'
export const authenticator = new Authenticator<User>(sessionStorage).use(
new DiscordStrategy(
{
clientID: serverEnv.DISCORD_CLIENT_ID,
clientSecret: serverEnv.DISCORD_CLIENT_SECRET,
// SITE_URL should be set to: http://localhost:3000 locally and https://yourdomain.com in production
callbackURL: serverEnv.SITE_URL + '/api/auth/discord/callback',
},
async ({ profile }) => {
let user = await prisma.user.findUnique({
where: {
id: profile.id,
},
})
if (!user) {
user = await prisma.user.create({
data: {
id: profile.id,
displayName: profile.__json.username,
avatar: profile.photos[0].value,
},
})
}
return user
}
)
)
But just because we created the authenticator doesn't mean it will work, we need to create the routes to actually handle the authentication:
// src/routes/api/auth/[...solidauth].ts
import { authenticator } from '~/server/auth' // the authenticator we created before
import { type User } from '@prisma/client' // the type of your user
import { createSolidAuthHandler } from '@solid-auth/core'
const handler = createSolidAuthHandler<User>(authenticator)
export const POST = handler // handle the login, logout etc
export const GET = handler // handle the callback
Now that we have the routes to handle the authentication, we need to make the requests to them, we can do that using the authClient
we created before:
import { authClient } from '~/utils/auth' // the authClient we created before
// Sign In
authClient.login('discord' /* or any other provider*/, {
successRedirect: '/', // where to redirect the user after the login
failureRedirect: '/account', // where to redirect the user if the login failed
})
// Sign Out
authClient.logout({ redirectTo: '/account' })
// src/routes/example.tsx
import { useRouteData } from "solid-start";
import { createServerData$ } from "solid-start/server";
import { authenticator } from '~/server/auth' // the authenticator we created before
export const routeData = () => {
return createServerData$(async (_, { request }) => {
return await authenticator.isAuthenticated(request);
});
};
export default function Example() {
const user = useRouteData<typeof routeData>(); // resource with type User | null | undefined
return (
{...}
);
}
// src/layouts/Protected.tsx
import { Match, Switch, type Component } from 'solid-js'
import { useRouteData } from 'solid-start'
import { createServerData$, redirect } from 'solid-start/server'
import { authenticator } from '~/server/auth' // the authenticator we created before
import { type User } from '@prisma/client' // the type of your user
export const withProtected = (Component: ProtectedRouter) => {
const routeData = () => {
return createServerData$(async (_, { request }) => {
const user = await authenticator.isAuthenticated(request) // check if user session exists
if (!user) {
// if not login redirect to login page
throw redirect('/login') // we use throw and not return because we dont't want to mess the return type
}
return user
})
}
return {
routeData,
Page: () => {
const current = useRouteData<typeof routeData>()
return (
// not loading, and current is not instance of Response, the user is logged in - we should render the page
<Switch fallback={<Component {...(current() as User)} />}>
{/* if current() is instance of Response, meaning that the user is being redirected to the login page */}
<Match when={current.loading || current() instanceof Response}>
<p>Loading</p>
</Match>
</Switch>
)
},
}
}
export type ProtectedRouter = Component<User>
// src/routes/protected.tsx
import { withProtected } from '../layouts/Protected' // the protected layout we created before
// we export the routeData to get the useRouteData to work
export const { routeData, Page } = withProtected((user) => {
// type safed user, no need to use ?. anymore
return <h1>Hey {user.displayName}</h1>
})
export default Page // The page should be default exported, we export `Page` above because we want to export the routeData and its just cleaner
Thank you Marco for converting this app into a monorepo and maintaining it.