Skip to content

Commit

Permalink
feat: Login with OAuth via OpenID Connect (OIDC) (#3280)
Browse files Browse the repository at this point in the history
* initial oidc implementation

* add dynamic scheme

* e2e test setup

* add caching

* fix

* try this

* add libldap-2.5 to runtime dependencies (#2849)

* New translations en-us.json (Norwegian) (#2851)

* New Crowdin updates (#2855)

* New translations en-us.json (Italian)

* New translations en-us.json (Norwegian)

* New translations en-us.json (Portuguese)

* fix

* remove cache

* cache yarn deps

* cache docker image

* cleanup action

* lint

* fix tests

* remove not needed variables

* run code gen

* fix tests

* add docs

* move code into custom scheme

* remove unneeded type

* fix oidc admin

* add more tests

* add better spacing on login page

* create auth providers

* clean up testing stuff

* type fixes

* add OIDC auth method to postgres enum

* add option to bypass login screen and go directly to iDP

* remove check so we can fallback to another auth method oauth fails

* Add provider name to be shown at the login screen

* add new properties to admin about api

* fix spec

* add a prompt to change auth method when changing password

* Create new auth section. Add more info on auth methods

* update docs

* run ruff

* update docs

* format

* docs gen

* formatting

* initialize logger in class

* mypy type fixes

* docs gen

* add models to get proper fields in docs and fix serialization

* validate id token before using it

* only request a mealie token on initial callback

* remove unused method

* fix unit tests

* docs gen

* check for valid idToken before getting token

* add iss to mealie token

* check to see if we already have a mealie token before getting one

* fix lock file

* update authlib

* update lock file

* add remember me environment variable

* add user group setting to allow only certain groups to log in

---------

Co-authored-by: Carter Mintey <cmintey8@gmail.com>
Co-authored-by: Carter <35710697+cmintey@users.noreply.github.com>
  • Loading branch information
3 people committed Mar 10, 2024
1 parent bea1a59 commit 5f6844e
Show file tree
Hide file tree
Showing 53 changed files with 1,532 additions and 399 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: E2E Tests
on:
pull_request:
branches:
- mealie-next
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./tests/e2e
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
cache-dependency-path: ./tests/e2e/yarn.lock
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Image
uses: docker/build-push-action@v5
with:
file: ./docker/Dockerfile
context: .
push: false
load: true
tags: mealie:e2e
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy E2E Test Environment
run: docker compose up -d
working-directory: ./tests/e2e/docker
- name: Install dependencies
run: npm install -g yarn && yarn
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Check test environment
run: docker ps
- name: Run Playwright tests
run: yarn playwright test
- name: Destroy Test Environment
if: always()
run: docker compose down --volumes
working-directory: ./tests/e2e/docker
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""add OIDC auth method
Revision ID: 09aba125b57a
Revises: 2298bb460ffd
Create Date: 2024-03-10 05:08:32.397027
"""

import sqlalchemy as sa

import mealie.db.migration_types
from alembic import op

# revision identifiers, used by Alembic.
revision = "09aba125b57a"
down_revision = "2298bb460ffd"
branch_labels = None
depends_on = None


def is_postgres():
return op.get_context().dialect.name == "postgresql"


def upgrade():
if is_postgres():
op.execute("ALTER TYPE authmethod ADD VALUE 'OIDC'")


def downgrade():
pass
88 changes: 88 additions & 0 deletions docs/docs/documentation/getting-started/authentication/oidc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# OpenID Connect (OIDC) Authentication

Mealie supports 3rd party authentication via [OpenID Connect (OIDC)](https://openid.net/connect/), an identity layer built on top of OAuth2. OIDC is supported by many identity providers, including:

- [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect)
- [Authelia](https://www.authelia.com/configuration/identity-providers/open-id-connect/)
- [Keycloak](https://www.keycloak.org/docs/latest/securing_apps/#_oidc)
- [Okta](https://www.okta.com/openid-connect/)

## Account Linking

Signing in with OAuth will automatically find your account in Mealie and link to it. If a user does not exist in Mealie, then one will be created (if enabled), but will be unable to log in with any other authentication method. An admin can configure another authentication method for such a user.

## Provider Setup

Before you can start using OIDC Authentication, you must first configure a new client application in your identity provider. Your identity provider must support the OAuth **Authorization Code** flow (with PKCE). The steps will vary by provider, but generally, the steps are as follows.

1. Create a new client application
- The Provider type should be OIDC or OAuth2
- The Grant type should be `Authorization Code`
- The Application type should be `Web`
- The Client type should be `public`

2. Configure redirect URI

The only redirect URI that is needed is `http(s)://DOMAIN:PORT/login`

The redirect URI should include any URL that Mealie is accessible from. Some examples include

http://localhost:9091/login
https://mealie.example.com/login

3. Configure origins

If your identity provider enforces CORS on any endpoints, you will need to specify your Mealie URL as an Allowed Origin.

4. Configure allowed scopes

The scopes required are `openid profile email groups`

## Mealie Setup

Take the client id and your discovery URL and update your environment variables to include the required OIDC variables described in [Installation - Backend Configuration](../installation/backend-config.md#openid-connect-oidc)

## Examples

### Authelia

Follow the instructions in [Authelia's documentation](https://www.authelia.com/configuration/identity-providers/open-id-connect/). Below is an example config

!!! warning

This is only an example and not meant to be an exhaustive configuration. You should read read through the documentation and adjust your configuration as needed.

```yaml
identity_providers:
oidc:
access_token_lifespan: 1h
authorize_code_lifespan: 1m
id_token_lifespan: 1h
refresh_token_lifespan: 90m
enable_client_debug_messages: false
enforce_pkce: public_clients_only
cors:
endpoints:
- authorization
- token
- revocation
- introspection
allowed_origins:
- https://mealie.example.com
allowed_origins_from_client_redirect_uris: false
clients:
- id: mealie
description: Mealie
authorization_policy: one_factor
redirect_uris:
- https://mealie.example.com/login
public: true
grant_types:
- authorization_code
scopes:
- openid
- profile
- groups
- email
- offline_access
```
4 changes: 4 additions & 0 deletions docs/docs/documentation/getting-started/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ docker exec -it mealie-next bash
python /app/mealie/scripts/change_password.py
```

## I can't log in with external auth. How can I change my authentication method?

Follow the [steps above](#how-can-i-change-my-password) for changing your password. You will be prompted if you would like to switch your authentication method back to local auth so you can log in again.

## How do private groups and recipes work?

Managing private groups and recipes can be confusing. The following diagram and notes should help explain how they work to determine if a recipe can be shared publicly.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
| LDAP_NAME_ATTRIBUTE | name | The LDAP attribute that maps to the user's name |
| LDAP_MAIL_ATTRIBUTE | mail | The LDAP attribute that maps to the user's email |

### OpenID Connect (OIDC)

For usage, see [Usage - OpenID Connect](../authentication/oidc.md)

| Variables | Default | Description |
| --- | :--: | --- |
| OIDC_AUTH_ENABLED | False | Enables authentication via OpenID Connect |
| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC |
| OIDC_CONFIGURATION_URL | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration |
| OIDC_CLIENT_ID | None | The client id of your configured client in your provider |
| OIDC_USER_GROUP| None | If specified, this group must be present in the user's group claim in order to authenticate |
| OIDC_ADMIN_GROUP | None | If this group is present in the group claims, the user will be set as an admin |
| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed an you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL |
| OIDC_PROVIDER_NAME | OAuth | The provider name is shown in SSO login button. "Login with <OIDC_PROVIDER_NAME\>" |
| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked |

### Themeing

Setting the following environmental variables will change the theme of the frontend. Note that the themes are the same for all users. This is a break-change when migration from v0.x.x -> 1.x.x.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Permissions and Public Access

Mealie provides various levels of user access and permissions. This includes:
- Authentication and registration ([check out the LDAP guide](./ldap.md) for how to configure access using LDAP)
- Authentication and registration ([LDAP](../authentication/ldap.md) and [OpenID Connect](../authentication/oidc.md) are both supported)
- Customizable user permissions
- Fine-tuned public access for non-users

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/overrides/api.html

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ nav:
- Backend Configuration: "documentation/getting-started/installation/backend-config.md"
- Usage:
- Backup and Restoring: "documentation/getting-started/usage/backups-and-restoring.md"
- LDAP Authentication: "documentation/getting-started/usage/ldap.md"
- Permissions and Public Access: "documentation/getting-started/usage/permissions-and-public-access.md"

- Authentication:
- LDAP: "documentation/getting-started/authentication/ldap.md"
- OpenID Connect: "documentation/getting-started/authentication/oidc.md"

- Community Guides:
- iOS Shortcuts: "documentation/community-guide/ios.md"
Expand Down
2 changes: 1 addition & 1 deletion frontend/composables/use-users/user-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const useUserForm = () => {
type: fieldTypes.SELECT,
hint: i18n.tc("user.authentication-method-hint"),
disableCreate: true,
options: [{ text: "Mealie" }, { text: "LDAP" }],
options: [{ text: "Mealie" }, { text: "LDAP" }, { text: "OIDC" }],
},
{
section: i18n.tc("user.permissions"),
Expand Down
7 changes: 6 additions & 1 deletion frontend/lang/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,10 @@
"ldap-ready-error-text": "Not all LDAP Values are configured. This can be ignored if you are not using LDAP Authentication.",
"ldap-ready-success-text": "Required LDAP variables are all set.",
"build": "Build",
"recipe-scraper-version": "Recipe Scraper Version"
"recipe-scraper-version": "Recipe Scraper Version",
"oidc-ready": "OIDC Ready",
"oidc-ready-error-text": "Not all OIDC Values are configured. This can be ignored if you are not using OIDC Authentication.",
"oidc-ready-success-text": "Required OIDC variables are all set."
},
"shopping-list": {
"all-lists": "All Lists",
Expand Down Expand Up @@ -836,6 +839,8 @@
"link-id": "Link ID",
"link-name": "Link Name",
"login": "Login",
"login-oidc": "Login with",
"or": "or",
"logout": "Logout",
"manage-users": "Manage Users",
"new-password": "New Password",
Expand Down
11 changes: 11 additions & 0 deletions frontend/lib/api/types/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export interface AdminAboutInfo {
version: string;
demoStatus: boolean;
allowSignup: boolean;
enableOidc: boolean;
oidcRedirect: boolean;
oidcProviderName: string;
versionLatest: string;
apiPort: number;
apiDocs: boolean;
Expand All @@ -34,6 +37,9 @@ export interface AppInfo {
demoStatus: boolean;
allowSignup: boolean;
defaultGroupSlug?: string;
enableOidc: boolean;
oidcRedirect: boolean;
oidcProviderName: string;
}
export interface AppStartupInfo {
isFirstLogin: boolean;
Expand Down Expand Up @@ -72,6 +78,7 @@ export interface BackupOptions {
export interface CheckAppConfig {
emailReady: boolean;
ldapReady: boolean;
oidcReady: boolean;
baseUrlSet: boolean;
isUpToDate: boolean;
}
Expand Down Expand Up @@ -218,6 +225,10 @@ export interface NotificationImport {
status: boolean;
exception?: string;
}
export interface OIDCInfo {
configurationUrl?: string;
clientId?: string;
}
export interface RecipeImport {
name: string;
status: boolean;
Expand Down
2 changes: 1 addition & 1 deletion frontend/lib/api/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/* Do not modify it by hand - just update the pydantic models and then re-run the script
*/

export type AuthMethod = "Mealie" | "LDAP";
export type AuthMethod = "Mealie" | "LDAP" | "OIDC";

export interface ChangePassword {
currentPassword: string;
Expand Down
11 changes: 10 additions & 1 deletion frontend/nuxt.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export default {
auth: {
redirect: {
login: "/login",
logout: "/login",
logout: "/login?direct=1",
callback: "/login",
home: "/",
},
Expand All @@ -134,6 +134,7 @@ export default {
path: "/",
},
},
rewriteRedirects: false,
// Options
strategies: {
local: {
Expand All @@ -158,6 +159,14 @@ export default {
user: { url: "api/users/self", method: "get" },
},
},
oidc: {
scheme: "~/schemes/DynamicOpenIDConnectScheme",
resetOnError: true,
clientId: "",
endpoints: {
configuration: "",
}
},
},
},

Expand Down
2 changes: 1 addition & 1 deletion frontend/pages/admin/manage/users/_id.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default defineComponent({
const user = ref<UserOut | null>(null);
const disabledFields = computed(() => {
return user.value?.authMethod === "LDAP" ? ["admin"] : [];
return user.value?.authMethod !== "Mealie" ? ["admin"] : [];
})
const userError = ref(false);
Expand Down
10 changes: 10 additions & 0 deletions frontend/pages/admin/site-settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export default defineComponent({
isSiteSecure: true,
isUpToDate: false,
ldapReady: false,
oidcReady: false,
});
function isLocalHostOrHttps() {
return window.location.hostname === "localhost" || window.location.protocol === "https:";
Expand Down Expand Up @@ -258,6 +259,15 @@ export default defineComponent({
color: appConfig.value.ldapReady ? goodColor : warningColor,
icon: appConfig.value.ldapReady ? goodIcon : warningIcon,
},
{
id: "oidc-ready",
text: i18n.t("settings.oidc-ready"),
status: appConfig.value.oidcReady,
errorText: i18n.t("settings.oidc-ready-error-text"),
successText: i18n.t("settings.oidc-ready-success-text"),
color: appConfig.value.oidcReady ? goodColor : warningColor,
icon: appConfig.value.oidcReady ? goodIcon : warningIcon,
},
];
return data;
});
Expand Down
Loading

0 comments on commit 5f6844e

Please sign in to comment.