Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
18 changes: 12 additions & 6 deletions src/client/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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() {
Expand Down
6 changes: 6 additions & 0 deletions src/client/oauth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
import Cookies from "js-cookie"

type BaseOptions = {
name: string
baseURL: string
additionalScope?: string
}

export type OauthProvider =
Expand All @@ -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
}
3 changes: 2 additions & 1 deletion src/client/signin.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down
8 changes: 8 additions & 0 deletions src/collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions src/core/protocols/oauth/oauth2_authorization.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
const callback_url = getCallbackURL(
request.payload.config.serverURL,
Expand All @@ -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",
Expand Down
9 changes: 8 additions & 1 deletion src/core/protocols/oauth/oauth2_callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export async function OAuth2Callback(
secret: string,
successRedirectPath: string,
errorRedirectPath: string,
additionalScope?: string,
): Promise<Response> {
const parsedCookies = parseCookies(request.headers)

Expand Down Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions src/core/protocols/oauth/oauth_authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export async function OAuthAuthentication(
issuer: string
picture?: string | undefined
access_token: string
refresh_token?: string
expires_in?: number
},
): Promise<Response> {
const {
Expand All @@ -42,6 +44,8 @@ export async function OAuthAuthentication(
issuer,
picture,
access_token,
refresh_token,
expires_in,
} = account
const { payload } = request

Expand Down Expand Up @@ -86,6 +90,8 @@ export async function OAuthAuthentication(
picture: picture,
issuerName: issuer,
access_token,
refresh_token,
expires_in,
}

const accountRecords = await payload.find({
Expand Down
10 changes: 8 additions & 2 deletions src/core/protocols/oauth/oidc_authorization.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
const callback_url = getCallbackURL(
request.payload.config.serverURL,
Expand All @@ -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",
Expand Down
9 changes: 8 additions & 1 deletion src/core/protocols/oauth/oidc_callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export async function OIDCCallback(
secret: string,
successRedirectPath: string,
errorRedirectPath: string,
additionalScope?: string,
): Promise<Response> {
const parsedCookies = parseCookies(request.headers)

Expand Down Expand Up @@ -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(
Expand Down
18 changes: 9 additions & 9 deletions src/core/protocols/password.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 (
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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()
}

Expand Down
25 changes: 21 additions & 4 deletions src/core/routeHandlers/oauth.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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()
}
Expand All @@ -53,6 +68,7 @@ export function OAuthHandlers(
secret,
successRedirectPath,
errorRedirectPath,
additionalScope,
)
}
case "oauth2": {
Expand All @@ -66,6 +82,7 @@ export function OAuthHandlers(
secret,
successRedirectPath,
errorRedirectPath,
additionalScope,
)
}
default:
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ export interface AccountInfo {
backedUp: boolean
}
access_token?: string
refresh_token?: string
expires_in?: number
}

export type PasswordProviderConfig = {
Expand Down