shakha 0.2.0 → 0.3.0
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 +3 -4
- data/app/controllers/shakha/application_controller.rb +1 -1
- data/app/controllers/shakha/auth_controller.rb +96 -198
- data/app/controllers/shakha/session_controller.rb +15 -60
- data/app/models/shakha/client.rb +0 -4
- data/app/models/shakha/session.rb +0 -5
- data/app/models/shakha/user.rb +3 -5
- data/app/views/shakha/auth/new.html.erb +6 -18
- data/lib/shakha/config.rb +5 -29
- data/lib/shakha/config_validator.rb +1 -2
- data/lib/shakha/controller_helpers.rb +14 -33
- data/lib/shakha/engine.rb +6 -16
- data/lib/shakha/error_handler.rb +2 -3
- data/lib/shakha/providers/base.rb +27 -0
- data/lib/shakha/providers/github.rb +99 -0
- data/lib/shakha/providers/google.rb +90 -0
- data/lib/shakha/providers.rb +19 -0
- data/lib/shakha/version.rb +1 -1
- data/lib/shakha.rb +2 -28
- metadata +5 -8
- data/app/controllers/shakha/jwks_controller.rb +0 -10
- data/app/controllers/shakha/openid_controller.rb +0 -21
- data/app/views/shakha/auth/sessions.html.erb +0 -66
- data/lib/shakha/auditable.rb +0 -47
- data/lib/shakha/jwt_handler.rb +0 -127
- data/lib/shakha/middleware.rb +0 -49
- data/lib/shakha/pairwise.rb +0 -26
|
@@ -7,15 +7,14 @@ module Shakha
|
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
8
|
|
|
9
9
|
included do
|
|
10
|
-
helper_method :
|
|
10
|
+
helper_method :current_user, :current_session, :signed_in?
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
private
|
|
14
14
|
|
|
15
15
|
def current_session
|
|
16
16
|
return @current_session if defined?(@current_session)
|
|
17
|
-
|
|
18
|
-
@current_session = find_session || authenticate_from_bearer || authenticate_from_cookie
|
|
17
|
+
@current_session = find_session_from_cookie || find_session_from_bearer
|
|
19
18
|
end
|
|
20
19
|
|
|
21
20
|
def current_user
|
|
@@ -29,42 +28,24 @@ module Shakha
|
|
|
29
28
|
def authenticate!
|
|
30
29
|
return if signed_in?
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return unless (token = bearer_token)
|
|
37
|
-
|
|
38
|
-
payload = Shakha.verify_token(token)
|
|
39
|
-
find_session_by_jti(payload["jti"])
|
|
31
|
+
respond_to do |format|
|
|
32
|
+
format.html { redirect_to shakha.new_auth_path(return_to: request.fullpath) }
|
|
33
|
+
format.json { render json: { error: "Authentication required" }, status: :unauthorized }
|
|
34
|
+
end
|
|
40
35
|
end
|
|
41
36
|
|
|
42
|
-
def
|
|
43
|
-
|
|
37
|
+
def find_session_from_cookie
|
|
38
|
+
token = cookies.encrypted[:shakha_session_token]
|
|
39
|
+
return unless token
|
|
40
|
+
Shakha::Session.active.find_by(token: token)
|
|
44
41
|
end
|
|
45
42
|
|
|
46
|
-
def
|
|
47
|
-
pattern = /^Bearer /
|
|
43
|
+
def find_session_from_bearer
|
|
48
44
|
header = request.headers["Authorization"]
|
|
49
|
-
return unless header&.
|
|
50
|
-
|
|
51
|
-
header.gsub(pattern, "")
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def session_token
|
|
55
|
-
request.cookie_jar.encrypted[:shakha_session_token]
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def find_session
|
|
59
|
-
return unless (token = session_token)
|
|
45
|
+
return unless header&.start_with?("Bearer ")
|
|
60
46
|
|
|
47
|
+
token = header.delete_prefix("Bearer ")
|
|
61
48
|
Shakha::Session.active.find_by(token: token)
|
|
62
49
|
end
|
|
63
|
-
|
|
64
|
-
def find_session_by_jti(jti)
|
|
65
|
-
return unless jti
|
|
66
|
-
|
|
67
|
-
Shakha::Session.active.find_by(jti: jti)
|
|
68
|
-
end
|
|
69
50
|
end
|
|
70
|
-
end
|
|
51
|
+
end
|
data/lib/shakha/engine.rb
CHANGED
|
@@ -4,30 +4,20 @@ module Shakha
|
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
5
|
isolate_namespace Shakha
|
|
6
6
|
|
|
7
|
-
config.app_middleware.use Shakha::Middleware
|
|
8
|
-
|
|
9
7
|
config.after_initialize do
|
|
10
8
|
Shakha::ConfigValidator.validate!(Shakha.config)
|
|
11
9
|
end
|
|
12
10
|
|
|
13
|
-
# Engine routes - these should be relative paths
|
|
14
11
|
routes do
|
|
15
12
|
root to: "auth#new"
|
|
16
13
|
|
|
17
|
-
get
|
|
18
|
-
get
|
|
19
|
-
|
|
20
|
-
get
|
|
21
|
-
|
|
22
|
-
get "session" => "session#show"
|
|
23
|
-
get "sessions" => "session#index"
|
|
24
|
-
get "sessions/view" => "session#list"
|
|
25
|
-
post "session/check" => "session#check"
|
|
26
|
-
delete "session" => "session#destroy"
|
|
27
|
-
delete "sessions/:id" => "session#revoke"
|
|
14
|
+
get ":provider/authorize" => "auth#authorize"
|
|
15
|
+
get ":provider/callback" => "auth#callback"
|
|
16
|
+
delete "sign_out" => "auth#destroy"
|
|
17
|
+
get "error" => "auth#error"
|
|
28
18
|
|
|
29
|
-
get
|
|
30
|
-
get
|
|
19
|
+
get "session" => "session#show"
|
|
20
|
+
get "session/check" => "session#check"
|
|
31
21
|
end
|
|
32
22
|
end
|
|
33
23
|
end
|
data/lib/shakha/error_handler.rb
CHANGED
|
@@ -8,9 +8,8 @@ module Shakha
|
|
|
8
8
|
|
|
9
9
|
included do
|
|
10
10
|
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
|
11
|
-
rescue_from Shakha::JWTError, with: :unauthorized
|
|
12
11
|
rescue_from Shakha::PKCEError, with: :bad_request
|
|
13
|
-
rescue_from Shakha::
|
|
12
|
+
rescue_from Shakha::OAuthError, with: :bad_gateway
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
private
|
|
@@ -28,7 +27,7 @@ module Shakha
|
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
def bad_gateway(exception)
|
|
31
|
-
Rails.logger.error("[Shakha]
|
|
30
|
+
Rails.logger.error("[Shakha] OAuth error: #{exception.message}")
|
|
32
31
|
render json: { error: "Authentication service unavailable" }, status: :bad_gateway
|
|
33
32
|
end
|
|
34
33
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shakha
|
|
4
|
+
module Providers
|
|
5
|
+
class Base
|
|
6
|
+
def authorize_url(state:, code_challenge:, redirect_uri:)
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def exchange_code(code:, code_verifier:, redirect_uri:)
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def identity_from_response(token_response)
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def provider_name
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def scopes
|
|
23
|
+
[]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Shakha
|
|
7
|
+
module Providers
|
|
8
|
+
class GitHub < Base
|
|
9
|
+
AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
|
|
10
|
+
TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
11
|
+
USER_API_URL = "https://api.github.com/user"
|
|
12
|
+
|
|
13
|
+
def provider_name
|
|
14
|
+
:github
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def authorize_url(state:, code_challenge:, redirect_uri:)
|
|
18
|
+
params = {
|
|
19
|
+
client_id: Shakha.config.github_client_id,
|
|
20
|
+
redirect_uri: redirect_uri,
|
|
21
|
+
scope: scopes.join(" "),
|
|
22
|
+
state: state
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
"#{AUTHORIZE_URL}?#{URI.encode_www_form(params)}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def exchange_code(code:, code_verifier:, redirect_uri:)
|
|
29
|
+
response = http_post(TOKEN_URL, {
|
|
30
|
+
code: code,
|
|
31
|
+
client_id: Shakha.config.github_client_id,
|
|
32
|
+
client_secret: Shakha.config.github_client_secret,
|
|
33
|
+
redirect_uri: redirect_uri
|
|
34
|
+
}, accept: :json)
|
|
35
|
+
|
|
36
|
+
JSON.parse(response.body)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def identity_from_response(token_response)
|
|
40
|
+
access_token = token_response["access_token"]
|
|
41
|
+
raise OAuthError, "No access_token received" unless access_token
|
|
42
|
+
|
|
43
|
+
user_data = fetch_user(access_token)
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
provider: :github,
|
|
47
|
+
uid: user_data["id"].to_s,
|
|
48
|
+
email: user_data["email"],
|
|
49
|
+
name: user_data["name"] || user_data["login"],
|
|
50
|
+
picture: user_data["avatar_url"]
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def scopes
|
|
55
|
+
%w[user:email]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def fetch_user(access_token)
|
|
61
|
+
uri = URI.parse(USER_API_URL)
|
|
62
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
63
|
+
http.use_ssl = true
|
|
64
|
+
http.open_timeout = 5
|
|
65
|
+
http.read_timeout = 10
|
|
66
|
+
|
|
67
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
68
|
+
request["Authorization"] = "Bearer #{access_token}"
|
|
69
|
+
request["Accept"] = "application/json"
|
|
70
|
+
|
|
71
|
+
response = http.request(request)
|
|
72
|
+
JSON.parse(response.body)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def http_post(url, body, accept: :json)
|
|
76
|
+
uri = URI.parse(url)
|
|
77
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
78
|
+
http.use_ssl = true
|
|
79
|
+
http.open_timeout = 5
|
|
80
|
+
http.read_timeout = 10
|
|
81
|
+
|
|
82
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
83
|
+
request["Accept"] = "application/json" if accept == :json
|
|
84
|
+
request["Content-Type"] = "application/x-www-form-urlencoded"
|
|
85
|
+
request.body = URI.encode_www_form(body)
|
|
86
|
+
|
|
87
|
+
response = http.request(request)
|
|
88
|
+
|
|
89
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
90
|
+
raise OAuthError, "GitHub returned HTTP #{response.code}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
response
|
|
94
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, SocketError => e
|
|
95
|
+
raise OAuthError, "Unable to reach GitHub: #{e.message}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Shakha
|
|
7
|
+
module Providers
|
|
8
|
+
class Google < Base
|
|
9
|
+
AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
|
10
|
+
TOKEN_URL = "https://oauth2.googleapis.com/token"
|
|
11
|
+
|
|
12
|
+
def provider_name
|
|
13
|
+
:google
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def authorize_url(state:, code_challenge:, redirect_uri:)
|
|
17
|
+
params = {
|
|
18
|
+
client_id: Shakha.config.google_client_id,
|
|
19
|
+
redirect_uri: redirect_uri,
|
|
20
|
+
response_type: "code",
|
|
21
|
+
scope: scopes.join(" "),
|
|
22
|
+
code_challenge: code_challenge,
|
|
23
|
+
code_challenge_method: "S256",
|
|
24
|
+
state: state,
|
|
25
|
+
access_type: "offline",
|
|
26
|
+
prompt: "consent",
|
|
27
|
+
nonce: SecureRandom.urlsafe_base64(32)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
"#{AUTHORIZE_URL}?#{URI.encode_www_form(params)}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def exchange_code(code:, code_verifier:, redirect_uri:)
|
|
34
|
+
response = http_post(TOKEN_URL, {
|
|
35
|
+
code: code,
|
|
36
|
+
client_id: Shakha.config.google_client_id,
|
|
37
|
+
client_secret: Shakha.config.google_client_secret,
|
|
38
|
+
redirect_uri: redirect_uri,
|
|
39
|
+
grant_type: "authorization_code",
|
|
40
|
+
code_verifier: code_verifier
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
JSON.parse(response.body)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def identity_from_response(token_response)
|
|
47
|
+
id_token = token_response["id_token"]
|
|
48
|
+
raise OAuthError, "No id_token received" unless id_token
|
|
49
|
+
|
|
50
|
+
payload = JWT.decode(id_token, nil, false)[0]
|
|
51
|
+
|
|
52
|
+
{
|
|
53
|
+
provider: :google,
|
|
54
|
+
uid: payload["sub"],
|
|
55
|
+
email: payload["email"],
|
|
56
|
+
name: payload["name"],
|
|
57
|
+
picture: payload["picture"]
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def scopes
|
|
62
|
+
%w[openid email profile]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def http_post(url, body)
|
|
68
|
+
uri = URI.parse(url)
|
|
69
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
70
|
+
http.use_ssl = true
|
|
71
|
+
http.open_timeout = 5
|
|
72
|
+
http.read_timeout = 10
|
|
73
|
+
|
|
74
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
75
|
+
request["Content-Type"] = "application/x-www-form-urlencoded"
|
|
76
|
+
request.body = URI.encode_www_form(body)
|
|
77
|
+
|
|
78
|
+
response = http.request(request)
|
|
79
|
+
|
|
80
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
81
|
+
raise OAuthError, "Google returned HTTP #{response.code}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
response
|
|
85
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, SocketError => e
|
|
86
|
+
raise OAuthError, "Unable to reach Google: #{e.message}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shakha/providers/base"
|
|
4
|
+
require "shakha/providers/google"
|
|
5
|
+
require "shakha/providers/github"
|
|
6
|
+
|
|
7
|
+
module Shakha
|
|
8
|
+
module Providers
|
|
9
|
+
PROVIDER_MAP = {
|
|
10
|
+
google: "Shakha::Providers::Google",
|
|
11
|
+
github: "Shakha::Providers::GitHub"
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def self.resolve(name)
|
|
15
|
+
class_name = PROVIDER_MAP[name.to_sym] || raise(ConfigurationError, "Unknown provider: #{name}")
|
|
16
|
+
class_name.constantize.new
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/shakha/version.rb
CHANGED
data/lib/shakha.rb
CHANGED
|
@@ -3,14 +3,11 @@
|
|
|
3
3
|
require "shakha/version"
|
|
4
4
|
require "shakha/config"
|
|
5
5
|
require "shakha/config_validator"
|
|
6
|
-
require "shakha/pairwise"
|
|
7
|
-
require "shakha/jwt_handler"
|
|
8
6
|
require "shakha/pkce"
|
|
9
7
|
require "shakha/rate_limiter"
|
|
10
|
-
require "shakha/auditable"
|
|
11
8
|
require "shakha/error_handler"
|
|
12
9
|
require "shakha/controller_helpers"
|
|
13
|
-
require "shakha/
|
|
10
|
+
require "shakha/providers"
|
|
14
11
|
require "shakha/engine"
|
|
15
12
|
|
|
16
13
|
module Shakha
|
|
@@ -22,32 +19,9 @@ module Shakha
|
|
|
22
19
|
def config
|
|
23
20
|
@config ||= Config.new
|
|
24
21
|
end
|
|
25
|
-
|
|
26
|
-
def verify_token(id_token, audience: nil)
|
|
27
|
-
JwtHandler.verify(id_token, audience: audience || default_audience)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def sign_token(payload, exp: 24.hours.from_now)
|
|
31
|
-
JwtHandler.encode(payload, exp: exp)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def derive_pairwise_sub(google_sub, client_id = nil)
|
|
35
|
-
Pairwise.derive(google_sub, client_id || default_client_id)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
def default_audience
|
|
41
|
-
"origin:#{config.app_origin&.then { |url| URI.parse(url).origin }}"
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def default_client_id
|
|
45
|
-
"origin:#{URI.parse(config.app_origin).origin}"
|
|
46
|
-
end
|
|
47
22
|
end
|
|
48
23
|
|
|
49
24
|
class ConfigurationError < StandardError; end
|
|
50
|
-
class JWTError < StandardError; end
|
|
51
25
|
class PKCEError < StandardError; end
|
|
52
|
-
class
|
|
26
|
+
class OAuthError < StandardError; end
|
|
53
27
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shakha
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Asrat
|
|
@@ -81,8 +81,6 @@ files:
|
|
|
81
81
|
- app/assets/stylesheets/shakha.css
|
|
82
82
|
- app/controllers/shakha/application_controller.rb
|
|
83
83
|
- app/controllers/shakha/auth_controller.rb
|
|
84
|
-
- app/controllers/shakha/jwks_controller.rb
|
|
85
|
-
- app/controllers/shakha/openid_controller.rb
|
|
86
84
|
- app/controllers/shakha/session_controller.rb
|
|
87
85
|
- app/models/shakha/client.rb
|
|
88
86
|
- app/models/shakha/session.rb
|
|
@@ -90,23 +88,22 @@ files:
|
|
|
90
88
|
- app/views/shakha/auth/callback.html.erb
|
|
91
89
|
- app/views/shakha/auth/error.html.erb
|
|
92
90
|
- app/views/shakha/auth/new.html.erb
|
|
93
|
-
- app/views/shakha/auth/sessions.html.erb
|
|
94
91
|
- app/views/shakha/errors/show.html.erb
|
|
95
92
|
- app/views/shakha/layouts/shakha.html.erb
|
|
96
93
|
- lib/generators/shakha/install_generator.rb
|
|
97
94
|
- lib/generators/shakha/templates/initializer.rb.erb
|
|
98
95
|
- lib/generators/shakha/templates/migration.rb.erb
|
|
99
96
|
- lib/shakha.rb
|
|
100
|
-
- lib/shakha/auditable.rb
|
|
101
97
|
- lib/shakha/config.rb
|
|
102
98
|
- lib/shakha/config_validator.rb
|
|
103
99
|
- lib/shakha/controller_helpers.rb
|
|
104
100
|
- lib/shakha/engine.rb
|
|
105
101
|
- lib/shakha/error_handler.rb
|
|
106
|
-
- lib/shakha/jwt_handler.rb
|
|
107
|
-
- lib/shakha/middleware.rb
|
|
108
|
-
- lib/shakha/pairwise.rb
|
|
109
102
|
- lib/shakha/pkce.rb
|
|
103
|
+
- lib/shakha/providers.rb
|
|
104
|
+
- lib/shakha/providers/base.rb
|
|
105
|
+
- lib/shakha/providers/github.rb
|
|
106
|
+
- lib/shakha/providers/google.rb
|
|
110
107
|
- lib/shakha/rate_limiter.rb
|
|
111
108
|
- lib/shakha/version.rb
|
|
112
109
|
homepage: https://shakha.dev
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Shakha
|
|
4
|
-
class OpenidController < ApplicationController
|
|
5
|
-
def configuration
|
|
6
|
-
render json: {
|
|
7
|
-
issuer: Shakha.config.issuer,
|
|
8
|
-
authorization_endpoint: "#{Shakha.config.service_base_url}/auth/shakha/authorize",
|
|
9
|
-
token_endpoint: "#{Shakha.config.service_base_url}/auth/shakha/token",
|
|
10
|
-
userinfo_endpoint: "#{Shakha.config.service_base_url}/auth/shakha/session",
|
|
11
|
-
jwks_uri: "#{Shakha.config.service_base_url}/.well-known/jwks.json",
|
|
12
|
-
response_types_supported: ["code"],
|
|
13
|
-
grant_types_supported: ["authorization_code"],
|
|
14
|
-
code_challenge_methods_supported: ["S256"],
|
|
15
|
-
subject_types_supported: ["pairwise"],
|
|
16
|
-
id_token_signing_alg_values_supported: ["ES256"],
|
|
17
|
-
scopes_supported: ["openid", "email", "profile"]
|
|
18
|
-
}, content_type: "application/json"
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
<% content_for :title, "Active Sessions" %>
|
|
2
|
-
|
|
3
|
-
<div class="sh-layout sh-animate-fade">
|
|
4
|
-
<div class="sh-card sh-animate-slide" style="max-width: 560px;">
|
|
5
|
-
<div class="sh-card__header">
|
|
6
|
-
<div class="sh-brand">
|
|
7
|
-
<div class="sh-brand__icon">S</div>
|
|
8
|
-
<div>
|
|
9
|
-
<div class="sh-brand__name">Active Sessions</div>
|
|
10
|
-
<div class="sh-brand__subtitle">Manage your signed-in devices</div>
|
|
11
|
-
</div>
|
|
12
|
-
</div>
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
<div class="sh-sessions">
|
|
16
|
-
<div class="sh-sessions__header">
|
|
17
|
-
<span class="sh-sessions__title">Devices</span>
|
|
18
|
-
<span class="sh-sessions__count"><%= @sessions&.size || 0 %> active</span>
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
<% if @sessions&.any? %>
|
|
22
|
-
<% @sessions.each do |session| %>
|
|
23
|
-
<div class="sh-session <%= 'sh-session--current' if session.token == @current_token %>">
|
|
24
|
-
<div class="sh-session__info">
|
|
25
|
-
<div style="display: flex; align-items: center; gap: var(--sh-space-2);">
|
|
26
|
-
<% if session.token == @current_token %>
|
|
27
|
-
<span class="sh-session__badge">Current</span>
|
|
28
|
-
<% end %>
|
|
29
|
-
<span class="sh-session__device">Web Browser</span>
|
|
30
|
-
</div>
|
|
31
|
-
<div class="sh-session__meta">
|
|
32
|
-
<%= session.ip_address || "Unknown IP" %> ·
|
|
33
|
-
<%= session.created_at.strftime("%b %d, %Y at %I:%M %p") %>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
<% if session.token != @current_token %>
|
|
38
|
-
<%= button_to revoke_session_path(session.id),
|
|
39
|
-
method: :delete,
|
|
40
|
-
class: "sh-session__btn",
|
|
41
|
-
data: { turbo: false, confirm: "Revoke this session?" } do %>
|
|
42
|
-
Revoke
|
|
43
|
-
<% end %>
|
|
44
|
-
<% end %>
|
|
45
|
-
</div>
|
|
46
|
-
<% end %>
|
|
47
|
-
<% else %>
|
|
48
|
-
<div class="sh-empty">
|
|
49
|
-
<div class="sh-empty__icon">
|
|
50
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
51
|
-
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
52
|
-
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
|
53
|
-
</svg>
|
|
54
|
-
</div>
|
|
55
|
-
<p>No active sessions found.</p>
|
|
56
|
-
</div>
|
|
57
|
-
<% end %>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<div class="sh-card__footer">
|
|
61
|
-
<%= link_to "/", class: "sh-link" do %>
|
|
62
|
-
← Back to app
|
|
63
|
-
<% end %>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
data/lib/shakha/auditable.rb
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Shakha
|
|
4
|
-
module Auditable
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
included do
|
|
8
|
-
after_action :log_sign_in
|
|
9
|
-
after_action :log_sign_out
|
|
10
|
-
after_action :log_token_exchange
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
|
|
15
|
-
def log_sign_in
|
|
16
|
-
return unless action_name == "callback" && response.successful? && @current_user
|
|
17
|
-
|
|
18
|
-
ActiveSupport::Notifications.instrument("shakha.sign_in", {
|
|
19
|
-
user_id: @current_user&.id,
|
|
20
|
-
pairwise_sub: @current_user&.pairwise_sub,
|
|
21
|
-
client_id: @current_client&.id,
|
|
22
|
-
ip: request.remote_ip,
|
|
23
|
-
user_agent: request.user_agent
|
|
24
|
-
})
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def log_sign_out
|
|
28
|
-
return unless action_name == "destroy"
|
|
29
|
-
|
|
30
|
-
ActiveSupport::Notifications.instrument("shakha.sign_out", {
|
|
31
|
-
session_id: @current_session&.id,
|
|
32
|
-
user_id: @current_session&.user_id,
|
|
33
|
-
ip: request.remote_ip
|
|
34
|
-
})
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def log_token_exchange
|
|
38
|
-
return unless action_name == "token"
|
|
39
|
-
|
|
40
|
-
ActiveSupport::Notifications.instrument("shakha.token_exchange", {
|
|
41
|
-
ip: request.remote_ip,
|
|
42
|
-
user_agent: request.user_agent,
|
|
43
|
-
success: response.successful?
|
|
44
|
-
})
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|