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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +52 -4
  3. data/app/assets/stylesheets/authorio/auth.css +55 -1
  4. data/app/controllers/authorio/auth_controller.rb +76 -91
  5. data/app/controllers/authorio/authorio_controller.rb +78 -0
  6. data/app/controllers/authorio/sessions_controller.rb +33 -0
  7. data/app/controllers/authorio/users_controller.rb +34 -0
  8. data/app/helpers/authorio/tag_helper.rb +17 -0
  9. data/app/jobs/authorio/application_job.rb +2 -0
  10. data/app/models/authorio/application_record.rb +2 -0
  11. data/app/models/authorio/request.rb +39 -1
  12. data/app/models/authorio/session.rb +43 -0
  13. data/app/models/authorio/token.rb +23 -1
  14. data/app/models/authorio/user.rb +14 -0
  15. data/app/views/authorio/auth/authorization_interface.html.erb +14 -35
  16. data/app/views/authorio/auth/issue_token.json.jbuilder +7 -0
  17. data/app/views/authorio/auth/send_profile.json.jbuilder +3 -0
  18. data/app/views/authorio/auth/verify_token.json.jbuilder +5 -0
  19. data/app/views/authorio/sessions/new.html.erb +14 -0
  20. data/app/views/authorio/users/_profile.json.jbuilder +10 -0
  21. data/app/views/authorio/users/edit.html.erb +25 -0
  22. data/app/views/authorio/users/show.html.erb +18 -0
  23. data/app/views/authorio/users/verify.html.erb +1 -0
  24. data/app/views/layouts/authorio/main.html.erb +38 -0
  25. data/app/views/shared/_login_form.html.erb +41 -0
  26. data/config/routes.rb +15 -5
  27. data/db/migrate/20210723161041_add_expiry_to_tokens.rb +5 -0
  28. data/db/migrate/20210726164625_create_authorio_sessions.rb +12 -0
  29. data/db/migrate/20210801184120_add_profile_to_users.rb +8 -0
  30. data/db/migrate/20210817010101_change_path_to_username_in_users.rb +7 -0
  31. data/lib/authorio/configuration.rb +14 -9
  32. data/lib/authorio/engine.rb +11 -8
  33. data/lib/authorio/exceptions.rb +20 -3
  34. data/lib/authorio/routes.rb +10 -7
  35. data/lib/authorio/version.rb +3 -1
  36. data/lib/authorio.rb +15 -21
  37. data/lib/generators/authorio/install/install_generator.rb +3 -3
  38. data/lib/generators/authorio/install/templates/authorio.rb +22 -8
  39. data/lib/tasks/authorio_tasks.rake +15 -14
  40. metadata +58 -30
  41. data/app/controllers/authorio/application_controller.rb +0 -4
  42. data/app/controllers/authorio/helpers.rb +0 -17
  43. data/app/helpers/authorio/application_helper.rb +0 -4
  44. data/app/helpers/authorio/test_helper.rb +0 -4
  45. 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: 9a376e3f8c81fdc53ac6c223c42fedadb43cdc02129713ca6756db1506c07f10
4
- data.tar.gz: 761c806afafae95a35e97e97b784ae4271cc09fb7cbb6eb6001571e9319cc6a2
3
+ metadata.gz: 78659edadab6ff85d24828c318525bac7b62b4a76b9905e0a1d819ec266f9d46
4
+ data.tar.gz: 817fe77d1c1fd89e68df07a594725997d598e0f0027210d083762da50cb447fd
5
5
  SHA512:
6
- metadata.gz: 32c86c4be9a8cf949ba616797d0a7b505213687d09435ae7c8e0a5588224076c73b0aab4e7b985af74baa89963dafabdc2db7f562982165886dc8085e2fd88c7
7
- data.tar.gz: c9bbaf3bce9c291ddf5619a62caa03962d31c1e57abf0fe45baae96d2325cbd1f0d46f1d6c5477fb9cef6dafa9eb54f4ee2874820e8e74b662dd7e8c9d6aa77d
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 at [Pin13](pin13.net/login). Enter your site's URL and click Sign In.
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
- You should be then be redirected back to your own site and the Authorio
89
- login UI
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 < ActionController::Base
4
+ class AuthController < AuthorioController
3
5
  require 'uri'
4
6
  require 'digest'
5
7
 
6
- protect_from_forgery except: [:send_profile, :issue_token, :authorize_user]
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
- # If there are any old requests from this (client, user), delete them now
21
- Request.where(authorio_user: user, client: p[:client_id]).delete_all
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
- auth_request = Request.new.tap do |req|
24
- req.code = SecureRandom.hex(20)
25
- req.redirect_uri = p[:redirect_uri]
26
- req.client = p[:client_id] # IndieAuth client_id conflicts with Rails' _id foreign key convention
27
- req.scope = p[:scope]
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
- p = auth_user_params
37
- user = User.find_by! profile_path: URI(p[:url]).path
38
- auth_req = Request.find_by! client: p[:client], authorio_user: user
39
- if user.authenticate(p[:password])
40
- params = { code: auth_req.code, state: session[:state] }
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
- begin
50
- render json: { 'me': user_url(validate_request.authorio_user) }
51
- rescue Authorio::Exceptions::InvalidGrant
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
- begin
58
- req = validate_request
59
- raise Authorio::Exceptions::InvalidGrant.new if req.scope.blank?
60
- token = Token.create(authorio_user: req.authorio_user, scope: req.scope, client: req.client)
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.find_by auth_token: bearer_token
74
- head :bad_request and return if token.nil?
75
- render json: {
76
- 'me': user_url(token.authorio_user),
77
- 'client_id': token.client,
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 auth_req_params
85
- %w(client_id redirect_uri state code_challenge).each do |param|
86
- unless params.key?(param) && !params[param].empty?
87
- raise ::ActionController::ParameterMissing.new(param)
88
- end
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 auth_user_params
94
- params.permit(:password, :url, :client)
76
+ def scope_params
77
+ params.require(:scope).permit(scope: [])
95
78
  end
96
79
 
97
- def user_url(user)
98
- "#{request.scheme}://#{request.host}#{user.profile_path}"
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 invalid_grant
102
- { json: { 'error': 'invalid_grant' }, status: :bad_request }
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 false if session[:code_challenge].nil?
91
+ return unless session[:code_challenge]
92
+
108
93
  sha256 = Digest::SHA256.hexdigest params[:code_verifier]
109
- base64 = Base64.urlsafe_encode64 sha256
110
- return base64 != session[:code_challenge]
94
+ Base64.urlsafe_encode64(sha256) != session[:code_challenge]
111
95
  end
112
96
 
113
- def invalid_request?(req)
114
- req.redirect_uri != params[:redirect_uri] \
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
- def validate_request
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 bearer_token
128
- bearer = /^Bearer /
129
- header = request.headers['Authorization']
130
- header.gsub(bearer, '') if header && header.match(bearer)
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