fix(domains,skatteverket): #1087 follow-ups: auth-path redirect exclusions, SKV callback hardening (#1094)
- Exclude auth/ and reset-password from the legacy-host redirect (#1092): email links sent before the cutover carry a PKCE code or recovery session whose cookies live on app.gnubok.se; forwarding them to the new domain breaks password resets and signup confirmations clicked after the flip. login/MFA stay redirected on purpose: a usable login page on the legacy host would establish sessions there and loop. Exclusion pattern extracted to lib/domains/legacy-redirect.ts with a unit test pinning the behavior. - Clean up ephemeral oauth state rows (incl. oauth_user_id) when the SKV token exchange fails (#1090): identity data must not outlive the flow; best-effort so cleanup failure never masks the user-facing error. - Assert the stored user is still a member of the company before the service-role storeTokens write (#1091): membership can be revoked between /authorize and the callback, and RLS no longer backstops the write. Checked before the exchange so the one-shot code is not burned. Fixes #1090, fixes #1091, fixes #1092. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e33cc2428d
commit
335d908614
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { isRedirectedFromLegacyHost } from '../legacy-redirect'
|
||||
|
||||
describe('legacy-host redirect exclusions', () => {
|
||||
it('forwards ordinary pages to the new domain', () => {
|
||||
expect(isRedirectedFromLegacyHost('/')).toBe(true)
|
||||
expect(isRedirectedFromLegacyHost('/dashboard')).toBe(true)
|
||||
expect(isRedirectedFromLegacyHost('/reports')).toBe(true)
|
||||
// login and MFA pages MUST redirect: a usable login page on the
|
||||
// legacy host would establish sessions there and loop users between
|
||||
// domains.
|
||||
expect(isRedirectedFromLegacyHost('/login')).toBe(true)
|
||||
expect(isRedirectedFromLegacyHost('/mfa')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps machine surfaces on the legacy host', () => {
|
||||
expect(isRedirectedFromLegacyHost('/api/v1/companies')).toBe(false)
|
||||
expect(isRedirectedFromLegacyHost('/api/extensions/ext/skatteverket/callback')).toBe(false)
|
||||
expect(isRedirectedFromLegacyHost('/.well-known/oauth-authorization-server')).toBe(false)
|
||||
expect(isRedirectedFromLegacyHost('/_next/static/chunk.js')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps PKCE-cookie-bound auth flows on the legacy host (#1092)', () => {
|
||||
// Password reset and signup confirmation links sent before the
|
||||
// cutover carry a PKCE code whose verifier cookie lives on the
|
||||
// legacy host; forwarding them would break the exchange.
|
||||
expect(isRedirectedFromLegacyHost('/auth/callback')).toBe(false)
|
||||
expect(isRedirectedFromLegacyHost('/reset-password')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Path exclusions for the legacy-host (app.gnubok.se) page redirect in
|
||||
* next.config.ts.
|
||||
*
|
||||
* Machine surfaces (api/, .well-known/), assets (_next/) and the
|
||||
* PKCE-cookie-bound auth flows (auth/, reset-password) must keep answering
|
||||
* on the legacy host after the app.accounted.se cutover; everything else
|
||||
* forwards to the new domain. auth/ and reset-password stay because email
|
||||
* links sent before the cutover carry a PKCE code or recovery session
|
||||
* whose cookies live on the legacy host (#1092); the login and MFA pages
|
||||
* are deliberately NOT excluded, since a usable login page on the legacy
|
||||
* host would establish sessions there and bounce users in a redirect loop.
|
||||
*/
|
||||
export const LEGACY_HOST_REDIRECT_EXCLUSIONS =
|
||||
'(?!api/|\\.well-known/|_next/|auth/|reset-password)'
|
||||
|
||||
/**
|
||||
* Mirror of how the path-to-regexp source `/:path((?!...).*)` decides
|
||||
* whether a legacy-host request is forwarded. Used by tests to pin the
|
||||
* exclusion behavior without booting Next's router.
|
||||
*/
|
||||
export function isRedirectedFromLegacyHost(pathname: string): boolean {
|
||||
const relative = pathname.replace(/^\//, '')
|
||||
return new RegExp(`^${LEGACY_HOST_REDIRECT_EXCLUSIONS}.*$`).test(relative)
|
||||
}
|
||||
Reference in New Issue
Block a user