1+ defmodule FuzzyCatalogWeb.OIDCController do
2+ use FuzzyCatalogWeb , :controller
3+ require Logger
4+
5+ alias FuzzyCatalog.Accounts
6+ alias FuzzyCatalog.Accounts.User
7+ alias FuzzyCatalogWeb.UserAuth
8+
9+ def authorize ( conn , _params ) do
10+ config = Application . get_env ( :fuzzy_catalog , :oidc )
11+
12+ Logger . info ( "OIDC authorize initiated with config: #{ inspect ( config , pretty: true ) } " )
13+
14+ case Assent.Strategy.OIDC . authorize_url ( config ) do
15+ { :ok , % { url: url , session_params: session_params } } ->
16+ Logger . info ( "OIDC authorize successful, redirecting to: #{ url } " )
17+ Logger . info ( "Session params to store: #{ inspect ( session_params , pretty: true ) } " )
18+
19+ conn
20+ |> put_session ( :oidc_state , session_params [ "state" ] )
21+ |> put_session ( :oidc_nonce , session_params [ "nonce" ] )
22+ |> put_session ( :oidc_session_params , session_params ) # Store full session params as backup
23+ |> redirect ( external: url )
24+
25+ { :error , error } ->
26+ Logger . error ( "OIDC authorize failed: #{ inspect ( error , pretty: true ) } " )
27+ conn
28+ |> put_flash ( :error , "Authentication service is currently unavailable. Please try again later or contact support." )
29+ |> redirect ( to: ~p" /users/log-in" )
30+ end
31+ end
32+
33+ def callback ( conn , params ) do
34+ config = Application . get_env ( :fuzzy_catalog , :oidc )
35+
36+ # Try to get session params from different sources
37+ state = get_session ( conn , :oidc_state )
38+ nonce = get_session ( conn , :oidc_nonce )
39+ stored_session_params = get_session ( conn , :oidc_session_params )
40+
41+ Logger . info ( "OIDC callback received with params: #{ inspect ( Map . drop ( params , [ "code" ] ) , pretty: true ) } " )
42+ Logger . info ( "Retrieved session state: #{ inspect ( state ) } " )
43+ Logger . info ( "Retrieved session nonce: #{ inspect ( nonce ) } " )
44+ Logger . info ( "Stored session params: #{ inspect ( stored_session_params , pretty: true ) } " )
45+
46+ # Use stored session params if individual values are missing, or fallback to params
47+ session_params = cond do
48+ state && nonce ->
49+ % {
50+ "state" => state ,
51+ "nonce" => nonce
52+ }
53+ stored_session_params ->
54+ # Convert atom keys to string keys if needed
55+ stored_session_params
56+ |> Enum . map ( fn
57+ { k , v } when is_atom ( k ) -> { Atom . to_string ( k ) , v }
58+ { k , v } -> { k , v }
59+ end )
60+ |> Enum . into ( % { } )
61+ params [ "state" ] ->
62+ # Fallback: use state from callback params if session is lost
63+ Logger . warning ( "Using state from callback params as fallback - session may have been lost" )
64+ % {
65+ "state" => params [ "state" ] ,
66+ "nonce" => nil
67+ }
68+ true ->
69+ % { }
70+ end
71+
72+ Logger . info ( "Final session params for callback: #{ inspect ( session_params , pretty: true ) } " )
73+
74+ # Check if we have required state parameter
75+ state_value = session_params [ "state" ]
76+ if is_nil ( state_value ) do
77+ Logger . error ( "OIDC callback missing state parameter - possible session issue" )
78+ conn
79+ |> put_flash ( :error , "Authentication session expired. Please try signing in again." )
80+ |> redirect ( to: ~p" /users/log-in" )
81+ else
82+ # Convert session_params to atom keys for Assent compatibility
83+ session_params_atoms = session_params
84+ |> Enum . map ( fn
85+ { "state" , v } -> { :state , v }
86+ { "nonce" , v } -> { :nonce , v }
87+ { k , v } -> { String . to_atom ( k ) , v }
88+ end )
89+ |> Enum . into ( % { } )
90+
91+ # Merge session_params into config for Assent
92+ config_with_session = Keyword . put ( config , :session_params , session_params_atoms )
93+ Logger . info ( "Config with session params (atom keys): #{ inspect ( config_with_session , pretty: true ) } " )
94+
95+ case Assent.Strategy.OIDC . callback ( config_with_session , params ) do
96+ { :ok , % { user: user_info , token: token } } ->
97+ Logger . info ( "OIDC callback successful for user: #{ inspect ( user_info [ "email" ] ) } " )
98+ handle_successful_auth ( conn , user_info , token )
99+
100+ { :error , error } ->
101+ Logger . error ( "OIDC callback failed: #{ inspect ( error , pretty: true ) } " )
102+
103+ error_message = case error do
104+ % Assent.MissingConfigError { key: :session_params } ->
105+ "Authentication session expired. Please try signing in again."
106+ % { error: "invalid_grant" } ->
107+ "Authentication expired or invalid. Please try signing in again."
108+ % { error: "access_denied" } ->
109+ "Access was denied. Please try again or contact support."
110+ % { error: error_type } when is_binary ( error_type ) ->
111+ "Authentication failed: #{ String . replace ( error_type , "_" , " " ) } . Please try again."
112+ _ ->
113+ "Authentication failed. Please try again or contact support if the problem persists."
114+ end
115+
116+ conn
117+ |> put_flash ( :error , error_message )
118+ |> redirect ( to: ~p" /users/log-in" )
119+ end
120+ end
121+ end
122+
123+ defp handle_successful_auth ( conn , user_info , token ) do
124+ email = user_info [ "email" ]
125+ provider_uid = user_info [ "sub" ]
126+ provider = "oidc"
127+
128+ Logger . info ( "Handling successful OIDC auth for email: #{ email } , provider_uid: #{ provider_uid } " )
129+
130+ case find_or_create_user ( email , provider , provider_uid , token ) do
131+ { :ok , user } ->
132+ Logger . info ( "OIDC user login successful for user ID: #{ user . id } " )
133+ conn
134+ |> delete_session ( :oidc_state )
135+ |> delete_session ( :oidc_nonce )
136+ |> delete_session ( :oidc_session_params )
137+ |> UserAuth . log_in_user ( user )
138+
139+ { :error , :email_taken } ->
140+ Logger . warning ( "OIDC login blocked - email #{ email } already exists with different provider" )
141+ conn
142+ |> put_flash ( :error , "An account with this email already exists. Please log in with your existing account." )
143+ |> redirect ( to: ~p" /users/log-in" )
144+
145+ { :error , changeset } ->
146+ Logger . error ( "OIDC user creation failed: #{ inspect ( changeset . errors , pretty: true ) } " )
147+ conn
148+ |> put_flash ( :error , "Failed to create your account. Please try again or contact support." )
149+ |> redirect ( to: ~p" /users/log-in" )
150+ end
151+ end
152+
153+ defp find_or_create_user ( email , provider , provider_uid , token ) do
154+ Logger . debug ( "Finding or creating user for email: #{ email } , provider: #{ provider } , provider_uid: #{ provider_uid } " )
155+
156+ case Accounts . get_user_by_provider ( provider , provider_uid ) do
157+ % User { } = user ->
158+ Logger . debug ( "Found existing OIDC user: #{ user . id } " )
159+ { :ok , user }
160+
161+ nil ->
162+ Logger . debug ( "No existing OIDC user found, checking for email conflicts" )
163+ case Accounts . get_user_by_email ( email ) do
164+ % User { provider: nil } ->
165+ Logger . debug ( "Email exists with local account, blocking OIDC registration" )
166+ { :error , :email_taken }
167+
168+ % User { } = user ->
169+ Logger . debug ( "Email exists with OIDC account, using existing user: #{ user . id } " )
170+ { :ok , user }
171+
172+ nil ->
173+ Logger . debug ( "Creating new OIDC user" )
174+ create_oidc_user ( email , provider , provider_uid , token )
175+ end
176+ end
177+ end
178+
179+ defp create_oidc_user ( email , provider , provider_uid , token ) do
180+ # Check if this is the first user (should be admin)
181+ is_first_user = Accounts . first_user? ( )
182+ role = if is_first_user , do: "admin" , else: "user"
183+
184+ Logger . info ( "Creating OIDC user - first user: #{ is_first_user } , role: #{ role } " )
185+
186+ attrs = % {
187+ email: email ,
188+ provider: provider ,
189+ provider_uid: provider_uid ,
190+ provider_token: token [ "access_token" ] ,
191+ role: role ,
192+ status: "active" ,
193+ confirmed_at: DateTime . utc_now ( :second )
194+ }
195+
196+ Logger . debug ( "Creating OIDC user with attrs: #{ inspect ( Map . drop ( attrs , [ :provider_token ] ) , pretty: true ) } " )
197+
198+ result = % User { }
199+ |> User . oidc_registration_changeset ( attrs )
200+ |> Accounts . create_user_from_changeset ( )
201+
202+ case result do
203+ { :ok , user } ->
204+ Logger . info ( "Successfully created OIDC user: #{ user . id } " )
205+ { :ok , user }
206+ { :error , changeset } ->
207+ Logger . error ( "Failed to create OIDC user: #{ inspect ( changeset . errors , pretty: true ) } " )
208+ { :error , changeset }
209+ end
210+ end
211+ end
0 commit comments