diff --git a/package.json b/package.json
index bca7d90..b3503e5 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
"@tsconfig/strictest": "^2.0.5",
"@types/bun": "latest",
"@types/jsonwebtoken": "^9.0.7",
+ "@types/js-cookie": "^3.0.6",
"cross-env": "^7.0.3",
"git-cliff": "2.7.0",
"globals": "^15.14.0",
@@ -88,6 +89,7 @@
"@simplewebauthn/browser": "^13.1.0",
"@simplewebauthn/server": "^13.1.0",
"jose": "6.0.8",
+ "js-cookie": "3.0.5",
"oauth4webapi": "^3.1.4",
"qs-esm": "7.0.2",
"uuid": "11.1.0"
diff --git a/src/client/index.ts b/src/client/index.ts
index 95107a9..896ad2d 100644
--- a/src/client/index.ts
+++ b/src/client/index.ts
@@ -1,17 +1,17 @@
+import { MissingPayloadAuthBaseURL } from "../core/errors/consoleErrors.js"
import {
- resetPassword,
forgotPassword,
recoverPassword,
- type PasswordResetPayload,
+ resetPassword,
type ForgotPasswordPayload,
type PasswordRecoverPayload,
+ type PasswordResetPayload,
} from "./password.js"
import { refresh } from "./refresh.js"
-import { signin } from "./signin.js"
import { register } from "./register.js"
-import { getSession, getClientSession } from "./session.js"
+import { getClientSession, getSession } from "./session.js"
+import { signin } from "./signin.js"
import { signout } from "./signout.js"
-import { MissingPayloadAuthBaseURL } from "../core/errors/consoleErrors.js"
class AuthClient {
private baseURL: string
@@ -32,10 +32,16 @@ class AuthClient {
(process.env.NEXT_PUBLIC_PAYLOAD_AUTH_URL as string)
}
- signin() {
+ /**
+ * Sign in a user
+ * @param additionalScope - Additional scope to request
+ * @returns The sign in response
+ */
+ signin(additionalScope?: string) {
return signin({
name: this.name,
baseURL: this.baseURL,
+ additionalScope,
})
}
register() {
diff --git a/src/client/oauth.ts b/src/client/oauth.ts
index 0c6c5f0..f5582a8 100644
--- a/src/client/oauth.ts
+++ b/src/client/oauth.ts
@@ -1,8 +1,11 @@
///
///
+import Cookies from "js-cookie"
+
type BaseOptions = {
name: string
baseURL: string
+ additionalScope?: string
}
export type OauthProvider =
@@ -22,6 +25,9 @@ export type OauthProvider =
| "okta"
export const oauth = (options: BaseOptions, provider: OauthProvider): void => {
+ const additionalScope = options.additionalScope || ""
+ Cookies.set("oauth_scope", additionalScope, { expires: 1 / 288, path: "/" })
+
const oauthURL = `${options.baseURL}/api/${options.name}/oauth/authorization/${provider}`
window.location.href = oauthURL
}
diff --git a/src/client/signin.ts b/src/client/signin.ts
index af6ff46..a7ae363 100644
--- a/src/client/signin.ts
+++ b/src/client/signin.ts
@@ -1,8 +1,9 @@
-import { passwordSignin, type PasswordSigninPayload } from "./password.js"
import { oauth, type OauthProvider } from "./oauth.js"
+import { passwordSignin, type PasswordSigninPayload } from "./password.js"
interface BaseOptions {
name: string
baseURL: string
+ additionalScope?: string
}
export const signin = (options: BaseOptions) => {
diff --git a/src/collection/index.ts b/src/collection/index.ts
index d9f42e4..f448ec1 100644
--- a/src/collection/index.ts
+++ b/src/collection/index.ts
@@ -148,6 +148,14 @@ export const withAccountCollection = (
name: "access_token",
type: "text",
},
+ {
+ name: "refresh_token",
+ type: "text",
+ },
+ {
+ name: "expires_in",
+ type: "number",
+ },
{
name: "passkey",
type: "group",
diff --git a/src/core/protocols/oauth/oauth2_authorization.ts b/src/core/protocols/oauth/oauth2_authorization.ts
index d0303ec..ba727b0 100644
--- a/src/core/protocols/oauth/oauth2_authorization.ts
+++ b/src/core/protocols/oauth/oauth2_authorization.ts
@@ -1,13 +1,14 @@
import * as oauth from "oauth4webapi"
+import type { PayloadRequest } from "payload"
import type { OAuth2ProviderConfig } from "../../../types.js"
import { getCallbackURL } from "../../utils/cb.js"
-import type { PayloadRequest } from "payload"
export async function OAuth2Authorization(
pluginType: string,
request: PayloadRequest,
providerConfig: OAuth2ProviderConfig,
clientOrigin?: string | undefined,
+ additionalScope?: string,
): Promise {
const callback_url = getCallbackURL(
request.payload.config.serverURL,
@@ -32,7 +33,12 @@ export async function OAuth2Authorization(
authorizationURL.searchParams.set("client_id", client.client_id)
authorizationURL.searchParams.set("redirect_uri", callback_url.toString())
authorizationURL.searchParams.set("response_type", "code")
- authorizationURL.searchParams.set("scope", scope as string)
+ if (additionalScope) {
+ const totalScope = `${scope} ${additionalScope}`
+ authorizationURL.searchParams.set("scope", totalScope)
+ } else {
+ authorizationURL.searchParams.set("scope", scope as string)
+ }
authorizationURL.searchParams.set("code_challenge", code_challenge)
authorizationURL.searchParams.set(
"code_challenge_method",
diff --git a/src/core/protocols/oauth/oauth2_callback.ts b/src/core/protocols/oauth/oauth2_callback.ts
index 521a637..d1c4330 100644
--- a/src/core/protocols/oauth/oauth2_callback.ts
+++ b/src/core/protocols/oauth/oauth2_callback.ts
@@ -18,6 +18,7 @@ export async function OAuth2Callback(
secret: string,
successRedirectPath: string,
errorRedirectPath: string,
+ additionalScope?: string,
): Promise {
const parsedCookies = parseCookies(request.headers)
@@ -82,10 +83,16 @@ export async function OAuth2Callback(
email: userInfo.email,
name: userInfo.name ?? "",
sub: userInfo.sub,
- scope: providerConfig.scope,
+ scope:
+ providerConfig.scope + (additionalScope ? ` ${additionalScope}` : ""),
issuer: providerConfig.authorization_server.issuer,
picture: userInfo.picture ?? "",
access_token: token_result.access_token,
+ refresh_token: token_result.refresh_token ?? "",
+ expires_in:
+ typeof token_result.expires_in === "number"
+ ? token_result.expires_in
+ : undefined,
}
return await OAuthAuthentication(
diff --git a/src/core/protocols/oauth/oauth_authentication.ts b/src/core/protocols/oauth/oauth_authentication.ts
index 09044b9..10ffa1c 100644
--- a/src/core/protocols/oauth/oauth_authentication.ts
+++ b/src/core/protocols/oauth/oauth_authentication.ts
@@ -32,6 +32,8 @@ export async function OAuthAuthentication(
issuer: string
picture?: string | undefined
access_token: string
+ refresh_token?: string
+ expires_in?: number
},
): Promise {
const {
@@ -42,6 +44,8 @@ export async function OAuthAuthentication(
issuer,
picture,
access_token,
+ refresh_token,
+ expires_in,
} = account
const { payload } = request
@@ -86,6 +90,8 @@ export async function OAuthAuthentication(
picture: picture,
issuerName: issuer,
access_token,
+ refresh_token,
+ expires_in,
}
const accountRecords = await payload.find({
diff --git a/src/core/protocols/oauth/oidc_authorization.ts b/src/core/protocols/oauth/oidc_authorization.ts
index 79980a7..bec97f4 100644
--- a/src/core/protocols/oauth/oidc_authorization.ts
+++ b/src/core/protocols/oauth/oidc_authorization.ts
@@ -1,12 +1,13 @@
import * as oauth from "oauth4webapi"
+import type { PayloadRequest } from "payload"
import type { OIDCProviderConfig } from "../../../types.js"
import { getCallbackURL } from "../../utils/cb.js"
-import type { PayloadRequest } from "payload"
export async function OIDCAuthorization(
pluginType: string,
request: PayloadRequest,
providerConfig: OIDCProviderConfig,
+ additionalScope?: string,
): Promise {
const callback_url = getCallbackURL(
request.payload.config.serverURL,
@@ -33,7 +34,12 @@ export async function OIDCAuthorization(
authorizationURL.searchParams.set("client_id", client.client_id)
authorizationURL.searchParams.set("redirect_uri", callback_url.toString())
authorizationURL.searchParams.set("response_type", "code")
- authorizationURL.searchParams.set("scope", scope as string)
+ if (additionalScope) {
+ const totalScope = `${scope} ${additionalScope}`
+ authorizationURL.searchParams.set("scope", totalScope)
+ } else {
+ authorizationURL.searchParams.set("scope", scope as string)
+ }
authorizationURL.searchParams.set("code_challenge", code_challenge)
authorizationURL.searchParams.set(
"code_challenge_method",
diff --git a/src/core/protocols/oauth/oidc_callback.ts b/src/core/protocols/oauth/oidc_callback.ts
index 5f5bcec..89d561e 100644
--- a/src/core/protocols/oauth/oidc_callback.ts
+++ b/src/core/protocols/oauth/oidc_callback.ts
@@ -23,6 +23,7 @@ export async function OIDCCallback(
secret: string,
successRedirectPath: string,
errorRedirectPath: string,
+ additionalScope?: string,
): Promise {
const parsedCookies = parseCookies(request.headers)
@@ -116,10 +117,16 @@ export async function OIDCCallback(
email: result.email,
name: result.name ?? "",
sub: result.sub,
- scope: providerConfig.scope,
+ scope:
+ providerConfig.scope + (additionalScope ? ` ${additionalScope}` : ""),
issuer: providerConfig.issuer,
picture: result.picture ?? "",
access_token: token_result.access_token,
+ refresh_token: token_result.refresh_token ?? "",
+ expires_in:
+ typeof token_result.expires_in === "number"
+ ? token_result.expires_in
+ : undefined,
}
return await OAuthAuthentication(
diff --git a/src/core/protocols/password.ts b/src/core/protocols/password.ts
index 2444c21..5d244d6 100644
--- a/src/core/protocols/password.ts
+++ b/src/core/protocols/password.ts
@@ -1,4 +1,7 @@
import { parseCookies, type PayloadRequest } from "payload"
+import { v4 as uuid } from "uuid"
+import { APP_COOKIE_SUFFIX } from "../../constants.js"
+import { SuccessKind } from "../../types.js"
import {
EmailAlreadyExistError,
InvalidCredentials,
@@ -8,16 +11,13 @@ import {
UnauthorizedAPIRequest,
UserNotFoundAPIError,
} from "../errors/apiErrors.js"
-import { hashPassword, verifyPassword } from "../utils/password.js"
-import { SuccessKind } from "../../types.js"
-import { ephemeralCode, verifyEphemeralCode } from "../utils/hash.js"
-import { APP_COOKIE_SUFFIX } from "../../constants.js"
import {
createSessionCookies,
invalidateOAuthCookies,
verifySessionCookie,
} from "../utils/cookies.js"
-import { v4 as uuid } from "uuid"
+import { ephemeralCode, verifyEphemeralCode } from "../utils/hash.js"
+import { hashPassword, verifyPassword } from "../utils/password.js"
import { removeExpiredSessions } from "../utils/session.js"
const redirectWithSession = async (
@@ -92,13 +92,13 @@ export const PasswordSignin = async (
return new InvalidCredentials()
}
- const isVerifed = await verifyPassword(
+ const isVerified = await verifyPassword(
body.password,
userRecord.hashedPassword,
userRecord.hashSalt,
userRecord.hashIterations,
)
- if (!isVerifed) {
+ if (!isVerified) {
return new InvalidCredentials()
}
@@ -456,13 +456,13 @@ export const ResetPassword = async (
}
const user = docs[0]
- const isVerifed = await verifyPassword(
+ const isVerified = await verifyPassword(
body.currentPassword,
user.hashedPassword,
user.hashSalt,
user.hashIterations,
)
- if (!isVerifed) {
+ if (!isVerified) {
return new InvalidCredentials()
}
diff --git a/src/core/routeHandlers/oauth.ts b/src/core/routeHandlers/oauth.ts
index 8cc5f92..6ec600b 100644
--- a/src/core/routeHandlers/oauth.ts
+++ b/src/core/routeHandlers/oauth.ts
@@ -1,14 +1,15 @@
import type { PayloadRequest } from "payload"
+import { parseCookies } from "payload"
import type { OAuthProviderConfig } from "../../types.js"
import {
InvalidOAuthAlgorithm,
InvalidOAuthResource,
InvalidProvider,
} from "../errors/consoleErrors.js"
-import { OIDCAuthorization } from "../protocols/oauth/oidc_authorization.js"
import { OAuth2Authorization } from "../protocols/oauth/oauth2_authorization.js"
-import { OIDCCallback } from "../protocols/oauth/oidc_callback.js"
import { OAuth2Callback } from "../protocols/oauth/oauth2_callback.js"
+import { OIDCAuthorization } from "../protocols/oauth/oidc_authorization.js"
+import { OIDCCallback } from "../protocols/oauth/oidc_callback.js"
export function OAuthHandlers(
pluginType: string,
@@ -30,13 +31,27 @@ export function OAuthHandlers(
const resource = request.routeParams?.resource as string
+ const headers = request.headers
+ const cookies = parseCookies(headers)
+ const additionalScope = cookies.get("oauth_scope")
+
switch (resource) {
case "authorization":
switch (provider.algorithm) {
case "oidc":
- return OIDCAuthorization(pluginType, request, provider)
+ return OIDCAuthorization(
+ pluginType,
+ request,
+ provider,
+ additionalScope,
+ )
case "oauth2":
- return OAuth2Authorization(pluginType, request, provider)
+ return OAuth2Authorization(
+ pluginType,
+ request,
+ provider,
+ additionalScope,
+ )
default:
throw new InvalidOAuthAlgorithm()
}
@@ -53,6 +68,7 @@ export function OAuthHandlers(
secret,
successRedirectPath,
errorRedirectPath,
+ additionalScope,
)
}
case "oauth2": {
@@ -66,6 +82,7 @@ export function OAuthHandlers(
secret,
successRedirectPath,
errorRedirectPath,
+ additionalScope,
)
}
default:
diff --git a/src/types.ts b/src/types.ts
index 858e3ac..7e55707 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -111,6 +111,8 @@ export interface AccountInfo {
backedUp: boolean
}
access_token?: string
+ refresh_token?: string
+ expires_in?: number
}
export type PasswordProviderConfig = {