Previously, we'd rely on defaults for configuring user token authentication. With these changes, we explicitly require :session_identifier and :require_token_presence_for_authentication to be configured in the application environment to make sure the system is configured the way it should be.
168 lines
4.9 KiB
Elixir
168 lines
4.9 KiB
Elixir
defmodule Mv.Accounts.User do
|
|
@moduledoc """
|
|
The ressource for keeping user-specific data related to the login process. It is used by AshAuthentication to handle the Authentication strategies like SSO.
|
|
"""
|
|
use Ash.Resource,
|
|
domain: Mv.Accounts,
|
|
data_layer: AshPostgres.DataLayer,
|
|
extensions: [AshAuthentication]
|
|
|
|
# authorizers: [Ash.Policy.Authorizer]
|
|
|
|
postgres do
|
|
table "users"
|
|
repo Mv.Repo
|
|
end
|
|
|
|
@doc """
|
|
AshAuthentication specific: Defines the strategies we want to use for authentication.
|
|
Currently password and SSO with Rauthy as OIDC provider
|
|
"""
|
|
authentication do
|
|
session_identifier Application.compile_env!(:mv, :session_identifier)
|
|
|
|
tokens do
|
|
enabled? true
|
|
token_resource Mv.Accounts.Token
|
|
|
|
require_token_presence_for_authentication? Application.compile_env!(
|
|
:mv,
|
|
:require_token_presence_for_authentication
|
|
)
|
|
|
|
store_all_tokens? true
|
|
|
|
# signing_algorithm "EdDSA" -> https://git.local-it.org/local-it/mitgliederverwaltung/issues/87
|
|
|
|
signing_secret fn _, _ ->
|
|
{:ok, Application.get_env(:mv, :token_signing_secret)}
|
|
end
|
|
end
|
|
|
|
strategies do
|
|
oidc :rauthy do
|
|
client_id Mv.Secrets
|
|
base_url Mv.Secrets
|
|
redirect_uri Mv.Secrets
|
|
client_secret Mv.Secrets
|
|
auth_method :client_secret_jwt
|
|
code_verifier true
|
|
|
|
# id_token_signed_response_alg "EdDSA" #-> https://git.local-it.org/local-it/mitgliederverwaltung/issues/87
|
|
end
|
|
|
|
password :password do
|
|
identity_field :email
|
|
hash_provider AshAuthentication.BcryptProvider
|
|
confirmation_required? false
|
|
end
|
|
end
|
|
end
|
|
|
|
actions do
|
|
defaults [:read, :create, :destroy, :update]
|
|
|
|
create :create_user do
|
|
accept [:email]
|
|
upsert? true
|
|
end
|
|
|
|
update :update_user do
|
|
accept [:email]
|
|
end
|
|
|
|
# Admin action for direct password changes in admin panel
|
|
# Uses the official Ash Authentication HashPasswordChange with correct context
|
|
update :admin_set_password do
|
|
accept [:email]
|
|
argument :password, :string, allow_nil?: false, sensitive?: true
|
|
|
|
# Set the strategy context that HashPasswordChange expects
|
|
change set_context(%{strategy_name: :password})
|
|
|
|
# Use the official Ash Authentication password change
|
|
change AshAuthentication.Strategy.Password.HashPasswordChange
|
|
end
|
|
|
|
read :get_by_subject do
|
|
description "Get a user by the subject claim in a JWT"
|
|
argument :subject, :string, allow_nil?: false
|
|
get? true
|
|
prepare AshAuthentication.Preparations.FilterBySubject
|
|
end
|
|
|
|
read :sign_in_with_rauthy do
|
|
argument :user_info, :map, allow_nil?: false
|
|
argument :oauth_tokens, :map, allow_nil?: false
|
|
prepare AshAuthentication.Strategy.OAuth2.SignInPreparation
|
|
|
|
filter expr(email == get_path(^arg(:user_info), [:preferred_username]))
|
|
end
|
|
|
|
create :register_with_rauthy do
|
|
argument :user_info, :map, allow_nil?: false
|
|
argument :oauth_tokens, :map, allow_nil?: false
|
|
upsert? true
|
|
upsert_identity :unique_oidc_id
|
|
|
|
validate &__MODULE__.validate_oidc_id_present/2
|
|
|
|
change AshAuthentication.GenerateTokenChange
|
|
|
|
change fn changeset, _ctx ->
|
|
user_info = Ash.Changeset.get_argument(changeset, :user_info)
|
|
|
|
changeset
|
|
|> Ash.Changeset.change_attribute(:email, user_info["preferred_username"])
|
|
|> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"] || user_info["id"])
|
|
end
|
|
end
|
|
end
|
|
|
|
# Global validations - applied to all relevant actions
|
|
validations do
|
|
# Password strength policy: minimum 8 characters for all password-related actions
|
|
validate string_length(:password, min: 8) do
|
|
where action_is([:register_with_password, :admin_set_password])
|
|
end
|
|
end
|
|
|
|
def validate_oidc_id_present(changeset, _context) do
|
|
user_info = Ash.Changeset.get_argument(changeset, :user_info) || %{}
|
|
|
|
if is_binary(user_info["sub"]) or is_binary(user_info["id"]) do
|
|
:ok
|
|
else
|
|
{:error, [user_info: "OIDC user_info must contain a non-empty 'sub' or 'id' field"]}
|
|
end
|
|
end
|
|
|
|
attributes do
|
|
uuid_primary_key :id
|
|
|
|
attribute :email, :ci_string, allow_nil?: false, public?: true
|
|
attribute :hashed_password, :string, sensitive?: true, allow_nil?: true
|
|
attribute :oidc_id, :string, allow_nil?: true
|
|
end
|
|
|
|
relationships do
|
|
belongs_to :member, Mv.Membership.Member
|
|
end
|
|
|
|
identities do
|
|
identity :unique_email, [:email]
|
|
identity :unique_oidc_id, [:oidc_id]
|
|
end
|
|
|
|
# You can customize this if you wish, but this is a safe default that
|
|
# only allows user data to be interacted with via AshAuthentication.
|
|
# policies do
|
|
# bypass AshAuthentication.Checks.AshAuthenticationInteraction do
|
|
# authorize_if(always())
|
|
# end
|
|
|
|
# policy always() do
|
|
# forbid_if(always())
|
|
# end
|
|
# end
|
|
end
|