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
7 changes: 7 additions & 0 deletions packages/core/src/@types/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,15 @@ export interface SessionStrategy<DefaultUser extends User = User> {

/**
* Revoke the access token for a specific OAuth provider.
* @unstable This API is experimental and may change in future releases.
*/
revokeToken(oauth: string, headers: Headers, disconnect: boolean): Promise<Headers>

/**
* Check if the user is connected to a specific OAuth provider.
* @unstable This API is experimental and may change in future releases.
*/
isProviderConnected(oauth: string, headers: Headers): Promise<boolean>
}

/** Inputs for constructing a session strategy implementation for a given identity schema. */
Expand Down
23 changes: 2 additions & 21 deletions packages/core/src/api/isProviderConnected.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getCookie } from "@/cookie.ts"
import { secureApiHeaders } from "@/shared/headers.ts"
import { createValidation, handleApiError } from "@/shared/utils/api.ts"
import type { LiteralUnion } from "@/@types/utility.ts"
Expand All @@ -9,36 +8,18 @@ export const isProviderConnected = async (
oauth: LiteralUnion<BuiltInOAuthProvider>,
{ ctx, headers: headersInit, request: requestInit }: FunctionAPIContext<ProviderConnectedAPIOptions>
): Promise<ProviderConnectedAPIReturn> => {
const { cookies, jwtManager } = ctx
try {
ctx.logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", {
structuredData: { provider: oauth, operation: "check_connection" },
})

const { headers, request } = await createValidation(ctx, headersInit ?? requestInit?.headers)
const { headers } = await createValidation(ctx, headersInit ?? requestInit?.headers)
.verifyOAuthProvider(oauth)
.verifySession()
.buildRequest(requestInit, `/providers/${oauth}`)
.execute()

const cookieName = `${cookies.accessToken.name}.${oauth}`
let cookieValue: string
try {
cookieValue = getCookie(request, cookieName)
} catch {
ctx.logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", {
structuredData: { provider: oauth, hasCookie: false },
})
return {
success: true,
connected: false,
headers,
toResponse: () => Response.json({ success: true, connected: false }, { status: 200, headers }),
}
}

const decodedToken = await jwtManager.verifyToken(cookieValue)
const connected = !!decodedToken
const connected = await ctx.sessionStrategy.isProviderConnected(oauth, headers)

ctx.logger?.log("OAUTH_ACCESS_TOKEN_SUCCESS", {
structuredData: { provider: oauth, connected },
Expand Down
84 changes: 84 additions & 0 deletions packages/core/src/session/stateful.ts
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,89 @@ export const createStatefulStrategy = <DefaultUser extends User = User>({
}
}

const isProviderConnected = async (oauthId: string, headers: Headers): Promise<boolean> => {
logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", {
structuredData: {
provider: oauthId,
operation: "isProviderConnected",
},
})

try {
const { sessionToken } = cookieConfig.getCookie(headers)
if (!sessionToken) {
logger?.log("SESSION_TOKEN_MISSING", {
structuredData: {
reason: "no_session_token",
},
})
return false
}

const sessionByToken = await config.adapter.getSessionByToken(sessionToken)
if (!sessionByToken || !sessionByToken.user) {
logger?.log("AUTH_SESSION_INVALID", {
structuredData: {
reason: "session_not_found_or_no_user",
},
})
return false
}

const isExpired = Date.now() > sessionByToken.expiresAt.getTime()
if (sessionByToken.status !== "active" || isExpired) {
if (isExpired) {
await config.adapter.revokeSession(sessionByToken.id, "user_logout")
}
logger?.log("AUTH_SESSION_INVALID", {
structuredData: {
reason: "session_expired_or_inactive",
},
})
return false
}
Comment thread
halvaradop marked this conversation as resolved.

logger?.log("AUTH_SESSION_VALID", {
structuredData: {
user_id: sessionByToken.userId,
session_id: sessionByToken.id,
},
})

const accounts = await config.adapter.getAccountsByUserId(sessionByToken.userId)
const account = accounts.find((acc) => acc.provider === oauthId)

if (!account) {
logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", {
structuredData: {
provider: oauthId,
reason: "account_not_found_for_user",
},
})
return false
}

const isConnected = account.status === "active"
logger?.log("OAUTH_ACCESS_TOKEN_SUCCESS", {
structuredData: {
provider: oauthId,
connected: isConnected,
},
})

return isConnected
} catch (error) {
logger?.log("OAUTH_ACCESS_TOKEN_ERROR", {
structuredData: {
provider: oauthId,
error_type: getErrorName(error),
error_message: error instanceof Error ? error.message : String(error),
},
})
return false
}
}

return {
getSession,
createSession,
Expand All @@ -966,5 +1049,6 @@ export const createStatefulStrategy = <DefaultUser extends User = User>({
revokeToken,
destroySession,
getProviderTokens,
isProviderConnected,
}
}
32 changes: 31 additions & 1 deletion packages/core/src/session/stateless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,27 @@ export const createStatelessStrategy = <DefaultUser extends User = User>({
return toUnionHeaders(builder, headers)
}

const isProviderConnected = async (oauthId: string, headers: Headers): Promise<boolean> => {
const cookieName = `${cookies().accessToken.name}.${oauthId}`
let cookieValue: string
try {
cookieValue = getCookie(headers, cookieName)
} catch {
logger?.log("OAUTH_ACCESS_TOKEN_REQUEST_INITIATED", {
structuredData: { provider: oauthId, hasCookie: false },
})
return false
}

try {
const decodedToken = await jwt.verifyToken(cookieValue)
return !!decodedToken
} catch (error) {
logger?.log("AUTH_SESSION_INVALID", { structuredData: { error_type: getErrorName(error) } })
return false
}
}
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 @@ -369,5 +390,14 @@ export const createStatelessStrategy = <DefaultUser extends User = User>({
return cookieConfig.clear()
}

return { getSession, createSession, getProviderTokens, refreshSession, revokeSession, revokeToken, destroySession }
return {
getSession,
createSession,
getProviderTokens,
refreshSession,
revokeSession,
revokeToken,
isProviderConnected,
destroySession,
}
}
Loading
Loading