authorio 0.8.0 → 0.8.4
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 +52 -4
- data/app/assets/stylesheets/authorio/auth.css +55 -1
- data/app/controllers/authorio/auth_controller.rb +76 -91
- 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 +39 -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/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 +58 -30
- 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: 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
|
data/README.md
CHANGED
@@ -34,13 +34,18 @@ You will need to install the migrations and then run them to add these tables
|
|
34
34
|
$ rails authorio:install:migrations
|
35
35
|
Copied migration 20210703002653_create_authorio_users.authorio.rb from authorio
|
36
36
|
Copied migration 20210703002654_create_authorio_requests.authorio.rb from authorio
|
37
|
+
Copied migration 20210710145519_create_authorio_tokens.authorio.rb from authorio
|
38
|
+
|
37
39
|
$ rails db:migrate
|
38
40
|
...
|
39
41
|
== 20210703002653 CreateAuthorioUsers: migrated (0.0038s) =====================
|
40
42
|
...
|
41
43
|
== 20210703002654 CreateAuthorioRequests: migrated (0.0041s) ==================
|
44
|
+
...
|
45
|
+
== 20210710145519 CreateAuthorioTokens: migrated (0.0037s) ====================
|
42
46
|
```
|
43
47
|
|
48
|
+
|
44
49
|
### 4. Install Authorio routes
|
45
50
|
Add the following line somewhere inside the `Rails.application.routes.draw do` block in your `config/routes.rb` file
|
46
51
|
```ruby
|
@@ -83,15 +88,58 @@ Now restart your rails app, and you should be all set!
|
|
83
88
|
|
84
89
|
## Usage
|
85
90
|
|
86
|
-
To test your authentication endpoint, find an IndieAuth client you can log in to. A simple test is
|
91
|
+
To test your authentication endpoint, find an IndieAuth client you can log in to. A simple test is to try and login
|
92
|
+
to the [IndieWeb.org website](https://indieweb.org)
|
87
93
|
|
88
|
-
|
89
|
-
|
94
|
+
- From the home page, click on *Log In* in the upper right, or visit the [login page](https://sso.indieweb.org/login?url=https%3A%2F%2Findieweb.org%2FMain_Page) directly.
|
95
|
+
- Enter your site's URL (or if you put the indieauth tag on a page other than your home page, enter that URL)
|
96
|
+
- You should be then be redirected back to your own site and the Authorio login UI
|
97
|
+
<p align="center">
|
90
98
|
<img src="./auth-ui.png" width="400">
|
99
|
+
</p>
|
91
100
|
|
92
|
-
Enter the password you set up when you installed Authorio. This should redirect you back to the client where you
|
101
|
+
- Enter the password you set up when you installed Authorio. This should redirect you back to the client where you
|
93
102
|
will be logged in!
|
94
103
|
|
104
|
+
## Configuration
|
105
|
+
|
106
|
+
When you installed Authorio it placed a config file in `config/initializers/authorio.rb`. If you want to change
|
107
|
+
one of the defaults you can uncomment it and specify it here.
|
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
|
+
|
134
|
+
### TODO
|
135
|
+
|
136
|
+
- [ ] Customizing the authentication view/UI
|
137
|
+
- [ ] Customizing the authentication method
|
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
|
+
|
95
143
|
## Contributing
|
96
144
|
Send pull requests to [Authorio on GitHub](https://github.com/reiterate-app/authorio)
|
97
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 .auth-btn:last-child {
|
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,134 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Authorio
|
2
|
-
class AuthController <
|
4
|
+
class AuthController < AuthorioController
|
3
5
|
require 'uri'
|
4
6
|
require 'digest'
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
def authorization_interface
|
9
|
-
p = auth_req_params
|
10
|
-
|
11
|
-
path = if p[:me]
|
12
|
-
URI(p[:me]).path
|
13
|
-
else
|
14
|
-
'/'
|
15
|
-
end
|
16
|
-
|
17
|
-
user = User.find_by! profile_path: path
|
18
|
-
@user_url = p[:me] || user_url(user)
|
8
|
+
# These API-only endpoints are protected by code challenge and do not need CSRF protextion
|
9
|
+
protect_from_forgery with: :exception, except: %i[send_profile issue_token]
|
19
10
|
|
20
|
-
|
21
|
-
|
11
|
+
rescue_from 'Authorio::Exceptions::SessionReplayAttack' do |exception|
|
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.'
|
21
|
+
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
req.authorio_user = user
|
29
|
-
end
|
30
|
-
auth_request.save
|
31
|
-
session[:state] = p[:state]
|
32
|
-
session[:code_challenge] = p[:code_challenge]
|
23
|
+
# GET /auth
|
24
|
+
def authorization_interface
|
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
|
33
28
|
end
|
34
29
|
|
30
|
+
# POST /user/:id/authorize
|
35
31
|
def authorize_user
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
if
|
40
|
-
|
41
|
-
redirect_to "#{auth_req.redirect_uri}?#{params.to_query}"
|
42
|
-
else
|
43
|
-
flash.now[:alert] = "Incorrect password. Try again."
|
44
|
-
redirect_back fallback_location: Authorio.authorization_path, allow_other_host: false
|
45
|
-
end
|
32
|
+
redirect_to session[:client_id] and return if params[:commit] == 'Cancel'
|
33
|
+
|
34
|
+
user = authenticate_user_from_session_or_password
|
35
|
+
write_session_cookie(user) if auth_user_params[:remember_me]
|
36
|
+
redirect_to_client(user)
|
46
37
|
end
|
47
38
|
|
48
39
|
def send_profile
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
render invalid_grant
|
53
|
-
end
|
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
|
54
43
|
end
|
55
44
|
|
56
45
|
def issue_token
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
render json: {
|
62
|
-
'me': user_url(req.authorio_user),
|
63
|
-
'access_token': token.auth_token,
|
64
|
-
'scope': req.scope,
|
65
|
-
'token_type': 'Bearer'
|
66
|
-
}
|
67
|
-
rescue Authorio::Exceptions::InvalidGrant
|
68
|
-
render invalid_grant
|
69
|
-
end
|
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
|
70
50
|
end
|
71
51
|
|
72
52
|
def verify_token
|
73
|
-
token = Token.
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
'scope': 'token.scope'
|
79
|
-
}
|
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
|
80
58
|
end
|
81
59
|
|
82
60
|
private
|
83
61
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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!
|
89
73
|
end
|
90
|
-
params.permit(:response_type, :code_challenge, :code_challenge_method, :scope, :me, :redirect_uri, :client_id, :state)
|
91
74
|
end
|
92
75
|
|
93
|
-
def
|
94
|
-
params.permit(:
|
76
|
+
def scope_params
|
77
|
+
params.require(:scope).permit(scope: [])
|
95
78
|
end
|
96
79
|
|
97
|
-
def
|
98
|
-
|
80
|
+
def oauth_error(error, message = nil, status = :bad_request)
|
81
|
+
{ json: { json: { error: error, error_message: message }.compact },
|
82
|
+
status: status }
|
99
83
|
end
|
100
84
|
|
101
|
-
def
|
102
|
-
|
85
|
+
def token_expired
|
86
|
+
oauth_error('invalid_token', 'The access token has expired', :unauthorized)
|
103
87
|
end
|
104
88
|
|
105
89
|
def code_challenge_failed?
|
106
90
|
# For now, if original request did not have code challenge, then we pass by default
|
107
|
-
return
|
91
|
+
return unless session[:code_challenge]
|
92
|
+
|
108
93
|
sha256 = Digest::SHA256.hexdigest params[:code_verifier]
|
109
|
-
|
110
|
-
return base64 != session[:code_challenge]
|
94
|
+
Base64.urlsafe_encode64(sha256) != session[:code_challenge]
|
111
95
|
end
|
112
96
|
|
113
|
-
def
|
114
|
-
|
115
|
-
|| req.client != params[:client_id] \
|
116
|
-
|| req.created_at < Time.now - 10.minutes
|
117
|
-
end
|
97
|
+
def validate_request(request)
|
98
|
+
raise Exceptions::InvalidGrant, 'validation failed' if request.invalid?(params) || code_challenge_failed?
|
118
99
|
|
119
|
-
|
120
|
-
req = Request.find_by code: params[:code]
|
121
|
-
raise Authorio::Exceptions::InvalidGrant.new if req.nil?
|
122
|
-
req.delete
|
123
|
-
raise Authorio::Exceptions::InvalidGrant.new if invalid_request?(req) || code_challenge_failed?
|
124
|
-
req
|
100
|
+
request
|
125
101
|
end
|
126
102
|
|
127
|
-
def
|
128
|
-
|
129
|
-
|
130
|
-
|
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}"
|
131
110
|
end
|
132
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
|
133
118
|
end
|
134
119
|
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
|