Skip to content

Commit e3d399d

Browse files
authored
RUBY-1728 Indicate which auth mechanism was used when auth fails (#1274)
1 parent 7fce62b commit e3d399d

File tree

14 files changed

+306
-68
lines changed

14 files changed

+306
-68
lines changed

lib/mongo/auth.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,23 @@ class Unauthorized < RuntimeError
102102
# Mongo::Auth::Unauthorized.new(user)
103103
#
104104
# @param [ Mongo::Auth::User ] user The unauthorized user.
105+
# @param [ String ] used_mechanism Auth mechanism actually used for
106+
# authentication. This is a full string like SCRAM-SHA-256.
105107
#
106108
# @since 2.0.0
107-
def initialize(user)
108-
super("User #{user.name} is not authorized to access #{user.database}.")
109+
def initialize(user, used_mechanism = nil)
110+
specified_mechanism = if user.mechanism
111+
" (mechanism: #{user.mechanism})"
112+
else
113+
''
114+
end
115+
used_mechanism = if used_mechanism
116+
" (used mechanism: #{used_mechanism})"
117+
else
118+
''
119+
end
120+
msg = "User #{user.name}#{specified_mechanism} is not authorized to access #{user.database}#{used_mechanism}"
121+
super(msg)
109122
end
110123
end
111124
end

lib/mongo/auth/cr/conversation.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def finalize(reply, connection = nil)
9292
end
9393

9494
# Start the CR conversation. This returns the first message that
95-
# needs to be send to the server.
95+
# needs to be sent to the server.
9696
#
9797
# @example Start the conversation.
9898
# conversation.start
@@ -130,7 +130,9 @@ def initialize(user)
130130
private
131131

132132
def validate!(reply)
133-
raise Unauthorized.new(user) if reply.documents[0][Operation::Result::OK] != 1
133+
if reply.documents[0][Operation::Result::OK] != 1
134+
raise Unauthorized.new(user, MECHANISM)
135+
end
134136
@nonce = reply.documents[0][Auth::NONCE]
135137
@reply = reply
136138
end

lib/mongo/auth/ldap/conversation.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def finalize(reply)
5151
end
5252

5353
# Start the PLAIN conversation. This returns the first message that
54-
# needs to be send to the server.
54+
# needs to be sent to the server.
5555
#
5656
# @example Start the conversation.
5757
# conversation.start
@@ -97,7 +97,9 @@ def payload
9797
end
9898

9999
def validate!(reply)
100-
raise Unauthorized.new(user) if reply.documents[0][Operation::Result::OK] != 1
100+
if reply.documents[0][Operation::Result::OK] != 1
101+
raise Unauthorized.new(user, MECHANISM)
102+
end
101103
@reply = reply
102104
end
103105
end

lib/mongo/auth/scram.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ class SCRAM
3232
# @since 2.6.0
3333
SCRAM_SHA_256_MECHANISM = 'SCRAM-SHA-256'.freeze
3434

35-
3635
# Map the user-specified authentication mechanism to the proper names of the mechanisms.
3736
#
3837
# @since 2.6.0
@@ -62,16 +61,13 @@ def initialize(user)
6261
# user.login(connection)
6362
#
6463
# @param [ Mongo::Connection ] connection The connection to log into.
65-
# on.
66-
# @param [ String ] mechanism The auth mechanism to use (either 'SCRAM-SHA-1' or
67-
# 'SCRAM-SHA-256');
6864
#
6965
# @return [ Protocol::Message ] The authentication response.
7066
#
7167
# @since 2.0.0
72-
def login(connection, mechanism = nil)
73-
mechanism ||= user.mechanism || :scram
74-
conversation = Conversation.new(user, MECHANISMS[mechanism])
68+
def login(connection)
69+
mechanism = user.mechanism || :scram
70+
conversation = Conversation.new(user, mechanism)
7571
reply = connection.dispatch([ conversation.start(connection) ])
7672
connection.update_cluster_time(Operation::Result.new(reply))
7773
reply = connection.dispatch([ conversation.continue(reply, connection) ])

lib/mongo/auth/scram/conversation.rb

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ module Mongo
1919
module Auth
2020
class SCRAM
2121

22-
# Defines behavior around a single SCRAM-SHA-1 conversation between the
23-
# client and server.
22+
# Defines behavior around a single SCRAM-SHA-1/256 conversation between
23+
# the client and server.
2424
#
2525
# @since 2.0.0
2626
class Conversation
@@ -168,7 +168,7 @@ def finalize(reply, connection = nil)
168168
end
169169

170170
# Start the SCRAM conversation. This returns the first message that
171-
# needs to be send to the server.
171+
# needs to be sent to the server.
172172
#
173173
# @example Start the conversation.
174174
# conversation.start
@@ -180,7 +180,8 @@ def finalize(reply, connection = nil)
180180
# @since 2.0.0
181181
def start(connection = nil)
182182
if connection && connection.features.op_msg_enabled?
183-
selector = CLIENT_FIRST_MESSAGE.merge(payload: client_first_message, mechanism: @mechanism)
183+
selector = CLIENT_FIRST_MESSAGE.merge(
184+
payload: client_first_message, mechanism: full_mechanism)
184185
selector[Protocol::Msg::DATABASE_IDENTIFIER] = user.auth_source
185186
cluster_time = connection.mongos? && connection.cluster_time
186187
selector[Operation::CLUSTER_TIME] = cluster_time if cluster_time
@@ -189,12 +190,17 @@ def start(connection = nil)
189190
Protocol::Query.new(
190191
user.auth_source,
191192
Database::COMMAND,
192-
CLIENT_FIRST_MESSAGE.merge(payload: client_first_message, mechanism: @mechanism),
193+
CLIENT_FIRST_MESSAGE.merge(
194+
payload: client_first_message, mechanism: full_mechanism),
193195
limit: -1
194196
)
195197
end
196198
end
197199

200+
def full_mechanism
201+
MECHANISMS[@mechanism]
202+
end
203+
198204
# Get the id of the conversation.
199205
#
200206
# @example Get the id of the conversation.
@@ -213,9 +219,14 @@ def id
213219
# Conversation.new(user, mechanism)
214220
#
215221
# @param [ Auth::User ] user The user to converse about.
222+
# @param [ Symbol ] mechanism Authentication mechanism.
216223
#
217224
# @since 2.0.0
218225
def initialize(user, mechanism)
226+
unless [:scram, :scram256].include?(mechanism)
227+
raise InvalidMechanism.new(mechanism)
228+
end
229+
219230
@user = user
220231
@nonce = SecureRandom.base64
221232
@client_key = user.send(:client_key)
@@ -343,7 +354,7 @@ def h(string)
343354
# @since 2.0.0
344355
def hi(data)
345356
case @mechanism
346-
when SCRAM::SCRAM_SHA_256_MECHANISM
357+
when :scram256
347358
OpenSSL::PKCS5.pbkdf2_hmac(
348359
data,
349360
Base64.strict_decode64(salt),
@@ -422,7 +433,7 @@ def salt
422433
# @since 2.0.0
423434
def salted_password
424435
@salted_password ||= case @mechanism
425-
when SCRAM::SCRAM_SHA_256_MECHANISM
436+
when :scram256
426437
hi(user.sasl_prepped_password)
427438
else
428439
hi(user.hashed_password)
@@ -510,15 +521,17 @@ def validate_first_message!(reply)
510521
end
511522

512523
def validate!(reply)
513-
raise Unauthorized.new(user) unless reply.documents[0][Operation::Result::OK] == 1
524+
if reply.documents[0][Operation::Result::OK] != 1
525+
raise Unauthorized.new(user, full_mechanism)
526+
end
514527
@reply = reply
515528
end
516529

517530
private
518531

519532
def digest
520533
@digest ||= case @mechanism
521-
when SCRAM::SCRAM_SHA_256_MECHANISM
534+
when :scram256
522535
OpenSSL::Digest::SHA256.new.freeze
523536
else
524537
OpenSSL::Digest::SHA1.new.freeze

lib/mongo/auth/user.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module Auth
2121
#
2222
# @since 2.0.0
2323
class User
24+
include Loggable
2425

2526
# @return [ String ] The authorization source, either a database or
2627
# external name.
@@ -44,6 +45,14 @@ class User
4445
# @return [ Array<String> ] roles The user roles.
4546
attr_reader :roles
4647

48+
# Loggable requires an options attribute. We don't have any options
49+
# hence provide this as a stub.
50+
#
51+
# @api private
52+
def options
53+
{}
54+
end
55+
4756
# Determine if this user is equal to another.
4857
#
4958
# @example Check user equality.
@@ -154,6 +163,25 @@ def initialize(options)
154163
@name = options[:user]
155164
@password = options[:password] || options[:pwd]
156165
@mechanism = options[:auth_mech]
166+
if @mechanism
167+
# Since the driver must select an authentication class for
168+
# the specified mechanism, mechanisms that the driver does not
169+
# know about, and cannot translate to an authentication class,
170+
# need to be rejected.
171+
unless @mechanism.is_a?(Symbol)
172+
# Although we documented auth_mech option as being a symbol, we
173+
# have not enforced this; warn, reject in lint mode
174+
if Lint.enabled?
175+
raise Error::LintError, "Auth mechanism #{@mechanism.inspect} must be specified as a symbol"
176+
else
177+
log_warn("Auth mechanism #{@mechanism.inspect} should be specified as a symbol")
178+
@mechanism = @mechanism.to_sym
179+
end
180+
end
181+
unless Auth::SOURCES.key?(@mechanism)
182+
raise InvalidMechanism.new(options[:auth_mech])
183+
end
184+
end
157185
@auth_mech_properties = options[:auth_mech_properties] || {}
158186
@roles = options[:roles] || []
159187
@client_key = options[:client_key]

lib/mongo/auth/x509/conversation.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def finalize(reply)
5151
end
5252

5353
# Start the x.509 conversation. This returns the first message that
54-
# needs to be send to the server.
54+
# needs to be sent to the server.
5555
#
5656
# @example Start the conversation.
5757
# conversation.start
@@ -95,7 +95,9 @@ def initialize(user)
9595
private
9696

9797
def validate!(reply)
98-
raise Unauthorized.new(user) if reply.documents[0][Operation::Result::OK] != 1
98+
if reply.documents[0][Operation::Result::OK] != 1
99+
raise Unauthorized.new(user, MECHANISM)
100+
end
99101
@reply = reply
100102
end
101103
end

spec/integration/auth_spec.rb

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
require 'spec_helper'
2+
3+
describe 'Auth' do
4+
describe 'Unauthorized exception message' do
5+
let(:server) do
6+
authorized_client.cluster.next_primary
7+
end
8+
9+
let(:connection) do
10+
Mongo::Server::Connection.new(server, options)
11+
end
12+
13+
before(:all) do
14+
# If auth is configured, the test suite uses the configured user
15+
# and does not create its own users. However, the configured user may
16+
# not have the auth mechanisms we need. Therefore we create a user
17+
# for this test without specifying auth mechanisms, which gets us
18+
# server default (scram for 4.0, scram & scram256 for 4.2).
19+
20+
users = ClientRegistry.instance.global_client('root_authorized').use(:admin).database.users
21+
unless users.info('existing_user').empty?
22+
users.remove('existing_user')
23+
end
24+
users.create('existing_user', password: 'password')
25+
end
26+
27+
context 'user mechanism not provided' do
28+
29+
context 'user does not exist' do
30+
let(:options) { SpecConfig.instance.ssl_options.merge(
31+
user: 'nonexistent_user') }
32+
33+
before do
34+
expect(connection.app_metadata.send(:document)[:saslSupportedMechs]).to eq('admin.nonexistent_user')
35+
end
36+
37+
context 'scram-sha-1 only server' do
38+
min_server_fcv '3.0'
39+
max_server_version '3.6'
40+
41+
it 'indicates scram-sha-1 was used' do
42+
expect do
43+
connection.connect!
44+
end.to raise_error(Mongo::Auth::Unauthorized, 'User nonexistent_user (mechanism: scram) is not authorized to access admin (used mechanism: SCRAM-SHA-1)')
45+
end
46+
end
47+
48+
context 'scram-sha-256 server' do
49+
min_server_fcv '4.0'
50+
51+
# An existing user on 4.0+ will negotiate scram-sha-256.
52+
# A non-existing user on 4.0+ will negotiate scram-sha-1.
53+
it 'indicates scram-sha-1 was used' do
54+
expect do
55+
connection.connect!
56+
end.to raise_error(Mongo::Auth::Unauthorized, 'User nonexistent_user (mechanism: scram) is not authorized to access admin (used mechanism: SCRAM-SHA-1)')
57+
end
58+
end
59+
end
60+
61+
context 'user exists' do
62+
let(:options) { SpecConfig.instance.ssl_options.merge(
63+
user: 'existing_user', password: 'bogus') }
64+
65+
before do
66+
expect(connection.app_metadata.send(:document)[:saslSupportedMechs]).to eq("admin.existing_user")
67+
end
68+
69+
context 'scram-sha-1 only server' do
70+
min_server_fcv '3.0'
71+
max_server_version '3.6'
72+
73+
it 'indicates scram-sha-1 was used' do
74+
expect do
75+
connection.connect!
76+
end.to raise_error(Mongo::Auth::Unauthorized, "User existing_user (mechanism: scram) is not authorized to access admin (used mechanism: SCRAM-SHA-1)")
77+
end
78+
end
79+
80+
context 'scram-sha-256 server' do
81+
min_server_fcv '4.0'
82+
83+
# An existing user on 4.0+ will negotiate scram-sha-256.
84+
# A non-existing user on 4.0+ will negotiate scram-sha-1.
85+
it 'indicates scram-sha-256 was used' do
86+
expect do
87+
connection.connect!
88+
end.to raise_error(Mongo::Auth::Unauthorized, "User existing_user (mechanism: scram256) is not authorized to access admin (used mechanism: SCRAM-SHA-256)")
89+
end
90+
end
91+
end
92+
end
93+
94+
context 'user mechanism is provided' do
95+
min_server_fcv '3.0'
96+
97+
context 'scram-sha-1 requested' do
98+
let(:options) { SpecConfig.instance.ssl_options.merge(
99+
user: 'nonexistent_user', auth_mech: :scram) }
100+
101+
it 'indicates scram-sha-1 was requested and used' do
102+
expect do
103+
connection.connect!
104+
end.to raise_error(Mongo::Auth::Unauthorized, 'User nonexistent_user (mechanism: scram) is not authorized to access admin (used mechanism: SCRAM-SHA-1)')
105+
end
106+
end
107+
108+
context 'scram-sha-256 requested' do
109+
min_server_fcv '4.0'
110+
111+
let(:options) { SpecConfig.instance.ssl_options.merge(
112+
user: 'nonexistent_user', auth_mech: :scram256) }
113+
114+
it 'indicates scram-sha-256 was requested and used' do
115+
expect do
116+
connection.connect!
117+
end.to raise_error(Mongo::Auth::Unauthorized, 'User nonexistent_user (mechanism: scram256) is not authorized to access admin (used mechanism: SCRAM-SHA-256)')
118+
end
119+
end
120+
end
121+
end
122+
end

0 commit comments

Comments
 (0)