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