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:
Balázs Orbán
2021-09-26 22:02:21 +02:00
committed by GitHub
parent 506672676a
commit b052d4cfc1
10 changed files with 164 additions and 99 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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<

View File

@@ -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",

View File

@@ -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,

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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,
})
}

View File

@@ -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
}

View File

@@ -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")