Skip to content
Closed
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
68 changes: 68 additions & 0 deletions docs/extending-jellyseerr/forward-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 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.

> The user has to exist, it will not be created automatically.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably better to use a warning instead of a citation.

Suggested change
> The user has to exist, it will not be created automatically.
:::warning
The user has to exist, it will not be created automatically.
:::


## 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
```
9 changes: 9 additions & 0 deletions overseerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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 Overseerr.
- **ForwardAuth Authentication**: Sign-in is also possible by passing an `X-Forwarded-User` header containing user's e-mail.
tags:
- name: public
description: Public API endpoints requiring no authentication.
Expand Down Expand Up @@ -186,6 +187,9 @@ components:
defaultPermissions:
type: number
example: 32
enableForwardAuth:
type: boolean
example: true
Comment on lines +190 to +192

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You wrote the setting here, but it is not available anywhere.

You should add this as a general setting, by adding a new properties in the MainSettings type in server/lib/settings.ts and adding a new input in the settings page in src/components/Settings/SettingsMain/index.tsx. You should also check that this setting is enabled before allowing login with forwardAuth.

Or did you intend to not make a setting and set it enabled by default?

PlexLibrary:
type: object
properties:
Expand Down Expand Up @@ -1939,6 +1943,10 @@ components:
type: apiKey
in: header
name: X-Api-Key
forwardAuth:
type: apiKey
in: header
name: X-Forwarded-User

paths:
/status:
Expand Down Expand Up @@ -6939,3 +6947,4 @@ paths:
security:
- cookieAuth: []
- apiKey: []
- forwardAuth: []

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

26 changes: 25 additions & 1 deletion server/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ export const checkUser: Middleware = async (req, _res, next) => {
}

user = await userRepository.findOne({ where: { id: userId } });
} else if (req.header('x-forwarded-user')) {
const userRepository = getRepository(User);
user = await userRepository.findOne({
where: { email: req.header('x-forwarded-user') },
});

if (user) {
req.user = user;
}

req.locale = user?.settings?.locale
? user.settings.locale
: settings.main.locale;
Comment on lines +30 to +36

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not necessary, it's a duplicate from below.

} else if (req.session?.userId) {
const userRepository = getRepository(User);

Expand All @@ -44,7 +57,18 @@ export const isAuthenticated = (
permissions?: Permission | Permission[],
options?: PermissionCheckOptions
): Middleware => {
const authMiddleware: Middleware = (req, res, next) => {
const authMiddleware: Middleware = async (req, res, next) => {
if (req.header('x-forwarded-user')) {
const userRepository = getRepository(User);
const user = await userRepository.findOne({
where: { email: req.header('x-forwarded-user') },
});

if (user) {
req.user = user;
}
}

if (!req.user || !req.user.hasPermission(permissions ?? 0, options)) {
res.status(403).json({
status: 403,
Expand Down
6 changes: 2 additions & 4 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { UserContext } from '@app/context/UserContext';
import type { User } from '@app/hooks/useUser';
import '@app/styles/globals.css';
import '@app/utils/fetchOverride';
import { getRequestHeaders } from '@app/utils/localRequestHelper';
import { polyfillIntl } from '@app/utils/polyfillIntl';
import { MediaServerType } from '@server/constants/server';
import type { PublicSettingsResponse } from '@server/interfaces/api/settingsInterfaces';
Expand Down Expand Up @@ -227,10 +228,7 @@ CoreApp.getInitialProps = async (initialProps) => {
const res = await fetch(
`http://localhost:${process.env.PORT || 5055}/api/v1/auth/me`,
{
headers:
ctx.req && ctx.req.headers.cookie
? { cookie: ctx.req.headers.cookie }
: undefined,
headers: getRequestHeaders(ctx),
}
);
if (!res.ok) throw new Error();
Expand Down
5 changes: 2 additions & 3 deletions src/pages/collection/[collectionId]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import CollectionDetails from '@app/components/CollectionDetails';
import { getRequestHeaders } from '@app/utils/localRequestHelper';
import type { Collection } from '@server/models/Collection';
import type { GetServerSideProps, NextPage } from 'next';

Expand All @@ -18,9 +19,7 @@ export const getServerSideProps: GetServerSideProps<
ctx.query.collectionId
}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }
: undefined,
headers: getRequestHeaders(ctx),
}
);
if (!res.ok) throw new Error();
Expand Down
5 changes: 2 additions & 3 deletions src/pages/movie/[movieId]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import MovieDetails from '@app/components/MovieDetails';
import { getRequestHeaders } from '@app/utils/localRequestHelper';
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
import type { GetServerSideProps, NextPage } from 'next';

Expand All @@ -18,9 +19,7 @@ export const getServerSideProps: GetServerSideProps<MoviePageProps> = async (
ctx.query.movieId
}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }
: undefined,
headers: getRequestHeaders(ctx),
}
);
if (!res.ok) throw new Error();
Expand Down
5 changes: 2 additions & 3 deletions src/pages/tv/[tvId]/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import TvDetails from '@app/components/TvDetails';
import { getRequestHeaders } from '@app/utils/localRequestHelper';
import type { TvDetails as TvDetailsType } from '@server/models/Tv';
import type { GetServerSideProps, NextPage } from 'next';

Expand All @@ -16,9 +17,7 @@ export const getServerSideProps: GetServerSideProps<TvPageProps> = async (
const res = await fetch(
`http://localhost:${process.env.PORT || 5055}/api/v1/tv/${ctx.query.tvId}`,
{
headers: ctx.req?.headers?.cookie
? { cookie: ctx.req.headers.cookie }
: undefined,
headers: getRequestHeaders(ctx),
}
);
if (!res.ok) throw new Error();
Expand Down
18 changes: 18 additions & 0 deletions src/utils/localRequestHelper.ts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdyt about renaming this file to something like serverSidePropsHelpers?

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { NextPageContext } from 'next/dist/shared/lib/utils';
import type { GetServerSidePropsContext, PreviewData } from 'next/types';
import type { ParsedUrlQuery } from 'querystring';

export const getRequestHeaders = (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this function be called something like getAuthHeaders instead of getRequestHeaders?

ctx: NextPageContext | GetServerSidePropsContext<ParsedUrlQuery, PreviewData>
) => {
return ctx.req && ctx.req.headers
? {
...(ctx.req.headers.cookie && {
cookie: ctx.req.headers.cookie,
}),
...(ctx.req.headers['x-forwarded-user'] && {
'x-forwarded-user': ctx.req.headers['x-forwarded-user'] as string,
}),
}
: undefined;
};