summaryrefslogtreecommitdiff
path: root/lib/net/ssh/authentication/session.rb
blob: e4fc648fb2a21a39d31fa48a22eb1025e119f1ef (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
require 'net/ssh/loggable'
require 'net/ssh/transport/constants'
require 'net/ssh/authentication/constants'
require 'net/ssh/authentication/key_manager'
require 'net/ssh/authentication/methods/none'
require 'net/ssh/authentication/methods/publickey'
require 'net/ssh/authentication/methods/hostbased'
require 'net/ssh/authentication/methods/password'
require 'net/ssh/authentication/methods/keyboard_interactive'

module Net
  module SSH
    module Authentication
      # Raised if the current authentication method is not allowed
      class DisallowedMethod < Net::SSH::Exception
      end

      # Represents an authentication session. It manages the authentication of
      # a user over an established connection (the "transport" object, see
      # Net::SSH::Transport::Session).
      #
      # The use of an authentication session to manage user authentication is
      # internal to Net::SSH (specifically Net::SSH.start). Consumers of the
      # Net::SSH library will never need to access this class directly.
      class Session
        include Loggable
        include Constants
        include Transport::Constants

        # transport layer abstraction
        attr_reader :transport

        # the list of authentication methods to try
        attr_reader :auth_methods

        # the list of authentication methods that are allowed
        attr_reader :allowed_auth_methods

        # a hash of options, given at construction time
        attr_reader :options

        # Instantiates a new Authentication::Session object over the given
        # transport layer abstraction.
        def initialize(transport, options = {})
          self.logger = transport.logger
          @transport = transport

          @auth_methods = options[:auth_methods] || Net::SSH::Config.default_auth_methods
          @options = options

          @allowed_auth_methods = @auth_methods
        end

        # Attempts to authenticate the given user, in preparation for the next
        # service request. Returns true if an authentication method succeeds in
        # authenticating the user, and false otherwise.
        def authenticate(next_service, username, password = nil)
          debug { "beginning authentication of `#{username}'" }

          transport.send_message(transport.service_request("ssh-userauth"))
          expect_message(SERVICE_ACCEPT)

          key_manager = KeyManager.new(logger, options)
          keys.each { |key| key_manager.add(key) } unless keys.empty?
          keycerts.each { |keycert| key_manager.add_keycert(keycert) } unless keycerts.empty?
          key_data.each { |key2| key_manager.add_key_data(key2) } unless key_data.empty?
          default_keys.each { |key| key_manager.add(key) } unless options.key?(:keys) || options.key?(:key_data)

          attempted = []

          @auth_methods.each do |name|
            next unless @allowed_auth_methods.include?(name)

            attempted << name

            debug { "trying #{name}" }
            begin
              auth_class = Methods.const_get(name.split(/\W+/).map { |p| p.capitalize }.join)
              method = auth_class.new(self, key_manager: key_manager, password_prompt: options[:password_prompt])
            rescue NameError
              debug {"Mechanism #{name} was requested, but isn't a known type.  Ignoring it."}
              next
            end

            return true if method.authenticate(next_service, username, password)
          rescue Net::SSH::Authentication::DisallowedMethod
          end

          error { "all authorization methods failed (tried #{attempted.join(', ')})" }
          return false
        ensure
          key_manager.finish if key_manager
        end

        # Blocks until a packet is received. It silently handles USERAUTH_BANNER
        # packets, and will raise an error if any packet is received that is not
        # valid during user authentication.
        def next_message
          loop do
            packet = transport.next_message

            case packet.type
            when USERAUTH_BANNER
              info { packet[:message] }
            # TODO add a hook for people to retrieve the banner when it is sent

            when USERAUTH_FAILURE
              @allowed_auth_methods = packet[:authentications].split(/,/)
              debug { "allowed methods: #{packet[:authentications]}" }
              return packet

            when USERAUTH_METHOD_RANGE, SERVICE_ACCEPT
              return packet

            when USERAUTH_SUCCESS
              transport.hint :authenticated
              return packet

            else
              raise Net::SSH::Exception, "unexpected message #{packet.type} (#{packet})"
            end
          end
        end

        # Blocks until a packet is received, and returns it if it is of the given
        # type. If it is not, an exception is raised.
        def expect_message(type)
          message = next_message
          raise Net::SSH::Exception, "expected #{type}, got #{message.type} (#{message})" unless message.type == type

          message
        end

        private

        # Returns an array of paths to the key files usually defined
        # by system default.
        def default_keys
          %w[~/.ssh/id_ed25519 ~/.ssh/id_rsa ~/.ssh/id_dsa ~/.ssh/id_ecdsa
             ~/.ssh2/id_ed25519 ~/.ssh2/id_rsa ~/.ssh2/id_dsa ~/.ssh2/id_ecdsa]
        end

        # Returns an array of paths to the key files that should be used when
        # attempting any key-based authentication mechanism.
        def keys
          Array(options[:keys])
        end

        # Returns an array of paths to the keycert files that should be used when
        # attempting any key-based authentication mechanism.
        def keycerts
          Array(options[:keycerts])
        end

        # Returns an array of the key data that should be used when
        # attempting any key-based authentication mechanism.
        def key_data
          Array(options[:key_data])
        end
      end
    end
  end
end