mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
13 Commits
@auth/soli
...
feat/oauth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f48e0f1e2d | ||
|
|
2eef2f85d5 | ||
|
|
bf31512071 | ||
|
|
acc9966285 | ||
|
|
6dd44000d7 | ||
|
|
2267292f5f | ||
|
|
883b36e2b2 | ||
|
|
029edb93ee | ||
|
|
ebcc3280df | ||
|
|
70a16e82c3 | ||
|
|
1365211a4e | ||
|
|
6df5773220 | ||
|
|
e3e3cf12d1 |
@@ -68,10 +68,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.16.3",
|
"@babel/runtime": "^7.16.3",
|
||||||
"@panva/hkdf": "^1.0.1",
|
"@panva/hkdf": "^1.0.1",
|
||||||
|
"@panva/oauth4webapi": "^0.0.10",
|
||||||
"cookie": "^0.4.1",
|
"cookie": "^0.4.1",
|
||||||
"jose": "^4.3.7",
|
"jose": "^4.3.7",
|
||||||
"oauth": "^0.9.15",
|
"oauth": "^0.9.15",
|
||||||
"openid-client": "^5.1.0",
|
|
||||||
"preact": "^10.6.3",
|
"preact": "^10.6.3",
|
||||||
"preact-render-to-string": "^5.1.19",
|
"preact-render-to-string": "^5.1.19",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { discoveryRequest, processDiscoveryResponse } from "@panva/oauth4webapi"
|
||||||
|
|
||||||
|
import type { AuthorizationServer } from "@panva/oauth4webapi"
|
||||||
|
import type { InternalProvider } from "src/lib/types"
|
||||||
|
|
||||||
|
export default async function getAuthorizationServer(
|
||||||
|
provider: InternalProvider<"oauth">
|
||||||
|
): Promise<AuthorizationServer> {
|
||||||
|
if (provider.idToken) {
|
||||||
|
const issuer = new URL(provider.issuer as string)
|
||||||
|
return await discoveryRequest(issuer).then(
|
||||||
|
async (response) => await processDiscoveryResponse(issuer, response)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
issuer: provider.issuer as string,
|
||||||
|
authorization_endpoint:
|
||||||
|
typeof provider.authorization === "string"
|
||||||
|
? provider.authorization
|
||||||
|
: provider.authorization?.url,
|
||||||
|
token_endpoint:
|
||||||
|
typeof provider.token === "string"
|
||||||
|
? provider.token
|
||||||
|
: provider.token?.url,
|
||||||
|
userinfo_endpoint:
|
||||||
|
typeof provider.userinfo === "string"
|
||||||
|
? provider.userinfo
|
||||||
|
: provider.userinfo?.url,
|
||||||
|
jwks_uri: provider.jwks_uri,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { openidClient } from "./client"
|
|
||||||
import { oAuth1Client } from "./client-legacy"
|
import { oAuth1Client } from "./client-legacy"
|
||||||
import { createState } from "./state-handler"
|
import { createState } from "./state-handler"
|
||||||
import { createPKCE } from "./pkce-handler"
|
import { createPKCE } from "./pkce-handler"
|
||||||
|
import getAuthorizationServer from "./authorization-server"
|
||||||
|
|
||||||
import type { AuthorizationParameters } from "openid-client"
|
import type { AuthorizationParameters } from "openid-client"
|
||||||
import type { InternalOptions } from "../../types"
|
import type { InternalOptions } from "../../types"
|
||||||
@@ -50,28 +50,53 @@ export default async function getAuthorizationUrl(params: {
|
|||||||
return { redirect: url }
|
return { redirect: url }
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = await openidClient(options)
|
const authorizationServer = await getAuthorizationServer(provider)
|
||||||
|
|
||||||
|
if (!authorizationServer.authorization_endpoint) throw new Error()
|
||||||
|
const authorizationUrl = new URL(authorizationServer.authorization_endpoint)
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
authorizationUrl.searchParams.set(key, value as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizationUrl.searchParams.set("client_id", provider.clientId as string)
|
||||||
|
authorizationUrl.searchParams.set("redirect_uri", provider.callbackUrl)
|
||||||
|
|
||||||
|
if (typeof provider.authorization !== "string" && provider.authorization) {
|
||||||
|
const { params: authorizationEndpointParams } = provider.authorization
|
||||||
|
|
||||||
|
if (typeof authorizationEndpointParams?.response_type === "string") {
|
||||||
|
authorizationUrl.searchParams.set(
|
||||||
|
"response_type",
|
||||||
|
authorizationEndpointParams.response_type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (typeof authorizationEndpointParams?.scope === "string") {
|
||||||
|
authorizationUrl.searchParams.set(
|
||||||
|
"scope",
|
||||||
|
authorizationEndpointParams.scope
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const authorizationParams: AuthorizationParameters = params
|
|
||||||
const cookies: Cookie[] = []
|
const cookies: Cookie[] = []
|
||||||
|
|
||||||
|
const pkce = await createPKCE(options, authorizationServer)
|
||||||
|
authorizationUrl.searchParams.set("code_challenge", pkce.code_challenge)
|
||||||
|
authorizationUrl.searchParams.set(
|
||||||
|
"code_challenge_method",
|
||||||
|
pkce.code_challenge_method
|
||||||
|
)
|
||||||
|
cookies.push(pkce.cookie)
|
||||||
|
|
||||||
const state = await createState(options)
|
const state = await createState(options)
|
||||||
if (state) {
|
if (!pkce.isSupported && state) {
|
||||||
authorizationParams.state = state.value
|
authorizationUrl.searchParams.set("state", state.value)
|
||||||
cookies.push(state.cookie)
|
cookies.push(state.cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pkce = await createPKCE(options)
|
logger.debug("GET_AUTHORIZATION_URL", { authorizationUrl, cookies })
|
||||||
if (pkce) {
|
return { redirect: authorizationUrl.href, cookies }
|
||||||
authorizationParams.code_challenge = pkce.code_challenge
|
|
||||||
authorizationParams.code_challenge_method = pkce.code_challenge_method
|
|
||||||
cookies.push(pkce.cookie)
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = client.authorizationUrl(authorizationParams)
|
|
||||||
|
|
||||||
logger.debug("GET_AUTHORIZATION_URL", { url, cookies })
|
|
||||||
return { redirect: url, cookies }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("GET_AUTHORIZATION_URL_ERROR", error as Error)
|
logger.error("GET_AUTHORIZATION_URL_ERROR", error as Error)
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
import { TokenSet } from "openid-client"
|
|
||||||
import { openidClient } from "./client"
|
import { openidClient } from "./client"
|
||||||
import { oAuth1Client } from "./client-legacy"
|
import { oAuth1Client } from "./client-legacy"
|
||||||
import { useState } from "./state-handler"
|
import { useState } from "./state-handler"
|
||||||
import { usePKCECodeVerifier } from "./pkce-handler"
|
import { usePKCECodeVerifier } from "./pkce-handler"
|
||||||
import { OAuthCallbackError } from "../../errors"
|
import { OAuthCallbackError } from "../../errors"
|
||||||
|
import {
|
||||||
|
authorizationCodeGrantRequest,
|
||||||
|
expectNoState,
|
||||||
|
getValidatedIdTokenClaims,
|
||||||
|
isOAuth2Error,
|
||||||
|
processAuthorizationCodeOAuth2Response,
|
||||||
|
processAuthorizationCodeOpenIDResponse,
|
||||||
|
processUserInfoResponse,
|
||||||
|
userInfoRequest,
|
||||||
|
validateAuthResponse,
|
||||||
|
} from "@panva/oauth4webapi"
|
||||||
|
import getAuthorizationServer from "./authorization-server"
|
||||||
|
|
||||||
import type { CallbackParamsType } from "openid-client"
|
|
||||||
import type { Account, LoggerInstance, Profile } from "../../.."
|
import type { Account, LoggerInstance, Profile } from "../../.."
|
||||||
import type { OAuthChecks, OAuthConfig } from "../../../providers"
|
import type { OAuthChecks, OAuthConfig } from "../../../providers"
|
||||||
import type { InternalOptions } from "../../types"
|
import type { InternalOptions } from "../../types"
|
||||||
import type { RequestInternal, OutgoingResponse } from "../.."
|
import type { RequestInternal, OutgoingResponse } from "../.."
|
||||||
import type { Cookie } from "../cookie"
|
import type { Cookie } from "../cookie"
|
||||||
|
import type {
|
||||||
|
OAuth2Error,
|
||||||
|
OAuth2TokenEndpointResponse,
|
||||||
|
OpenIDTokenEndpointResponse,
|
||||||
|
} from "@panva/oauth4webapi"
|
||||||
|
|
||||||
export default async function oAuthCallback(params: {
|
export default async function oAuthCallback(params: {
|
||||||
options: InternalOptions<"oauth">
|
options: InternalOptions<"oauth">
|
||||||
@@ -19,7 +34,7 @@ export default async function oAuthCallback(params: {
|
|||||||
method: Required<RequestInternal>["method"]
|
method: Required<RequestInternal>["method"]
|
||||||
cookies: RequestInternal["cookies"]
|
cookies: RequestInternal["cookies"]
|
||||||
}): Promise<GetProfileResult & { cookies?: OutgoingResponse["cookies"] }> {
|
}): Promise<GetProfileResult & { cookies?: OutgoingResponse["cookies"] }> {
|
||||||
const { options, query, body, method, cookies } = params
|
const { options, query, body, cookies } = params
|
||||||
const { logger, provider } = options
|
const { logger, provider } = options
|
||||||
|
|
||||||
const errorMessage = body?.error ?? query?.error
|
const errorMessage = body?.error ?? query?.error
|
||||||
@@ -65,81 +80,111 @@ export default async function oAuthCallback(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = await openidClient(options)
|
const client = openidClient(provider)
|
||||||
|
const authorizationServer = await getAuthorizationServer(provider)
|
||||||
|
|
||||||
let tokens: TokenSet
|
let tokens:
|
||||||
|
| OpenIDTokenEndpointResponse
|
||||||
|
| OAuth2TokenEndpointResponse
|
||||||
|
| OAuth2Error
|
||||||
|
|
||||||
const checks: OAuthChecks = {}
|
let expectedState: string | typeof expectNoState = expectNoState
|
||||||
const resCookies: Cookie[] = []
|
const resCookies: Cookie[] = []
|
||||||
|
const authParams = new URLSearchParams(query)
|
||||||
|
|
||||||
|
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
|
||||||
|
const pkce = await usePKCECodeVerifier(
|
||||||
|
codeVerifier,
|
||||||
|
options,
|
||||||
|
authorizationServer
|
||||||
|
)
|
||||||
|
resCookies.push(pkce.cookie)
|
||||||
|
|
||||||
const state = await useState(cookies?.[options.cookies.state.name], options)
|
const state = await useState(cookies?.[options.cookies.state.name], options)
|
||||||
|
|
||||||
if (state) {
|
if (!pkce.isSupported && state) {
|
||||||
checks.state = state.value
|
|
||||||
resCookies.push(state.cookie)
|
resCookies.push(state.cookie)
|
||||||
|
expectedState = state.value
|
||||||
|
authParams.append("state", state.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
|
const callbackParameters = validateAuthResponse(
|
||||||
const pkce = await usePKCECodeVerifier(codeVerifier, options)
|
authorizationServer,
|
||||||
if (pkce) {
|
client,
|
||||||
checks.code_verifier = pkce.codeVerifier
|
authParams,
|
||||||
resCookies.push(pkce.cookie)
|
expectedState
|
||||||
|
)
|
||||||
|
if (isOAuth2Error(callbackParameters)) {
|
||||||
|
throw new OAuthCallbackError(callbackParameters.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const params: CallbackParamsType = {
|
const response = await authorizationCodeGrantRequest(
|
||||||
...client.callbackParams({
|
authorizationServer,
|
||||||
url: `http://n?${new URLSearchParams(query)}`,
|
client,
|
||||||
// TODO: Ask to allow object to be passed upstream:
|
callbackParameters,
|
||||||
// https://github.com/panva/node-openid-client/blob/3ae206dfc78c02134aa87a07f693052c637cab84/types/index.d.ts#L439
|
provider.callbackUrl,
|
||||||
// @ts-expect-error
|
pkce.codeVerifier
|
||||||
body,
|
)
|
||||||
method,
|
|
||||||
}),
|
|
||||||
// @ts-expect-error
|
|
||||||
...provider.token?.params,
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error
|
if (typeof provider.token !== "string" && provider.token?.request) {
|
||||||
if (provider.token?.request) {
|
const params = {
|
||||||
// @ts-expect-error
|
...callbackParameters,
|
||||||
|
...provider.token?.params,
|
||||||
|
}
|
||||||
|
const checks = new URLSearchParams()
|
||||||
|
if (state) checks.append("state", state.value)
|
||||||
|
checks.append("code_verifier", pkce.codeVerifier)
|
||||||
const response = await provider.token.request({
|
const response = await provider.token.request({
|
||||||
provider,
|
provider,
|
||||||
params,
|
params,
|
||||||
checks,
|
checks,
|
||||||
client,
|
client,
|
||||||
})
|
})
|
||||||
tokens = new TokenSet(response.tokens)
|
tokens = response.tokens
|
||||||
} else if (provider.idToken) {
|
} else if (provider.idToken) {
|
||||||
tokens = await client.callback(provider.callbackUrl, params, checks)
|
tokens = await processAuthorizationCodeOpenIDResponse(
|
||||||
|
authorizationServer,
|
||||||
|
client,
|
||||||
|
response
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
tokens = await client.oauthCallback(provider.callbackUrl, params, checks)
|
tokens = await processAuthorizationCodeOAuth2Response(
|
||||||
|
authorizationServer,
|
||||||
|
client,
|
||||||
|
response
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// REVIEW: How can scope be returned as an array?
|
if (isOAuth2Error(tokens)) {
|
||||||
if (Array.isArray(tokens.scope)) {
|
throw new OAuthCallbackError(tokens.error)
|
||||||
tokens.scope = tokens.scope.join(" ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let profile: Profile
|
let profile: Profile | Response
|
||||||
// @ts-expect-error
|
if (typeof provider.userinfo !== "string" && provider.userinfo?.request) {
|
||||||
if (provider.userinfo?.request) {
|
|
||||||
// @ts-expect-error
|
|
||||||
profile = await provider.userinfo.request({
|
profile = await provider.userinfo.request({
|
||||||
provider,
|
provider,
|
||||||
tokens,
|
tokens,
|
||||||
client,
|
client,
|
||||||
})
|
})
|
||||||
} else if (provider.idToken) {
|
} else if (provider.idToken) {
|
||||||
profile = tokens.claims()
|
const idToken = getValidatedIdTokenClaims(tokens)
|
||||||
|
|
||||||
|
profile = await processUserInfoResponse(
|
||||||
|
authorizationServer,
|
||||||
|
client,
|
||||||
|
idToken?.sub as string,
|
||||||
|
response
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
profile = await client.userinfo(tokens, {
|
profile = await userInfoRequest(
|
||||||
// @ts-expect-error
|
authorizationServer,
|
||||||
params: provider.userinfo?.params,
|
client,
|
||||||
})
|
tokens.access_token
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const profileResult = await getProfile({
|
const profileResult = await getProfile({
|
||||||
profile,
|
profile: profile as Profile,
|
||||||
provider,
|
provider,
|
||||||
tokens,
|
tokens,
|
||||||
logger,
|
logger,
|
||||||
@@ -156,7 +201,7 @@ export default async function oAuthCallback(params: {
|
|||||||
|
|
||||||
export interface GetProfileParams {
|
export interface GetProfileParams {
|
||||||
profile: Profile
|
profile: Profile
|
||||||
tokens: TokenSet
|
tokens: OpenIDTokenEndpointResponse | OAuth2TokenEndpointResponse
|
||||||
provider: OAuthConfig<any>
|
provider: OAuthConfig<any>
|
||||||
logger: LoggerInstance
|
logger: LoggerInstance
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
|
import { InternalProvider } from "src/lib/types"
|
||||||
|
import type { Client as WebApiClient } from "@panva/oauth4webapi"
|
||||||
import { Issuer, custom } from "openid-client"
|
import { Issuer, custom } from "openid-client"
|
||||||
import type { Client } from "openid-client"
|
import type { Client } from "openid-client"
|
||||||
import type { InternalOptions } from "../../types"
|
import type { InternalOptions } from "../../types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NOTE: We can add auto discovery of the provider's endpoint
|
|
||||||
* that requires only one endpoint to be specified by the user.
|
|
||||||
* Check out `Issuer.discover`
|
|
||||||
*
|
|
||||||
* Client supporting OAuth 2.x and OIDC
|
* Client supporting OAuth 2.x and OIDC
|
||||||
*/
|
*/
|
||||||
|
export function webApiClient(provider: InternalProvider<"oauth">): WebApiClient {
|
||||||
|
return {
|
||||||
|
client_id: provider.clientId as string,
|
||||||
|
client_secret: provider.clientSecret as string,
|
||||||
|
token_endpoint_auth_method: "client_secret_basic",
|
||||||
|
...provider.client,
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function openidClient(
|
export async function openidClient(
|
||||||
options: InternalOptions<"oauth">
|
options: InternalOptions<"oauth">
|
||||||
): Promise<Client> {
|
): Promise<Client> {
|
||||||
@@ -31,21 +37,4 @@ export async function openidClient(
|
|||||||
userinfo_endpoint: provider.userinfo?.url ?? provider.userinfo,
|
userinfo_endpoint: provider.userinfo?.url ?? provider.userinfo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new issuer.Client(
|
|
||||||
{
|
|
||||||
client_id: provider.clientId as string,
|
|
||||||
client_secret: provider.clientSecret as string,
|
|
||||||
redirect_uris: [provider.callbackUrl],
|
|
||||||
...provider.client,
|
|
||||||
},
|
|
||||||
provider.jwks
|
|
||||||
)
|
|
||||||
|
|
||||||
// allow a 10 second skew
|
|
||||||
// See https://github.com/nextauthjs/next-auth/issues/3032
|
|
||||||
// and https://github.com/nextauthjs/next-auth/issues/3067
|
|
||||||
client[custom.clock_tolerance] = 10
|
|
||||||
|
|
||||||
return client
|
|
||||||
}
|
}
|
||||||
|
|||||||
9
packages/next-auth/src/core/lib/oauth/helper.ts
Normal file
9
packages/next-auth/src/core/lib/oauth/helper.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { generateRandomCodeVerifier } from "@panva/oauth4webapi"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random `state` value encoded as base64url. This method returns oauth4webapi's `generateRandomCodeVerifier` for convenience.
|
||||||
|
* @see {@link https://github.com/panva/oauth4webapi/blob/main/docs/functions/generateRandomCodeVerifier.md generateRandomCodeVerifier.}
|
||||||
|
*/
|
||||||
|
export function generateRandomState() {
|
||||||
|
return generateRandomCodeVerifier()
|
||||||
|
}
|
||||||
@@ -1,30 +1,43 @@
|
|||||||
import * as jwt from "../../../jwt"
|
import * as jwt from "../../../jwt"
|
||||||
|
import {
|
||||||
|
generateRandomCodeVerifier,
|
||||||
|
calculatePKCECodeChallenge,
|
||||||
|
} from "@panva/oauth4webapi"
|
||||||
import { generators } from "openid-client"
|
import { generators } from "openid-client"
|
||||||
import type { InternalOptions } from "../../types"
|
import type { InternalOptions } from "../../types"
|
||||||
import type { Cookie } from "../cookie"
|
import type { Cookie } from "../cookie"
|
||||||
|
import type { AuthorizationServer } from "@panva/oauth4webapi"
|
||||||
|
|
||||||
const PKCE_CODE_CHALLENGE_METHOD = "S256"
|
const PKCE_CODE_CHALLENGE_METHOD = "S256"
|
||||||
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if PKCE is supported by the authorization server.
|
||||||
|
* @see {@link https://datatracker.ietf.org/doc/html/rfc8414#section-2 code_challenge_methods_supported}
|
||||||
|
* @param as The authorization server. @see {@link https://github.com/panva/oauth4webapi/blob/main/docs/interfaces/AuthorizationServer.md AuthorizationServer}
|
||||||
|
*/
|
||||||
|
function isPKCESupported(as: AuthorizationServer) {
|
||||||
|
return !!as.code_challenge_methods_supported?.includes(
|
||||||
|
PKCE_CODE_CHALLENGE_METHOD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns `code_challenge` and `code_challenge_method`
|
* Returns `code_challenge` and `code_challenge_method`
|
||||||
* and saves them in a cookie.
|
* and saves them in a cookie.
|
||||||
*/
|
*/
|
||||||
export async function createPKCE(options: InternalOptions<"oauth">): Promise<
|
export async function createPKCE(
|
||||||
| undefined
|
options: InternalOptions<"oauth">,
|
||||||
| {
|
as: AuthorizationServer
|
||||||
code_challenge: string
|
): Promise<{
|
||||||
code_challenge_method: "S256"
|
code_challenge: string
|
||||||
cookie: Cookie
|
code_challenge_method: "S256"
|
||||||
}
|
cookie: Cookie
|
||||||
> {
|
isSupported: boolean
|
||||||
const { cookies, logger, provider } = options
|
}> {
|
||||||
if (!provider.checks?.includes("pkce")) {
|
const { cookies, logger } = options
|
||||||
// Provider does not support PKCE, return nothing.
|
const code_verifier = generateRandomCodeVerifier()
|
||||||
return
|
const code_challenge = await calculatePKCECodeChallenge(code_verifier)
|
||||||
}
|
|
||||||
const code_verifier = generators.codeVerifier()
|
|
||||||
const code_challenge = generators.codeChallenge(code_verifier)
|
|
||||||
|
|
||||||
const expires = new Date()
|
const expires = new Date()
|
||||||
expires.setTime(expires.getTime() + PKCE_MAX_AGE * 1000)
|
expires.setTime(expires.getTime() + PKCE_MAX_AGE * 1000)
|
||||||
@@ -51,34 +64,37 @@ export async function createPKCE(options: InternalOptions<"oauth">): Promise<
|
|||||||
value: encryptedCodeVerifier,
|
value: encryptedCodeVerifier,
|
||||||
options: { ...cookies.pkceCodeVerifier.options, expires },
|
options: { ...cookies.pkceCodeVerifier.options, expires },
|
||||||
},
|
},
|
||||||
|
isSupported: isPKCESupported(as),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns code_verifier if provider uses PKCE,
|
* Returns `code_verifier`,
|
||||||
* and clears the container cookie afterwards.
|
* and clears the container cookie afterwards.
|
||||||
*/
|
*/
|
||||||
export async function usePKCECodeVerifier(
|
export async function usePKCECodeVerifier(
|
||||||
codeVerifier: string | undefined,
|
codeVerifier: string | undefined,
|
||||||
options: InternalOptions<"oauth">
|
options: InternalOptions<"oauth">,
|
||||||
): Promise<{ codeVerifier: string; cookie: Cookie } | undefined> {
|
as: AuthorizationServer
|
||||||
const { cookies, provider } = options
|
): Promise<{ codeVerifier: string; cookie: Cookie; isSupported: boolean }> {
|
||||||
|
if (codeVerifier === undefined) throw new Error("Invalid code verifier")
|
||||||
|
|
||||||
if (!provider?.checks?.includes("pkce") || !codeVerifier) {
|
const { cookies } = options
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const pkce = (await jwt.decode({
|
const pkce = await jwt.decode({
|
||||||
...options.jwt,
|
...options.jwt,
|
||||||
token: codeVerifier,
|
token: codeVerifier,
|
||||||
})) as any
|
})
|
||||||
|
|
||||||
|
if (pkce === null) throw new Error("Invalid code verifier")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
codeVerifier: pkce?.code_verifier ?? undefined,
|
codeVerifier: pkce.code_verifier as string,
|
||||||
cookie: {
|
cookie: {
|
||||||
name: cookies.pkceCodeVerifier.name,
|
name: cookies.pkceCodeVerifier.name,
|
||||||
value: "",
|
value: "",
|
||||||
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
|
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
|
||||||
},
|
},
|
||||||
|
isSupported: isPKCESupported(as),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { generators } from "openid-client"
|
|||||||
|
|
||||||
import type { InternalOptions } from "../../types"
|
import type { InternalOptions } from "../../types"
|
||||||
import type { Cookie } from "../cookie"
|
import type { Cookie } from "../cookie"
|
||||||
|
import { generateRandomState } from "./helper"
|
||||||
|
|
||||||
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ export async function createState(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = generators.state()
|
const state = generateRandomState()
|
||||||
|
|
||||||
const encodedState = await jwt.encode({
|
const encodedState = await jwt.encode({
|
||||||
...jwt,
|
...jwt,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { TokenSetParameters } from "openid-client"
|
|||||||
import type { JWT, JWTOptions } from "../jwt"
|
import type { JWT, JWTOptions } from "../jwt"
|
||||||
import type { LoggerInstance } from "../utils/logger"
|
import type { LoggerInstance } from "../utils/logger"
|
||||||
import type { CookieSerializeOptions } from "cookie"
|
import type { CookieSerializeOptions } from "cookie"
|
||||||
|
import type { TokenEndpointResponse } from "@panva/oauth4webapi"
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next"
|
import type { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
@@ -224,7 +225,7 @@ export interface Theme {
|
|||||||
* Some of them are available with different casing,
|
* Some of them are available with different casing,
|
||||||
* but they refer to the same value.
|
* but they refer to the same value.
|
||||||
*/
|
*/
|
||||||
export type TokenSet = TokenSetParameters
|
export type TokenSet = TokenEndpointResponse
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Usually contains information about the provider being used
|
* Usually contains information about the provider being used
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AuthorizationServer, userInfoRequest } from "@panva/oauth4webapi"
|
||||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||||
|
|
||||||
interface FacebookPictureData {
|
interface FacebookPictureData {
|
||||||
@@ -26,11 +27,17 @@ export default function Facebook<P extends FacebookProfile>(
|
|||||||
// https://developers.facebook.com/docs/graph-api/reference/user/#fields
|
// https://developers.facebook.com/docs/graph-api/reference/user/#fields
|
||||||
params: { fields: "id,name,email,picture" },
|
params: { fields: "id,name,email,picture" },
|
||||||
async request({ tokens, client, provider }) {
|
async request({ tokens, client, provider }) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// @ts-expect-error
|
||||||
return await client.userinfo(tokens.access_token!, {
|
const userinfo_endpoint = new URL(provider.userinfo?.url)
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
params: provider.userinfo?.params,
|
Object.entries(provider.userinfo?.params).forEach(([key, value]) => {
|
||||||
|
userinfo_endpoint.searchParams.append(key, value as string)
|
||||||
})
|
})
|
||||||
|
const as: AuthorizationServer = {
|
||||||
|
issuer: options.issuer as string,
|
||||||
|
userinfo_endpoint: userinfo_endpoint.href,
|
||||||
|
}
|
||||||
|
return await userInfoRequest(as, client, tokens.access_token)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
profile(profile: P) {
|
profile(profile: P) {
|
||||||
|
|||||||
@@ -1,33 +1,20 @@
|
|||||||
import type { CommonProviderOptions } from "../providers"
|
import type { CommonProviderOptions } from "../providers"
|
||||||
import type { Profile, TokenSet, User, Awaitable } from ".."
|
import type { Profile, TokenSet, User, Awaitable } from ".."
|
||||||
|
|
||||||
import type {
|
|
||||||
AuthorizationParameters,
|
|
||||||
CallbackParamsType,
|
|
||||||
Issuer,
|
|
||||||
ClientMetadata,
|
|
||||||
IssuerMetadata,
|
|
||||||
OAuthCallbackChecks,
|
|
||||||
OpenIDCallbackChecks,
|
|
||||||
HttpOptions,
|
|
||||||
} from "openid-client"
|
|
||||||
import type { JWK } from "jose"
|
import type { JWK } from "jose"
|
||||||
|
import type { AuthorizationServer, Client } from "@panva/oauth4webapi"
|
||||||
type Client = InstanceType<Issuer["Client"]>
|
|
||||||
|
|
||||||
export type { OAuthProviderType } from "./oauth-types"
|
export type { OAuthProviderType } from "./oauth-types"
|
||||||
|
|
||||||
type ChecksType = "pkce" | "state" | "none"
|
type ChecksType = "pkce" | "state" | "none"
|
||||||
|
|
||||||
export type OAuthChecks = OpenIDCallbackChecks | OAuthCallbackChecks
|
type PartialIssuer = Partial<Pick<AuthorizationServer, "jwks_uri" | "issuer">>
|
||||||
|
|
||||||
type PartialIssuer = Partial<Pick<IssuerMetadata, "jwks_endpoint" | "issuer">>
|
|
||||||
|
|
||||||
type UrlParams = Record<string, unknown>
|
type UrlParams = Record<string, unknown>
|
||||||
|
|
||||||
type EndpointRequest<C, R, P> = (
|
type EndpointRequest<C, R, P> = (
|
||||||
context: C & {
|
context: C & {
|
||||||
/** `openid-client` Client */
|
/** `oauth4webapi` Client */
|
||||||
client: Client
|
client: Client
|
||||||
/** Provider is passed for convenience, ans also contains the `callbackUrl`. */
|
/** Provider is passed for convenience, ans also contains the `callbackUrl`. */
|
||||||
provider: OAuthConfig<P> & {
|
provider: OAuthConfig<P> & {
|
||||||
@@ -61,8 +48,7 @@ export type EndpointHandler<
|
|||||||
R = any
|
R = any
|
||||||
> = AdvancedEndpointHandler<P, C, R>
|
> = AdvancedEndpointHandler<P, C, R>
|
||||||
|
|
||||||
export type AuthorizationEndpointHandler =
|
export type AuthorizationEndpointHandler = EndpointHandler<UrlParams>
|
||||||
EndpointHandler<AuthorizationParameters>
|
|
||||||
|
|
||||||
export type TokenEndpointHandler = EndpointHandler<
|
export type TokenEndpointHandler = EndpointHandler<
|
||||||
UrlParams,
|
UrlParams,
|
||||||
@@ -71,12 +57,12 @@ export type TokenEndpointHandler = EndpointHandler<
|
|||||||
* Parameters extracted from the request to the `/api/auth/callback/:providerId` endpoint.
|
* Parameters extracted from the request to the `/api/auth/callback/:providerId` endpoint.
|
||||||
* Contains params like `state`.
|
* Contains params like `state`.
|
||||||
*/
|
*/
|
||||||
params: CallbackParamsType
|
params: URLSearchParams
|
||||||
/**
|
/**
|
||||||
* When using this custom flow, make sure to do all the necessary security checks.
|
* 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.
|
* This object contains parameters you have to match against the request to make sure it is valid.
|
||||||
*/
|
*/
|
||||||
checks: OAuthChecks
|
checks: URLSearchParams
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tokens: TokenSet
|
tokens: TokenSet
|
||||||
@@ -86,7 +72,7 @@ export type TokenEndpointHandler = EndpointHandler<
|
|||||||
export type UserinfoEndpointHandler = EndpointHandler<
|
export type UserinfoEndpointHandler = EndpointHandler<
|
||||||
UrlParams,
|
UrlParams,
|
||||||
{ tokens: TokenSet },
|
{ tokens: TokenSet },
|
||||||
Profile
|
Profile | Response
|
||||||
>
|
>
|
||||||
|
|
||||||
export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
||||||
@@ -112,7 +98,7 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
|||||||
version?: string
|
version?: string
|
||||||
profile?: (profile: P, tokens: TokenSet) => Awaitable<User & { id: string }>
|
profile?: (profile: P, tokens: TokenSet) => Awaitable<User & { id: string }>
|
||||||
checks?: ChecksType | ChecksType[]
|
checks?: ChecksType | ChecksType[]
|
||||||
client?: Partial<ClientMetadata>
|
client?: Partial<Client>
|
||||||
jwks?: { keys: JWK[] }
|
jwks?: { keys: JWK[] }
|
||||||
clientId?: string
|
clientId?: string
|
||||||
clientSecret?: string
|
clientSecret?: string
|
||||||
@@ -128,11 +114,6 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
|||||||
idToken?: boolean
|
idToken?: boolean
|
||||||
// TODO: only allow for BattleNet
|
// TODO: only allow for BattleNet
|
||||||
region?: string
|
region?: string
|
||||||
// TODO: only allow for some
|
|
||||||
issuer?: string
|
|
||||||
/** Read more at: https://github.com/panva/node-openid-client/tree/main/docs#customizing-http-requests */
|
|
||||||
httpOptions?: HttpOptions
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The options provided by the user.
|
* The options provided by the user.
|
||||||
* We will perform a deep-merge of these values
|
* We will perform a deep-merge of these values
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||||
|
import {
|
||||||
|
authorizationCodeGrantRequest,
|
||||||
|
AuthorizationServer,
|
||||||
|
isOAuth2Error,
|
||||||
|
processAuthorizationCodeOAuth2Response,
|
||||||
|
} from "@panva/oauth4webapi"
|
||||||
|
|
||||||
export interface TwitterLegacyProfile {
|
export interface TwitterLegacyProfile {
|
||||||
id: number
|
id: number
|
||||||
@@ -183,13 +189,33 @@ export default function Twitter<
|
|||||||
url: "https://api.twitter.com/2/oauth2/token",
|
url: "https://api.twitter.com/2/oauth2/token",
|
||||||
// TODO: Remove this
|
// TODO: Remove this
|
||||||
async request({ client, params, checks, provider }) {
|
async request({ client, params, checks, provider }) {
|
||||||
const response = await client.oauthCallback(
|
const as: AuthorizationServer = {
|
||||||
provider.callbackUrl,
|
issuer: options.issuer as string,
|
||||||
|
// @ts-expect-error
|
||||||
|
token_endpoint: provider.token?.url,
|
||||||
|
}
|
||||||
|
const additionalParameters = new URLSearchParams()
|
||||||
|
additionalParameters.append("client_id", options.clientId)
|
||||||
|
const response = await authorizationCodeGrantRequest(
|
||||||
|
as,
|
||||||
|
client,
|
||||||
params,
|
params,
|
||||||
checks,
|
provider.callbackUrl,
|
||||||
{ exchangeBody: { client_id: options.clientId } }
|
checks.get("code_verifier") as string,
|
||||||
|
{
|
||||||
|
additionalParameters,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return { tokens: response }
|
const tokens = await processAuthorizationCodeOAuth2Response(
|
||||||
|
as,
|
||||||
|
client,
|
||||||
|
response
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isOAuth2Error(tokens)) {
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
return { tokens }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
userinfo: {
|
userinfo: {
|
||||||
|
|||||||
12276
pnpm-lock.yaml
generated
12276
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user