authorio 0.8.0 → 0.8.4

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