authorio 0.8.3 → 0.8.4

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