authorio 0.8.3 → 0.8.7
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/app/assets/stylesheets/authorio/auth.css +1 -1
- data/app/controllers/authorio/auth_controller.rb +63 -136
- data/app/controllers/authorio/authorio_controller.rb +23 -11
- data/app/controllers/authorio/sessions_controller.rb +6 -5
- data/app/controllers/authorio/users_controller.rb +13 -3
- data/app/helpers/authorio/tag_helper.rb +5 -5
- data/app/jobs/authorio/application_job.rb +2 -0
- data/app/models/authorio/application_record.rb +2 -0
- data/app/models/authorio/request.rb +46 -4
- data/app/models/authorio/session.rb +13 -10
- data/app/models/authorio/token.rb +16 -2
- data/app/models/authorio/user.rb +12 -3
- data/app/views/authorio/auth/authorization_interface.html.erb +2 -2
- data/app/views/authorio/auth/issue_token.json.jbuilder +7 -0
- data/app/views/authorio/auth/send_profile.json.jbuilder +3 -0
- data/app/views/authorio/auth/verify_token.json.jbuilder +5 -0
- data/app/views/authorio/sessions/new.html.erb +1 -2
- data/app/views/authorio/users/_profile.json.jbuilder +10 -0
- data/app/views/authorio/users/show.html.erb +18 -0
- data/app/views/authorio/users/verify.html.erb +1 -0
- data/app/views/shared/_login_form.html.erb +19 -14
- data/config/routes.rb +16 -11
- data/db/migrate/20210817010101_change_path_to_username_in_users.rb +7 -0
- data/db/migrate/20210831155106_add_code_challenge_to_requests.rb +5 -0
- data/lib/authorio/configuration.rb +14 -12
- data/lib/authorio/engine.rb +13 -11
- data/lib/authorio/exceptions.rb +20 -12
- data/lib/authorio/routes.rb +10 -7
- data/lib/authorio/version.rb +3 -1
- data/lib/authorio.rb +15 -21
- data/lib/generators/authorio/install/install_generator.rb +3 -3
- data/lib/generators/authorio/install/templates/authorio.rb +20 -15
- data/lib/tasks/authorio_tasks.rake +15 -14
- metadata +34 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f44cb9e20ad1a6c77dcb24da16623276d876ef4ebd0f3e630e3f27d72587646c
|
4
|
+
data.tar.gz: '08ca2eff8c2cb5a77801af0705981220499f58acfefa6f0b103ebda5c5f2eb62'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f9d5a642af5a2a76f7d3ff12bafcd345bdfb8ab92f3792aa0f24d1ff8db29790e3cc37aab8474f3342660181cee08cf4af25f3c45fa3c8d2fef041697ed60b8f
|
7
|
+
data.tar.gz: 363031d389f74620cff7ae761935bcaa225d1c3c96ebc24122b9d7f45a746aeec46315dee1ff78da4f31329e5210c71707cfe387308f2f8b72483ce0b57ffc7c
|
@@ -1,181 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
4
|
class AuthController < AuthorioController
|
3
|
-
require 'uri'
|
4
|
-
require 'digest'
|
5
|
-
|
6
5
|
# These API-only endpoints are protected by code challenge and do not need CSRF protextion
|
7
|
-
protect_from_forgery with: :exception, except: [
|
6
|
+
protect_from_forgery with: :exception, except: %i[send_profile issue_token]
|
8
7
|
|
9
8
|
rescue_from 'Authorio::Exceptions::SessionReplayAttack' do |exception|
|
10
|
-
redirect_back_with_error
|
11
|
-
logger.info
|
12
|
-
|
9
|
+
redirect_back_with_error 'Session Replay attack detected. This has been logged.'
|
10
|
+
logger.info 'Session replay attack detected!'
|
11
|
+
Session.where(user: exception.session.user).delete_all
|
12
|
+
end
|
13
|
+
rescue_from 'Authorio::Exceptions::UserNotFound' do
|
14
|
+
redirect_back_with_error 'User not found'
|
15
|
+
end
|
16
|
+
rescue_from 'Authorio::Exceptions::InvalidPassword' do
|
17
|
+
redirect_back_with_error 'Incorrect password. Try again.'
|
13
18
|
end
|
14
|
-
|
15
|
-
helper_method :user_scope_description
|
16
19
|
|
17
20
|
# GET /auth
|
18
21
|
def authorization_interface
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
@user = User.find_by_url! params[:me]
|
23
|
-
|
24
|
-
# If there are any old requests from this (client, user), delete them now
|
25
|
-
Request.where(authorio_user: @user, client: params[:client_id]).delete_all
|
26
|
-
|
27
|
-
auth_request = Request.create(
|
28
|
-
code: SecureRandom.hex(20),
|
29
|
-
redirect_uri: params[:redirect_uri],
|
30
|
-
client: params[:client_id], # IndieAuth client_id conflicts with Rails' _id foreign key convention
|
31
|
-
scope: params[:scope],
|
32
|
-
authorio_user: @user
|
33
|
-
)
|
34
|
-
session.update request.parameters.slice(*%w(state client_id code_challenge))
|
35
|
-
@rememberable = Authorio.configuration.local_session_lifetime && !@user_logged_in_locally
|
36
|
-
@scope = params[:scope]&.split
|
37
|
-
rescue ActiveRecord::RecordNotFound
|
38
|
-
redirect_back_with_error "Invalid user"
|
39
|
-
rescue ActionController::ParameterMissing => error
|
40
|
-
render oauth_error "invalid_request", "missing parameter #{error}"
|
22
|
+
session.update auth_interface_params.slice(:state, :client_id, :code_challenge, :redirect_uri)
|
23
|
+
rescue ActionController::ParameterMissing, ActionController::UnpermittedParameters => e
|
24
|
+
render oauth_error 'invalid_request', e
|
41
25
|
end
|
42
26
|
|
43
27
|
# POST /user/:id/authorize
|
44
28
|
def authorize_user
|
45
|
-
redirect_to
|
46
|
-
|
47
|
-
user = authenticate_user_from_session_or_password
|
48
|
-
set_session_cookie(user) if auth_user_params[:remember_me]
|
29
|
+
redirect_to(session[:client_id], allow_other_host: true) and return if params[:commit] == 'Cancel'
|
49
30
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
rescue ActiveRecord::RecordNotFound
|
55
|
-
redirect_back_with_error "Invalid user"
|
56
|
-
rescue Authorio::Exceptions::InvalidPassword
|
57
|
-
redirect_back_with_error "Incorrect password. Try again."
|
31
|
+
@user = authenticate_user_from_session_or_password
|
32
|
+
write_session_cookie(@user) if auth_user_params[:remember_me]
|
33
|
+
create_auth_request
|
34
|
+
redirect_to_client
|
58
35
|
end
|
59
36
|
|
60
37
|
def send_profile
|
61
|
-
|
62
|
-
render json: profile(request)
|
63
|
-
rescue Authorio::Exceptions::InvalidGrant => error
|
64
|
-
render oauth_error 'invalid_grant', error.message
|
38
|
+
@auth_request = find_auth_request or (render validation_failed and return)
|
65
39
|
end
|
66
40
|
|
67
41
|
def issue_token
|
68
|
-
|
69
|
-
|
70
|
-
token = Token.create(authorio_user: req.authorio_user, scope: req.scope, client: req.client)
|
71
|
-
render json: {
|
72
|
-
'access_token': token.auth_token,
|
73
|
-
'scope': req.scope,
|
74
|
-
'expires_in': Authorio.configuration.token_expiration,
|
75
|
-
'token_type': 'Bearer'
|
76
|
-
}.merge(profile(req))
|
77
|
-
rescue Authorio::Exceptions::InvalidGrant => error
|
78
|
-
render oauth_error, 'invalid_grant', error.message
|
42
|
+
@auth_request = find_auth_request or (render validation_failed and return)
|
43
|
+
@token = Token.create_from_request(@auth_request)
|
79
44
|
end
|
80
45
|
|
81
46
|
def verify_token
|
82
|
-
token = Token.
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
render json: {
|
88
|
-
'me': user_url(token.authorio_user),
|
89
|
-
'client_id': token.client,
|
90
|
-
'scope': 'token.scope'
|
91
|
-
}
|
92
|
-
end
|
93
|
-
rescue ActiveRecord::RecordNotFound
|
94
|
-
head :bad_request
|
47
|
+
@token = Token.find_by_auth_token(bearer_token) or (head :bad_request and return)
|
48
|
+
return unless @token.expired?
|
49
|
+
|
50
|
+
@token.delete
|
51
|
+
render token_expired
|
95
52
|
end
|
96
53
|
|
97
54
|
private
|
98
55
|
|
56
|
+
def auth_interface_params
|
57
|
+
@auth_interface_params ||= begin
|
58
|
+
required = %w[client_id redirect_uri state]
|
59
|
+
permitted = %w[me scope code_challenge_method response_type code_challenge dummy]
|
60
|
+
params.require(required)
|
61
|
+
params.permit(permitted + required)
|
62
|
+
params.permit!
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
99
66
|
def scope_params
|
100
67
|
params.require(:scope).permit(scope: [])
|
101
68
|
end
|
102
69
|
|
103
|
-
def oauth_error(error, message=nil)
|
104
|
-
|
105
|
-
|
106
|
-
{ json: resp, status: :bad_request }
|
70
|
+
def oauth_error(error, message = nil, status = :bad_request)
|
71
|
+
{ json: { json: { error:, error_message: message }.compact },
|
72
|
+
status: }
|
107
73
|
end
|
108
74
|
|
109
75
|
def token_expired
|
110
|
-
|
111
|
-
end
|
112
|
-
|
113
|
-
def code_challenge_failed?
|
114
|
-
# For now, if original request did not have code challenge, then we pass by default
|
115
|
-
return false if session[:code_challenge].nil?
|
116
|
-
sha256 = Digest::SHA256.hexdigest params[:code_verifier]
|
117
|
-
base64 = Base64.urlsafe_encode64 sha256
|
118
|
-
return base64 != session[:code_challenge]
|
119
|
-
end
|
120
|
-
|
121
|
-
def invalid_request?(req)
|
122
|
-
req.redirect_uri != params[:redirect_uri] \
|
123
|
-
|| req.client != params[:client_id] \
|
124
|
-
|| req.created_at < Time.now - 10.minutes
|
125
|
-
end
|
126
|
-
|
127
|
-
def validate_request
|
128
|
-
req = Request.find_by code: params[:code]
|
129
|
-
raise Authorio::Exceptions::InvalidGrant, "code not found" if req.nil?
|
130
|
-
req.delete
|
131
|
-
raise Authorio::Exceptions::InvalidGrant, "validation failed" if invalid_request?(req) || code_challenge_failed?
|
132
|
-
req
|
133
|
-
end
|
134
|
-
|
135
|
-
def profile(request)
|
136
|
-
profile = { me: user_url(request.authorio_user) }
|
137
|
-
if request.scope
|
138
|
-
scopes = request.scope.split
|
139
|
-
if scopes.include? 'profile'
|
140
|
-
profile['profile'] = {
|
141
|
-
name: request.authorio_user.full_name,
|
142
|
-
url: request.authorio_user.url,
|
143
|
-
photo: request.authorio_user.photo
|
144
|
-
}.compact
|
145
|
-
if scopes.include? 'email'
|
146
|
-
profile['profile']['email'] = request.authorio_user.email
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
profile
|
76
|
+
oauth_error('invalid_token', 'The access token has expired', :unauthorized)
|
151
77
|
end
|
152
78
|
|
153
|
-
def
|
154
|
-
|
155
|
-
header = request.headers['Authorization']
|
156
|
-
header.gsub(bearer, '') if header && header.match(bearer)
|
79
|
+
def validation_failed
|
80
|
+
oauth_error('invalid_grant', 'validation failed')
|
157
81
|
end
|
158
82
|
|
159
|
-
def
|
160
|
-
|
161
|
-
|
162
|
-
return session.authorio_user
|
163
|
-
else
|
164
|
-
user = User.find_by! profile_path: URI(auth_user_params[:url]).path
|
165
|
-
raise Authorio::Exceptions::InvalidPassword unless user.authenticate(auth_user_params[:password])
|
166
|
-
return user
|
167
|
-
end
|
83
|
+
def find_auth_request
|
84
|
+
auth_request = Request.find_by code: params[:code]
|
85
|
+
auth_request&.validate_oauth params
|
168
86
|
end
|
169
87
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
88
|
+
def create_auth_request
|
89
|
+
@auth_req = Request.create(client: session[:client_id],
|
90
|
+
redirect_uri: session[:redirect_uri],
|
91
|
+
code_challenge: (session[:code_challenge] if session.key? :code_challenge),
|
92
|
+
scope: (scope_params[:scope].join(' ') if params.key? :scope),
|
93
|
+
authorio_user: @user)
|
94
|
+
end
|
175
95
|
|
176
|
-
def
|
177
|
-
|
96
|
+
def redirect_to_client
|
97
|
+
redirect_params = { code: @auth_req.code, state: session[:state] }
|
98
|
+
redirect_to "#{@auth_req.redirect_uri}?#{redirect_params.to_query}", allow_other_host: true
|
178
99
|
end
|
179
100
|
|
101
|
+
def authenticate_user_from_session_or_password
|
102
|
+
user_session&.authorio_user or
|
103
|
+
User.find_by_username!(auth_user_params[:username])
|
104
|
+
.authenticate(auth_user_params[:password]) or
|
105
|
+
raise Exceptions::InvalidPassword
|
106
|
+
end
|
180
107
|
end
|
181
108
|
end
|
@@ -1,12 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
4
|
class AuthorioController < ActionController::Base
|
3
5
|
layout 'authorio/main'
|
4
6
|
|
5
|
-
helper_method :logged_in?, :rememberable?, :
|
7
|
+
helper_method :logged_in?, :rememberable?, :current_user,
|
8
|
+
:user_scope_description, :profile_url
|
6
9
|
|
7
10
|
def index
|
8
11
|
if logged_in?
|
9
|
-
redirect_to edit_user_path(
|
12
|
+
redirect_to edit_user_path(current_user)
|
10
13
|
else
|
11
14
|
redirect_to new_session_path
|
12
15
|
end
|
@@ -33,20 +36,28 @@ module Authorio
|
|
33
36
|
end
|
34
37
|
|
35
38
|
def current_user
|
36
|
-
user_session&.authorio_user
|
39
|
+
user_session&.authorio_user&.id
|
40
|
+
end
|
41
|
+
|
42
|
+
def user_scope_description(scope)
|
43
|
+
Authorio::Request.user_scope_description(scope)
|
37
44
|
end
|
38
45
|
|
39
|
-
def
|
40
|
-
|
46
|
+
def profile_url(user)
|
47
|
+
if Authorio.configuration.multiuser
|
48
|
+
verify_user_url(user)
|
49
|
+
else
|
50
|
+
"#{request.scheme}://#{request.host}"
|
51
|
+
end
|
41
52
|
end
|
42
53
|
|
43
54
|
protected
|
44
55
|
|
45
56
|
def auth_user_params
|
46
|
-
params.require(:user).permit(:
|
57
|
+
params.require(:user).permit(:username, :password, :remember_me)
|
47
58
|
end
|
48
59
|
|
49
|
-
def
|
60
|
+
def write_session_cookie(user)
|
50
61
|
cookies.encrypted[:user] = {
|
51
62
|
value: Authorio::Session.create(authorio_user: user).as_cookie,
|
52
63
|
expires: Authorio.configuration.local_session_lifetime
|
@@ -55,12 +66,13 @@ module Authorio
|
|
55
66
|
|
56
67
|
def redirect_back_with_error(error)
|
57
68
|
flash[:alert] = error
|
58
|
-
redirect_back fallback_location: Authorio.authorization_path, allow_other_host: false
|
69
|
+
redirect_back fallback_location: Authorio.authorization_path.prepend('/'), allow_other_host: false
|
59
70
|
end
|
60
71
|
|
61
|
-
def
|
62
|
-
|
72
|
+
def bearer_token
|
73
|
+
bearer = /^Bearer /
|
74
|
+
header = request.headers['Authorization']
|
75
|
+
header.gsub(bearer, '') if header&.match(bearer)
|
63
76
|
end
|
64
|
-
|
65
77
|
end
|
66
78
|
end
|
@@ -1,6 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
4
|
class SessionsController < AuthorioController
|
3
|
-
|
4
5
|
# GET /session/new
|
5
6
|
def new
|
6
7
|
@session = Session.new(authorio_user: User.first)
|
@@ -8,21 +9,21 @@ module Authorio
|
|
8
9
|
|
9
10
|
# POST /session
|
10
11
|
def create
|
11
|
-
user = User.
|
12
|
+
user = User.find_by_username! auth_user_params[:username]
|
12
13
|
raise Exceptions::InvalidPassword unless user.authenticate(auth_user_params[:password])
|
13
|
-
set_session_cookie(user) if auth_user_params[:remember_me]
|
14
14
|
|
15
|
+
write_session_cookie(user) if auth_user_params[:remember_me]
|
15
16
|
# Even if we don't have a permanent remember-me session, we make a temporary session
|
16
17
|
session[:user_id] = user.id
|
17
18
|
redirect_to edit_user_path(user)
|
18
19
|
rescue Exceptions::InvalidPassword
|
19
|
-
redirect_back_with_error
|
20
|
+
redirect_back_with_error 'Incorrect password. Try again.'
|
20
21
|
end
|
21
22
|
|
22
23
|
# DELETE /session
|
23
24
|
def destroy
|
24
25
|
reset_session
|
25
|
-
if (cookie = cookies.encrypted[:user]) && session = Session.find_by_cookie(cookie)
|
26
|
+
if (cookie = cookies.encrypted[:user]) && (session = Session.find_by_cookie(cookie))
|
26
27
|
cookies.delete :user
|
27
28
|
session.destroy
|
28
29
|
end
|
@@ -1,9 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
4
|
class UsersController < AuthorioController
|
5
|
+
before_action :authorized?, except: :verify
|
3
6
|
|
4
|
-
|
7
|
+
# GET /users/:id
|
8
|
+
def show
|
9
|
+
@user = User.find(params[:id])
|
10
|
+
end
|
5
11
|
|
6
|
-
# GET
|
12
|
+
# GET /users/:id/edit
|
7
13
|
def edit
|
8
14
|
@user = User.find(params[:id])
|
9
15
|
end
|
@@ -11,10 +17,14 @@ module Authorio
|
|
11
17
|
# PATCH /users/:id
|
12
18
|
def update
|
13
19
|
User.find(params[:id]).update(user_params)
|
14
|
-
flash[:info] =
|
20
|
+
flash[:info] = 'Profile Saved'
|
15
21
|
redirect_to edit_user_path
|
16
22
|
end
|
17
23
|
|
24
|
+
# This is only called by IndieAuth clients who wish to verify that a
|
25
|
+
# user profile URL we generated is in fact ours.
|
26
|
+
def verify; end
|
27
|
+
|
18
28
|
private
|
19
29
|
|
20
30
|
def user_params
|
@@ -1,17 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
4
|
# These helpers are provided to the main application
|
3
5
|
module TagHelper
|
4
6
|
extend ActiveSupport::Concern
|
5
7
|
|
6
8
|
included do
|
7
|
-
if respond_to?(:helper_method)
|
8
|
-
helper_method :indieauth_tag
|
9
|
-
end
|
9
|
+
helper_method :indieauth_tag if respond_to?(:helper_method)
|
10
10
|
end
|
11
11
|
|
12
12
|
def indieauth_tag
|
13
|
-
tag(:link, rel: 'authorization_endpoint', href: URI.join(root_url, Authorio.authorization_path)) <<
|
14
|
-
|
13
|
+
tag(:link, rel: 'authorization_endpoint', href: URI.join(main_app.root_url, Authorio.authorization_path)) <<
|
14
|
+
tag(:link, rel: 'token_endpoint', href: URI.join(main_app.root_url, Authorio.token_path))
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -1,12 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
4
|
class Request < ApplicationRecord
|
3
|
-
belongs_to :authorio_user, class_name:
|
5
|
+
belongs_to :authorio_user, class_name: '::Authorio::User'
|
4
6
|
|
5
7
|
validates_presence_of :code, :redirect_uri, :client
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
|
9
|
+
before_validation :set_code, on: :create
|
10
|
+
before_create :sweep_requests
|
11
|
+
|
12
|
+
# The IndieAuth spec uses 'client_id' to specify the client in the address, as a URL (eg "https://example.com")
|
13
|
+
# But Rails uses '_id' to tag associations (foreign keys). So we save that as 'client' here, but map
|
14
|
+
# client_id as an alias since that is what the HTTP parameter will be
|
15
|
+
def client_id=(value)
|
16
|
+
self.client = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate_oauth(params)
|
20
|
+
redirect_uri == params[:redirect_uri] &&
|
21
|
+
client == params[:client_id] &&
|
22
|
+
created_at > 10.minutes.ago &&
|
23
|
+
code_challenge_matches(params[:code_verifier]) &&
|
24
|
+
self
|
10
25
|
end
|
26
|
+
|
27
|
+
def code_challenge_matches(verifier)
|
28
|
+
# For now, if original request did not have code challenge, then we pass by default
|
29
|
+
return true if code_challenge.blank?
|
30
|
+
|
31
|
+
sha256 = Digest::SHA256.digest verifier
|
32
|
+
Base64.urlsafe_encode64(sha256).sub(/=*$/, '') == code_challenge
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.user_scope_description(scope)
|
36
|
+
USER_SCOPE_DESCRIPTION[scope.to_sym] || scope
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def set_code
|
42
|
+
self.code = SecureRandom.hex(20)
|
43
|
+
end
|
44
|
+
|
45
|
+
def sweep_requests
|
46
|
+
Request.where(client:, authorio_user:).destroy_all
|
47
|
+
end
|
48
|
+
|
49
|
+
USER_SCOPE_DESCRIPTION = {
|
50
|
+
profile: 'View basic profile information',
|
51
|
+
email: 'View your email address'
|
52
|
+
}.freeze
|
11
53
|
end
|
12
54
|
end
|
@@ -1,34 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
4
|
class Session < ApplicationRecord
|
3
5
|
# Implement a session cookie store based on best security practices
|
4
6
|
# See: https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence
|
5
|
-
belongs_to :authorio_user, class_name:
|
7
|
+
belongs_to :authorio_user, class_name: '::Authorio::User'
|
6
8
|
|
7
9
|
# 1. Protect against having database stolen by only storing token hashes
|
8
|
-
attribute :token
|
10
|
+
attribute :token # This will not be persisted in the DB
|
9
11
|
has_secure_token
|
10
12
|
|
11
13
|
before_create do
|
12
14
|
self.expires_at = Time.now + Authorio.configuration.token_expiration
|
13
15
|
self.selector = SecureRandom.hex(12)
|
14
|
-
self.hashed_token = Digest::SHA256.hexdigest
|
16
|
+
self.hashed_token = Digest::SHA256.hexdigest token
|
15
17
|
end
|
16
18
|
|
17
19
|
# 2. To guard against timing attacks, we lookup tokens based on a separate selector attribute
|
18
20
|
# and compare them using a secure time-constant comparison method
|
19
21
|
def self.find_by_cookie(cookie)
|
20
|
-
|
21
|
-
session = find_by selector:
|
22
|
-
raise Authorio::Exceptions::SessionReplayAttack.new
|
22
|
+
selector, _token = cookie.split(':')
|
23
|
+
session = find_by selector: selector
|
24
|
+
raise Authorio::Exceptions::SessionReplayAttack.new, session unless session.matches_cookie?(cookie)
|
25
|
+
|
23
26
|
session
|
24
27
|
end
|
25
28
|
|
26
29
|
def matches_cookie?(cookie)
|
27
|
-
_selector,
|
28
|
-
|
29
|
-
!expired? && ActiveSupport::SecurityUtils.secure_compare(
|
30
|
+
_selector, token = cookie.split(':')
|
31
|
+
cookie_hashed_token = Digest::SHA256.hexdigest token
|
32
|
+
!expired? && ActiveSupport::SecurityUtils.secure_compare(cookie_hashed_token, hashed_token)
|
30
33
|
end
|
31
|
-
|
34
|
+
|
32
35
|
def expired?
|
33
36
|
expires_at < Time.now
|
34
37
|
end
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
4
|
class Token < ApplicationRecord
|
3
|
-
belongs_to :authorio_user, class_name:
|
5
|
+
belongs_to :authorio_user, class_name: '::Authorio::User'
|
4
6
|
has_secure_token :auth_token
|
5
7
|
|
6
8
|
validates_presence_of :scope, :client
|
@@ -9,8 +11,20 @@ module Authorio
|
|
9
11
|
self.expires_at = Time.now + Authorio.configuration.token_expiration
|
10
12
|
end
|
11
13
|
|
14
|
+
# The token endpoint can get hit by bots, so short-circut the find if they
|
15
|
+
# don't send a bearer token
|
16
|
+
def self.find_by_auth_token(token)
|
17
|
+
token and find_by auth_token: token
|
18
|
+
end
|
19
|
+
|
12
20
|
def expired?
|
13
|
-
|
21
|
+
expires_at < Time.now
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.create_from_request(req)
|
25
|
+
raise Exceptions::InvalidGrant, 'missing scope' if req.scope.blank?
|
26
|
+
|
27
|
+
Token.create(authorio_user: req.authorio_user, scope: req.scope, client: req.client)
|
14
28
|
end
|
15
29
|
end
|
16
30
|
end
|
data/app/models/authorio/user.rb
CHANGED
@@ -1,10 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
4
|
class User < ApplicationRecord
|
3
5
|
has_secure_password
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
7
|
+
class << self
|
8
|
+
def find_by_url!(url)
|
9
|
+
find_by_username!(URI(url).path)
|
10
|
+
end
|
8
11
|
|
12
|
+
def find_by_username!(name)
|
13
|
+
return first unless Authorio.configuration.multiuser
|
14
|
+
|
15
|
+
find_by(username: name) or raise Exceptions::UserNotFound
|
16
|
+
end
|
17
|
+
end
|
9
18
|
end
|
10
19
|
end
|
@@ -7,9 +7,9 @@
|
|
7
7
|
<h3>Authorio</h3>
|
8
8
|
<div class="client-row">
|
9
9
|
<span class="client"><%= params[:client_id] %></span> wants to authenticate
|
10
|
-
<% if
|
10
|
+
<% if params[:scope] %>and also<% end %>
|
11
11
|
</div>
|
12
|
-
<%= render 'shared/login_form', target: authorize_user_path
|
12
|
+
<%= render 'shared/login_form', target: authorize_user_path, cancel: true %>
|
13
13
|
</div>
|
14
14
|
<div class="col-md-4"></div>
|
15
15
|
</div>
|
@@ -7,8 +7,7 @@
|
|
7
7
|
<div class="col-md-4 auth-panel">
|
8
8
|
<h3>Authorio</h3>
|
9
9
|
<div class="client-row">Local Login</div>
|
10
|
-
<%= render 'shared/login_form', target: session_path(@session),
|
11
|
-
user: @session.authorio_user, scopes: nil, cancel: false %>
|
10
|
+
<%= render 'shared/login_form', target: session_path(@session), cancel: false %>
|
12
11
|
</div>
|
13
12
|
<div class="col-md-4"></div>
|
14
13
|
</div>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
json.me profile_url(@auth_request.authorio_user)
|
4
|
+
if @auth_request.scope&.include? 'profile'
|
5
|
+
json.profile do
|
6
|
+
json.name(@auth_request.authorio_user.full_name)
|
7
|
+
json.call(@auth_request.authorio_user, :url, :photo)
|
8
|
+
json.email(@auth_request.authorio_user.email) if @auth_request.scope.include?('email')
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<%= stylesheet_link_tag "authorio/auth" %>
|
2
|
+
<% content_for :title, "User Account" %>
|
3
|
+
|
4
|
+
<%= indieauth_tag %>
|
5
|
+
|
6
|
+
<div class="container authorio-auth">
|
7
|
+
<div class="row">
|
8
|
+
<div class="col-md-4"></div>
|
9
|
+
<div class="col-md-4 auth-panel">
|
10
|
+
<h3>IndieAuth Account</h3>
|
11
|
+
Full Name <%= @user.full_name %>
|
12
|
+
URL <%= @user.url %>
|
13
|
+
Photo <%= image_tag @user.photo %>
|
14
|
+
Email <%= @user.email %>
|
15
|
+
</div>
|
16
|
+
<div class="col-md-4"></div>
|
17
|
+
</div>
|
18
|
+
</div>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= indieauth_tag %>
|
@@ -1,9 +1,9 @@
|
|
1
|
-
<%= form_with(
|
2
|
-
<% if
|
1
|
+
<%= form_with(url: target, method: :post) do |form| %>
|
2
|
+
<% if params[:scope] %>
|
3
3
|
<%= fields_for :scope do |req_scope| %>
|
4
4
|
<div class="scopes">
|
5
5
|
<ul class="scope">
|
6
|
-
<% for scope in
|
6
|
+
<% for scope in params[:scope].split %>
|
7
7
|
<li>
|
8
8
|
<%= label_tag(:scope, class: 'scope-label') do %>
|
9
9
|
<%= req_scope.check_box(:scope, {multiple: true, checked: true}, scope, nil) %>
|
@@ -15,22 +15,27 @@
|
|
15
15
|
</div>
|
16
16
|
<% end %>
|
17
17
|
<% end -%>
|
18
|
-
<%=
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
<%
|
24
|
-
|
25
|
-
|
26
|
-
|
18
|
+
<%= fields_for :user do |user_scope| %>
|
19
|
+
<%= user_scope.hidden_field :dummy, value: 42 %>
|
20
|
+
<% if Authorio.configuration.multiuser %>
|
21
|
+
<%= user_scope.label(:username, "Username") %>
|
22
|
+
<%= user_scope.text_field(:username) %>
|
23
|
+
<% end -%>
|
24
|
+
<% unless logged_in? %>
|
25
|
+
<%= user_scope.label(:password, "Password") %>
|
26
|
+
<%= user_scope.password_field(:password, autofocus: true) %>
|
27
|
+
<% if rememberable? %>
|
28
|
+
<%= label_tag(:remember_me, class: 'remember') do %>
|
29
|
+
<%= user_scope.check_box :remember_me %>
|
30
|
+
<span class='r-m'>Remember me for <%= distance_of_time_in_words Authorio.configuration.local_session_lifetime -%></span>
|
31
|
+
<% end %>
|
27
32
|
<% end %>
|
28
33
|
<% end %>
|
29
|
-
<% end
|
34
|
+
<% end -%>
|
30
35
|
<div class='auth-btn-row'>
|
36
|
+
<%= form.submit("Sign in", class: 'btn btn-success auth-btn') %>
|
31
37
|
<% if cancel %>
|
32
38
|
<%= form.submit("Cancel", class: 'btn btn-default auth-btn') %>
|
33
39
|
<% end %>
|
34
|
-
<%= form.submit("Sign in", class: 'btn btn-success auth-btn') %>
|
35
40
|
</div>
|
36
41
|
<% end %>
|
data/config/routes.rb
CHANGED
@@ -1,12 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
Authorio::Engine.routes.draw do
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
4
|
+
root to: 'authorio#index'
|
5
|
+
|
6
|
+
get Authorio.configuration.authorization_endpoint, controller: 'auth', action: 'authorization_interface'
|
7
|
+
resources :users, only: %i[show edit update]
|
8
|
+
post 'user/authorize', to: 'auth#authorize_user', as: 'authorize_user'
|
9
|
+
resource :session, only: %i[new create]
|
10
|
+
get 'session', to: 'sessions#destroy', as: 'logout'
|
11
|
+
get 'user/(:id)/verify', to: 'users#verify', as: 'verify_user'
|
12
|
+
defaults format: :json do
|
13
|
+
post Authorio.configuration.authorization_endpoint, controller: 'auth', action: 'send_profile'
|
14
|
+
get Authorio.configuration.token_endpoint, controller: 'auth', action: 'verify_token'
|
15
|
+
post Authorio.configuration.token_endpoint, controller: 'auth', action: 'issue_token'
|
16
|
+
end
|
17
|
+
end
|
@@ -1,15 +1,17 @@
|
|
1
|
-
|
2
|
-
class Configuration
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
|
3
|
+
module Authorio
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :authorization_endpoint, :token_endpoint, :mount_point, :token_expiration,
|
6
|
+
:local_session_lifetime, :multiuser
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
def initialize
|
9
|
+
@authorization_endpoint = 'auth'
|
10
|
+
@token_endpoint = 'token'
|
11
|
+
@mount_point = 'authorio'
|
12
|
+
@token_expiration = 4.weeks
|
13
|
+
@local_session_lifetime = nil
|
14
|
+
@multiuser = false
|
15
|
+
end
|
16
|
+
end
|
15
17
|
end
|
data/lib/authorio/engine.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
|
-
|
3
|
-
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace Authorio
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
initializer 'authorio.load_helpers' do
|
8
|
+
Rails.application.reloader.to_prepare do
|
9
|
+
ActionView::Base.send :include, Authorio::TagHelper
|
10
|
+
end
|
11
|
+
end
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
initializer 'authorio.assets.precompile' do |app|
|
14
|
+
app.config.assets.precompile << ['authorio/application.css', 'authorio/auth.css']
|
15
|
+
end
|
16
|
+
end
|
15
17
|
end
|
data/lib/authorio/exceptions.rb
CHANGED
@@ -1,14 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
4
|
+
module Exceptions
|
5
|
+
class InvalidGrant < RuntimeError; end
|
6
|
+
|
7
|
+
class InvalidPassword < RuntimeError; end
|
8
|
+
|
9
|
+
class SessionReplayAttack < StandardError
|
10
|
+
attr_accessor :session
|
11
|
+
|
12
|
+
def initialize(session)
|
13
|
+
super("Session replay attack on user account #{session.authorio_user.id}")
|
14
|
+
@session = session
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class UserNotFound < StandardError; end
|
19
|
+
|
20
|
+
class TokenExpired < StandardError; end
|
21
|
+
end
|
14
22
|
end
|
data/lib/authorio/routes.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
-
|
2
|
-
class Mapper
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
module ActionDispatch
|
4
|
+
module Routing
|
5
|
+
class Mapper
|
6
|
+
# Provide a custom mounting command, just so we can track our own mount point
|
7
|
+
def authorio_routes
|
8
|
+
mount Authorio::Engine, at: Authorio.configuration.mount_point
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
9
12
|
end
|
data/lib/authorio/version.rb
CHANGED
data/lib/authorio.rb
CHANGED
@@ -1,27 +1,21 @@
|
|
1
|
-
|
2
|
-
require "authorio/engine"
|
3
|
-
require "authorio/configuration"
|
4
|
-
require "authorio/routes"
|
5
|
-
require "authorio/exceptions"
|
1
|
+
# frozen_string_literal: true
|
6
2
|
|
7
|
-
|
8
|
-
class << self
|
9
|
-
attr_accessor :configuration, :authorization_path
|
10
|
-
end
|
3
|
+
Dir[File.join(__dir__, 'authorio', '*.rb')].sort.each { |f| require f }
|
11
4
|
|
12
|
-
|
13
|
-
|
14
|
-
|
5
|
+
module Authorio
|
6
|
+
def self.configuration
|
7
|
+
@configuration ||= Configuration.new
|
8
|
+
end
|
15
9
|
|
16
|
-
|
17
|
-
|
18
|
-
|
10
|
+
def self.configure
|
11
|
+
yield configuration
|
12
|
+
end
|
19
13
|
|
20
|
-
|
21
|
-
|
22
|
-
|
14
|
+
def self.authorization_path
|
15
|
+
[Authorio.configuration.mount_point, Authorio.configuration.authorization_endpoint].join('/')
|
16
|
+
end
|
23
17
|
|
24
|
-
|
25
|
-
|
26
|
-
|
18
|
+
def self.token_path
|
19
|
+
[Authorio.configuration.mount_point, Authorio.configuration.token_endpoint].join('/')
|
20
|
+
end
|
27
21
|
end
|
@@ -1,17 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
4
|
class InstallGenerator < Rails::Generators::Base
|
3
|
-
|
4
5
|
def self.source_paths
|
5
6
|
paths = []
|
6
7
|
paths << File.expand_path('../templates', "../../#{__FILE__}")
|
7
8
|
paths << File.expand_path('../templates', "../#{__FILE__}")
|
8
|
-
paths << File.expand_path('
|
9
|
+
paths << File.expand_path('templates', __dir__)
|
9
10
|
paths.flatten
|
10
11
|
end
|
11
12
|
|
12
13
|
def add_files
|
13
14
|
template 'authorio.rb', 'config/initializers/authorio.rb'
|
14
15
|
end
|
15
|
-
|
16
16
|
end
|
17
17
|
end
|
@@ -1,24 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Configuration for Authorio IndieAuth authentication
|
2
4
|
|
3
5
|
Authorio.configure do |config|
|
6
|
+
# Mount point for Authorio URLs. Typically you would call this in your routes.rb
|
7
|
+
# as mount Authorio::Engine, at: mount_point
|
8
|
+
# But Authorio needs to know its own mount point, so we define it here and use a custom mount command in the config
|
9
|
+
# config.mount_point = "authorio"
|
4
10
|
|
5
|
-
|
6
|
-
|
7
|
-
# But Authorio needs to know its own mount point, so we define it here and use a custom mount command in the config
|
8
|
-
# config.mount_point = "authorio"
|
11
|
+
# The path where clients will be redirected to provide authentication
|
12
|
+
# config.authorization_endpoint = "auth"
|
9
13
|
|
10
|
-
|
11
|
-
|
14
|
+
# The path for token requests
|
15
|
+
# config.token_endpoint = "token"
|
12
16
|
|
13
|
-
|
14
|
-
|
17
|
+
# Set to true to enable multiple user accounts. By default (in single user mode)
|
18
|
+
# there is only one user, and therefore you do not need to enter a username
|
19
|
+
# config.multiuser = false
|
15
20
|
|
16
|
-
|
17
|
-
|
21
|
+
# How long tokens will last before expiring
|
22
|
+
# config.token_expiration = 4.weeks
|
18
23
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
+
# Enable local session lifetime to keep yourself "logged in" to your own server
|
25
|
+
# If set to eg:
|
26
|
+
# config.local_session_lifetime = 30.days
|
27
|
+
# then you will only have to enter your password every 30 days. Default is off (nil)
|
28
|
+
# config.local_session_lifetime = nil
|
24
29
|
end
|
@@ -1,18 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
namespace :authorio do
|
2
|
-
|
3
|
-
|
4
|
+
desc 'Set password for initial Authorio user'
|
5
|
+
require 'io/console'
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
def input_no_echo(prompt)
|
8
|
+
print("\n#{prompt}")
|
9
|
+
$stdin.noecho(&:gets).chop
|
10
|
+
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
12
|
+
task password: :environment do
|
13
|
+
passwd = input_no_echo('Enter new password: ')
|
14
|
+
passwd_confirm = input_no_echo('Confirm password: ')
|
15
|
+
Authorio::User.create_with(password: passwd, password_confirmation: passwd_confirm)
|
16
|
+
.find_or_create_by!(profile_path: '/')
|
17
|
+
puts("\nPassword set")
|
18
|
+
end
|
18
19
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: authorio
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Meckler
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-05-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,20 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
20
|
-
- - ">="
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: 6.1.3.2
|
19
|
+
version: '7.0'
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
24
|
- - "~>"
|
28
25
|
- !ruby/object:Gem::Version
|
29
|
-
version:
|
30
|
-
- - ">="
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: 6.1.3.2
|
26
|
+
version: '7.0'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: bcrypt
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,6 +38,20 @@ dependencies:
|
|
44
38
|
- - "~>"
|
45
39
|
- !ruby/object:Gem::Version
|
46
40
|
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: jbuilder
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
47
55
|
- !ruby/object:Gem::Dependency
|
48
56
|
name: factory_bot_rails
|
49
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -125,8 +133,14 @@ files:
|
|
125
133
|
- app/models/authorio/token.rb
|
126
134
|
- app/models/authorio/user.rb
|
127
135
|
- app/views/authorio/auth/authorization_interface.html.erb
|
136
|
+
- app/views/authorio/auth/issue_token.json.jbuilder
|
137
|
+
- app/views/authorio/auth/send_profile.json.jbuilder
|
138
|
+
- app/views/authorio/auth/verify_token.json.jbuilder
|
128
139
|
- app/views/authorio/sessions/new.html.erb
|
140
|
+
- app/views/authorio/users/_profile.json.jbuilder
|
129
141
|
- app/views/authorio/users/edit.html.erb
|
142
|
+
- app/views/authorio/users/show.html.erb
|
143
|
+
- app/views/authorio/users/verify.html.erb
|
130
144
|
- app/views/layouts/authorio/main.html.erb
|
131
145
|
- app/views/shared/_login_form.html.erb
|
132
146
|
- config/routes.rb
|
@@ -136,6 +150,8 @@ files:
|
|
136
150
|
- db/migrate/20210723161041_add_expiry_to_tokens.rb
|
137
151
|
- db/migrate/20210726164625_create_authorio_sessions.rb
|
138
152
|
- db/migrate/20210801184120_add_profile_to_users.rb
|
153
|
+
- db/migrate/20210817010101_change_path_to_username_in_users.rb
|
154
|
+
- db/migrate/20210831155106_add_code_challenge_to_requests.rb
|
139
155
|
- lib/authorio.rb
|
140
156
|
- lib/authorio/configuration.rb
|
141
157
|
- lib/authorio/engine.rb
|
@@ -145,28 +161,29 @@ files:
|
|
145
161
|
- lib/generators/authorio/install/install_generator.rb
|
146
162
|
- lib/generators/authorio/install/templates/authorio.rb
|
147
163
|
- lib/tasks/authorio_tasks.rake
|
148
|
-
homepage:
|
164
|
+
homepage: https://blog.reiterate.app/tag/authorio/
|
149
165
|
licenses:
|
150
166
|
- MIT
|
151
167
|
metadata:
|
152
168
|
source_code_uri: https://github.com/reiterate-app/authorio
|
153
|
-
|
169
|
+
changelog_uri: https://github.com/reiterate-app/authorio/blob/master/CHANGELOG.md
|
170
|
+
post_install_message:
|
154
171
|
rdoc_options: []
|
155
172
|
require_paths:
|
156
173
|
- lib
|
157
174
|
required_ruby_version: !ruby/object:Gem::Requirement
|
158
175
|
requirements:
|
159
|
-
- - "
|
176
|
+
- - "~>"
|
160
177
|
- !ruby/object:Gem::Version
|
161
|
-
version: '0'
|
178
|
+
version: '3.0'
|
162
179
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
163
180
|
requirements:
|
164
181
|
- - ">="
|
165
182
|
- !ruby/object:Gem::Version
|
166
183
|
version: '0'
|
167
184
|
requirements: []
|
168
|
-
rubygems_version: 3.
|
169
|
-
signing_key:
|
185
|
+
rubygems_version: 3.3.10
|
186
|
+
signing_key:
|
170
187
|
specification_version: 4
|
171
188
|
summary: Indieauth Authentication endpoint for Rails
|
172
189
|
test_files: []
|