wikk_web_auth 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/History.txt +20 -0
  3. data/lib/wikk_web_auth.rb +199 -106
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f11e7bb6190975e4175a2e0b67a4a01b157c3c4787538138099dda2a020ae07a
4
- data.tar.gz: 622a9d3c4d29584a860b39f3a1e30b5462b765f02373dae5d8ab1139a9b5b441
3
+ metadata.gz: 671e30332d7611b59813c740329597c67743e6dfb1d14f5624b0d2f048f735ed
4
+ data.tar.gz: 370ae2207160bb1d6b7304c2451cdf9de71ea0f0da2e760e60642c9f3a8c822b
5
5
  SHA512:
6
- metadata.gz: 83560ee013e45a01a0b8d7c62a4daef09d4c8e33501673d96b643a8ffb5675e136b3f4392b59041138d63abcc3e1584312bf027433ca941ef1da71c6f0fc22f7
7
- data.tar.gz: 7fdd8602fa9427be49777ffa2b221f9718109c05c9c00b5a914b01a1af24fb242ef946c86f404efad63d818455c2626b7c8624cc31506f27c43f622ec663b249
6
+ metadata.gz: 80f9e4d1c65fdd6d96f35399ba86fa2a11ff09a505bfa8154fc6f0f3f464bb1ec9b4f5697655822439e7523cde7da2c4dddc210e78aeaeca690d499c04231cfa
7
+ data.tar.gz: 874afd4511eae12ece2931f7bce38bcb991ae6ae76660d30c53e3aa2254660fa778d3d31eed573e33b66d9dd23d3ed7cf19da263ea1f52c554ba4647c0b75cc8
data/History.txt CHANGED
@@ -1,3 +1,23 @@
1
+ robertburrowes Sat Apr 15 16:18:34 2023 +1200
2
+ deleted an end in error
3
+ robertburrowes Sat Apr 15 16:05:26 2023 +1200
4
+ removed the syslogs. Getting odd errors, depending on the context.
5
+ robertburrowes Sat Apr 15 15:53:04 2023 +1200
6
+ factor out code into new_session() Clean up old cookie entries in cgi instance.
7
+ robertburrowes Sat Apr 15 15:49:37 2023 +1200
8
+ more output from the tests
9
+ robertburrowes Wed Apr 12 18:31:54 2023 +1200
10
+ Add test for authenticated on a reconnect Add test for trying to reauthenticate, by sending a previous response.
11
+ robertburrowes Wed Apr 12 18:30:25 2023 +1200
12
+ Initialize instance vars earlier @log.error now failing. Replaced with full call, using LOG_NOTICE session and @session mix up Explicitly set no_cookies and no_hidden to false (should be default)
13
+ robertburrowes Tue Apr 11 17:56:27 2023 +1200
14
+ bumped the version
15
+ robertburrowes Tue Apr 11 17:56:01 2023 +1200
16
+ Added test/direct_test.rb Cleaned up test, moving conf files into test/conf Created test/conf/pstore for the pstore temp files (which need cleaning out after the test)
17
+ robertburrowes Tue Apr 11 15:06:47 2023 +1200
18
+ refactor: Bump version Set return_url globally Initialize @session globally Separate generating challenge into its own method (for new rpc) Optionally call to authencate in initialize (for new rpc) Added check for the username changing during login. Fail authorized step, if challenge not set (i.e. we jumped straight to step 2. nil cgi params get converted to '' seed now reset, if authorization fails. Authorized? should have been private session_state_init should have been private
19
+ robertburrowes Sun Apr 9 17:57:33 2023 +1200
20
+ not used
1
21
  robertburrowes Wed Mar 29 22:03:06 2023 +1300
2
22
  Test against the new lib version, not the gem
3
23
  robertburrowes Wed Mar 29 22:02:44 2023 +1300
data/lib/wikk_web_auth.rb CHANGED
@@ -3,25 +3,30 @@ module WIKK
3
3
  require 'cgi/session'
4
4
  require 'cgi/session/pstore' # provides CGI::Session::PStore
5
5
  require 'digest/sha2'
6
- require 'syslog/logger'
7
- require 'wikk_aes_256'
6
+ require 'securerandom'
8
7
  require 'wikk_password'
9
8
 
10
9
  # Provides common authentication mechanism for all our cgis.
10
+ # Uses standard cgi parameters, unless overridden e.g. cgi?user=x&response=y
11
+ # Returns values imbedded as hidden fields in the login form
11
12
  # @attr_reader [String] user , the remote user's user name
12
13
  # @attr_reader [String] session , the persistent Session record for this user
13
14
  class Web_Auth
14
- VERSION = '0.1.5' # Gem version
15
+ VERSION = '0.1.6' # Gem version
15
16
 
16
- attr_reader :user, :session
17
+ attr_reader :user, :challenge
18
+ attr_accessor :response
17
19
 
18
20
  # Create new Web_Auth instance, and proceed through authentication process by creating a login web form, if the user isn't authenticated.
19
21
  # @param cgi [CGI] Which carries the client data, cookies, and PUT/POST form data.
20
22
  # @param pwd_config [WIKK::Configuration|Hash] the location of the password file is embedded here.
23
+ # @param user [String] overrides cgi['user']
24
+ # @param response [String] overrides cgi['response']
25
+ # @param user_logout [Boolean] overrides cgi['logout']
21
26
  # @param pstore_config [Hash] overrides default pstore settings
22
27
  # @param return_url [String] If we successfully authenticate, return here.
23
28
  # @return [WIKK::Web_Auth]
24
- def initialize(cgi, pwd_config = nil, return_url = nil, pstore_config: nil)
29
+ def initialize(cgi, pwd_config = nil, return_url = nil, user: nil, response: nil, user_logout: false, pstore_config: nil, run_auth: true)
25
30
  if pwd_config.instance_of?(Hash)
26
31
  sym = pwd_config.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
27
32
  @config = Struct.new(*(k = sym.keys)).new(*sym.values_at(*k))
@@ -31,14 +36,56 @@ module WIKK
31
36
 
32
37
  @cgi = cgi
33
38
  @pstore_config = pstore_config
34
- @user = ''
35
- @session = nil
39
+
40
+ # Set variables from the method's params, or alternately, from the CGI params
41
+ @user = user.nil? ? cgi_param('Username') : user
42
+ @response = response.nil? ? cgi_param('Response') : response
43
+ @return_url = return_url.nil? ? cgi_param('ReturnURL') : return_url
44
+
45
+ # Look for existing session, but don't start a new one.
36
46
  begin
37
- @log = Syslog::Logger.syslog
38
- rescue StandardError
39
- @log = Syslog::Logger.new('wikk_web_auth')
47
+ @session = CGI::Session.new(@cgi, Web_Auth.session_config( { 'new_session' => false }, pstore_config: @pstore_config ))
48
+ rescue ArgumentError => _e # if no old session
49
+ @session = nil
50
+ rescue Exception => e # rubocop:disable Lint/RescueException In CGI, we want to handle every exception
51
+ raise e.class, 'Authenticate, CGI::Session.new ' + e.message
52
+ end
53
+
54
+ if @session.nil?
55
+ @challenge = '' # there is no current challenge
56
+ elsif @session['session_expires'].nil? || # Shouldn't be the case
57
+ @session['session_expires'] < Time.now || # Session has expired
58
+ @session['ip'] != @cgi.remote_addr || # Not coming from same IP address
59
+ # @session['user'] != @user || # Not the same user
60
+ cgi_param('logout') != '' || # Requested a logout
61
+ user_logout # Alternate way to request a logout
62
+ logout
63
+ else
64
+ # We ignore the cgi['Challenge'] value, and always get this from the pstore
65
+ @challenge = @session['seed'] # Recover the challenge from the pstore entry. It may be ''
66
+ end
67
+
68
+ authenticate if run_auth # This generates html output, so it is now conditionally run.
69
+ end
70
+
71
+ # Debug dump of session keys
72
+ def session_to_s
73
+ return '' if @session.nil?
74
+
75
+ s = '{'
76
+ [ 'auth', 'seed', 'ip', 'user', 'session_expires' ].each do |k|
77
+ s += "'#{k}':'#{@session[k]}', "
40
78
  end
41
- authenticate(return_url)
79
+ s += '}'
80
+ return s
81
+ end
82
+
83
+ # expose the session_id. This is also returned by modifying the cgi instance passed in to initialize
84
+ # * The cgi.output_cookies Array of Cookies gets modified if no_cookies is false (the default)
85
+ # * And cgi.output_hidden Hash get modified if no_hidden is false (the default)
86
+ # @return [String] random session id
87
+ def session_id
88
+ @session.nil? ? '' : @session.session_id
42
89
  end
43
90
 
44
91
  # way of checking without doing a full login sequence.
@@ -48,52 +95,39 @@ module WIKK
48
95
  def self.authenticated?(cgi, pstore_config: nil )
49
96
  begin
50
97
  session = CGI::Session.new(cgi, Web_Auth.session_config( { 'new_session' => false }, pstore_config: pstore_config ) )
51
- authenticated = (session != nil && session['session_expires'] > Time.now && session['auth'] == true && session['ip'] == cgi.remote_addr)
52
- session.close # Writes back the session data
98
+ authenticated = (session != nil && !session['session_expires'].nil? && session['session_expires'] > Time.now && session['auth'] == true && session['ip'] == cgi.remote_addr)
99
+ session.close # Tidy up, so we don't leak file descriptors
53
100
  return authenticated
54
- rescue ArgumentError => e # if no old session to find.
55
- begin
56
- @log = Syslog::Logger.syslog
57
- rescue StandardError
58
- @log = Syslog::Logger.new('wikk_web_auth')
59
- end
60
- @log.error(e.message)
101
+ rescue ArgumentError => _e # if no old session to find.
61
102
  return false
62
103
  end
63
104
  end
64
105
 
106
+ # Test to see if user authenticated.
107
+ # If this is the only call, then follow this with close_session()
108
+ # @return [Boolean] True, if this session is authenticated
109
+ def authenticated?
110
+ @session != nil && !@session['session_expires'].nil? && @session['session_expires'] > Time.now && @session['auth'] == true && @session['ip'] == @cgi.remote_addr
111
+ end
112
+
65
113
  # get the session reference and delete the session.
66
114
  # @param pstore_config [Hash] overrides default pstore settings
67
115
  # @param cgi [CGI] Which carries the client data, cookies, and PUT/POST form data.
68
116
  def self.logout(cgi, pstore_config: nil)
69
117
  begin
70
118
  session = CGI::Session.new(cgi, Web_Auth.session_config( { 'new_session' => false }, pstore_config: pstore_config ))
71
- session.delete if session != nil
72
- rescue ArgumentError => e # if no old session
73
- begin
74
- @log = Syslog::Logger.syslog
75
- rescue StandardError
76
- @log = Syslog::Logger.new('wikk_web_auth')
77
- end
78
- @log.error(e.message)
119
+ session.delete unless session.nil? # Also closes the session
120
+ rescue ArgumentError => _e # if no old session
121
+ # Not an error.
79
122
  end
80
123
  end
81
124
 
82
- # Checks password file to see if the response from the user matches generating a hash from the password locally.
83
- # @param user [String] Who the remote user claims to be
84
- # @param challenge [String] Random string we sent to this user, and they used in hashing their password.
85
- # @param received_hash [String] The hex_SHA256(password + challenge) string that the user sent back.
86
- # @return [Boolean] True for authorization test suceeded.
87
- def authorized?(user, challenge, received_hash)
88
- begin
89
- return WIKK::Password.valid_sha256_response?(user, @pwd_config, challenge, received_hash)
90
- rescue IndexError => e # User didn't exist
91
- @log.error("authorized?(#{user}) User not found: " + e.message)
92
- return false
93
- rescue Exception => e # rubocop:disable Lint/RescueException # In a cgi, we want to log all errors.
94
- @log.error("authorized?(#{user}): " + e.message)
95
- return false
96
- end
125
+ # clean up the session, deleting the session state.
126
+ def logout
127
+ @session.delete unless @session.nil? # Will close the existing session
128
+ @session = nil
129
+ @challenge = '' # no current session, so no challenge string
130
+ clear_cgi_cookies
97
131
  end
98
132
 
99
133
  # Generate the new Session's config parameters, mixing in and/or overriding the preset values.
@@ -104,100 +138,105 @@ module WIKK
104
138
  instance_of?(Hash)
105
139
  session_conf = {
106
140
  'database_manager' => CGI::Session::PStore, # use PStore
107
- 'session_key' => '_wikk_rb_sess_id', # custom session key
108
- 'session_expires' => (Time.now + 86400), # 1 day timeout
109
- 'prefix' => 'pstore_sid_', # Prefix for pstore file
141
+ 'session_key' => '_wikk_rb_sess_id', # custom session key
142
+ 'session_expires' => (Time.now + 86400), # 1 day timeout
143
+ 'prefix' => 'pstore_sid_', # Prefix for pstore file
144
+ # 'suffix' => ?
110
145
  'tmpdir' => '/tmp', # PStore option. Under Apache2, this is a private namespace /tmp
111
- 'session_path' => '/' # The cookie gets returned for URLs starting with this path
112
- # 'session_id' => ?, # Created for new sessions. Merged in for existing sessions
146
+ 'session_path' => '/', # The cookie gets returned for URLs starting with this path
113
147
  # 'new_session' => true, # Default, is to create a new session if it doesn't already exist
114
- # 'no_hidden' => ?,
115
148
  # 'session_domain' => ?,
116
149
  # 'session_secure' => ?,
117
- # 'no_cookies' => ?, #boolean
118
- # 'suffix' => ?
150
+ # 'session_id' => ?, # Created for new sessions. Merged in for existing sessions
151
+ 'no_cookies' => false, # boolean. Do fill in cgi output_cookies array of Cookies
152
+ 'no_hidden' => false # boolean fill in the cgi output_hidden Hash key=cookie, value=session_id
119
153
  }
120
154
  session_conf.merge!(pstore_config) if pstore_config.instance_of?(Hash)
121
155
  session_conf.merge!(extra_arguments) if extra_arguments.instance_of?(Hash)
122
156
  return session_conf
123
157
  end
124
158
 
125
- def session_state_init(session_options = {})
126
- session_options.each { |k, v| @session[k] = v }
159
+ # Generate a challenge, as step 1 of a login
160
+ # If this is the only call, then follow this with close_session()
161
+ def gen_challenge
162
+ # Short session, which gets replaced if we successfully authenticate
163
+ new_session( { 'session_expires' => Time.now + 120 } )
164
+ raise 'gen_challenge: @session == nil' if @session.nil?
165
+
166
+ @challenge = SecureRandom.base64(32)
167
+ # Store the challenge in the pstore, ready for the 2nd login step, along with browser details
168
+ session_state_init('auth' => false, 'seed' => @challenge, 'ip' => @cgi.remote_addr, 'user' => @user, 'session_expires' => @session_options['session_expires'])
169
+ @session.update
170
+ return @challenge
171
+ end
172
+
173
+ # Test the response against the password file
174
+ # If this is the only call, then follow this with close_session()
175
+ # @return [Boolean] We got authorized
176
+ def valid_response?
177
+ if authorized?
178
+ # We got a challenge string, so we are on step 2 of the authentication
179
+ # And have passed the password check ( authorized?() )
180
+ new_session # regenerate the cookie with a longer lifetime.
181
+ raise 'valid_response?: @session == nil' if @session.nil?
182
+
183
+ session_state_init('auth' => true, 'seed' => '', 'ip' => @cgi.remote_addr, 'user' => @user, 'session_expires' => @session_options['session_expires'])
184
+ @session.update # Should also update on close, which we probably do next
185
+ return true
186
+ else # Failed to authorize. The temporary challenge session cookie is no longer valid.
187
+ logout
188
+ return false
189
+ end
127
190
  end
128
191
 
129
192
  # Test to see if we are already authenticated, and if not, generate an HTML login page.
130
- # @param return_url [String] We return here if we sucessfully login
193
+ # @param return_url [String] We return here if we sucessfully login. Overrides initialize value
131
194
  def authenticate(return_url = nil)
132
- begin
133
- @session = CGI::Session.new(@cgi, Web_Auth.session_config( { 'new_session' => false }, pstore_config: @pstore_config )) # Look for existing session.
134
- return gen_html_login_page(return_url) if @session.nil?
135
- rescue ArgumentError => _e # if no old session
136
- return gen_html_login_page(return_url)
137
- rescue Exception => e # rubocop:disable Lint/RescueException In CGI, we want to handle every exception
138
- @log.error("authenticate(#{@session}): #{e.message}")
139
- raise e.class, 'Authenticate, CGI::Session.new ' + e.message
195
+ @return_url = return_url unless return_url.nil? # Update the return url (Backward compatibility)
196
+
197
+ # We have no session setup, or haven't sent the challenge yet.
198
+ # So we are at step 1 of the authentication
199
+ if @session.nil? || @challenge == ''
200
+ gen_html_login_page(message: 'no current challenge')
201
+ return
140
202
  end
141
203
 
204
+ # We are now at step 2, expecting a response to the challenge
142
205
  begin
143
- @session['auth'] = false if @session['session_expires'].nil? ||
206
+ # Might be a while since we initialized the class, so repeat this test
207
+ @session['auth'] = false if @session['session_expires'].nil? || # Shouldn't ever happen, but has
144
208
  @session['session_expires'] < Time.now || # Session has expired
145
- @session['ip'] != @cgi.remote_addr || # Not coming from same IP address
146
- CGI.escapeHTML(@cgi['logout']) != '' # Are trying to logout
209
+ @session['ip'] != @cgi.remote_addr # || # Not coming from same IP address
210
+ # @session['user'] != @user # Username not the same as the session
147
211
 
148
212
  return if @session['auth'] == true # if this is true, then we have already authenticated this session.
149
213
 
150
- if (challenge = @session['seed']) != '' # see if we are looking at a login response.
151
- @user = CGI.escapeHTML(@cgi['Username'])
152
- response = CGI.escapeHTML(@cgi['Response'])
153
- if @user != '' && response != '' && authorized?(@user, challenge, response)
154
- @session['auth'] = true # Response valid.
155
- @session['user'] = @user
156
- @session['ip'] = @cgi.remote_addr
157
- @session['seed'] = '' # Don't use the same one twice.
158
- @session.close
159
- return
160
- end
214
+ unless valid_response?
215
+ gen_html_login_page(message: 'invalid response:')
161
216
  end
162
-
163
- @session.delete # Start a new session.
164
- gen_html_login_page(return_url)
165
- @session.close if @session != nil # Saves the session state.
217
+ @session.close unless @session.nil? # Saves the session state.
166
218
  rescue Exception => e # rubocop:disable Lint/RescueException
167
- @log.error("authenticate(#{@session}): #{e.message}")
168
219
  raise e.class, 'Authenticate, CGI::Session.new ' + e.message
169
220
  end
170
221
  end
171
222
 
172
- # clean up the session, setting @authenticated to false and deleting the session state.
173
- def logout
174
- @session.delete if @session != nil
175
- end
176
-
177
- # Test to see if user authenticated,
178
- # @return [Boolean] i.e @authenticated's value.
179
- def authenticated?
180
- @session != nil && @session['session_expires'] > Time.now && @session['auth'] == true && session['ip'] == @cgi.remote_addr
223
+ # Ensure we don't consume all file descriptors
224
+ # Call after last call (though most calls do close the session)
225
+ def close_session
226
+ @session.close unless @session.nil?
227
+ @session = nil
181
228
  end
182
229
 
183
230
  # Used by calling cgi to generate a standard login page
184
- # @param return_url [String] We return here if we sucessfully login
185
- def gen_html_login_page(return_url = nil)
186
- session_options = Web_Auth.session_config( pstore_config: @pstore_config )
187
- @session = CGI::Session.new(@cgi, session_options ) # Start a new session for future authentications.
188
-
189
- raise 'gen_html_login_page: @session == nil' if @session.nil?
190
-
191
- challenge = WIKK::AES_256.gen_key_to_s
192
- session_state_init('auth' => false, 'seed' => challenge, 'ip' => @cgi.remote_addr, 'session_expires' => session_options['session_expires'])
231
+ def gen_html_login_page(message: '')
232
+ gen_challenge
193
233
  @cgi.header('type' => 'text/html')
194
234
  @cgi.out do
195
235
  @cgi.html do
196
236
  @cgi.head { @cgi.title { 'login' } + html_nocache + html_script } +
197
- @cgi.body { html_login_form(user, challenge, return_url) + "\n" }
237
+ @cgi.body { html_login_form(message: message) + "\n" }
198
238
  end
199
239
  end
200
- @session.update
201
240
  end
202
241
 
203
242
  # Used by calling cgi to inject a return URL into the html response.
@@ -223,6 +262,59 @@ module WIKK
223
262
  HTML
224
263
  end
225
264
 
265
+ # Get a CGI param
266
+ # @param key [String] name of the CGI param
267
+ # @return [String] Either the value, or ''
268
+ private def cgi_param(key)
269
+ value = @cgi[key]
270
+ return value.nil? ? '' : CGI.escapeHTML(value)
271
+ end
272
+
273
+ # Short hand for set up of the pstore session entry
274
+ # @param session_options [Hash] key pairs for the pstore session
275
+ private def session_state_init(session_options = {})
276
+ session_options.each { |k, v| @session[k] = v }
277
+ end
278
+
279
+ # Blat the session cookies, that would have been sent back to the server
280
+ private def clear_cgi_cookies
281
+ # Update the cgi record, removing the cookies
282
+ @cgi.instance_eval do
283
+ @output_hidden = {}
284
+ @output_cookies = []
285
+ end
286
+ end
287
+
288
+ # Create a new session in the pstore, having deleted any existing session.
289
+ # @param session_params [Hash] Optionally alter the session params
290
+ private def new_session(session_params = nil)
291
+ @session.delete unless @session.nil? # Closes and Deletes the existing session
292
+ clear_cgi_cookies
293
+ # Start a new session for future authentications.
294
+ # This resets the expiry timestamp
295
+ @session_options = Web_Auth.session_config( session_params, pstore_config: @pstore_config )
296
+ @session = CGI::Session.new(@cgi, @session_options )
297
+ end
298
+
299
+ # Checks password file to see if the response from the user matches generating a hash from the password locally.
300
+ # @param user [String] Who the remote user claims to be
301
+ # @param challenge [String] Random string we sent to this user, and they used in hashing their password.
302
+ # @param response [String] The hex_SHA256(password + challenge) string that the user sent back.
303
+ # @return [Boolean] True for authorization test suceeded.
304
+ private def authorized?
305
+ begin
306
+ unless @session.nil? ||
307
+ @challenge.nil? || @challenge == '' ||
308
+ @user.nil? || @user.empty? ||
309
+ @response.nil? || @response.empty?
310
+ return WIKK::Password.valid_sha256_response?(@user, @pwd_config, @challenge, @response)
311
+ end
312
+ rescue IndexError => _e # User didn't exist
313
+ # Not an error.
314
+ end
315
+ return false
316
+ end
317
+
226
318
  # Login form javascript helper to SHA256 Hash a password and the challenge string sent by the server.
227
319
  # @return [String] Javascript to embed in html response.
228
320
  private def html_script
@@ -246,16 +338,17 @@ module WIKK
246
338
  # Generate html login form.
247
339
  # @param user [String] user's login name.
248
340
  # @param challenge [String] Random bytes to add to password, before sending back to server.
249
- # @param return_url [String] Pass the url we want to return to if the login succeeds.
341
+ # @param return_url [String] We return here if we sucessfully login. Overrides initialize value
250
342
  # @return [String] Login form to embed in html response to user.
251
- private def html_login_form(user, challenge, return_url = '')
343
+ private def html_login_form(message: '')
252
344
  <<~HTML
253
345
  <form NAME="login" ACTION="/ruby/login.rbx" METHOD="post">
254
- <input TYPE="hidden" NAME="Challenge" VALUE="#{challenge}">
346
+ <input TYPE="hidden" NAME="Challenge" VALUE="#{@challenge}">
255
347
  <input TYPE="hidden" NAME="Response" VALUE="">
256
- <input TYPE="hidden" NAME="ReturnURL" VALUE="#{return_url}">
348
+ <input TYPE="hidden" NAME="ReturnURL" VALUE="#{@return_url}">
349
+ <span hidden>#{message}</span>
257
350
  <table>
258
- <tr><th>User name</th><td><input TYPE="text" NAME="Username" VALUE="#{user}" SIZE="32" MAXLENGTH="32"></td></tr>
351
+ <tr><th>User name</th><td><input TYPE="text" NAME="Username" VALUE="#{@user}" SIZE="32" MAXLENGTH="32"></td></tr>
259
352
  <tr><th>Password</th><td><input TYPE="password" NAME="Password" VALUE="" SIZE="32" MAXLENGTH="32"></td></tr>
260
353
  <tr><td>&nbsp;</td><td>
261
354
  <input ONCLICK="sendhash(); return false;" TYPE="submit" NAME="login" VALUE="Login">
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wikk_web_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Burrowes
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-29 00:00:00.000000000 Z
11
+ date: 2023-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: wikk_password