authorio 0.8.3 → 0.8.7
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|