Compare commits

..

1 Commits

Author SHA1 Message Date
Balázs Orbán
3f91731ba7 chore(release): bump package version(s) [skip ci] 2022-10-05 17:26:36 +00:00
57 changed files with 323 additions and 634 deletions

View File

@@ -3,27 +3,10 @@ const path = require("path")
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
overrides: [
{
files: ["*.ts", "*.tsx"],
extends: ["standard-with-typescript", "prettier"],
rules: {
camelcase: "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/restrict-template-expressions": "off",
},
parserOptions: {
project: [
path.resolve(__dirname, "./packages/**/tsconfig.eslint.json"),
path.resolve(__dirname, "./apps/**/tsconfig.json"),
],
},
},
],
extends: ["prettier"],
parserOptions: {
project: [path.resolve(__dirname, "./packages/**/tsconfig.eslint.json")],
},
extends: ["standard-with-typescript", "prettier"],
globals: {
localStorage: "readonly",
location: "readonly",
@@ -31,6 +14,10 @@ module.exports = {
},
rules: {
camelcase: "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/restrict-template-expressions": "off",
},
plugins: ["jest"],
env: {

View File

@@ -6,7 +6,6 @@
"scripts": {
"clean": "rm -rf .next",
"dev": "next dev",
"lint": "next lint",
"build": "next build",
"start": "next start",
"email": "fake-smtp-server",

View File

@@ -18,7 +18,6 @@ import Freshbooks from "next-auth/providers/freshbooks"
import GitHub from "next-auth/providers/github"
import Gitlab from "next-auth/providers/gitlab"
import Google from "next-auth/providers/google"
import Hubspot from "next-auth/providers/hubspot"
import IDS4 from "next-auth/providers/identity-server4"
import Instagram from "next-auth/providers/instagram"
import Keycloak from "next-auth/providers/keycloak"
@@ -36,7 +35,6 @@ import Twitter, { TwitterLegacy } from "next-auth/providers/twitter"
import Vk from "next-auth/providers/vk"
import Wikimedia from "next-auth/providers/wikimedia"
import WorkOS from "next-auth/providers/workos"
import Zitadel from "next-auth/providers/zitadel"
// Adapters
import { PrismaClient } from "@prisma/client"
@@ -104,7 +102,6 @@ export const authOptions: NextAuthOptions = {
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }),
Hubspot({ clientId: process.env.HUBSPOT_ID, clientSecret: process.env.HUBSPOT_SECRET }),
IDS4({ clientId: process.env.IDS4_ID, clientSecret: process.env.IDS4_SECRET, issuer: process.env.IDS4_ISSUER }),
Instagram({ clientId: process.env.INSTAGRAM_ID, clientSecret: process.env.INSTAGRAM_SECRET }),
Keycloak({ clientId: process.env.KEYCLOAK_ID, clientSecret: process.env.KEYCLOAK_SECRET, issuer: process.env.KEYCLOAK_ISSUER }),
@@ -123,7 +120,6 @@ export const authOptions: NextAuthOptions = {
Vk({ clientId: process.env.VK_ID, clientSecret: process.env.VK_SECRET }),
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
WorkOS({ clientId: process.env.WORKOS_ID, clientSecret: process.env.WORKOS_SECRET }),
Zitadel({ issuer: process.env.ZITADEL_ISSUER, clientId: process.env.ZITADEL_CLIENT_ID, clientSecret: process.env.ZITADEL_CLIENT_SECRET }),
],
}

View File

@@ -156,7 +156,7 @@ interface OAuthConfig {
*/
id: string
version: string
profile(profile: P, tokens: TokenSet): Awaitable<User>
profile(profile: P, tokens: TokenSet): Awaitable<User & { id: string }>
checks?: ChecksType | ChecksType[]
clientId: string
clientSecret: string

View File

@@ -50,7 +50,7 @@ providers: [
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
// Add logic here to look up the user from the credentials supplied

View File

@@ -1,87 +0,0 @@
---
id: zitadel
title: Zitadel
---
## Documentation
https://docs.zitadel.com/docs/apis/openidoauth/endpoints
## Configuration
https://docs.zitadel.com/docs/guides/integrate/oauth-recommended-flows
The Redirect URIs used when creating the credentials must include your full domain and end in the callback path. For example:
- For production: `https://{YOUR_DOMAIN}/api/auth/callback/zitadel`
- For development: `http://localhost:3000/api/auth/callback/zitadel`
Make sure to enable **dev mode** in ZITADEL console to allow redirects for local development.
## Options
The **ZITADEL Provider** comes with a set of default options:
- [ZITADEL Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/zitadel.ts)
You can override any of the options to suit your own use case.
## Example
```js
import ZitadelProvider from "next-auth/providers/zitadel";
...
providers: [
ZitadelProvider({
issuer: process.env.ZITADEL_ISSUER,
clientId: process.env.ZITADEL_CLIENT_ID,
clientSecret: process.env.ZITADEL_CLIENT_SECRET,
})
]
...
```
If you need access to ZITADEL APIs or need additional information, make sure to add the corresponding scopes.
To get the full list of supported claims take a look [here](https://docs.zitadel.com/docs/apis/openidoauth/endpoints).
```js
const options = {
...
providers: [
ZitadelProvider({
clientId: process.env.ZITADEL_CLIENT_ID,
authorization: {
params: {
scope: `openid email profile urn:zitadel:iam:org:project:id:${process.env.ZITADEL_PROJECT_ID}:aud`
}
}
})
],
...
}
```
:::
:::tip
ZITADEL also returns a `email_verified` boolean property in the profile.
You can use this property to restrict access to people with verified accounts.
```js
const options = {
...
callbacks: {
async signIn({ account, profile }) {
if (account.provider === "zitadel") {
return profile.email_verified;
}
return true; // Do different verification for other providers that don't have `email_verified`
},
}
...
}
```
:::

View File

@@ -105,11 +105,6 @@ This tutorial covers:
## Database
#### [Create a NextAuth.js Custom Adapter with HarperDB & Next.js](https://spacejelly.dev/posts/how-to-create-a-nextauth-js-custom-adapter-with-harperdb-next-js/) <svg xmlns="http://www.w3.org/2000/svg" style={{ marginLeft: '5px', marginBottom:'-6px'}} height="20" width="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"><title>External</title> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> </svg>
- Use a custom database in a Custom Adapter for persisted NextAuth.js sessions using HarperDB as an example.
- Video tutorial also available: <https://www.youtube.com/watch?v=pu7xBv7sZ8s>
#### [Using NextAuth.js with Prisma and PlanetScale serverless databases](https://github.com/planetscale/nextjs-planetscale-starter) <svg xmlns="http://www.w3.org/2000/svg" style={{ marginLeft: '5px', marginBottom:'-6px'}} height="20" width="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"><title>External</title> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> </svg>
- How to set up a PlanetScale database to fetch and store user / account data with the Prisma adapter.

View File

@@ -1,7 +1,7 @@
{
"name": "@next-auth/dynamodb-adapter",
"repository": "https://github.com/nextauthjs/next-auth",
"version": "1.0.5",
"version": "1.0.4",
"description": "AWS DynamoDB adapter for next-auth.",
"keywords": [
"next-auth",
@@ -43,4 +43,4 @@
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -4,10 +4,10 @@ import type {
BatchWriteCommandInput,
DynamoDBDocument,
} from "@aws-sdk/lib-dynamodb"
import type { Account } from "next-auth"
import type {
Adapter,
AdapterSession,
AdapterAccount,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
@@ -86,7 +86,7 @@ export function DynamoDBAdapter(
})
if (!data.Items?.length) return null
const accounts = data.Items[0] as AdapterAccount
const accounts = data.Items[0] as Account
const res = await client.get({
TableName,
Key: {
@@ -174,7 +174,7 @@ export function DynamoDBAdapter(
":gsi1sk": `ACCOUNT#${providerAccountId}`,
},
})
const account = format.from<AdapterAccount>(data.Items?.[0])
const account = format.from<Account>(data.Items?.[0])
if (!account) return
await client.delete({
TableName,

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/firebase-adapter",
"version": "1.0.2",
"version": "1.0.1",
"description": "Firebase adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -43,4 +43,4 @@
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -15,18 +15,17 @@ import {
where,
connectFirestoreEmulator,
} from "firebase/firestore"
import type { Account } from "next-auth"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
import { getConverter } from "./converter"
export type IndexableObject = Record<string, unknown>
type IndexableObject = Record<string, unknown>
export interface FirestoreAdapterOptions {
emulator?: {
@@ -51,13 +50,13 @@ export function FirestoreAdapter({
}
const Users = collection(db, "users").withConverter(
getConverter<AdapterUser & IndexableObject>()
getConverter<AdapterUser>()
)
const Sessions = collection(db, "sessions").withConverter(
getConverter<AdapterSession & IndexableObject>()
)
const Accounts = collection(db, "accounts").withConverter(
getConverter<AdapterAccount>()
getConverter<Account>()
)
const VerificationTokens = collection(db, "verificationTokens").withConverter(
getConverter<VerificationToken & IndexableObject>({ excludeId: true })

View File

@@ -14,7 +14,7 @@ connectFirestoreEmulator(firestore, 'localhost', 8080);
type IndexableObject = Record<string, unknown>;
const Users = collection(firestore, 'users').withConverter(getConverter<AdapterUser & IndexableObject>());
const Users = collection(firestore, 'users').withConverter(getConverter<AdapterUser>());
const Sessions = collection(firestore, 'sessions').withConverter(getConverter<AdapterSession & IndexableObject>());
const Accounts = collection(firestore, 'accounts').withConverter(getConverter<Account>());
const VerificationTokens = collection(firestore, 'verificationTokens').withConverter(getConverter<VerificationToken & IndexableObject>({ excludeId: true }));

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/mikro-orm-adapter",
"version": "3.0.1",
"version": "3.0.0",
"description": "MikroORM adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -50,4 +50,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -5,16 +5,17 @@ import {
Unique,
PrimaryKey,
Entity,
Enum,
OneToMany,
Collection,
ManyToOne,
types,
} from "@mikro-orm/core"
import type { DefaultAccount } from "next-auth"
import type {
AdapterUser,
AdapterAccount,
AdapterSession,
AdapterUser,
VerificationToken as AdapterVerificationToken,
} from "next-auth/adapters"
import type { ProviderType } from "next-auth/providers"
@@ -34,7 +35,7 @@ export class User implements RemoveIndex<AdapterUser> {
@Property({ type: types.string, nullable: true })
@Unique()
email: string = ""
email?: string
@Property({ type: types.datetime, nullable: true })
emailVerified: Date | null = null
@@ -43,7 +44,7 @@ export class User implements RemoveIndex<AdapterUser> {
image?: string
@OneToMany({
entity: "Session",
entity: 'Session',
mappedBy: (session: Session) => session.user,
hidden: true,
orphanRemoval: true,
@@ -51,7 +52,7 @@ export class User implements RemoveIndex<AdapterUser> {
sessions = new Collection<Session, object>(this)
@OneToMany({
entity: "Account",
entity: 'Account',
mappedBy: (account: Account) => account.user,
hidden: true,
orphanRemoval: true,
@@ -66,7 +67,7 @@ export class Session implements AdapterSession {
id: string = randomUUID()
@ManyToOne({
entity: "User",
entity: 'User',
hidden: true,
onDelete: "cascade",
})
@@ -75,7 +76,7 @@ export class Session implements AdapterSession {
@Property({ type: types.string, persist: false })
userId!: string
@Property({ type: "Date" })
@Property({ type: 'Date' })
expires!: Date
@Property({ type: types.string })
@@ -85,13 +86,13 @@ export class Session implements AdapterSession {
@Entity()
@Unique({ properties: ["provider", "providerAccountId"] })
export class Account implements RemoveIndex<AdapterAccount> {
export class Account implements RemoveIndex<DefaultAccount> {
@PrimaryKey()
@Property({ type: types.string })
id: string = randomUUID()
@ManyToOne({
entity: "User",
entity: 'User',
hidden: true,
onDelete: "cascade",
})
@@ -138,7 +139,7 @@ export class VerificationToken implements AdapterVerificationToken {
@Property({ type: types.string })
token!: string
@Property({ type: "Date" })
@Property({ type: 'Date' })
expires!: Date
@Property({ type: types.string })

View File

@@ -1,4 +1,7 @@
import { Options, types } from "@mikro-orm/core"
import type { SqliteDriver } from "@mikro-orm/sqlite"
import { MikroORM, wrap } from "@mikro-orm/core"
import { runBasicTests } from "@next-auth/adapter-test"
import { MikroOrmAdapter, defaultEntities } from "../src"
import {
Cascade,
@@ -8,12 +11,8 @@ import {
PrimaryKey,
Property,
Unique,
MikroORM,
wrap,
Options,
types,
} from "@mikro-orm/core"
import { randomUUID, runBasicTests } from "@next-auth/adapter-test"
import { randomUUID } from "@next-auth/adapter-test"
@Entity()
export class User implements defaultEntities.User {
@@ -26,16 +25,16 @@ export class User implements defaultEntities.User {
@Property({ type: types.string, nullable: true })
@Unique()
email: string = ""
email?: string
@Property({ type: "Date", nullable: true })
@Property({ type: 'Date', nullable: true })
emailVerified: Date | null = null
@Property({ type: types.string, nullable: true })
image?: string
@OneToMany({
entity: "Session",
entity: 'Session',
mappedBy: (session: defaultEntities.Session) => session.user,
hidden: true,
orphanRemoval: true,
@@ -44,7 +43,7 @@ export class User implements defaultEntities.User {
sessions = new Collection<defaultEntities.Session>(this)
@OneToMany({
entity: "Account",
entity: 'Account',
mappedBy: (account: defaultEntities.Account) => account.user,
hidden: true,
orphanRemoval: true,

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/mongodb-adapter",
"version": "1.1.1",
"version": "1.1.0",
"description": "mongoDB adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -44,4 +44,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -3,12 +3,12 @@ import { ObjectId } from "mongodb"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
import type { MongoClient } from "mongodb"
import type { Account } from "next-auth"
export interface MongoDBAdapterOptions {
collections?: {
@@ -56,7 +56,7 @@ export const format = {
else if (key === "id") continue
else newObject[key] = value
}
return newObject as T & { _id: ObjectId }
return newObject as T
},
}
@@ -78,7 +78,7 @@ export function MongoDBAdapter(
const c = { ...defaultCollections, ...collections }
return {
U: _db.collection<AdapterUser>(c.Users),
A: _db.collection<AdapterAccount>(c.Accounts),
A: _db.collection<Account>(c.Accounts),
S: _db.collection<AdapterSession>(c.Sessions),
V: _db.collection<VerificationToken>(c?.VerificationTokens),
}
@@ -128,7 +128,7 @@ export function MongoDBAdapter(
])
},
linkAccount: async (data) => {
const account = to<AdapterAccount>(data)
const account = to<Account>(data)
await (await db).A.insertOne(account)
return account
},
@@ -136,7 +136,7 @@ export function MongoDBAdapter(
const { value: account } = await (
await db
).A.findOneAndDelete(provider_providerAccountId)
return from<AdapterAccount>(account!)
return from<Account>(account!)
},
async getSessionAndUser(sessionToken) {
const session = await (await db).S.findOne({ sessionToken })
@@ -156,6 +156,7 @@ export function MongoDBAdapter(
return from<AdapterSession>(session)
},
async updateSession(data) {
// @ts-expect-error
const { _id, ...session } = to<AdapterSession>(data)
const result = await (

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/neo4j-adapter",
"version": "1.0.5",
"version": "1.0.4",
"description": "neo4j adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -50,4 +50,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -87,6 +87,8 @@ export function Neo4jAdapter(session: Session): Adapter {
)
},
// @ts-expect-error Property 'id' is missing in type
// We never use `session.id` anywhere in the core, so this is fine.
async createSession(data) {
const { userId, ...s } = format.to(data)
await write(

View File

@@ -38,7 +38,7 @@ runBasicTests({
return format.from(result?.records[0]?.get("u")?.properties)
},
async session(sessionToken: string) {
async session(sessionToken: any) {
const result = await neo4jSession.readTransaction((tx) =>
tx.run(
`MATCH (u:User)-[:HAS_SESSION]->(s:Session)

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
NEO4J_USER=neo4j
NEO4J_PASS=password
CONTAINER_NAME=next-auth-neo4j-test-e
@@ -28,7 +29,7 @@ neo4j:4.2.0
# -e NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \
# neo4j:4.2.0-enterprise
echo "Waiting 10 sec for db to start..." && sleep 10
echo "Waiting 5 sec for db to start..." && sleep 5
if $JEST_WATCH; then
# Run jest in watch mode

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/prisma-adapter",
"version": "1.0.5",
"version": "1.0.4",
"description": "Prisma adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -52,4 +52,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -20,6 +20,7 @@ model User {
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
@@ -34,10 +35,11 @@ model Account {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
@@ -49,5 +51,5 @@ model VerificationToken {
token String @unique
expires DateTime
@@id([identifier, token])
@@unique([identifier, token])
}

View File

@@ -4,7 +4,8 @@ datasource db {
}
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
previewFeatures = ["mongoDb"]
}
model Account {

View File

@@ -10,7 +10,7 @@ generator client {
model User {
id String @id @default(cuid())
name String?
email String @unique
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
@@ -18,6 +18,7 @@ model User {
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
@@ -32,10 +33,11 @@ model Account {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
@@ -47,5 +49,5 @@ model VerificationToken {
token String @unique
expires DateTime
@@id([identifier, token])
@@unique([identifier, token])
}

View File

@@ -1,5 +1,5 @@
import type { PrismaClient, Prisma } from "@prisma/client"
import type { Adapter, AdapterAccount } from "next-auth/adapters"
import type { Adapter } from "next-auth/adapters"
export function PrismaAdapter(p: PrismaClient): Adapter {
return {
@@ -15,12 +15,9 @@ export function PrismaAdapter(p: PrismaClient): Adapter {
},
updateUser: ({ id, ...data }) => p.user.update({ where: { id }, data }),
deleteUser: (id) => p.user.delete({ where: { id } }),
linkAccount: (data) =>
p.account.create({ data }) as unknown as AdapterAccount,
linkAccount: (data) => p.account.create({ data }) as any,
unlinkAccount: (provider_providerAccountId) =>
p.account.delete({
where: { provider_providerAccountId },
}) as unknown as AdapterAccount,
p.account.delete({ where: { provider_providerAccountId } }) as any,
async getSessionAndUser(sessionToken) {
const userAndSession = await p.session.findUnique({
where: { sessionToken },
@@ -36,18 +33,17 @@ export function PrismaAdapter(p: PrismaClient): Adapter {
deleteSession: (sessionToken) =>
p.session.delete({ where: { sessionToken } }),
async createVerificationToken(data) {
const verificationToken = await p.verificationToken.create({ data })
// @ts-expect-errors // MongoDB needs an ID, but we don't
if (verificationToken.id) delete verificationToken.id
// @ts-ignore
const { id: _, ...verificationToken } = await p.verificationToken.create({
data,
})
return verificationToken
},
async useVerificationToken(identifier_token) {
try {
const verificationToken = await p.verificationToken.delete({
where: { identifier_token },
})
// @ts-expect-errors // MongoDB needs an ID, but we don't
if (verificationToken.id) delete verificationToken.id
// @ts-ignore
const { id: _, ...verificationToken } =
await p.verificationToken.delete({ where: { identifier_token } })
return verificationToken
} catch (error) {
// If token already used/deleted, just return null

View File

@@ -40,9 +40,9 @@ runBasicTests({
where: { identifier_token },
})
if (!result) return null
// @ts-ignore // MongoDB needs an ID, but we don't
delete result.id
return result
// @ts-ignore
const { id: _, ...verificationToken } = result
return verificationToken
},
},
})

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/sequelize-adapter",
"version": "1.0.6",
"version": "1.0.5",
"description": "Sequelize adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",

View File

@@ -1,7 +1,7 @@
import type { Account as AdapterAccount } from "next-auth"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/typeorm-legacy-adapter",
"version": "2.0.1",
"version": "2.0.0",
"description": "TypeORM (legacy) adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",

View File

@@ -1,10 +1,6 @@
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
} from "next-auth/adapters"
import type { Adapter, AdapterSession, AdapterUser } from "next-auth/adapters"
import { DataSourceOptions, DataSource, EntityManager } from "typeorm"
import type { Account } from "next-auth"
import * as defaultEntities from "./entities"
import { parseDataSourceConfig, updateConnectionEntities } from "./utils"
@@ -91,7 +87,7 @@ export function TypeORMLegacyAdapter(
},
async getUserByAccount(provider_providerAccountId) {
const m = await getManager(c)
const account = await m.findOne<AdapterAccount & { user: AdapterUser }>(
const account = await m.findOne<Account & { user: AdapterUser }>(
"AccountEntity",
{ where: provider_providerAccountId, relations: ["user"] }
)
@@ -119,8 +115,9 @@ export function TypeORMLegacyAdapter(
},
async unlinkAccount(providerAccountId) {
const m = await getManager(c)
await m.delete<AdapterAccount>("AccountEntity", providerAccountId)
await m.delete<Account>("AccountEntity", providerAccountId)
},
// @ts-expect-error
async createSession(data) {
const m = await getManager(c)
const session = await m.save("SessionEntity", data)

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/upstash-redis-adapter",
"version": "3.0.3",
"version": "3.0.2",
"description": "Upstash adapter for next-auth. It uses Upstash's connectionless (HTTP based) Redis client.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -49,4 +49,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,7 +1,7 @@
import type { Account as AdapterAccount } from "next-auth"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"
@@ -117,6 +117,7 @@ export function UpstashRedisAdapter(
const id = uuid()
// TypeScript thinks the emailVerified field is missing
// but all fields are copied directly from user, so it's there
// @ts-expect-error
return await setUser(id, { ...user, id })
},
getUser,
@@ -143,7 +144,10 @@ export function UpstashRedisAdapter(
const id = `${account.provider}:${account.providerAccountId}`
return await setAccount(id, { ...account, id })
},
createSession: (session) => setSession(session.sessionToken, session),
async createSession(session) {
const id = session.sessionToken
return await setSession(id, { ...session, id })
},
async getSessionAndUser(sessionToken) {
const session = await getSession(sessionToken)
if (!session) return null

View File

@@ -11,14 +11,6 @@ if (!process.env.UPSTASH_REDIS_URL || !process.env.UPSTASH_REDIS_KEY) {
process.exit(0)
}
if (process.env.CI) {
// TODO: Fix this
test('Skipping UpstashRedisAdapter tests in CI because of "Request failed" errors. Should revisit', () => {
expect(true).toBe(true)
})
process.exit(0)
}
const client = new Redis({
url: process.env.UPSTASH_REDIS_URL,
token: process.env.UPSTASH_REDIS_KEY,

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "4.13.0",
"version": "4.12.3",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
@@ -130,4 +130,4 @@
"engines": {
"node": "^12.19.0 || ^14.15.0 || ^16.13.0"
}
}
}

View File

@@ -2,15 +2,11 @@ import { Account, User, Awaitable } from "."
export interface AdapterUser extends User {
id: string
email: string
emailVerified: Date | null
}
export interface AdapterAccount extends Account {
userId: string
}
export interface AdapterSession {
id: string
/** A randomly generated value that is used to get hold of the session. */
sessionToken: string
/** Used to connect the session to a particular user */
@@ -59,30 +55,13 @@ export interface VerificationToken {
* [Adapters Overview](https://next-auth.js.org/adapters/overview) |
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
*/
export type Adapter<WithVerificationToken = boolean> = DefaultAdapter &
(WithVerificationToken extends true
? {
createVerificationToken: (
verificationToken: VerificationToken
) => Awaitable<VerificationToken | null | undefined>
/**
* Return verification token from the database
* and delete it so it cannot be used again.
*/
useVerificationToken: (params: {
identifier: string
token: string
}) => Awaitable<VerificationToken | null>
}
: {})
export interface DefaultAdapter {
export interface Adapter {
createUser: (user: Omit<AdapterUser, "id">) => Awaitable<AdapterUser>
getUser: (id: string) => Awaitable<AdapterUser | null>
getUserByEmail: (email: string) => Awaitable<AdapterUser | null>
/** Using the provider id and the id of the user for a specific account, get the user. */
getUserByAccount: (
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
providerAccountId: Pick<Account, "provider" | "providerAccountId">
) => Awaitable<AdapterUser | null>
updateUser: (user: Partial<AdapterUser>) => Awaitable<AdapterUser>
/** @todo Implement */
@@ -90,12 +69,12 @@ export interface DefaultAdapter {
userId: string
) => Promise<void> | Awaitable<AdapterUser | null | undefined>
linkAccount: (
account: AdapterAccount
) => Promise<void> | Awaitable<AdapterAccount | null | undefined>
account: Account
) => Promise<void> | Awaitable<Account | null | undefined>
/** @todo Implement */
unlinkAccount?: (
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
) => Promise<void> | Awaitable<AdapterAccount | undefined>
providerAccountId: Pick<Account, "provider" | "providerAccountId">
) => Promise<void> | Awaitable<Account | undefined>
/** Creates a session for the user and returns it. */
createSession: (session: {
sessionToken: string

View File

@@ -1,4 +1,5 @@
import type { EventCallbacks, LoggerInstance } from ".."
import type { Adapter } from "../adapters"
/**
* Same as the default `Error`, but it is JSON serializable.
@@ -57,11 +58,6 @@ export class MissingAdapter extends UnknownError {
code = "EMAIL_REQUIRES_ADAPTER_ERROR"
}
export class MissingAdapterMethods extends UnknownError {
name = "MissingAdapterMethodsError"
code = "MISSING_ADAPTER_METHODS_ERROR"
}
export class UnsupportedStrategy extends UnknownError {
name = "UnsupportedStrategyError"
code = "CALLBACK_CREDENTIALS_JWT_ERROR"
@@ -103,10 +99,10 @@ export function eventsErrorHandler(
}
/** Handles adapter induced errors. */
export function adapterErrorHandler<TAdapter>(
adapter: TAdapter | undefined,
export function adapterErrorHandler(
adapter: Adapter | undefined,
logger: LoggerInstance
): TAdapter | undefined {
): Adapter | undefined {
if (!adapter) return
return Object.keys(adapter).reduce<any>((acc, name) => {

View File

@@ -71,7 +71,6 @@ export async function init({
// and are request-specific.
url,
action,
// @ts-expect-errors
provider,
cookies: {
...cookie.defaultCookies(

View File

@@ -5,7 +5,6 @@ import {
MissingSecret,
UnsupportedStrategy,
InvalidCallbackUrl,
MissingAdapterMethods,
} from "../errors"
import parseUrl from "../../utils/parse-url"
import { defaultCookies } from "./cookie"
@@ -121,23 +120,8 @@ export function assertConfig(params: {
}
}
if (hasEmail) {
const { adapter } = options
if (!adapter) {
return new MissingAdapter("E-mail login requires an adapter.")
}
const missingMethods = [
"createVerificationToken",
"useVerificationToken",
"getUserByEmail",
].filter((method) => !adapter[method])
if (missingMethods.length) {
return new MissingAdapterMethods(
`Required adapter methods were missing: ${missingMethods.join(", ")}`
)
}
if (hasEmail && !options.adapter) {
return new MissingAdapter("E-mail login requires an adapter.")
}
if (!warned) {

View File

@@ -21,11 +21,11 @@ import type { SessionToken } from "./cookie"
*/
export default async function callbackHandler(params: {
sessionToken?: SessionToken
profile: User | AdapterUser | { email: string }
account: Account | null
profile: User
account: Account
options: InternalOptions
}) {
const { sessionToken, profile: _profile, account, options } = params
const { sessionToken, profile, account, options } = params
// Input validation
if (!account?.providerAccountId || !account.type)
throw new Error("Missing or invalid provider account")
@@ -42,11 +42,9 @@ export default async function callbackHandler(params: {
// If no adapter is configured then we don't have a database and cannot
// persist data; in this mode we just return a dummy session object.
if (!adapter) {
return { user: _profile as User, account }
return { user: profile, account, session: {} }
}
const profile = _profile as AdapterUser
const {
createUser,
updateUser,
@@ -86,7 +84,9 @@ export default async function callbackHandler(params: {
if (account.type === "email") {
// If signing in with an email, check if an account with the same email address exists already
const userByEmail = await getUserByEmail(profile.email)
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
// If they are not already signed in as the same user, this flow will
// sign them out of the current session and sign them in as the new user
@@ -101,7 +101,8 @@ export default async function callbackHandler(params: {
user = await updateUser({ id: userByEmail.id, emailVerified: new Date() })
await events.updateUser?.({ user })
} else {
const { id: _, ...newUser } = { ...profile, emailVerified: new Date() }
const newUser = { ...profile, emailVerified: new Date() }
delete (newUser as Omit<AdapterUser, "id">).id
// Create user account if there isn't one for the email address already
user = await createUser(newUser)
await events.createUser?.({ user })
@@ -197,7 +198,8 @@ export default async function callbackHandler(params: {
// If no account matching the same [provider].id or .email exists, we can
// create a new account for the user, link it to the OAuth acccount and
// create a new session for them so they are signed in with it.
const { id: _, ...newUser } = { ...profile, emailVerified: null }
const newUser = { ...profile, emailVerified: null }
delete (newUser as Omit<AdapterUser, "id">).id
user = await createUser(newUser)
await events.createUser?.({ user })
@@ -215,6 +217,4 @@ export default async function callbackHandler(params: {
return { session, user, isNewUser: true }
}
}
throw new Error("Unsupported account type")
}

View File

@@ -1,19 +0,0 @@
import type { InternalOptions } from "../../types"
export default async function getUserFromEmail({
email,
adapter,
withId = false,
}: {
email: string
adapter: InternalOptions<"email">["adapter"]
withId: boolean
}) {
const { getUserByEmail } = adapter
// If is an existing user return a user object (otherwise use placeholder)
return (email ? await getUserByEmail(email) : null) ?? withId
? { id: email, email }
: {
email,
}
}

View File

@@ -36,6 +36,7 @@ export default async function email(
theme,
}),
// Save in database
// @ts-expect-error // verified in `assertConfig`
adapter.createVerificationToken({
identifier,
token: hashToken(token, options),

View File

@@ -39,7 +39,10 @@ export default async function getAuthorizationUrl({
if (provider.version?.startsWith("1.")) {
const client = oAuth1Client(options)
const tokens = (await client.getOAuthRequestToken(params)) as any
const url = `${provider.authorization?.url}?${new URLSearchParams({
const url = `${
// @ts-expect-error
provider.authorization?.url ?? provider.authorization
}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params,
@@ -65,7 +68,7 @@ export default async function getAuthorizationUrl({
authorizationParams.nonce = nonce.value
cookies.push(nonce.cookie)
}
const pkce = await createPKCE(options)
if (pkce) {
authorizationParams.code_challenge = pkce.code_challenge

View File

@@ -7,10 +7,10 @@ import { useNonce } from "./nonce-handler"
import { OAuthCallbackError } from "../../errors"
import type { CallbackParamsType, OpenIDCallbackChecks } from "openid-client"
import type { LoggerInstance, Profile } from "../../.."
import type { Account, LoggerInstance, Profile } from "../../.."
import type { OAuthChecks, OAuthConfig } from "../../../providers"
import type { InternalOptions } from "../../types"
import type { RequestInternal } from "../.."
import type { RequestInternal, OutgoingResponse } from "../.."
import type { Cookie } from "../cookie"
export default async function oAuthCallback(params: {
@@ -19,7 +19,7 @@ export default async function oAuthCallback(params: {
body: RequestInternal["body"]
method: Required<RequestInternal>["method"]
cookies: RequestInternal["cookies"]
}) {
}): Promise<GetProfileResult & { cookies?: OutgoingResponse["cookies"] }> {
const { options, query, body, method, cookies } = params
const { logger, provider } = options
@@ -34,19 +34,23 @@ export default async function oAuthCallback(params: {
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", { body })
throw error
}
if (provider.version?.startsWith("1.")) {
try {
const client = await oAuth1Client(options)
// Handle OAuth v1.x
const { oauth_token, oauth_verifier } = query ?? {}
const tokens = (await (client as any).getOAuthAccessToken(
oauth_token,
// @ts-expect-error
const tokens: TokenSet = await client.getOAuthAccessToken(
oauth_token as string,
// @ts-expect-error
null,
oauth_verifier
)) as TokenSet
let profile: Profile = await (client as any).get(
provider.profileUrl,
)
// @ts-expect-error
let profile: Profile = await client.get(
(provider as any).profileUrl,
tokens.oauth_token,
tokens.oauth_token_secret
)
@@ -55,8 +59,7 @@ export default async function oAuthCallback(params: {
profile = JSON.parse(profile)
}
const newProfile = await getProfile({ profile, tokens, provider, logger })
return { ...newProfile, cookies: [] }
return await getProfile({ profile, tokens, provider, logger })
} catch (error) {
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error as Error)
throw error
@@ -79,7 +82,7 @@ export default async function oAuthCallback(params: {
const nonce = await useNonce(cookies?.[options.cookies.nonce.name], options)
if (nonce && provider.idToken) {
;(checks as OpenIDCallbackChecks).nonce = nonce.value
(checks as OpenIDCallbackChecks).nonce = nonce.value
resCookies.push(nonce.cookie)
}
@@ -99,10 +102,13 @@ export default async function oAuthCallback(params: {
body,
method,
}),
// @ts-expect-error
...provider.token?.params,
}
// @ts-expect-error
if (provider.token?.request) {
// @ts-expect-error
const response = await provider.token.request({
provider,
params,
@@ -122,7 +128,9 @@ export default async function oAuthCallback(params: {
}
let profile: Profile
// @ts-expect-error
if (provider.userinfo?.request) {
// @ts-expect-error
profile = await provider.userinfo.request({
provider,
tokens,
@@ -132,6 +140,7 @@ export default async function oAuthCallback(params: {
profile = tokens.claims()
} else {
profile = await client.userinfo(tokens, {
// @ts-expect-error
params: provider.userinfo?.params,
})
}
@@ -155,22 +164,25 @@ export interface GetProfileParams {
logger: LoggerInstance
}
export interface GetProfileResult {
// @ts-expect-error
profile: ReturnType<OAuthConfig["profile"]> | null
account: Omit<Account, "userId"> | null
OAuthProfile: Profile
}
/** Returns profile, raw profile and auth provider details */
async function getProfile({
profile: OAuthProfile,
tokens,
provider,
logger,
}: GetProfileParams) {
}: GetProfileParams): Promise<GetProfileResult> {
try {
logger.debug("PROFILE_DATA", { OAuthProfile })
// @ts-expect-error
const profile = await provider.profile(OAuthProfile, tokens)
profile.email = profile.email?.toLowerCase()
if (!profile.id)
throw new TypeError(
`Profile id is missing in ${provider.name} OAuth profile response`
)
// Return profile, raw profile and auth provider details
return {
profile,
@@ -190,9 +202,11 @@ async function getProfile({
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.
logger.error("OAUTH_PARSE_PROFILE_ERROR", {
error: error as Error,
logger.error("OAUTH_PARSE_PROFILE_ERROR", error as Error)
return {
profile: null,
account: null,
OAuthProfile,
})
}
}
}

View File

@@ -22,9 +22,13 @@ export async function openidClient(
} else {
issuer = new Issuer({
issuer: provider.issuer as string,
authorization_endpoint: provider.authorization?.url,
token_endpoint: provider.token?.url,
userinfo_endpoint: provider.userinfo?.url,
authorization_endpoint:
// @ts-expect-error
provider.authorization?.url ?? provider.authorization,
// @ts-expect-error
token_endpoint: provider.token?.url ?? provider.token,
// @ts-expect-error
userinfo_endpoint: provider.userinfo?.url ?? provider.userinfo,
})
}

View File

@@ -1,11 +1,7 @@
import { merge } from "../../utils/merge"
import type { InternalProvider } from "../types"
import type {
InternalOAuthConfig,
OAuthConfig,
Provider,
} from "../../providers"
import type { Provider } from "../../providers"
import type { InternalUrl } from "../../utils/parse-url"
/**
@@ -22,72 +18,52 @@ export default function parseProviders(params: {
} {
const { url, providerId } = params
const providers = params.providers.map<InternalProvider>(
({ options: userOptions, ...rest }) => {
if (rest.type === "oauth") {
const normalizedOptions = normalizeOAuthOptions(rest)
const normalizedUserOptions = normalizeOAuthOptions(userOptions, true)
return merge(normalizedOptions, {
...normalizedUserOptions,
signinUrl: `${url}/signin/${normalizedUserOptions?.id ?? rest.id}`,
callbackUrl: `${url}/callback/${
normalizedUserOptions?.id ?? rest.id
}`,
})
}
return merge(rest, {
...userOptions,
signinUrl: `${url}/signin/${userOptions?.id ?? rest.id}`,
callbackUrl: `${url}/callback/${userOptions?.id ?? rest.id}`,
})
}
)
const providers = params.providers.map(({ options, ...rest }) => {
const defaultOptions = normalizeProvider(rest as Provider)
const userOptions = normalizeProvider(options as Provider)
return {
providers,
provider: providers.find(({ id }) => id === providerId),
}
return merge(defaultOptions, {
...userOptions,
signinUrl: `${url}/signin/${userOptions?.id ?? rest.id}`,
callbackUrl: `${url}/callback/${userOptions?.id ?? rest.id}`,
})
})
const provider = providers.find(({ id }) => id === providerId)
return { providers, provider }
}
/**
* Transform OAuth options `authorization`, `token` and `profile` strings to `{ url: string; params: Record<string, string> }`
*/
function normalizeOAuthOptions(
oauthOptions?: Partial<OAuthConfig<any>> | Record<string, unknown>,
isUserOptions = false
) {
if (!oauthOptions) return
function normalizeProvider(provider?: Provider) {
if (!provider) return
const normalized = Object.entries(oauthOptions).reduce<
InternalOAuthConfig<Record<string, unknown>>
>(
(acc, [key, value]) => {
if (
["authorization", "token", "userinfo"].includes(key) &&
typeof value === "string"
) {
const url = new URL(value)
acc[key] = {
url: `${url.origin}${url.pathname}`,
params: Object.fromEntries(url.searchParams ?? []),
}
} else {
acc[key] = value
const normalized: InternalProvider = Object.entries(
provider
).reduce<InternalProvider>((acc, [key, value]) => {
if (
["authorization", "token", "userinfo"].includes(key) &&
typeof value === "string"
) {
const url = new URL(value)
acc[key] = {
url: `${url.origin}${url.pathname}`,
params: Object.fromEntries(url.searchParams ?? []),
}
} else {
acc[key] = value
}
return acc
},
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter
{} as any
)
return acc
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter, @typescript-eslint/consistent-type-assertions
}, {} as any)
if (!isUserOptions && !normalized.version?.startsWith("1.")) {
if (normalized.type === "oauth" && !normalized.version?.startsWith("1.")) {
// If provider has as an "openid-configuration" well-known endpoint
// or an "openid" scope request, it will also likely be able to receive an `id_token`
// Only do this if this function is not called with user options to avoid overriding in later stage.
normalized.idToken = Boolean(
normalized.idToken ??
normalized.wellKnown?.includes("openid-configuration") ??
// @ts-expect-error
normalized.authorization?.params?.scope?.includes("openid")
)

View File

@@ -1,17 +1,15 @@
import oAuthCallback from "../lib/oauth/callback"
import callbackHandler from "../lib/callback-handler"
import { hashToken } from "../lib/utils"
import getUserFromEmail from "../lib/email/getUserFromEmail"
import type { InternalOptions } from "../types"
import type { RequestInternal, OutgoingResponse } from ".."
import type { Cookie, SessionStore } from "../lib/cookie"
import type { User } from "../.."
import type { AdapterSession } from "../../adapters"
/** Handle callbacks from login services */
export default async function callback(params: {
options: InternalOptions
options: InternalOptions<"oauth" | "credentials" | "email">
query: RequestInternal["query"]
method: Required<RequestInternal>["method"]
body: RequestInternal["body"]
@@ -52,7 +50,7 @@ export default async function callback(params: {
cookies: params.cookies,
})
if (oauthCookies.length) cookies.push(...oauthCookies)
if (oauthCookies) cookies.push(...oauthCookies)
try {
// Make it easier to debug when adding a new provider
@@ -70,7 +68,7 @@ export default async function callback(params: {
// Note: In oAuthCallback an error is logged with debug info, so it
// should at least be visible to developers what happened if it is an
// error with the provider.
if (!profile || !account || !OAuthProfile) {
if (!profile) {
return { redirect: `${url}/signin`, cookies }
}
@@ -82,6 +80,7 @@ export default async function callback(params: {
if (adapter) {
const { getUserByAccount } = adapter
const userByAccount = await getUserByAccount({
// @ts-expect-error
providerAccountId: account.providerAccountId,
provider: provider.id,
})
@@ -92,6 +91,7 @@ export default async function callback(params: {
try {
const isAllowed = await callbacks.signIn({
user: userOrProfile,
// @ts-expect-error
account,
profile: OAuthProfile,
})
@@ -110,9 +110,11 @@ export default async function callback(params: {
}
// Sign user in
// @ts-expect-error
const { user, session, isNewUser } = await callbackHandler({
sessionToken: sessionStore.value,
profile,
// @ts-expect-error
account,
options,
})
@@ -127,6 +129,7 @@ export default async function callback(params: {
const token = await callbacks.jwt({
token: defaultToken,
user,
// @ts-expect-error
account,
profile: OAuthProfile,
isNewUser,
@@ -147,10 +150,10 @@ export default async function callback(params: {
// Save Session Token in cookie
cookies.push({
name: options.cookies.sessionToken.name,
value: (session as AdapterSession).sessionToken,
value: session.sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: (session as AdapterSession).expires,
expires: session.expires,
},
})
}
@@ -198,16 +201,14 @@ export default async function callback(params: {
}
} else if (provider.type === "email") {
try {
const token = query?.token as string | undefined
const identifier = query?.email as string | undefined
// Verified in `assertConfig`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { useVerificationToken, getUserByEmail } = adapter!
// If these are missing, the sign-in URL was manually opened without these params or the `sendVerificationRequest` method did not send the link correctly in the email.
if (!token || !identifier) {
return { redirect: `${url}/error?error=configuration`, cookies }
}
const token = query?.token
const identifier = query?.email
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
const invite = await adapter.useVerificationToken({
const invite = await useVerificationToken?.({
identifier,
token: hashToken(token, options),
})
@@ -217,23 +218,29 @@ export default async function callback(params: {
return { redirect: `${url}/error?error=Verification`, cookies }
}
const profile = await getUserFromEmail({
// If it is an existing user, use that, otherwise use a placeholder
const profile = (identifier
? await getUserByEmail(identifier)
: null) ?? {
email: identifier,
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
adapter,
})
}
/** @type {import("src").Account} */
const account = {
providerAccountId: profile.email,
type: "email" as const,
type: "email",
provider: provider.id,
}
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn({
// @ts-expect-error
user: profile,
// @ts-expect-error
account,
// @ts-expect-error
email: { email: identifier },
})
if (!signInCallbackResponse) {
return { redirect: `${url}/error?error=AccessDenied`, cookies }
@@ -250,9 +257,12 @@ export default async function callback(params: {
}
// Sign user in
// @ts-expect-error
const { user, session, isNewUser } = await callbackHandler({
sessionToken: sessionStore.value,
// @ts-expect-error
profile,
// @ts-expect-error
account,
options,
})
@@ -267,6 +277,7 @@ export default async function callback(params: {
const token = await callbacks.jwt({
token: defaultToken,
user,
// @ts-expect-error
account,
isNewUser,
})
@@ -286,14 +297,15 @@ export default async function callback(params: {
// Save Session Token in cookie
cookies.push({
name: options.cookies.sessionToken.name,
value: (session as AdapterSession).sessionToken,
value: session.sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: (session as AdapterSession).expires,
expires: session.expires,
},
})
}
// @ts-expect-error
await events.signIn?.({ user, account, isNewUser })
// Handle first logins on new accounts

View File

@@ -1,9 +1,8 @@
import getAuthorizationUrl from "../lib/oauth/authorization-url"
import emailSignin from "../lib/email/signin"
import getUserFromEmail from "../lib/email/getUserFromEmail"
import type { RequestInternal, OutgoingResponse } from ".."
import type { InternalOptions } from "../types"
import type { Account } from "../.."
import type { Account, User } from "../.."
/** Handle requests to /api/auth/signin */
export default async function signin(params: {
@@ -12,7 +11,7 @@ export default async function signin(params: {
body: RequestInternal["body"]
}): Promise<OutgoingResponse> {
const { options, query, body } = params
const { url, callbacks, logger, provider } = options
const { url, adapter, callbacks, logger, provider } = options
if (!provider.type) {
return {
@@ -55,12 +54,14 @@ export default async function signin(params: {
return { redirect: `${url}/error?error=EmailSignin` }
}
const user = await getUserFromEmail({
// Verified in `assertConfig`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { getUserByEmail } = adapter!
// If is an existing user return a user object (otherwise use placeholder)
const user: User = (email ? await getUserByEmail(email) : null) ?? {
email,
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
adapter: options.adapter,
withId: true,
})
id: email,
}
const account: Account = {
providerAccountId: email,
@@ -71,6 +72,7 @@ export default async function signin(params: {
// Check if user is allowed to sign in
try {
// @ts-expect-error
const signInCallbackResponse = await callbacks.signIn({
user,
account,

View File

@@ -1,11 +1,11 @@
import type { Adapter, AdapterUser } from "../adapters"
import type { Adapter } from "../adapters"
import type {
Provider,
CredentialInput,
ProviderType,
OAuthConfig,
EmailConfig,
CredentialsConfig,
InternalOAuthConfig,
} from "../providers"
import type { TokenSetParameters } from "openid-client"
import type { JWT, JWTOptions } from "../jwt"
@@ -231,7 +231,7 @@ export type TokenSet = TokenSetParameters
* Usually contains information about the provider being used
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
*/
export interface Account extends Partial<TokenSet> {
export interface DefaultAccount extends Partial<TokenSet> {
/**
* This value depends on the type of the provider being used to create the account.
* - oauth: The OAuth account's id, returned from the `profile()` callback.
@@ -240,23 +240,30 @@ export interface Account extends Partial<TokenSet> {
*/
providerAccountId: string
/** id of the user this account belongs to. */
userId?: string
userId: string
/** id of the provider used for this account */
provider: string
/** Provider's type for this account */
type: ProviderType
}
/** The OAuth profile returned from your provider */
export interface Profile {
export interface Account extends Record<string, unknown>, DefaultAccount {}
export interface DefaultProfile {
sub?: string
name?: string
email?: string
image?: string
}
/** The OAuth profile returned from your provider */
export interface Profile extends Record<string, unknown>, DefaultProfile {}
/** [Documentation](https://next-auth.js.org/configuration/callbacks) */
export interface CallbacksOptions<P = Profile, A = Account> {
export interface CallbacksOptions<
P extends Record<string, unknown> = Profile,
A extends Record<string, unknown> = Account
> {
/**
* Use this callback to control if a user is allowed to sign in.
* Returning true will continue the sign-in flow.
@@ -265,13 +272,13 @@ export interface CallbacksOptions<P = Profile, A = Account> {
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
*/
signIn: (params: {
user: User | { email: string }
account: A | null
user: User
account: A
/**
* If OAuth provider is used, it contains the full
* OAuth profile returned by your provider.
*/
profile?: P
profile: P & Record<string, unknown>
/**
* If Email provider is used, on the first call, it contains a
* `verificationRequest: true` property to indicate it is being triggered in the verification request flow.
@@ -280,7 +287,7 @@ export interface CallbacksOptions<P = Profile, A = Account> {
* to avoid sending emails to addresses or domains on a blocklist or to only explicitly generate them
* for email address in an allow list.
*/
email?: {
email: {
verificationRequest?: boolean
}
/** If Credentials provider is used, it contains the user credentials */
@@ -334,8 +341,8 @@ export interface CallbacksOptions<P = Profile, A = Account> {
*/
jwt: (params: {
token: JWT
user?: User | AdapterUser
account?: A | null
user?: User
account?: A
profile?: P
isNewUser?: boolean
}) => Awaitable<JWT>
@@ -371,7 +378,7 @@ export interface EventCallbacks {
*/
signIn: (message: {
user: User
account: Account | null
account: Account
profile?: Profile
isNewUser?: boolean
}) => Awaitable<void>
@@ -385,9 +392,9 @@ export interface EventCallbacks {
createUser: (message: { user: User }) => Awaitable<void>
updateUser: (message: { user: User }) => Awaitable<void>
linkAccount: (message: {
user: User | AdapterUser | { email: string }
user: User
account: Account
profile: User | AdapterUser | { email: string }
profile: User
}) => Awaitable<void>
/**
* The message object will contain one of these depending on
@@ -413,7 +420,7 @@ export interface PagesOptions {
export type ISODateString = string
export interface DefaultSession {
export interface DefaultSession extends Record<string, unknown> {
user?: {
name?: string | null
email?: string | null
@@ -431,7 +438,7 @@ export interface DefaultSession {
* [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends DefaultSession {}
export interface Session extends Record<string, unknown>, DefaultSession {}
export type SessionStrategy = "jwt" | "database"
@@ -487,13 +494,13 @@ export interface DefaultUser {
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
*/
export interface User extends DefaultUser {}
export interface User extends Record<string, unknown>, DefaultUser {}
// Below are types that are only supposed be used by next-auth internally
/** @internal */
export type InternalProvider<T = ProviderType> = (T extends "oauth"
? InternalOAuthConfig<any>
export type InternalProvider<T extends ProviderType = any> = (T extends "oauth"
? OAuthConfig<any>
: T extends "email"
? EmailConfig
: T extends "credentials"
@@ -515,10 +522,7 @@ export type NextAuthAction =
| "_log"
/** @internal */
export interface InternalOptions<
TProviderType = ProviderType,
WithVerificationToken = TProviderType extends "email" ? true : false
> {
export interface InternalOptions<T extends ProviderType = any> {
providers: InternalProvider[]
/**
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel.
@@ -526,7 +530,9 @@ export interface InternalOptions<
*/
url: InternalUrl
action: NextAuthAction
provider: InternalProvider<TProviderType>
provider: T extends string
? InternalProvider<T>
: InternalProvider<T> | undefined
csrfToken?: string
csrfTokenVerified?: boolean
secret: string
@@ -537,9 +543,7 @@ export interface InternalOptions<
pages: Partial<PagesOptions>
jwt: JWTOptions
events: Partial<EventCallbacks>
adapter: WithVerificationToken extends true
? Adapter<WithVerificationToken>
: Adapter<WithVerificationToken> | undefined
adapter?: Adapter
callbacks: CallbacksOptions
cookies: CookiesOptions
callbackUrl: string

View File

@@ -101,17 +101,17 @@ async function handleMiddleware(
options: NextAuthMiddlewareOptions | undefined,
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult>
) {
const { pathname, search, origin, basePath } = req.nextUrl
const { pathname, search, origin } = req.nextUrl
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
const errorPage = options?.pages?.error ?? "/api/auth/error"
const authPath = parseUrl(process.env.NEXTAUTH_URL).path
const basePath = parseUrl(process.env.NEXTAUTH_URL).path
const publicPaths = ["/_next", "/favicon.ico"]
// Avoid infinite redirects/invalid response
// on paths that never require authentication
if (
`${basePath}${pathname}`.startsWith(authPath) ||
pathname.startsWith(basePath) ||
[signInPage, errorPage].includes(pathname) ||
publicPaths.some((p) => pathname.startsWith(p))
) {
@@ -125,7 +125,7 @@ async function handleMiddleware(
`\nhttps://next-auth.js.org/errors#no_secret`
)
const errorUrl = new URL(`${basePath}${errorPage}`, origin)
const errorUrl = new URL(errorPage, origin)
errorUrl.searchParams.append("error", "Configuration")
return NextResponse.redirect(errorUrl)
@@ -145,8 +145,8 @@ async function handleMiddleware(
if (isAuthorized) return await onSuccess?.(token)
// the user is not logged in, redirect to the sign-in page
const signInUrl = new URL(`${basePath}${signInPage}`, origin)
signInUrl.searchParams.append("callbackUrl", `${basePath}${pathname}${search}`)
const signInUrl = new URL(signInPage, origin)
signInUrl.searchParams.append("callbackUrl", `${pathname}${search}`)
return NextResponse.redirect(signInUrl)
}

View File

@@ -1,25 +1,28 @@
import type { OAuthConfig, OAuthUserConfig } from "."
interface HubSpotProfile extends Record<string, any> {
// TODO: figure out additional fields, for now using
// TODO: figure out additional fields, for now using
// https://legacydocs.hubspot.com/docs/methods/oauth2/get-access-token-information
user: string
user_id: string
user: string,
user_id: string,
hub_domain: string
hub_id: string
hub_domain: string,
hub_id: string,
}
const HubSpotConfig = {
authorizationUrl: "https://app.hubspot.com/oauth/authorize",
tokenUrl: "https://api.hubapi.com/oauth/v1/token",
profileUrl: "https://api.hubapi.com/oauth/v1/access-tokens",
profileUrl: "https://api.hubapi.com/oauth/v1/access-tokens"
}
export default function HubSpot<P extends HubSpotProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "hubspot",
name: "HubSpot",
@@ -33,6 +36,7 @@ export default function HubSpot<P extends HubSpotProfile>(
scope: "oauth",
client_id: options.clientId,
},
},
client: {
token_endpoint_auth_method: "client_secret_post",
@@ -41,27 +45,33 @@ export default function HubSpot<P extends HubSpotProfile>(
userinfo: {
url: HubSpotConfig.profileUrl,
async request(context) {
const url = `${HubSpotConfig.profileUrl}/${context.tokens.access_token}`
const url = `${HubSpotConfig.profileUrl}/${context.tokens.access_token}`;
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
},
method: "GET",
})
});
return await response.json()
},
const userInfo = await response.json();
return { userInfo }
}
},
profile(profile) {
return {
id: profile.user_id,
name: profile.user,
email: profile.user,
// TODO: get image from profile once it's available
const { userInfo } = profile
return {
id: userInfo.user_id,
name: userInfo.user,
email: userInfo.user,
// TODO: get image from profile once it's available
// Details available https://community.hubspot.com/t5/APIs-Integrations/Profile-photo-is-not-retrieved-with-User-API/m-p/325521
image: null,
image: null
}
},
options,

View File

@@ -110,7 +110,7 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
userinfo?: string | UserinfoEndpointHandler
type: "oauth"
version?: string
profile: (profile: P, tokens: TokenSet) => Awaitable<User>
profile?: (profile: P, tokens: TokenSet) => Awaitable<User & { id: string }>
checks?: ChecksType | ChecksType[]
client?: Partial<ClientMetadata>
jwks?: { keys: JWK[] }
@@ -147,14 +147,6 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
encoding?: string
}
/** @internal */
export interface InternalOAuthConfig<P>
extends Omit<OAuthConfig<P>, "authorization" | "token" | "userinfo"> {
authorization?: AuthorizationEndpointHandler
token?: TokenEndpointHandler
userinfo?: UserinfoEndpointHandler
}
export type OAuthUserConfig<P> = Omit<
Partial<OAuthConfig<P>>,
"options" | "type"

View File

@@ -1,51 +0,0 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface ZitadelProfile extends Record<string, any> {
amr: string // Authentication Method References as defined in RFC8176
aud: string // The audience of the token, by default all client id's and the project id are included
auth_time: number // Unix time of the authentication
azp: string // Client id of the client who requested the token
email: string // Email Address of the subject
email_verified: boolean // if the email was verified by ZITADEL
exp: number // Time the token expires (as unix time)
family_name: string // The subjects family name
given_name: string // Given name of the subject
gender: string // Gender of the subject
iat: number // Time of the token was issued at (as unix time)
iss: string // Issuing domain of a token
jti: string // Unique id of the token
locale: string // Language from the subject
name: string // The subjects full name
nbf: number // Time the token must not be used before (as unix time)
picture: string // The subjects profile picture
phone: string // Phone number provided by the user
phone_verified: boolean // if the phonenumber was verified by ZITADEL
preferred_username: string // ZITADEL's login name of the user. Consist of username@primarydomain
sub: string // Subject ID of the user
}
export default function Zitadel<P extends ZitadelProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
const { issuer } = options
return {
id: "zitadel",
name: "ZITADEL",
type: "oauth",
version: "2",
wellKnown: `${issuer}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid email profile" } },
idToken: true,
checks: ["pkce", "state"],
async profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
}
},
options,
}
}

View File

@@ -1,11 +1,5 @@
import {
InvalidCallbackUrl,
MissingAdapter,
MissingAdapterMethods,
MissingSecret,
} from "../src/core/errors"
import { InvalidCallbackUrl, MissingSecret } from "../src/core/errors"
import { handler } from "./lib"
import EmailProvider from "../src/providers/email"
it("Show error page if secret is not defined", async () => {
const { res, log } = await handler(
@@ -20,48 +14,6 @@ it("Show error page if secret is not defined", async () => {
expect(log.error).toBeCalledWith("NO_SECRET", expect.any(MissingSecret))
})
it("Show error page if adapter is missing functions when using with email", async () => {
const sendVerificationRequest = jest.fn()
const missingFunctionAdapter: any = {}
const { res, log } = await handler(
{
adapter: missingFunctionAdapter,
providers: [EmailProvider({ sendVerificationRequest })],
secret: "secret",
},
{ prod: true }
)
expect(res.status).toBe(500)
expect(res.html).toMatch(/there is a problem with the server configuration./i)
expect(res.html).toMatch(/check the server logs for more information./i)
expect(log.error).toBeCalledWith(
"MISSING_ADAPTER_METHODS_ERROR",
expect.any(MissingAdapterMethods)
)
})
it("Show error page if adapter is not configured when using with email", async () => {
const sendVerificationRequest = jest.fn()
const { res, log } = await handler(
{
providers: [EmailProvider({ sendVerificationRequest })],
secret: "secret",
},
{ prod: true }
)
expect(res.status).toBe(500)
expect(res.html).toMatch(/there is a problem with the server configuration./i)
expect(res.html).toMatch(/check the server logs for more information./i)
expect(log.error).toBeCalledWith(
"EMAIL_REQUIRES_ADAPTER_ERROR",
expect.any(MissingAdapter)
)
})
it("Should show configuration error page on invalid `callbackUrl`", async () => {
const { res, log } = await handler(
{ providers: [] },

View File

@@ -156,7 +156,6 @@ it("Redirect to error page if multiple addresses aren't allowed", async () => {
expect(signIn).toBeCalledTimes(0)
expect(sendVerificationRequest).toBeCalledTimes(0)
// @ts-expect-error
expect(log.error.mock.calls[0]).toEqual([
"SIGNIN_EMAIL_ERROR",
{ error, providerId: "email" },

View File

@@ -59,10 +59,10 @@ export function createCSRF() {
}
export function mockAdapter(): Adapter {
// @ts-expect-error
const adapter: Adapter = {
createVerificationToken: jest.fn(() => {}),
useVerificationToken: jest.fn(() => {}),
getUserByEmail: jest.fn(() => {}),
}
return adapter
return adapter;
}

View File

@@ -38,58 +38,3 @@ it("should not redirect on public paths", async () => {
const res = await handleMiddleware(req, null as any)
expect(res).toBeUndefined()
})
it("should redirect according to nextUrl basePath", async () => {
const options: NextAuthMiddlewareOptions = {
secret: "secret"
}
const nextUrl: any = {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
basePath: "/custom-base-path",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null as any)
expect(res).toBeDefined()
expect(res.status).toEqual(307)
expect(res.headers.get('location')).toContain("http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA")
})
it("should redirect according to nextUrl basePath", async () => {
// given
const options: NextAuthMiddlewareOptions = {
secret: "secret"
}
const handleMiddleware = withAuth(options) as NextMiddleware
// when
const res = await handleMiddleware({
nextUrl: {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
basePath: "/custom-base-path"
}, headers: { authorization: "" }
} as any, null as any)
// then
expect(res).toBeDefined()
expect(res.status).toEqual(307)
expect(res.headers.get("location")).toContain("http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA")
// and when follow redirect
const resFromRedirectedUrl = await handleMiddleware({
nextUrl: {
pathname: "/api/auth/signin",
search: "callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA",
origin: "http://127.0.0.1",
basePath: "/custom-base-path"
}, headers: { authorization: "" }
} as any, null as any)
// then return sign in page
expect(resFromRedirectedUrl).toBeUndefined()
})