Skip to content

Commit 830ff8c

Browse files
cycomacheadclaude
authored andcommitted
refactor: simplify developer login gate and trim comments
- Move `developer_login_allowed?` check inside `developer_lookup_or_create` for better encapsulation - Remove verbose security comments and documentation blocks in favor of concise method contracts - Update user-facing alert to a neutral message for all account-linking failures - Add test coverage for developer path bailing out in production environments Co-authored-by: Claude Code <noreply@anthropic.com>
1 parent 8deaf8e commit 830ff8c

3 files changed

Lines changed: 29 additions & 54 deletions

File tree

app/controllers/session_controller.rb

Lines changed: 13 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -65,33 +65,19 @@ def omniauth_callback
6565
expires_at: expires_at
6666
)
6767

68-
# Choose the account-resolution path based on the provider.
69-
#
70-
# The :developer provider is a deliberate masquerade hole that lets a
71-
# developer log in *as* an existing user without going through Canvas. It
72-
# must never be reachable in production, so we gate it twice: the provider
73-
# is only registered in dev/test (config/initializers/omniauth.rb) AND we
74-
# re-check developer_login_allowed? here as defense in depth.
7568
developer = auth.provider == 'developer'
76-
if developer && !developer_login_allowed?
77-
Rails.logger.error('Developer login attempted in a non-permitted environment')
78-
redirect_to root_path, alert: 'Authentication failed. Please try again.'
79-
return
80-
end
81-
8269
user =
8370
if developer
8471
developer_lookup_or_create(user_data, access_token)
8572
else
8673
canvas_lookup_or_create(user_data, access_token)
8774
end
8875

89-
# canvas_lookup_or_create returns nil when it refuses to silently re-key an
90-
# existing account to a new canvas_uid (potential account takeover).
76+
# Either path returns nil when it refuses the login (e.g. canvas_lookup
77+
# declining to re-key an existing account to a new canvas_uid).
9178
if user.nil?
9279
redirect_to root_path,
93-
alert: 'We could not link your Canvas account to an existing ' \
94-
'account with the same email. Please contact an administrator.'
80+
alert: 'We could not link your account. Please contact an administrator.'
9581
return
9682
end
9783

@@ -126,39 +112,21 @@ def ensure_developer_test_enrollments(user)
126112
end
127113
end
128114

129-
# Whether the developer (masquerade) login path may be used.
130-
#
131-
# This mirrors the env guard that registers the :developer OmniAuth provider
132-
# in config/initializers/omniauth.rb -- both must be true for developer login
133-
# to be reachable. Keeping the check here (and not only in the initializer)
134-
# ensures the impersonation path can never run in production even if a
135-
# :developer callback somehow reaches the controller.
136115
def developer_login_allowed?
137-
# Rails.env.local? is true only in development and test, matching the env
138-
# guard around the :developer provider in config/initializers/omniauth.rb.
139116
Rails.env.local?
140117
end
141118

142-
# Canonical production login path.
143-
#
144-
# Canvas is the source of truth for identity, so a user is keyed by their
145-
# canvas_uid:
146-
# * If we recognize the canvas_uid, we refresh the email from Canvas.
147-
# * If we DON'T recognize the canvas_uid but the incoming email already
148-
# belongs to another account, we REFUSE to re-key that account to the new
149-
# canvas_uid and return nil. Silently re-keying would let anyone able to
150-
# create a Canvas account with a victim's email take over the victim's
151-
# account here.
152-
# * Otherwise we create a brand new account.
119+
# Canonical production login path. Canvas owns identity, so users are keyed by
120+
# canvas_uid: we refresh the email on a canvas_uid match and create a new
121+
# account when the canvas_uid is unknown. We return nil (rather than re-keying)
122+
# when a different canvas_uid presents an email that already belongs to an
123+
# account, leaving the existing account untouched.
153124
def canvas_lookup_or_create(user_data, auth_token)
154125
user = User.find_by(canvas_uid: user_data['id'])
155126

156127
if user
157128
user.email = user_data['email']
158129
elsif User.exists?(email: user_data['primary_email'])
159-
# A different canvas_uid is presenting an email we already know. Refuse
160-
# the silent account re-key (potential takeover) and let the caller
161-
# surface an error to the user.
162130
Rails.logger.warn(
163131
"Refusing to link canvas_uid=#{user_data['id']} to existing account " \
164132
"with email=#{user_data['primary_email']}"
@@ -175,15 +143,12 @@ def canvas_lookup_or_create(user_data, auth_token)
175143
persist_login!(user, auth_token)
176144
end
177145

178-
# Developer / test masquerade path.
179-
#
180-
# This INTENTIONALLY preserves the legacy email-first matching so a developer
181-
# can log in *as* an existing user without going through Canvas. It is a
182-
# deliberate impersonation hole and is only ever reached when
183-
# developer_login_allowed? is true (see omniauth_callback and omniauth.rb).
184-
# Do NOT use this logic for the production Canvas path -- see
185-
# canvas_lookup_or_create for why email-first matching is unsafe there.
146+
# Developer / test masquerade path. INTENTIONALLY matches an existing user by
147+
# email so a developer can log in *as* them without Canvas. Gated to dev/test
148+
# only; returns nil otherwise. Do not use this matching for the Canvas path.
186149
def developer_lookup_or_create(user_data, auth_token)
150+
return nil unless developer_login_allowed?
151+
187152
if User.exists?(email: user_data['primary_email'])
188153
user = User.find_by(email: user_data['primary_email'])
189154
user.canvas_uid = user_data['id']

config/initializers/omniauth.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,6 @@ def build_access_token
1818
# URL-encode the scopes defined in CanvasFacade
1919
encoded_scopes = CGI.escape(CanvasFacade::CANVAS_API_SCOPES)
2020

21-
# SECURITY: The :developer provider enables the masquerade login path in
22-
# SessionController (developer_lookup_or_create), which lets a developer log
23-
# in as any existing user by email without Canvas. It must NEVER be available
24-
# in production. This env guard is one of two gates; the controller also
25-
# re-checks developer_login_allowed? before taking that path.
2621
provider :developer, fields: [:email] if Rails.env.development? || Rails.env.test?
2722

2823
provider :canvas,

spec/controllers/session_controller_spec.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858

5959
expect(existing_user.reload.canvas_uid).to eq('old_uid')
6060
expect(response).to redirect_to(root_path)
61-
expect(flash[:alert]).to include('could not link your Canvas account')
61+
expect(flash[:alert]).to include('could not link your account')
6262
end
6363
end
6464

@@ -162,6 +162,21 @@
162162
expect(user.lms_credentials.first.token).to eq('new-token')
163163
end
164164
end
165+
166+
context 'when developer login is not permitted' do
167+
before do
168+
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
169+
end
170+
171+
it 'returns nil without creating a user' do
172+
result = nil
173+
expect do
174+
result = controller.send(:developer_lookup_or_create, user_data, mock_token)
175+
end.not_to change(User, :count)
176+
177+
expect(result).to be_nil
178+
end
179+
end
165180
end
166181

167182
describe '#developer_login_allowed?' do

0 commit comments

Comments
 (0)