shakha 0.1.7 → 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 +5 -4
- data/app/controllers/shakha/application_controller.rb +1 -1
- data/app/controllers/shakha/auth_controller.rb +101 -175
- 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 +7 -30
- 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/pkce.rb +12 -18
- 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/rate_limiter.rb +2 -8
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f2fb69e4f483c981312b4211c9205e9ae5d0d100a0d3d8f0d61f693478c8369
|
|
4
|
+
data.tar.gz: 6dfb50d826c90ea54ed5453d7eebad7a94bf93cea02a1bb407fbd69a61da421e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1bac5c8e1d92fb2997713d5ad19d7b16327fc21ec7e7520adf09d736bbe2c15c1cb72ef7c859798cba62a8a012819a1fb4863bd6c49830cd10d98f3082d4afc9
|
|
7
|
+
data.tar.gz: 8e9ae9652bb51b48bc30623305113e75c2a1c56676ec17a4e9503f4dc9694f55cf87d1d58fa9a2aaee956e47fa8ee1ecdc35024f3500c616e228942f42efbfac
|
data/README.md
CHANGED
|
@@ -28,12 +28,13 @@ class CreateShakhaTables < ActiveRecord::Migration[7.1]
|
|
|
28
28
|
|
|
29
29
|
create_table :shakha_users do |t|
|
|
30
30
|
t.references :client, null: false, foreign_key: { to_table: :shakha_clients }
|
|
31
|
-
t.string :
|
|
31
|
+
t.string :provider, null: false
|
|
32
|
+
t.string :uid, null: false
|
|
32
33
|
t.string :email
|
|
33
34
|
t.string :name
|
|
34
35
|
t.string :picture
|
|
35
36
|
t.timestamps
|
|
36
|
-
t.index :
|
|
37
|
+
t.index [:provider, :uid], unique: true
|
|
37
38
|
t.index :email
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -41,10 +42,10 @@ class CreateShakhaTables < ActiveRecord::Migration[7.1]
|
|
|
41
42
|
t.references :user, foreign_key: { to_table: :shakha_users }
|
|
42
43
|
t.references :client, null: false, foreign_key: { to_table: :shakha_clients }
|
|
43
44
|
t.string :token, null: false
|
|
44
|
-
t.string :
|
|
45
|
+
t.string :ip_address
|
|
46
|
+
t.string :user_agent
|
|
45
47
|
t.timestamps
|
|
46
48
|
t.index :token, unique: true
|
|
47
|
-
t.index :jti, unique: true
|
|
48
49
|
t.index :created_at
|
|
49
50
|
end
|
|
50
51
|
end
|
|
@@ -6,183 +6,92 @@ require "uri"
|
|
|
6
6
|
module Shakha
|
|
7
7
|
class AuthController < ApplicationController
|
|
8
8
|
include PKCEMixin
|
|
9
|
-
include Auditable
|
|
10
9
|
|
|
11
|
-
skip_before_action :verify_authenticity_token, only: [:callback
|
|
10
|
+
skip_before_action :verify_authenticity_token, only: [:callback]
|
|
12
11
|
|
|
13
12
|
def new
|
|
14
13
|
@client = find_or_create_client
|
|
15
14
|
@return_to = sanitize_return_to(params[:return_to])
|
|
15
|
+
@providers = Shakha.config.providers
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def authorize
|
|
19
|
-
|
|
19
|
+
provider = resolve_provider
|
|
20
20
|
pkce = create_pkce_bundle
|
|
21
|
-
@client = find_or_create_client
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
redirect_uri = "#{Shakha.config.app_origin}/auth/shakha/#{provider.provider_name}/callback"
|
|
23
|
+
auth_url = provider.authorize_url(
|
|
24
|
+
state: pkce[:state],
|
|
25
|
+
code_challenge: pkce[:challenge],
|
|
26
|
+
redirect_uri: redirect_uri
|
|
27
|
+
)
|
|
24
28
|
|
|
25
|
-
redirect_to
|
|
29
|
+
redirect_to auth_url, allow_other_host: true
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
def callback
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
rescue PKCEError, GoogleOAuthError => e
|
|
32
|
-
ActiveSupport::Notifications.instrument("shakha.sign_in_failed", {
|
|
33
|
-
reason: e.class.name,
|
|
34
|
-
ip: request.remote_ip
|
|
35
|
-
})
|
|
36
|
-
Rails.logger.warn("[Shakha] Auth error: #{e.class}: #{e.message}")
|
|
37
|
-
redirect_to "/auth/shakha/error?message=#{URI.encode_www_form_component(user_facing_error(e))}"
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def token
|
|
41
|
-
code = params[:code]
|
|
42
|
-
verifier = params[:code_verifier]
|
|
43
|
-
|
|
44
|
-
raise PKCEError, "Missing code" unless code
|
|
45
|
-
raise PKCEError, "Missing code_verifier" unless verifier
|
|
46
|
-
|
|
47
|
-
id_token = exchange_code_for_id_token(code, verifier)
|
|
48
|
-
|
|
49
|
-
render json: {
|
|
50
|
-
id_token: id_token,
|
|
51
|
-
pairwise_sub: id_token_payload(id_token)[:sub],
|
|
52
|
-
expires_in: 24.hours.to_i
|
|
53
|
-
}
|
|
54
|
-
rescue PKCEError, JWTError, GoogleOAuthError => e
|
|
55
|
-
render json: { error: e.message }, status: :unauthorized
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def error
|
|
59
|
-
@message = params[:message] || "Authentication failed"
|
|
60
|
-
end
|
|
33
|
+
provider = resolve_provider
|
|
34
|
+
pkce_result = verify_pkce!(params[:state])
|
|
61
35
|
|
|
62
|
-
|
|
36
|
+
token_response = provider.exchange_code(
|
|
37
|
+
code: params[:code],
|
|
38
|
+
code_verifier: pkce_result[:verifier],
|
|
39
|
+
redirect_uri: "#{Shakha.config.app_origin}/auth/shakha/#{provider.provider_name}/callback"
|
|
40
|
+
)
|
|
63
41
|
|
|
64
|
-
|
|
65
|
-
|
|
42
|
+
identity = provider.identity_from_response(token_response)
|
|
43
|
+
user = find_or_create_user(provider.provider_name, identity)
|
|
44
|
+
session_record = create_session(user)
|
|
45
|
+
set_session_cookie(session_record)
|
|
46
|
+
redirect_to build_return_url(pkce_result[:return_to], session_record)
|
|
66
47
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return "/" unless uri.path.present? && uri.path.start_with?("/")
|
|
70
|
-
|
|
71
|
-
uri.path
|
|
72
|
-
rescue URI::InvalidURIError
|
|
73
|
-
"/"
|
|
48
|
+
rescue PKCEError, OAuthError => e
|
|
49
|
+
handle_auth_failure(e, pkce_result)
|
|
74
50
|
end
|
|
75
51
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
|
|
52
|
+
def destroy
|
|
53
|
+
current_session&.destroy
|
|
54
|
+
cookies.delete(:shakha_session_token)
|
|
79
55
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
nil
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def user_facing_error(exception)
|
|
87
|
-
case exception
|
|
88
|
-
when PKCEError
|
|
89
|
-
"Authentication failed. Please try again."
|
|
90
|
-
when GoogleOAuthError
|
|
91
|
-
"Unable to sign in with Google. Please try again later."
|
|
92
|
-
else
|
|
93
|
-
"An unexpected error occurred. Please try again."
|
|
56
|
+
respond_to do |format|
|
|
57
|
+
format.html { redirect_to params[:return_to].presence || "/" }
|
|
58
|
+
format.json { render json: { status: "signed_out" } }
|
|
94
59
|
end
|
|
95
60
|
end
|
|
96
61
|
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
origin_uri = URI.parse(origin).origin
|
|
100
|
-
|
|
101
|
-
if Shakha.config.embedded?
|
|
102
|
-
Shakha::Client.find_or_create_by!(origin: origin_uri) do |client|
|
|
103
|
-
client.name = URI.parse(origin).host
|
|
104
|
-
end
|
|
105
|
-
else
|
|
106
|
-
Shakha::Client.find_by!(origin: origin_uri)
|
|
107
|
-
end
|
|
108
|
-
rescue ActiveRecord::RecordNotFound
|
|
109
|
-
raise ConfigurationError, "Unknown client origin: #{origin_uri}. Register this origin in shakha_clients first."
|
|
62
|
+
def error
|
|
63
|
+
@message = params[:message] || "Authentication failed"
|
|
110
64
|
end
|
|
111
65
|
|
|
112
|
-
|
|
113
|
-
client_id = Shakha.config.google_client_id || ENV["GOOGLE_CLIENT_ID"]
|
|
114
|
-
base_url = Shakha.config.service_base_url || "http://localhost:3000"
|
|
115
|
-
redirect_uri = "#{base_url}/auth/shakha/callback"
|
|
116
|
-
|
|
117
|
-
scopes = ["openid", "email", "profile"].join(" ")
|
|
118
|
-
scopes += " https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile" if params[:request_pii]
|
|
119
|
-
|
|
120
|
-
params = {
|
|
121
|
-
client_id: client_id,
|
|
122
|
-
redirect_uri: redirect_uri,
|
|
123
|
-
response_type: "code",
|
|
124
|
-
scope: scopes,
|
|
125
|
-
code_challenge: pkce[:challenge],
|
|
126
|
-
code_challenge_method: "S256",
|
|
127
|
-
state: pkce[:state],
|
|
128
|
-
access_type: "offline",
|
|
129
|
-
prompt: "consent"
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
URI.parse("https://accounts.google.com/o/oauth2/v2/auth").tap do |uri|
|
|
133
|
-
uri.query = URI.encode_www_form(params)
|
|
134
|
-
end.to_s
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def exchange_code_for_tokens(code, verifier, return_to = "/")
|
|
138
|
-
client_id = Shakha.config.google_client_id || ENV["GOOGLE_CLIENT_ID"]
|
|
139
|
-
client_secret = Shakha.config.google_client_secret || ENV["GOOGLE_CLIENT_SECRET"]
|
|
140
|
-
base_url = Shakha.config.service_base_url || "http://localhost:3000"
|
|
141
|
-
redirect_uri = "#{base_url}/auth/shakha/callback"
|
|
142
|
-
|
|
143
|
-
response = http_post(
|
|
144
|
-
"https://oauth2.googleapis.com/token",
|
|
145
|
-
{
|
|
146
|
-
code: code,
|
|
147
|
-
client_id: client_id,
|
|
148
|
-
client_secret: client_secret,
|
|
149
|
-
redirect_uri: redirect_uri,
|
|
150
|
-
grant_type: "authorization_code",
|
|
151
|
-
code_verifier: verifier
|
|
152
|
-
}
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
tokens = JSON.parse(response.body)
|
|
156
|
-
id_token = tokens["id_token"]
|
|
157
|
-
access_token = tokens["access_token"]
|
|
158
|
-
|
|
159
|
-
raise GoogleOAuthError, "No id_token received" unless id_token
|
|
160
|
-
|
|
161
|
-
payload = decode_id_token(id_token)
|
|
162
|
-
google_sub = payload["sub"]
|
|
163
|
-
|
|
164
|
-
client = find_or_create_client
|
|
165
|
-
pairwise_sub = Shakha.derive_pairwise_sub(google_sub, client.client_id)
|
|
66
|
+
private
|
|
166
67
|
|
|
167
|
-
|
|
68
|
+
def resolve_provider
|
|
69
|
+
provider_name = (params[:provider] || :google).to_sym
|
|
70
|
+
Shakha::Providers.resolve(provider_name)
|
|
71
|
+
end
|
|
168
72
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
73
|
+
def find_or_create_user(provider_name, identity)
|
|
74
|
+
Shakha::User.find_or_create_by!(
|
|
75
|
+
provider: provider_name.to_s,
|
|
76
|
+
uid: identity[:uid]
|
|
77
|
+
) do |user|
|
|
78
|
+
user.client = find_or_create_client
|
|
79
|
+
user.email = identity[:email]
|
|
80
|
+
user.name = identity[:name]
|
|
81
|
+
user.picture = identity[:picture]
|
|
175
82
|
end
|
|
176
|
-
|
|
83
|
+
end
|
|
177
84
|
|
|
178
|
-
|
|
85
|
+
def create_session(user)
|
|
86
|
+
Shakha::Session.create!(
|
|
179
87
|
user: user,
|
|
180
|
-
client:
|
|
181
|
-
jti: SecureRandom.uuid,
|
|
88
|
+
client: find_or_create_client,
|
|
182
89
|
ip_address: request.remote_ip,
|
|
183
90
|
user_agent: request.user_agent
|
|
184
91
|
)
|
|
92
|
+
end
|
|
185
93
|
|
|
94
|
+
def set_session_cookie(session_record)
|
|
186
95
|
cookies.encrypted[:shakha_session_token] = {
|
|
187
96
|
value: session_record.token,
|
|
188
97
|
httponly: true,
|
|
@@ -190,49 +99,66 @@ module Shakha
|
|
|
190
99
|
same_site: :lax,
|
|
191
100
|
expires: Shakha.config.session_lifetime.from_now
|
|
192
101
|
}
|
|
193
|
-
|
|
194
|
-
redirect_to sanitize_return_to(return_to)
|
|
195
102
|
end
|
|
196
103
|
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
104
|
+
def build_return_url(return_to, session_record)
|
|
105
|
+
uri = URI.parse(return_to || "/")
|
|
106
|
+
existing = URI.decode_www_form(uri.query || "").to_h
|
|
107
|
+
existing["token"] = session_record.token
|
|
108
|
+
existing["expires_at"] = session_record.expires_at.iso8601
|
|
109
|
+
uri.query = URI.encode_www_form(existing)
|
|
110
|
+
uri.to_s
|
|
111
|
+
end
|
|
201
112
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
{
|
|
205
|
-
code: code,
|
|
206
|
-
client_id: client_id,
|
|
207
|
-
client_secret: client_secret,
|
|
208
|
-
redirect_uri: redirect_uri,
|
|
209
|
-
grant_type: "authorization_code",
|
|
210
|
-
code_verifier: verifier
|
|
211
|
-
}
|
|
212
|
-
)
|
|
113
|
+
def handle_auth_failure(exception, pkce_result)
|
|
114
|
+
return_to = pkce_result&.dig(:return_to) || "/"
|
|
213
115
|
|
|
214
|
-
|
|
215
|
-
|
|
116
|
+
if request.format.json? || api_request?
|
|
117
|
+
render json: { error: user_facing_error(exception) }, status: :unauthorized
|
|
118
|
+
else
|
|
119
|
+
redirect_to "#{return_to}?error=#{URI.encode_www_form_component(user_facing_error(exception))}"
|
|
120
|
+
end
|
|
216
121
|
end
|
|
217
122
|
|
|
218
|
-
def
|
|
219
|
-
|
|
123
|
+
def api_request?
|
|
124
|
+
request.headers["Accept"]&.include?("application/json")
|
|
220
125
|
end
|
|
221
126
|
|
|
222
|
-
def
|
|
223
|
-
|
|
127
|
+
def sanitize_return_to(raw)
|
|
128
|
+
return "/" if raw.blank?
|
|
129
|
+
|
|
130
|
+
uri = URI.parse(raw)
|
|
131
|
+
app_host = URI.parse(Shakha.config.app_origin).host
|
|
132
|
+
|
|
133
|
+
return "/" unless uri.path.present? && uri.path.start_with?("/")
|
|
134
|
+
|
|
135
|
+
if uri.host.present? && uri.host != app_host && !allowed_origin?(uri.origin)
|
|
136
|
+
return "/"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
raw
|
|
140
|
+
rescue URI::InvalidURIError
|
|
141
|
+
"/"
|
|
224
142
|
end
|
|
225
143
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
http.use_ssl = uri.scheme == "https"
|
|
144
|
+
def allowed_origin?(origin)
|
|
145
|
+
Shakha.config.allowed_redirect_origins&.include?(origin) || false
|
|
146
|
+
end
|
|
230
147
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
148
|
+
def find_or_create_client
|
|
149
|
+
origin = request.origin || Shakha.config.app_origin
|
|
150
|
+
origin_uri = URI.parse(origin).origin
|
|
151
|
+
Shakha::Client.find_or_create_by!(origin: origin_uri) do |client|
|
|
152
|
+
client.name = URI.parse(origin).host
|
|
153
|
+
end
|
|
154
|
+
end
|
|
234
155
|
|
|
235
|
-
|
|
156
|
+
def user_facing_error(exception)
|
|
157
|
+
case exception
|
|
158
|
+
when PKCEError then "Authentication failed. Please try again."
|
|
159
|
+
when OAuthError then "Unable to sign in. Please try again later."
|
|
160
|
+
else "An unexpected error occurred. Please try again."
|
|
161
|
+
end
|
|
236
162
|
end
|
|
237
163
|
end
|
|
238
|
-
end
|
|
164
|
+
end
|
|
@@ -2,76 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
module Shakha
|
|
4
4
|
class SessionController < ApplicationController
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return render json: { error: "Authentication required" }, status: :unauthorized unless signed_in?
|
|
10
|
-
|
|
11
|
-
sessions = current_user.sessions.active.order(created_at: :desc)
|
|
5
|
+
def show
|
|
6
|
+
unless signed_in?
|
|
7
|
+
return render json: { error: "Authentication required" }, status: :unauthorized
|
|
8
|
+
end
|
|
12
9
|
|
|
13
10
|
render json: {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
user: {
|
|
12
|
+
id: current_user.id,
|
|
13
|
+
email: current_user.email,
|
|
14
|
+
name: current_user.name,
|
|
15
|
+
picture: current_user.picture,
|
|
16
|
+
provider: current_user.provider
|
|
17
|
+
},
|
|
18
|
+
session: {
|
|
19
|
+
expires_at: current_session.expires_at.iso8601
|
|
23
20
|
}
|
|
24
21
|
}
|
|
25
22
|
end
|
|
26
23
|
|
|
27
|
-
def show
|
|
28
|
-
render json: {
|
|
29
|
-
user_id: current_user&.pairwise_sub,
|
|
30
|
-
email: current_user&.email,
|
|
31
|
-
name: current_user&.name,
|
|
32
|
-
expires_at: current_session&.expires_at&.iso8601
|
|
33
|
-
}
|
|
34
|
-
end
|
|
35
|
-
|
|
36
24
|
def check
|
|
37
25
|
if signed_in?
|
|
38
26
|
render json: { status: "active" }
|
|
39
27
|
else
|
|
40
|
-
render json: {
|
|
41
|
-
status: "login_required",
|
|
42
|
-
reason: "no_session"
|
|
43
|
-
}, status: :unauthorized
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def destroy
|
|
48
|
-
current_session&.destroy
|
|
49
|
-
cookies.delete(:shakha_session_token)
|
|
50
|
-
|
|
51
|
-
respond_to do |format|
|
|
52
|
-
format.html { redirect_to params[:return_to].presence || "/" }
|
|
53
|
-
format.json { render json: { status: "signed_out" } }
|
|
28
|
+
render json: { status: "expired" }, status: :unauthorized
|
|
54
29
|
end
|
|
55
30
|
end
|
|
56
|
-
|
|
57
|
-
def list
|
|
58
|
-
return redirect_to "/auth/shakha" unless signed_in?
|
|
59
|
-
|
|
60
|
-
@sessions = current_user.sessions.active.order(created_at: :desc)
|
|
61
|
-
@current_token = current_session&.token
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def revoke
|
|
65
|
-
return render json: { error: "Authentication required" }, status: :unauthorized unless signed_in?
|
|
66
|
-
|
|
67
|
-
session = current_user.sessions.find(params[:id])
|
|
68
|
-
session.destroy
|
|
69
|
-
|
|
70
|
-
cookies.delete(:shakha_session_token) if session.token == current_session&.token
|
|
71
|
-
|
|
72
|
-
log_session_revoked(session)
|
|
73
|
-
|
|
74
|
-
render json: { status: "revoked" }
|
|
75
|
-
end
|
|
76
31
|
end
|
|
77
|
-
end
|
|
32
|
+
end
|
data/app/models/shakha/client.rb
CHANGED
|
@@ -8,7 +8,6 @@ module Shakha
|
|
|
8
8
|
belongs_to :client, class_name: "Shakha::Client"
|
|
9
9
|
|
|
10
10
|
before_create :generate_token
|
|
11
|
-
before_create :generate_jti
|
|
12
11
|
|
|
13
12
|
scope :active, -> { where("created_at > ?", Shakha.config.session_lifetime.ago) }
|
|
14
13
|
|
|
@@ -25,9 +24,5 @@ module Shakha
|
|
|
25
24
|
def generate_token
|
|
26
25
|
self.token ||= SecureRandom.urlsafe_base64(32)
|
|
27
26
|
end
|
|
28
|
-
|
|
29
|
-
def generate_jti
|
|
30
|
-
self.jti ||= SecureRandom.uuid
|
|
31
|
-
end
|
|
32
27
|
end
|
|
33
28
|
end
|
data/app/models/shakha/user.rb
CHANGED
|
@@ -7,11 +7,9 @@ module Shakha
|
|
|
7
7
|
belongs_to :client, class_name: "Shakha::Client"
|
|
8
8
|
has_many :sessions, class_name: "Shakha::Session", dependent: :destroy
|
|
9
9
|
|
|
10
|
-
validates :
|
|
10
|
+
validates :provider, presence: true
|
|
11
|
+
validates :uid, presence: true
|
|
12
|
+
validates :uid, uniqueness: { scope: :provider }
|
|
11
13
|
validates :email, uniqueness: { scope: :client_id }, allow_blank: true
|
|
12
|
-
|
|
13
|
-
def can_access?(resource)
|
|
14
|
-
true
|
|
15
|
-
end
|
|
16
14
|
end
|
|
17
15
|
end
|
|
@@ -16,25 +16,13 @@
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
<div class="sh-card__body">
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<path fill="#FBBC05" d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z"/>
|
|
26
|
-
<path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z"/>
|
|
27
|
-
</svg>
|
|
28
|
-
Continue with Google
|
|
19
|
+
<% @providers.each do |provider| %>
|
|
20
|
+
<%= link_to shakha.send("#{provider}_authorize_path"),
|
|
21
|
+
class: "sh-btn sh-btn--#{provider}",
|
|
22
|
+
data: { turbo: false } do %>
|
|
23
|
+
Continue with <%= provider.to_s.titleize %>
|
|
24
|
+
<% end %>
|
|
29
25
|
<% end %>
|
|
30
|
-
|
|
31
|
-
<div class="sh-divider">or</div>
|
|
32
|
-
|
|
33
|
-
<p class="sh-text-center" style="color: var(--sh-text-tertiary); font-size: var(--sh-text-sm);">
|
|
34
|
-
By signing in, you agree to our
|
|
35
|
-
<a href="#">Terms</a> and
|
|
36
|
-
<a href="#">Privacy Policy</a>.
|
|
37
|
-
</p>
|
|
38
26
|
</div>
|
|
39
27
|
|
|
40
28
|
<div class="sh-card__footer">
|
data/lib/shakha/config.rb
CHANGED
|
@@ -3,42 +3,19 @@
|
|
|
3
3
|
module Shakha
|
|
4
4
|
class Config
|
|
5
5
|
attr_accessor :app_origin,
|
|
6
|
-
:service_url,
|
|
7
|
-
:service_secret,
|
|
8
6
|
:google_client_id,
|
|
9
7
|
:google_client_secret,
|
|
10
|
-
:
|
|
8
|
+
:github_client_id,
|
|
9
|
+
:github_client_secret,
|
|
10
|
+
:providers,
|
|
11
11
|
:session_lifetime,
|
|
12
|
-
:
|
|
13
|
-
:
|
|
14
|
-
:key_id,
|
|
15
|
-
:rate_limiting_enabled
|
|
12
|
+
:rate_limiting_enabled,
|
|
13
|
+
:allowed_redirect_origins
|
|
16
14
|
|
|
17
15
|
def initialize
|
|
18
16
|
@session_lifetime = 30.days
|
|
19
|
-
@issuer = "https://shakha.dev"
|
|
20
17
|
@rate_limiting_enabled = false
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def embedded?
|
|
24
|
-
service_url.blank?
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def service_base_url
|
|
28
|
-
return app_origin if embedded?
|
|
29
|
-
|
|
30
|
-
service_url.chomp("/")
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def client_id
|
|
34
|
-
return @client_id if defined?(@client_id)
|
|
35
|
-
|
|
36
|
-
origin = URI.parse(app_origin).origin
|
|
37
|
-
@client_id = "origin:#{origin}"
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def audience
|
|
41
|
-
client_id
|
|
18
|
+
@providers = [:google]
|
|
42
19
|
end
|
|
43
20
|
end
|
|
44
|
-
end
|
|
21
|
+
end
|
|
@@ -5,10 +5,9 @@ module Shakha
|
|
|
5
5
|
class << self
|
|
6
6
|
def validate!(config)
|
|
7
7
|
missing = []
|
|
8
|
-
missing << "
|
|
8
|
+
missing << "APP_ORIGIN" unless config.app_origin.present?
|
|
9
9
|
missing << "GOOGLE_CLIENT_ID" unless config.google_client_id.present?
|
|
10
10
|
missing << "GOOGLE_CLIENT_SECRET" unless config.google_client_secret.present?
|
|
11
|
-
missing << "SHAKHA_SERVICE_SECRET" unless config.service_secret.present?
|
|
12
11
|
|
|
13
12
|
unless missing.empty?
|
|
14
13
|
message = "Shakha: missing required configuration: #{missing.join(', ')}"
|