Class: WIKK::Web_Auth

Inherits:
Object
  • Object
show all
Defined in:
lib/wikk_web_auth.rb

Overview

Provides common authentication mechanism for all our cgis. Uses standard cgi parameters, unless overridden e.g. cgi?user=x&response=y Returns values imbedded as hidden fields in the login form @attr_reader [String] user , the remote user’s user name @attr_reader [String] session , the persistent Session record for this user

Constant Summary collapse

VERSION =

Gem version

'0.1.6'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cgi, pwd_config = nil, return_url = nil, user: nil, response: nil, user_logout: false, pstore_config: nil, run_auth: true) ⇒ Web_Auth

Create new Web_Auth instance, and proceed through authentication process by creating a login web form, if the user isn’t authenticated. @param cgi [CGI] Which carries the client data, cookies, and PUT/POST form data. @param pwd_config [WIKK::Configuration|Hash] the location of the password file is embedded here. @param user [String] overrides cgi[‘user’] @param response [String] overrides cgi[‘response’] @param user_logout [Boolean] overrides cgi[‘logout’] @param pstore_config [Hash] overrides default pstore settings @param return_url [String] If we successfully authenticate, return here. @return [WIKK::Web_Auth]



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
# File 'lib/wikk_web_auth.rb', line 29

def initialize(cgi, pwd_config = nil, return_url = nil, user: nil, response: nil, user_logout: false, pstore_config: nil, run_auth: true)
  if pwd_config.instance_of?(Hash)
    sym = pwd_config.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
    @config = Struct.new(*(k = sym.keys)).new(*sym.values_at(*k))
  else
    @pwd_config = pwd_config
  end

  @cgi = cgi
  @pstore_config = pstore_config

  # Set variables from the method's params, or alternately, from the CGI params
  @user = user.nil? ? cgi_param('Username') : user
  @response = response.nil? ? cgi_param('Response') : response
  @return_url = return_url.nil? ? cgi_param('ReturnURL') : return_url

  # Look for existing session, but don't start a new one.
  begin
    @session = CGI::Session.new(@cgi, Web_Auth.session_config( { 'new_session' => false }, pstore_config: @pstore_config ))
  rescue ArgumentError => _e # if no old session
    @session = nil
  rescue Exception => e # rubocop:disable Lint/RescueException In CGI, we want to handle every exception
    raise e.class, 'Authenticate, CGI::Session.new ' + e.message
  end

  if @session.nil?
    @challenge = '' # there is no current challenge
  elsif @session['session_expires'].nil? ||       # Shouldn't be the case
        @session['session_expires'] < Time.now || # Session has expired
        @session['ip'] != @cgi.remote_addr ||     # Not coming from same IP address
        # @session['user'] != @user ||              # Not the same user
        cgi_param('logout') != '' ||        # Requested a logout
        user_logout                               # Alternate way to request a logout
    logout
  else
    # We ignore the cgi['Challenge'] value, and always get this from the pstore
    @challenge = @session['seed'] # Recover the challenge from the pstore entry. It may be ''
  end

  authenticate if run_auth # This generates html output, so it is now conditionally run.
end

Instance Attribute Details

#challengeObject (readonly)

Returns the value of attribute challenge.



17
18
19
# File 'lib/wikk_web_auth.rb', line 17

def challenge
  @challenge
end

#responseObject

Returns the value of attribute response.



18
19
20
# File 'lib/wikk_web_auth.rb', line 18

def response
  @response
end

#userObject (readonly)

Returns the value of attribute user.



17
18
19
# File 'lib/wikk_web_auth.rb', line 17

def user
  @user
end

Class Method Details

.authenticated?(cgi, pstore_config: nil) ⇒ Boolean

way of checking without doing a full login sequence. @param cgi [CGI] Which carries the client data, cookies, and PUT/POST form data. @param pstore_config [Hash] overrides default pstore settings @return [Boolean] authenticated == true.

Returns:

  • (Boolean)


95
96
97
98
99
100
101
102
103
104
# File 'lib/wikk_web_auth.rb', line 95

def self.authenticated?(cgi, pstore_config: nil )
  begin
    session = CGI::Session.new(cgi, Web_Auth.session_config( { 'new_session' => false }, pstore_config: pstore_config ) )
    authenticated = (session != nil && !session['session_expires'].nil? && session['session_expires'] > Time.now && session['auth'] == true && session['ip'] == cgi.remote_addr)
    session.close # Tidy up, so we don't leak file descriptors
    return authenticated
  rescue ArgumentError => _e # if no old session to find.
    return false
  end
end

.logout(cgi, pstore_config: nil) ⇒ Object

get the session reference and delete the session. @param pstore_config [Hash] overrides default pstore settings @param cgi [CGI] Which carries the client data, cookies, and PUT/POST form data.



116
117
118
119
120
121
122
123
# File 'lib/wikk_web_auth.rb', line 116

def self.logout(cgi, pstore_config: nil)
  begin
    session = CGI::Session.new(cgi, Web_Auth.session_config( { 'new_session' => false }, pstore_config: pstore_config ))
    session.delete unless session.nil? # Also closes the session
  rescue ArgumentError => _e # if no old session
    # Not an error.
  end
end

.session_config(extra_arguments = nil, pstore_config: nil) ⇒ Object

Generate the new Session’s config parameters, mixing in and/or overriding the preset values. @param pstore_config [Hash] Override the default pstore configurations. Only changed keys need to be included @param extra_arguments [Hash] Extra arguments that get added to the hash. Will also override values with the same key. @return [Hash] The configuration hash.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/wikk_web_auth.rb', line 137

def self.session_config( extra_arguments = nil, pstore_config: nil )
  instance_of?(Hash)
  session_conf = {
    'database_manager' => CGI::Session::PStore,  # use PStore
    'session_key' => '_wikk_rb_sess_id',         # custom session key
    'session_expires' => (Time.now + 86400),     # 1 day timeout
    'prefix' => 'pstore_sid_',                   # Prefix for pstore file
    # 'suffix' => ?
    'tmpdir' => '/tmp',                          # PStore option. Under Apache2, this is a private namespace /tmp
    'session_path' => '/',                       # The cookie gets returned for URLs starting with this path
    # 'new_session' => true,                     # Default, is to create a new session if it doesn't already exist
    # 'session_domain' => ?,
    # 'session_secure' => ?,
    # 'session_id' => ?,                         # Created for new sessions. Merged in for existing sessions
    'no_cookies' => false,                       # boolean. Do fill in cgi output_cookies array of Cookies
    'no_hidden' => false                         # boolean fill in the cgi output_hidden Hash key=cookie, value=session_id
  }
  session_conf.merge!(pstore_config) if pstore_config.instance_of?(Hash)
  session_conf.merge!(extra_arguments) if extra_arguments.instance_of?(Hash)
  return session_conf
end

Instance Method Details

#authenticate(return_url = nil) ⇒ Object

Test to see if we are already authenticated, and if not, generate an HTML login page. @param return_url [String] We return here if we sucessfully login. Overrides initialize value



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/wikk_web_auth.rb', line 194

def authenticate(return_url = nil)
  @return_url = return_url unless return_url.nil? # Update the return url (Backward compatibility)

  # We have no session setup, or haven't sent the challenge yet.
  # So we are at step 1 of the authentication
  if @session.nil? || @challenge == ''
    (message: 'no current challenge')
    return
  end

  # We are now at step 2, expecting a response to the challenge
  begin
    # Might be a while since we initialized the class, so repeat this test
    @session['auth'] = false if @session['session_expires'].nil? ||       # Shouldn't ever happen, but has
                                @session['session_expires'] < Time.now || # Session has expired
                                @session['ip'] != @cgi.remote_addr # ||     # Not coming from same IP address
    #                           @session['user'] != @user                 # Username not the same as the session

    return if @session['auth'] == true # if this is true, then we have already authenticated this session.

    hold_challenge = @challenge
    unless valid_response?
      (message: "invalid response: '#{@user}' '#{hold_challenge}' '#{@response}'")
    end
    @session.close unless @session.nil? # Saves the session state.
  rescue Exception => e # rubocop:disable Lint/RescueException
    raise e.class, 'Authenticate, CGI::Session.new ' + e.message
  end
end

#authenticated?Boolean

Test to see if user authenticated. If this is the only call, then follow this with close_session() @return [Boolean] True, if this session is authenticated

Returns:

  • (Boolean)


109
110
111
# File 'lib/wikk_web_auth.rb', line 109

def authenticated?
  @session != nil && !@session['session_expires'].nil? && @session['session_expires'] > Time.now && @session['auth'] == true && @session['ip'] == @cgi.remote_addr
end

#close_sessionObject

Ensure we don’t consume all file descriptors Call after last call (though most calls do close the session)



226
227
228
229
# File 'lib/wikk_web_auth.rb', line 226

def close_session
  @session.close unless @session.nil?
  @session = nil
end

#gen_challengeObject

Generate a challenge, as step 1 of a login If this is the only call, then follow this with close_session()



161
162
163
164
165
166
167
168
169
170
171
# File 'lib/wikk_web_auth.rb', line 161

def gen_challenge
  # Short session, which gets replaced if we successfully authenticate
  new_session( { 'session_expires' => Time.now + 120 } )
  raise 'gen_challenge: @session == nil' if @session.nil?

  @challenge = SecureRandom.base64(32)
  # Store the challenge in the pstore, ready for the 2nd login step, along with browser details
  session_state_init('auth' => false, 'seed' => @challenge, 'ip' => @cgi.remote_addr, 'user' => @user, 'session_expires' => @session_options['session_expires'])
  @session.update
  return @challenge
end

#gen_html_login_page(message: '') ⇒ Object

Used by calling cgi to generate a standard login page



232
233
234
235
236
237
238
239
240
241
# File 'lib/wikk_web_auth.rb', line 232

def (message: '')
  gen_challenge
  @cgi.header('type' => 'text/html')
  @cgi.out do
    @cgi.html do
      @cgi.head { @cgi.title { 'login' } + html_nocache + html_script } +
        @cgi.body { (message: message) + "\n" }
    end
  end
end

#html_logout_form(cgi_dir) ⇒ Object

Used by calling cgi to generate logout with this form. @param cgi_dir [String] directory holding the login.rbx cgi. @return [String] Html logout form.



258
259
260
261
262
263
264
# File 'lib/wikk_web_auth.rb', line 258

def html_logout_form(cgi_dir)
  <<~HTML
    <form NAME="login" ACTION="#{cgi_dir}/login.rbx" METHOD="post">
    <input TYPE="submit" NAME="logout" VALUE="logout" >
    </form>
  HTML
end

#html_reload(url = nil) ⇒ Object

Used by calling cgi to inject a return URL into the html response. Called by calling cgi, when constructing their html headers. @param url [String] URL to redirect to. @return [String] The HTML meta header, or “”, if url is empty.



247
248
249
250
251
252
253
# File 'lib/wikk_web_auth.rb', line 247

def html_reload(url = nil)
  if url != nil && url != ''
    "<meta http-equiv=\"Refresh\" content=\"0; URL=#{url}\">\n"
  else
    ''
  end
end

#logoutObject

clean up the session, deleting the session state.



126
127
128
129
130
131
# File 'lib/wikk_web_auth.rb', line 126

def logout
  @session.delete unless @session.nil? # Will close the existing session
  @session = nil
  @challenge = '' # no current session, so no challenge string
  clear_cgi_cookies
end

#session_idString

expose the session_id. This is also returned by modifying the cgi instance passed in to initialize * The cgi.output_cookies Array of Cookies gets modified if no_cookies is false (the default) * And cgi.output_hidden Hash get modified if no_hidden is false (the default)

Returns:

  • (String)

    random session id



87
88
89
# File 'lib/wikk_web_auth.rb', line 87

def session_id
  @session.nil? ? '' : @session.session_id
end

#session_to_sObject

Debug dump of session keys



72
73
74
75
76
77
78
79
80
81
# File 'lib/wikk_web_auth.rb', line 72

def session_to_s
  return '' if @session.nil?

  s = '{'
  [ 'auth', 'seed', 'ip', 'user', 'session_expires' ].each do |k|
    s += "'#{k}':'#{@session[k]}', "
  end
  s += '}'
  return s
end

#valid_response?Boolean

Test the response against the password file If this is the only call, then follow this with close_session()

Returns:

  • (Boolean)

    We got authorized



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/wikk_web_auth.rb', line 176

def valid_response?
  if authorized?
    # We got a challenge string, so we are on step 2 of the authentication
    # And have passed the password check ( authorized?() )
    new_session # regenerate the cookie with a longer lifetime.
    raise 'valid_response?: @session == nil' if @session.nil?

    session_state_init('auth' => true, 'seed' => '', 'ip' => @cgi.remote_addr, 'user' => @user, 'session_expires' => @session_options['session_expires'])
    @session.update       # Should also update on close, which we probably do next
    return true
  else # Failed to authorize. The temporary challenge session cookie is no longer valid.
    logout
    return false
  end
end