authralia 0.0.1 → 0.0.3
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 +4 -4
- data/lib/authenticate/base.rb +6 -0
- data/lib/authenticate/resource.rb +27 -0
- data/lib/authenticated_session/acceptence.rb +24 -0
- data/lib/authenticated_session/base.rb +74 -0
- data/lib/authenticated_session/creation.rb +42 -0
- data/lib/authenticated_session/rejection.rb +16 -0
- data/lib/authenticated_session/removal.rb +36 -0
- data/lib/authenticated_session/validation.rb +95 -0
- data/lib/authenticated_session/wipeout.rb +15 -0
- data/lib/authralia.rb +5 -5
- data/lib/controllers/helpers.rb +112 -0
- data/lib/models/thydney_the_protector.rb +21 -0
- metadata +43 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 414a5e20e68e0a941cfee786236b8936b20c6c501abb1c0915eb88c0d2a75ca4
|
4
|
+
data.tar.gz: 4a88420c5c8a63f744c0aa8d5a88314fd59060ec73ae717105bf6423d40a078f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 19aaf48d110abd50ec78b293312c5e34ad80b23fa99af8c5c1017037398939395bc1f376af6f3113f7749e35dc90d2f7f438e71927f80bfeb07df0aaaebb27d1
|
7
|
+
data.tar.gz: a7f01ccea9d49569e61174d1d34c26885219ee1a91a296906da809dd17b6e67e419320594aa93f56d3109d84dc430a83f741fbcc1d8d25e7b5a46a921e008201
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Authralia
|
2
|
+
module Authenticate
|
3
|
+
class Resource < Base
|
4
|
+
def initialize(resource_class_name, auth_data)
|
5
|
+
@auth_data = auth_data
|
6
|
+
@resource_class = resource_class_name.constantize
|
7
|
+
end
|
8
|
+
|
9
|
+
def call
|
10
|
+
if @auth_data.class.name == @resource_class.to_s
|
11
|
+
response(status: SUCCESS, payload: @auth_data)
|
12
|
+
else
|
13
|
+
resource = @resource_class.protect_thcope(@auth_data)
|
14
|
+
|
15
|
+
if resource&.authenticate(@auth_data[:password])
|
16
|
+
response(status: SUCCESS, payload: resource)
|
17
|
+
else
|
18
|
+
response(
|
19
|
+
status: FAIL,
|
20
|
+
message: "Incorrect Login Data"
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Authralia
|
2
|
+
module AuthenticatedSession
|
3
|
+
class Acceptence < Base
|
4
|
+
def initialize(session_identifier, guid)
|
5
|
+
@session_identifier = session_identifier
|
6
|
+
@guid = guid
|
7
|
+
end
|
8
|
+
|
9
|
+
def call
|
10
|
+
response(status: SUCCESS) if @session_identifier.blank?
|
11
|
+
|
12
|
+
update_sessions(@session_identifier) do |session|
|
13
|
+
if session[:guid] == @guid
|
14
|
+
session[:is_accepted] = true
|
15
|
+
else
|
16
|
+
session[:new_session_guids].delete(@guid)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
response(status: SUCCESS)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Authralia
|
2
|
+
module AuthenticatedSession
|
3
|
+
class Base < SerpisObjek::Base
|
4
|
+
protected
|
5
|
+
|
6
|
+
def extract_resource_identifier(session_identifier)
|
7
|
+
session_identifier.split('#').first(2).join('#')
|
8
|
+
end
|
9
|
+
|
10
|
+
def extract_guid(session_identifier)
|
11
|
+
session_identifier.split('#').last
|
12
|
+
end
|
13
|
+
|
14
|
+
def build_resource_identifier(resource)
|
15
|
+
"#{resource.class.name}##{resource.id}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def filter_sessions(value)
|
19
|
+
return [] unless value.is_a? Array
|
20
|
+
|
21
|
+
value.select do |session|
|
22
|
+
session.is_a?(Hash)
|
23
|
+
end.map do |session|
|
24
|
+
session.symbolize_keys
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_session(resource, session)
|
29
|
+
handler = get_session_handler(resource)
|
30
|
+
|
31
|
+
handler.value ||= []
|
32
|
+
handler.value << session
|
33
|
+
handler.save
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_sessions(resource, &block)
|
37
|
+
handler = get_session_handler(resource)
|
38
|
+
sessions = filter_sessions(handler.value)
|
39
|
+
|
40
|
+
sessions.each do |session|
|
41
|
+
yield(session)
|
42
|
+
end
|
43
|
+
|
44
|
+
handler.value = sessions
|
45
|
+
handler.save
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_session_handler(value)
|
49
|
+
resource_identifier =
|
50
|
+
if is_session_identifier?(value)
|
51
|
+
extract_resource_identifier(value)
|
52
|
+
elsif is_resource_identifier?(value)
|
53
|
+
value
|
54
|
+
else
|
55
|
+
build_resource_identifier(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
@session_handler ||= Chredis::Json.new resource_identifier
|
59
|
+
end
|
60
|
+
|
61
|
+
# TODO: validate using start_with?
|
62
|
+
def is_session_identifier?(value)
|
63
|
+
value.is_a?(String) &&
|
64
|
+
value.count('#').eql?(2)
|
65
|
+
end
|
66
|
+
|
67
|
+
# TODO: validate using start_with?
|
68
|
+
def is_resource_identifier?(value)
|
69
|
+
value.is_a?(String) &&
|
70
|
+
value.count('#').eql?(1)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Authralia
|
2
|
+
module AuthenticatedSession
|
3
|
+
class Creation < Base
|
4
|
+
def initialize(resource, controller)
|
5
|
+
@resource = resource
|
6
|
+
@controller = controller
|
7
|
+
end
|
8
|
+
|
9
|
+
def call
|
10
|
+
session_guid = SecureRandom.uuid
|
11
|
+
browser_guid = SecureRandom.uuid
|
12
|
+
resource_identifier = build_resource_identifier(@resource)
|
13
|
+
|
14
|
+
handler = get_session_handler(@resource)
|
15
|
+
sessions = filter_sessions(handler.value)
|
16
|
+
|
17
|
+
add_session(
|
18
|
+
@resource,
|
19
|
+
{
|
20
|
+
guid: session_guid,
|
21
|
+
browser_guid: @controller.send(:cookies)[:browser_guid],
|
22
|
+
login_at: Time.now.utc,
|
23
|
+
login_ip: @controller.request.ip,
|
24
|
+
host: @controller.request.host,
|
25
|
+
user_agent: @controller.request.user_agent,
|
26
|
+
expires_at: Time.now.utc + Authralia.expires_in.hours,
|
27
|
+
is_accepted: sessions.size.zero?,
|
28
|
+
new_session_guids: []
|
29
|
+
}
|
30
|
+
)
|
31
|
+
|
32
|
+
response(
|
33
|
+
status: SUCCESS,
|
34
|
+
payload: {
|
35
|
+
browser_guid: browser_guid,
|
36
|
+
session_identifier: "#{resource_identifier}##{session_guid}"
|
37
|
+
}
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Authralia
|
2
|
+
module AuthenticatedSession
|
3
|
+
class Rejection < Removal
|
4
|
+
def initialize(session_identifier, guid)
|
5
|
+
@session_identifier = session_identifier
|
6
|
+
@guid = guid
|
7
|
+
end
|
8
|
+
|
9
|
+
def call
|
10
|
+
response(status: FAIL) if @session_identifier.blank?
|
11
|
+
remove_session_based_on_guid(@session_identifier, @guid)
|
12
|
+
response(status: SUCCESS)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Authralia
|
2
|
+
module AuthenticatedSession
|
3
|
+
class Removal < Base
|
4
|
+
def initialize(session_identifier)
|
5
|
+
@session_identifier = session_identifier
|
6
|
+
end
|
7
|
+
|
8
|
+
def call
|
9
|
+
guid = extract_guid(@session_identifier)
|
10
|
+
|
11
|
+
response(status: FAIL) if @session_identifier.blank?
|
12
|
+
remove_session_based_on_guid(@session_identifier, guid)
|
13
|
+
response(status: SUCCESS)
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def remove_session_based_on_guid(session_identifier, guid)
|
19
|
+
handler = get_session_handler(session_identifier)
|
20
|
+
collection = filter_sessions(handler.value)
|
21
|
+
collection = collection.reject { |s| s[:guid] == guid }
|
22
|
+
|
23
|
+
collection.each do |session|
|
24
|
+
session[:new_session_guids].delete(guid)
|
25
|
+
end
|
26
|
+
|
27
|
+
if collection.size.zero?
|
28
|
+
handler.clear
|
29
|
+
else
|
30
|
+
handler.value = collection
|
31
|
+
handler.save
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Authralia
|
2
|
+
module AuthenticatedSession
|
3
|
+
class Validation < Base
|
4
|
+
AMOGUS = :AMOGUS
|
5
|
+
|
6
|
+
def initialize(session_identifier, controller)
|
7
|
+
@controller = controller
|
8
|
+
@session_identifier = session_identifier
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
login_message = 'Please login to continue'
|
13
|
+
|
14
|
+
unless is_session_identifier?(@session_identifier)
|
15
|
+
return response(status: FAIL, message: login_message)
|
16
|
+
end
|
17
|
+
|
18
|
+
resource_name, resource_id, guid = @session_identifier.split('#')
|
19
|
+
resource = resource_name.constantize.find_by_id(resource_id)
|
20
|
+
|
21
|
+
handler = get_session_handler(resource)
|
22
|
+
sessions = filter_sessions(handler.value)
|
23
|
+
session = sessions.detect { |s| s[:guid].eql? guid }
|
24
|
+
|
25
|
+
if resource.blank? || session.blank?
|
26
|
+
return response(status: FAIL, message: login_message)
|
27
|
+
end
|
28
|
+
|
29
|
+
if is_session_expire?(session)
|
30
|
+
response(status: FAIL, message: "Session expire. #{login_message}")
|
31
|
+
elsif is_session_sus?(guid, sessions)
|
32
|
+
response(status: AMOGUS, message: 'AMOGUS')
|
33
|
+
else
|
34
|
+
if sessions.size >= 1 && !session[:is_accepted]
|
35
|
+
sessions = notify_other_sessions(sessions, guid)
|
36
|
+
handler.value = sessions
|
37
|
+
handler.save
|
38
|
+
end
|
39
|
+
|
40
|
+
response(
|
41
|
+
status: SUCCESS,
|
42
|
+
payload: { resource:, session: }
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def notify_other_sessions(sessions, guid)
|
50
|
+
sessions.each do |s|
|
51
|
+
new_session_guids = s[:new_session_guids]
|
52
|
+
is_accepted_session = s[:guid] != guid && s[:is_accepted]
|
53
|
+
|
54
|
+
if is_accepted_session && !new_session_guids.include?(guid)
|
55
|
+
s.update(new_session_guids: new_session_guids << guid)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def is_session_expire?(session)
|
61
|
+
(Time.now.utc - session[:expires_at].to_time) >= Authralia.expires_in.hours
|
62
|
+
end
|
63
|
+
|
64
|
+
def is_session_sus?(guid, sessions)
|
65
|
+
sessions.detect do |session|
|
66
|
+
is_guid_valid?(guid, session) &&
|
67
|
+
is_browser_valid?(session) &&
|
68
|
+
is_ip_valid?(session) &&
|
69
|
+
is_host_valid?(session) &&
|
70
|
+
is_user_agent_valid?(session)
|
71
|
+
end.blank?
|
72
|
+
end
|
73
|
+
|
74
|
+
def is_guid_valid?(guid, session)
|
75
|
+
session[:guid] == guid
|
76
|
+
end
|
77
|
+
|
78
|
+
def is_browser_valid?(session)
|
79
|
+
session[:browser_guid] == @controller.send(:cookies)[:browser_guid]
|
80
|
+
end
|
81
|
+
|
82
|
+
def is_ip_valid?(session)
|
83
|
+
session[:login_ip] == @controller.request.ip
|
84
|
+
end
|
85
|
+
|
86
|
+
def is_host_valid?(session)
|
87
|
+
session[:host] == @controller.request.host
|
88
|
+
end
|
89
|
+
|
90
|
+
def is_user_agent_valid?(session)
|
91
|
+
session[:user_agent] == @controller.request.user_agent
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/authralia.rb
CHANGED
@@ -9,7 +9,7 @@ module Authralia
|
|
9
9
|
end
|
10
10
|
|
11
11
|
module AuthenticatedSession
|
12
|
-
autoload :Base, '
|
12
|
+
autoload :Base, 'authenticated_session/base'
|
13
13
|
autoload :Acceptence, 'authenticated_session/acceptence'
|
14
14
|
autoload :Creation, 'authenticated_session/creation'
|
15
15
|
autoload :Rejection, 'authenticated_session/rejection'
|
@@ -26,8 +26,8 @@ module Authralia
|
|
26
26
|
autoload :ThydneyTheProtector, 'models/thydney_the_protector'
|
27
27
|
end
|
28
28
|
|
29
|
-
mattr_accessor :
|
30
|
-
@@
|
29
|
+
mattr_accessor :single_active_session_for
|
30
|
+
@@single_active_session_for = []
|
31
31
|
|
32
32
|
mattr_accessor :expires_in
|
33
33
|
@@expires_in = 1.hour
|
@@ -41,11 +41,11 @@ module Authralia
|
|
41
41
|
end
|
42
42
|
|
43
43
|
ActiveSupport.on_load(:action_controller_base) do
|
44
|
-
include
|
44
|
+
include Authralia::Controllers::Helpers
|
45
45
|
end
|
46
46
|
|
47
47
|
ActiveSupport.on_load(:active_record) do
|
48
|
-
include
|
48
|
+
include Authralia::Models::ThydneyTheProtector
|
49
49
|
end
|
50
50
|
|
51
51
|
module ActionDispatch::Routing
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Authralia
|
2
|
+
module Controllers
|
3
|
+
module Helpers
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
add_flash_types :error
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
Authralia.resource_class_names.each do |resource_class_name|
|
12
|
+
resource_name = resource_class_name.underscore
|
13
|
+
|
14
|
+
attr_reader "current_#{resource_name}".to_sym,
|
15
|
+
"current_#{resource_name}_session".to_sym
|
16
|
+
|
17
|
+
helper_method "current_#{resource_name}".to_sym,
|
18
|
+
"current_#{resource_name}_session".to_sym
|
19
|
+
|
20
|
+
# SECURITY: CSRF countermeasure
|
21
|
+
rescue_from ActionController::InvalidAuthenticityToken,
|
22
|
+
with: "logout_#{resource_name}!".to_sym
|
23
|
+
|
24
|
+
define_method "authenticate_#{resource_name}!" do
|
25
|
+
validation_response = AuthenticatedSession::Validation.call(
|
26
|
+
session[resource_name], self
|
27
|
+
)
|
28
|
+
|
29
|
+
case validation_response.status
|
30
|
+
when AuthenticatedSession::Validation::SUCCESS
|
31
|
+
instance_variable_set(
|
32
|
+
"@current_#{resource_name}",
|
33
|
+
validation_response.payload[:resource]
|
34
|
+
)
|
35
|
+
|
36
|
+
instance_variable_set(
|
37
|
+
"@current_#{resource_name}_session",
|
38
|
+
validation_response.payload[:session]
|
39
|
+
)
|
40
|
+
when AuthenticatedSession::Validation::AMOGUS
|
41
|
+
reset_session
|
42
|
+
|
43
|
+
when_authtralia_fail(validation_response)
|
44
|
+
when AuthenticatedSession::Validation::FAIL
|
45
|
+
send("logout_#{resource_name}!")
|
46
|
+
when_authtralia_fail(validation_response)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
define_method "logout_#{resource_name}!" do
|
51
|
+
return unless session[resource_name]
|
52
|
+
|
53
|
+
removal_response = AuthenticatedSession::Removal.call(
|
54
|
+
session[resource_name]
|
55
|
+
)
|
56
|
+
|
57
|
+
case removal_response.status
|
58
|
+
when Authenticate::Resource::SUCCESS
|
59
|
+
session.delete(resource_name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
define_method "login_#{resource_name}!" do |auth_data, &block|
|
64
|
+
set_browser_guid
|
65
|
+
|
66
|
+
auth_res_response = Authenticate::Resource.call(
|
67
|
+
resource_class_name, auth_data
|
68
|
+
)
|
69
|
+
|
70
|
+
case auth_res_response.status
|
71
|
+
when Authenticate::Resource::SUCCESS
|
72
|
+
resource = auth_res_response.payload
|
73
|
+
|
74
|
+
if Authralia.single_active_session_for.include?(resource_class_name) &&
|
75
|
+
AuthenticatedSession::Wipeout.call(resource)
|
76
|
+
end
|
77
|
+
|
78
|
+
# SECURITY: multi device and session fixation countermeasure
|
79
|
+
authed_sess_response = AuthenticatedSession::Creation.call(
|
80
|
+
resource, self
|
81
|
+
)
|
82
|
+
|
83
|
+
# SECURITY: session fixation countermeasure
|
84
|
+
session.delete(resource_name)
|
85
|
+
|
86
|
+
session[resource_name] =
|
87
|
+
authed_sess_response.payload[:session_identifier]
|
88
|
+
|
89
|
+
block.call(true, auth_res_response) if block
|
90
|
+
when Authenticate::Resource::FAIL
|
91
|
+
block.call(false, auth_res_response) if block
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
define_method "accept_#{resource_name}_session!" do |guid:|
|
96
|
+
AuthenticatedSession::Acceptence.call(session[resource_name], guid)
|
97
|
+
end
|
98
|
+
|
99
|
+
define_method "reject_#{resource_name}_session!" do |guid:|
|
100
|
+
AuthenticatedSession::Rejection.call(session[resource_name], guid)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def set_browser_guid
|
106
|
+
if cookies[:browser_guid].blank?
|
107
|
+
cookies.permanent.signed.encrypted[:browser_guid] = SecureRandom.uuid
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Authralia
|
2
|
+
module Models
|
3
|
+
module ThydneyTheProtector
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
def thydney_protects(*fields)
|
8
|
+
has_secure_password
|
9
|
+
|
10
|
+
define_singleton_method :protect_thcope do |params|
|
11
|
+
field = fields.detect { |f| params[f].present? }
|
12
|
+
|
13
|
+
return nil unless field
|
14
|
+
|
15
|
+
find_by("#{field}": params[field])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: authralia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gilang Mugni Respaty
|
@@ -9,14 +9,54 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
date: 2022-11-16 00:00:00.000000000 Z
|
12
|
-
dependencies:
|
13
|
-
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: chredis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: serpis_objek
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: A authentication dedicated to Mike Tyson based on his favorite place,
|
42
|
+
Australia
|
14
43
|
email: gilmoregarland@gmail.com
|
15
44
|
executables: []
|
16
45
|
extensions: []
|
17
46
|
extra_rdoc_files: []
|
18
47
|
files:
|
48
|
+
- lib/authenticate/base.rb
|
49
|
+
- lib/authenticate/resource.rb
|
50
|
+
- lib/authenticated_session/acceptence.rb
|
51
|
+
- lib/authenticated_session/base.rb
|
52
|
+
- lib/authenticated_session/creation.rb
|
53
|
+
- lib/authenticated_session/rejection.rb
|
54
|
+
- lib/authenticated_session/removal.rb
|
55
|
+
- lib/authenticated_session/validation.rb
|
56
|
+
- lib/authenticated_session/wipeout.rb
|
19
57
|
- lib/authralia.rb
|
58
|
+
- lib/controllers/helpers.rb
|
59
|
+
- lib/models/thydney_the_protector.rb
|
20
60
|
homepage: https://rubygems.org/gems/hola
|
21
61
|
licenses:
|
22
62
|
- MIT
|