diff --git a/Dockerfile b/Dockerfile index 96fecbe983..91f7096374 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN \ ;; \ esac -RUN npm install --global pnpm@9 +RUN npm install --global pnpm COPY package.json pnpm-lock.yaml postinstall-win.js ./ RUN CYPRESS_INSTALL_BINARY=0 pnpm install --frozen-lockfile @@ -45,7 +45,7 @@ WORKDIR /app RUN apk add --no-cache tzdata tini && rm -rf /tmp/* -RUN npm install -g pnpm@9 +RUN npm install -g pnpm # copy from build image COPY --from=BUILD_IMAGE /app ./ diff --git a/docs/extending-jellyseerr/forward-auth.md b/docs/extending-jellyseerr/forward-auth.md new file mode 100644 index 0000000000..46eb3f89bb --- /dev/null +++ b/docs/extending-jellyseerr/forward-auth.md @@ -0,0 +1,74 @@ +# Forward auth + +You can use [forward-auth](https://doc.traefik.io/traefik/middlewares/http/forwardauth/) mechanism to log into Jellyseer. + +This works by passing the authenticated user e-mail in `X-Forwarded-User` header by the auth server, therefore enabling single-sign-on (SSO) login. + +:::warning +The user has to exist, it will not be created automatically. +::: + +:::info +If the user has no email set, the username will also work +::: + +## Example with Goauthentik and Traefik + +This example assumes that you have already configured an `application` and `provider` for Jellyseer in Authentik, and added the `provider` to the `outpost`. + +We now have to create a scope mapping that will pass the `X-Forwarded-User` header containing user e-mail to Jellyseerr application. + +### Create scope mapping + +In Authentik go to `Customization > Propperty Mappings` and create `Scope Mapping`: + +* Name: `jellyseerr-forwardauth` +* Scope: `ak_proxy` +* Expression: + +```py +return { + "ak_proxy": { + "user_attributes": { + "additionalHeaders": { + "X-Forwarded-User": request.user.email + } + } + } +} +``` + +### Add the scope mapping to provider scopes + +In authentik go to `Applications > Providers`, edit your `jellyseer` provider: + +* Under `Advanced protocol settings` - `Available scopes` select the `jellyseerr-forwardauth` scope that was created in the previous step and add it to the `Selected scopes` list +* Save the changes by clicking the `Update` button + +### Create the forward-auth middleware in Traefik + +Now you have to define the forward-auth middleware in Traefik and attach it to the `jellyseerr` router. Authentik also requires to set up login page routing so it could redirect properly to Authentik. + +```yml + labels: + - traefik.enable=true + + # Forward auth middleware + - traefik.http.middlewares.auth-authentik.forwardauth.address=http://authentik-server:9000/outpost.goauthentik.io/auth/jellyseerr + - traefik.http.middlewares.auth-authentik.forwardauth.trustForwardHeader=true + - traefik.http.middlewares.auth-authentik.forwardauth.authResponseHeaders=X-Forwarded-User + + # Router for jellyseerr + - traefik.http.routers.jellyseerr.rule=Host(`jellyseerr.domain.com`) + - traefik.http.routers.jellyseerr.entrypoints=websecure + - traefik.http.routers.jellyseerr.middlewares=auth-authentik@docker + # Service for jellyseerr + - traefik.http.services.jellyseerr.loadbalancer.server.port=5055 + - traefik.http.routers.jellyseerr.service=jellyseerr + + # Router for login pages + - traefik.http.routers.jellyseerr-auth.rule=Host(`jellyseerr.domain.co`) && PathPrefix(`/outpost.goauthentik.io/`) + - traefik.http.routers.jellyseerr-auth.entrypoints=websecure + # Service - reference the authentik outpost service name + - traefik.http.routers.jellyseerr-auth.service=authentik@docker +``` diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 00b0959686..f8b79291d3 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -9,6 +9,9 @@ info: - **Cookie Authentication**: A valid sign-in to the `/auth/plex` or `/auth/local` will generate a valid authentication cookie. - **API Key Authentication**: Sign-in is also possible by passing an `X-Api-Key` header along with a valid API Key generated by Jellyseerr. + + Additionally, if forward authentication is enabled, Jellyseerr will authenticate as the email set in the `X-Forwarded-User` header. + tags: - name: public description: Public API endpoints requiring no authentication. @@ -197,6 +200,9 @@ components: trustProxy: type: boolean example: true + enableForwardAuth: + type: boolean + example: true PlexLibrary: type: object properties: @@ -1962,6 +1968,10 @@ components: type: apiKey in: header name: X-Api-Key + forwardAuth: + type: apiKey + in: header + name: X-Forwarded-User paths: /status: @@ -7194,3 +7204,4 @@ paths: security: - cookieAuth: [] - apiKey: [] + - forwardAuth: [] diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index d85f71379e..fae96cd1c0 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -128,6 +128,8 @@ export interface MainSettings { discoverRegion: string; streamingRegion: string; originalLanguage: string; + trustProxy: boolean; + enableForwardAuth: boolean; mediaServerType: number; partialRequestsEnabled: boolean; enableSpecialEpisodes: boolean; @@ -349,6 +351,8 @@ class Settings { discoverRegion: '', streamingRegion: '', originalLanguage: '', + trustProxy: false, + enableForwardAuth: false, mediaServerType: MediaServerType.NOT_CONFIGURED, partialRequestsEnabled: true, enableSpecialEpisodes: false, diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts index 326d460d8b..3f0efc70f4 100644 --- a/server/middleware/auth.ts +++ b/server/middleware/auth.ts @@ -10,9 +10,9 @@ export const checkUser: Middleware = async (req, _res, next) => { const settings = getSettings(); let user: User | undefined | null; - if (req.header('X-API-Key') === settings.main.apiKey) { - const userRepository = getRepository(User); + const userRepository = getRepository(User); + if (req.header('X-API-Key') === settings.main.apiKey) { let userId = 1; // Work on original administrator account // If a User ID is provided, we will act on that user's behalf @@ -21,9 +21,14 @@ export const checkUser: Middleware = async (req, _res, next) => { } user = await userRepository.findOne({ where: { id: userId } }); + } else if ( + settings.main.enableForwardAuth === true && + req.header('X-Forwarded-User') + ) { + user = await userRepository.findOne({ + where: { email: req.header('X-Forwarded-User') }, + }); } else if (req.session?.userId) { - const userRepository = getRepository(User); - user = await userRepository.findOne({ where: { id: req.session.userId }, }); diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx index 5ccfaaa2a3..aa8d89f5a3 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -44,6 +44,12 @@ const messages = defineMessages('components.Settings.SettingsMain', { cacheImages: 'Enable Image Caching', cacheImagesTip: 'Cache externally sourced images (requires a significant amount of disk space)', + trustProxy: 'Enable Proxy Support', + trustProxyTip: + 'Allow Jellyseerr to correctly register client IP addresses behind a proxy', + enableForwardAuth: 'Enable Proxy Forward Authentication', + enableForwardAuthTip: + 'Authenticate as the user specified by the X-Forwarded-User header. Only enable when secured behind a trusted proxy.', validationApplicationTitle: 'You must provide an application title', validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', @@ -134,6 +140,10 @@ const SettingsMain = () => { streamingRegion: data?.streamingRegion || 'US', partialRequestsEnabled: data?.partialRequestsEnabled, enableSpecialEpisodes: data?.enableSpecialEpisodes, + forceIpv4First: data?.forceIpv4First, + dnsServers: data?.dnsServers, + trustProxy: data?.trustProxy, + enableForwardAuth: data?.enableForwardAuth, cacheImages: data?.cacheImages, }} enableReinitialize @@ -155,6 +165,10 @@ const SettingsMain = () => { originalLanguage: values.originalLanguage, partialRequestsEnabled: values.partialRequestsEnabled, enableSpecialEpisodes: values.enableSpecialEpisodes, + forceIpv4First: values.forceIpv4First, + dnsServers: values.dnsServers, + trustProxy: values.trustProxy, + enableForwardAuth: values.enableForwardAuth, cacheImages: values.cacheImages, }), }); @@ -264,6 +278,82 @@ const SettingsMain = () => { )} +