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
13 changes: 13 additions & 0 deletions packages/core/src/@types/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,19 @@ export interface SessionStrategy<DefaultUser extends User = User> {
* @unstable This API is experimental and may change in future releases.
*/
isProviderConnected(oauth: string, headers: Headers): Promise<boolean>

/**
* Refresh the user info in the session.
* @unstable This API is experimental and may change in future releases.
*/
refreshUserInfo(
user: Partial<DefaultUser>,
headers: Headers,
skipCSRFCheck?: boolean
): Promise<{
session: Session<DefaultUser> | null
headers: Headers
}>
Comment thread
halvaradop marked this conversation as resolved.
}

/** Inputs for constructing a session strategy implementation for a given identity schema. */
Expand Down
30 changes: 11 additions & 19 deletions packages/core/src/api/refreshUserInfo.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { HeadersBuilder } from "@aura-stack/router"
import { AuraAuthError } from "@/shared/errors.ts"
import { secureApiHeaders } from "@/shared/headers.ts"
import { getProviderTokens } from "./getProviderTokens.ts"
import { createValidation, handleApiError } from "@/shared/utils/api.ts"
import { toUnionHeaders, getStandardSession } from "@/shared/utils.ts"
import type {
FunctionAPIContext,
RefreshUserInfoAPIOptions,
Expand All @@ -24,16 +22,16 @@ export const refreshUserInfo = async <DefaultUser extends User = User>(
doubleSubmitToken = undefined,
}: FunctionAPIContext<RefreshUserInfoAPIOptions>
): Promise<RefreshUserInfoAPIReturn<DefaultUser>> => {
const { cookies } = ctx
try {
const doubleSubmitValidation = skipCSRFCheck && !!doubleSubmitToken
ctx.logger?.log("OAUTH_USERINFO_REQUEST_INITIATED", {
structuredData: { provider: oauth, skipCSRFCheck: skipCSRFCheck || Boolean(doubleSubmitToken) },
structuredData: { provider: oauth, skipCSRFCheck: doubleSubmitValidation },
})

const { provider, headers, rateLimit } = await createValidation(ctx, headersInit ?? requestInit?.headers)
.verifyOAuthProvider(oauth)
.verifySession()
.verifyCSRFToken(skipCSRFCheck && !!doubleSubmitToken)
.verifyCSRFToken(doubleSubmitValidation)
.buildRequest(requestInit, `/providers/${oauth}/user/refresh`)
.verifyRateLimit("refreshUserInfo")
.execute()
Expand Down Expand Up @@ -78,33 +76,27 @@ export const refreshUserInfo = async <DefaultUser extends User = User>(
structuredData: { provider: oauth, userId: userInfo.sub },
})

const sessionToken = await ctx.sessionStrategy.createSession(userInfo)
const newHeaders = new HeadersBuilder(headers)
.setCookie(cookies.sessionToken.name, sessionToken, cookies.sessionToken.attributes)
.toHeaders()

const mergedHeaders = toUnionHeaders(newHeaders, headers)

const session = await getStandardSession({
sessionToken,
jwt: ctx.jwtManager,
identity: ctx.identity,
})
const { session, headers: newHeaders } = await ctx.sessionStrategy.refreshUserInfo(
userInfo,
headers,
doubleSubmitValidation
)
return {
success: !!session,
headers: mergedHeaders,
headers: newHeaders,
session: session,
toResponse: () => {
return Response.json(
{
success: !!session,
session,
},
{ headers: mergedHeaders, status: 200 }
{ headers: newHeaders, status: 200 }
)
},
} as RefreshUserInfoAPIReturn<DefaultUser>
} catch (error) {
console.warn("Error refreshing user info:", error)
const { code, message, statusCode } = handleApiError(
error,
"UNKNOWN_REFRESH_USER_INFO_ERROR",
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/session/stateful.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,10 @@ export const createStatefulStrategy = <DefaultUser extends User = User>({
}
}

const refreshUserInfo = async (userInfo: TypedJWTPayload<DefaultUser>, headers: Headers, skipCSRFCheck: boolean) => {
return await refreshSession(headers, { user: userInfo }, skipCSRFCheck)
}

return {
getSession,
createSession,
Expand All @@ -1050,5 +1054,6 @@ export const createStatefulStrategy = <DefaultUser extends User = User>({
destroySession,
getProviderTokens,
isProviderConnected,
refreshUserInfo,
}
}
26 changes: 25 additions & 1 deletion packages/core/src/session/stateless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import { handleApiError } from "@/shared/utils/api.ts"
import { createJoseManager } from "@/session/jose-manager.ts"
import { createCookieManager } from "@/session/cookie-manager.ts"
import { refreshProviderToken } from "@/shared/utils/refresh-tokens.ts"
import { verifyCSRFToken, getErrorName, verifySessionToken, shouldRefresh, toUnionHeaders } from "@/shared/utils.ts"
import {
verifyCSRFToken,
getErrorName,
verifySessionToken,
shouldRefresh,
toUnionHeaders,
getStandardSession,
} from "@/shared/utils.ts"
import type {
Session,
SessionStrategy,
Expand Down Expand Up @@ -381,6 +388,22 @@ export const createStatelessStrategy = <DefaultUser extends User = User>({
}
}

const refreshUserInfo = async (userInfo: TypedJWTPayload<DefaultUser>, headers: Headers) => {
const sessionToken = await createSession(userInfo)

const newHeaders = new HeadersBuilder(headers)
.setCookie(cookies().sessionToken.name, sessionToken, cookies().sessionToken.attributes)
.toHeaders()

const session = await getStandardSession({
jwt,
identity,
sessionToken,
})
const mergedHeaders = toUnionHeaders(newHeaders, secureApiHeaders)
return { session, headers: mergedHeaders }
Comment thread
halvaradop marked this conversation as resolved.
Comment thread
halvaradop marked this conversation as resolved.
}

// JWT strategy: stateless tokens cannot be revoked server-side
const revokeSession = async (_sessionId: string): Promise<void> => {}

Expand All @@ -398,6 +421,7 @@ export const createStatelessStrategy = <DefaultUser extends User = User>({
revokeSession,
revokeToken,
isProviderConnected,
refreshUserInfo,
destroySession,
}
}
2 changes: 1 addition & 1 deletion packages/core/src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export const getStandardSession = async ({
if (!userClaims.sub) return null
return {
user: userClaims,
expires: exp,
expires: new Date(exp * 1000).toISOString(),
}
}

Expand Down
Loading
Loading