mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
fix(providers): make string endpoint handlers overrideable (#2842)
* chore: remove `console.log` * chore(ts): improve `InternalProvider` type * refactor(ts): convert some files to TypeScript * fix(providers): make string endpoint handlers overrideable
This commit is contained in:
@@ -11,22 +11,38 @@ import type {
|
||||
Awaitable,
|
||||
} from ".."
|
||||
|
||||
import type { Provider } from "../providers"
|
||||
import type {
|
||||
OAuthConfig,
|
||||
EmailConfig,
|
||||
CredentialsConfig,
|
||||
AuthorizationEndpointHandler,
|
||||
TokenEndpointHandler,
|
||||
UserinfoEndpointHandler,
|
||||
ProviderType,
|
||||
} from "../providers"
|
||||
import type { JWTOptions } from "../jwt"
|
||||
import type { Adapter } from "../adapters"
|
||||
|
||||
// Below are types that are only supposed be used by next-auth internally
|
||||
|
||||
/** @internal */
|
||||
export type InternalProvider = Provider & {
|
||||
export type InternalProvider<T extends ProviderType = any> = (T extends "oauth"
|
||||
? Omit<OAuthConfig<any>, "authorization" | "token" | "userinfo"> & {
|
||||
authorization: AuthorizationEndpointHandler
|
||||
token: TokenEndpointHandler
|
||||
userinfo: UserinfoEndpointHandler
|
||||
}
|
||||
: T extends "email"
|
||||
? EmailConfig
|
||||
: T extends "credentials"
|
||||
? CredentialsConfig
|
||||
: never) & {
|
||||
signinUrl: string
|
||||
callbackUrl: string
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalOptions<
|
||||
P extends InternalProvider = InternalProvider
|
||||
> {
|
||||
export interface InternalOptions<T extends ProviderType = any> {
|
||||
providers: InternalProvider[]
|
||||
baseUrl: string
|
||||
basePath: string
|
||||
@@ -39,7 +55,9 @@ export interface InternalOptions<
|
||||
| "callback"
|
||||
| "verify-request"
|
||||
| "error"
|
||||
provider: P
|
||||
provider: T extends string
|
||||
? InternalProvider<T>
|
||||
: InternalProvider<T> | undefined
|
||||
csrfToken?: string
|
||||
csrfTokenVerified?: boolean
|
||||
secret: string
|
||||
|
||||
@@ -72,7 +72,6 @@ export default function Email(options: EmailUserConfig): EmailConfig {
|
||||
provider: { server, from },
|
||||
}) {
|
||||
const { host } = new URL(url)
|
||||
console.log(server)
|
||||
const transport = createTransport(server)
|
||||
await transport.sendMail({
|
||||
to: email,
|
||||
|
||||
@@ -50,9 +50,39 @@ interface AdvancedEndpointHandler<P extends UrlParams, C, R> {
|
||||
}
|
||||
|
||||
/** Either an URL (containing all the parameters) or an object with more granular control. */
|
||||
type EndpointHandler<P extends UrlParams, C = any, R = any> =
|
||||
| string
|
||||
| AdvancedEndpointHandler<P, C, R>
|
||||
export type EndpointHandler<
|
||||
P extends UrlParams,
|
||||
C = any,
|
||||
R = any
|
||||
> = AdvancedEndpointHandler<P, C, R>
|
||||
|
||||
export type AuthorizationEndpointHandler =
|
||||
EndpointHandler<AuthorizationParameters>
|
||||
|
||||
export type TokenEndpointHandler = EndpointHandler<
|
||||
UrlParams,
|
||||
{
|
||||
/**
|
||||
* Parameters extracted from the request to the `/api/auth/callback/:providerId` endpoint.
|
||||
* Contains params like `state`.
|
||||
*/
|
||||
params: CallbackParamsType
|
||||
/**
|
||||
* When using this custom flow, make sure to do all the necessary security checks.
|
||||
* Thist object contains parameters you have to match against the request to make sure it is valid.
|
||||
*/
|
||||
checks: OAuthChecks
|
||||
},
|
||||
{
|
||||
tokens: TokenSet
|
||||
}
|
||||
>
|
||||
|
||||
export type UserinfoEndpointHandler = EndpointHandler<
|
||||
UrlParams,
|
||||
{ tokens: TokenSet },
|
||||
Profile
|
||||
>
|
||||
|
||||
export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
||||
/**
|
||||
@@ -70,40 +100,11 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
||||
*
|
||||
* [Authorization endpoint](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1)
|
||||
*/
|
||||
authorization?: EndpointHandler<AuthorizationParameters>
|
||||
/**
|
||||
* Endpoint that returns OAuth 2/OIDC tokens and information about them.
|
||||
* This includes `access_token`, `id_token`, `refresh_token`, etc.
|
||||
*
|
||||
* [Token endpoint](https://datatracker.ietf.org/doc/html/rfc6749#section-3.2)
|
||||
*/
|
||||
token?: EndpointHandler<
|
||||
UrlParams,
|
||||
{
|
||||
/**
|
||||
* Parameters extracted from the request to the `/api/auth/callback/:providerId` endpoint.
|
||||
* Contains params like `state`.
|
||||
*/
|
||||
params: CallbackParamsType
|
||||
/**
|
||||
* When using this custom flow, make sure to do all the necessary security checks.
|
||||
* Thist object contains parameters you have to match against the request to make sure it is valid.
|
||||
*/
|
||||
checks: OAuthChecks
|
||||
},
|
||||
{ tokens: TokenSet }
|
||||
>
|
||||
/**
|
||||
* When using an OAuth 2 provider, the user information must be requested
|
||||
* through an additional request from the userinfo endpoint.
|
||||
*
|
||||
* [Userinfo endpoint](https://www.oauth.com/oauth2-servers/signing-in-with-google/verifying-the-user-info)
|
||||
*/
|
||||
userinfo?: EndpointHandler<UrlParams, { tokens: TokenSet }, Profile>
|
||||
authorization?: string | AuthorizationEndpointHandler
|
||||
token?: string | TokenEndpointHandler
|
||||
userinfo?: string | UserinfoEndpointHandler
|
||||
type: "oauth"
|
||||
version?: string
|
||||
accessTokenUrl?: string
|
||||
requestTokenUrl?: string
|
||||
profile?: (profile: P, tokens: TokenSet) => Awaitable<User & { id: string }>
|
||||
checks?: ChecksType | ChecksType[]
|
||||
clientId?: string
|
||||
@@ -133,6 +134,11 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
||||
* with the default configuration.
|
||||
*/
|
||||
options?: OAuthUserConfig<P>
|
||||
|
||||
// These are kept around for backwards compatibility with OAuth 1.x
|
||||
accessTokenUrl?: string
|
||||
requestTokenUrl?: string
|
||||
encoding?: string
|
||||
}
|
||||
|
||||
export type OAuthUserConfig<P> = Omit<
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function Spotify(options) {
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface SpotifyImage {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface SpotifyProfile {
|
||||
id: string
|
||||
display_name: string
|
||||
email: string
|
||||
images: SpotifyImage[]
|
||||
}
|
||||
export default function Spotify<P extends Record<string, any> = SpotifyProfile>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "spotify",
|
||||
name: "Spotify",
|
||||
@@ -73,33 +73,23 @@ async function NextAuthHandler(
|
||||
|
||||
const secret = createSecret({ userOptions, basePath, baseUrl })
|
||||
|
||||
const providers = parseProviders({
|
||||
const { providers, provider } = parseProviders({
|
||||
providers: userOptions.providers,
|
||||
base: `${baseUrl}${basePath}`,
|
||||
providerId: providerId as string | undefined,
|
||||
})
|
||||
|
||||
const provider = providers.find(({ id }) => id === providerId)
|
||||
|
||||
// Checks only work on OAuth 2.x + OIDC providers
|
||||
if (
|
||||
provider?.type === "oauth" &&
|
||||
!provider.version?.startsWith("1.") &&
|
||||
!provider.checks
|
||||
) {
|
||||
provider.checks = ["state"]
|
||||
}
|
||||
|
||||
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default
|
||||
|
||||
// User provided options are overriden by other options,
|
||||
// except for the options with special handling above
|
||||
const options: InternalOptions<any> = {
|
||||
const options: InternalOptions = {
|
||||
debug: false,
|
||||
pages: {},
|
||||
theme: {
|
||||
colorScheme: "auto",
|
||||
logo: '',
|
||||
brandColor: ''
|
||||
logo: "",
|
||||
brandColor: "",
|
||||
},
|
||||
// Custom options override defaults
|
||||
...userOptions,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { randomBytes } from "crypto"
|
||||
import { EmailConfig } from "src/providers"
|
||||
import { InternalOptions, InternalProvider } from "src/lib/types"
|
||||
import { InternalOptions } from "src/lib/types"
|
||||
import { hashToken } from "../utils"
|
||||
|
||||
/**
|
||||
@@ -9,7 +8,7 @@ import { hashToken } from "../utils"
|
||||
*/
|
||||
export default async function email(
|
||||
identifier: string,
|
||||
options: InternalOptions<EmailConfig & InternalProvider>
|
||||
options: InternalOptions<"email">
|
||||
) {
|
||||
const { baseUrl, basePath, adapter, provider, logger, callbackUrl } = options
|
||||
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
// We have the intentions to provide only minor fixes for this in the future.
|
||||
|
||||
import { OAuth } from "oauth"
|
||||
import { InternalOptions } from "src/lib/types"
|
||||
|
||||
/**
|
||||
* Client supporting OAuth 1.x
|
||||
* @param {import("src/lib/types").InternalOptions} options
|
||||
*/
|
||||
export function oAuth1Client(options) {
|
||||
/** @type {import("src/providers").OAuthConfig} */
|
||||
export function oAuth1Client(options: InternalOptions<"oauth">) {
|
||||
const provider = options.provider
|
||||
|
||||
const oauth1Client = new OAuth(
|
||||
provider.requestTokenUrl,
|
||||
provider.accessTokenUrl,
|
||||
provider.clientId,
|
||||
provider.clientSecret,
|
||||
provider.version || "1.0",
|
||||
provider.requestTokenUrl as string,
|
||||
provider.accessTokenUrl as string,
|
||||
provider.clientId as string,
|
||||
provider.clientSecret as string,
|
||||
provider.version ?? "1.0",
|
||||
provider.callbackUrl,
|
||||
provider.encoding || "HMAC-SHA1"
|
||||
provider.encoding ?? "HMAC-SHA1"
|
||||
)
|
||||
|
||||
// Promisify get() for OAuth1
|
||||
const originalGet = oauth1Client.get.bind(oauth1Client)
|
||||
oauth1Client.get = (...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// @ts-expect-error
|
||||
oauth1Client.get = async (...args) => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
originalGet(...args, (error, result) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
@@ -36,8 +36,8 @@ export function oAuth1Client(options) {
|
||||
// Promisify getOAuth1AccessToken() for OAuth1
|
||||
const originalGetOAuth1AccessToken =
|
||||
oauth1Client.getOAuthAccessToken.bind(oauth1Client)
|
||||
oauth1Client.getOAuthAccessToken = (...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
oauth1Client.getOAuthAccessToken = async (...args: any[]) => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
originalGetOAuth1AccessToken(
|
||||
...args,
|
||||
(error, oauth_token, oauth_token_secret) => {
|
||||
@@ -52,8 +52,8 @@ export function oAuth1Client(options) {
|
||||
|
||||
const originalGetOAuthRequestToken =
|
||||
oauth1Client.getOAuthRequestToken.bind(oauth1Client)
|
||||
oauth1Client.getOAuthRequestToken = (params = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
oauth1Client.getOAuthRequestToken = async (params = {}) => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
originalGetOAuthRequestToken(
|
||||
params,
|
||||
(error, oauth_token, oauth_token_secret, params) => {
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Issuer } from "openid-client"
|
||||
import { InternalOptions } from "src/lib/types"
|
||||
|
||||
/**
|
||||
* NOTE: We can add auto discovery of the provider's endpoint
|
||||
@@ -6,10 +7,8 @@ import { Issuer } from "openid-client"
|
||||
* Check out `Issuer.discover`
|
||||
*
|
||||
* Client supporting OAuth 2.x and OIDC
|
||||
* @param {import("src/lib/types").InternalOptions} options
|
||||
*/
|
||||
export async function openidClient(options) {
|
||||
/** @type {import("src/providers").OAuthConfig} */
|
||||
export async function openidClient(options: InternalOptions<"oauth">) {
|
||||
const provider = options.provider
|
||||
|
||||
let issuer
|
||||
@@ -17,11 +16,10 @@ export async function openidClient(options) {
|
||||
issuer = await Issuer.discover(provider.wellKnown)
|
||||
} else {
|
||||
issuer = new Issuer({
|
||||
issuer: provider.issuer,
|
||||
authorization_endpoint:
|
||||
provider.authorization.url ?? provider.authorization,
|
||||
token_endpoint: provider.token.url ?? provider.token,
|
||||
userinfo_endpoint: provider.userinfo.url ?? provider.userinfo,
|
||||
issuer: provider.issuer as string,
|
||||
authorization_endpoint: provider.authorization.url,
|
||||
token_endpoint: provider.token.url,
|
||||
userinfo_endpoint: provider.userinfo.url,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,13 +9,59 @@ import { merge } from "../../lib/merge"
|
||||
export default function parseProviders(params: {
|
||||
providers: Provider[]
|
||||
base: string
|
||||
}): InternalProvider[] {
|
||||
const { providers = [], base } = params
|
||||
return providers.map(({ options, ...defaultOptions }) =>
|
||||
merge(defaultOptions, {
|
||||
signinUrl: `${base}/signin/${options?.id ?? defaultOptions.id}`,
|
||||
callbackUrl: `${base}/callback/${options?.id ?? defaultOptions.id}`,
|
||||
...options,
|
||||
providerId?: string
|
||||
}): {
|
||||
providers: InternalProvider[]
|
||||
provider?: InternalProvider
|
||||
} {
|
||||
const { base, providerId } = params
|
||||
|
||||
const providers = params.providers.map(({ options, ...rest }) => {
|
||||
const defaultOptions = normalizeProvider(rest as Provider)
|
||||
const userOptions = normalizeProvider(options as Provider)
|
||||
|
||||
return merge(defaultOptions, {
|
||||
...userOptions,
|
||||
signinUrl: `${base}/signin/${userOptions?.id ?? rest.id}`,
|
||||
callbackUrl: `${base}/callback/${userOptions?.id ?? rest.id}`,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const provider = providers.find(({ id }) => id === providerId)
|
||||
|
||||
return { providers, provider }
|
||||
}
|
||||
|
||||
function normalizeProvider(provider?: Provider) {
|
||||
if (!provider) return
|
||||
|
||||
const normalizedProvider: any = Object.entries(provider).reduce(
|
||||
(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
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
// Checks only work on OAuth 2.x + OIDC providers
|
||||
if (
|
||||
provider.type === "oauth" &&
|
||||
!provider.version?.startsWith("1.") &&
|
||||
!provider.checks
|
||||
) {
|
||||
normalizedProvider.checks = ["state"]
|
||||
}
|
||||
return normalizedProvider as InternalProvider
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createHash } from "crypto"
|
||||
import { NextAuthOptions } from "../.."
|
||||
import { EmailConfig } from "../../providers"
|
||||
import { InternalOptions, InternalProvider } from "../../lib/types"
|
||||
import { InternalOptions } from "../../lib/types"
|
||||
|
||||
/**
|
||||
* Takes a number in seconds and returns the date in the future.
|
||||
@@ -12,10 +11,7 @@ export function fromDate(time, date = Date.now()) {
|
||||
return new Date(date + time * 1000)
|
||||
}
|
||||
|
||||
export function hashToken(
|
||||
token: string,
|
||||
options: InternalOptions<EmailConfig & InternalProvider>
|
||||
) {
|
||||
export function hashToken(token: string, options: InternalOptions<"email">) {
|
||||
const { provider, secret } = options
|
||||
return (
|
||||
createHash("sha256")
|
||||
|
||||
Reference in New Issue
Block a user