rack-bacoo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/rack/bacoo/authenticator.rb +49 -0
- data/lib/rack/bacoo/basic_auth_request.rb +25 -0
- data/lib/rack/bacoo/cookie_attributes_parser.rb +36 -0
- data/lib/rack/bacoo/credentials.rb +43 -0
- data/lib/rack/bacoo/encryptor.rb +97 -0
- data/lib/rack/bacoo/session_cookie.rb +61 -0
- data/lib/rack/bacoo/version.rb +7 -0
- data/lib/rack-bacoo.rb +82 -0
- metadata +96 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3fdf5ceccc9b9ea971009174fc466efd4c6ce6591e5874e07be5de1a0790c1af
|
|
4
|
+
data.tar.gz: fb3aa483a0bd4bb5dfede0b42222a2e057be8e06e592890ae750208e95575aba
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 61b3d57dd281538ffd9c55096a934d8428a08b006aa24cf913a5107116d088b7726283ff0d276301e4bc2d8fdd7dec59c99b03480aaed3285129e4e375d899d6
|
|
7
|
+
data.tar.gz: ccb197ead61d53a9ebeb0947e757cd40c4514ecdf5da4dbfb1546754c5df30909d72323ca9370ab3dbdce65583a59de26be7727c4105667b019d115b585b61dc
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "basic_auth_request"
|
|
4
|
+
require_relative "session_cookie"
|
|
5
|
+
|
|
6
|
+
module Rack
|
|
7
|
+
module Bacoo
|
|
8
|
+
class Authenticator
|
|
9
|
+
def initialize(cookie_attributes:, cookie_encryptor:, password_cost:, users:)
|
|
10
|
+
@cookie_attributes = cookie_attributes
|
|
11
|
+
@cookie_encryptor = cookie_encryptor
|
|
12
|
+
@password_cost = password_cost
|
|
13
|
+
@users = users
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def with_env(env)
|
|
17
|
+
@env = env
|
|
18
|
+
@basic_auth_request = BasicAuthRequest.new(env)
|
|
19
|
+
@session_cookie = SessionCookie.new(@cookie_attributes, @cookie_encryptor, env)
|
|
20
|
+
@credentials = Credentials.new(@password_cost, @users)
|
|
21
|
+
yield
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def basic_auth_provided?
|
|
25
|
+
@basic_auth_request.provided?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def valid_basic_auth?
|
|
29
|
+
@basic_auth_request.valid_basic_auth?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def basic_authenticated
|
|
33
|
+
return unless @credentials.basic_authenticated?(@basic_auth_request)
|
|
34
|
+
|
|
35
|
+
yield @credentials
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def cookie_authenticated
|
|
39
|
+
return unless @credentials.cookie_authenticated?(@session_cookie)
|
|
40
|
+
|
|
41
|
+
yield @credentials
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def persist_session!(credentials, headers)
|
|
45
|
+
@session_cookie.set(headers, credentials.username, credentials.password)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rack
|
|
4
|
+
module Bacoo
|
|
5
|
+
class BasicAuthRequest < Rack::Auth::AbstractRequest
|
|
6
|
+
def valid_basic_auth?
|
|
7
|
+
scheme == "basic" && credentials.length == 2
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def username
|
|
11
|
+
credentials.first
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def password
|
|
15
|
+
credentials.last
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def credentials
|
|
21
|
+
@credentials ||= params.unpack1("m").split(":", 2)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rack
|
|
4
|
+
module Bacoo
|
|
5
|
+
class CookieAttributesParser
|
|
6
|
+
DEFAULTS = {
|
|
7
|
+
cookie_name: "rack-bacoo",
|
|
8
|
+
path: "/",
|
|
9
|
+
max_age: 24 * 60 * 60, # 24 hours
|
|
10
|
+
secure: true,
|
|
11
|
+
http_only: true,
|
|
12
|
+
same_site: "Lax"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def self.call(cookie_attributes)
|
|
16
|
+
cookie_attributes.each do |pair|
|
|
17
|
+
case pair
|
|
18
|
+
in [:secure, nil] | [:secure, false]
|
|
19
|
+
raise ArgumentError, "secure must be true"
|
|
20
|
+
in [:http_only, nil] | [:http_only, false]
|
|
21
|
+
raise ArgumentError, "http_only must be true"
|
|
22
|
+
in [:httponly, nil] | [:httponly, false]
|
|
23
|
+
raise ArgumentError, "httponly must be true"
|
|
24
|
+
in [:same_site, nil] | [:same_site, false]
|
|
25
|
+
raise ArgumentError, "same_site must be either lax or strict"
|
|
26
|
+
in [:same_site, val] if val.to_s.downcase == "none"
|
|
27
|
+
raise ArgumentError, "same_site must be either lax or strict"
|
|
28
|
+
else
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
DEFAULTS.merge(cookie_attributes)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bcrypt"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Rack
|
|
7
|
+
module Bacoo
|
|
8
|
+
class Credentials
|
|
9
|
+
WRONG_USER = [SecureRandom.hex, SecureRandom.hex].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :username, :password
|
|
12
|
+
|
|
13
|
+
def initialize(password_cost, users)
|
|
14
|
+
@password_cost = password_cost
|
|
15
|
+
@users = users
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def basic_authenticated?(session)
|
|
19
|
+
@username = session.username
|
|
20
|
+
@password = BCrypt::Password.create(session.password, cost: @password_cost)
|
|
21
|
+
authenticated?(@username, @password)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def cookie_authenticated?(session)
|
|
25
|
+
@username = session.username
|
|
26
|
+
@password = session.password
|
|
27
|
+
authenticated?(@username, BCrypt::Password.new(@password))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def authenticated?(username, encrypted_password)
|
|
33
|
+
valid_password = WRONG_USER[1]
|
|
34
|
+
|
|
35
|
+
@users.each do |u, p|
|
|
36
|
+
valid_password = p if Rack::Utils.secure_compare(u, username)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
encrypted_password == valid_password
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module Rack
|
|
7
|
+
module Bacoo
|
|
8
|
+
class Encryptor
|
|
9
|
+
DecryptionError = Class.new(StandardError)
|
|
10
|
+
|
|
11
|
+
AUTH_TAG_LENGTH = 16
|
|
12
|
+
CIPHER = "aes-256-gcm"
|
|
13
|
+
SALT = "entropy comes from the password"
|
|
14
|
+
SEPARATOR = "--"
|
|
15
|
+
|
|
16
|
+
def initialize(password)
|
|
17
|
+
key_len = new_cipher.key_len
|
|
18
|
+
digest = OpenSSL::Digest.new("SHA256")
|
|
19
|
+
# Rationale on how the key is generated: https://github.com/rails/rails/pull/6952
|
|
20
|
+
@key = OpenSSL::PKCS5.pbkdf2_hmac(password, SALT, 1000, key_len, digest)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def encrypt(data)
|
|
24
|
+
cipher = new_cipher
|
|
25
|
+
cipher.encrypt
|
|
26
|
+
cipher.key = @key
|
|
27
|
+
iv = cipher.random_iv
|
|
28
|
+
cipher.auth_data = ""
|
|
29
|
+
encrypted_data = cipher.update(data) + cipher.final
|
|
30
|
+
parts = [encrypted_data, iv, cipher.auth_tag(AUTH_TAG_LENGTH)]
|
|
31
|
+
parts.map { ::Base64.strict_encode64(_1) }.join(SEPARATOR)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def decrypt(encrypted_message)
|
|
35
|
+
cipher = new_cipher
|
|
36
|
+
encrypted_data, iv, auth_tag = extract_parts(encrypted_message)
|
|
37
|
+
|
|
38
|
+
# Currently the OpenSSL bindings do not raise an error if auth_tag is
|
|
39
|
+
# truncated, which would allow an attacker to easily forge it:
|
|
40
|
+
# https://github.com/ruby/openssl/issues/63
|
|
41
|
+
raise DecryptionError, "truncated auth_tag" if auth_tag.bytesize != AUTH_TAG_LENGTH
|
|
42
|
+
|
|
43
|
+
cipher.decrypt
|
|
44
|
+
cipher.key = @key
|
|
45
|
+
cipher.iv = iv
|
|
46
|
+
cipher.auth_tag = auth_tag
|
|
47
|
+
cipher.auth_data = ""
|
|
48
|
+
cipher.update(encrypted_data) + cipher.final
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Base64 encodes with a 6-bit alphabet plus padding:
|
|
54
|
+
# https://en.wikipedia.org/wiki/Base64
|
|
55
|
+
def length_after_base64(length_before_base64)
|
|
56
|
+
4 * (length_before_base64 / 3.0).ceil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def length_of_encoded_iv
|
|
60
|
+
@length_of_encoded_iv ||= length_after_base64(new_cipher.iv_len)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def length_of_encoded_auth_tag
|
|
64
|
+
@length_of_encoded_auth_tag ||= length_after_base64(AUTH_TAG_LENGTH)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def extract_parts(encrypted_message)
|
|
68
|
+
parts = []
|
|
69
|
+
rindex = encrypted_message.length
|
|
70
|
+
|
|
71
|
+
parts << extract_part(encrypted_message, rindex, length_of_encoded_auth_tag)
|
|
72
|
+
rindex -= SEPARATOR.length + length_of_encoded_auth_tag
|
|
73
|
+
|
|
74
|
+
parts << extract_part(encrypted_message, rindex, length_of_encoded_iv)
|
|
75
|
+
rindex -= SEPARATOR.length + length_of_encoded_iv
|
|
76
|
+
|
|
77
|
+
parts << encrypted_message[0, rindex]
|
|
78
|
+
|
|
79
|
+
parts.reverse!.map! { ::Base64.strict_decode64(_1) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def extract_part(encrypted_message, rindex, length)
|
|
83
|
+
index = rindex - length
|
|
84
|
+
|
|
85
|
+
unless encrypted_message[index - SEPARATOR.length, SEPARATOR.length] == SEPARATOR
|
|
86
|
+
raise DecryptionError, "missing separator"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
encrypted_message[index, length]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def new_cipher
|
|
93
|
+
OpenSSL::Cipher.new(CIPHER)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bcrypt"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Rack
|
|
7
|
+
module Bacoo
|
|
8
|
+
class SessionCookie
|
|
9
|
+
WRONG_USERNAME = SecureRandom.hex
|
|
10
|
+
WRONG_PASSWORD = BCrypt::Password.create(SecureRandom.hex, cost: 1)
|
|
11
|
+
|
|
12
|
+
def initialize(cookie_attributes, encryptor, env)
|
|
13
|
+
@cookie_attributes = cookie_attributes.dup
|
|
14
|
+
@cookie_name = @cookie_attributes.delete(:cookie_name)
|
|
15
|
+
@encryptor = encryptor
|
|
16
|
+
@env = env
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def username
|
|
20
|
+
get.nil? ? WRONG_USERNAME : get.first
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def password
|
|
24
|
+
get.nil? ? WRONG_PASSWORD : get.last
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def set(headers, username, encrypted_password)
|
|
28
|
+
value = @encryptor.encrypt("#{username}:#{encrypted_password}:#{expires_at}")
|
|
29
|
+
value = @cookie_attributes.merge(value: value)
|
|
30
|
+
Utils.set_cookie_header!(headers, @cookie_name, value)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def get
|
|
36
|
+
value = Utils.parse_cookies(@env).fetch(@cookie_name, nil)
|
|
37
|
+
return nil if value.nil?
|
|
38
|
+
|
|
39
|
+
username, password, expires_at = @encryptor.decrypt(value).split(":")
|
|
40
|
+
return nil if expires_at.to_i < Time.now.to_i
|
|
41
|
+
|
|
42
|
+
[username, password]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# If a cookie has both the Max-Age and the Expires attribute, the
|
|
46
|
+
# Max-Age attribute has precedence and controls the expiration date
|
|
47
|
+
# of the cookie.
|
|
48
|
+
# https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.2.2
|
|
49
|
+
def expires_at
|
|
50
|
+
case @cookie_attributes
|
|
51
|
+
in { max_age: max_age }
|
|
52
|
+
Time.now.to_i + max_age.to_i
|
|
53
|
+
in { expires: expires }
|
|
54
|
+
expires.to_i
|
|
55
|
+
else
|
|
56
|
+
Time.now.to_i
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/rack-bacoo.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
|
|
5
|
+
require_relative "rack/bacoo/version"
|
|
6
|
+
require_relative "rack/bacoo/credentials"
|
|
7
|
+
require_relative "rack/bacoo/authenticator"
|
|
8
|
+
require_relative "rack/bacoo/cookie_attributes_parser"
|
|
9
|
+
require_relative "rack/bacoo/encryptor"
|
|
10
|
+
|
|
11
|
+
module Rack
|
|
12
|
+
module Bacoo
|
|
13
|
+
class Middleware
|
|
14
|
+
DEFAULTS = {
|
|
15
|
+
cookie_attributes: {},
|
|
16
|
+
password_cost: BCrypt::Engine.cost,
|
|
17
|
+
paths: [Regexp.new(".*")],
|
|
18
|
+
realm: nil
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
attr_accessor :realm
|
|
22
|
+
|
|
23
|
+
def initialize(app, config)
|
|
24
|
+
config = DEFAULTS.merge(config)
|
|
25
|
+
|
|
26
|
+
@app = app
|
|
27
|
+
@paths, @realm = config.fetch_values(:paths, :realm)
|
|
28
|
+
@authenticator = Authenticator.new(
|
|
29
|
+
cookie_attributes: CookieAttributesParser.call(config.fetch(:cookie_attributes)),
|
|
30
|
+
cookie_encryptor: Encryptor.new(config.fetch(:encrypt_cookie_with)),
|
|
31
|
+
password_cost: config.fetch(:password_cost),
|
|
32
|
+
users: config.fetch(:users)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call(env)
|
|
37
|
+
@authenticator.with_env(env) do
|
|
38
|
+
actual_path = Utils.clean_path_info(Utils.unescape_path(env["PATH_INFO"]))
|
|
39
|
+
return @app.call(env) if @paths.none? { _1.match? actual_path }
|
|
40
|
+
@authenticator.cookie_authenticated { |credentials| return call_app(credentials, env) }
|
|
41
|
+
return unauthorized unless @authenticator.basic_auth_provided?
|
|
42
|
+
return bad_request unless @authenticator.valid_basic_auth?
|
|
43
|
+
@authenticator.basic_authenticated { |credentials| return call_app(credentials, env) }
|
|
44
|
+
|
|
45
|
+
unauthorized
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def call_app(credentials, env)
|
|
52
|
+
env["REMOTE_USER"] = credentials.username
|
|
53
|
+
@app.call(env).tap do |_, headers, _|
|
|
54
|
+
@authenticator.persist_session!(credentials, headers)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def unauthorized
|
|
59
|
+
[
|
|
60
|
+
401,
|
|
61
|
+
{
|
|
62
|
+
CONTENT_TYPE => "text/plain",
|
|
63
|
+
CONTENT_LENGTH => "0",
|
|
64
|
+
"www-authenticate" => "Basic realm=\"#{realm}\""
|
|
65
|
+
},
|
|
66
|
+
[]
|
|
67
|
+
]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def bad_request
|
|
71
|
+
[
|
|
72
|
+
400,
|
|
73
|
+
{
|
|
74
|
+
CONTENT_TYPE => "text/plain",
|
|
75
|
+
CONTENT_LENGTH => "0"
|
|
76
|
+
},
|
|
77
|
+
[]
|
|
78
|
+
]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rack-bacoo
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- 3v0k4
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: bcrypt
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rack
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.2'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.2'
|
|
54
|
+
description: Rack::Bacoo combines HTTP Basic Authentication with a session cookie
|
|
55
|
+
so that you don't have to input username and password on each visit. The session
|
|
56
|
+
cookie is encrypted (aes-256-gcm), and the password inside is hashed (bcrypt).
|
|
57
|
+
email:
|
|
58
|
+
- riccardo.odone@gmail.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- lib/rack-bacoo.rb
|
|
64
|
+
- lib/rack/bacoo/authenticator.rb
|
|
65
|
+
- lib/rack/bacoo/basic_auth_request.rb
|
|
66
|
+
- lib/rack/bacoo/cookie_attributes_parser.rb
|
|
67
|
+
- lib/rack/bacoo/credentials.rb
|
|
68
|
+
- lib/rack/bacoo/encryptor.rb
|
|
69
|
+
- lib/rack/bacoo/session_cookie.rb
|
|
70
|
+
- lib/rack/bacoo/version.rb
|
|
71
|
+
homepage: https://github.com/3v0k4/rack-bacoo
|
|
72
|
+
licenses:
|
|
73
|
+
- MIT
|
|
74
|
+
metadata:
|
|
75
|
+
homepage_uri: https://github.com/3v0k4/rack-bacoo
|
|
76
|
+
source_code_uri: https://github.com/3v0k4/rack-bacoo
|
|
77
|
+
changelog_uri: https://github.com/3v0k4/rack-bacoo/blob/main/CHANGELOG.md
|
|
78
|
+
rubygems_mfa_required: 'true'
|
|
79
|
+
rdoc_options: []
|
|
80
|
+
require_paths:
|
|
81
|
+
- lib
|
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: 3.1.0
|
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
requirements: []
|
|
93
|
+
rubygems_version: 3.6.9
|
|
94
|
+
specification_version: 4
|
|
95
|
+
summary: Combine HTTP Basic Authentication with a session cookie
|
|
96
|
+
test_files: []
|