Compare commits
3 Commits
@auth/svel
...
fix/callba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
079fbd27fa | ||
|
|
c7101981bc | ||
|
|
f7b052a5fd |
10
.github/sync.yml
vendored
@@ -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: .
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
AUTH_SECRET=
|
||||
27
apps/examples/solid-start/.gitignore
vendored
@@ -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
|
||||
@@ -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 (:
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
Before Width: | Height: | Size: 664 B |
@@ -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"] }
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./NavBar";
|
||||
@@ -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;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./Protected";
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as NavBar } from "./NavBar";
|
||||
export { default as Protected } from "./Protected";
|
||||
@@ -1,3 +0,0 @@
|
||||
import { mount, StartClient } from "solid-start/entry-client";
|
||||
|
||||
mount(() => <StartClient />, document);
|
||||
@@ -1,9 +0,0 @@
|
||||
import {
|
||||
StartServer,
|
||||
createHandler,
|
||||
renderAsync,
|
||||
} from "solid-start/entry-server";
|
||||
|
||||
export default createHandler(
|
||||
renderAsync((event) => <StartServer event={event} />)
|
||||
);
|
||||
24
apps/examples/solid-start/src/env/client.ts
vendored
@@ -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;
|
||||
15
apps/examples/solid-start/src/env/schema.ts
vendored
@@ -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"),
|
||||
});
|
||||
24
apps/examples/solid-start/src/env/server.ts
vendored
@@ -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;
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }) })],
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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**
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -18,7 +18,7 @@ sidebar_position: 0
|
||||
|
||||
- Next.js
|
||||
- SvelteKit
|
||||
- SolidStart
|
||||
- SolidState
|
||||
- Remix
|
||||
- Nuxt
|
||||
- Gatsby
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
margin-right: 1rem !important;
|
||||
}
|
||||
|
||||
.navbar__logo {
|
||||
width: 29px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.navbar__title {
|
||||
font-size: 1.2rem;
|
||||
margin-left: 0.2rem;
|
||||
|
||||
@@ -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'
|
||||
|
||||
BIN
docs/static/img/logo/logo-sm.webp
vendored
|
Before Width: | Height: | Size: 19 KiB |
BIN
docs/static/img/logo/logo-xs.webp
vendored
|
Before Width: | Height: | Size: 2.3 KiB |
BIN
docs/static/img/logo/logo.webp
vendored
|
Before Width: | Height: | Size: 47 KiB |
BIN
docs/static/img/mesh-1.webp
vendored
|
Before Width: | Height: | Size: 33 KiB |
BIN
docs/static/img/mesh-2.webp
vendored
|
Before Width: | Height: | Size: 32 KiB |
BIN
docs/static/img/mesh-3.webp
vendored
|
Before Width: | Height: | Size: 36 KiB |
@@ -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": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function signin(
|
||||
|
||||
const account: Account = {
|
||||
providerAccountId: email,
|
||||
userId: user.id,
|
||||
userId: email,
|
||||
type: "email",
|
||||
provider: provider.id,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
7
packages/frameworks-solid-start/.gitignore
vendored
@@ -1,7 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
**/*.d.ts
|
||||
**/*.js
|
||||
!tsup.config.js
|
||||
!scripts/**/*.js
|
||||
.vercel
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["./*.js", "./*.d.ts"]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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,
|
||||
}));
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
19
turbo.json
@@ -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
|
||||
|
||||