authorio 0.8.0 → 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/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
|