authorio 0.8.1 → 0.8.5
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/README.md +29 -0
- data/app/assets/stylesheets/authorio/auth.css +55 -1
- data/app/controllers/authorio/auth_controller.rb +69 -102
- data/app/controllers/authorio/authorio_controller.rb +78 -0
- data/app/controllers/authorio/sessions_controller.rb +33 -0
- data/app/controllers/authorio/users_controller.rb +34 -0
- data/app/helpers/authorio/tag_helper.rb +17 -0
- data/app/jobs/authorio/application_job.rb +2 -0
- data/app/models/authorio/application_record.rb +2 -0
- data/app/models/authorio/request.rb +48 -1
- data/app/models/authorio/session.rb +43 -0
- data/app/models/authorio/token.rb +23 -1
- data/app/models/authorio/user.rb +14 -0
- data/app/views/authorio/auth/authorization_interface.html.erb +14 -35
- 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 +14 -0
- data/app/views/authorio/users/_profile.json.jbuilder +10 -0
- data/app/views/authorio/users/edit.html.erb +25 -0
- data/app/views/authorio/users/show.html.erb +18 -0
- data/app/views/authorio/users/verify.html.erb +1 -0
- data/app/views/layouts/authorio/main.html.erb +38 -0
- data/app/views/shared/_login_form.html.erb +41 -0
- data/config/routes.rb +15 -5
- data/db/migrate/20210723161041_add_expiry_to_tokens.rb +5 -0
- data/db/migrate/20210726164625_create_authorio_sessions.rb +12 -0
- data/db/migrate/20210801184120_add_profile_to_users.rb +8 -0
- 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 -9
- data/lib/authorio/engine.rb +11 -8
- data/lib/authorio/exceptions.rb +20 -3
- 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 +22 -8
- data/lib/tasks/authorio_tasks.rake +15 -14
- metadata +49 -20
- data/app/controllers/authorio/application_controller.rb +0 -4
- data/app/controllers/authorio/helpers.rb +0 -17
- data/app/helpers/authorio/application_helper.rb +0 -4
- data/app/helpers/authorio/test_helper.rb +0 -4
- data/app/views/layouts/authorio/application.html.erb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e4cd85ad8ec9b0e70f4d276f7c600d5cc3ebab5b04f0f0313da9f6151e78d680
|
4
|
+
data.tar.gz: 7ec85565cc8fb4ea711836ab8069f7062929d13af6f1520809095bdee7c859b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 845bc518d44332a7e71eabd020da0f450979dcf35ec35539850ee40b931c270c7c89f6205fa021b27b78b86c07fbcd1b36354c49c79c28d6e5d2b8ea501aad00
|
7
|
+
data.tar.gz: ca1f67ba7804bd630ce55713cfd1c3f2b542f04781583ec165073c1ad0eea829bef439d362af9e302e530da8ccc650736b00039b1151e7559b1cb8b8cdcc64b2
|
data/README.md
CHANGED
@@ -106,11 +106,40 @@ will be logged in!
|
|
106
106
|
When you installed Authorio it placed a config file in `config/initializers/authorio.rb`. If you want to change
|
107
107
|
one of the defaults you can uncomment it and specify it here.
|
108
108
|
|
109
|
+
### Mount Point
|
110
|
+
|
111
|
+
Most Rails engines are mounted via `mount Authorio::Engine, at: mount_point`. But Authorio needs to know its own
|
112
|
+
mount point (to specify its url in the header tag) so you specify the mount point here. The default `authorio`
|
113
|
+
should work for everyone.
|
114
|
+
|
115
|
+
### Authorization and Token Endpoint
|
116
|
+
|
117
|
+
These endpointd are given to servers via discovery. The default values should suffice.
|
118
|
+
|
119
|
+
### Token Expiration
|
120
|
+
|
121
|
+
If a client asks for an authentication token, the token will be valid for this length of time, after which
|
122
|
+
you will have to re-authenticate. Longer-lasting
|
123
|
+
tokens can possibly be a security risk. Default is 4 weeks.
|
124
|
+
|
125
|
+
### Local Session Lifetime
|
126
|
+
|
127
|
+
Setting this to a time interval will enable you to authenticate without typing in your password. It enables a
|
128
|
+
"remember me" chekbox on the authentication form. If you check that, then enter your
|
129
|
+
password once, then your session will be saved in a cookie, and any time you are asked to authenticate again,
|
130
|
+
you can just click "Sign In" without your password. It can be a security risk if someone else has access to
|
131
|
+
the machine you are using to login with (eg your laptop). Obviously you don't want to check "remember me"
|
132
|
+
on a public-access computer. Default is *nil* (disabled)
|
133
|
+
|
109
134
|
### TODO
|
110
135
|
|
111
136
|
- [ ] Customizing the authentication view/UI
|
112
137
|
- [ ] Customizing the authentication method
|
113
138
|
|
139
|
+
## User Profile
|
140
|
+
|
141
|
+
You can set up your <a href="doc/profile.md">user profile</a> which can be sent to authenticating clients.
|
142
|
+
|
114
143
|
## Contributing
|
115
144
|
Send pull requests to [Authorio on GitHub](https://github.com/reiterate-app/authorio)
|
116
145
|
|
@@ -27,7 +27,7 @@ div.authorio-auth {
|
|
27
27
|
margin-left: 1.2em;
|
28
28
|
}
|
29
29
|
|
30
|
-
.authorio-auth input {
|
30
|
+
.authorio-auth input:not([type='checkbox']):not([type='submit']) {
|
31
31
|
margin: 0 1em 1em 1em;
|
32
32
|
width: 90%;
|
33
33
|
}
|
@@ -39,3 +39,57 @@ div.authorio-auth {
|
|
39
39
|
.authorio-auth input.btn {
|
40
40
|
margin-top: 2em;
|
41
41
|
}
|
42
|
+
|
43
|
+
.auth-btn-row {
|
44
|
+
margin: 1em;
|
45
|
+
width: 90%;
|
46
|
+
}
|
47
|
+
|
48
|
+
.authorio-auth .auth-btn-row .auth-btn {
|
49
|
+
width: 45%;
|
50
|
+
margin: 0.1em;
|
51
|
+
}
|
52
|
+
|
53
|
+
.authorio-auth .auth-btn-row .btn-success {
|
54
|
+
float: right;
|
55
|
+
}
|
56
|
+
|
57
|
+
span.r-m {
|
58
|
+
font-weight: 200;
|
59
|
+
}
|
60
|
+
|
61
|
+
label.remember {
|
62
|
+
margin-top: -1em;
|
63
|
+
}
|
64
|
+
|
65
|
+
div.scopes {
|
66
|
+
margin-top: -1.5em;
|
67
|
+
}
|
68
|
+
|
69
|
+
ul.scope {
|
70
|
+
list-style: none;
|
71
|
+
padding-left: 20px;
|
72
|
+
}
|
73
|
+
|
74
|
+
ul.scope li label {
|
75
|
+
font-weight: normal;
|
76
|
+
}
|
77
|
+
|
78
|
+
div.topbar {
|
79
|
+
border-bottom: 1px solid darkgray;
|
80
|
+
}
|
81
|
+
|
82
|
+
div.topbar li {
|
83
|
+
display: inline-block;
|
84
|
+
padding: 12px;
|
85
|
+
}
|
86
|
+
|
87
|
+
div.topbar ul {
|
88
|
+
margin: 0 10px;
|
89
|
+
padding: 0;
|
90
|
+
text-align: right;
|
91
|
+
}
|
92
|
+
|
93
|
+
div.topbar li:first-child {
|
94
|
+
float: left;
|
95
|
+
}
|
@@ -1,141 +1,108 @@
|
|
1
|
-
|
2
|
-
class AuthController < ActionController::Base
|
3
|
-
require 'uri'
|
4
|
-
require 'digest'
|
1
|
+
# frozen_string_literal: true
|
5
2
|
|
6
|
-
|
3
|
+
module Authorio
|
4
|
+
class AuthController < AuthorioController
|
5
|
+
# These API-only endpoints are protected by code challenge and do not need CSRF protextion
|
6
|
+
protect_from_forgery with: :exception, except: %i[send_profile issue_token]
|
7
|
+
|
8
|
+
rescue_from 'Authorio::Exceptions::SessionReplayAttack' do |exception|
|
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.'
|
18
|
+
end
|
7
19
|
|
20
|
+
# GET /auth
|
8
21
|
def authorization_interface
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
user = User.find_by! profile_path: URI(p[:me]).path
|
13
|
-
@user_url = p[:me] || user_url(user)
|
14
|
-
|
15
|
-
# If there are any old requests from this (client, user), delete them now
|
16
|
-
Request.where(authorio_user: user, client: p[:client_id]).delete_all
|
17
|
-
|
18
|
-
auth_request = Request.new.tap do |req|
|
19
|
-
req.code = SecureRandom.hex(20)
|
20
|
-
req.redirect_uri = p[:redirect_uri]
|
21
|
-
req.client = p[:client_id] # IndieAuth client_id conflicts with Rails' _id foreign key convention
|
22
|
-
req.scope = p[:scope]
|
23
|
-
req.authorio_user = user
|
24
|
-
end
|
25
|
-
auth_request.save
|
26
|
-
session[:state] = p[:state]
|
27
|
-
session[:code_challenge] = p[:code_challenge]
|
28
|
-
|
29
|
-
rescue ActiveRecord::RecordNotFound
|
30
|
-
flash.now[:alert] = "Invalid user"
|
31
|
-
redirect_back fallback_location: Authorio.authorization_path, allow_other_host: false
|
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
|
32
25
|
end
|
33
26
|
|
27
|
+
# POST /user/:id/authorize
|
34
28
|
def authorize_user
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
if
|
39
|
-
|
40
|
-
|
41
|
-
else
|
42
|
-
flash.now[:alert] = "Incorrect password. Try again."
|
43
|
-
redirect_back fallback_location: Authorio.authorization_path, allow_other_host: false
|
44
|
-
end
|
45
|
-
rescue ActiveRecord::RecordNotFound
|
46
|
-
flash.now[:alert] = "Invlaid user"
|
47
|
-
redirect_back fallback_location: Authorio.authorization_path, allow_other_host: false
|
29
|
+
redirect_to session[:client_id] and return if params[:commit] == 'Cancel'
|
30
|
+
|
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
|
48
35
|
end
|
49
36
|
|
50
37
|
def send_profile
|
51
|
-
|
52
|
-
render json: { 'me': user_url(validate_request.authorio_user) }
|
53
|
-
rescue Authorio::Exceptions::InvalidGrant
|
54
|
-
render invalid_grant
|
55
|
-
end
|
38
|
+
@auth_request = find_auth_request or (render validation_failed and return)
|
56
39
|
end
|
57
40
|
|
58
41
|
def issue_token
|
59
|
-
|
60
|
-
|
61
|
-
raise Authorio::Exceptions::InvalidGrant.new if req.scope.blank?
|
62
|
-
token = Token.create(authorio_user: req.authorio_user, scope: req.scope, client: req.client)
|
63
|
-
render json: {
|
64
|
-
'me': user_url(req.authorio_user),
|
65
|
-
'access_token': token.auth_token,
|
66
|
-
'scope': req.scope,
|
67
|
-
'token_type': 'Bearer'
|
68
|
-
}
|
69
|
-
rescue Authorio::Exceptions::InvalidGrant
|
70
|
-
render invalid_grant
|
71
|
-
end
|
42
|
+
@auth_request = find_auth_request or (render validation_failed and return)
|
43
|
+
@token = Token.create_from_request(@auth_request)
|
72
44
|
end
|
73
45
|
|
74
46
|
def verify_token
|
75
|
-
token = Token.
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
}
|
81
|
-
rescue ActiveRecord::RecordNotFound
|
82
|
-
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
|
83
52
|
end
|
84
53
|
|
85
54
|
private
|
86
55
|
|
87
|
-
def
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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!
|
92
63
|
end
|
93
|
-
params.permit(:response_type, :code_challenge, :code_challenge_method, :scope, :me, :redirect_uri, :client_id, :state)
|
94
64
|
end
|
95
65
|
|
96
|
-
def
|
97
|
-
params.permit(:
|
66
|
+
def scope_params
|
67
|
+
params.require(:scope).permit(scope: [])
|
98
68
|
end
|
99
69
|
|
100
|
-
def
|
101
|
-
|
70
|
+
def oauth_error(error, message = nil, status = :bad_request)
|
71
|
+
{ json: { json: { error: error, error_message: message }.compact },
|
72
|
+
status: status }
|
102
73
|
end
|
103
74
|
|
104
|
-
def
|
105
|
-
|
75
|
+
def token_expired
|
76
|
+
oauth_error('invalid_token', 'The access token has expired', :unauthorized)
|
106
77
|
end
|
107
78
|
|
108
|
-
def
|
109
|
-
|
79
|
+
def validation_failed
|
80
|
+
oauth_error('invalid_grant', 'validation failed')
|
110
81
|
end
|
111
82
|
|
112
|
-
def
|
113
|
-
|
114
|
-
|
115
|
-
sha256 = Digest::SHA256.hexdigest params[:code_verifier]
|
116
|
-
base64 = Base64.urlsafe_encode64 sha256
|
117
|
-
return base64 != session[:code_challenge]
|
83
|
+
def find_auth_request
|
84
|
+
auth_request = Request.find_by code: params[:code]
|
85
|
+
auth_request&.validate_oauth params
|
118
86
|
end
|
119
87
|
|
120
|
-
def
|
121
|
-
|
122
|
-
|
123
|
-
|
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)
|
124
94
|
end
|
125
95
|
|
126
|
-
def
|
127
|
-
|
128
|
-
|
129
|
-
req.delete
|
130
|
-
raise Authorio::Exceptions::InvalidGrant.new if invalid_request?(req) || code_challenge_failed?
|
131
|
-
req
|
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}"
|
132
99
|
end
|
133
100
|
|
134
|
-
def
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
138
106
|
end
|
139
|
-
|
140
107
|
end
|
141
108
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authorio
|
4
|
+
class AuthorioController < ActionController::Base
|
5
|
+
layout 'authorio/main'
|
6
|
+
|
7
|
+
helper_method :logged_in?, :rememberable?, :current_user,
|
8
|
+
:user_scope_description, :profile_url
|
9
|
+
|
10
|
+
def index
|
11
|
+
if logged_in?
|
12
|
+
redirect_to edit_user_path(current_user)
|
13
|
+
else
|
14
|
+
redirect_to new_session_path
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def user_session
|
19
|
+
if session[:user_id]
|
20
|
+
Session.new(authorio_user: Authorio::User.find(session[:user_id]))
|
21
|
+
else
|
22
|
+
cookie = cookies.encrypted[:user] and Session.find_by_cookie(cookie)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def logged_in?
|
27
|
+
!user_session.nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def rememberable?
|
31
|
+
!logged_in? && Authorio.configuration.local_session_lifetime
|
32
|
+
end
|
33
|
+
|
34
|
+
def authorized?
|
35
|
+
redirect_to new_session_path unless logged_in?
|
36
|
+
end
|
37
|
+
|
38
|
+
def current_user
|
39
|
+
user_session&.authorio_user&.id
|
40
|
+
end
|
41
|
+
|
42
|
+
def user_scope_description(scope)
|
43
|
+
Authorio::Request.user_scope_description(scope)
|
44
|
+
end
|
45
|
+
|
46
|
+
def profile_url(user)
|
47
|
+
if Authorio.configuration.multiuser
|
48
|
+
verify_user_url(user)
|
49
|
+
else
|
50
|
+
"#{request.scheme}://#{request.host}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def auth_user_params
|
57
|
+
params.require(:user).permit(:username, :password, :remember_me)
|
58
|
+
end
|
59
|
+
|
60
|
+
def write_session_cookie(user)
|
61
|
+
cookies.encrypted[:user] = {
|
62
|
+
value: Authorio::Session.create(authorio_user: user).as_cookie,
|
63
|
+
expires: Authorio.configuration.local_session_lifetime
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def redirect_back_with_error(error)
|
68
|
+
flash[:alert] = error
|
69
|
+
redirect_back fallback_location: Authorio.authorization_path.prepend('/'), allow_other_host: false
|
70
|
+
end
|
71
|
+
|
72
|
+
def bearer_token
|
73
|
+
bearer = /^Bearer /
|
74
|
+
header = request.headers['Authorization']
|
75
|
+
header.gsub(bearer, '') if header&.match(bearer)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authorio
|
4
|
+
class SessionsController < AuthorioController
|
5
|
+
# GET /session/new
|
6
|
+
def new
|
7
|
+
@session = Session.new(authorio_user: User.first)
|
8
|
+
end
|
9
|
+
|
10
|
+
# POST /session
|
11
|
+
def create
|
12
|
+
user = User.find_by_username! auth_user_params[:username]
|
13
|
+
raise Exceptions::InvalidPassword unless user.authenticate(auth_user_params[:password])
|
14
|
+
|
15
|
+
write_session_cookie(user) if auth_user_params[:remember_me]
|
16
|
+
# Even if we don't have a permanent remember-me session, we make a temporary session
|
17
|
+
session[:user_id] = user.id
|
18
|
+
redirect_to edit_user_path(user)
|
19
|
+
rescue Exceptions::InvalidPassword
|
20
|
+
redirect_back_with_error 'Incorrect password. Try again.'
|
21
|
+
end
|
22
|
+
|
23
|
+
# DELETE /session
|
24
|
+
def destroy
|
25
|
+
reset_session
|
26
|
+
if (cookie = cookies.encrypted[:user]) && (session = Session.find_by_cookie(cookie))
|
27
|
+
cookies.delete :user
|
28
|
+
session.destroy
|
29
|
+
end
|
30
|
+
redirect_to new_session_path
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authorio
|
4
|
+
class UsersController < AuthorioController
|
5
|
+
before_action :authorized?, except: :verify
|
6
|
+
|
7
|
+
# GET /users/:id
|
8
|
+
def show
|
9
|
+
@user = User.find(params[:id])
|
10
|
+
end
|
11
|
+
|
12
|
+
# GET /users/:id/edit
|
13
|
+
def edit
|
14
|
+
@user = User.find(params[:id])
|
15
|
+
end
|
16
|
+
|
17
|
+
# PATCH /users/:id
|
18
|
+
def update
|
19
|
+
User.find(params[:id]).update(user_params)
|
20
|
+
flash[:info] = 'Profile Saved'
|
21
|
+
redirect_to edit_user_path
|
22
|
+
end
|
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
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def user_params
|
31
|
+
params.require(:user).permit(:url, :photo, :full_name, :email)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|