authorio 0.8.1 → 0.8.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|