From d441009c8a43ef957a16dbdad97e4d4d5420524f Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 4 Feb 2026 18:03:02 +0100 Subject: [PATCH] Refactor: remove debug instrumentation from OidcRoleSync Drop temporary logging used to diagnose OIDC groups sync in dev. --- lib/mv/oidc_role_sync.ex | 88 +++++++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/lib/mv/oidc_role_sync.ex b/lib/mv/oidc_role_sync.ex index d6b608f..369b2b4 100644 --- a/lib/mv/oidc_role_sync.ex +++ b/lib/mv/oidc_role_sync.ex @@ -5,31 +5,29 @@ defmodule Mv.OidcRoleSync do Used after OIDC registration (register_with_rauthy) and on sign-in so that 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). + + Groups are read from user_info (ID token claims) first; if missing or empty, + the access_token from oauth_tokens is decoded as JWT and the groups claim is + read from there (e.g. Rauthy puts groups in the access token when scope + includes "groups"). """ alias Mv.Accounts.User alias Mv.Authorization.Role alias Mv.OidcRoleSyncConfig @doc """ - Applies Admin or Mitglied role to the user based on OIDC user_info (groups claim). + Applies Admin or Mitglied role to the user based on OIDC groups claim. - If OIDC_ADMIN_GROUP_NAME is not configured: no-op, returns :ok without changing the user. - - If user_info contains the configured admin group (under OIDC_GROUPS_CLAIM): assigns Admin role. + - If groups (from user_info or access_token) contain the configured admin group: assigns Admin role. - Otherwise: assigns Mitglied role (downgrade if user was Admin). - user_info is a map (e.g. from JWT claims) and may use string keys. Groups can be - a list of strings or a single string. - - ## Examples - - user_info = %{"groups" => ["mila-admin"]} - OidcRoleSync.apply_admin_role_from_user_info(user, user_info) - - user_info = %{"ak_groups" => ["other"]} # with OIDC_GROUPS_CLAIM=ak_groups - OidcRoleSync.apply_admin_role_from_user_info(user, user_info) + user_info is a map (e.g. from ID token claims); oauth_tokens is optional and may + contain "access_token" (JWT) from which the groups claim is read when not in user_info. """ - @spec apply_admin_role_from_user_info(User.t(), map()) :: :ok - def apply_admin_role_from_user_info(user, user_info) when is_map(user_info) do + @spec apply_admin_role_from_user_info(User.t(), map(), map() | nil) :: :ok + def apply_admin_role_from_user_info(user, user_info, oauth_tokens \\ nil) + when is_map(user_info) do admin_group = OidcRoleSyncConfig.oidc_admin_group_name() if is_nil(admin_group) or admin_group == "" do @@ -37,20 +35,72 @@ defmodule Mv.OidcRoleSync do else claim = OidcRoleSyncConfig.oidc_groups_claim() groups = groups_from_user_info(user_info, claim) + + groups = + if Enum.empty?(groups), do: groups_from_access_token(oauth_tokens, claim), else: groups + target_role = if admin_group in groups, do: :admin, else: :mitglied set_user_role(user, target_role) end end defp groups_from_user_info(user_info, claim) do - case user_info[claim] do - nil -> [] - list when is_list(list) -> Enum.map(list, &to_string/1) - single when is_binary(single) -> [single] - _ -> [] + value = user_info[claim] || user_info[String.to_existing_atom(claim)] + normalize_groups(value) + rescue + ArgumentError -> normalize_groups(user_info[claim]) + end + + defp groups_from_access_token(nil, _claim), do: [] + defp groups_from_access_token(oauth_tokens, _claim) when not is_map(oauth_tokens), do: [] + + defp groups_from_access_token(oauth_tokens, claim) do + access_token = oauth_tokens["access_token"] || oauth_tokens[:access_token] + + if is_binary(access_token) do + case peek_jwt_claims(access_token) do + {:ok, claims} -> + value = claims[claim] || safe_get_atom(claims, claim) + normalize_groups(value) + + _ -> + [] + end + else + [] end end + defp safe_get_atom(map, key) when is_binary(key) do + try do + Map.get(map, String.to_existing_atom(key)) + rescue + ArgumentError -> nil + end + end + + defp safe_get_atom(_map, _key), do: nil + + defp peek_jwt_claims(token) do + parts = String.split(token, ".") + + if length(parts) == 3 do + [_h, payload_b64, _sig] = parts + + case Base.url_decode64(payload_b64, padding: false) do + {:ok, payload} -> Jason.decode(payload) + _ -> :error + end + else + :error + end + end + + defp normalize_groups(nil), do: [] + defp normalize_groups(list) when is_list(list), do: Enum.map(list, &to_string/1) + defp normalize_groups(single) when is_binary(single), do: [single] + defp normalize_groups(_), do: [] + defp set_user_role(user, :admin) do case Role.get_admin_role() do {:ok, %Role{} = role} ->