authorio 0.8.3 → 0.8.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/authorio/auth_controller.rb +67 -129
- 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 +37 -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 +18 -13
- data/config/routes.rb +16 -11
- data/db/migrate/20210817010101_change_path_to_username_in_users.rb +7 -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 +24 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 78659edadab6ff85d24828c318525bac7b62b4a76b9905e0a1d819ec266f9d46
|
4
|
+
data.tar.gz: 817fe77d1c1fd89e68df07a594725997d598e0f0027210d083762da50cb447fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8205bfe7bc6798f560da3f56b19f26822653d6f6fb8baac925be38a713f871e1faf90f0492ea25187124cd98db928a7d875da47b53faf14afe1efc6418498dbd
|
7
|
+
data.tar.gz: 3c772c7777f016316911f36573f5d2982ca27a40edb18dcac16782fb380021971feaeea4271adebe411101b39a9bb544d3f27f88dec93d72aae6f0e5adc483fd
|
@@ -1,181 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
4
|
class AuthController < AuthorioController
|
3
5
|
require 'uri'
|
4
6
|
require 'digest'
|
5
7
|
|
6
8
|
# These API-only endpoints are protected by code challenge and do not need CSRF protextion
|
7
|
-
protect_from_forgery with: :exception, except: [
|
9
|
+
protect_from_forgery with: :exception, except: %i[send_profile issue_token]
|
8
10
|
|
9
11
|
rescue_from 'Authorio::Exceptions::SessionReplayAttack' do |exception|
|
10
|
-
redirect_back_with_error
|
11
|
-
logger.info
|
12
|
-
|
12
|
+
redirect_back_with_error 'Session Replay attack detected. This has been logged.'
|
13
|
+
logger.info 'Session replay attack detected!'
|
14
|
+
Session.where(user: exception.session.user).delete_all
|
15
|
+
end
|
16
|
+
rescue_from 'Authorio::Exceptions::UserNotFound' do
|
17
|
+
redirect_back_with_error 'User not found'
|
18
|
+
end
|
19
|
+
rescue_from 'Authorio::Exceptions::InvalidPassword' do
|
20
|
+
redirect_back_with_error 'Incorrect password. Try again.'
|
13
21
|
end
|
14
|
-
|
15
|
-
helper_method :user_scope_description
|
16
22
|
|
17
23
|
# GET /auth
|
18
24
|
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}"
|
25
|
+
session.update auth_interface_params.slice(:state, :client_id, :code_challenge, :redirect_uri)
|
26
|
+
rescue ActionController::ParameterMissing, ActionController::UnpermittedParameters => e
|
27
|
+
render oauth_error 'invalid_request', e
|
41
28
|
end
|
42
29
|
|
43
30
|
# POST /user/:id/authorize
|
44
31
|
def authorize_user
|
45
|
-
redirect_to session[:client_id] and return if params[:commit] ==
|
32
|
+
redirect_to session[:client_id] and return if params[:commit] == 'Cancel'
|
46
33
|
|
47
34
|
user = authenticate_user_from_session_or_password
|
48
|
-
|
49
|
-
|
50
|
-
auth_req = Request.find_by! client: session[:client_id], authorio_user: user
|
51
|
-
auth_req.update_scope(scope_params[:scope]) if params.has_key? :scope
|
52
|
-
redirect_params = { code: auth_req.code, state: session[:state] }
|
53
|
-
redirect_to "#{auth_req.redirect_uri}?#{redirect_params.to_query}"
|
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."
|
35
|
+
write_session_cookie(user) if auth_user_params[:remember_me]
|
36
|
+
redirect_to_client(user)
|
58
37
|
end
|
59
38
|
|
60
39
|
def send_profile
|
61
|
-
request = validate_request
|
62
|
-
|
63
|
-
|
64
|
-
render oauth_error 'invalid_grant', error.message
|
40
|
+
@request = validate_request Request.find_by! code: params[:code]
|
41
|
+
rescue Exceptions::InvalidGrant, ActiveRecord::RecordNotFound => e
|
42
|
+
render oauth_error 'invalid_grant', e.message
|
65
43
|
end
|
66
44
|
|
67
45
|
def issue_token
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
render
|
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
|
46
|
+
@request = validate_request Request.find_by! code: params[:code]
|
47
|
+
@token = Token.create_from_request(@request)
|
48
|
+
rescue Exceptions::InvalidGrant, ActiveRecord::RecordNotFound => e
|
49
|
+
render oauth_error 'invalid_grant', e.message
|
79
50
|
end
|
80
51
|
|
81
52
|
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
|
53
|
+
@token = Token.find_by_auth_token(bearer_token) or (head :bad_request and return)
|
54
|
+
return unless @token.expired?
|
55
|
+
|
56
|
+
@token.delete
|
57
|
+
render token_expired
|
95
58
|
end
|
96
59
|
|
97
60
|
private
|
98
61
|
|
62
|
+
def auth_interface_params
|
63
|
+
@auth_interface_params ||= begin
|
64
|
+
required = %w[client_id redirect_uri state code_challenge]
|
65
|
+
permitted = %w[me scope code_challenge_method response_type action controller]
|
66
|
+
missing = required - params.keys
|
67
|
+
raise ::ActionController::ParameterMissing, missing unless missing.empty?
|
68
|
+
|
69
|
+
unpermitted = params.keys - required - permitted
|
70
|
+
raise ::ActionController::UnpermittedParameters, unpermitted unless unpermitted.empty?
|
71
|
+
|
72
|
+
params.permit!
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
99
76
|
def scope_params
|
100
77
|
params.require(:scope).permit(scope: [])
|
101
78
|
end
|
102
79
|
|
103
|
-
def oauth_error(error, message=nil)
|
104
|
-
|
105
|
-
|
106
|
-
{ json: resp, status: :bad_request }
|
80
|
+
def oauth_error(error, message = nil, status = :bad_request)
|
81
|
+
{ json: { json: { error: error, error_message: message }.compact },
|
82
|
+
status: status }
|
107
83
|
end
|
108
84
|
|
109
85
|
def token_expired
|
110
|
-
|
86
|
+
oauth_error('invalid_token', 'The access token has expired', :unauthorized)
|
111
87
|
end
|
112
88
|
|
113
89
|
def code_challenge_failed?
|
114
90
|
# For now, if original request did not have code challenge, then we pass by default
|
115
|
-
return
|
91
|
+
return unless session[:code_challenge]
|
92
|
+
|
116
93
|
sha256 = Digest::SHA256.hexdigest params[:code_verifier]
|
117
|
-
|
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
|
94
|
+
Base64.urlsafe_encode64(sha256) != session[:code_challenge]
|
151
95
|
end
|
152
96
|
|
153
|
-
def
|
154
|
-
|
155
|
-
header = request.headers['Authorization']
|
156
|
-
header.gsub(bearer, '') if header && header.match(bearer)
|
157
|
-
end
|
97
|
+
def validate_request(request)
|
98
|
+
raise Exceptions::InvalidGrant, 'validation failed' if request.invalid?(params) || code_challenge_failed?
|
158
99
|
|
159
|
-
|
160
|
-
session = user_session
|
161
|
-
if session
|
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
|
100
|
+
request
|
168
101
|
end
|
169
102
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
ScopeDescriptions.dig(scope.to_sym) || scope
|
103
|
+
def redirect_to_client(user)
|
104
|
+
auth_req = Request.create(client: session[:client_id],
|
105
|
+
redirect_uri: session[:redirect_uri],
|
106
|
+
scope: (scope_params[:scope].join(' ') if params.key? :scope),
|
107
|
+
authorio_user: user)
|
108
|
+
redirect_params = { code: auth_req.code, state: session[:state] }
|
109
|
+
redirect_to "#{auth_req.redirect_uri}?#{redirect_params.to_query}"
|
178
110
|
end
|
179
111
|
|
112
|
+
def authenticate_user_from_session_or_password
|
113
|
+
user_session&.authorio_user or
|
114
|
+
User.find_by_username!(auth_user_params[:username])
|
115
|
+
.authenticate(auth_user_params[:password]) or
|
116
|
+
raise Exceptions::InvalidPassword
|
117
|
+
end
|
180
118
|
end
|
181
119
|
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,45 @@
|
|
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 invalid?(params)
|
20
|
+
redirect_uri != params[:redirect_uri] ||
|
21
|
+
client != params[:client_id] ||
|
22
|
+
created_at < Time.now - 10.minutes
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.user_scope_description(scope)
|
26
|
+
USER_SCOPE_DESCRIPTION[scope.to_sym] || scope
|
10
27
|
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def set_code
|
32
|
+
self.code = SecureRandom.hex(20)
|
33
|
+
end
|
34
|
+
|
35
|
+
def sweep_requests
|
36
|
+
Request.where(client: client, authorio_user: authorio_user).destroy_all
|
37
|
+
end
|
38
|
+
|
39
|
+
USER_SCOPE_DESCRIPTION = {
|
40
|
+
profile: 'View basic profile information',
|
41
|
+
email: 'View your email address',
|
42
|
+
offline_access: 'Keep you logged in permanently (until revoked)'
|
43
|
+
}.freeze
|
11
44
|
end
|
12
45
|
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(request.authorio_user)
|
4
|
+
if request.scope&.include? 'profile'
|
5
|
+
json.profile do
|
6
|
+
json.name(request.authorio_user.full_name)
|
7
|
+
json.call(request.authorio_user, :url, :photo)
|
8
|
+
json.email(request.authorio_user.email) if 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,18 +15,23 @@
|
|
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'>
|
31
36
|
<% if cancel %>
|
32
37
|
<%= form.submit("Cancel", class: 'btn btn-default auth-btn') %>
|
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 += %w[authorio/auth.css authorio/application.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.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Meckler
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-08-
|
11
|
+
date: 2021-08-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -44,6 +44,20 @@ dependencies:
|
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '3.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: jbuilder
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.0'
|
47
61
|
- !ruby/object:Gem::Dependency
|
48
62
|
name: factory_bot_rails
|
49
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -125,8 +139,14 @@ files:
|
|
125
139
|
- app/models/authorio/token.rb
|
126
140
|
- app/models/authorio/user.rb
|
127
141
|
- app/views/authorio/auth/authorization_interface.html.erb
|
142
|
+
- app/views/authorio/auth/issue_token.json.jbuilder
|
143
|
+
- app/views/authorio/auth/send_profile.json.jbuilder
|
144
|
+
- app/views/authorio/auth/verify_token.json.jbuilder
|
128
145
|
- app/views/authorio/sessions/new.html.erb
|
146
|
+
- app/views/authorio/users/_profile.json.jbuilder
|
129
147
|
- app/views/authorio/users/edit.html.erb
|
148
|
+
- app/views/authorio/users/show.html.erb
|
149
|
+
- app/views/authorio/users/verify.html.erb
|
130
150
|
- app/views/layouts/authorio/main.html.erb
|
131
151
|
- app/views/shared/_login_form.html.erb
|
132
152
|
- config/routes.rb
|
@@ -136,6 +156,7 @@ files:
|
|
136
156
|
- db/migrate/20210723161041_add_expiry_to_tokens.rb
|
137
157
|
- db/migrate/20210726164625_create_authorio_sessions.rb
|
138
158
|
- db/migrate/20210801184120_add_profile_to_users.rb
|
159
|
+
- db/migrate/20210817010101_change_path_to_username_in_users.rb
|
139
160
|
- lib/authorio.rb
|
140
161
|
- lib/authorio/configuration.rb
|
141
162
|
- lib/authorio/engine.rb
|
@@ -150,6 +171,7 @@ licenses:
|
|
150
171
|
- MIT
|
151
172
|
metadata:
|
152
173
|
source_code_uri: https://github.com/reiterate-app/authorio
|
174
|
+
changelog_uri: https://github.com/reiterate-app/authorio/blob/master/CHANGELOG.md
|
153
175
|
post_install_message:
|
154
176
|
rdoc_options: []
|
155
177
|
require_paths:
|