fix: recipe config cleanup, report-only CSP, logout redirect validation (#6124)

This commit is contained in:
Alireza authored and GitHub committed 2026-07-07 13:18:46 -04:00
1 parent 973631b7e8
commit f8546ce0e0
27 files changed
+446 -75

No files matched your search

@@ -0,0 +1,7 @@
# Copy this file to .env next to docker-compose.yml and fill in strong
# values. docker compose refuses to start while either is unset.
KEYCLOAK_ADMIN_PASSWORD=
# POSTGRES_PASSWORD is the single PostgreSQL credential: it provisions the
# keycloak database role and Keycloak reuses it to connect (KC_DB_PASSWORD is
# derived from it in docker-compose.yml), so there is one value, not two.
POSTGRES_PASSWORD=
@@ -0,0 +1,42 @@
# Security notes for this recipe
## Secrets
This recipe no longer ships working credential values. Before `docker compose up`:
- Generate a fresh OAuth client secret for the `ohif_viewer` client and use it
to replace the `REPLACE_WITH_A_GENERATED_CLIENT_SECRET` placeholder in BOTH
`config/ohif-keycloak-realm.json` and `config/oauth2-proxy.cfg` (the two
values must match). To generate one after the realm is imported: Keycloak
admin console -> Clients -> ohif_viewer -> Credentials -> Regenerate.
- Copy `.env.example` to `.env` and set `POSTGRES_PASSWORD` and
`KEYCLOAK_ADMIN_PASSWORD` to strong values. `docker compose` refuses to
start while either is unset. `POSTGRES_PASSWORD` is the single PostgreSQL
credential - it both provisions the `keycloak` database role and is what
Keycloak uses to connect (the compose file derives `KC_DB_PASSWORD` from it),
so there is one database password to set, not two.
## Rotate if you deployed from an earlier checkout
Earlier versions of this recipe committed a fixed client secret and default
admin/database passwords to the public repository. A committed secret stays
burned even after this change, so any deployment created from an earlier
checkout must rotate:
- the `ohif_viewer` client secret (Keycloak admin console -> Clients ->
ohif_viewer -> Credentials -> Regenerate, then update
`config/oauth2-proxy.cfg` to match), and
- the Keycloak admin and PostgreSQL passwords. Note that changing
`POSTGRES_PASSWORD` in `.env` alone does not re-password an existing
database: Postgres only applies it when the `postgres_data` volume is first
initialized. To actually rotate it on an existing deployment, either
`ALTER ROLE keycloak WITH PASSWORD ...` inside the running database or
recreate the `postgres_data` volume.
## CORS
The wildcard `Access-Control-Allow-Origin: *` defaults were removed from
`config/nginx.conf`. The viewer is served by the same nginx as the dcm4chee
proxy, so same-origin deployments need no CORS headers. If you host the viewer
on a different origin, use the commented explicit-origin example in the nginx
config - never `*` on an authenticated endpoint that serves PHI.
@@ -54,6 +54,15 @@ http {
gzip_comp_level 9;
etag on;
# Content-Security-Policy: test in Report-Only mode first and watch
# the browser console for violations before switching the header name
# to the enforcing Content-Security-Policy. Note: nginx add_header does
# not merge into a location that sets its own add_header - if you
# promote this to an active header, re-declare it inside every location
# block that already uses add_header (several proxy locations here do)
# or it will silently not apply there.
# add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https: blob:; worker-src 'self' blob:; object-src 'self' blob:; frame-src 'self' blob:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
location /sw.js {
add_header Cache-Control "no-cache";
proxy_cache_bypass $http_pragma;
@@ -101,19 +110,25 @@ http {
expires 0;
add_header Cache-Control private;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept' always;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# CORS: this recipe serves the viewer from this same nginx, so
# browser requests to this endpoint are same-origin and need NO
# CORS headers at all. If you host the viewer on a different
# origin, allow that origin explicitly and only that origin -
# never use '*' on an authenticated endpoint that serves PHI.
# Cross-origin example (response headers plus OPTIONS preflight):
# add_header 'Access-Control-Allow-Origin' 'https://viewer.example.com' always;
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
# add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept' always;
#
# if ($request_method = OPTIONS) {
# add_header 'Access-Control-Allow-Origin' 'https://viewer.example.com';
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
# add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept';
# add_header 'Access-Control-Max-Age' 1728000;
# add_header 'Content-Type' 'text/plain; charset=utf-8';
# add_header 'Content-Length' 0;
# return 204;
# }
rewrite ^/pacs/(.*) /dcm4chee-arc/aets/DCM4CHEE/rs/$1 break;
proxy_pass http://arc:8080;
@@ -155,19 +170,8 @@ http {
expires 0;
add_header Cache-Control private;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept' always;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# CORS: same-origin deployment - no CORS headers needed here; see
# the note on the /pacs/ location above before adding any.
proxy_pass http://arc:8080;
}
@@ -4,7 +4,7 @@ email_domains=["*"]
cookie_secure="false"
cookie_expire="9m30s"
cookie_refresh="5m"
client_secret="2Xtlde7aozdkzzYHdIxQNfPDr0wNPTgg"
client_secret="REPLACE_WITH_A_GENERATED_CLIENT_SECRET"
client_id="ohif_viewer"
redirect_url="http://YOUR_DOMAIN/oauth2/callback"
@@ -686,7 +686,7 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "2Xtlde7aozdkzzYHdIxQNfPDr0wNPTgg",
"secret": "REPLACE_WITH_A_GENERATED_CLIENT_SECRET",
"redirectUris": [
"http://127.0.0.1/oauth2/callback"
],
@@ -94,14 +94,17 @@ services:
KC_DB_URL: 'jdbc:postgresql://postgres:5432/keycloak'
KC_DB_SCHEMA: public
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: password
# Keycloak authenticates to Postgres as the keycloak role provisioned
# below, so this must be the same value as POSTGRES_PASSWORD. Sourced
# from the single POSTGRES_PASSWORD variable to keep them in lockstep.
KC_DB_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in your .env}
KC_HOSTNAME_ADMIN_URL: http://YOUR_DOMAIN/keycloak/
KC_HOSTNAME_URL: http://YOUR_DOMAIN/keycloak/
KC_HOSTNAME_STRICT_BACKCHANNEL: true
KC_HOSTNAME_STRICT_HTTPS: false
KC_HTTP_ENABLED: true
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?set KEYCLOAK_ADMIN_PASSWORD in your .env}
KC_HEALTH_ENABLED: true
KC_METRICS_ENABLED: true
KC_PROXY: edge
@@ -139,7 +142,7 @@ services:
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in your .env}
restart: unless-stopped
networks:
- default
@@ -0,0 +1,7 @@
# Copy this file to .env next to docker-compose.yml and fill in strong
# values. docker compose refuses to start while either is unset.
KEYCLOAK_ADMIN_PASSWORD=
# POSTGRES_PASSWORD is the single PostgreSQL credential: it provisions the
# keycloak database role and Keycloak reuses it to connect (KC_DB_PASSWORD is
# derived from it in docker-compose.yml), so there is one value, not two.
POSTGRES_PASSWORD=
@@ -0,0 +1,42 @@
# Security notes for this recipe
## Secrets
This recipe no longer ships working credential values. Before `docker compose up`:
- Generate a fresh OAuth client secret for the `ohif_viewer` client and use it
to replace the `REPLACE_WITH_A_GENERATED_CLIENT_SECRET` placeholder in BOTH
`config/ohif-keycloak-realm.json` and `config/oauth2-proxy.cfg` (the two
values must match). To generate one after the realm is imported: Keycloak
admin console -> Clients -> ohif_viewer -> Credentials -> Regenerate.
- Copy `.env.example` to `.env` and set `POSTGRES_PASSWORD` and
`KEYCLOAK_ADMIN_PASSWORD` to strong values. `docker compose` refuses to
start while either is unset. `POSTGRES_PASSWORD` is the single PostgreSQL
credential - it both provisions the `keycloak` database role and is what
Keycloak uses to connect (the compose file derives `KC_DB_PASSWORD` from it),
so there is one database password to set, not two.
## Rotate if you deployed from an earlier checkout
Earlier versions of this recipe committed a fixed client secret and default
admin/database passwords to the public repository. A committed secret stays
burned even after this change, so any deployment created from an earlier
checkout must rotate:
- the `ohif_viewer` client secret (Keycloak admin console -> Clients ->
ohif_viewer -> Credentials -> Regenerate, then update
`config/oauth2-proxy.cfg` to match), and
- the Keycloak admin and PostgreSQL passwords. Note that changing
`POSTGRES_PASSWORD` in `.env` alone does not re-password an existing
database: Postgres only applies it when the `postgres_data` volume is first
initialized. To actually rotate it on an existing deployment, either
`ALTER ROLE keycloak WITH PASSWORD ...` inside the running database or
recreate the `postgres_data` volume.
## CORS
The wildcard `Access-Control-Allow-Origin: *` defaults were removed from
`config/nginx.conf`. The viewer is served by the same nginx as the DICOMweb
proxy, so same-origin deployments need no CORS headers. If you host the viewer
on a different origin, use the commented explicit-origin example in the nginx
config - never `*` on an authenticated endpoint that serves PHI.
@@ -50,6 +50,15 @@ http {
gzip_comp_level 9;
etag on;
# Content-Security-Policy: test in Report-Only mode first and watch
# the browser console for violations before switching the header name
# to the enforcing Content-Security-Policy. Note: nginx add_header does
# not merge into a location that sets its own add_header - if you
# promote this to an active header, re-declare it inside every location
# block that already uses add_header (several proxy locations here do)
# or it will silently not apply there.
# add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https: blob:; worker-src 'self' blob:; object-src 'self' blob:; frame-src 'self' blob:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
location /sw.js {
add_header Cache-Control "no-cache";
proxy_cache_bypass $http_pragma;
@@ -105,19 +114,25 @@ http {
expires 0;
add_header Cache-Control private;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept' always;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# CORS: this recipe serves the viewer from this same nginx, so
# browser requests to this endpoint are same-origin and need NO
# CORS headers at all. If you host the viewer on a different
# origin, allow that origin explicitly and only that origin -
# never use '*' on an authenticated endpoint that serves PHI.
# Cross-origin example (response headers plus OPTIONS preflight):
# add_header 'Access-Control-Allow-Origin' 'https://viewer.example.com' always;
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
# add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept' always;
#
# if ($request_method = OPTIONS) {
# add_header 'Access-Control-Allow-Origin' 'https://viewer.example.com';
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
# add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept';
# add_header 'Access-Control-Max-Age' 1728000;
# add_header 'Content-Type' 'text/plain; charset=utf-8';
# add_header 'Content-Length' 0;
# return 204;
# }
proxy_pass http://orthanc:8042/;
}
@@ -137,19 +152,25 @@ http {
expires 0;
add_header Cache-Control private;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept' always;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# CORS: this recipe serves the viewer from this same nginx, so
# browser requests to this endpoint are same-origin and need NO
# CORS headers at all. If you host the viewer on a different
# origin, allow that origin explicitly and only that origin -
# never use '*' on an authenticated endpoint that serves PHI.
# Cross-origin example (response headers plus OPTIONS preflight):
# add_header 'Access-Control-Allow-Origin' 'https://viewer.example.com' always;
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
# add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept' always;
#
# if ($request_method = OPTIONS) {
# add_header 'Access-Control-Allow-Origin' 'https://viewer.example.com';
# add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
# add_header 'Access-Control-Allow-Headers' 'Authorization, Origin, X-Requested-With, Content-Type, Accept';
# add_header 'Access-Control-Max-Age' 1728000;
# add_header 'Content-Type' 'text/plain; charset=utf-8';
# add_header 'Content-Length' 0;
# return 204;
# }
proxy_pass http://orthanc:8042/dicom-web/;
}
@@ -4,7 +4,7 @@ email_domains=["*"]
cookie_secure="false"
cookie_expire="9m30s"
cookie_refresh="5m"
client_secret="2Xtlde7aozdkzzYHdIxQNfPDr0wNPTgg"
client_secret="REPLACE_WITH_A_GENERATED_CLIENT_SECRET"
client_id="ohif_viewer"
redirect_url="http://YOUR_DOMAIN/oauth2/callback"
@@ -686,7 +686,7 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "2Xtlde7aozdkzzYHdIxQNfPDr0wNPTgg",
"secret": "REPLACE_WITH_A_GENERATED_CLIENT_SECRET",
"redirectUris": [
"http://127.0.0.1/oauth2/callback"
],
@@ -51,14 +51,17 @@ services:
KC_DB_URL: 'jdbc:postgresql://postgres:5432/keycloak'
KC_DB_SCHEMA: public
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: password
# Keycloak authenticates to Postgres as the keycloak role provisioned
# below, so this must be the same value as POSTGRES_PASSWORD. Sourced
# from the single POSTGRES_PASSWORD variable to keep them in lockstep.
KC_DB_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in your .env}
KC_HOSTNAME_ADMIN_URL: http://YOUR_DOMAIN/keycloak/
KC_HOSTNAME_URL: http://YOUR_DOMAIN/keycloak/
KC_HOSTNAME_STRICT_BACKCHANNEL: true
KC_HOSTNAME_STRICT_HTTPS: false
KC_HTTP_ENABLED: true
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?set KEYCLOAK_ADMIN_PASSWORD in your .env}
KC_HEALTH_ENABLED: true
KC_METRICS_ENABLED: true
KC_PROXY: edge
@@ -104,7 +107,7 @@ services:
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in your .env}
restart: unless-stopped
networks:
- default
@@ -24,3 +24,12 @@ localhost/pacs -> Orthanc
See [here](../../../docs/docs/deployment/nginx--image-archive.md) for more information about this recipe.
# Security notes
- CORS: earlier versions of this recipe set `Access-Control-Allow-Origin: *`
on the `/pacs/` proxy. That default has been removed. The viewer is served
by the same nginx as the proxy, so same-origin deployments need no CORS
headers. If you host the viewer on a different origin, set that origin
explicitly in `config/nginx.conf` - never `*` on an endpoint that serves
PHI.
@@ -32,6 +32,15 @@ http {
gzip_comp_level 9;
etag on;
# Content-Security-Policy: test in Report-Only mode first and watch the
# browser console for violations before switching the header name to the
# enforcing Content-Security-Policy. Note: nginx add_header does not merge
# into a location that sets its own add_header - if you promote this to an
# active header, re-declare it inside every location block that already
# uses add_header (the /pacs/ proxy here does) or it will silently not
# apply there.
# add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https: blob:; worker-src 'self' blob:; object-src 'self' blob:; frame-src 'self' blob:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
# Reverse Proxy for `orthanc` APIs (including DICOMWeb)
#
@@ -47,17 +56,14 @@ http {
expires 0;
add_header Cache-Control private;
# Add CORS headers
# Note: uncomment the following line to allow all domains to access the Orthanc APIs
# You should actually only allow the domains you trust to access the APIs
# add_header 'Access-Control-Allow-Origin' '*' always;
# CORS: this recipe serves the viewer from this same nginx, so browser
# requests to /pacs/ are same-origin and need NO CORS header at all.
# If you host the viewer on a different origin, allow that origin
# explicitly and only that origin - never use '*' on an endpoint that
# serves PHI. Example:
# add_header 'Access-Control-Allow-Origin' 'https://viewer.example.com' always;
proxy_pass http://orthanc:8042/;
# By default, this endpoint is protected by CORS (cross-origin-resource-sharing)
# You can add headers to allow other domains to request this resource.
# See the "Updating CORS Settings" example below
add_header 'Access-Control-Allow-Origin' '*' always;
}
+1
View File
@@ -0,0 +1 @@
2800f82e3cd02d991146af3f2887fca18c4ab161
@@ -1,6 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<!--
Content-Security-Policy is delivered via hosting headers (netlify.toml
for Netlify deploys; commented add_header examples in the nginx
recipes under platform/app/.recipes), not as a meta tag: a meta tag
cannot express Report-Only mode. See the deployment docs
(docs/deployment) for the policy and the criteria for promoting it
from Report-Only to enforcing.
-->
<meta charset="UTF-8" />
<meta
name="viewport"
@@ -5,6 +5,7 @@ import CallbackPage from '../routes/CallbackPage';
import SignoutCallbackComponent from '../routes/SignoutCallbackComponent';
import LegacyClient from './legacyOIDCClient';
import NextClient from './nextOIDCClient';
import { sanitizeSameOriginRedirect } from './sanitizeRedirect';
function _isAbsoluteUrl(url) {
return url.includes('http://') || url.includes('https://');
@@ -55,7 +56,10 @@ function LogoutComponent(props) {
const location = useLocation();
const query = new URLSearchParams(location.search);
userManager.signoutRedirect({
post_logout_redirect_uri: query.get('redirect_uri'),
post_logout_redirect_uri: sanitizeSameOriginRedirect(
query.get('redirect_uri'),
window.location.origin
),
});
return null;
}
@@ -0,0 +1,40 @@
import { sanitizeSameOriginRedirect } from './sanitizeRedirect';
describe('sanitizeSameOriginRedirect', () => {
const origin = 'https://viewer.example.com';
it('returns the same href back for an absolute same-origin URL', () => {
expect(sanitizeSameOriginRedirect('https://viewer.example.com/worklist?a=1', origin)).toBe(
'https://viewer.example.com/worklist?a=1'
);
});
it('resolves relative paths against the app origin', () => {
expect(sanitizeSameOriginRedirect('/worklist', origin)).toBe(
'https://viewer.example.com/worklist'
);
});
it('rejects an absolute cross-origin URL', () => {
expect(sanitizeSameOriginRedirect('https://evil.example.com/', origin)).toBeUndefined();
});
it('rejects a protocol-relative cross-origin URL', () => {
expect(sanitizeSameOriginRedirect('//evil.example.com/x', origin)).toBeUndefined();
});
it('rejects javascript: scheme values', () => {
expect(sanitizeSameOriginRedirect('javascript:alert(1)', origin)).toBeUndefined();
});
it('rejects null and empty values', () => {
expect(sanitizeSameOriginRedirect(null, origin)).toBeUndefined();
expect(sanitizeSameOriginRedirect('', origin)).toBeUndefined();
});
it('rejects lookalike hosts that merely start with the app host', () => {
expect(
sanitizeSameOriginRedirect('https://viewer.example.com.evil.com/', origin)
).toBeUndefined();
});
});
@@ -0,0 +1,22 @@
/**
* Returns the given redirect target only when it resolves to the provided
* origin; otherwise returns undefined so callers fall back to the configured
* post-logout destination.
*/
export function sanitizeSameOriginRedirect(
value: string | null,
origin: string
): string | undefined {
if (!value) {
return undefined;
}
try {
const url = new URL(value, origin);
if (url.origin !== origin) {
return undefined;
}
return url.href;
} catch {
return undefined;
}
}
+1
View File
@@ -0,0 +1 @@
3.13.0-beta.110-new-one-click