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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -0
  3. data/app/assets/stylesheets/authorio/auth.css +55 -1
  4. data/app/controllers/authorio/auth_controller.rb +69 -102
  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 +48 -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/db/migrate/20210831155106_add_code_challenge_to_requests.rb +5 -0
  32. data/lib/authorio/configuration.rb +14 -9
  33. data/lib/authorio/engine.rb +11 -8
  34. data/lib/authorio/exceptions.rb +20 -3
  35. data/lib/authorio/routes.rb +10 -7
  36. data/lib/authorio/version.rb +3 -1
  37. data/lib/authorio.rb +15 -21
  38. data/lib/generators/authorio/install/install_generator.rb +3 -3
  39. data/lib/generators/authorio/install/templates/authorio.rb +22 -8
  40. data/lib/tasks/authorio_tasks.rake +15 -14
  41. metadata +49 -20
  42. data/app/controllers/authorio/application_controller.rb +0 -4
  43. data/app/controllers/authorio/helpers.rb +0 -17
  44. data/app/helpers/authorio/application_helper.rb +0 -4
  45. data/app/helpers/authorio/test_helper.rb +0 -4
  46. 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: 415f389a073c49afa82e47fc8caa28fb66d5586cc81758f63e4840f953fb7950
4
- data.tar.gz: 6184be72a5c9f999984c33d6b24e4bd09ea6f0a15469512df70c833610477a69
3
+ metadata.gz: e4cd85ad8ec9b0e70f4d276f7c600d5cc3ebab5b04f0f0313da9f6151e78d680
4
+ data.tar.gz: 7ec85565cc8fb4ea711836ab8069f7062929d13af6f1520809095bdee7c859b9
5
5
  SHA512:
6
- metadata.gz: fbfd300b93d372aa86257b484164be9944ce7950ebc91bfff3a1fe585f858e618e9dee0afbca33ea07953a8aea35b9c79f90cbca6b87e2b78e393cd7e1b9d810
7
- data.tar.gz: ea5e5f5b850d0c88be5dfe2fbfd75abb747625059d544bfbd1988076a3c259bc69db8781036e4ad33d8a7d00f23577e06cc8402d11d0d126080334156048f9f1
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
- module Authorio
2
- class AuthController < ActionController::Base
3
- require 'uri'
4
- require 'digest'
1
+ # frozen_string_literal: true
5
2
 
6
- protect_from_forgery except: [:send_profile, :issue_token, :authorize_user]
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
- p = auth_req_params
10
- p[:me] ||= "#{host_with_protocol}/"
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
- p = auth_user_params
36
- user = User.find_by! profile_path: URI(p[:url]).path
37
- auth_req = Request.find_by! client: p[:client], authorio_user: user
38
- if user.authenticate(p[:password])
39
- params = { code: auth_req.code, state: session[:state] }
40
- redirect_to "#{auth_req.redirect_uri}?#{params.to_query}"
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
- begin
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
- begin
60
- req = validate_request
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.find_by! auth_token: bearer_token
76
- render json: {
77
- 'me': user_url(token.authorio_user),
78
- 'client_id': token.client,
79
- 'scope': 'token.scope'
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 auth_req_params
88
- %w(client_id redirect_uri state code_challenge).each do |param|
89
- unless params.key?(param) && !params[param].empty?
90
- raise ::ActionController::ParameterMissing.new(param)
91
- end
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 auth_user_params
97
- params.permit(:password, :url, :client)
66
+ def scope_params
67
+ params.require(:scope).permit(scope: [])
98
68
  end
99
69
 
100
- def host_with_protocol
101
- "#{request.scheme}://#{request.host}"
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 user_url(user)
105
- "#{host_with_protocol}#{user.profile_path}"
75
+ def token_expired
76
+ oauth_error('invalid_token', 'The access token has expired', :unauthorized)
106
77
  end
107
78
 
108
- def invalid_grant
109
- { json: { 'error': 'invalid_grant' }, status: :bad_request }
79
+ def validation_failed
80
+ oauth_error('invalid_grant', 'validation failed')
110
81
  end
111
82
 
112
- def code_challenge_failed?
113
- # For now, if original request did not have code challenge, then we pass by default
114
- return false if session[:code_challenge].nil?
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 invalid_request?(req)
121
- req.redirect_uri != params[:redirect_uri] \
122
- || req.client != params[:client_id] \
123
- || req.created_at < Time.now - 10.minutes
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 validate_request
127
- req = Request.find_by code: params[:code]
128
- raise Authorio::Exceptions::InvalidGrant.new if req.nil?
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 bearer_token
135
- bearer = /^Bearer /
136
- header = request.headers['Authorization']
137
- header.gsub(bearer, '') if header && header.match(bearer)
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