authorio 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/authorio/auth_controller.rb +67 -129
  3. data/app/controllers/authorio/authorio_controller.rb +23 -11
  4. data/app/controllers/authorio/sessions_controller.rb +6 -5
  5. data/app/controllers/authorio/users_controller.rb +13 -3
  6. data/app/helpers/authorio/tag_helper.rb +5 -5
  7. data/app/jobs/authorio/application_job.rb +2 -0
  8. data/app/models/authorio/application_record.rb +2 -0
  9. data/app/models/authorio/request.rb +37 -4
  10. data/app/models/authorio/session.rb +13 -10
  11. data/app/models/authorio/token.rb +16 -2
  12. data/app/models/authorio/user.rb +12 -3
  13. data/app/views/authorio/auth/authorization_interface.html.erb +2 -2
  14. data/app/views/authorio/auth/issue_token.json.jbuilder +7 -0
  15. data/app/views/authorio/auth/send_profile.json.jbuilder +3 -0
  16. data/app/views/authorio/auth/verify_token.json.jbuilder +5 -0
  17. data/app/views/authorio/sessions/new.html.erb +1 -2
  18. data/app/views/authorio/users/_profile.json.jbuilder +10 -0
  19. data/app/views/authorio/users/show.html.erb +18 -0
  20. data/app/views/authorio/users/verify.html.erb +1 -0
  21. data/app/views/shared/_login_form.html.erb +18 -13
  22. data/config/routes.rb +16 -11
  23. data/db/migrate/20210817010101_change_path_to_username_in_users.rb +7 -0
  24. data/lib/authorio/configuration.rb +14 -12
  25. data/lib/authorio/engine.rb +13 -11
  26. data/lib/authorio/exceptions.rb +20 -12
  27. data/lib/authorio/routes.rb +10 -7
  28. data/lib/authorio/version.rb +3 -1
  29. data/lib/authorio.rb +15 -21
  30. data/lib/generators/authorio/install/install_generator.rb +3 -3
  31. data/lib/generators/authorio/install/templates/authorio.rb +20 -15
  32. data/lib/tasks/authorio_tasks.rake +15 -14
  33. metadata +24 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5ecd8cf849002e116b21a3c4f0073fc17988e62791f356ab57a703397edc77c
4
- data.tar.gz: 4c0c4908c722b65ccd1559b5fea382231933e2840e5d0b6e08271ed5efd43f15
3
+ metadata.gz: 78659edadab6ff85d24828c318525bac7b62b4a76b9905e0a1d819ec266f9d46
4
+ data.tar.gz: 817fe77d1c1fd89e68df07a594725997d598e0f0027210d083762da50cb447fd
5
5
  SHA512:
6
- metadata.gz: 8bb01ec581f584fe9eadc7d77d477fa2f57e8883101ba51b5f8cb8729bf7486f061bc996a17cab023d476119dda8f37d7676a7f04e281180ad8dda8e649eb16c
7
- data.tar.gz: 7d8e0e19113cd7748a64212ee98f514ba953027409adf20a71b47f14c3e1c5ef0db28924bca9afa4fd498ea56a686b1169f2f8ceb7f53297472c6b9cd34d86cf
6
+ metadata.gz: 8205bfe7bc6798f560da3f56b19f26822653d6f6fb8baac925be38a713f871e1faf90f0492ea25187124cd98db928a7d875da47b53faf14afe1efc6418498dbd
7
+ data.tar.gz: 3c772c7777f016316911f36573f5d2982ca27a40edb18dcac16782fb380021971feaeea4271adebe411101b39a9bb544d3f27f88dec93d72aae6f0e5adc483fd
@@ -1,181 +1,119 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class AuthController < AuthorioController
3
5
  require 'uri'
4
6
  require 'digest'
5
7
 
6
8
  # 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]
9
+ protect_from_forgery with: :exception, except: %i[send_profile issue_token]
8
10
 
9
11
  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
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.'
13
21
  end
14
-
15
- helper_method :user_scope_description
16
22
 
17
23
  # GET /auth
18
24
  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}"
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
41
28
  end
42
29
 
43
30
  # POST /user/:id/authorize
44
31
  def authorize_user
45
- redirect_to session[:client_id] and return if params[:commit] == "Cancel"
32
+ redirect_to session[:client_id] and return if params[:commit] == 'Cancel'
46
33
 
47
34
  user = authenticate_user_from_session_or_password
48
- set_session_cookie(user) if auth_user_params[:remember_me]
49
-
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."
35
+ write_session_cookie(user) if auth_user_params[:remember_me]
36
+ redirect_to_client(user)
58
37
  end
59
38
 
60
39
  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
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
65
43
  end
66
44
 
67
45
  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
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
79
50
  end
80
51
 
81
52
  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
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
95
58
  end
96
59
 
97
60
  private
98
61
 
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!
73
+ end
74
+ end
75
+
99
76
  def scope_params
100
77
  params.require(:scope).permit(scope: [])
101
78
  end
102
79
 
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 }
80
+ def oauth_error(error, message = nil, status = :bad_request)
81
+ { json: { json: { error: error, error_message: message }.compact },
82
+ status: status }
107
83
  end
108
84
 
109
85
  def token_expired
110
- { json: {'error': 'invalid_token', 'error_message': 'The access token has expired' }, status: :unauthorized }
86
+ oauth_error('invalid_token', 'The access token has expired', :unauthorized)
111
87
  end
112
88
 
113
89
  def code_challenge_failed?
114
90
  # For now, if original request did not have code challenge, then we pass by default
115
- return false if session[:code_challenge].nil?
91
+ return unless session[:code_challenge]
92
+
116
93
  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
94
+ Base64.urlsafe_encode64(sha256) != session[:code_challenge]
151
95
  end
152
96
 
153
- def bearer_token
154
- bearer = /^Bearer /
155
- header = request.headers['Authorization']
156
- header.gsub(bearer, '') if header && header.match(bearer)
157
- end
97
+ def validate_request(request)
98
+ raise Exceptions::InvalidGrant, 'validation failed' if request.invalid?(params) || code_challenge_failed?
158
99
 
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
100
+ request
168
101
  end
169
102
 
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
- }
175
-
176
- def user_scope_description(scope)
177
- ScopeDescriptions.dig(scope.to_sym) || scope
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}"
178
110
  end
179
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
180
118
  end
181
119
  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,45 @@
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 invalid?(params)
20
+ redirect_uri != params[:redirect_uri] ||
21
+ client != params[:client_id] ||
22
+ created_at < Time.now - 10.minutes
23
+ end
24
+
25
+ def self.user_scope_description(scope)
26
+ USER_SCOPE_DESCRIPTION[scope.to_sym] || scope
10
27
  end
28
+
29
+ private
30
+
31
+ def set_code
32
+ self.code = SecureRandom.hex(20)
33
+ end
34
+
35
+ def sweep_requests
36
+ Request.where(client: client, authorio_user: authorio_user).destroy_all
37
+ end
38
+
39
+ USER_SCOPE_DESCRIPTION = {
40
+ profile: 'View basic profile information',
41
+ email: 'View your email address',
42
+ offline_access: 'Keep you logged in permanently (until revoked)'
43
+ }.freeze
11
44
  end
12
45
  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: @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(request.authorio_user)
4
+ if request.scope&.include? 'profile'
5
+ json.profile do
6
+ json.name(request.authorio_user.full_name)
7
+ json.call(request.authorio_user, :url, :photo)
8
+ json.email(request.authorio_user.email) if 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,18 +15,23 @@
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'>
31
36
  <% if cancel %>
32
37
  <%= form.submit("Cancel", class: 'btn btn-default auth-btn') %>
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
@@ -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 += %w[authorio/auth.css authorio/application.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.4'
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.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Meckler
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-09 00:00:00.000000000 Z
11
+ date: 2021-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -44,6 +44,20 @@ dependencies:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '3.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: jbuilder
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
47
61
  - !ruby/object:Gem::Dependency
48
62
  name: factory_bot_rails
49
63
  requirement: !ruby/object:Gem::Requirement
@@ -125,8 +139,14 @@ files:
125
139
  - app/models/authorio/token.rb
126
140
  - app/models/authorio/user.rb
127
141
  - app/views/authorio/auth/authorization_interface.html.erb
142
+ - app/views/authorio/auth/issue_token.json.jbuilder
143
+ - app/views/authorio/auth/send_profile.json.jbuilder
144
+ - app/views/authorio/auth/verify_token.json.jbuilder
128
145
  - app/views/authorio/sessions/new.html.erb
146
+ - app/views/authorio/users/_profile.json.jbuilder
129
147
  - app/views/authorio/users/edit.html.erb
148
+ - app/views/authorio/users/show.html.erb
149
+ - app/views/authorio/users/verify.html.erb
130
150
  - app/views/layouts/authorio/main.html.erb
131
151
  - app/views/shared/_login_form.html.erb
132
152
  - config/routes.rb
@@ -136,6 +156,7 @@ files:
136
156
  - db/migrate/20210723161041_add_expiry_to_tokens.rb
137
157
  - db/migrate/20210726164625_create_authorio_sessions.rb
138
158
  - db/migrate/20210801184120_add_profile_to_users.rb
159
+ - db/migrate/20210817010101_change_path_to_username_in_users.rb
139
160
  - lib/authorio.rb
140
161
  - lib/authorio/configuration.rb
141
162
  - lib/authorio/engine.rb
@@ -150,6 +171,7 @@ licenses:
150
171
  - MIT
151
172
  metadata:
152
173
  source_code_uri: https://github.com/reiterate-app/authorio
174
+ changelog_uri: https://github.com/reiterate-app/authorio/blob/master/CHANGELOG.md
153
175
  post_install_message:
154
176
  rdoc_options: []
155
177
  require_paths: