authorio 0.8.3 → 0.8.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/authorio/auth.css +1 -1
  3. data/app/controllers/authorio/auth_controller.rb +63 -136
  4. data/app/controllers/authorio/authorio_controller.rb +23 -11
  5. data/app/controllers/authorio/sessions_controller.rb +6 -5
  6. data/app/controllers/authorio/users_controller.rb +13 -3
  7. data/app/helpers/authorio/tag_helper.rb +5 -5
  8. data/app/jobs/authorio/application_job.rb +2 -0
  9. data/app/models/authorio/application_record.rb +2 -0
  10. data/app/models/authorio/request.rb +46 -4
  11. data/app/models/authorio/session.rb +13 -10
  12. data/app/models/authorio/token.rb +16 -2
  13. data/app/models/authorio/user.rb +12 -3
  14. data/app/views/authorio/auth/authorization_interface.html.erb +2 -2
  15. data/app/views/authorio/auth/issue_token.json.jbuilder +7 -0
  16. data/app/views/authorio/auth/send_profile.json.jbuilder +3 -0
  17. data/app/views/authorio/auth/verify_token.json.jbuilder +5 -0
  18. data/app/views/authorio/sessions/new.html.erb +1 -2
  19. data/app/views/authorio/users/_profile.json.jbuilder +10 -0
  20. data/app/views/authorio/users/show.html.erb +18 -0
  21. data/app/views/authorio/users/verify.html.erb +1 -0
  22. data/app/views/shared/_login_form.html.erb +19 -14
  23. data/config/routes.rb +16 -11
  24. data/db/migrate/20210817010101_change_path_to_username_in_users.rb +7 -0
  25. data/db/migrate/20210831155106_add_code_challenge_to_requests.rb +5 -0
  26. data/lib/authorio/configuration.rb +14 -12
  27. data/lib/authorio/engine.rb +13 -11
  28. data/lib/authorio/exceptions.rb +20 -12
  29. data/lib/authorio/routes.rb +10 -7
  30. data/lib/authorio/version.rb +3 -1
  31. data/lib/authorio.rb +15 -21
  32. data/lib/generators/authorio/install/install_generator.rb +3 -3
  33. data/lib/generators/authorio/install/templates/authorio.rb +20 -15
  34. data/lib/tasks/authorio_tasks.rake +15 -14
  35. metadata +34 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5ecd8cf849002e116b21a3c4f0073fc17988e62791f356ab57a703397edc77c
4
- data.tar.gz: 4c0c4908c722b65ccd1559b5fea382231933e2840e5d0b6e08271ed5efd43f15
3
+ metadata.gz: f44cb9e20ad1a6c77dcb24da16623276d876ef4ebd0f3e630e3f27d72587646c
4
+ data.tar.gz: '08ca2eff8c2cb5a77801af0705981220499f58acfefa6f0b103ebda5c5f2eb62'
5
5
  SHA512:
6
- metadata.gz: 8bb01ec581f584fe9eadc7d77d477fa2f57e8883101ba51b5f8cb8729bf7486f061bc996a17cab023d476119dda8f37d7676a7f04e281180ad8dda8e649eb16c
7
- data.tar.gz: 7d8e0e19113cd7748a64212ee98f514ba953027409adf20a71b47f14c3e1c5ef0db28924bca9afa4fd498ea56a686b1169f2f8ceb7f53297472c6b9cd34d86cf
6
+ metadata.gz: f9d5a642af5a2a76f7d3ff12bafcd345bdfb8ab92f3792aa0f24d1ff8db29790e3cc37aab8474f3342660181cee08cf4af25f3c45fa3c8d2fef041697ed60b8f
7
+ data.tar.gz: 363031d389f74620cff7ae761935bcaa225d1c3c96ebc24122b9d7f45a746aeec46315dee1ff78da4f31329e5210c71707cfe387308f2f8b72483ce0b57ffc7c
@@ -50,7 +50,7 @@ div.authorio-auth {
50
50
  margin: 0.1em;
51
51
  }
52
52
 
53
- .authorio-auth .auth-btn-row .auth-btn:last-child {
53
+ .authorio-auth .auth-btn-row .btn-success {
54
54
  float: right;
55
55
  }
56
56
 
@@ -1,181 +1,108 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class AuthController < AuthorioController
3
- require 'uri'
4
- require 'digest'
5
-
6
5
  # These API-only endpoints are protected by code challenge and do not need CSRF protextion
7
- protect_from_forgery with: :exception, except: [:send_profile, :issue_token]
6
+ protect_from_forgery with: :exception, except: %i[send_profile issue_token]
8
7
 
9
8
  rescue_from 'Authorio::Exceptions::SessionReplayAttack' do |exception|
10
- redirect_back_with_error "Session Replay attack detected. This has been logged."
11
- logger.info "Session replay attack detected!"
12
- Authorio::Session.where(user: exception.session.user).delete_all
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.'
13
18
  end
14
-
15
- helper_method :user_scope_description
16
19
 
17
20
  # GET /auth
18
21
  def authorization_interface
19
- %w(client_id redirect_uri state code_challenge).each do |param|
20
- raise ::ActionController::ParameterMissing, param unless params[param].present?
21
- end
22
- @user = User.find_by_url! params[:me]
23
-
24
- # If there are any old requests from this (client, user), delete them now
25
- Request.where(authorio_user: @user, client: params[:client_id]).delete_all
26
-
27
- auth_request = Request.create(
28
- code: SecureRandom.hex(20),
29
- redirect_uri: params[:redirect_uri],
30
- client: params[:client_id], # IndieAuth client_id conflicts with Rails' _id foreign key convention
31
- scope: params[:scope],
32
- authorio_user: @user
33
- )
34
- session.update request.parameters.slice(*%w(state client_id code_challenge))
35
- @rememberable = Authorio.configuration.local_session_lifetime && !@user_logged_in_locally
36
- @scope = params[:scope]&.split
37
- rescue ActiveRecord::RecordNotFound
38
- redirect_back_with_error "Invalid user"
39
- rescue ActionController::ParameterMissing => error
40
- render oauth_error "invalid_request", "missing parameter #{error}"
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
41
25
  end
42
26
 
43
27
  # POST /user/:id/authorize
44
28
  def authorize_user
45
- redirect_to session[:client_id] and return if params[:commit] == "Cancel"
46
-
47
- user = authenticate_user_from_session_or_password
48
- set_session_cookie(user) if auth_user_params[:remember_me]
29
+ redirect_to(session[:client_id], allow_other_host: true) and return if params[:commit] == 'Cancel'
49
30
 
50
- auth_req = Request.find_by! client: session[:client_id], authorio_user: user
51
- auth_req.update_scope(scope_params[:scope]) if params.has_key? :scope
52
- redirect_params = { code: auth_req.code, state: session[:state] }
53
- redirect_to "#{auth_req.redirect_uri}?#{redirect_params.to_query}"
54
- rescue ActiveRecord::RecordNotFound
55
- redirect_back_with_error "Invalid user"
56
- rescue Authorio::Exceptions::InvalidPassword
57
- redirect_back_with_error "Incorrect password. Try again."
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
58
35
  end
59
36
 
60
37
  def send_profile
61
- request = validate_request
62
- render json: profile(request)
63
- rescue Authorio::Exceptions::InvalidGrant => error
64
- render oauth_error 'invalid_grant', error.message
38
+ @auth_request = find_auth_request or (render validation_failed and return)
65
39
  end
66
40
 
67
41
  def issue_token
68
- req = validate_request
69
- raise Authorio::Exceptions::InvalidGrant, 'missing scope' if req.scope.blank?
70
- token = Token.create(authorio_user: req.authorio_user, scope: req.scope, client: req.client)
71
- render json: {
72
- 'access_token': token.auth_token,
73
- 'scope': req.scope,
74
- 'expires_in': Authorio.configuration.token_expiration,
75
- 'token_type': 'Bearer'
76
- }.merge(profile(req))
77
- rescue Authorio::Exceptions::InvalidGrant => error
78
- render oauth_error, 'invalid_grant', error.message
42
+ @auth_request = find_auth_request or (render validation_failed and return)
43
+ @token = Token.create_from_request(@auth_request)
79
44
  end
80
45
 
81
46
  def verify_token
82
- token = Token.find_by! auth_token: bearer_token
83
- if token.expired?
84
- token.delete
85
- render token_expired
86
- else
87
- render json: {
88
- 'me': user_url(token.authorio_user),
89
- 'client_id': token.client,
90
- 'scope': 'token.scope'
91
- }
92
- end
93
- rescue ActiveRecord::RecordNotFound
94
- 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
95
52
  end
96
53
 
97
54
  private
98
55
 
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!
63
+ end
64
+ end
65
+
99
66
  def scope_params
100
67
  params.require(:scope).permit(scope: [])
101
68
  end
102
69
 
103
- def oauth_error(error, message=nil)
104
- resp = { json: {'error': error} }
105
- resp[:json]['error_message'] = message unless message.nil?
106
- { json: resp, status: :bad_request }
70
+ def oauth_error(error, message = nil, status = :bad_request)
71
+ { json: { json: { error:, error_message: message }.compact },
72
+ status: }
107
73
  end
108
74
 
109
75
  def token_expired
110
- { json: {'error': 'invalid_token', 'error_message': 'The access token has expired' }, status: :unauthorized }
111
- end
112
-
113
- def code_challenge_failed?
114
- # For now, if original request did not have code challenge, then we pass by default
115
- return false if session[:code_challenge].nil?
116
- sha256 = Digest::SHA256.hexdigest params[:code_verifier]
117
- base64 = Base64.urlsafe_encode64 sha256
118
- return base64 != session[:code_challenge]
119
- end
120
-
121
- def invalid_request?(req)
122
- req.redirect_uri != params[:redirect_uri] \
123
- || req.client != params[:client_id] \
124
- || req.created_at < Time.now - 10.minutes
125
- end
126
-
127
- def validate_request
128
- req = Request.find_by code: params[:code]
129
- raise Authorio::Exceptions::InvalidGrant, "code not found" if req.nil?
130
- req.delete
131
- raise Authorio::Exceptions::InvalidGrant, "validation failed" if invalid_request?(req) || code_challenge_failed?
132
- req
133
- end
134
-
135
- def profile(request)
136
- profile = { me: user_url(request.authorio_user) }
137
- if request.scope
138
- scopes = request.scope.split
139
- if scopes.include? 'profile'
140
- profile['profile'] = {
141
- name: request.authorio_user.full_name,
142
- url: request.authorio_user.url,
143
- photo: request.authorio_user.photo
144
- }.compact
145
- if scopes.include? 'email'
146
- profile['profile']['email'] = request.authorio_user.email
147
- end
148
- end
149
- end
150
- profile
76
+ oauth_error('invalid_token', 'The access token has expired', :unauthorized)
151
77
  end
152
78
 
153
- def bearer_token
154
- bearer = /^Bearer /
155
- header = request.headers['Authorization']
156
- header.gsub(bearer, '') if header && header.match(bearer)
79
+ def validation_failed
80
+ oauth_error('invalid_grant', 'validation failed')
157
81
  end
158
82
 
159
- def authenticate_user_from_session_or_password
160
- session = user_session
161
- if session
162
- return session.authorio_user
163
- else
164
- user = User.find_by! profile_path: URI(auth_user_params[:url]).path
165
- raise Authorio::Exceptions::InvalidPassword unless user.authenticate(auth_user_params[:password])
166
- return user
167
- end
83
+ def find_auth_request
84
+ auth_request = Request.find_by code: params[:code]
85
+ auth_request&.validate_oauth params
168
86
  end
169
87
 
170
- ScopeDescriptions = {
171
- 'profile': 'View basic profile information',
172
- 'email': 'View your email address',
173
- 'offline_access': 'Keep you logged in permanently (until revoked)'
174
- }
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)
94
+ end
175
95
 
176
- def user_scope_description(scope)
177
- ScopeDescriptions.dig(scope.to_sym) || scope
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}", allow_other_host: true
178
99
  end
179
100
 
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
106
+ end
180
107
  end
181
108
  end
@@ -1,12 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class AuthorioController < ActionController::Base
3
5
  layout 'authorio/main'
4
6
 
5
- helper_method :logged_in?, :rememberable?, :user_url, :current_user
7
+ helper_method :logged_in?, :rememberable?, :current_user,
8
+ :user_scope_description, :profile_url
6
9
 
7
10
  def index
8
11
  if logged_in?
9
- redirect_to edit_user_path(1)
12
+ redirect_to edit_user_path(current_user)
10
13
  else
11
14
  redirect_to new_session_path
12
15
  end
@@ -33,20 +36,28 @@ module Authorio
33
36
  end
34
37
 
35
38
  def current_user
36
- user_session&.authorio_user.id
39
+ user_session&.authorio_user&.id
40
+ end
41
+
42
+ def user_scope_description(scope)
43
+ Authorio::Request.user_scope_description(scope)
37
44
  end
38
45
 
39
- def user_url(user)
40
- "#{host_with_protocol}#{user.profile_path}"
46
+ def profile_url(user)
47
+ if Authorio.configuration.multiuser
48
+ verify_user_url(user)
49
+ else
50
+ "#{request.scheme}://#{request.host}"
51
+ end
41
52
  end
42
53
 
43
54
  protected
44
55
 
45
56
  def auth_user_params
46
- params.require(:user).permit(:password, :url, :remember_me)
57
+ params.require(:user).permit(:username, :password, :remember_me)
47
58
  end
48
59
 
49
- def set_session_cookie(user)
60
+ def write_session_cookie(user)
50
61
  cookies.encrypted[:user] = {
51
62
  value: Authorio::Session.create(authorio_user: user).as_cookie,
52
63
  expires: Authorio.configuration.local_session_lifetime
@@ -55,12 +66,13 @@ module Authorio
55
66
 
56
67
  def redirect_back_with_error(error)
57
68
  flash[:alert] = error
58
- redirect_back fallback_location: Authorio.authorization_path, allow_other_host: false
69
+ redirect_back fallback_location: Authorio.authorization_path.prepend('/'), allow_other_host: false
59
70
  end
60
71
 
61
- def host_with_protocol
62
- "#{request.scheme}://#{request.host}"
72
+ def bearer_token
73
+ bearer = /^Bearer /
74
+ header = request.headers['Authorization']
75
+ header.gsub(bearer, '') if header&.match(bearer)
63
76
  end
64
-
65
77
  end
66
78
  end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class SessionsController < AuthorioController
3
-
4
5
  # GET /session/new
5
6
  def new
6
7
  @session = Session.new(authorio_user: User.first)
@@ -8,21 +9,21 @@ module Authorio
8
9
 
9
10
  # POST /session
10
11
  def create
11
- user = User.find_by! profile_path: URI(auth_user_params[:url]).path
12
+ user = User.find_by_username! auth_user_params[:username]
12
13
  raise Exceptions::InvalidPassword unless user.authenticate(auth_user_params[:password])
13
- set_session_cookie(user) if auth_user_params[:remember_me]
14
14
 
15
+ write_session_cookie(user) if auth_user_params[:remember_me]
15
16
  # Even if we don't have a permanent remember-me session, we make a temporary session
16
17
  session[:user_id] = user.id
17
18
  redirect_to edit_user_path(user)
18
19
  rescue Exceptions::InvalidPassword
19
- redirect_back_with_error "Incorrect password. Try again."
20
+ redirect_back_with_error 'Incorrect password. Try again.'
20
21
  end
21
22
 
22
23
  # DELETE /session
23
24
  def destroy
24
25
  reset_session
25
- if (cookie = cookies.encrypted[:user]) && session = Session.find_by_cookie(cookie)
26
+ if (cookie = cookies.encrypted[:user]) && (session = Session.find_by_cookie(cookie))
26
27
  cookies.delete :user
27
28
  session.destroy
28
29
  end
@@ -1,9 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class UsersController < AuthorioController
5
+ before_action :authorized?, except: :verify
3
6
 
4
- before_action :authorized?
7
+ # GET /users/:id
8
+ def show
9
+ @user = User.find(params[:id])
10
+ end
5
11
 
6
- # GET /users/:id/edit
12
+ # GET /users/:id/edit
7
13
  def edit
8
14
  @user = User.find(params[:id])
9
15
  end
@@ -11,10 +17,14 @@ module Authorio
11
17
  # PATCH /users/:id
12
18
  def update
13
19
  User.find(params[:id]).update(user_params)
14
- flash[:info] = "Profile Saved"
20
+ flash[:info] = 'Profile Saved'
15
21
  redirect_to edit_user_path
16
22
  end
17
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
+
18
28
  private
19
29
 
20
30
  def user_params
@@ -1,17 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  # These helpers are provided to the main application
3
5
  module TagHelper
4
6
  extend ActiveSupport::Concern
5
7
 
6
8
  included do
7
- if respond_to?(:helper_method)
8
- helper_method :indieauth_tag
9
- end
9
+ helper_method :indieauth_tag if respond_to?(:helper_method)
10
10
  end
11
11
 
12
12
  def indieauth_tag
13
- tag(:link, rel: 'authorization_endpoint', href: URI.join(root_url, Authorio.authorization_path)) <<
14
- tag(:link, rel: 'token_endpoint', href: URI.join(root_url, Authorio.token_path))
13
+ tag(:link, rel: 'authorization_endpoint', href: URI.join(main_app.root_url, Authorio.authorization_path)) <<
14
+ tag(:link, rel: 'token_endpoint', href: URI.join(main_app.root_url, Authorio.token_path))
15
15
  end
16
16
  end
17
17
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class ApplicationJob < ActiveJob::Base
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class ApplicationRecord < ActiveRecord::Base
3
5
  self.abstract_class = true
@@ -1,12 +1,54 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class Request < ApplicationRecord
3
- belongs_to :authorio_user, class_name: "::Authorio::User"
5
+ belongs_to :authorio_user, class_name: '::Authorio::User'
4
6
 
5
7
  validates_presence_of :code, :redirect_uri, :client
6
8
 
7
- # User has the right to modify requested scope
8
- def update_scope(scope)
9
- update(scope: scope.join(' '))
9
+ before_validation :set_code, on: :create
10
+ before_create :sweep_requests
11
+
12
+ # The IndieAuth spec uses 'client_id' to specify the client in the address, as a URL (eg "https://example.com")
13
+ # But Rails uses '_id' to tag associations (foreign keys). So we save that as 'client' here, but map
14
+ # client_id as an alias since that is what the HTTP parameter will be
15
+ def client_id=(value)
16
+ self.client = value
17
+ end
18
+
19
+ def validate_oauth(params)
20
+ redirect_uri == params[:redirect_uri] &&
21
+ client == params[:client_id] &&
22
+ created_at > 10.minutes.ago &&
23
+ code_challenge_matches(params[:code_verifier]) &&
24
+ self
10
25
  end
26
+
27
+ def code_challenge_matches(verifier)
28
+ # For now, if original request did not have code challenge, then we pass by default
29
+ return true if code_challenge.blank?
30
+
31
+ sha256 = Digest::SHA256.digest verifier
32
+ Base64.urlsafe_encode64(sha256).sub(/=*$/, '') == code_challenge
33
+ end
34
+
35
+ def self.user_scope_description(scope)
36
+ USER_SCOPE_DESCRIPTION[scope.to_sym] || scope
37
+ end
38
+
39
+ private
40
+
41
+ def set_code
42
+ self.code = SecureRandom.hex(20)
43
+ end
44
+
45
+ def sweep_requests
46
+ Request.where(client:, authorio_user:).destroy_all
47
+ end
48
+
49
+ USER_SCOPE_DESCRIPTION = {
50
+ profile: 'View basic profile information',
51
+ email: 'View your email address'
52
+ }.freeze
11
53
  end
12
54
  end
@@ -1,34 +1,37 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class Session < ApplicationRecord
3
5
  # Implement a session cookie store based on best security practices
4
6
  # See: https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence
5
- belongs_to :authorio_user, class_name: "::Authorio::User"
7
+ belongs_to :authorio_user, class_name: '::Authorio::User'
6
8
 
7
9
  # 1. Protect against having database stolen by only storing token hashes
8
- attribute :token # This will not be persisted in the DB
10
+ attribute :token # This will not be persisted in the DB
9
11
  has_secure_token
10
12
 
11
13
  before_create do
12
14
  self.expires_at = Time.now + Authorio.configuration.token_expiration
13
15
  self.selector = SecureRandom.hex(12)
14
- self.hashed_token = Digest::SHA256.hexdigest self.token
16
+ self.hashed_token = Digest::SHA256.hexdigest token
15
17
  end
16
18
 
17
19
  # 2. To guard against timing attacks, we lookup tokens based on a separate selector attribute
18
20
  # and compare them using a secure time-constant comparison method
19
21
  def self.find_by_cookie(cookie)
20
- _selector, _token = cookie.split(':')
21
- session = find_by selector: _selector
22
- raise Authorio::Exceptions::SessionReplayAttack.new(session) unless session.matches_cookie?(cookie)
22
+ selector, _token = cookie.split(':')
23
+ session = find_by selector: selector
24
+ raise Authorio::Exceptions::SessionReplayAttack.new, session unless session.matches_cookie?(cookie)
25
+
23
26
  session
24
27
  end
25
28
 
26
29
  def matches_cookie?(cookie)
27
- _selector, _token = cookie.split(':')
28
- _hashed_token = Digest::SHA256.hexdigest _token
29
- !expired? && ActiveSupport::SecurityUtils.secure_compare(_hashed_token, hashed_token)
30
+ _selector, token = cookie.split(':')
31
+ cookie_hashed_token = Digest::SHA256.hexdigest token
32
+ !expired? && ActiveSupport::SecurityUtils.secure_compare(cookie_hashed_token, hashed_token)
30
33
  end
31
-
34
+
32
35
  def expired?
33
36
  expires_at < Time.now
34
37
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class Token < ApplicationRecord
3
- belongs_to :authorio_user, class_name: "::Authorio::User"
5
+ belongs_to :authorio_user, class_name: '::Authorio::User'
4
6
  has_secure_token :auth_token
5
7
 
6
8
  validates_presence_of :scope, :client
@@ -9,8 +11,20 @@ module Authorio
9
11
  self.expires_at = Time.now + Authorio.configuration.token_expiration
10
12
  end
11
13
 
14
+ # The token endpoint can get hit by bots, so short-circut the find if they
15
+ # don't send a bearer token
16
+ def self.find_by_auth_token(token)
17
+ token and find_by auth_token: token
18
+ end
19
+
12
20
  def expired?
13
- return expires_at < Time.now
21
+ expires_at < Time.now
22
+ end
23
+
24
+ def self.create_from_request(req)
25
+ raise Exceptions::InvalidGrant, 'missing scope' if req.scope.blank?
26
+
27
+ Token.create(authorio_user: req.authorio_user, scope: req.scope, client: req.client)
14
28
  end
15
29
  end
16
30
  end
@@ -1,10 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class User < ApplicationRecord
3
5
  has_secure_password
4
6
 
5
- def self.find_by_url!(url)
6
- find_by! profile_path: URI(url || "/").path
7
- end
7
+ class << self
8
+ def find_by_url!(url)
9
+ find_by_username!(URI(url).path)
10
+ end
8
11
 
12
+ def find_by_username!(name)
13
+ return first unless Authorio.configuration.multiuser
14
+
15
+ find_by(username: name) or raise Exceptions::UserNotFound
16
+ end
17
+ end
9
18
  end
10
19
  end
@@ -7,9 +7,9 @@
7
7
  <h3>Authorio</h3>
8
8
  <div class="client-row">
9
9
  <span class="client"><%= params[:client_id] %></span> wants to authenticate
10
- <% if @scope %>and also<% end %>
10
+ <% if params[:scope] %>and also<% end %>
11
11
  </div>
12
- <%= render 'shared/login_form', target: authorize_user_path(@user), user: @user, scopes: @scope, cancel: true %>
12
+ <%= render 'shared/login_form', target: authorize_user_path, cancel: true %>
13
13
  </div>
14
14
  <div class="col-md-4"></div>
15
15
  </div>
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.access_token @token.auth_token
4
+ json.expires_in Authorio.configuration.token_expiration
5
+ json.token_type 'Bearer'
6
+ json.scope @token.scope
7
+ json.partial! 'authorio/users/profile', request: @auth_request
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.partial! 'authorio/users/profile', request: @request
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.me url_for(@token.authorio_user)
4
+ json.client_id @token.client
5
+ json.scope @token.scope
@@ -7,8 +7,7 @@
7
7
  <div class="col-md-4 auth-panel">
8
8
  <h3>Authorio</h3>
9
9
  <div class="client-row">Local Login</div>
10
- <%= render 'shared/login_form', target: session_path(@session),
11
- user: @session.authorio_user, scopes: nil, cancel: false %>
10
+ <%= render 'shared/login_form', target: session_path(@session), cancel: false %>
12
11
  </div>
13
12
  <div class="col-md-4"></div>
14
13
  </div>
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.me profile_url(@auth_request.authorio_user)
4
+ if @auth_request.scope&.include? 'profile'
5
+ json.profile do
6
+ json.name(@auth_request.authorio_user.full_name)
7
+ json.call(@auth_request.authorio_user, :url, :photo)
8
+ json.email(@auth_request.authorio_user.email) if @auth_request.scope.include?('email')
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ <%= stylesheet_link_tag "authorio/auth" %>
2
+ <% content_for :title, "User Account" %>
3
+
4
+ <%= indieauth_tag %>
5
+
6
+ <div class="container authorio-auth">
7
+ <div class="row">
8
+ <div class="col-md-4"></div>
9
+ <div class="col-md-4 auth-panel">
10
+ <h3>IndieAuth Account</h3>
11
+ Full Name <%= @user.full_name %>
12
+ URL <%= @user.url %>
13
+ Photo <%= image_tag @user.photo %>
14
+ Email <%= @user.email %>
15
+ </div>
16
+ <div class="col-md-4"></div>
17
+ </div>
18
+ </div>
@@ -0,0 +1 @@
1
+ <%= indieauth_tag %>
@@ -1,9 +1,9 @@
1
- <%= form_with(model: user, url: target, method: :post) do |form| %>
2
- <% if scopes %>
1
+ <%= form_with(url: target, method: :post) do |form| %>
2
+ <% if params[:scope] %>
3
3
  <%= fields_for :scope do |req_scope| %>
4
4
  <div class="scopes">
5
5
  <ul class="scope">
6
- <% for scope in scopes %>
6
+ <% for scope in params[:scope].split %>
7
7
  <li>
8
8
  <%= label_tag(:scope, class: 'scope-label') do %>
9
9
  <%= req_scope.check_box(:scope, {multiple: true, checked: true}, scope, nil) %>
@@ -15,22 +15,27 @@
15
15
  </div>
16
16
  <% end %>
17
17
  <% end -%>
18
- <%= form.label(:url, "User URL") %>
19
- <%= form.text_field(:url, value: params[:me] || user_url(user), readonly: true) %>
20
- <% unless logged_in? %>
21
- <%= form.label(:password, "Password") %>
22
- <%= form.password_field(:password, autofocus: true) %>
23
- <% if rememberable? %>
24
- <%= label_tag(:remember_me, class: 'remember') do %>
25
- <%= form.check_box :remember_me %>
26
- <span class='r-m'>Remember me for <%= distance_of_time_in_words Authorio.configuration.local_session_lifetime -%></span>
18
+ <%= fields_for :user do |user_scope| %>
19
+ <%= user_scope.hidden_field :dummy, value: 42 %>
20
+ <% if Authorio.configuration.multiuser %>
21
+ <%= user_scope.label(:username, "Username") %>
22
+ <%= user_scope.text_field(:username) %>
23
+ <% end -%>
24
+ <% unless logged_in? %>
25
+ <%= user_scope.label(:password, "Password") %>
26
+ <%= user_scope.password_field(:password, autofocus: true) %>
27
+ <% if rememberable? %>
28
+ <%= label_tag(:remember_me, class: 'remember') do %>
29
+ <%= user_scope.check_box :remember_me %>
30
+ <span class='r-m'>Remember me for <%= distance_of_time_in_words Authorio.configuration.local_session_lifetime -%></span>
31
+ <% end %>
27
32
  <% end %>
28
33
  <% end %>
29
- <% end %>
34
+ <% end -%>
30
35
  <div class='auth-btn-row'>
36
+ <%= form.submit("Sign in", class: 'btn btn-success auth-btn') %>
31
37
  <% if cancel %>
32
38
  <%= form.submit("Cancel", class: 'btn btn-default auth-btn') %>
33
39
  <% end %>
34
- <%= form.submit("Sign in", class: 'btn btn-success auth-btn') %>
35
40
  </div>
36
41
  <% end %>
data/config/routes.rb CHANGED
@@ -1,12 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Authorio::Engine.routes.draw do
2
- get Authorio.configuration.authorization_endpoint, controller: 'auth', action: 'authorization_interface'
3
- post Authorio.configuration.authorization_endpoint, controller: 'auth', action: 'send_profile'
4
- resources :users, only: [:edit, :update] do
5
- post 'authorize', on: :member, to: 'auth#authorize_user'
6
- end
7
- resource :session, only: [:new, :create]
8
- get 'session' => 'sessions#destroy', as: 'logout'
9
- get Authorio.configuration.token_endpoint, controller: 'auth', action: 'verify_token'
10
- post Authorio.configuration.token_endpoint, controller: 'auth', action: 'issue_token'
11
- root to: 'authorio#index'
12
- end
4
+ root to: 'authorio#index'
5
+
6
+ get Authorio.configuration.authorization_endpoint, controller: 'auth', action: 'authorization_interface'
7
+ resources :users, only: %i[show edit update]
8
+ post 'user/authorize', to: 'auth#authorize_user', as: 'authorize_user'
9
+ resource :session, only: %i[new create]
10
+ get 'session', to: 'sessions#destroy', as: 'logout'
11
+ get 'user/(:id)/verify', to: 'users#verify', as: 'verify_user'
12
+ defaults format: :json do
13
+ post Authorio.configuration.authorization_endpoint, controller: 'auth', action: 'send_profile'
14
+ get Authorio.configuration.token_endpoint, controller: 'auth', action: 'verify_token'
15
+ post Authorio.configuration.token_endpoint, controller: 'auth', action: 'issue_token'
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ class ChangePathToUsernameInUsers < ActiveRecord::Migration[6.1]
2
+ def change
3
+ change_table :authorio_users do |t|
4
+ t.rename :profile_path, :username
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ class AddCodeChallengeToRequests < ActiveRecord::Migration[6.1]
2
+ def change
3
+ add_column :authorio_requests, :code_challenge, :string
4
+ end
5
+ end
@@ -1,15 +1,17 @@
1
- module Authorio
2
- class Configuration
1
+ # frozen_string_literal: true
3
2
 
4
- attr_accessor :authorization_endpoint, :token_endpoint, :mount_point, :token_expiration,
5
- :local_session_lifetime
3
+ module Authorio
4
+ class Configuration
5
+ attr_accessor :authorization_endpoint, :token_endpoint, :mount_point, :token_expiration,
6
+ :local_session_lifetime, :multiuser
6
7
 
7
- def initialize
8
- @authorization_endpoint = "auth"
9
- @token_endpoint = "token"
10
- @mount_point = "authorio"
11
- @token_expiration = 4.weeks
12
- @local_session_lifetime = nil
13
- end
14
- end
8
+ def initialize
9
+ @authorization_endpoint = 'auth'
10
+ @token_endpoint = 'token'
11
+ @mount_point = 'authorio'
12
+ @token_expiration = 4.weeks
13
+ @local_session_lifetime = nil
14
+ @multiuser = false
15
+ end
16
+ end
15
17
  end
@@ -1,15 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
- class Engine < ::Rails::Engine
3
- isolate_namespace Authorio
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Authorio
4
6
 
5
- initializer "authorio.load_helpers" do |app|
6
- Rails.application.reloader.to_prepare do
7
- ActionView::Base.send :include, Authorio::TagHelper
8
- end
9
- end
7
+ initializer 'authorio.load_helpers' do
8
+ Rails.application.reloader.to_prepare do
9
+ ActionView::Base.send :include, Authorio::TagHelper
10
+ end
11
+ end
10
12
 
11
- initializer "authorio.assets.precompile" do |app|
12
- app.config.assets.precompile += %w( authorio/auth.css authorio/application.css )
13
- end
14
- end
13
+ initializer 'authorio.assets.precompile' do |app|
14
+ app.config.assets.precompile << ['authorio/application.css', 'authorio/auth.css']
15
+ end
16
+ end
15
17
  end
@@ -1,14 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
- module Exceptions
3
- class InvalidGrant < RuntimeError; end
4
- class InvalidPassword < RuntimeError; end
5
-
6
- class SessionReplayAttack < StandardError
7
- attr_accessor :session
8
-
9
- def initialize(session)
10
- @session = session
11
- end
12
- end
13
- end
4
+ module Exceptions
5
+ class InvalidGrant < RuntimeError; end
6
+
7
+ class InvalidPassword < RuntimeError; end
8
+
9
+ class SessionReplayAttack < StandardError
10
+ attr_accessor :session
11
+
12
+ def initialize(session)
13
+ super("Session replay attack on user account #{session.authorio_user.id}")
14
+ @session = session
15
+ end
16
+ end
17
+
18
+ class UserNotFound < StandardError; end
19
+
20
+ class TokenExpired < StandardError; end
21
+ end
14
22
  end
@@ -1,9 +1,12 @@
1
- module ActionDispatch::Routing
2
- class Mapper
1
+ # frozen_string_literal: true
3
2
 
4
- # Provide a custom mounting command, just so we can track our own mount point
5
- def authorio_routes
6
- mount Authorio::Engine, at: Authorio.configuration.mount_point
7
- end
8
- end
3
+ module ActionDispatch
4
+ module Routing
5
+ class Mapper
6
+ # Provide a custom mounting command, just so we can track our own mount point
7
+ def authorio_routes
8
+ mount Authorio::Engine, at: Authorio.configuration.mount_point
9
+ end
10
+ end
11
+ end
9
12
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
- VERSION = '0.8.3'
4
+ VERSION = '0.8.7'
3
5
  end
data/lib/authorio.rb CHANGED
@@ -1,27 +1,21 @@
1
- require "authorio/version"
2
- require "authorio/engine"
3
- require "authorio/configuration"
4
- require "authorio/routes"
5
- require "authorio/exceptions"
1
+ # frozen_string_literal: true
6
2
 
7
- module Authorio
8
- class << self
9
- attr_accessor :configuration, :authorization_path
10
- end
3
+ Dir[File.join(__dir__, 'authorio', '*.rb')].sort.each { |f| require f }
11
4
 
12
- def self.configuration
13
- @configuration ||= Configuration.new
14
- end
5
+ module Authorio
6
+ def self.configuration
7
+ @configuration ||= Configuration.new
8
+ end
15
9
 
16
- def self.configure
17
- yield configuration
18
- end
10
+ def self.configure
11
+ yield configuration
12
+ end
19
13
 
20
- def self.authorization_path
21
- return [Authorio.configuration.mount_point, Authorio.configuration.authorization_endpoint].join("/")
22
- end
14
+ def self.authorization_path
15
+ [Authorio.configuration.mount_point, Authorio.configuration.authorization_endpoint].join('/')
16
+ end
23
17
 
24
- def self.token_path
25
- return [Authorio.configuration.mount_point, Authorio.configuration.token_endpoint].join("/")
26
- end
18
+ def self.token_path
19
+ [Authorio.configuration.mount_point, Authorio.configuration.token_endpoint].join('/')
20
+ end
27
21
  end
@@ -1,17 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class InstallGenerator < Rails::Generators::Base
3
-
4
5
  def self.source_paths
5
6
  paths = []
6
7
  paths << File.expand_path('../templates', "../../#{__FILE__}")
7
8
  paths << File.expand_path('../templates', "../#{__FILE__}")
8
- paths << File.expand_path('../templates', __FILE__)
9
+ paths << File.expand_path('templates', __dir__)
9
10
  paths.flatten
10
11
  end
11
12
 
12
13
  def add_files
13
14
  template 'authorio.rb', 'config/initializers/authorio.rb'
14
15
  end
15
-
16
16
  end
17
17
  end
@@ -1,24 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Configuration for Authorio IndieAuth authentication
2
4
 
3
5
  Authorio.configure do |config|
6
+ # Mount point for Authorio URLs. Typically you would call this in your routes.rb
7
+ # as mount Authorio::Engine, at: mount_point
8
+ # But Authorio needs to know its own mount point, so we define it here and use a custom mount command in the config
9
+ # config.mount_point = "authorio"
4
10
 
5
- # Mount point for Authorio URLs. Typically you would call this in your routes.rb
6
- # as mount Authorio::Engine, at: mount_point
7
- # But Authorio needs to know its own mount point, so we define it here and use a custom mount command in the config
8
- # config.mount_point = "authorio"
11
+ # The path where clients will be redirected to provide authentication
12
+ # config.authorization_endpoint = "auth"
9
13
 
10
- # The path where clients will be redirected to provide authentication
11
- # config.authorization_endpoint = "auth"
14
+ # The path for token requests
15
+ # config.token_endpoint = "token"
12
16
 
13
- # The path for token requests
14
- # config.token_endpoint = "token"
17
+ # Set to true to enable multiple user accounts. By default (in single user mode)
18
+ # there is only one user, and therefore you do not need to enter a username
19
+ # config.multiuser = false
15
20
 
16
- # How long tokens will last before expiring
17
- # config.token_expiration = 4.weeks
21
+ # How long tokens will last before expiring
22
+ # config.token_expiration = 4.weeks
18
23
 
19
- # Enable local session lifetime to keep yourself "logged in" to your own server
20
- # If set to eg:
21
- # config.local_session_lifetime = 30.days
22
- # then you will only have to enter your password every 30 days. Default is off (nil)
23
- # config.local_session_lifetime = nil
24
+ # Enable local session lifetime to keep yourself "logged in" to your own server
25
+ # If set to eg:
26
+ # config.local_session_lifetime = 30.days
27
+ # then you will only have to enter your password every 30 days. Default is off (nil)
28
+ # config.local_session_lifetime = nil
24
29
  end
@@ -1,18 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  namespace :authorio do
2
- desc "Set password for initial Authorio user"
3
- require 'io/console'
4
+ desc 'Set password for initial Authorio user'
5
+ require 'io/console'
4
6
 
5
- def input_no_echo(prompt)
6
- print("\n#{prompt}")
7
- STDIN.noecho(&:gets).chop
8
- end
7
+ def input_no_echo(prompt)
8
+ print("\n#{prompt}")
9
+ $stdin.noecho(&:gets).chop
10
+ end
9
11
 
10
- task :password => :environment do
11
- passwd = input_no_echo("Enter new password: ")
12
- passwd_confirm = input_no_echo("Confirm password: ")
13
- user = Authorio::User.
14
- create_with(password: passwd, password_confirmation:passwd_confirm).
15
- find_or_create_by!(profile_path: '/')
16
- puts("\nPassword set")
17
- end
12
+ task password: :environment do
13
+ passwd = input_no_echo('Enter new password: ')
14
+ passwd_confirm = input_no_echo('Confirm password: ')
15
+ Authorio::User.create_with(password: passwd, password_confirmation: passwd_confirm)
16
+ .find_or_create_by!(profile_path: '/')
17
+ puts("\nPassword set")
18
+ end
18
19
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: authorio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.8.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Meckler
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-09 00:00:00.000000000 Z
11
+ date: 2022-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,20 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 6.1.3
20
- - - ">="
21
- - !ruby/object:Gem::Version
22
- version: 6.1.3.2
19
+ version: '7.0'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - "~>"
28
25
  - !ruby/object:Gem::Version
29
- version: 6.1.3
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: 6.1.3.2
26
+ version: '7.0'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: bcrypt
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,6 +38,20 @@ dependencies:
44
38
  - - "~>"
45
39
  - !ruby/object:Gem::Version
46
40
  version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: jbuilder
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
47
55
  - !ruby/object:Gem::Dependency
48
56
  name: factory_bot_rails
49
57
  requirement: !ruby/object:Gem::Requirement
@@ -125,8 +133,14 @@ files:
125
133
  - app/models/authorio/token.rb
126
134
  - app/models/authorio/user.rb
127
135
  - app/views/authorio/auth/authorization_interface.html.erb
136
+ - app/views/authorio/auth/issue_token.json.jbuilder
137
+ - app/views/authorio/auth/send_profile.json.jbuilder
138
+ - app/views/authorio/auth/verify_token.json.jbuilder
128
139
  - app/views/authorio/sessions/new.html.erb
140
+ - app/views/authorio/users/_profile.json.jbuilder
129
141
  - app/views/authorio/users/edit.html.erb
142
+ - app/views/authorio/users/show.html.erb
143
+ - app/views/authorio/users/verify.html.erb
130
144
  - app/views/layouts/authorio/main.html.erb
131
145
  - app/views/shared/_login_form.html.erb
132
146
  - config/routes.rb
@@ -136,6 +150,8 @@ files:
136
150
  - db/migrate/20210723161041_add_expiry_to_tokens.rb
137
151
  - db/migrate/20210726164625_create_authorio_sessions.rb
138
152
  - db/migrate/20210801184120_add_profile_to_users.rb
153
+ - db/migrate/20210817010101_change_path_to_username_in_users.rb
154
+ - db/migrate/20210831155106_add_code_challenge_to_requests.rb
139
155
  - lib/authorio.rb
140
156
  - lib/authorio/configuration.rb
141
157
  - lib/authorio/engine.rb
@@ -145,28 +161,29 @@ files:
145
161
  - lib/generators/authorio/install/install_generator.rb
146
162
  - lib/generators/authorio/install/templates/authorio.rb
147
163
  - lib/tasks/authorio_tasks.rake
148
- homepage:
164
+ homepage: https://blog.reiterate.app/tag/authorio/
149
165
  licenses:
150
166
  - MIT
151
167
  metadata:
152
168
  source_code_uri: https://github.com/reiterate-app/authorio
153
- post_install_message:
169
+ changelog_uri: https://github.com/reiterate-app/authorio/blob/master/CHANGELOG.md
170
+ post_install_message:
154
171
  rdoc_options: []
155
172
  require_paths:
156
173
  - lib
157
174
  required_ruby_version: !ruby/object:Gem::Requirement
158
175
  requirements:
159
- - - ">="
176
+ - - "~>"
160
177
  - !ruby/object:Gem::Version
161
- version: '0'
178
+ version: '3.0'
162
179
  required_rubygems_version: !ruby/object:Gem::Requirement
163
180
  requirements:
164
181
  - - ">="
165
182
  - !ruby/object:Gem::Version
166
183
  version: '0'
167
184
  requirements: []
168
- rubygems_version: 3.2.11
169
- signing_key:
185
+ rubygems_version: 3.3.10
186
+ signing_key:
170
187
  specification_version: 4
171
188
  summary: Indieauth Authentication endpoint for Rails
172
189
  test_files: []