Skip to content
Merged
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
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"start": "next start"
},
"dependencies": {
"next": "16.0.8",
"next": "16.0.10",
"react": "19.2.1",
"react-dom": "19.2.1"
},
Expand Down
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const oauth = Object.keys(integrations) as Array<keyof typeof integrations>

const auth = createAuth({
oauth,
trustedProxyHeaders: true,
})

const {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ export const OAuthProviders = ({ providers, isAuthenticated }: OAuthProvidersPro
</p>
</div>
{provider.configured && !isAuthenticated && (
<form action={`auth/signIn/${provider.id}`} method="GET" className="mt-3">
<form
action={`auth/signIn/${provider.id}`}
method="GET"
className="mt-3"
>
<button className="w-full px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium transition-colors flex items-center justify-center gap-2">
SignIn with {provider.name}
</button>
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion docs/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { NextConfig } from "next"

const withMDX = createMDX()

const config = {
const config: NextConfig = {
reactStrictMode: true,
}

Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/@types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ export interface AuthConfig {
* Base path for all authentication routes. Default is `/auth`.
*/
basePath?: RoutePattern
/**
* Enable trusted proxy headers for scenarios where the application is behind a reverse proxy or load balancer.
* This setting allows Aura Auth to correctly interpret headers like `X-Forwarded-For` and `X-Forwarded-Proto`
* to determine the original client IP address and protocol.
*
* Default is `false`. Enable this option only if you are certain that your application is behind a trusted proxy.
* Misconfiguration can lead to security vulnerabilities, such as incorrect handling of secure cookies or
* inaccurate client IP logging.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded
*/
trustedProxyHeaders?: boolean
}

export interface AuthConfigInternal {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/@types/router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ declare module "@aura-stack/router" {
oauth: ReturnType<typeof createOAuthIntegrations>
cookies: CookieOptions
jose: ReturnType<typeof createJoseInstance>
basePath: string
trustedProxyHeaders?: boolean
}
}
4 changes: 1 addition & 3 deletions packages/core/src/actions/callback/access-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ export const createAccessToken = async (
}
const { accessToken, clientId, clientSecret, code: codeParsed, redirectURI: redirectParsed } = parsed.data
try {
const request = new Request(accessToken, {
const response = await fetch(accessToken, {
method: "POST",
headers: {
Accept: "application/json",

"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
Expand All @@ -41,7 +40,6 @@ export const createAccessToken = async (
code_verifier: codeVerifier,
}).toString(),
})
const response = await fetch(request)
const json = await response.json()
const token = OAuthAccessTokenResponse.safeParse(json)
if (!token.success) {
Expand Down
16 changes: 10 additions & 6 deletions packages/core/src/actions/callback/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ export const callbackAction = (oauth: AuthConfigInternal["oauth"]) => {
request,
params: { oauth },
searchParams: { code, state },
context: { oauth: oauthIntegrations, cookies, jose },
context: { oauth: oauthIntegrations, cookies, jose, trustedProxyHeaders },
} = ctx
try {
const oauthConfig = oauthIntegrations[oauth]

const cookieOptions = secureCookieOptions(request, cookies)
const cookieOptions = secureCookieOptions(request, cookies, trustedProxyHeaders)
const cookieState = getCookie(request, "state", cookieOptions)
const cookieRedirectTo = getCookie(request, "redirect_to", cookieOptions)
const cookieRedirectURI = getCookie(request, "redirect_uri", cookieOptions)
Expand Down Expand Up @@ -85,10 +85,14 @@ export const callbackAction = (oauth: AuthConfigInternal["oauth"]) => {
const csrfCookie = setCookie(
"csrfToken",
csrfToken,
secureCookieOptions(request, {
...cookies,
flag: "host",
})
secureCookieOptions(
request,
{
...cookies,
flag: "host",
},
trustedProxyHeaders
)
)
headers.set("Set-Cookie", sessionCookie)
headers.append("Set-Cookie", expireCookie("state", cookieOptions))
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/actions/csrfToken/csrfToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { getCookie, secureCookieOptions, setCookie } from "@/cookie.js"
export const csrfTokenAction = createEndpoint("GET", "/csrfToken", async (ctx) => {
const {
request,
context: { cookies, jose },
context: { cookies, jose, trustedProxyHeaders },
} = ctx
const cookieOptions = secureCookieOptions(request, { ...cookies, flag: "host" })
const cookieOptions = secureCookieOptions(request, { ...cookies, flag: "host" }, trustedProxyHeaders)

const existingCSRFToken = getCookie(request, "csrfToken", cookieOptions, true)
const csrfToken = await createCSRF(jose, existingCSRFToken)
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/actions/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export const SESSION_VERSION = "v0.1.0"
export const sessionAction = createEndpoint("GET", "/session", async (ctx) => {
const {
request,
context: { cookies, jose },
context: { cookies, jose, trustedProxyHeaders },
} = ctx
const cookieOptions = secureCookieOptions(request, cookies)
const cookieOptions = secureCookieOptions(request, cookies, trustedProxyHeaders)
try {
const session = getCookie(request, "sessionToken", cookieOptions)
const decoded = (await jose.decodeJWT(session)) as OAuthUserProfile as OAuthUserProfileInternal
Expand Down
30 changes: 23 additions & 7 deletions packages/core/src/actions/signIn/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,31 @@ export const createAuthorizationURL = (
return `${authorizeURL}?${searchParams}`
}

export const getOriginURL = (request: Request, trustedProxyHeaders?: boolean) => {
const headers = request.headers
if (trustedProxyHeaders) {
const protocol = headers.get("X-Forwarded-Proto") ?? headers.get("Forwarded")?.match(/proto=([^;]+)/i)?.[1] ?? "http"
const host =
headers.get("X-Forwarded-Host") ??
headers.get("Host") ??
headers.get("Forwarded")?.match(/host=([^;]+)/i)?.[1] ??
null
return new URL(`${protocol}://${host}${getNormalizedOriginPath(new URL(request.url).pathname)}`)
} else {
return new URL(getNormalizedOriginPath(request.url))
}
}

/**
* Creates the redirect URI for the OAuth callback based on the original request URL and the OAuth provider.
*
* @param requestURL - the original request URL
* @param oauth - OAuth provider name
* @returns The redirect URI for the OAuth callback.
*/
export const createRedirectURI = (requestURL: string, oauth: string) => {
const url = new URL(requestURL)
return `${url.origin}/auth/callback/${oauth}`
export const createRedirectURI = (request: Request, oauth: string, basePath: string, trustedProxyHeaders?: boolean) => {
const url = getOriginURL(request, trustedProxyHeaders)
return `${url.origin}${basePath}/callback/${oauth}`
}

/**
Expand All @@ -55,11 +70,12 @@ export const createRedirectURI = (requestURL: string, oauth: string) => {
* @param redirectTo Optional redirectTo parameter to override the referer
* @returns The pathname of the referer URL if origins match
*/
export const createRedirectTo = (request: Request, redirectTo?: string) => {
export const createRedirectTo = (request: Request, redirectTo?: string, trustedProxyHeaders?: boolean) => {
try {
const hostedURL = new URL(getNormalizedOriginPath(request.url))
const origin = request.headers.get("Origin")
const referer = request.headers.get("Referer")
const headers = request.headers
const origin = headers.get("Origin")
const referer = headers.get("Referer")
let hostedURL = getOriginURL(request, trustedProxyHeaders)
if (redirectTo) {
if (redirectTo.startsWith("/")) {
return sanitizeURL(redirectTo)
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/actions/signIn/signIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@ export const signInAction = (oauth: AuthConfigInternal["oauth"]) => {
const {
request,
params: { oauth, redirectTo },
context: { oauth: oauthIntegrations, cookies },
context: { oauth: oauthIntegrations, cookies, trustedProxyHeaders, basePath },
} = ctx
try {
const cookieOptions = secureCookieOptions(request, cookies)
const cookieOptions = secureCookieOptions(request, cookies, trustedProxyHeaders)
const state = generateSecure()
const redirectURI = createRedirectURI(request.url, oauth)
const redirectURI = createRedirectURI(request, oauth, basePath, trustedProxyHeaders)
const stateCookie = setCookie("state", state, oauthCookie(cookieOptions))
const redirectURICookie = setCookie("redirect_uri", redirectURI, oauthCookie(cookieOptions))
const redirectToCookie = setCookie(
"redirect_to",
createRedirectTo(request, redirectTo),
createRedirectTo(request, redirectTo, trustedProxyHeaders),
oauthCookie(cookieOptions)
)

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/actions/signOut/signOut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ export const signOutAction = createEndpoint(
request,
headers,
searchParams: { redirectTo },
context: { cookies, jose },
context: { cookies, jose, trustedProxyHeaders },
} = ctx
try {
const cookiesOptions = secureCookieOptions(request, cookies)
const cookiesOptions = secureCookieOptions(request, cookies, trustedProxyHeaders)
const session = getCookie(request, "sessionToken", cookiesOptions)
const csrfToken = getCookie(request, "csrfToken", {
...cookiesOptions,
Expand Down
15 changes: 10 additions & 5 deletions packages/core/src/cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,17 @@ export const createSessionCookie = async (
* @param cookieOptions Cookie options from the Aura Auth configuration
* @returns The finalized cookie options to be used for setting cookies
*/
export const secureCookieOptions = (request: Request, cookieOptions: CookieOptions): CookieOptionsInternal => {
export const secureCookieOptions = (
request: Request,
cookieOptions: CookieOptions,
trustedProxyHeaders?: boolean
): CookieOptionsInternal => {
const name = cookieOptions.name ?? COOKIE_NAME
const isSecure =
request.url.startsWith("https://") ||
request.headers.get("X-Forwarded-Proto") === "https" ||
request.headers.get("Forwarded")?.includes("proto=https")
const isSecure = trustedProxyHeaders
? request.url.startsWith("https://") ||
request.headers.get("X-Forwarded-Proto") === "https" ||
request.headers.get("Forwarded")?.includes("proto=https")
: request.url.startsWith("https://")
if (!cookieOptions.options?.httpOnly) {
console.warn(
"[WARNING]: Cookie is configured without HttpOnly. This allows JavaScript access via document.cookie and increases XSS risk."
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const createInternalConfig = (authConfig?: AuthConfig): RouterConfig => {
oauth: createOAuthIntegrations(authConfig?.oauth),
cookies: authConfig?.cookies ?? defaultCookieConfig,
jose: createJoseInstance(authConfig?.secret),
basePath: authConfig?.basePath ?? "/auth",
trustedProxyHeaders: !!authConfig?.trustedProxyHeaders,
},
}
}
Expand Down
Loading