authorio 0.8.1 → 0.8.5

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -0
  3. data/app/assets/stylesheets/authorio/auth.css +55 -1
  4. data/app/controllers/authorio/auth_controller.rb +69 -102
  5. data/app/controllers/authorio/authorio_controller.rb +78 -0
  6. data/app/controllers/authorio/sessions_controller.rb +33 -0
  7. data/app/controllers/authorio/users_controller.rb +34 -0
  8. data/app/helpers/authorio/tag_helper.rb +17 -0
  9. data/app/jobs/authorio/application_job.rb +2 -0
  10. data/app/models/authorio/application_record.rb +2 -0
  11. data/app/models/authorio/request.rb +48 -1
  12. data/app/models/authorio/session.rb +43 -0
  13. data/app/models/authorio/token.rb +23 -1
  14. data/app/models/authorio/user.rb +14 -0
  15. data/app/views/authorio/auth/authorization_interface.html.erb +14 -35
  16. data/app/views/authorio/auth/issue_token.json.jbuilder +7 -0
  17. data/app/views/authorio/auth/send_profile.json.jbuilder +3 -0
  18. data/app/views/authorio/auth/verify_token.json.jbuilder +5 -0
  19. data/app/views/authorio/sessions/new.html.erb +14 -0
  20. data/app/views/authorio/users/_profile.json.jbuilder +10 -0
  21. data/app/views/authorio/users/edit.html.erb +25 -0
  22. data/app/views/authorio/users/show.html.erb +18 -0
  23. data/app/views/authorio/users/verify.html.erb +1 -0
  24. data/app/views/layouts/authorio/main.html.erb +38 -0
  25. data/app/views/shared/_login_form.html.erb +41 -0
  26. data/config/routes.rb +15 -5
  27. data/db/migrate/20210723161041_add_expiry_to_tokens.rb +5 -0
  28. data/db/migrate/20210726164625_create_authorio_sessions.rb +12 -0
  29. data/db/migrate/20210801184120_add_profile_to_users.rb +8 -0
  30. data/db/migrate/20210817010101_change_path_to_username_in_users.rb +7 -0
  31. data/db/migrate/20210831155106_add_code_challenge_to_requests.rb +5 -0
  32. data/lib/authorio/configuration.rb +14 -9
  33. data/lib/authorio/engine.rb +11 -8
  34. data/lib/authorio/exceptions.rb +20 -3
  35. data/lib/authorio/routes.rb +10 -7
  36. data/lib/authorio/version.rb +3 -1
  37. data/lib/authorio.rb +15 -21
  38. data/lib/generators/authorio/install/install_generator.rb +3 -3
  39. data/lib/generators/authorio/install/templates/authorio.rb +22 -8
  40. data/lib/tasks/authorio_tasks.rake +15 -14
  41. metadata +49 -20
  42. data/app/controllers/authorio/application_controller.rb +0 -4
  43. data/app/controllers/authorio/helpers.rb +0 -17
  44. data/app/helpers/authorio/application_helper.rb +0 -4
  45. data/app/helpers/authorio/test_helper.rb +0 -4
  46. data/app/views/layouts/authorio/application.html.erb +0 -15
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authorio
4
+ # These helpers are provided to the main application
5
+ module TagHelper
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ helper_method :indieauth_tag if respond_to?(:helper_method)
10
+ end
11
+
12
+ def indieauth_tag
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
+ end
16
+ end
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,7 +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
8
+
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
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: client, authorio_user: 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
6
53
  end
7
54
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authorio
4
+ class Session < ApplicationRecord
5
+ # Implement a session cookie store based on best security practices
6
+ # See: https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence
7
+ belongs_to :authorio_user, class_name: '::Authorio::User'
8
+
9
+ # 1. Protect against having database stolen by only storing token hashes
10
+ attribute :token # This will not be persisted in the DB
11
+ has_secure_token
12
+
13
+ before_create do
14
+ self.expires_at = Time.now + Authorio.configuration.token_expiration
15
+ self.selector = SecureRandom.hex(12)
16
+ self.hashed_token = Digest::SHA256.hexdigest token
17
+ end
18
+
19
+ # 2. To guard against timing attacks, we lookup tokens based on a separate selector attribute
20
+ # and compare them using a secure time-constant comparison method
21
+ def self.find_by_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
+
26
+ session
27
+ end
28
+
29
+ def matches_cookie?(cookie)
30
+ _selector, token = cookie.split(':')
31
+ cookie_hashed_token = Digest::SHA256.hexdigest token
32
+ !expired? && ActiveSupport::SecurityUtils.secure_compare(cookie_hashed_token, hashed_token)
33
+ end
34
+
35
+ def expired?
36
+ expires_at < Time.now
37
+ end
38
+
39
+ def as_cookie
40
+ "#{selector}:#{token}"
41
+ end
42
+ end
43
+ end
@@ -1,8 +1,30 @@
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
+
10
+ before_create do
11
+ self.expires_at = Time.now + Authorio.configuration.token_expiration
12
+ end
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
+
20
+ def expired?
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)
28
+ end
7
29
  end
8
30
  end
@@ -1,5 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Authorio
2
4
  class User < ApplicationRecord
3
5
  has_secure_password
6
+
7
+ class << self
8
+ def find_by_url!(url)
9
+ find_by_username!(URI(url).path)
10
+ end
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
4
18
  end
5
19
  end
@@ -1,37 +1,16 @@
1
- <!DOCTYPE html>
2
- <html lang="en-GB">
3
- <head>
4
- <meta charset="utf-8">
5
- <title>Authorio Login</title>
6
- <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <link rel="stylesheet"
8
- href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
9
- integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
10
- crossorigin="anonymous">
11
- <%= stylesheet_link_tag "authorio/auth" %>
12
- </head>
13
- <body>
14
- <div class="container authorio-auth">
15
- <div class="row">
16
- <div class="col-md-4">
17
- </div>
18
- <div class="col-md-4 auth-panel">
19
- <h3>Authorio</h3>
20
- <div class="client-row">
21
- Authenticating with <span class="client"><%= params[:client_id] %>
22
- </div>
23
- <%= form_with(url: "authorize_user", method: :post) do |form| %>
24
- <%= form.label(:url, "User URL") %>
25
- <%= form.text_field(:url, value: @user_url, readonly: true) %>
26
- <%= form.label(:password, "Password") %>
27
- <%= form.password_field(:password, autofocus: true) %>
28
- <%= form.hidden_field(:client, value: params[:client_id]) %>
29
- <%= form.submit("Sign in", class: 'btn btn-success') %>
30
- <% end %>
31
- </div>
32
- <div class="col-md-4">
33
- </div>
1
+ <% content_for :title, "Authorio Login" %>
2
+
3
+ <div class="container authorio-auth">
4
+ <div class="row">
5
+ <div class="col-md-4"></div>
6
+ <div class="col-md-4 auth-panel">
7
+ <h3>Authorio</h3>
8
+ <div class="client-row">
9
+ <span class="client"><%= params[:client_id] %></span> wants to authenticate
10
+ <% if params[:scope] %>and also<% end %>
34
11
  </div>
12
+ <%= render 'shared/login_form', target: authorize_user_path, cancel: true %>
35
13
  </div>
36
- </body>
37
- </html>
14
+ <div class="col-md-4"></div>
15
+ </div>
16
+ </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
@@ -0,0 +1,14 @@
1
+ <%= stylesheet_link_tag "authorio/auth" %>
2
+ <% content_for :title, "Authorio Local Login" %>
3
+
4
+ <div class="container authorio-auth">
5
+ <div class="row">
6
+ <div class="col-md-4"></div>
7
+ <div class="col-md-4 auth-panel">
8
+ <h3>Authorio</h3>
9
+ <div class="client-row">Local Login</div>
10
+ <%= render 'shared/login_form', target: session_path(@session), cancel: false %>
11
+ </div>
12
+ <div class="col-md-4"></div>
13
+ </div>
14
+ </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,25 @@
1
+ <%= stylesheet_link_tag "authorio/auth" %>
2
+ <% content_for :title, "Account Settings" %>
3
+
4
+ <div class="container authorio-auth">
5
+ <div class="row">
6
+ <div class="col-md-4"></div>
7
+ <div class="col-md-4 auth-panel">
8
+ <h3>Account Settings</h3>
9
+ <%= form_with model: @user do |form| %>
10
+ <%= form.label(:full_name, "Full Name") %>
11
+ <%= form.text_field(:full_name) %>
12
+ <%= form.label(:url, "URL") %>
13
+ <%= form.text_field(:url) %>
14
+ <%= form.label(:photo, "Photo URL") %>
15
+ <%= form.text_field(:photo) %>
16
+ <%= form.label(:email, "Email") %>
17
+ <%= form.text_field(:email) %>
18
+ <div class='auth-btn-row'>
19
+ <%= form.submit("Save Changes", class: 'btn btn-success auth-btn') %>
20
+ </div>
21
+ <% end -%>
22
+ </div>
23
+ <div class="col-md-4"></div>
24
+ </div>
25
+ </div>
@@ -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 %>
@@ -0,0 +1,38 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= yield(:title) %></title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ <meta charset="utf-8">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
9
+ <link rel="stylesheet"
10
+ href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
11
+ integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
12
+ crossorigin="anonymous">
13
+
14
+ <%= stylesheet_link_tag "authorio/application", media: "all" %>
15
+ <%= stylesheet_link_tag "authorio/auth" %>
16
+ </head>
17
+ <body>
18
+
19
+ <% if logged_in? %>
20
+ <div class="topbar">
21
+ <ul>
22
+ <li>Authorio</li>
23
+ <li><a href="<%= edit_user_path(current_user) -%>">Account Settings</a></li>
24
+ <li><a href="<%= logout_path(method: :delete) -%>">Log Out</a></li>
25
+ </ul>
26
+ </div>
27
+ <% end -%>
28
+
29
+ <% flash.each do |key, value| %>
30
+ <div class="alert alert-warning">
31
+ <p><%= value %></p>
32
+ </div>
33
+ <% end %>
34
+
35
+ <%= yield %>
36
+
37
+ </body>
38
+ </html>
@@ -0,0 +1,41 @@
1
+ <%= form_with(url: target, method: :post) do |form| %>
2
+ <% if params[:scope] %>
3
+ <%= fields_for :scope do |req_scope| %>
4
+ <div class="scopes">
5
+ <ul class="scope">
6
+ <% for scope in params[:scope].split %>
7
+ <li>
8
+ <%= label_tag(:scope, class: 'scope-label') do %>
9
+ <%= req_scope.check_box(:scope, {multiple: true, checked: true}, scope, nil) %>
10
+ <%= user_scope_description scope %>
11
+ <% end -%>
12
+ </li>
13
+ <%- end %>
14
+ </ul>
15
+ </div>
16
+ <% end %>
17
+ <% end -%>
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 %>
32
+ <% end %>
33
+ <% end %>
34
+ <% end -%>
35
+ <div class='auth-btn-row'>
36
+ <%= form.submit("Sign in", class: 'btn btn-success auth-btn') %>
37
+ <% if cancel %>
38
+ <%= form.submit("Cancel", class: 'btn btn-default auth-btn') %>
39
+ <% end %>
40
+ </div>
41
+ <% end %>
data/config/routes.rb CHANGED
@@ -1,7 +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
- post '/authorize_user', controller: 'auth', action: 'authorize_user'
5
- get Authorio.configuration.token_endpoint, controller: 'auth', action: 'verify_token'
6
- post Authorio.configuration.token_endpoint, controller: 'auth', action: 'issue_token'
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
7
17
  end