yiffspace-auth 0.0.1
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 +7 -0
- data/CHANGELOG.md +4 -0
- data/LICENSE +20 -0
- data/README.md +41 -0
- data/Rakefile +10 -0
- data/app/controllers/yiff_space/auth/application_controller.rb +11 -0
- data/app/controllers/yiff_space/auth/root_controller.rb +49 -0
- data/app/controllers/yiff_space/auth/webhook_controller.rb +73 -0
- data/app/helpers/yiff_space/auth_helper.rb +6 -0
- data/app/views/yiff_space/auth/root/permissions.html.erb +14 -0
- data/config/routes.rb +12 -0
- data/lib/tasks/yiffspace_auth_tasks.rake +41 -0
- data/lib/yiffspace/auth/api_user.rb +43 -0
- data/lib/yiffspace/auth/auth_info/anonymous.rb +60 -0
- data/lib/yiffspace/auth/auth_info.rb +78 -0
- data/lib/yiffspace/auth/client.rb +56 -0
- data/lib/yiffspace/auth/discord_info.rb +65 -0
- data/lib/yiffspace/auth/engine.rb +49 -0
- data/lib/yiffspace/auth/helper.rb +174 -0
- data/lib/yiffspace/auth/permissions.rb +87 -0
- data/lib/yiffspace/auth/set_client_name.rb +15 -0
- data/lib/yiffspace/auth/user_info/anonymous.rb +72 -0
- data/lib/yiffspace/auth/user_info.rb +87 -0
- data/lib/yiffspace/auth/version.rb +7 -0
- data/lib/yiffspace/auth.rb +81 -0
- data/lib/yiffspace/core_ext/action_dispatch/set_auth_client/scoped.rb +13 -0
- data/lib/yiffspace/core_ext/action_dispatch/set_auth_client.rb +13 -0
- data/lib/yiffspace/core_ext/logto/named_session_storage.rb +7 -0
- data/lib/yiffspace/extensions/action_dispatch/set_auth_client/scoped.rb +15 -0
- data/lib/yiffspace/extensions/action_dispatch/set_auth_client.rb +13 -0
- data/lib/yiffspace/extensions/logto/named_session_storage.rb +22 -0
- data/lib/yiffspace/logto_management_client.rb +136 -0
- data/lib/yiffspace/serializers/anonymous_auth_info_serializer.rb +23 -0
- data/lib/yiffspace/serializers/anonymous_user_info_serializer.rb +23 -0
- data/lib/yiffspace/serializers/auth_info_serializer.rb +23 -0
- data/lib/yiffspace/serializers/discord_info_serializer.rb +23 -0
- data/lib/yiffspace/serializers/permissions_serializer.rb +23 -0
- data/lib/yiffspace/serializers/user_info_serializer.rb +23 -0
- data/lib/yiffspace/utils/user_deduper.rb +66 -0
- metadata +153 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: eaf8f70d591dd62b1fe71e4c6b0269b89178dbbfc96aa00d2f220dc0c051b625
|
|
4
|
+
data.tar.gz: 60e9eb43743d443d4cced565c7891fd491344d5274317b72a2a599d9c5118cb0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: eca3a821613c9e57b8216ecc8a80608bd2068b31a2521d281183dbe1bd3dd2750111dfa20727912d6076ab08e1a53750b77d8ff0a111602792e791d3bd47fa96
|
|
7
|
+
data.tar.gz: 3df50557690c89ffc87e32a94c30b9f3e664702a168fb441cb5a0aa3897928114f169a3da43b8f09bd4dca599906ae42a823d9a66a246e6336a93652584f5a3c
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Donovan_DMC
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# YiffSpace::Auth
|
|
2
|
+
|
|
3
|
+
Logto-based auth engine for https://yiff.space and related projects. Extracted
|
|
4
|
+
from the [yiffspace](https://github.com/YiffSpace/Gem) gem, which this gem
|
|
5
|
+
depends on.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "yiffspace-auth"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
$ bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Development
|
|
22
|
+
|
|
23
|
+
This gem depends on [`yiffspace`](https://github.com/YiffSpace/Gem) via git
|
|
24
|
+
(see the `Gemfile`). To develop against a local checkout of that repo instead
|
|
25
|
+
of the pushed `master` branch, point Bundler at it with a local override:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
$ bundle config set local.yiffspace ../Gem
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This is stored in `.bundle/config` (gitignored) and doesn't touch the
|
|
32
|
+
`Gemfile`/`Gemfile.lock`, so it only affects your machine. Remove it with
|
|
33
|
+
`bundle config unset local.yiffspace` to go back to the pushed branch.
|
|
34
|
+
|
|
35
|
+
## Contributing
|
|
36
|
+
|
|
37
|
+
Go away
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Auth
|
|
5
|
+
class RootController < ApplicationController
|
|
6
|
+
include(Helper)
|
|
7
|
+
|
|
8
|
+
before_action(:sync_auth_if_dirty!)
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
client.sign_in(redirect_uri: auth_client_config.redirect_uri, post_redirect_uri: params[:path] || "/")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def cb
|
|
15
|
+
client.handle_sign_in_callback(url: request.original_url)
|
|
16
|
+
user = client.fetch_user_info
|
|
17
|
+
self.user = UserInfo.new(id: user["identities"]["discord"]["userId"], user: user, discord: user["identities"]["discord"]["details"]["rawData"])
|
|
18
|
+
token = client.access_token_claims(resource: auth_client_config.resource)
|
|
19
|
+
self.auth = AuthInfo.new(id: user["identities"]["discord"]["userId"], token: token, permissions: token["scope"].split, roles: user["roles"], client_id: auth_client_config.client_id)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def permissions; end
|
|
23
|
+
|
|
24
|
+
def logout
|
|
25
|
+
client.sign_out(post_logout_redirect_uri: request.base_url)
|
|
26
|
+
full_reset!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def debug
|
|
30
|
+
return render("yiffspace/error", locals: { message: "Access Denied" }, status: :forbidden) unless YiffSpace::Auth.enable_debug_action?
|
|
31
|
+
|
|
32
|
+
render(json: {
|
|
33
|
+
env: request.env.select { |env| env.start_with?("yiffspace.") },
|
|
34
|
+
params: params,
|
|
35
|
+
session: session,
|
|
36
|
+
client: auth_client_config.as_json.merge(client_secret: "[REDACTED]"),
|
|
37
|
+
user: client.fetch_user_info,
|
|
38
|
+
token: client.access_token_claims(resource: auth_client_config.resource),
|
|
39
|
+
})
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def client
|
|
45
|
+
@client ||= auth_client_config.logto(self)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require("openssl")
|
|
4
|
+
require("active_support/security_utils")
|
|
5
|
+
|
|
6
|
+
module YiffSpace
|
|
7
|
+
module Auth
|
|
8
|
+
class WebhookController < ApplicationController
|
|
9
|
+
skip_before_action(:verify_authenticity_token)
|
|
10
|
+
|
|
11
|
+
HANDLED_EVENTS = %w[
|
|
12
|
+
User.Roles.Updated
|
|
13
|
+
User.SuspensionStatus.Updated
|
|
14
|
+
User.Deleted
|
|
15
|
+
Role.Scopes.Updated
|
|
16
|
+
].freeze
|
|
17
|
+
DIRTY_FLAG_TTL = 24.hours
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
unless verify_signature
|
|
21
|
+
render(plain: "Unauthorized", status: :unauthorized)
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
payload = JSON.parse(request.raw_post)
|
|
26
|
+
event = payload["event"]
|
|
27
|
+
|
|
28
|
+
handle_event(event, payload) if HANDLED_EVENTS.include?(event)
|
|
29
|
+
|
|
30
|
+
head(:ok)
|
|
31
|
+
rescue JSON::ParserError
|
|
32
|
+
render(plain: "Bad Request", status: :bad_request)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def verify_signature
|
|
38
|
+
secret = auth_client_config.logto_webhook_secret
|
|
39
|
+
return true if secret.blank?
|
|
40
|
+
|
|
41
|
+
received = request.headers["logto-signature-sha-256"].to_s
|
|
42
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, request.raw_post)
|
|
43
|
+
ActiveSupport::SecurityUtils.secure_compare(received, expected)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def handle_event(event, payload)
|
|
47
|
+
data = payload["data"] || {}
|
|
48
|
+
|
|
49
|
+
if event == "Role.Scopes.Updated"
|
|
50
|
+
handle_role_scopes_updated(data)
|
|
51
|
+
else
|
|
52
|
+
user_id = data["id"]
|
|
53
|
+
mark_dirty(user_id) if user_id.present?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def handle_role_scopes_updated(data)
|
|
58
|
+
role_id = data["id"]
|
|
59
|
+
return if role_id.blank?
|
|
60
|
+
|
|
61
|
+
management = auth_client_config.logto_management
|
|
62
|
+
users = management.get_users_with_role(role_id)
|
|
63
|
+
users.each { |u| mark_dirty(u["id"]) }
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
Rails.logger.error("[YiffSpace::Auth::WebhookController] Role.Scopes.Updated fan-out failed: #{e.message}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def mark_dirty(user_id)
|
|
69
|
+
Rails.cache.write(format(Helper::DIRTY_FLAG_KEY, user_id), true, expires_in: DIRTY_FLAG_TTL)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<h1 class="title">Permissions</h1>
|
|
2
|
+
<% if logged_in? %>
|
|
3
|
+
<% if auth.permissions.any? %>
|
|
4
|
+
<ul class="permissions-list">
|
|
5
|
+
<% auth.permissions.each do |perm| %>
|
|
6
|
+
<li class="permission"><%= perm %></li>
|
|
7
|
+
<% end %>
|
|
8
|
+
</ul>
|
|
9
|
+
<% else %>
|
|
10
|
+
<p class="message">You have no permissions.</p>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% else %>
|
|
13
|
+
<p class="message">You are not logged in.</p>
|
|
14
|
+
<% end %>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
YiffSpace::Auth::Engine.routes.draw do
|
|
4
|
+
constraints(YiffSpace::Auth::SetClientName.default) do
|
|
5
|
+
post(:webhook, controller: :webhook)
|
|
6
|
+
get(:cb, controller: :root)
|
|
7
|
+
get(:logout, controller: :root)
|
|
8
|
+
get(:permissions, controller: :root)
|
|
9
|
+
get(:debug, controller: :root) if YiffSpace::Auth.enable_debug_action?
|
|
10
|
+
root(action: :show, controller: :root, as: :auth)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace(:yiffspace) do
|
|
4
|
+
desc("Report and interactively remove duplicate/orphaned Logto users left behind by " \
|
|
5
|
+
"get_or_create_user races. Prompts for confirmation before deleting each one. " \
|
|
6
|
+
"Pass AUTH_CLIENT=name to use a registered auth client other than :default " \
|
|
7
|
+
"(e.g. AUTH_CLIENT=yiffyapi_manage).")
|
|
8
|
+
task(dedupe_users: :environment) do
|
|
9
|
+
client_name = (ENV["AUTH_CLIENT"] || YiffSpace::Auth::DEFAULT_CLIENT_NAME).to_sym
|
|
10
|
+
management = YiffSpace::Auth[client_name].logto_management
|
|
11
|
+
|
|
12
|
+
puts("Scanning users via auth client #{client_name.inspect}...")
|
|
13
|
+
result = YiffSpace::Utils::UserDeduper.scan(management)
|
|
14
|
+
|
|
15
|
+
puts("\n#{result[:conflicts].size} discord id(s) with more than one linked user (ambiguous - not handled by this task, review manually):")
|
|
16
|
+
result[:conflicts].each do |discord_id, group|
|
|
17
|
+
puts(" discordId=#{discord_id}: #{group.map { |u| "#{u.id} (#{u.name})" }.join(', ')}")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
puts("\n#{result[:unresolved_orphans].size} orphaned user(s) with no matching linked account (not handled by this task, review manually):")
|
|
21
|
+
result[:unresolved_orphans].each do |user|
|
|
22
|
+
puts(" #{user.id} (name=#{user.name}, avatar=#{user.data.avatar})")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
puts("\n#{result[:deletable_orphans].size} confirmed duplicate(s) - orphan left by a losing get_or_create_user race:")
|
|
26
|
+
if result[:deletable_orphans].empty?
|
|
27
|
+
puts(" none found")
|
|
28
|
+
else
|
|
29
|
+
result[:deletable_orphans].each do |orphan, keeper|
|
|
30
|
+
print(" delete #{orphan.id} (name=#{orphan.name}, avatar=#{orphan.data.avatar}), keeping #{keeper.id} (name=#{keeper.name}) for discordId=#{YiffSpace::Utils::UserDeduper.discord_id_for(keeper)}? [y/N] ")
|
|
31
|
+
answer = $stdin.gets&.strip&.downcase
|
|
32
|
+
if answer == "y"
|
|
33
|
+
management.delete_user(orphan.id)
|
|
34
|
+
puts(" deleted #{orphan.id}")
|
|
35
|
+
else
|
|
36
|
+
puts(" skipped")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Auth
|
|
5
|
+
class ApiUser
|
|
6
|
+
attr_reader(:data)
|
|
7
|
+
|
|
8
|
+
def initialize(data)
|
|
9
|
+
@data = Utils::OpenHash.from(data)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
delegate(:id, :name, to: :data)
|
|
13
|
+
|
|
14
|
+
def discord_id
|
|
15
|
+
data.identities.discord.userId
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def created_at
|
|
19
|
+
Time.zone.at(data.createdAt)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def updated_at
|
|
23
|
+
Time.zone.at(data.updatedAt)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def last_sign_in_at
|
|
27
|
+
data.lastSignInAt.nil? ? nil : Time.zone.at(data.lastSignInAt)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def discord
|
|
31
|
+
DiscordInfo.new(data.identities.discord.details.rawData)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def avatar
|
|
35
|
+
Images::Avatar.get_for(discord_id, :discord)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def banner
|
|
39
|
+
Images::Banner.get_for(discord_id, :discord)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require("singleton")
|
|
4
|
+
|
|
5
|
+
module YiffSpace
|
|
6
|
+
module Auth
|
|
7
|
+
class AuthInfo
|
|
8
|
+
class Anonymous
|
|
9
|
+
include(::Singleton)
|
|
10
|
+
|
|
11
|
+
%i[id token].each do |attr|
|
|
12
|
+
define_method(attr) { |*, **| raise(NotImplementedError, "#{attr} is not present on anonymous auth") }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def roles
|
|
16
|
+
[]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def permissions
|
|
20
|
+
Permissions.new([])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def has_permission?(*)
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def anonymous?
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# this feels wrong, but it hopefully shouldn't break anything
|
|
32
|
+
def present?
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def blank?
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def serializable_hash(*)
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_session
|
|
45
|
+
serializable_hash
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.from_json(*)
|
|
49
|
+
Anonymous.instance
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.from_session(data)
|
|
53
|
+
return nil if data.blank?
|
|
54
|
+
|
|
55
|
+
from_json(data)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Auth
|
|
5
|
+
class AuthInfo
|
|
6
|
+
attr_reader(:id, :token, :roles, :permissions, :client_id)
|
|
7
|
+
|
|
8
|
+
# @param id String
|
|
9
|
+
# @param roles Array(String)
|
|
10
|
+
# @param permissions Array(String)
|
|
11
|
+
# @param token LogtoClient::AccessTokenClaims
|
|
12
|
+
# @param client_id String
|
|
13
|
+
def initialize(id:, roles:, permissions:, token:, client_id:)
|
|
14
|
+
raise(ArgumentError, "no id provided") if id.blank?
|
|
15
|
+
raise(ArgumentError, "no token provided") if token.blank?
|
|
16
|
+
raise(ArgumentError, "no client id provided") if client_id.blank?
|
|
17
|
+
|
|
18
|
+
@id = id
|
|
19
|
+
@token = token
|
|
20
|
+
@roles = Array(roles)
|
|
21
|
+
@permissions = Permissions.new(permissions, separator: YiffSpace::Auth.get_by_id(client_id).permissions_separator)
|
|
22
|
+
@client_id = client_id
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def anonymous?
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# this feels wrong, but it hopefully shouldn't break anything
|
|
30
|
+
def present?
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def blank?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def has_permission?(name)
|
|
39
|
+
permissions.include?(name.to_s)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def serializable_hash(*)
|
|
43
|
+
{
|
|
44
|
+
"id" => id,
|
|
45
|
+
"token" => token.as_json,
|
|
46
|
+
"roles" => roles,
|
|
47
|
+
"permissions" => permissions.values,
|
|
48
|
+
"client_id" => client_id,
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_session
|
|
53
|
+
serializable_hash
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.from_json(data)
|
|
57
|
+
raise(ArgumentError, "invalid data") if data.blank?
|
|
58
|
+
|
|
59
|
+
data = JSON.parse(data) if data.is_a?(String)
|
|
60
|
+
data = ::YiffSpace::Utils::OpenHash.from(data)
|
|
61
|
+
|
|
62
|
+
new(
|
|
63
|
+
id: data.id,
|
|
64
|
+
token: data.token,
|
|
65
|
+
roles: data.roles,
|
|
66
|
+
permissions: data.permissions,
|
|
67
|
+
client_id: data.client_id,
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.from_session(data)
|
|
72
|
+
return nil if data.blank?
|
|
73
|
+
|
|
74
|
+
from_json(data)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative("../core_ext/logto/named_session_storage")
|
|
4
|
+
require("httparty")
|
|
5
|
+
|
|
6
|
+
module YiffSpace
|
|
7
|
+
module Auth
|
|
8
|
+
class Client
|
|
9
|
+
attr_accessor(:client_id, :client_secret, :scopes, :resource,
|
|
10
|
+
:redirect_uri, :server_url, :auth_session_key, :user_session_key,
|
|
11
|
+
:update_discord_images, :permissions_separator, :logto_webhook_secret)
|
|
12
|
+
|
|
13
|
+
attr_reader(:name)
|
|
14
|
+
|
|
15
|
+
def initialize(name)
|
|
16
|
+
@name = name.to_sym
|
|
17
|
+
@scopes = %i[openid offline_access profile roles identities custom_data]
|
|
18
|
+
@redirect_uri = "http://127.0.0.1:3000/auth/cb"
|
|
19
|
+
@server_url = "https://auth.yiff.space"
|
|
20
|
+
@auth_session_key = :"yiffspace_auth_#{name}"
|
|
21
|
+
@user_session_key = :"yiffspace_user_#{name}"
|
|
22
|
+
@update_discord_images = true
|
|
23
|
+
@permissions_separator = ":"
|
|
24
|
+
@logto_webhook_secret = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def logto(controller)
|
|
28
|
+
LogtoClient.new(
|
|
29
|
+
config: LogtoClient::Config.new(
|
|
30
|
+
endpoint: server_url,
|
|
31
|
+
app_id: client_id,
|
|
32
|
+
app_secret: client_secret,
|
|
33
|
+
scopes: scopes,
|
|
34
|
+
resources: [resource].compact,
|
|
35
|
+
),
|
|
36
|
+
# Allow the client to redirect to other hosts (i.e. your Logto tenant)
|
|
37
|
+
navigate: ->(uri) { controller.redirect_to(uri, allow_other_host: true) },
|
|
38
|
+
# Controller has access to the session object
|
|
39
|
+
storage: LogtoClient::NamedSessionStorage.new("yiffspace", controller.session, app_id: @name),
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def logto_management
|
|
44
|
+
@logto_management ||= LogtoManagementClient.new(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def fetch_discord_user(id)
|
|
48
|
+
response = HTTParty.get("https://discord.com/api/v10/users/#{id}", { headers: { "Authorization" => "Bot #{YiffSpace.config.discord_bot_token}" } })
|
|
49
|
+
return nil if response.code == 404
|
|
50
|
+
raise("failed to fetch discord user: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
|
|
51
|
+
|
|
52
|
+
DiscordInfo.new(response.parsed_response)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Auth
|
|
5
|
+
class DiscordInfo
|
|
6
|
+
# locale, mfa_enabled, and premium_type are not present for legacy users backfilled from api data,
|
|
7
|
+
# we don't need that data either way
|
|
8
|
+
ATTRIBUTES = %i[id flags avatar banner username global_name public_flags discriminator].freeze
|
|
9
|
+
attr_reader(:data, *ATTRIBUTES)
|
|
10
|
+
|
|
11
|
+
def initialize(options = {}, **kwargs)
|
|
12
|
+
@data = options.merge(kwargs).with_indifferent_access
|
|
13
|
+
ATTRIBUTES.each do |attr|
|
|
14
|
+
instance_variable_set("@#{attr}", @data[attr])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
return unless YiffSpace.config.auth.update_discord_images
|
|
18
|
+
|
|
19
|
+
last_avatar = Rails.cache.fetch("yiffspace:auth:discord_avatar_update:#{id}")
|
|
20
|
+
last_banner = Rails.cache.fetch("yiffspace:auth:discord_banner_update:#{id}")
|
|
21
|
+
|
|
22
|
+
if last_avatar != avatar
|
|
23
|
+
Rails.cache.write("yiffspace:auth:discord_avatar_update:#{id}", avatar, expires_in: 30.days)
|
|
24
|
+
update_avatar
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
return unless last_banner != banner
|
|
28
|
+
|
|
29
|
+
Rails.cache.write("yiffspace:auth:discord_banner_update:#{id}", banner, expires_in: 30.days)
|
|
30
|
+
update_banner
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update_avatar
|
|
34
|
+
Images::Avatar.get_for(id, :discord).update(avatar)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def update_banner
|
|
38
|
+
Images::Banner.get_for(id, :discord).update(banner)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def serializable_hash(*)
|
|
42
|
+
ATTRIBUTES.to_h { |key| [key.to_s, instance_variable_get("@#{key}")] }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_session
|
|
46
|
+
serializable_hash
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.from_json(data)
|
|
50
|
+
raise(ArgumentError, "invalid data") if data.blank?
|
|
51
|
+
|
|
52
|
+
data = JSON.parse(data) if data.is_a?(String)
|
|
53
|
+
data = ::YiffSpace::Utils::OpenHash.from(data)
|
|
54
|
+
|
|
55
|
+
new(data)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.from_session(data)
|
|
59
|
+
return nil if data.blank?
|
|
60
|
+
|
|
61
|
+
from_json(data)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require("rails")
|
|
4
|
+
|
|
5
|
+
module YiffSpace
|
|
6
|
+
module Auth
|
|
7
|
+
class Engine < ::Rails::Engine
|
|
8
|
+
isolate_namespace(YiffSpace::Auth)
|
|
9
|
+
config.root = File.expand_path("../../..", __dir__)
|
|
10
|
+
|
|
11
|
+
initializer("yiffspace.auth.serializers") do
|
|
12
|
+
ActiveSupport.on_load(:active_job) do
|
|
13
|
+
ActiveJob::Serializers.add_serializers(
|
|
14
|
+
Serializers::AnonymousAuthInfoSerializer,
|
|
15
|
+
Serializers::AnonymousUserInfoSerializer,
|
|
16
|
+
Serializers::AuthInfoSerializer,
|
|
17
|
+
Serializers::DiscordInfoSerializer,
|
|
18
|
+
Serializers::PermissionsSerializer,
|
|
19
|
+
Serializers::UserInfoSerializer,
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def for(name)
|
|
26
|
+
@instances ||= {}
|
|
27
|
+
@instances[name.to_sym] ||= begin
|
|
28
|
+
subclass = Class.new(self)
|
|
29
|
+
subclass.engine_name("yiffspace_auth_#{name}")
|
|
30
|
+
# Inherit isolation settings that aren't copied from the parent class
|
|
31
|
+
subclass.instance_variable_set(:@isolated, true)
|
|
32
|
+
subclass.routes.default_scope = { module: "yiff_space/auth" }
|
|
33
|
+
subclass.routes.draw do
|
|
34
|
+
constraints(SetClientName.new(name)) do
|
|
35
|
+
post(:webhook, controller: :webhook, action: :create)
|
|
36
|
+
get(:cb, controller: :root)
|
|
37
|
+
get(:logout, controller: :root)
|
|
38
|
+
get(:permissions, controller: :root)
|
|
39
|
+
get(:debug, controller: :root) if ::YiffSpace::Auth.enable_debug_action?
|
|
40
|
+
root(action: :show, controller: :root, as: :auth)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
subclass
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|