Compare commits

..

3 Commits

Author SHA1 Message Date
Balázs Orbán
079fbd27fa update in core 2022-12-31 08:51:19 +01:00
Balázs Orbán
c7101981bc Merge branch 'main' into fix/callbackJwtDocstringUpdate 2022-12-31 08:49:40 +01:00
=
f7b052a5fd fix(core): update CallbacksOptions.jwt docstring
Change description to reflect that JWT is encrypted by default
2022-11-15 19:43:39 +01:00
72 changed files with 341 additions and 2666 deletions

10
.github/sync.yml vendored
View File

@@ -7,16 +7,6 @@ nextauthjs/sveltekit-auth-example:
- .github/FUNDING.yml
- LICENSE
# FIXME: Should re-enable, but currently fails:
# https://github.com/nextauthjs/next-auth/actions/runs/3811709391/jobs/6484533340
# (issue seems to be the name of the target repo)
# nextauthjs/solid-start-auth-example:
# - source: "apps/examples/solid-start"
# dest: .
# deleteOrphaned: true
# - .github/FUNDING.yml
# - LICENSE
nextauthjs/next-auth-gatsby-example:
- source: apps/playgrounds/gatsby
dest: .

View File

@@ -3,10 +3,9 @@ import { authOptions } from "./api/auth/[...nextauth]"
import Layout from "../components/layout"
import type { GetServerSidePropsContext } from "next"
import { useSession } from "next-auth/react"
import type { Session } from "next-auth"
export default function ServerSidePage() {
const { data: session } = useSession()
export default function ServerSidePage({ session }: { session: Session }) {
// As this page uses Server Side Rendering, the `session` will be already
// populated on render without needing to go through a loading stage.
return (

View File

@@ -1,3 +0,0 @@
GITHUB_ID=
GITHUB_SECRET=
AUTH_SECRET=

View File

@@ -1,27 +0,0 @@
dist
.solid
.output
.vercel
.netlify
netlify
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
*.launch
.settings/
# Temp
gitignore
# System Files
.DS_Store
Thumbs.db
.env
.vercel

View File

@@ -1,37 +0,0 @@
# Create JD App
This project was created using [Create JD App](https://github.com/OrJDev/create-jd-app)
## Deploying To Vercel
### Installing
```bash
npm install solid-start-vercel@latest -D
```
### Adding to vite config
```ts
import solid from "solid-start/vite";
import dotenv from "dotenv";
import { defineConfig } from "vite";
// @ts-expect-error no typing
import vercel from "solid-start-vercel";
export default defineConfig(() => {
dotenv.config();
return {
plugins: [solid({ ssr: true, adapter: vercel({ edge: false }) })],
};
});
```
### Enviroment Variables
- `ENABLE_VC_BUILD`=`1` .
### You Are Done
Create a github repo and push your code to it, then deploy it to vercel (:

View File

@@ -1,32 +0,0 @@
{
"name": "my-app",
"scripts": {
"dev": "solid-start dev",
"build": "solid-start build",
"start": "solid-start start",
"lint": "eslint --fix \"**/*.{ts,tsx,js,jsx}\""
},
"type": "module",
"devDependencies": {
"autoprefixer": "^10.4.13",
"postcss": "^8.4.19",
"solid-start-node": "^0.2.9",
"solid-start-vercel": "^0.2.9",
"tailwindcss": "^3.2.4",
"typescript": "^4.8.3",
"vite": "^3.1.0"
},
"dependencies": {
"@auth/core": "^0.1.4",
"@solid-auth/next": "^0.0.19",
"@solidjs/meta": "^0.28.0",
"@solidjs/router": "^0.6.0",
"solid-js": "^1.5.7",
"solid-start": "^0.2.9",
"undici": "5.11.0",
"zod": "^3.19.1"
},
"engines": {
"node": ">=16"
}
}

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

View File

@@ -1,72 +0,0 @@
import { Match, Show, Switch, type Component } from "solid-js";
import { createServerData$ } from "solid-start/server";
import { authOpts } from "~/routes/api/auth/[...solidauth]";
import { signIn, signOut } from "@solid-auth/next/client";
import { getSession } from "@solid-auth/next";
import { A } from "solid-start";
interface INavBarProps {}
const NavBar: Component<INavBarProps> = () => {
const session = useSession();
return (
<header class="flex flex-col w-full gap-2 fixed left-2/4 right-2/4 -translate-x-2/4 items-center">
<nav class="w-[70vw] sm:w-2/4 lg:w-[40%] p-5 bg-[#0000000d] flex items-center justify-between rounded-lg">
<Show
when={session()?.user}
keyed
fallback={
<>
<p class="text-lg font-semibold">You are not signed in</p>
<button
class="p-2.5 rounded-lg bg-[#346df1] text-white text-lg font-bold flex items-center justify-center"
onClick={() => signIn("github")}
>
Sign in
</button>
</>
}
>
{(us) => (
<>
<div class="flex gap-2 items-center">
<Show when={us.image} keyed>
{(im) => <img src={im} class="w-12 h-12 rounded-full" />}
</Show>
<div class="flex flex-col">
<h3 class="font-bold text-lg">Signed in as</h3>
<p class="text-lg font-semibold">{us.name}</p>
</div>
</div>
<button
onClick={() => signOut()}
class="text-[#555] font-semibold underline"
>
Sign out
</button>
</>
)}
</Show>
</nav>
<div class="flex gap-2 items-center">
<A class="text-blue-500 font-bold underline" href="/">
Home
</A>
<A class="text-blue-500 font-bold underline" href="/protected">
Protected
</A>
</div>
</header>
);
};
export default NavBar;
export const useSession = () => {
return createServerData$(
async (_, { request }) => {
return await getSession(request, authOpts);
},
{ key: () => ["auth_user"] }
);
};

View File

@@ -1 +0,0 @@
export { default } from "./NavBar";

View File

@@ -1,37 +0,0 @@
import { type Session } from "@auth/core";
import { getSession } from "@solid-auth/next";
import { Component, Show } from "solid-js";
import { useRouteData } from "solid-start";
import { createServerData$, redirect } from "solid-start/server";
import { authOpts } from "~/routes/api/auth/[...solidauth]";
const Protected = (Comp: IProtectedComponent) => {
const routeData = () => {
return createServerData$(
async (_, event) => {
const session = await getSession(event.request, authOpts);
if (!session || !session.user) {
throw redirect("/");
}
return session;
},
{ key: () => ["auth_user"] }
);
};
return {
routeData,
Page: () => {
const session = useRouteData<typeof routeData>();
return (
<Show when={session()} keyed>
{(sess) => <Comp {...sess} />}
</Show>
);
},
};
};
type IProtectedComponent = Component<Session>;
export default Protected;

View File

@@ -1 +0,0 @@
export { default } from "./Protected";

View File

@@ -1,2 +0,0 @@
export { default as NavBar } from "./NavBar";
export { default as Protected } from "./Protected";

View File

@@ -1,3 +0,0 @@
import { mount, StartClient } from "solid-start/entry-client";
mount(() => <StartClient />, document);

View File

@@ -1,9 +0,0 @@
import {
StartServer,
createHandler,
renderAsync,
} from "solid-start/entry-server";
export default createHandler(
renderAsync((event) => <StartServer event={event} />)
);

View File

@@ -1,24 +0,0 @@
import type { ZodFormattedError } from "zod";
import { clientScheme } from "./schema";
export const formatErrors = (
errors: ZodFormattedError<Map<string, string>, string>
) =>
Object.entries(errors)
.map(([name, value]) => {
if (value && "_errors" in value)
return `${name}: ${value._errors.join(", ")}\n`;
})
.filter(Boolean);
const env = clientScheme.safeParse(import.meta.env);
if (env.success === false) {
console.error(
"❌ Invalid environment variables:\n",
...formatErrors(env.error.format())
);
throw new Error("Invalid environment variables");
}
export const clientEnv = env.data;

View File

@@ -1,15 +0,0 @@
import { z } from "zod";
export const serverScheme = z.object({
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
GITHUB_ID: z.string(),
GITHUB_SECRET: z.string(),
AUTH_SECRET: z.string(),
NEXTAUTH_URL: z.string().optional(),
});
export const clientScheme = z.object({
MODE: z.enum(["development", "production", "test"]).default("development"),
});

View File

@@ -1,24 +0,0 @@
import { serverScheme } from "./schema";
import type { ZodFormattedError } from "zod";
export const formatErrors = (
errors: ZodFormattedError<Map<string, string>, string>
) =>
Object.entries(errors)
.map(([name, value]) => {
if (value && "_errors" in value)
return `${name}: ${value._errors.join(", ")}\n`;
})
.filter(Boolean);
const env = serverScheme.safeParse(process.env);
if (env.success === false) {
console.error(
"❌ Invalid environment variables:\n",
...formatErrors(env.error.format())
);
throw new Error("Invalid environment variables");
}
export const serverEnv = env.data;

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,40 +0,0 @@
// @refresh reload
import "./root.css";
import { Suspense } from "solid-js";
import {
Body,
ErrorBoundary,
FileRoutes,
Head,
Html,
Meta,
Routes,
Scripts,
Title,
} from "solid-start";
import { NavBar } from "./components";
export default function Root() {
return (
<Html lang="en">
<Head>
<Title>Create JD App</Title>
<Meta charset="utf-8" />
<Meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<Body>
<Suspense>
<NavBar />
<div class="py-44 px-8">
<ErrorBoundary>
<Routes>
<FileRoutes />
</Routes>
</ErrorBoundary>
</div>
</Suspense>
<Scripts />
</Body>
</Html>
);
}

View File

@@ -1,16 +0,0 @@
import { SolidAuth, type SolidAuthConfig } from "@solid-auth/next";
import GitHub from "@auth/core/providers/github";
import { serverEnv } from "~/env/server";
import { type APIEvent } from "solid-start";
export const authOpts: SolidAuthConfig = {
providers: [
GitHub({
clientId: serverEnv.GITHUB_ID,
clientSecret: serverEnv.GITHUB_SECRET,
}),
],
debug: false,
};
export const { GET, POST } = SolidAuth(authOpts);

View File

@@ -1,44 +0,0 @@
import { type ParentComponent } from "solid-js";
import { A, Title, useRouteData } from "solid-start";
import { createServerData$ } from "solid-start/server";
import { authOpts } from "./api/auth/[...solidauth]";
import { getSession } from "@solid-auth/next";
export const routeData = () => {
return createServerData$(
async (_, { request }) => {
return await getSession(request, authOpts);
},
{ key: () => ["auth_user"] }
);
};
const Home: ParentComponent = () => {
const user = useRouteData<typeof routeData>();
return (
<>
<Title>Create JD App</Title>
<div class="flex flex-col gap-2 items-center">
<h1 class="text-4xl font-bold">SolidStart Auth Example</h1>
<p class="font-semibold text-md max-w-[40rem]">
This is an example site to demonstrate how to use{" "}
<A
href="https://start.solidjs.com/getting-started/what-is-solidstart"
class="text-blue-500 underline font-bold"
>
SolidStart
</A>{" "}
with{" "}
<A
href="https://authjs.dev/reference/solidstart"
class="text-blue-500 underline font-bold"
>
SolidStart Auth
</A>{" "}
for authentication.
</p>
</div>
</>
);
};
export default Home;

View File

@@ -1,11 +0,0 @@
import { Protected } from "~/components";
export const { routeData, Page } = Protected((session) => {
return (
<main class="flex flex-col gap-2 items-center">
<h1>This is a proteced route</h1>
</main>
);
});
export default Page;

View File

@@ -1,8 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"jsxImportSource": "solid-js",
"jsx": "preserve",
"types": ["vite/client"],
"baseUrl": "./",
"paths": {
"~/*": ["./src/*"]
}
}
}

View File

@@ -1,10 +0,0 @@
import solid from "solid-start/vite";
import { defineConfig } from "vite";
// @ts-expect-error no typings
import vercel from "solid-start-vercel";
export default defineConfig(() => {
return {
plugins: [solid({ ssr: true, adapter: vercel({ edge: false }) })],
};
});

View File

@@ -14,7 +14,7 @@ We know, authentication is hard. Is a rabbit hole and it's easy to get lost on i
The easiest way is to setup Auth.js with an [OAuth](https://en.wikipedia.org/wiki/OAuth) provider. In this tutorial we'll be setting Auth.js in a **Next.js app** to be able to login with **Github**.
:::info
Auth.js comes with a long list of [built-in providers](/reference/providers/oauth-builtin) (Google, Facebook, Twitter, etc...) you can also integrate it with your own OAuth service easily by [building a custom provider](/guides/providers/custom-provider). Auth.js can integrate as well with other frameworks like SvelteKit, SolidStart and Gatsby.
Auth.js comes with a long list of [built-in providers](/reference/providers/oauth-builtin) (Google, Facebook, Twitter, etc...) you can also integrate it with your own OAuth service easily by [building a custom provider](/guides/providers/custom-provider). Auth.js can integrate as well with other frameworks like SvelteKit and Gatsby.
:::
## 1. Configuring Auth.js

View File

@@ -2,203 +2,119 @@
title: Refresh token rotation
---
Refresh token rotation is the practice of updating an `access_token` on behalf of the user, without requiring interaction (eg.: re-sign in). `access_token`s are usually issued for a limited time. After they expire, the service verifying them will ignore the value. Instead of asking the user to sign in again to obtain a new `access_token`, certain providers support exchanging a `refresh_token` for a new `access_token`, renewing the expiry time. Let's see how this can be achieved.
While Auth.js doesn't automatically handle access token rotation for [OAuth providers](/reference/providers/oauth-builtin) yet, this functionality can be implemented using [callbacks](/guides/basics/callbacks).
:::note
Our goal is to add zero-config support for built-in providers eventually. Let us know if you would like to help.
:::
## Source Code
A working example can be accessed [here](https://github.com/nextauthjs/next-auth-refresh-token-example).
## Implementation
First, make sure that the provider you want to use supports `refresh_token`'s. Check out [The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749#section-6) spec for more details.
### Server Side
Depending on the session strategy, `refresh_token` can be persisted either in a database, or in a cookie, in an encrypted JWT.
:::info
Using a JWT to store the `refresh_token` is less secure than saving it in a database, and you need to evaluate based on your requirements which strategy you choose.
:::
#### JWT strategy
Using the [jwt](../../reference/03-core/interfaces/types.CallbacksOptions.md#jwt) and [session](../../reference/03-core/interfaces/types.CallbacksOptions.md#session) callbacks, we can persist OAuth tokens and refresh them when they expire.
Using a [JWT callback](https://authjs.dev/guides/basics/callbacks#jwt-callback) and a [session callback](https://authjs.dev/guides/basics/callbacks#session-callback), we can persist OAuth tokens and refresh them when they expire.
Below is a sample implementation using Google's Identity Provider. Please note that the OAuth 2.0 request in the `refreshAccessToken()` function will vary between different providers, but the core logic should remain similar.
```ts
import { Auth } from "@auth/core"
import { type TokenSet } from "@auth/core/types"
import Google from "@auth/core/providers/google"
```js title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
export default Auth(new Request("https://example.com"), {
const GOOGLE_AUTHORIZATION_URL =
"https://accounts.google.com/o/oauth2/v2/auth?" +
new URLSearchParams({
prompt: "consent",
access_type: "offline",
response_type: "code",
})
/**
* Takes a token, and returns a new token with updated
* `accessToken` and `accessTokenExpires`. If an error occurs,
* returns the old token and an error property
*/
async function refreshAccessToken(token) {
try {
const url =
"https://oauth2.googleapis.com/token?" +
new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
})
const response = await fetch(url, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
})
const refreshedTokens = await response.json()
if (!response.ok) {
throw refreshedTokens
}
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_at * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
}
} catch (error) {
console.log(error)
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
export default NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
authorization: { params: { access_type: "offline", prompt: "consent" } },
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: GOOGLE_AUTHORIZATION_URL,
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) {
// Save the access token and refresh token in the JWT on the initial login
async jwt({ token, user, account }) {
// Initial sign in
if (account && user) {
return {
access_token: account.access_token,
expires_at: Date.now() + account.expires_in * 1000,
refresh_token: account.refresh_token,
}
} else if (Date.now() < token.expires_at) {
// If the access token has not expired yet, return it
return token
} else {
// If the access token has expired, try to refresh it
try {
// https://accounts.google.com/.well-known/openid-configuration
// We need the `token_endpoint`.
const response = await fetch("https://oauth2.googleapis.com/token", {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.GOOGLE_ID,
client_secret: process.env.GOOGLE_SECRET,
grant_type: "refresh_token",
refresh_token: token.refresh_token,
}),
method: "POST",
})
const tokens: TokenSet = await response.json()
if (!response.ok) throw tokens
return {
...token, // Keep the previous token properties
access_token: tokens.access_token,
expires_at: Date.now() + tokens.expires_in * 1000,
// Fall back to old refresh token, but note that
// many providers may only allow using a refresh token once.
refresh_token: tokens.refresh_token ?? token.refresh_token,
}
} catch (error) {
console.error("Error refreshing access token", error)
// The error property will be used client-side to handle the refresh token error
return { ...token, error: "RefreshAccessTokenError" as const }
accessToken: account.access_token,
accessTokenExpires: Date.now() + account.expires_at * 1000,
refreshToken: account.refresh_token,
user,
}
}
// Return previous token if the access token has not expired yet
if (Date.now() < token.accessTokenExpires) {
return token
}
// Access token has expired, try to update it
return refreshAccessToken(token)
},
async session({ session, token }) {
session.user = token.user
session.accessToken = token.accessToken
session.error = token.error
return session
},
},
})
declare module "@auth/core/types" {
interface Session {
error?: "RefreshAccessTokenError"
}
}
declare module "@auth/core/jwt" {
interface JWT {
access_token: string
expires_at: number
refresh_token: string
error?: "RefreshAccessTokenError"
}
}
```
#### Database strategy
Using the database strategy is very similar, but instead of preserving the `access_token` and `refresh_token`, we save it, well, in the database.
```ts
import { Auth } from "@auth/core"
import { type TokenSet } from "@auth/core/types"
import Google from "@auth/core/providers/google"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export default Auth(new Request("https://example.com"), {
adapter: PrismaAdapter(prisma),
providers: [
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
authorization: { params: { access_type: "offline", prompt: "consent" } },
}),
],
callbacks: {
async session({ session, user }) {
const [google] = await prisma.account.findMany({
where: { userId: user.id, provider: "google" },
})
if (google.expires_at >= Date.now()) {
// If the access token has expired, try to refresh it
try {
// https://accounts.google.com/.well-known/openid-configuration
// We need the `token_endpoint`.
const response = await fetch("https://oauth2.googleapis.com/token", {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.GOOGLE_ID,
client_secret: process.env.GOOGLE_SECRET,
grant_type: "refresh_token",
refresh_token: google.refresh_token,
}),
method: "POST",
})
const tokens: TokenSet = await response.json()
if (!response.ok) throw tokens
await prisma.account.update({
data: {
access_token: tokens.access_token,
expires_at: Date.now() + tokens.expires_in * 1000,
refresh_token: tokens.refresh_token ?? google.refresh_token,
},
where: {
provider_providerAccountId: {
provider: "google",
providerAccountId: google.providerAccountId,
},
},
})
} catch (error) {
console.error("Error refreshing access token", error)
// The error property will be used client-side to handle the refresh token error
session.error = "RefreshAccessTokenError"
}
}
return session
},
},
})
declare module "@auth/core/types" {
interface Session {
error?: "RefreshAccessTokenError"
}
}
declare module "@auth/core/jwt" {
interface JWT {
access_token: string
expires_at: number
refresh_token: string
error?: "RefreshAccessTokenError"
}
}
```
### Client Side
The `RefreshAccessTokenError` error that is caught in the `refreshAccessToken()` method is passed to the client. This means that you can direct the user to the sign-in flow if we cannot refresh their token.
The `RefreshAccessTokenError` error that is caught in the `refreshAccessToken()` method is passed all the way to the client. This means that you can direct the user to the sign in flow if we cannot refresh their token.
We can handle this functionality as a side effect:
@@ -218,8 +134,3 @@ const HomePage() {
return (...)
}
```
## Source Code
A working example can be accessed [here](https://github.com/nextauthjs/next-auth-refresh-token-example).

View File

@@ -1,14 +1,14 @@
---
title: Available OAuth providers
sidebar_label: OAuth providers
sidebar_label: Oauth providers
---
Authentication Providers in **Auth.js** are services that can be used to sign a user in.
Authentication Providers in **Auth.js** are services that can be used to sign in a user.
Auth.js comes with a set of built-in providers. You can find them [here](https://github.com/nextauthjs/next-auth/tree/main/packages/core/src/providers). Each built-in provider has its own documentation page:
:::note
Auth.js supports any **2.x** and **OpenID Connect (OIDC)** compliant providers and has built-in support for the most popular services.
Auth.js is designed to work with any OAuth service, it supports **OAuth 1.0**, **1.0A**, **2.0** and **OpenID Connect (OIDC)** and has built-in support for most popular sign-in services.
:::
<ul>

View File

@@ -1,18 +0,0 @@
---
title: Client
---
## Signing in
```ts
import { signIn } from "@auth/solid-start/client"
signIn()
signIn("provider") // example: signIn("github")
```
## Signing out
```ts
import { signOut } from "@auth/solid-start/client"
signOut()
```

View File

@@ -1,76 +0,0 @@
---
title: SolidStart Auth
---
# Getting started
Recommended to use [create-jd-app](https://github.com/OrJDev/create-jd-app)
```bash
npm install @auth/solid-start@latest @auth/core@latest
```
## Setting It Up
[Generate auth secret](https://generate-secret.vercel.app/32), then set it as an environment variable:
```
AUTH_SECRET=your_auth_secret
```
## Creating the api handler
in this example we are using github so make sure to set the following environment variables:
```
GITHUB_ID=your_github_oatuh_id
GITHUB_SECRET=your_github_oatuh_secret
```
```ts
// routes/api/auth/[...solidauth].ts
import { SolidAuth, type SolidAuthConfig } from "@auth/solid-start"
import GitHub from "@auth/core/providers/github"
export const authOpts: SolidAuthConfig = {
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
debug: false,
}
export const { GET, POST } = SolidAuth(authOpts)
```
## Signing in and out
```ts
import { signIn, signOut } from "@auth/solid-start/client"
const login = () => signIn("github")
const logout = () => signOut()
```
## Getting the current session
```ts
import { getSession } from "@auth/solid-start"
import { createServerData$ } from "solid-start/server"
import { authOpts } from "~/routes/api/auth/[...solidauth]"
export const useSession = () => {
return createServerData$(
async (_, { request }) => {
return await getSession(request, authOpts)
},
{ key: () => ["auth_user"] }
)
}
// useSession returns a resource:
const session = useSession()
const loading = session.loading
const user = () => session()?.user
```

View File

@@ -1,119 +0,0 @@
---
title: Protected
---
# Protected Routes
## When Using SSR
When using SSR, I recommend creating a `Protected` component that will trigger suspense using the `Show` component. It should look like this:
```tsx
// components/Protected.tsx
import { type Session } from "@auth/core";
import { getSession } from "@auth/solid-start";
import { Component, Show } from "solid-js";
import { useRouteData } from "solid-start";
import { createServerData$, redirect } from "solid-start/server";
import { authOpts } from "~/routes/api/auth/[...solidauth]";
const Protected = (Comp: IProtectedComponent) => {
const routeData = () => {
return createServerData$(
async (_, event) => {
const session = await getSession(event.request, authOpts);
if (!session || !session.user) {
throw redirect("/");
}
return session;
},
{ key: () => ["auth_user"] }
);
};
return {
routeData,
Page: () => {
const session = useRouteData<typeof routeData>();
return (
<Show when={session()} keyed>
{(sess) => <Comp {...sess} />}
</Show>
);
},
};
};
type IProtectedComponent = Component<Session>;
export default Protected;
```
It can be used like this:
```tsx
// routes/protected.tsx
import Protected from "~/components/Protected";
export const { routeData, Page } = Protected((session) => {
return (
<main class="flex flex-col gap-2 items-center">
<h1>This is a proteced route</h1>
</main>
);
});
export default Page;
```
## When Using CSR
When using CSR, the `Protected` component will not work as expected and will cause the screen to flash, so I had to come up with a tricky solution, we will use a Solid-Start middleare:
```tsx
// entry-server.tsx
import { Session } from "@auth/core";
import { getSession } from "@auth/solid-start";
import { redirect } from "solid-start";
import {
StartServer,
createHandler,
renderAsync,
} from "solid-start/entry-server";
import { authOpts } from "./routes/api/auth/[...solidauth]";
const protectedPaths = ["/protected"]; // add any route you wish in here
export default createHandler(
({ forward }) => {
return async (event) => {
if (protectedPaths.includes(new URL(event.request.url).pathname)) {
const session = await getSession(event.request, authOpts);
if (!session) {
return redirect("/");
}
}
return forward(event);
};
},
renderAsync((event) => <StartServer event={event} />)
);
```
And now you can easily create a protected route:
```tsx
// routes/protected.tsx
export default () => {
return (
<main class="flex flex-col gap-2 items-center">
<h1>This is a proteced route</h1>
</main>
);
};
```
**Note: the CSR method should also work when using SSR, the SSR method shouldn't work when using CSR**

View File

@@ -11,7 +11,7 @@ http://developers.strava.com/docs/reference/
The **Strava Provider** comes with a set of default options:
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.ts)
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.js)
You can override any of the options to suit your own use case.

View File

@@ -3,7 +3,7 @@ id: dynamodb
title: DynamoDB
---
This is the AWS DynamoDB Adapter for `next-auth`. This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
This is the AWS DynamoDB Adapter for next-auth. This package can only be used in conjunction with the primary next-auth package. It is not a standalone package.
By default, the adapter expects a table with a partition key `pk` and a sort key `sk`, as well as a global secondary index named `GSI1` with `GSI1PK` as partition key and `GSI1SK` as sorting key. To automatically delete sessions and verification requests after they expire using [dynamodb TTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) you should [enable the TTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/time-to-live-ttl-how-to.html) with attribute name 'expires'. You can set whatever you want as the table name and the billing method.
@@ -11,10 +11,10 @@ You can find the full schema in the table structure section below.
## Getting Started
1. Install `next-auth`, `@next-auth/dynamodb-adapter`, `@aws-sdk/client-dynamodb` and `@aws-sdk/lib-dynamodb`
1. Install `next-auth` and `@next-auth/dynamodb-adapter`
```bash npm2yarn2pnpm
npm install next-auth @next-auth/dynamodb-adapter @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
```bash npm2yarn
npm install next-auth @next-auth/dynamodb-adapter
```
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
@@ -23,7 +23,7 @@ You need to pass `DynamoDBDocument` client from the modular [`aws-sdk`](https://
The default table name is `next-auth`, but you can customise that by passing `{ tableName: 'your-table-name' }` as the second parameter in the adapter.
```javascript title="pages/api/auth/[...nextauth].js"
import { DynamoDB, DynamoDBClientConfig } from "@aws-sdk/client-dynamodb"
import { DynamoDB } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
@@ -73,7 +73,7 @@ The table respects the single table design pattern. This has many advantages:
- Only one table to manage, monitor and provision.
- Querying relations is faster than with multi-table schemas (for eg. retrieving all sessions for a user).
- Only one table needs to be replicated if you want to go multi-region.
- Only one table needs to be replicated, if you want to go multi-region.
> This schema is adapted for use in DynamoDB and based upon our main [schema](/reference/adapters/models)
@@ -94,7 +94,7 @@ new dynamodb.Table(this, `NextAuthTable`, {
})
```
Alternatively, you can use this cloudformation template:
Alternatively you can use this cloudformation template:
```yaml title=cloudformation.yaml
NextAuthTable:

View File

@@ -18,7 +18,7 @@ sidebar_position: 0
- Next.js
- SvelteKit
- SolidStart
- SolidState
- Remix
- Nuxt
- Gatsby

View File

@@ -46,7 +46,7 @@ const docusaurusConfig = {
title: "Auth.js",
logo: {
alt: "Auth.js Logo",
src: "img/logo/logo-xs.webp",
src: "img/logo/logo-xs.png",
},
items: [
{
@@ -101,7 +101,7 @@ const docusaurusConfig = {
announcementBar: {
id: "new-major-announcement",
content:
"<a target='_blank' rel='noopener noreferrer' href='https://next-auth.js.org'>NextAuth.js</a> is becoming Auth.js! 🎉 We're creating Authentication for the Web. Everyone included. Starting with SvelteKit, check out <a href='/reference/sveltekit'>the docs</a>.",
"<a target='_blank' rel='noopener noreferrer' href='https://next-auth.js.org'>NextAuth.js</a> is becoming Auth.js! 🎉 We're creating Authentication for the Web. Everyone included. Starting with SvelteKit, check out the docs <a href='/reference/sveltekit'>here</a>.",
backgroundColor: "#000",
textColor: "#fff",
},
@@ -121,7 +121,6 @@ const docusaurusConfig = {
alt="Powered by Vercel"
style="margin-top: 8px"
height="32"
width="167"
src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/docs/static/img/powered-by-vercel.svg"
/>
</a>`,
@@ -182,10 +181,7 @@ const docusaurusConfig = {
lastVersion: "current",
showLastUpdateAuthor: true,
showLastUpdateTime: true,
remarkPlugins: [
require("@sapphire/docusaurus-plugin-npm2yarn2pnpm").npm2yarn2pnpm,
require("remark-github"),
],
remarkPlugins: [require("@sapphire/docusaurus-plugin-npm2yarn2pnpm").npm2yarn2pnpm, require("remark-github")],
versions: {
current: {
label: "experimental",
@@ -205,15 +201,7 @@ const docusaurusConfig = {
...typedocConfig,
id: "core",
plugin: ["./tyepdoc"],
entryPoints: [
"index.ts",
"adapters.ts",
"errors.ts",
"jwt.ts",
"types.ts",
]
.map((e) => `${coreSrc}/${e}`)
.concat(providers),
entryPoints: ["index.ts", "adapters.ts", "errors.ts", "jwt.ts", "types.ts"].map((e) => `${coreSrc}/${e}`).concat(providers),
tsconfig: "../packages/core/tsconfig.json",
out: "reference/03-core",
watch: process.env.TYPEDOC_WATCH,
@@ -226,9 +214,7 @@ const docusaurusConfig = {
...typedocConfig,
id: "sveltekit",
plugin: ["./tyepdoc"],
entryPoints: ["index.ts", "client.ts"].map(
(e) => `../packages/frameworks-sveltekit/src/lib/${e}`
),
entryPoints: ["index.ts", "client.ts"].map((e) => `../packages/frameworks-sveltekit/src/lib/${e}`),
tsconfig: "../packages/frameworks-sveltekit/tsconfig.json",
out: "reference/04-sveltekit",
watch: process.env.TYPEDOC_WATCH,

View File

@@ -22,7 +22,6 @@
"classnames": "^2.3.2",
"mdx-mermaid": "1.2.2",
"mermaid": "9.0.1",
"next-auth": "workspace:*",
"prism-react-renderer": "1.3.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@@ -53,15 +53,6 @@ module.exports = {
},
],
},
{
type: "category",
label: "@auth/solid-start",
link: {
type: "doc",
id: "reference/solidstart/index",
},
items: ["reference/solidstart/client", "reference/solidstart/protected"],
},
{
type: "category",
label: "@auth/nextjs",

View File

@@ -140,19 +140,19 @@ html[data-theme="dark"] hr {
border-radius: 10rem;
overflow: visible;
box-shadow: 0 0 2rem rgba(0, 0, 0, 0.1);
background-image: url("/img/mesh-1.webp");
background-image: url("/img/mesh-1.jpg");
background-size: cover;
background-origin: center;
}
.home-main .section-features .row .col:nth-child(2) .feature-image-wrapper {
background-image: url("/img/mesh-2.webp");
background-image: url("/img/mesh-2.jpg");
background-size: cover;
background-origin: center;
}
.home-main .section-features .row .col:nth-child(3) .feature-image-wrapper {
background-image: url("/img/mesh-3.webp");
background-image: url("/img/mesh-3.jpg");
background-size: cover;
background-origin: center;
}

View File

@@ -6,11 +6,6 @@
margin-right: 1rem !important;
}
.navbar__logo {
width: 29px;
height: 32px;
}
.navbar__title {
font-size: 1.2rem;
margin-left: 0.2rem;

View File

@@ -45,7 +45,7 @@ const features = [
<li>
Use with any modern framework!
<br />
<em>Next.js, SolidStart, SvelteKit</em>
<em>Next.js, SvelteKit</em>
</li>
<li>
Bring Your Own Database - or none!
@@ -117,11 +117,9 @@ export default function Home() {
<div className="container">
<div className="hero-inner">
<img
src="/img/logo/logo-sm.webp"
src="/img/logo/logo-sm.png"
alt="Shield with key icon"
className={styles.heroLogo}
height="142"
width="128"
/>
<div className={styles.heroText}>
<h1 className="hero__title">{siteConfig.title}</h1>
@@ -146,15 +144,6 @@ export default function Home() {
>
Live Demo (SvelteKit)
</a>
<a
className={classnames(
"button button--outline button--secondary button--lg rounded-pill",
styles.button
)}
href="https://auth-solid.vercel.app"
>
Live Demo (SolidStart)
</a>
<Link
className={classnames(
"button button--primary button--lg rounded-pill",
@@ -216,9 +205,9 @@ export default function Home() {
<div className="row">
<div className="col col--6">
<div className="code">
<div className="code-heading">
<h4 className="code-heading">
Next.js <span>/pages/api/auth/[...nextauth].ts</span>
</div>
</h4>
<CodeBlock className="prism-code language-js">
{nextJsCode}
</CodeBlock>
@@ -226,24 +215,14 @@ export default function Home() {
</div>
<div className="col col--6">
<div className="code">
<div className="code-heading">
<h4 className="code-heading">
SvelteKit <span>/hooks.server.ts</span>
</div>
</h4>
<CodeBlock className="prism-code language-js">
{svelteKitCode}
</CodeBlock>
</div>
</div>
<div className="col col--6">
<div className="code">
<div className="code-heading">
SolidStart <span>/routes/api/auth/[...solidauth].ts</span>
</div>
<CodeBlock className="prism-code language-js">
{solidStartCode}
</CodeBlock>
</div>
</div>
</div>
<div className="row">
<div className="col">
@@ -292,22 +271,6 @@ export const handle = SvelteKitAuth({
})
`.trim()
const solidStartCode =
`import { SolidAuth, type SolidAuthConfig } from "@auth/solid-start";
import GitHub from "@auth/core/providers/github";
export const authOpts: SolidAuthConfig = {
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
debug: false,
};
export const { GET, POST } = SolidAuth(authOpts);`.trim()
const nextJsCode = `
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -60,11 +60,6 @@
"destination": "https://github.com/nextauthjs/next-auth/discussions/categories/questions",
"permanent": true
},
{
"source": "/reference/solid-start/:path*",
"destination": "/reference/solidstart/:path*",
"permanent": true
},
{
"source": "/",
"has": [
@@ -75,16 +70,6 @@
],
"destination": "https://authjs.dev/reference/sveltekit/modules/main"
},
{
"source": "/",
"has": [
{
"type": "host",
"value": "solid-start.authjs.dev"
}
],
"destination": "https://authjs.dev/reference/solid-start"
},
{
"source": "/:path(.*)",
"has": [

View File

@@ -5,7 +5,6 @@
"repository": "https://github.com/nextauthjs/next-auth.git",
"scripts": {
"build:app": "turbo run build --filter=next-auth-app",
"build:docs": "turbo run build --filter=docs",
"build": "turbo run build --filter=next-auth --filter=@next-auth/* --filter=@auth/* --no-deps",
"test": "turbo run test --concurrency=1 --filter=[HEAD^1] --filter=./packages/* --filter=!*pouchdb-* --filter=!@*upstash* --filter=!*dynamodb-*",
"clean": "turbo run clean --no-cache",

View File

@@ -1,7 +1,7 @@
{
"name": "@next-auth/dynamodb-adapter",
"repository": "https://github.com/nextauthjs/next-auth",
"version": "1.0.6",
"version": "1.0.5",
"description": "AWS DynamoDB adapter for next-auth.",
"keywords": [
"next-auth",
@@ -31,7 +31,6 @@
"author": "Pol Marnette",
"license": "ISC",
"peerDependencies": {
"@aws-sdk/client-dynamodb": "^3.36.1",
"@aws-sdk/lib-dynamodb": "^3.36.1",
"next-auth": "^4"
},
@@ -44,4 +43,4 @@
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@auth/core",
"version": "0.2.5",
"version": "0.2.3",
"description": "Authentication for the Web.",
"keywords": [
"authentication",
@@ -20,7 +20,7 @@
"Balázs Orbán <info@balazsorban.com>",
"Nico Domino <yo@ndo.dev>",
"Lluis Agusti <hi@llu.lu>",
"Thang Huu Vu <hi@thvu.dev>",
"Thang Huu Vu <thvu@hey.com>",
"Iain Collins <me@iaincollins.com"
],
"type": "module",
@@ -61,10 +61,10 @@
},
"license": "ISC",
"dependencies": {
"@panva/hkdf": "^1.0.2",
"@panva/hkdf": "1.0.2",
"cookie": "0.5.0",
"jose": "^4.11.1",
"oauth4webapi": "^2.0.6",
"jose": "4.11.1",
"oauth4webapi": "2.0.6",
"preact": "10.11.3",
"preact-render-to-string": "5.2.3"
},
@@ -92,4 +92,4 @@
"postcss": "8.4.19",
"postcss-nested": "6.0.0"
}
}
}

View File

@@ -41,7 +41,6 @@ import { EncryptJWT, jwtDecrypt } from "jose"
import { SessionStore } from "./lib/cookie.js"
import { Awaitable } from "./types.js"
import type { LoggerInstance } from "./lib/utils/logger.js"
import { MissingSecret } from "./errors.js"
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
@@ -98,16 +97,13 @@ export interface GetTokenParams<R extends boolean = false> {
}
/**
* Takes an Auth.js request (`req`) and returns either the Auth.js issued JWT's payload,
* Takes a Auth.js request (`req`) and returns either the Auth.js issued JWT's payload,
* or the raw JWT string. We look for the JWT in the either the cookies, or the `Authorization` header.
* [Documentation](https://authjs.dev/guides/basics/securing-pages-and-api-routes#using-gettoken)
*/
export async function getToken<R extends boolean = false>(
params: GetTokenParams<R>
): Promise<R extends true ? string : JWT | null>
export async function getToken(
params: GetTokenParams
): Promise<string | JWT | null> {
): Promise<R extends true ? string : JWT | null> {
const {
req,
secureCookie = process.env.NEXTAUTH_URL?.startsWith("https://") ??
@@ -122,8 +118,6 @@ export async function getToken(
} = params
if (!req) throw new Error("Must pass `req` to JWT getToken()")
if (!secret)
throw new MissingSecret("Must pass `secret` if not set to JWT getToken()")
const sessionStore = new SessionStore(
{ name: cookieName, options: { secure: secureCookie } },
@@ -144,13 +138,17 @@ export async function getToken(
token = decodeURIComponent(urlEncodedToken)
}
// @ts-expect-error
if (!token) return null
// @ts-expect-error
if (raw) return token
try {
// @ts-expect-error
return await _decode({ token, secret })
} catch {
// @ts-expect-error
return null
}
}

View File

@@ -22,8 +22,7 @@ export async function getAuthorizationUrl(
let url = provider.authorization?.url
let as: o.AuthorizationServer | undefined
// Falls back to authjs.dev if the user only passed params
if (!url || url.host === "authjs.dev") {
if (!url) {
// If url is undefined, we assume that issuer is always defined
// We check this in assert.ts
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -49,9 +48,9 @@ export async function getAuthorizationUrl(
redirect_uri: provider.callbackUrl,
// @ts-expect-error TODO:
...provider.authorization?.params,
},
Object.fromEntries(provider.authorization?.url.searchParams ?? []),
query
}, // Defaults
Object.fromEntries(authParams), // From provider config
query // From `signIn` call
)
for (const k in params) authParams.set(k, params[k])

View File

@@ -31,12 +31,7 @@ export async function handleOAuth(
const { logger, provider } = options
let as: o.AuthorizationServer
const { token, userinfo } = provider
// Falls back to authjs.dev if the user only passed params
if (
(!token?.url || token.url.host === "authjs.dev") &&
(!userinfo?.url || userinfo.url.host === "authjs.dev")
) {
if (!provider.token?.url && !provider.userinfo?.url) {
// We assume that issuer is always defined as this has been asserted earlier
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const issuer = new URL(provider.issuer!)
@@ -59,9 +54,9 @@ export async function handleOAuth(
as = discoveredAs
} else {
as = {
issuer: provider.issuer ?? "https://authjs.dev", // TODO: review fallback issuer
token_endpoint: token?.url.toString(),
userinfo_endpoint: userinfo?.url.toString(),
issuer: provider.issuer ?? "https://a", // TODO: review fallback issuer
token_endpoint: provider.token?.url.toString(),
userinfo_endpoint: provider.userinfo?.url.toString(),
}
}
@@ -148,9 +143,9 @@ export async function handleOAuth(
throw new Error("TODO: Handle OAuth 2.0 response body error")
}
if (userinfo?.request) {
profile = await userinfo.request({ tokens, provider })
} else if (userinfo?.url) {
if (provider.userinfo?.request) {
profile = await provider.userinfo.request({ tokens, provider })
} else if (provider.userinfo?.url) {
const userinfoResponse = await o.userInfoRequest(
as,
client,

View File

@@ -45,11 +45,11 @@ export default function parseProviders(params: {
}
}
// TODO: Also add discovery here, if some endpoints/config are missing.
// We should return both a client and authorization server config.
function normalizeOAuth(
c: OAuthConfig<any> | OAuthUserConfig<any>
c?: OAuthConfig<any> | OAuthUserConfig<any>
): OAuthConfigInternal<any> | {} {
if (!c) return {}
if (c.issuer) c.wellKnown ??= `${c.issuer}/.well-known/openid-configuration`
const authorization = normalizeEndpoint(c.authorization, c.issuer)
@@ -84,18 +84,18 @@ function normalizeEndpoint(
e?: OAuthConfig<any>[OAuthEndpointType],
issuer?: string
): OAuthConfigInternal<any>[OAuthEndpointType] {
if (!e && issuer) return
if (!e || issuer) return
if (typeof e === "string") {
return { url: new URL(e) }
}
// If e.url is undefined, it's because the provider config
// If v.url is undefined, it's because the provider config
// assumes that we will use the issuer endpoint.
// The existence of either e.url or provider.issuer is checked in
// assert.ts. We fallback to "https://authjs.dev" to be able to pass around
// a valid URL even if the user only provided params.
// NOTE: This need to be checked when constructing the URL
// for the authorization, token and userinfo endpoints.
const url = new URL(e?.url ?? "https://authjs.dev")
for (const k in e?.params) url.searchParams.set(k, e?.params[k])
return { url, request: e?.request }
// The existence of either v.url or provider.issuer is checked in
// assert.ts
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const url = new URL(e.url!)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
for (const k in e.params) url.searchParams.set(k, e.params[k] as any)
return { ...e, url }
}

View File

@@ -2,7 +2,7 @@ import { handleLogin } from "../callback-handler.js"
import { CallbackRouteError } from "../../errors.js"
import { handleOAuth } from "../oauth/callback.js"
import { createHash } from "../web.js"
import { getAdapterUserFromEmail, handleAuthorized } from "./shared.js"
import { handleAuthorized } from "./shared.js"
import type { AdapterSession } from "../../adapters.js"
import type {
@@ -10,7 +10,6 @@ import type {
ResponseInternal,
User,
InternalOptions,
Account,
} from "../../types.js"
import type { Cookie, SessionStore } from "../cookie.js"
@@ -173,40 +172,40 @@ export async function callback(params: {
}
// @ts-expect-error -- Verified in `assertConfig`.
const user = await getAdapterUserFromEmail(identifier, adapter)
const profile = await getAdapterUserFromEmail(identifier, adapter)
const account: Account = {
providerAccountId: user.email,
userId: user.id,
const account = {
providerAccountId: profile.email,
type: "email" as const,
provider: provider.id,
}
// Check if user is allowed to sign in
const unauthorizedOrError = await handleAuthorized(
{ user, account },
{ user: profile, account },
options
)
if (unauthorizedOrError) return { ...unauthorizedOrError, cookies }
// Sign user in
const {
user: loggedInUser,
session,
isNewUser,
} = await handleLogin(sessionStore.value, user, account, options)
const { user, session, isNewUser } = await handleLogin(
sessionStore.value,
profile,
account,
options
)
if (useJwtSession) {
const defaultToken = {
name: loggedInUser.name,
email: loggedInUser.email,
picture: loggedInUser.image,
sub: loggedInUser.id?.toString(),
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const token = await callbacks.jwt({
token: defaultToken,
user: loggedInUser,
user,
account,
isNewUser,
})
@@ -234,7 +233,7 @@ export async function callback(params: {
})
}
await events.signIn?.({ user: loggedInUser, account, isNewUser })
await events.signIn?.({ user, account, isNewUser })
// Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login

View File

@@ -33,7 +33,7 @@ export async function signin(
const account: Account = {
providerAccountId: email,
userId: user.id,
userId: email,
type: "email",
provider: provider.id,
}

View File

@@ -2,7 +2,6 @@ import type { OAuthConfig, OAuthUserConfig } from "./index.js"
export type DateTime = string
export type Gender = "female" | "male"
export type Birthday = "SOLAR" | "LUNAR"
export type AgeRange =
| "1-9"
| "10-14"
@@ -56,7 +55,7 @@ export interface KakaoProfile extends Record<string, any> {
birthyear?: string
birthday_needs_agreement?: boolean
birthday?: string
birthday_type?: Birthday
birthday_type?: string
gender_needs_agreement?: boolean
gender?: Gender
phone_number_needs_agreement?: boolean

View File

@@ -1,7 +0,0 @@
node_modules
dist
**/*.d.ts
**/*.js
!tsup.config.js
!scripts/**/*.js
.vercel

View File

@@ -1,80 +0,0 @@
# Getting started
Recommended to use [create-jd-app](https://github.com/OrJDev/create-jd-app)
```bash
npm install @auth/solid-start@latest @auth/core@latest
```
## Setting It Up
[Generate auth secret](https://generate-secret.vercel.app/32), then set it as an environment variable:
```
AUTH_SECRET=your_auth_secret
```
### On Production
Don't forget to trust the host.
```
AUTH_TRUST_HOST=true
```
## Creating the api handler
in this example we are using github so make sure to set the following environment variables:
```
GITHUB_ID=your_github_oatuh_id
GITHUB_SECRET=your_github_oatuh_secret
```
```ts
// routes/api/auth/[...solidauth].ts
import { SolidAuth, type SolidAuthConfig } from "@auth/solid-start"
import GitHub from "@auth/core/providers/github"
export const authOpts: SolidAuthConfig = {
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
debug: false,
}
export const { GET, POST } = SolidAuth(authOpts)
```
## Signing in and out
```ts
import { signIn, signOut } from "@auth/solid-start/client"
const login = () => signIn("github")
const logout = () => signOut()
```
## Getting the current session
```ts
import { getSession } from "@auth/solid-start"
import { createServerData$ } from "solid-start/server"
import { authOpts } from "~/routes/api/auth/[...solidauth]"
export const useSession = () => {
return createServerData$(
async (_, { request }) => {
return await getSession(request, authOpts)
},
{ key: () => ["auth_user"] }
)
}
// useSession returns a resource:
const session = useSession()
const loading = session.loading
const user = () => session()?.user
```

View File

@@ -1,58 +0,0 @@
{
"name": "@auth/solid-start",
"description": "Authentication for SolidStart.",
"version": "0.1.0",
"type": "module",
"files": [
"client.*",
"index.*",
"src"
],
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
},
"./client": {
"types": "./client.d.ts",
"import": "./client.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "tsup --config ./tsup.config.js && node scripts/postbuild",
"patch": "npm version patch --no-git-tag-version",
"clean": "rm -rf client.* index.*"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@auth/core": "workspace:*",
"@solidjs/meta": "^0.28.0",
"@types/cookie": "0.5.1",
"@types/node": "^18.7.14",
"@types/set-cookie-parser": "^2.4.2",
"next-auth": "workspace:*",
"solid-js": "^1.5.7",
"solid-start": "^0.2.1",
"tsup": "^6.5.0",
"typescript": "^4.8.2"
},
"peerDependencies": {
"@auth/core": "~0.2.2 || ^0.2.2",
"solid-js": "^1.5.7",
"solid-start": "^0.2.1"
},
"dependencies": {
"set-cookie-parser": "^2.5.1"
},
"keywords": [
"SolidJS",
"SolidStart",
"Auth"
],
"author": "OrJDev <orjdeveloper@gmail.com>",
"repository": "https://github.com/nextauthjs/next-auth",
"license": "ISC"
}

View File

@@ -1,16 +0,0 @@
import path from "path";
import fs from "fs/promises";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function main() {
const root = path.join(__dirname, "../");
const dist = path.join(root, "dist");
await fs.cp(dist, root, {
recursive: true,
});
}
main();

View File

@@ -1,102 +0,0 @@
import type {
LiteralUnion,
SignInOptions,
SignInAuthorizationParams,
SignOutParams,
} from "next-auth/react"
import type {
BuiltInProviderType,
RedirectableProviderType,
} from "@auth/core/providers"
/**
* Client-side method to initiate a signin flow
* or send the user to the signin page listing all possible providers.
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signin)
*/
export async function signIn<
P extends RedirectableProviderType | undefined = undefined
>(
providerId?: LiteralUnion<
P extends RedirectableProviderType
? P | BuiltInProviderType
: BuiltInProviderType
>,
options?: SignInOptions,
authorizationParams?: SignInAuthorizationParams
) {
const { callbackUrl = window.location.href, redirect = true } = options ?? {}
// TODO: Support custom providers
const isCredentials = providerId === "credentials"
const isEmail = providerId === "email"
const isSupportingReturn = isCredentials || isEmail
// TODO: Handle custom base path
const signInUrl = `/api/auth/${
isCredentials ? "callback" : "signin"
}/${providerId}`
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
// TODO: Handle custom base path
const csrfTokenResponse = await fetch("/api/auth/csrf")
const { csrfToken } = await csrfTokenResponse.json()
const res = await fetch(_signInUrl, {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-Auth-Return-Redirect": "1",
},
// @ts-expect-error -- ignore
body: new URLSearchParams({
...options,
csrfToken,
callbackUrl,
}),
})
const data = await res.clone().json()
const error = new URL(data.url).searchParams.get("error")
if (redirect || !isSupportingReturn || !error) {
// TODO: Do not redirect for Credentials and Email providers by default in next major
window.location.href = data.url ?? data.redirect ?? callbackUrl
// If url contains a hash, the browser does not reload the page. We reload manually
if (data.url.includes("#")) window.location.reload()
return
}
return res
}
/**
* Signs the user out, by removing the session cookie.
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signout)
*/
export async function signOut(options?: SignOutParams) {
const { callbackUrl = window.location.href } = options ?? {}
// TODO: Custom base path
const csrfTokenResponse = await fetch("/api/auth/csrf")
const { csrfToken } = await csrfTokenResponse.json()
const res = await fetch(`/api/auth/signout`, {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-Auth-Return-Redirect": "1",
},
body: new URLSearchParams({
csrfToken,
callbackUrl,
}),
})
const data = await res.json()
const url = data.url ?? data.redirect ?? callbackUrl
window.location.href = url
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
}

View File

@@ -1,114 +0,0 @@
import { Auth } from "@auth/core"
import { Cookie, parseString, splitCookiesString } from "set-cookie-parser"
import { serialize } from "cookie"
import type { AuthAction, AuthConfig, Session } from "@auth/core/types"
export interface SolidAuthConfig extends AuthConfig {
/**
* Defines the base path for the auth routes.
* @default '/api/auth'
*/
prefix?: string
}
const actions: AuthAction[] = [
"providers",
"session",
"csrf",
"signin",
"signout",
"callback",
"verify-request",
"error",
]
// currently multiple cookies are not supported, so we keep the next-auth.pkce.code_verifier cookie for now:
// because it gets updated anyways
// src: https://github.com/solidjs/solid-start/issues/293
const getSetCookieCallback = (cook?: string | null): Cookie | undefined => {
if (!cook) return
const splitCookie = splitCookiesString(cook)
for (const cookName of [
"__Secure-next-auth.session-token",
"next-auth.session-token",
"next-auth.pkce.code_verifier",
"__Secure-next-auth.pkce.code_verifier",
]) {
const temp = splitCookie.find((e) => e.startsWith(`${cookName}=`))
if (temp) {
return parseString(temp)
}
}
return parseString(splitCookie?.[0] ?? "") // just return the first cookie if no session token is found
}
function SolidAuthHandler(prefix: string, authOptions: SolidAuthConfig) {
return async (event: any) => {
const { request } = event
const url = new URL(request.url)
const action = url.pathname
.slice(prefix.length + 1)
.split("/")[0] as AuthAction
if (!actions.includes(action) || !url.pathname.startsWith(prefix + "/")) {
return
}
const res = await Auth(request, authOptions)
if (["callback", "signin", "signout"].includes(action)) {
const parsedCookie = getSetCookieCallback(
res.clone().headers.get("Set-Cookie")
)
if (parsedCookie) {
res.headers.set(
"Set-Cookie",
serialize(parsedCookie.name, parsedCookie.value, parsedCookie as any)
)
}
}
return res
}
}
export function SolidAuth(config: SolidAuthConfig) {
const { prefix = "/api/auth", ...authOptions } = config
authOptions.secret ??= process.env.AUTH_SECRET
authOptions.trustHost ??= !!(
process.env.AUTH_TRUST_HOST ??
process.env.VERCEL ??
process.env.NODE_ENV !== "production"
)
const handler = SolidAuthHandler(prefix, authOptions)
return {
async GET(event: any) {
return await handler(event)
},
async POST(event: any) {
return await handler(event)
},
}
}
export type GetSessionResult = Promise<Session | null>
export async function getSession(
req: Request,
options: AuthConfig
): GetSessionResult {
options.secret ??= process.env.AUTH_SECRET
options.trustHost ??= true
const url = new URL("/api/auth/session", req.url)
const response = await Auth(
new Request(url, { headers: req.headers }),
options
)
const { status = 200 } = response
const data = await response.json()
if (!data || !Object.keys(data).length) return null
if (status === 200) return data
throw new Error(data.message)
}

View File

@@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["./*.js", "./*.d.ts"]
}

View File

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"declaration": true,
"allowSyntheticDefaultImports": true,
"target": "esnext",
"moduleResolution": "Node",
"strict": false,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"module": "esnext",
"outDir": "./dist",
"rootDir": "./src",
"strictNullChecks": true
},
"exclude": ["node_modules", "dist"],
"include": ["./src"]
}

View File

@@ -1,15 +0,0 @@
import { defineConfig } from "tsup";
export default defineConfig((options) => ({
entry: ["src/**/*.ts"],
target: "esnext",
sourcemap: options.watch ? "inline" : false,
clean: true,
minify: false,
keepNames: false,
tsconfig: "./tsconfig.json",
format: ["esm"],
external: ["solid-js", "solid-js/web", "solid-start"],
dts: true,
bundle: false,
}));

View File

@@ -1,6 +1,6 @@
{
"name": "@auth/sveltekit",
"version": "0.1.12",
"version": "0.1.10",
"description": "Authentication for SvelteKit.",
"keywords": [
"authentication",
@@ -69,4 +69,4 @@
},
"./package.json": "./package.json"
}
}
}

View File

@@ -18,7 +18,7 @@
* ## Usage
*
* ```ts title="src/hooks.server.ts"
* import { SvelteKitAuth } from "@auth/sveltekit"
* import SvelteKitAuth from "@auth/sveltekit"
* import GitHub from "@auth/core/providers/github"
* import { GITHUB_ID, GITHUB_SECRET } from "$env/static/private"
*
@@ -27,7 +27,7 @@
* })
* ```
*
* Don't forget to set the `AUTH_SECRET` [environment variable](https://kit.svelte.dev/docs/modules#$env-dynamic-private). This should be a minimum of 32 characters, random string. On UNIX systems you can use `openssl rand -hex 32` or check out `https://generate-secret.vercel.app/32`.
* Don't forget to set the `AUTH_SECRET` [environment variable](https://kit.svelte.dev/docs/modules#$env-static-private). This should be a random 32 character string. On unix systems you can use `openssl rand -hex 32` or check out `https://generate-secret.vercel.app/32`.
*
* When deploying your app outside Vercel, set the `AUTH_TRUST_HOST` variable to `true` for other hosting providers like Cloudflare Pages or Netlify.
*
@@ -83,6 +83,7 @@ import type { Handle } from "@sveltejs/kit"
import { dev } from "$app/environment"
import { env } from "$env/dynamic/private"
import { AUTH_SECRET } from "$env/static/private"
import { Auth } from "@auth/core"
import type { AuthAction, AuthConfig, Session } from "@auth/core/types"
@@ -91,7 +92,7 @@ export async function getSession(
req: Request,
config: AuthConfig
): ReturnType<App.Locals["getSession"]> {
config.secret ??= env.AUTH_SECRET
config.secret ??= AUTH_SECRET
config.trustHost ??= true
const url = new URL("/api/auth/session", req.url)
@@ -153,7 +154,7 @@ function AuthHandle(prefix: string, authOptions: AuthConfig): Handle {
*/
export function SvelteKitAuth(options: SvelteKitAuthConfig): Handle {
const { prefix = "/auth", ...authOptions } = options
authOptions.secret ??= env.AUTH_SECRET
authOptions.secret ??= AUTH_SECRET
authOptions.trustHost ??= !!(env.AUTH_TRUST_HOST ?? env.VERCEL ?? dev)
return AuthHandle(prefix, authOptions)
}
@@ -171,7 +172,10 @@ declare global {
}
declare module "$env/dynamic/private" {
export const AUTH_SECRET: string
export const AUTH_TRUST_HOST: string
export const VERCEL: string
}
declare module "$env/static/private" {
export const AUTH_SECRET: string
}

View File

@@ -9,7 +9,7 @@
"Balázs Orbán <info@balazsorban.com>",
"Nico Domino <yo@ndo.dev>",
"Lluis Agusti <hi@llu.lu>",
"Thang Huu Vu <hi@thvu.dev>"
"Thang Huu Vu <thvu@hey.com>"
],
"main": "index.js",
"module": "index.js",

1365
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,14 @@
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"docs#build": {
"dependsOn": ["^build", "next-auth#build"]
},
"build": {
"dependsOn": ["^build"]
},
"next-auth#build": {
"dependsOn": ["^build"],
"outputs": [
"client/**",
"core/**",
"css/**",
"jwt/**",
"next/**",
"providers/**",
"react/**",
"index.d.ts",
"index.js",
"adapters.d.ts",
"middleware.d.ts",
"middleware.js"
]
"dependsOn": ["^build"]
},
"clean": {
"cache": false