Refinex CSV import and PDf export closes #299 and #433 #446

Merged
carla merged 16 commits from feat/299_plz into main 2026-02-24 16:32:32 +01:00
36 changed files with 250 additions and 187 deletions
Showing only changes of commit c8d7dd3e55 - Show all commits

View file

@ -22,8 +22,8 @@ ASSOCIATION_NAME="Sportsclub XYZ"
# These have defaults in docker-compose.prod.yml, only override if needed # These have defaults in docker-compose.prod.yml, only override if needed
# OIDC_CLIENT_ID=mv # OIDC_CLIENT_ID=mv
# OIDC_BASE_URL=http://localhost:8080/auth/v1 # OIDC_BASE_URL=http://localhost:8080/auth/v1
# OIDC_REDIRECT_URI=http://localhost:4001/auth/user/rauthy/callback # OIDC_REDIRECT_URI=http://localhost:4001/auth/user/oidc/callback
# OIDC_CLIENT_SECRET=your-rauthy-client-secret # OIDC_CLIENT_SECRET=your-oidc-client-secret
# Optional: OIDC group → Admin role sync (e.g. Authentik groups from profile scope) # Optional: OIDC group → Admin role sync (e.g. Authentik groups from profile scope)
# If OIDC_ADMIN_GROUP_NAME is set, users in that group get Admin role on registration/sign-in. # If OIDC_ADMIN_GROUP_NAME is set, users in that group get Admin role on registration/sign-in.

View file

@ -983,9 +983,9 @@ defmodule Mv.Accounts.User do
hashed_password_field :hashed_password hashed_password_field :hashed_password
end end
oauth2 :rauthy do oidc :oidc do
client_id fn _, _ -> client_id fn _, _ ->
Application.fetch_env!(:mv, :rauthy)[:client_id] Application.fetch_env!(:mv, :oidc)[:client_id]
end end
# ... other config # ... other config
end end
@ -1866,7 +1866,7 @@ authentication do
hashed_password_field :hashed_password hashed_password_field :hashed_password
end end
oauth2 :rauthy do oidc :oidc do
# OIDC configuration # OIDC configuration
end end
end end
@ -2093,7 +2093,7 @@ plug :protect_from_forgery
```elixir ```elixir
# config/runtime.exs # config/runtime.exs
config :mv, :rauthy, config :mv, :oidc,
client_id: System.get_env("OIDC_CLIENT_ID") || "mv", client_id: System.get_env("OIDC_CLIENT_ID") || "mv",
client_secret: System.get_env("OIDC_CLIENT_SECRET"), client_secret: System.get_env("OIDC_CLIENT_SECRET"),
base_url: System.get_env("OIDC_BASE_URL") base_url: System.get_env("OIDC_BASE_URL")

View file

@ -142,7 +142,7 @@ Mila uses OIDC for Single Sign-On. In development, a local **Rauthy** instance i
3. Login with "admin@localhost" and password from `BOOTSTRAP_ADMIN_PASSWORD_PLAIN` in docker-compose.yml 3. Login with "admin@localhost" and password from `BOOTSTRAP_ADMIN_PASSWORD_PLAIN` in docker-compose.yml
4. add client from the admin panel 4. add client from the admin panel
- Client ID: mv - Client ID: mv
- redirect uris: http://localhost:4000/auth/user/rauthy/callback - redirect uris: http://localhost:4000/auth/user/oidc/callback
- Authorization Flows: authorization_code - Authorization Flows: authorization_code
- allowed origins: http://localhost:4000 - allowed origins: http://localhost:4000
- access/id token algortihm: RS256 (EDDSA did not work for me, found just few infos in the ashauthentication docs) - access/id token algortihm: RS256 (EDDSA did not work for me, found just few infos in the ashauthentication docs)
@ -153,13 +153,13 @@ Now you can log in to Mila via OIDC!
### OIDC with other providers (Authentik, Keycloak, etc.) ### OIDC with other providers (Authentik, Keycloak, etc.)
Mila works with any OIDC-compliant provider. The internal strategy is named `:rauthy`, but this is just a name — it works with any provider. Mila works with any OIDC-compliant provider. The internal strategy is named `:oidc` — it works with any OIDC-compliant provider.
**Important:** The redirect URI must always end with `/auth/user/rauthy/callback`. **Important:** The redirect URI must always end with `/auth/user/oidc/callback`.
Example for Authentik: Example for Authentik:
1. Create an OAuth2/OpenID Provider in Authentik 1. Create an OAuth2/OpenID Provider in Authentik
2. Set the redirect URI to: `https://your-domain.com/auth/user/rauthy/callback` 2. Set the redirect URI to: `https://your-domain.com/auth/user/oidc/callback`
3. Configure environment variables: 3. Configure environment variables:
```bash ```bash
DOMAIN=your-domain.com # or PHX_HOST=your-domain.com DOMAIN=your-domain.com # or PHX_HOST=your-domain.com
@ -168,7 +168,7 @@ Example for Authentik:
OIDC_CLIENT_SECRET=your-client-secret # or use OIDC_CLIENT_SECRET_FILE OIDC_CLIENT_SECRET=your-client-secret # or use OIDC_CLIENT_SECRET_FILE
``` ```
The `OIDC_REDIRECT_URI` is auto-generated as `https://{DOMAIN}/auth/user/rauthy/callback` if not explicitly set. The `OIDC_REDIRECT_URI` is auto-generated as `https://{DOMAIN}/auth/user/oidc/callback` if not explicitly set.
## ⚙️ Configuration ## ⚙️ Configuration
@ -238,7 +238,7 @@ For testing the production Docker build locally:
# OIDC_CLIENT_ID=mv # OIDC_CLIENT_ID=mv
# OIDC_BASE_URL=http://localhost:8080/auth/v1 # OIDC_BASE_URL=http://localhost:8080/auth/v1
# OIDC_CLIENT_SECRET=<from-your-oidc-provider> # OIDC_CLIENT_SECRET=<from-your-oidc-provider>
# OIDC_REDIRECT_URI is auto-generated as https://{DOMAIN}/auth/user/rauthy/callback # OIDC_REDIRECT_URI is auto-generated as https://{DOMAIN}/auth/user/oidc/callback
# Alternative: Use _FILE variables for Docker secrets (takes priority over regular vars): # Alternative: Use _FILE variables for Docker secrets (takes priority over regular vars):
# SECRET_KEY_BASE_FILE=/run/secrets/secret_key_base # SECRET_KEY_BASE_FILE=/run/secrets/secret_key_base

View file

@ -357,4 +357,16 @@
} }
} }
/* ============================================
Collapsed Sidebar: User Menu Dropdown Richtung
============================================ */
/* Bei eingeklappter Sidebar liegt der Avatar-Button am linken Rand.
dropdown-end würde das Menü nach links öffnen (off-screen).
Stattdessen nach rechts öffnen (in den Content-Bereich). */
#app-layout[data-sidebar-expanded="false"] .dropdown.dropdown-top > ul.dropdown-content {
right: auto !important;
left: 0 !important;
}
/* This file is for your main application CSS */ /* This file is for your main application CSS */

View file

@ -86,6 +86,16 @@ Hooks.SidebarState = {
this.setSidebarState(!current) this.setSidebarState(!current)
} }
}, },
updated() {
// LiveView patches data-sidebar-expanded back to the template default ("true")
// on every DOM update. Re-apply the stored state from localStorage after each patch.
const expanded = localStorage.getItem('sidebar-expanded') !== 'false'
const current = this.el.dataset.sidebarExpanded === 'true'
if (current !== expanded) {
this.setSidebarState(expanded)
}
},
setSidebarState(expanded) { setSidebarState(expanded) {
// Convert boolean to string for consistency // Convert boolean to string for consistency
@ -228,6 +238,13 @@ document.addEventListener("DOMContentLoaded", () => {
// Listen for changes to the drawer checkbox // Listen for changes to the drawer checkbox
drawerToggle.addEventListener("change", () => { drawerToggle.addEventListener("change", () => {
// On desktop (lg:drawer-open), the mobile drawer must never open.
// The hamburger label is lg:hidden, but guard here as a safety net
// against any accidental toggles (e.g. from overlapping elements or JS).
if (drawerToggle.checked && window.innerWidth >= 1024) {
drawerToggle.checked = false
return
}
const isOpen = drawerToggle.checked const isOpen = drawerToggle.checked
updateAriaExpanded() updateAriaExpanded()
updateSidebarTabIndex(isOpen) updateSidebarTabIndex(isOpen)

View file

@ -93,11 +93,11 @@ config :mv, :secret_key_base, "ryn7D6ssmIHQFWIks2sFiTGATgwwAR1+3bN8p7fy6qVtB8qnx
# Signing Secret for Authentication # Signing Secret for Authentication
config :mv, :token_signing_secret, "IwUwi65TrEeExwBXXFPGm2I7889NsL" config :mv, :token_signing_secret, "IwUwi65TrEeExwBXXFPGm2I7889NsL"
config :mv, :rauthy, config :mv, :oidc,
client_id: "mv", client_id: "mv",
base_url: "http://localhost:8080/auth/v1", base_url: "http://localhost:8080/auth/v1",
client_secret: System.get_env("OIDC_CLIENT_SECRET"), client_secret: System.get_env("OIDC_CLIENT_SECRET"),
redirect_uri: "http://localhost:4000/auth/user/rauthy/callback" redirect_uri: "http://localhost:4000/auth/user/oidc/callback"
# AshAuthentication development configuration # AshAuthentication development configuration
config :mv, :session_identifier, :jti config :mv, :session_identifier, :jti

View file

@ -129,8 +129,7 @@ if config_env() == :prod do
config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
# OIDC configuration (works with any OIDC provider: Authentik, Rauthy, Keycloak, etc.) # OIDC configuration (works with any OIDC provider: Authentik, Rauthy, Keycloak, etc.)
# Note: The strategy is named :rauthy internally, but works with any OIDC provider. # The redirect_uri callback path is /auth/user/oidc/callback.
# The redirect_uri callback path is always /auth/user/rauthy/callback regardless of provider.
# #
# Supports OIDC_CLIENT_SECRET or OIDC_CLIENT_SECRET_FILE for Docker secrets. # Supports OIDC_CLIENT_SECRET or OIDC_CLIENT_SECRET_FILE for Docker secrets.
# OIDC_CLIENT_SECRET is required only if OIDC is being used (indicated by explicit OIDC env vars). # OIDC_CLIENT_SECRET is required only if OIDC is being used (indicated by explicit OIDC env vars).
@ -150,9 +149,9 @@ if config_env() == :prod do
# Build redirect_uri: use OIDC_REDIRECT_URI if set, otherwise build from host. # Build redirect_uri: use OIDC_REDIRECT_URI if set, otherwise build from host.
# Uses HTTPS since production runs behind TLS termination. # Uses HTTPS since production runs behind TLS termination.
default_redirect_uri = "https://#{host}/auth/user/rauthy/callback" default_redirect_uri = "https://#{host}/auth/user/oidc/callback"
config :mv, :rauthy, config :mv, :oidc,
client_id: oidc_client_id || "mv", client_id: oidc_client_id || "mv",
base_url: oidc_base_url || "http://localhost:8080/auth/v1", base_url: oidc_base_url || "http://localhost:8080/auth/v1",
client_secret: client_secret, client_secret: client_secret,

View file

@ -18,11 +18,11 @@ services:
PHX_HOST: "${PHX_HOST:-localhost}" PHX_HOST: "${PHX_HOST:-localhost}"
PORT: "4001" PORT: "4001"
PHX_SERVER: "true" PHX_SERVER: "true"
# Rauthy OIDC config - use host.docker.internal to reach host services # OIDC config - use host.docker.internal to reach host services
OIDC_CLIENT_ID: "mv" OIDC_CLIENT_ID: "mv"
OIDC_BASE_URL: "http://host.docker.internal:8080/auth/v1" OIDC_BASE_URL: "http://host.docker.internal:8080/auth/v1"
OIDC_CLIENT_SECRET_FILE: "/run/secrets/oidc_client_secret" OIDC_CLIENT_SECRET_FILE: "/run/secrets/oidc_client_secret"
OIDC_REDIRECT_URI: "http://localhost:4001/auth/user/rauthy/callback" OIDC_REDIRECT_URI: "http://localhost:4001/auth/user/oidc/callback"
secrets: secrets:
- db_password - db_password
- secret_key_base - secret_key_base

View file

@ -39,8 +39,8 @@
### Where It Runs ### Where It Runs
1. Registration: register_with_rauthy after_action calls OidcRoleSync. 1. Registration: register_with_oidc after_action calls OidcRoleSync.
2. Sign-in: sign_in_with_rauthy prepare after_action calls OidcRoleSync for each user. 2. Sign-in: sign_in_with_oidc prepare after_action calls OidcRoleSync for each user.
### Internal Action ### Internal Action

View file

@ -886,7 +886,7 @@ just regen-migrations <name>
**Checklist:** **Checklist:**
1. ✅ Rauthy running: `docker compose ps` 1. ✅ Rauthy running: `docker compose ps`
2. ✅ Client created in Rauthy admin panel 2. ✅ Client created in Rauthy admin panel
3. ✅ Redirect URI matches exactly: `http://localhost:4000/auth/user/rauthy/callback` 3. ✅ Redirect URI matches exactly: `http://localhost:4000/auth/user/oidc/callback`
4. ✅ OIDC_CLIENT_SECRET in .env 4. ✅ OIDC_CLIENT_SECRET in .env
5. ✅ App restarted after .env update 5. ✅ App restarted after .env update

View file

@ -501,8 +501,8 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
|--------|-------|---------|------|---------|----------| |--------|-------|---------|------|---------|----------|
| `GET` | `/auth/user/password/sign_in` | Show password login form | 🔓 | - | HTML form | | `GET` | `/auth/user/password/sign_in` | Show password login form | 🔓 | - | HTML form |
| `POST` | `/auth/user/password/sign_in` | Submit password login | 🔓 | `{email, password}` | Redirect + session cookie | | `POST` | `/auth/user/password/sign_in` | Submit password login | 🔓 | `{email, password}` | Redirect + session cookie |
| `GET` | `/auth/user/rauthy` | Initiate OIDC flow | 🔓 | - | Redirect to Rauthy | | `GET` | `/auth/user/oidc` | Initiate OIDC flow | 🔓 | - | Redirect to Rauthy |
| `GET` | `/auth/user/rauthy/callback` | Handle OIDC callback | 🔓 | `{code, state}` | Redirect + session cookie | | `GET` | `/auth/user/oidc/callback` | Handle OIDC callback | 🔓 | `{code, state}` | Redirect + session cookie |
| `POST` | `/auth/user/sign_out` | Sign out user | 🔐 | - | Redirect to login | | `POST` | `/auth/user/sign_out` | Sign out user | 🔐 | - | Redirect to login |
| `GET` | `/auth/link-oidc-account` | OIDC account linking (password verification) | 🔓 | - | LiveView form | ✅ Implemented | | `GET` | `/auth/link-oidc-account` | OIDC account linking (password verification) | 🔓 | - | LiveView form | ✅ Implemented |
| `GET` | `/auth/user/password/reset` | Show password reset form | 🔓 | - | HTML form | | `GET` | `/auth/user/password/reset` | Show password reset form | 🔓 | - | HTML form |
@ -515,9 +515,9 @@ Since this is a **Phoenix LiveView** application with **Ash Framework**, we have
| Resource | Action | Purpose | Auth | Input | Output | | Resource | Action | Purpose | Auth | Input | Output |
|----------|--------|---------|------|-------|--------| |----------|--------|---------|------|-------|--------|
| `User` | `:sign_in_with_password` | Password authentication | 🔓 | `{email, password}` | `{:ok, user}` or `{:error, reason}` | | `User` | `:sign_in_with_password` | Password authentication | 🔓 | `{email, password}` | `{:ok, user}` or `{:error, reason}` |
| `User` | `:sign_in_with_rauthy` | OIDC authentication | 🔓 | `{oidc_id, email, user_info}` | `{:ok, user}` or `{:error, reason}` | | `User` | `:sign_in_with_oidc` | OIDC authentication | 🔓 | `{oidc_id, email, user_info}` | `{:ok, user}` or `{:error, reason}` |
| `User` | `:register_with_password` | Create user with password | 🔓 | `{email, password}` | `{:ok, user}` | | `User` | `:register_with_password` | Create user with password | 🔓 | `{email, password}` | `{:ok, user}` |
| `User` | `:register_with_rauthy` | Create user via OIDC | 🔓 | `{oidc_id, email}` | `{:ok, user}` | | `User` | `:register_with_oidc` | Create user via OIDC | 🔓 | `{oidc_id, email}` | `{:ok, user}` |
| `User` | `:request_password_reset` | Generate reset token | 🔓 | `{email}` | `{:ok, token}` | | `User` | `:request_password_reset` | Generate reset token | 🔓 | `{email}` | `{:ok, token}` |
| `User` | `:reset_password` | Reset password with token | 🔓 | `{token, password}` | `{:ok, user}` | | `User` | `:reset_password` | Reset password with token | 🔓 | `{token, password}` | `{:ok, user}` |
| `Token` | `:revoke` | Revoke authentication token | 🔐 | `{jti}` | `{:ok, token}` | | `Token` | `:revoke` | Revoke authentication token | 🔐 | `{jti}` | `{:ok, token}` |

View file

@ -10,10 +10,10 @@ This feature implements secure account linking between password-based accounts a
#### 1. Security Fix: `lib/accounts/user.ex` #### 1. Security Fix: `lib/accounts/user.ex`
**Change**: The `sign_in_with_rauthy` action now filters by `oidc_id` instead of `email`. **Change**: The `sign_in_with_oidc` action now filters by `oidc_id` instead of `email`.
```elixir ```elixir
read :sign_in_with_rauthy do read :sign_in_with_oidc do
argument :user_info, :map, allow_nil?: false argument :user_info, :map, allow_nil?: false
argument :oauth_tokens, :map, allow_nil?: false argument :oauth_tokens, :map, allow_nil?: false
prepare AshAuthentication.Strategy.OAuth2.SignInPreparation prepare AshAuthentication.Strategy.OAuth2.SignInPreparation

View file

@ -9,7 +9,7 @@ defmodule Mv.Accounts do
## Public API ## Public API
The domain exposes these main actions: The domain exposes these main actions:
- User CRUD: `create_user/1`, `list_users/0`, `update_user/2`, `destroy_user/1` - User CRUD: `create_user/1`, `list_users/0`, `update_user/2`, `destroy_user/1`
- Authentication: `create_register_with_rauthy/1`, `read_sign_in_with_rauthy/1` - Authentication: `create_register_with_oidc/1`, `read_sign_in_with_oidc/1`
""" """
use Ash.Domain, use Ash.Domain,
extensions: [AshAdmin.Domain, AshPhoenix] extensions: [AshAdmin.Domain, AshPhoenix]
@ -24,8 +24,8 @@ defmodule Mv.Accounts do
define :list_users, action: :read define :list_users, action: :read
define :update_user, action: :update_user define :update_user, action: :update_user
define :destroy_user, action: :destroy define :destroy_user, action: :destroy
define :create_register_with_rauthy, action: :register_with_rauthy define :create_register_with_oidc, action: :register_with_oidc
define :read_sign_in_with_rauthy, action: :sign_in_with_rauthy define :read_sign_in_with_oidc, action: :sign_in_with_oidc
end end
resource Mv.Accounts.Token resource Mv.Accounts.Token

View file

@ -28,7 +28,7 @@ defmodule Mv.Accounts.User do
@doc """ @doc """
AshAuthentication specific: Defines the strategies we want to use for authentication. AshAuthentication specific: Defines the strategies we want to use for authentication.
Currently password and SSO with Rauthy as OIDC provider Currently password and SSO via OIDC (supports any provider: Authentik, Rauthy, Keycloak, etc.)
""" """
authentication do authentication do
session_identifier Application.compile_env!(:mv, :session_identifier) session_identifier Application.compile_env!(:mv, :session_identifier)
@ -52,7 +52,7 @@ defmodule Mv.Accounts.User do
end end
strategies do strategies do
oidc :rauthy do oidc :oidc do
client_id Mv.Secrets client_id Mv.Secrets
base_url Mv.Secrets base_url Mv.Secrets
redirect_uri Mv.Secrets redirect_uri Mv.Secrets
@ -88,7 +88,7 @@ defmodule Mv.Accounts.User do
# Always use one of these explicit create actions instead: # Always use one of these explicit create actions instead:
# - :create_user (for manual user creation with optional member link) # - :create_user (for manual user creation with optional member link)
# - :register_with_password (for password-based registration) # - :register_with_password (for password-based registration)
# - :register_with_rauthy (for OIDC-based registration) # - :register_with_oidc (for OIDC-based registration)
defaults [:read] defaults [:read]
destroy :destroy do destroy :destroy do
@ -267,7 +267,7 @@ defmodule Mv.Accounts.User do
prepare AshAuthentication.Preparations.FilterBySubject prepare AshAuthentication.Preparations.FilterBySubject
end end
read :sign_in_with_rauthy do read :sign_in_with_oidc do
# Single record expected; required for AshAuthentication OAuth2 strategy (returns list of 0 or 1). # Single record expected; required for AshAuthentication OAuth2 strategy (returns list of 0 or 1).
get? true get? true
argument :user_info, :map, allow_nil?: false argument :user_info, :map, allow_nil?: false
@ -302,7 +302,7 @@ defmodule Mv.Accounts.User do
end) end)
end end
create :register_with_rauthy do create :register_with_oidc do
argument :user_info, :map, allow_nil?: false argument :user_info, :map, allow_nil?: false
argument :oauth_tokens, :map, allow_nil?: false argument :oauth_tokens, :map, allow_nil?: false
upsert? true upsert? true

View file

@ -52,7 +52,8 @@ defmodule Mv.Membership.CustomField do
use Ash.Resource, use Ash.Resource,
domain: Mv.Membership, domain: Mv.Membership,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer] authorizers: [Ash.Policy.Authorizer],
primary_read_warning?: false
postgres do postgres do
table "custom_fields" table "custom_fields"
@ -60,9 +61,13 @@ defmodule Mv.Membership.CustomField do
end end
actions do actions do
defaults [:read]
default_accept [:name, :value_type, :description, :required, :show_in_overview] default_accept [:name, :value_type, :description, :required, :show_in_overview]
read :read do
primary? true
prepare build(sort: [name: :asc])
end
create :create do create :create do
accept [:name, :value_type, :description, :required, :show_in_overview] accept [:name, :value_type, :description, :required, :show_in_overview]
change Mv.Membership.Changes.GenerateSlug change Mv.Membership.Changes.GenerateSlug

View file

@ -2,7 +2,7 @@ defmodule Mv.OidcRoleSync do
@moduledoc """ @moduledoc """
Syncs user role from OIDC user_info (e.g. groups claim Admin role). Syncs user role from OIDC user_info (e.g. groups claim Admin role).
Used after OIDC registration (register_with_rauthy) and on sign-in so that Used after OIDC registration (register_with_oidc) and on sign-in so that
users in the configured admin group get the Admin role; others get Mitglied. users in the configured admin group get the Admin role; others get Mitglied.
Configure via OIDC_ADMIN_GROUP_NAME and OIDC_GROUPS_CLAIM (see OidcRoleSyncConfig). Configure via OIDC_ADMIN_GROUP_NAME and OIDC_GROUPS_CLAIM (see OidcRoleSyncConfig).

View file

@ -7,7 +7,7 @@ defmodule Mv.Secrets do
particularly for OIDC (Rauthy) authentication. particularly for OIDC (Rauthy) authentication.
## Configuration Source ## Configuration Source
Secrets are read from the `:rauthy` key in the application configuration, Secrets are read from the `:oidc` key in the application configuration,
which is typically set in `config/runtime.exs` from environment variables: which is typically set in `config/runtime.exs` from environment variables:
- `OIDC_CLIENT_ID` - `OIDC_CLIENT_ID`
- `OIDC_CLIENT_SECRET` - `OIDC_CLIENT_SECRET`
@ -21,7 +21,7 @@ defmodule Mv.Secrets do
use AshAuthentication.Secret use AshAuthentication.Secret
def secret_for( def secret_for(
[:authentication, :strategies, :rauthy, :client_id], [:authentication, :strategies, :oidc, :client_id],
Mv.Accounts.User, Mv.Accounts.User,
_opts, _opts,
_meth _meth
@ -30,7 +30,7 @@ defmodule Mv.Secrets do
end end
def secret_for( def secret_for(
[:authentication, :strategies, :rauthy, :redirect_uri], [:authentication, :strategies, :oidc, :redirect_uri],
Mv.Accounts.User, Mv.Accounts.User,
_opts, _opts,
_meth _meth
@ -39,7 +39,7 @@ defmodule Mv.Secrets do
end end
def secret_for( def secret_for(
[:authentication, :strategies, :rauthy, :client_secret], [:authentication, :strategies, :oidc, :client_secret],
Mv.Accounts.User, Mv.Accounts.User,
_opts, _opts,
_meth _meth
@ -48,7 +48,7 @@ defmodule Mv.Secrets do
end end
def secret_for( def secret_for(
[:authentication, :strategies, :rauthy, :base_url], [:authentication, :strategies, :oidc, :base_url],
Mv.Accounts.User, Mv.Accounts.User,
_opts, _opts,
_meth _meth
@ -58,7 +58,7 @@ defmodule Mv.Secrets do
defp get_config(key) do defp get_config(key) do
:mv :mv
|> Application.fetch_env!(:rauthy) |> Application.fetch_env!(:oidc)
|> Keyword.fetch!(key) |> Keyword.fetch!(key)
|> then(&{:ok, &1}) |> then(&{:ok, &1})
end end

View file

@ -54,7 +54,7 @@ defmodule MvWeb.Layouts do
data-sidebar-expanded="true" data-sidebar-expanded="true"
phx-hook="SidebarState" phx-hook="SidebarState"
> >
<input id="mobile-drawer" type="checkbox" class="drawer-toggle" /> <input id="mobile-drawer" type="checkbox" class="drawer-toggle" phx-update="ignore" />
<div class="drawer-content flex flex-col relative z-0"> <div class="drawer-content flex flex-col relative z-0">
<!-- Mobile Header (only visible on mobile) --> <!-- Mobile Header (only visible on mobile) -->

View file

@ -48,8 +48,8 @@ defmodule MvWeb.AuthController do
log_failure_safely(activity, reason) log_failure_safely(activity, reason)
case {activity, reason} do case {activity, reason} do
{{:rauthy, _action}, reason} -> {{:oidc, _action}, reason} ->
handle_rauthy_failure(conn, reason) handle_oidc_failure(conn, reason)
{_, %AshAuthentication.Errors.AuthenticationFailed{caused_by: caused_by}} -> {_, %AshAuthentication.Errors.AuthenticationFailed{caused_by: caused_by}} ->
handle_authentication_failed(conn, caused_by) handle_authentication_failed(conn, caused_by)
@ -61,8 +61,8 @@ defmodule MvWeb.AuthController do
end end
end end
# Log authentication failures safely, avoiding sensitive data for {:rauthy, _} activities # Log authentication failures safely, avoiding sensitive data for {:oidc, _} activities
defp log_failure_safely({:rauthy, _action} = activity, reason) do defp log_failure_safely({:oidc, _action} = activity, reason) do
# For Assent errors, use safe_assent_meta to avoid logging tokens/URLs with query params # For Assent errors, use safe_assent_meta to avoid logging tokens/URLs with query params
case reason do case reason do
%Assent.ServerUnreachableError{} = err -> %Assent.ServerUnreachableError{} = err ->
@ -76,7 +76,7 @@ defmodule MvWeb.AuthController do
Logger.warning(message) Logger.warning(message)
_ -> _ ->
# For other rauthy errors, log only error type, not full details # For other OIDC errors, log only error type, not full details
error_type = get_error_type(reason) error_type = get_error_type(reason)
Logger.warning( Logger.warning(
@ -86,7 +86,7 @@ defmodule MvWeb.AuthController do
end end
defp log_failure_safely(activity, reason) do defp log_failure_safely(activity, reason) do
# For non-rauthy activities, safe to log full reason # For non-OIDC activities, safe to log full reason
Logger.warning( Logger.warning(
"Authentication failure - Activity: #{inspect(activity)}, Reason: #{inspect(reason)}" "Authentication failure - Activity: #{inspect(activity)}, Reason: #{inspect(reason)}"
) )
@ -119,12 +119,12 @@ defmodule MvWeb.AuthController do
if Enum.empty?(parts), do: "", else: " - " <> Enum.join(parts, ", ") if Enum.empty?(parts), do: "", else: " - " <> Enum.join(parts, ", ")
end end
# Handle all Rauthy (OIDC) authentication failures # Handle all OIDC authentication failures
defp handle_rauthy_failure(conn, %Ash.Error.Invalid{errors: errors}) do defp handle_oidc_failure(conn, %Ash.Error.Invalid{errors: errors}) do
handle_oidc_email_collision(conn, errors) handle_oidc_email_collision(conn, errors)
end end
defp handle_rauthy_failure(conn, %AshAuthentication.Errors.AuthenticationFailed{ defp handle_oidc_failure(conn, %AshAuthentication.Errors.AuthenticationFailed{
caused_by: caused_by caused_by: caused_by
}) do }) do
case caused_by do case caused_by do
@ -139,7 +139,7 @@ defmodule MvWeb.AuthController do
end end
# Handle Assent server unreachable errors (network/connectivity issues) # Handle Assent server unreachable errors (network/connectivity issues)
defp handle_rauthy_failure(conn, %Assent.ServerUnreachableError{} = _err) do defp handle_oidc_failure(conn, %Assent.ServerUnreachableError{} = _err) do
# Logging already done safely in failure/3 via log_failure_safely/2 # Logging already done safely in failure/3 via log_failure_safely/2
# No need to log again here to avoid duplicate logs # No need to log again here to avoid duplicate logs
@ -152,7 +152,7 @@ defmodule MvWeb.AuthController do
end end
# Handle Assent invalid response errors (configuration or malformed responses) # Handle Assent invalid response errors (configuration or malformed responses)
defp handle_rauthy_failure(conn, %Assent.InvalidResponseError{} = _err) do defp handle_oidc_failure(conn, %Assent.InvalidResponseError{} = _err) do
# Logging already done safely in failure/3 via log_failure_safely/2 # Logging already done safely in failure/3 via log_failure_safely/2
# No need to log again here to avoid duplicate logs # No need to log again here to avoid duplicate logs
@ -165,7 +165,7 @@ defmodule MvWeb.AuthController do
end end
# Catch-all clause for any other error types # Catch-all clause for any other error types
defp handle_rauthy_failure(conn, _reason) do defp handle_oidc_failure(conn, _reason) do
# Logging already done safely in failure/3 via log_failure_safely/2 # Logging already done safely in failure/3 via log_failure_safely/2
# No need to log again here to avoid duplicate logs # No need to log again here to avoid duplicate logs

View file

@ -84,7 +84,7 @@ defmodule MvWeb.LinkOidcAccountLive do
:info, :info,
dgettext("auth", "Account activated! Redirecting to complete sign-in...") dgettext("auth", "Account activated! Redirecting to complete sign-in...")
) )
|> Phoenix.LiveView.redirect(to: ~p"/auth/user/rauthy") |> Phoenix.LiveView.redirect(to: ~p"/auth/user/oidc")
{:error, error} -> {:error, error} ->
Logger.warning( Logger.warning(
@ -223,7 +223,7 @@ defmodule MvWeb.LinkOidcAccountLive do
"Your OIDC account has been successfully linked! Redirecting to complete sign-in..." "Your OIDC account has been successfully linked! Redirecting to complete sign-in..."
) )
) )
|> Phoenix.LiveView.redirect(to: ~p"/auth/user/rauthy")} |> Phoenix.LiveView.redirect(to: ~p"/auth/user/oidc")}
{:error, error} -> {:error, error} ->
Logger.warning( Logger.warning(

View file

@ -214,47 +214,49 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
</:col> </:col>
<:action :let={cycle}> <:action :let={cycle}>
<div class="flex gap-1"> <div class="flex gap-2">
<%= if @can_update_cycle do %> <%= if @can_update_cycle do %>
<button <div class="join">
:if={cycle.status != :paid} <button
type="button" type="button"
phx-click="mark_cycle_status" phx-click="mark_cycle_status"
phx-value-cycle_id={cycle.id} phx-value-cycle_id={cycle.id}
phx-value-status="paid" phx-value-status="paid"
phx-target={@myself} phx-target={@myself}
class="btn btn-sm btn-success" class={cycle_status_btn_class(cycle.status, :paid)}
title={gettext("Mark as paid")} aria-pressed={cycle.status == :paid}
> title={gettext("Mark as paid")}
<.icon name="hero-check-circle" class="size-4" /> >
{gettext("Paid")} <.icon name="hero-check-circle" class="size-4" />
</button> {gettext("Paid")}
<button </button>
:if={cycle.status != :suspended} <button
type="button" type="button"
phx-click="mark_cycle_status" phx-click="mark_cycle_status"
phx-value-cycle_id={cycle.id} phx-value-cycle_id={cycle.id}
phx-value-status="suspended" phx-value-status="suspended"
phx-target={@myself} phx-target={@myself}
class="btn btn-sm btn-outline btn-warning" class={cycle_status_btn_class(cycle.status, :suspended)}
title={gettext("Mark as suspended")} aria-pressed={cycle.status == :suspended}
> title={gettext("Mark as suspended")}
<.icon name="hero-pause-circle" class="size-4" /> >
{gettext("Suspended")} <.icon name="hero-pause-circle" class="size-4" />
</button> {gettext("Suspended")}
<button </button>
:if={cycle.status != :unpaid} <button
type="button" type="button"
phx-click="mark_cycle_status" phx-click="mark_cycle_status"
phx-value-cycle_id={cycle.id} phx-value-cycle_id={cycle.id}
phx-value-status="unpaid" phx-value-status="unpaid"
phx-target={@myself} phx-target={@myself}
class="btn btn-sm btn-error" class={cycle_status_btn_class(cycle.status, :unpaid)}
title={gettext("Mark as unpaid")} aria-pressed={cycle.status == :unpaid}
> title={gettext("Mark as unpaid")}
<.icon name="hero-x-circle" class="size-4" /> >
{gettext("Unpaid")} <.icon name="hero-x-circle" class="size-4" />
</button> {gettext("Unpaid")}
</button>
</div>
<% end %> <% end %>
<%= if @can_destroy_cycle do %> <%= if @can_destroy_cycle do %>
<button <button
@ -1219,6 +1221,20 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
defp translate_receipt_type("income"), do: gettext("Income") defp translate_receipt_type("income"), do: gettext("Income")
defp translate_receipt_type(other), do: other defp translate_receipt_type(other), do: other
# Returns CSS classes for a cycle status button.
# Active (current) status is highlighted with color and non-interactive;
# inactive buttons are neutral gray. Matches the filter button pattern.
defp cycle_status_btn_class(current_status, btn_status) do
base = "join-item btn btn-sm"
case {current_status == btn_status, btn_status} do
{true, :paid} -> "#{base} btn-success btn-active pointer-events-none"
{true, :suspended} -> "#{base} btn-warning btn-active pointer-events-none"
{true, :unpaid} -> "#{base} btn-error btn-active pointer-events-none"
_ -> base
end
end
# Helper component for section box # Helper component for section box
attr :title, :string, required: true attr :title, :string, required: true
slot :inner_block, required: true slot :inner_block, required: true

View file

@ -59,6 +59,12 @@ msgstr ""
msgid "Sign in" msgid "Sign in"
msgstr "" msgstr ""
## Dynamic string from ash_authentication_phoenix OAuth2 component (strategy_name = "Oidc").
## Not auto-extractable because the msgid is constructed at runtime via string interpolation.
## Generated by Phoenix.Naming.humanize(:oidc) = "Oidc"
msgid "Sign in with Oidc"
msgstr ""
msgid "Signing in ..." msgid "Signing in ..."
msgstr "" msgstr ""

View file

@ -58,6 +58,9 @@ msgstr "Neues Passwort setzen"
msgid "Sign in" msgid "Sign in"
msgstr "Anmelden" msgstr "Anmelden"
msgid "Sign in with Oidc"
msgstr "Single Sign On"
msgid "Signing in ..." msgid "Signing in ..."
msgstr "Anmelden..." msgstr "Anmelden..."

View file

@ -55,6 +55,9 @@ msgstr ""
msgid "Sign in" msgid "Sign in"
msgstr "" msgstr ""
msgid "Sign in with Oidc"
msgstr "Single Sign On"
msgid "Signing in ..." msgid "Signing in ..."
msgstr "" msgstr ""

View file

@ -328,6 +328,7 @@ member_attrs_list = [
city: "Berlin", city: "Berlin",
street: "Kastanienallee", street: "Kastanienallee",
house_number: "8", house_number: "8",
postal_code: "10435",
membership_fee_type_id: Enum.at(all_fee_types, 2).id, membership_fee_type_id: Enum.at(all_fee_types, 2).id,
cycle_status: :mixed cycle_status: :mixed
}, },
@ -338,7 +339,8 @@ member_attrs_list = [
join_date: ~D[2022-11-10], join_date: ~D[2022-11-10],
city: "Berlin", city: "Berlin",
street: "Kastanienallee", street: "Kastanienallee",
house_number: "8" house_number: "8",
postal_code: "10435"
# No membership_fee_type_id - member without fee type # No membership_fee_type_id - member without fee type
} }
] ]

View file

@ -103,13 +103,13 @@ defmodule Mv.Accounts.UserAuthenticationTest do
"preferred_username" => "oidc.user@example.com" "preferred_username" => "oidc.user@example.com"
} }
# Use sign_in_with_rauthy to find user by oidc_id # Use sign_in_with_oidc to find user by oidc_id
# Note: This test will FAIL until we implement the security fix # Note: This test will FAIL until we implement the security fix
# that changes the filter from email to oidc_id # that changes the filter from email to oidc_id
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
result = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -145,11 +145,11 @@ defmodule Mv.Accounts.UserAuthenticationTest do
"preferred_username" => "newuser@example.com" "preferred_username" => "newuser@example.com"
} }
# Should create via register_with_rauthy # Should create via register_with_oidc
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, new_user} = {:ok, new_user} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -196,8 +196,8 @@ defmodule Mv.Accounts.UserAuthenticationTest do
describe "Mixed authentication scenarios" do describe "Mixed authentication scenarios" do
@tag :test_proposal @tag :test_proposal
test "user with oidc_id cannot be found by email-only query in sign_in_with_rauthy" do test "user with oidc_id cannot be found by email-only query in sign_in_with_oidc" do
# This test verifies the security fix: sign_in_with_rauthy should NOT # This test verifies the security fix: sign_in_with_oidc should NOT
# match users by email, only by oidc_id # match users by email, only by oidc_id
_user = _user =
@ -218,7 +218,7 @@ defmodule Mv.Accounts.UserAuthenticationTest do
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
result = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -238,12 +238,12 @@ defmodule Mv.Accounts.UserAuthenticationTest do
:ok :ok
other -> other ->
flunk("sign_in_with_rauthy should not match by email alone, got: #{inspect(other)}") flunk("sign_in_with_oidc should not match by email alone, got: #{inspect(other)}")
end end
end end
@tag :test_proposal @tag :test_proposal
test "password user (oidc_id=nil) is not found by sign_in_with_rauthy" do test "password user (oidc_id=nil) is not found by sign_in_with_oidc" do
# Create a password-only user # Create a password-only user
_user = _user =
create_test_user(%{ create_test_user(%{
@ -262,7 +262,7 @@ defmodule Mv.Accounts.UserAuthenticationTest do
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
result = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -283,7 +283,7 @@ defmodule Mv.Accounts.UserAuthenticationTest do
other -> other ->
flunk( flunk(
"Password-only user should not be found by sign_in_with_rauthy, got: #{inspect(other)}" "Password-only user should not be found by sign_in_with_oidc, got: #{inspect(other)}"
) )
end end
end end

View file

@ -206,7 +206,7 @@ defmodule Mv.Accounts.UserEmailSyncTest do
# Simulate OIDC registration # Simulate OIDC registration
{:ok, user} = {:ok, user} =
Mv.Accounts.User Mv.Accounts.User
|> Ash.Changeset.for_create(:register_with_rauthy, %{ |> Ash.Changeset.for_create(:register_with_oidc, %{
user_info: user_info, user_info: user_info,
oauth_tokens: oauth_tokens oauth_tokens: oauth_tokens
}) })

View file

@ -283,7 +283,7 @@ defmodule Mv.Accounts.UserPoliciesTest do
assert user_with_role.role.name == "Mitglied" assert user_with_role.role.name == "Mitglied"
end end
test "register_with_rauthy works without actor via AshAuthentication bypass" do test "register_with_oidc works without actor via AshAuthentication bypass" do
# Test that AshAuthentication bypass allows OIDC registration without actor # Test that AshAuthentication bypass allows OIDC registration without actor
user_info = %{ user_info = %{
"sub" => "oidc_sub_#{System.unique_integer([:positive])}", "sub" => "oidc_sub_#{System.unique_integer([:positive])}",
@ -294,7 +294,7 @@ defmodule Mv.Accounts.UserPoliciesTest do
changeset = changeset =
Accounts.User Accounts.User
|> Ash.Changeset.for_create(:register_with_rauthy, %{ |> Ash.Changeset.for_create(:register_with_oidc, %{
user_info: user_info, user_info: user_info,
oauth_tokens: oauth_tokens oauth_tokens: oauth_tokens
}) })
@ -306,7 +306,7 @@ defmodule Mv.Accounts.UserPoliciesTest do
assert user.oidc_id == user_info["sub"] assert user.oidc_id == user_info["sub"]
end end
test "sign_in_with_rauthy works without actor via AshAuthentication bypass" do test "sign_in_with_oidc works without actor via AshAuthentication bypass" do
# First create a user with OIDC ID (using system_actor for setup) # First create a user with OIDC ID (using system_actor for setup)
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
@ -319,16 +319,16 @@ defmodule Mv.Accounts.UserPoliciesTest do
{:ok, user} = {:ok, user} =
Accounts.User Accounts.User
|> Ash.Changeset.for_create(:register_with_rauthy, %{ |> Ash.Changeset.for_create(:register_with_oidc, %{
user_info: user_info_create, user_info: user_info_create,
oauth_tokens: oauth_tokens oauth_tokens: oauth_tokens
}) })
|> Ash.create(actor: system_actor) |> Ash.create(actor: system_actor)
# Now test sign_in_with_rauthy without actor (should work via AshAuthentication bypass) # Now test sign_in_with_oidc without actor (should work via AshAuthentication bypass)
query = query =
Accounts.User Accounts.User
|> Ash.Query.for_read(:sign_in_with_rauthy, %{ |> Ash.Query.for_read(:sign_in_with_oidc, %{
user_info: user_info_create, user_info: user_info_create,
oauth_tokens: oauth_tokens oauth_tokens: oauth_tokens
}) })

View file

@ -104,8 +104,8 @@ defmodule Mv.OidcRoleSyncTest do
end end
end end
# B3: Role sync after registration is implemented via after_action in register_with_rauthy. # B3: Role sync after registration is implemented via after_action in register_with_oidc.
# Full integration tests (create_register_with_rauthy + assert role) are skipped: when the # Full integration tests (create_register_with_oidc + assert role) are skipped: when the
# nested Ash.update! runs inside the create's after_action, authorization may evaluate in # nested Ash.update! runs inside the create's after_action, authorization may evaluate in
# the create context so set_role_from_oidc_sync bypass does not apply. Sync logic is covered # the create context so set_role_from_oidc_sync bypass does not apply. Sync logic is covered
# by the apply_admin_role_from_user_info tests above. B4 sign-in sync will also use that. # by the apply_admin_role_from_user_info tests above. B4 sign-in sync will also use that.

View file

@ -254,7 +254,7 @@ defmodule MvWeb.AuthControllerTest do
end end
# OIDC/Rauthy error handling tests # OIDC/Rauthy error handling tests
describe "handle_rauthy_failure/2" do describe "handle_oidc_failure/2" do
test "Assent.ServerUnreachableError redirects to sign-in with error flash", %{ test "Assent.ServerUnreachableError redirects to sign-in with error flash", %{
conn: authenticated_conn conn: authenticated_conn
} do } do
@ -266,7 +266,7 @@ defmodule MvWeb.AuthControllerTest do
reason: %Mint.TransportError{reason: :econnrefused} reason: %Mint.TransportError{reason: :econnrefused}
} }
conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error) conn = MvWeb.AuthController.failure(conn, {:oidc, :callback}, error)
assert redirected_to(conn) == ~p"/sign-in" assert redirected_to(conn) == ~p"/sign-in"
@ -288,7 +288,7 @@ defmodule MvWeb.AuthControllerTest do
} }
} }
conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error) conn = MvWeb.AuthController.failure(conn, {:oidc, :callback}, error)
assert redirected_to(conn) == ~p"/sign-in" assert redirected_to(conn) == ~p"/sign-in"
@ -302,7 +302,7 @@ defmodule MvWeb.AuthControllerTest do
conn = build_unauthenticated_conn(authenticated_conn) conn = build_unauthenticated_conn(authenticated_conn)
unknown_reason = :oops unknown_reason = :oops
conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, unknown_reason) conn = MvWeb.AuthController.failure(conn, {:oidc, :callback}, unknown_reason)
assert redirected_to(conn) == ~p"/sign-in" assert redirected_to(conn) == ~p"/sign-in"
@ -326,7 +326,7 @@ defmodule MvWeb.AuthControllerTest do
log = log =
capture_log(fn -> capture_log(fn ->
MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error) MvWeb.AuthController.failure(conn, {:oidc, :callback}, error)
end) end)
# Should log redacted URL (only scheme and host) # Should log redacted URL (only scheme and host)
@ -352,17 +352,17 @@ defmodule MvWeb.AuthControllerTest do
log = log =
capture_log(fn -> capture_log(fn ->
MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error) MvWeb.AuthController.failure(conn, {:oidc, :callback}, error)
end) end)
# Should log error type but not full error details # Should log error type but not full error details
assert log =~ "Authentication failure" assert log =~ "Authentication failure"
assert log =~ "rauthy" assert log =~ "oidc"
# Should not log full error struct with inspect # Should not log full error struct with inspect
refute log =~ "Assent.InvalidResponseError" refute log =~ "Assent.InvalidResponseError"
end end
test "does not log full reason for unknown rauthy errors", %{ test "does not log full reason for unknown OIDC errors", %{
conn: authenticated_conn conn: authenticated_conn
} do } do
conn = build_unauthenticated_conn(authenticated_conn) conn = build_unauthenticated_conn(authenticated_conn)
@ -375,19 +375,19 @@ defmodule MvWeb.AuthControllerTest do
log = log =
capture_log(fn -> capture_log(fn ->
MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error_with_sensitive_data) MvWeb.AuthController.failure(conn, {:oidc, :callback}, error_with_sensitive_data)
end) end)
# Should log error type but not full error details # Should log error type but not full error details
assert log =~ "Authentication failure" assert log =~ "Authentication failure"
assert log =~ "rauthy" assert log =~ "oidc"
# Should NOT log sensitive data # Should NOT log sensitive data
refute log =~ "secret_token_123" refute log =~ "secret_token_123"
refute log =~ "access_token=abc123" refute log =~ "access_token=abc123"
refute log =~ "callback?access_token" refute log =~ "callback?access_token"
end end
test "logs full reason for non-rauthy activities (password auth)", %{ test "logs full reason for non-OIDC activities (password auth)", %{
conn: authenticated_conn conn: authenticated_conn
} do } do
conn = build_unauthenticated_conn(authenticated_conn) conn = build_unauthenticated_conn(authenticated_conn)
@ -401,7 +401,7 @@ defmodule MvWeb.AuthControllerTest do
MvWeb.AuthController.failure(conn, {:password, :sign_in}, reason) MvWeb.AuthController.failure(conn, {:password, :sign_in}, reason)
end) end)
# For non-rauthy activities, full reason is safe to log # For non-OIDC activities, full reason is safe to log
assert log =~ "Authentication failure" assert log =~ "Authentication failure"
assert log =~ "password" assert log =~ "password"
assert log =~ "AuthenticationFailed" assert log =~ "AuthenticationFailed"

View file

@ -23,7 +23,7 @@ defmodule MvWeb.OidcE2EFlowTest do
# Call register action # Call register action
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -38,7 +38,7 @@ defmodule MvWeb.OidcE2EFlowTest do
# Verify user can be found by oidc_id # Verify user can be found by oidc_id
result = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -74,7 +74,7 @@ defmodule MvWeb.OidcE2EFlowTest do
# Register (upsert) with new email # Register (upsert) with new email
{:ok, updated_user} = {:ok, updated_user} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: updated_user_info, user_info: updated_user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -107,7 +107,7 @@ defmodule MvWeb.OidcE2EFlowTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -146,7 +146,7 @@ defmodule MvWeb.OidcE2EFlowTest do
} }
{:error, %Ash.Error.Invalid{errors: errors}} = {:error, %Ash.Error.Invalid{errors: errors}} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -185,7 +185,7 @@ defmodule MvWeb.OidcE2EFlowTest do
# Step 5: User can now sign in via OIDC # Step 5: User can now sign in via OIDC
result = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -222,7 +222,7 @@ defmodule MvWeb.OidcE2EFlowTest do
# Collision detected # Collision detected
{:error, %Ash.Error.Invalid{}} = {:error, %Ash.Error.Invalid{}} =
Mv.Accounts.create_register_with_rauthy(%{ Mv.Accounts.create_register_with_oidc(%{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
}) })
@ -279,7 +279,7 @@ defmodule MvWeb.OidcE2EFlowTest do
# Collision detected # Collision detected
{:error, %Ash.Error.Invalid{}} = {:error, %Ash.Error.Invalid{}} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -333,7 +333,7 @@ defmodule MvWeb.OidcE2EFlowTest do
# Sign-in should fail (no matching oidc_id) # Sign-in should fail (no matching oidc_id)
result = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -357,7 +357,7 @@ defmodule MvWeb.OidcE2EFlowTest do
# Registration should trigger password requirement # Registration should trigger password requirement
{:error, %Ash.Error.Invalid{errors: errors}} = {:error, %Ash.Error.Invalid{errors: errors}} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -389,7 +389,7 @@ defmodule MvWeb.OidcE2EFlowTest do
# Should trigger hard error (not PasswordVerificationRequired) # Should trigger hard error (not PasswordVerificationRequired)
{:error, %Ash.Error.Invalid{errors: errors}} = {:error, %Ash.Error.Invalid{errors: errors}} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -431,7 +431,7 @@ defmodule MvWeb.OidcE2EFlowTest do
} }
{:error, %Ash.Error.Invalid{errors: errors}} = {:error, %Ash.Error.Invalid{errors: errors}} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -453,7 +453,7 @@ defmodule MvWeb.OidcE2EFlowTest do
} }
{:error, %Ash.Error.Invalid{errors: errors}} = {:error, %Ash.Error.Invalid{errors: errors}} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -472,7 +472,7 @@ defmodule MvWeb.OidcE2EFlowTest do
} }
{:error, %Ash.Error.Invalid{errors: errors}} = {:error, %Ash.Error.Invalid{errors: errors}} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}

View file

@ -28,7 +28,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
@ -70,7 +70,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
@ -135,7 +135,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
@ -190,7 +190,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
@ -234,7 +234,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
@ -271,7 +271,7 @@ defmodule MvWeb.OidcEmailUpdateTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}

View file

@ -24,11 +24,11 @@ defmodule MvWeb.OidcIntegrationTest do
"preferred_username" => "existing@example.com" "preferred_username" => "existing@example.com"
} }
# Test sign_in_with_rauthy action directly # Test sign_in_with_oidc action directly
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
result = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -48,17 +48,17 @@ defmodule MvWeb.OidcIntegrationTest do
assert found_user.oidc_id == "existing_oidc_123" assert found_user.oidc_id == "existing_oidc_123"
end end
test "new OIDC user gets created via register_with_rauthy" do test "new OIDC user gets created via register_with_oidc" do
# Simulate OIDC callback for completely new user # Simulate OIDC callback for completely new user
user_info = %{ user_info = %{
"sub" => "brand_new_oidc_456", "sub" => "brand_new_oidc_456",
"preferred_username" => "newuser@example.com" "preferred_username" => "newuser@example.com"
} }
# Test register_with_rauthy action # Test register_with_oidc action
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
case Mv.Accounts.create_register_with_rauthy( case Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -78,7 +78,7 @@ defmodule MvWeb.OidcIntegrationTest do
describe "OIDC sign-in security tests" do describe "OIDC sign-in security tests" do
@tag :test_proposal @tag :test_proposal
test "sign_in_with_rauthy does NOT match user with only email (no oidc_id)" do test "sign_in_with_oidc does NOT match user with only email (no oidc_id)" do
# SECURITY TEST: Ensure password-only users cannot be accessed via OIDC # SECURITY TEST: Ensure password-only users cannot be accessed via OIDC
# Create a password-only user (no oidc_id) # Create a password-only user (no oidc_id)
_password_user = _password_user =
@ -98,7 +98,7 @@ defmodule MvWeb.OidcIntegrationTest do
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
result = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -123,7 +123,7 @@ defmodule MvWeb.OidcIntegrationTest do
end end
@tag :test_proposal @tag :test_proposal
test "sign_in_with_rauthy only matches when oidc_id matches" do test "sign_in_with_oidc only matches when oidc_id matches" do
# Create user with specific OIDC ID # Create user with specific OIDC ID
user = user =
create_test_user(%{ create_test_user(%{
@ -140,7 +140,7 @@ defmodule MvWeb.OidcIntegrationTest do
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
result = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_oidc(
%{ %{
user_info: correct_user_info, user_info: correct_user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -164,7 +164,7 @@ defmodule MvWeb.OidcIntegrationTest do
} }
result = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_oidc(
%{ %{
user_info: wrong_user_info, user_info: wrong_user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -189,7 +189,7 @@ defmodule MvWeb.OidcIntegrationTest do
end end
@tag :test_proposal @tag :test_proposal
test "sign_in_with_rauthy does not match user with empty string oidc_id" do test "sign_in_with_oidc does not match user with empty string oidc_id" do
# Edge case: empty string should be treated like nil # Edge case: empty string should be treated like nil
_user = _user =
create_test_user(%{ create_test_user(%{
@ -205,7 +205,7 @@ defmodule MvWeb.OidcIntegrationTest do
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
result = result =
Mv.Accounts.read_sign_in_with_rauthy( Mv.Accounts.read_sign_in_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -248,7 +248,7 @@ defmodule MvWeb.OidcIntegrationTest do
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -284,7 +284,7 @@ defmodule MvWeb.OidcIntegrationTest do
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -308,7 +308,7 @@ defmodule MvWeb.OidcIntegrationTest do
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -338,7 +338,7 @@ defmodule MvWeb.OidcIntegrationTest do
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, user} = {:ok, user} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -360,7 +360,7 @@ defmodule MvWeb.OidcIntegrationTest do
system_actor = Mv.Helpers.SystemActor.get_system_actor() system_actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, user} = {:ok, user} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}

View file

@ -32,7 +32,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -70,7 +70,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
} }
{:error, %Ash.Error.Invalid{errors: errors}} = {:error, %Ash.Error.Invalid{errors: errors}} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -167,7 +167,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -201,7 +201,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
# This should work via upsert # This should work via upsert
{:ok, updated_user} = {:ok, updated_user} =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -308,7 +308,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{} oauth_tokens: %{}
@ -380,7 +380,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy(%{ Mv.Accounts.create_register_with_oidc(%{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
}) })
@ -421,7 +421,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
@ -459,7 +459,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
@ -507,7 +507,7 @@ defmodule MvWeb.OidcPasswordLinkingTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy( Mv.Accounts.create_register_with_oidc(
%{ %{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}

View file

@ -45,7 +45,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
# Now OIDC sign-in should work # Now OIDC sign-in should work
result = result =
Mv.Accounts.User Mv.Accounts.User
|> Ash.Query.for_read(:sign_in_with_rauthy, %{ |> Ash.Query.for_read(:sign_in_with_oidc, %{
user_info: %{ user_info: %{
"sub" => "auto_link_oidc_123", "sub" => "auto_link_oidc_123",
"preferred_username" => "invited@example.com" "preferred_username" => "invited@example.com"
@ -79,7 +79,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy(%{ Mv.Accounts.create_register_with_oidc(%{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
}) })
@ -119,7 +119,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy(%{ Mv.Accounts.create_register_with_oidc(%{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
}) })
@ -165,7 +165,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy(%{ Mv.Accounts.create_register_with_oidc(%{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
}) })
@ -200,7 +200,7 @@ defmodule MvWeb.OidcPasswordlessLinkingTest do
} }
result = result =
Mv.Accounts.create_register_with_rauthy(%{ Mv.Accounts.create_register_with_oidc(%{
user_info: user_info, user_info: user_info,
oauth_tokens: %{"access_token" => "test_token"} oauth_tokens: %{"access_token" => "test_token"}
}) })

View file

@ -89,7 +89,7 @@ defmodule MvWeb.ProfileNavigationTest do
user = user =
Mv.Accounts.User Mv.Accounts.User
|> Ash.Changeset.for_create(:register_with_rauthy, %{ |> Ash.Changeset.for_create(:register_with_oidc, %{
user_info: user_info, user_info: user_info,
oauth_tokens: oauth_tokens oauth_tokens: oauth_tokens
}) })
@ -140,7 +140,7 @@ defmodule MvWeb.ProfileNavigationTest do
oidc_user = oidc_user =
Mv.Accounts.User Mv.Accounts.User
|> Ash.Changeset.for_create(:register_with_rauthy, %{ |> Ash.Changeset.for_create(:register_with_oidc, %{
user_info: user_info, user_info: user_info,
oauth_tokens: oauth_tokens oauth_tokens: oauth_tokens
}) })