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.
- checksums.yaml +4 -4
- data/README.md +29 -0
- data/app/assets/stylesheets/authorio/auth.css +55 -1
- data/app/controllers/authorio/auth_controller.rb +69 -102
- data/app/controllers/authorio/authorio_controller.rb +78 -0
- data/app/controllers/authorio/sessions_controller.rb +33 -0
- data/app/controllers/authorio/users_controller.rb +34 -0
- data/app/helpers/authorio/tag_helper.rb +17 -0
- data/app/jobs/authorio/application_job.rb +2 -0
- data/app/models/authorio/application_record.rb +2 -0
- data/app/models/authorio/request.rb +48 -1
- data/app/models/authorio/session.rb +43 -0
- data/app/models/authorio/token.rb +23 -1
- data/app/models/authorio/user.rb +14 -0
- data/app/views/authorio/auth/authorization_interface.html.erb +14 -35
- data/app/views/authorio/auth/issue_token.json.jbuilder +7 -0
- data/app/views/authorio/auth/send_profile.json.jbuilder +3 -0
- data/app/views/authorio/auth/verify_token.json.jbuilder +5 -0
- data/app/views/authorio/sessions/new.html.erb +14 -0
- data/app/views/authorio/users/_profile.json.jbuilder +10 -0
- data/app/views/authorio/users/edit.html.erb +25 -0
- data/app/views/authorio/users/show.html.erb +18 -0
- data/app/views/authorio/users/verify.html.erb +1 -0
- data/app/views/layouts/authorio/main.html.erb +38 -0
- data/app/views/shared/_login_form.html.erb +41 -0
- data/config/routes.rb +15 -5
- data/db/migrate/20210723161041_add_expiry_to_tokens.rb +5 -0
- data/db/migrate/20210726164625_create_authorio_sessions.rb +12 -0
- data/db/migrate/20210801184120_add_profile_to_users.rb +8 -0
- data/db/migrate/20210817010101_change_path_to_username_in_users.rb +7 -0
- data/db/migrate/20210831155106_add_code_challenge_to_requests.rb +5 -0
- data/lib/authorio/configuration.rb +14 -9
- data/lib/authorio/engine.rb +11 -8
- data/lib/authorio/exceptions.rb +20 -3
- data/lib/authorio/routes.rb +10 -7
- data/lib/authorio/version.rb +3 -1
- data/lib/authorio.rb +15 -21
- data/lib/generators/authorio/install/install_generator.rb +3 -3
- data/lib/generators/authorio/install/templates/authorio.rb +22 -8
- data/lib/tasks/authorio_tasks.rake +15 -14
- metadata +49 -20
- data/app/controllers/authorio/application_controller.rb +0 -4
- data/app/controllers/authorio/helpers.rb +0 -17
- data/app/helpers/authorio/application_helper.rb +0 -4
- data/app/helpers/authorio/test_helper.rb +0 -4
- 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,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:
|
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:
|
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
|
data/app/models/authorio/user.rb
CHANGED
@@ -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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
<
|
6
|
-
<
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
37
|
-
</
|
14
|
+
<div class="col-md-4"></div>
|
15
|
+
</div>
|
16
|
+
</div>
|
@@ -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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|