36 changed files with 250 additions and 187 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}` |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) -->
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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..."
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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: %{}
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
|
||||||
|
|
@ -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: %{}
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue