authorio 0.8.1 → 0.8.5

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