diff --git a/src/lib/types.ts b/src/lib/types.ts index f2ecd7b2..6c85eef3 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -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 "oauth" + ? Omit, "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 { providers: InternalProvider[] baseUrl: string basePath: string @@ -39,7 +55,9 @@ export interface InternalOptions< | "callback" | "verify-request" | "error" - provider: P + provider: T extends string + ? InternalProvider + : InternalProvider | undefined csrfToken?: string csrfTokenVerified?: boolean secret: string diff --git a/src/providers/email.ts b/src/providers/email.ts index 955e4ee9..d4e4848e 100644 --- a/src/providers/email.ts +++ b/src/providers/email.ts @@ -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, diff --git a/src/providers/oauth.ts b/src/providers/oauth.ts index cc361c29..713b47b6 100644 --- a/src/providers/oauth.ts +++ b/src/providers/oauth.ts @@ -50,9 +50,39 @@ interface AdvancedEndpointHandler

{ } /** Either an URL (containing all the parameters) or an object with more granular control. */ -type EndpointHandler

= - | string - | AdvancedEndpointHandler +export type EndpointHandler< + P extends UrlParams, + C = any, + R = any +> = AdvancedEndpointHandler + +export type AuthorizationEndpointHandler = + EndpointHandler + +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

extends CommonProviderOptions, PartialIssuer { /** @@ -70,40 +100,11 @@ export interface OAuthConfig

extends CommonProviderOptions, PartialIssuer { * * [Authorization endpoint](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1) */ - authorization?: EndpointHandler - /** - * 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 + authorization?: string | AuthorizationEndpointHandler + token?: string | TokenEndpointHandler + userinfo?: string | UserinfoEndpointHandler type: "oauth" version?: string - accessTokenUrl?: string - requestTokenUrl?: string profile?: (profile: P, tokens: TokenSet) => Awaitable checks?: ChecksType | ChecksType[] clientId?: string @@ -133,6 +134,11 @@ export interface OAuthConfig

extends CommonProviderOptions, PartialIssuer { * with the default configuration. */ options?: OAuthUserConfig

+ + // These are kept around for backwards compatibility with OAuth 1.x + accessTokenUrl?: string + requestTokenUrl?: string + encoding?: string } export type OAuthUserConfig

= Omit< diff --git a/src/providers/spotify.js b/src/providers/spotify.ts similarity index 57% rename from src/providers/spotify.js rename to src/providers/spotify.ts index 9bc29823..96312015 100644 --- a/src/providers/spotify.js +++ b/src/providers/spotify.ts @@ -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

= SpotifyProfile>( + options: OAuthUserConfig

+): OAuthConfig

{ return { id: "spotify", name: "Spotify", diff --git a/src/server/index.ts b/src/server/index.ts index 81c0b44d..4170bc8a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -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 = { + const options: InternalOptions = { debug: false, pages: {}, theme: { colorScheme: "auto", - logo: '', - brandColor: '' + logo: "", + brandColor: "", }, // Custom options override defaults ...userOptions, diff --git a/src/server/lib/email/signin.ts b/src/server/lib/email/signin.ts index 470fb415..4ac966b8 100644 --- a/src/server/lib/email/signin.ts +++ b/src/server/lib/email/signin.ts @@ -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 + options: InternalOptions<"email"> ) { const { baseUrl, basePath, adapter, provider, logger, callbackUrl } = options diff --git a/src/server/lib/oauth/client-legacy.js b/src/server/lib/oauth/client-legacy.ts similarity index 66% rename from src/server/lib/oauth/client-legacy.js rename to src/server/lib/oauth/client-legacy.ts index 747f135d..d7776adb 100644 --- a/src/server/lib/oauth/client-legacy.js +++ b/src/server/lib/oauth/client-legacy.ts @@ -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) => { diff --git a/src/server/lib/oauth/client.js b/src/server/lib/oauth/client.ts similarity index 59% rename from src/server/lib/oauth/client.js rename to src/server/lib/oauth/client.ts index 64a8c8b2..cd501eef 100644 --- a/src/server/lib/oauth/client.js +++ b/src/server/lib/oauth/client.ts @@ -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, }) } diff --git a/src/server/lib/providers.ts b/src/server/lib/providers.ts index f59773b6..e6ae497d 100644 --- a/src/server/lib/providers.ts +++ b/src/server/lib/providers.ts @@ -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 } diff --git a/src/server/lib/utils.ts b/src/server/lib/utils.ts index 7cd22b78..dc132f19 100644 --- a/src/server/lib/utils.ts +++ b/src/server/lib/utils.ts @@ -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 -) { +export function hashToken(token: string, options: InternalOptions<"email">) { const { provider, secret } = options return ( createHash("sha256")