-
Notifications
You must be signed in to change notification settings - Fork 68
feat(auth): add Ory auth provider integration #357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import type { NextRequest } from 'next/server' | ||
| import { NextResponse } from 'next/server' | ||
| import { AUTH_URLS } from '@/configs/urls' | ||
| import { l } from '@/core/shared/clients/logger/logger' | ||
|
|
||
| // Auth.js renders its built-in `${basePath}/error` page when something fails | ||
| // during the OAuth dance (most commonly a stale state/PKCE/nonce cookie that | ||
| // expired while the user lingered on the Ory hosted UI). We point | ||
| // `pages.error` here so the user never sees that page - we log the failure | ||
| // for observability and bounce them back to /sign-in, which restarts the | ||
| // flow with fresh cookies via the middleware -> oauth-start chain. | ||
| // | ||
| // A short-lived cookie prevents tight loops when the underlying failure is | ||
| // genuinely persistent (e.g. ORY_SDK_URL misconfigured). After one recovery | ||
| // attempt in the window, subsequent failures fall back to the marketing | ||
| // root so the user isn't bounced indefinitely. | ||
| const RECOVERY_COOKIE = 'auth_recover_attempted' | ||
| const RECOVERY_COOKIE_MAX_AGE_SECONDS = 30 | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| const errorCode = request.nextUrl.searchParams.get('error') ?? 'unknown' | ||
| const alreadyAttempted = request.cookies.get(RECOVERY_COOKIE)?.value === '1' | ||
|
|
||
| l.error( | ||
| { | ||
| key: 'oauth_recover:auth_js_error', | ||
| context: { error_code: errorCode, already_attempted: alreadyAttempted }, | ||
| }, | ||
| 'Auth.js OAuth flow failed; recovering user' | ||
| ) | ||
|
|
||
| const destination = alreadyAttempted ? '/' : AUTH_URLS.SIGN_IN | ||
| const response = NextResponse.redirect(new URL(destination, request.url)) | ||
|
|
||
| if (alreadyAttempted) { | ||
| response.cookies.delete(RECOVERY_COOKIE) | ||
| } else { | ||
| response.cookies.set(RECOVERY_COOKIE, '1', { | ||
| maxAge: RECOVERY_COOKIE_MAX_AGE_SECONDS, | ||
| httpOnly: true, | ||
| sameSite: 'lax', | ||
| path: '/', | ||
| secure: process.env.NODE_ENV === 'production', | ||
| }) | ||
| } | ||
|
|
||
| return response | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { signIn } from '@/auth' | ||
| import { normalizeOryReturnTo } from '@/core/server/auth/ory/build-start-url' | ||
| import { | ||
| readOrySignupMetadataFromHeaders, | ||
| setOrySignupMetadataCookie, | ||
| } from '@/core/server/auth/ory/signup-metadata' | ||
|
|
||
| // Server-side entry point for the Ory OAuth2 flow. Pages redirect here | ||
| // instead of rendering a client-side form so that Auth.js can set its | ||
| // state/PKCE cookies (only allowed in route handlers / server actions | ||
| // / middleware) without any client JS in the loop. | ||
| // | ||
| // `intent=signup` forwards `prompt=registration` to Hydra, which routes | ||
| // to its registration UI (`urls.registration`, default `/ui/registration`) | ||
| // instead of the login UI. | ||
| // | ||
| // `intent=reauth` forwards `prompt=login`, forcing Hydra to redo the login | ||
| // flow even with an active session so we get a fresh `auth_time`. Used to | ||
| // re-authenticate before sensitive account changes (password). | ||
| // https://www.ory.com/docs/oauth2-oidc/authorization-code-flow | ||
| export async function GET(request: Request) { | ||
| const url = new URL(request.url) | ||
| const intent = url.searchParams.get('intent') | ||
| const redirectTo = | ||
| normalizeOryReturnTo(url.searchParams.get('returnTo')) ?? '/dashboard' | ||
|
|
||
| const authorizationParams = | ||
| intent === 'signup' | ||
| ? { prompt: 'registration' } | ||
| : intent === 'reauth' | ||
| ? { prompt: 'login' } | ||
| : undefined | ||
|
|
||
| if (intent === 'signup') { | ||
| await setOrySignupMetadataCookie( | ||
| readOrySignupMetadataFromHeaders(request.headers) | ||
| ) | ||
| } | ||
|
|
||
| await signIn('ory', { redirectTo }, authorizationParams) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { handlers } from '@/auth' | ||
|
|
||
| export const { GET, POST } = handlers |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import 'server-only' | ||
|
|
||
| import type { NextRequest } from 'next/server' | ||
| import { NextResponse } from 'next/server' | ||
| import { signOut } from '@/auth' | ||
| import { | ||
| buildOryLogoutUrl, | ||
| ORY_BOOTSTRAP_FAILURE_ID_TOKEN_COOKIE, | ||
| ORY_POST_LOGOUT_PATH, | ||
| } from '@/core/server/auth/ory/signout' | ||
| import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger' | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| const origin = request.nextUrl.origin | ||
| const idToken = request.cookies.get( | ||
| ORY_BOOTSTRAP_FAILURE_ID_TOKEN_COOKIE | ||
| )?.value | ||
|
|
||
| try { | ||
| await signOut({ redirect: false }) | ||
| } catch (error) { | ||
| l.warn( | ||
| { | ||
| key: 'oauth_bootstrap_failed:authjs_sign_out:error', | ||
| error: serializeErrorForLog(error), | ||
| }, | ||
| 'Auth.js signOut() failed after Ory bootstrap failure' | ||
| ) | ||
| } | ||
|
|
||
| const logoutUrl = idToken ? buildOryLogoutUrl({ idToken, origin }) : null | ||
|
|
||
| if (!logoutUrl) { | ||
| l.error( | ||
| { | ||
| key: 'oauth_bootstrap_failed:missing_logout_context', | ||
| context: { | ||
| has_id_token: !!idToken, | ||
| has_ory_sdk_url: !!process.env.ORY_SDK_URL, | ||
| }, | ||
| }, | ||
| 'Could not perform Ory logout after bootstrap failure' | ||
| ) | ||
| } | ||
|
|
||
| const response = NextResponse.redirect( | ||
| logoutUrl ?? new URL(ORY_POST_LOGOUT_PATH, origin) | ||
| ) | ||
| response.cookies.delete(ORY_BOOTSTRAP_FAILURE_ID_TOKEN_COOKIE) | ||
| return response | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import 'server-only' | ||
|
|
||
| import type { NextRequest } from 'next/server' | ||
| import { NextResponse } from 'next/server' | ||
| import { auth, signOut } from '@/auth' | ||
| import { revokeKratosSessionsForIdentity } from '@/core/server/auth/ory/kratos-session' | ||
| import { | ||
| buildOryLogoutUrl, | ||
| ORY_POST_LOGOUT_PATH, | ||
| } from '@/core/server/auth/ory/signout' | ||
| import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger' | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Agentic Security Review Impact: An attacker can force a logged-in victim to be signed out and have their Ory sessions revoked, causing involuntary session disruption without user intent. Reviewed by Cursor Security Reviewer for commit 0651a6e. Configure here. |
||
| export async function GET(request: NextRequest) { | ||
| const origin = request.nextUrl.origin | ||
| const postLogoutUrl = new URL(ORY_POST_LOGOUT_PATH, origin) | ||
|
|
||
| let idToken: string | undefined | ||
| let identityId: string | undefined | ||
| try { | ||
| const session = await auth() | ||
| idToken = session?.idToken | ||
| // The Kratos identity id resolved at sign-in — NOT the OIDC subject (which | ||
| // is the E2B user id) — so we revoke the right identity's Kratos sessions. | ||
| identityId = session?.identityId | ||
| } catch (error) { | ||
| l.warn( | ||
| { | ||
| key: 'oauth_signout:read_session:error', | ||
| error: serializeErrorForLog(error), | ||
| }, | ||
| 'failed to read Auth.js session before sign-out' | ||
| ) | ||
| } | ||
|
|
||
| try { | ||
| await signOut({ redirect: false }) | ||
| } catch (error) { | ||
| l.warn( | ||
| { | ||
| key: 'oauth_signout:authjs_sign_out:error', | ||
| error: serializeErrorForLog(error), | ||
| }, | ||
| 'Auth.js signOut() failed' | ||
| ) | ||
| } | ||
|
|
||
| if (identityId) { | ||
| await revokeKratosSessionsForIdentity(identityId) | ||
| } | ||
|
|
||
| const logoutUrl = idToken ? buildOryLogoutUrl({ idToken, origin }) : null | ||
| if (!logoutUrl) { | ||
| return NextResponse.redirect(postLogoutUrl) | ||
| } | ||
|
|
||
| return NextResponse.redirect(logoutUrl.toString()) | ||
| } | ||


Uh oh!
There was an error while loading. Please reload this page.