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
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,12 +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 :jti, null: false
|
|
45
45
|
t.string :ip_address
|
|
46
46
|
t.string :user_agent
|
|
47
47
|
t.timestamps
|
|
48
48
|
t.index :token, unique: true
|
|
49
|
-
t.index :jti, unique: true
|
|
50
49
|
t.index :created_at
|
|
51
50
|
end
|
|
52
51
|
end
|
|
@@ -6,199 +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
|
|
33
|
+
provider = resolve_provider
|
|
29
34
|
pkce_result = verify_pkce!(params[:state])
|
|
30
|
-
exchange_code_for_tokens(params[:code], pkce_result[:verifier], pkce_result[:return_to], pkce_result[:nonce])
|
|
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
35
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
)
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
rescue PKCEError, JWTError, GoogleOAuthError => e
|
|
55
|
-
render json: { error: e.message }, status: :unauthorized
|
|
56
|
-
end
|
|
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)
|
|
57
47
|
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
rescue PKCEError, OAuthError => e
|
|
49
|
+
handle_auth_failure(e, pkce_result)
|
|
60
50
|
end
|
|
61
51
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return "/" if raw.blank?
|
|
66
|
-
|
|
67
|
-
uri = URI.parse(raw)
|
|
68
|
-
app_host = URI.parse(Shakha.config.app_origin).host
|
|
69
|
-
|
|
70
|
-
# Must have a path
|
|
71
|
-
return "/" unless uri.path.present? && uri.path.start_with?("/")
|
|
52
|
+
def destroy
|
|
53
|
+
current_session&.destroy
|
|
54
|
+
cookies.delete(:shakha_session_token)
|
|
72
55
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
56
|
+
respond_to do |format|
|
|
57
|
+
format.html { redirect_to params[:return_to].presence || "/" }
|
|
58
|
+
format.json { render json: { status: "signed_out" } }
|
|
76
59
|
end
|
|
77
|
-
|
|
78
|
-
raw
|
|
79
|
-
rescue URI::InvalidURIError
|
|
80
|
-
"/"
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def allowed_origin?(origin)
|
|
84
|
-
Shakha.config.allowed_redirect_origins&.include?(origin) || false
|
|
85
60
|
end
|
|
86
61
|
|
|
87
|
-
def
|
|
88
|
-
|
|
62
|
+
def error
|
|
63
|
+
@message = params[:message] || "Authentication failed"
|
|
89
64
|
end
|
|
90
65
|
|
|
91
|
-
|
|
92
|
-
URI.parse(Shakha.config.service_base_url).host
|
|
93
|
-
rescue URI::InvalidURIError
|
|
94
|
-
nil
|
|
95
|
-
end
|
|
66
|
+
private
|
|
96
67
|
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"Authentication failed. Please try again."
|
|
101
|
-
when GoogleOAuthError
|
|
102
|
-
"Unable to sign in with Google. Please try again later."
|
|
103
|
-
else
|
|
104
|
-
"An unexpected error occurred. Please try again."
|
|
105
|
-
end
|
|
68
|
+
def resolve_provider
|
|
69
|
+
provider_name = (params[:provider] || :google).to_sym
|
|
70
|
+
Shakha::Providers.resolve(provider_name)
|
|
106
71
|
end
|
|
107
72
|
|
|
108
|
-
def
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Shakha::Client.find_by!(origin: origin_uri)
|
|
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]
|
|
118
82
|
end
|
|
119
|
-
rescue ActiveRecord::RecordNotFound
|
|
120
|
-
raise ConfigurationError, "Unknown client origin: #{origin_uri}. Register this origin in shakha_clients first."
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def build_google_auth_url(pkce)
|
|
124
|
-
client_id = Shakha.config.google_client_id || ENV["GOOGLE_CLIENT_ID"]
|
|
125
|
-
base_url = Shakha.config.service_base_url || "http://localhost:3000"
|
|
126
|
-
redirect_uri = "#{base_url}/auth/shakha/callback"
|
|
127
|
-
|
|
128
|
-
scopes = ["openid", "email", "profile"].join(" ")
|
|
129
|
-
scopes += " https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile" if params[:request_pii]
|
|
130
|
-
|
|
131
|
-
params = {
|
|
132
|
-
client_id: client_id,
|
|
133
|
-
redirect_uri: redirect_uri,
|
|
134
|
-
response_type: "code",
|
|
135
|
-
scope: scopes,
|
|
136
|
-
code_challenge: pkce[:challenge],
|
|
137
|
-
code_challenge_method: "S256",
|
|
138
|
-
state: pkce[:state],
|
|
139
|
-
nonce: pkce[:nonce],
|
|
140
|
-
access_type: "offline",
|
|
141
|
-
prompt: "consent"
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
URI.parse("https://accounts.google.com/o/oauth2/v2/auth").tap do |uri|
|
|
145
|
-
uri.query = URI.encode_www_form(params)
|
|
146
|
-
end.to_s
|
|
147
83
|
end
|
|
148
84
|
|
|
149
|
-
def
|
|
150
|
-
|
|
151
|
-
client_secret = Shakha.config.google_client_secret || ENV["GOOGLE_CLIENT_SECRET"]
|
|
152
|
-
base_url = Shakha.config.service_base_url || "http://localhost:3000"
|
|
153
|
-
redirect_uri = "#{base_url}/auth/shakha/callback"
|
|
154
|
-
|
|
155
|
-
response = http_post(
|
|
156
|
-
"https://oauth2.googleapis.com/token",
|
|
157
|
-
{
|
|
158
|
-
code: code,
|
|
159
|
-
client_id: client_id,
|
|
160
|
-
client_secret: client_secret,
|
|
161
|
-
redirect_uri: redirect_uri,
|
|
162
|
-
grant_type: "authorization_code",
|
|
163
|
-
code_verifier: verifier
|
|
164
|
-
}
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
tokens = JSON.parse(response.body)
|
|
168
|
-
id_token = tokens["id_token"]
|
|
169
|
-
access_token = tokens["access_token"]
|
|
170
|
-
|
|
171
|
-
raise GoogleOAuthError, "No id_token received" unless id_token
|
|
172
|
-
|
|
173
|
-
payload = decode_id_token(id_token)
|
|
174
|
-
google_sub = payload["sub"]
|
|
175
|
-
|
|
176
|
-
# Verify nonce (OIDC replay protection)
|
|
177
|
-
if expected_nonce && payload["nonce"] != expected_nonce
|
|
178
|
-
raise GoogleOAuthError, "Nonce mismatch"
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
client = find_or_create_client
|
|
182
|
-
pairwise_sub = Shakha.derive_pairwise_sub(google_sub, client.client_id)
|
|
183
|
-
|
|
184
|
-
user = Shakha::User.find_or_initialize_by(pairwise_sub: pairwise_sub, client: client)
|
|
185
|
-
|
|
186
|
-
if payload["email"]
|
|
187
|
-
user.assign_attributes(
|
|
188
|
-
email: payload["email"],
|
|
189
|
-
name: payload["name"],
|
|
190
|
-
picture: payload["picture"]
|
|
191
|
-
)
|
|
192
|
-
end
|
|
193
|
-
user.save!
|
|
194
|
-
|
|
195
|
-
session_record = Shakha::Session.create!(
|
|
85
|
+
def create_session(user)
|
|
86
|
+
Shakha::Session.create!(
|
|
196
87
|
user: user,
|
|
197
|
-
client:
|
|
88
|
+
client: find_or_create_client,
|
|
198
89
|
ip_address: request.remote_ip,
|
|
199
90
|
user_agent: request.user_agent
|
|
200
91
|
)
|
|
92
|
+
end
|
|
201
93
|
|
|
94
|
+
def set_session_cookie(session_record)
|
|
202
95
|
cookies.encrypted[:shakha_session_token] = {
|
|
203
96
|
value: session_record.token,
|
|
204
97
|
httponly: true,
|
|
@@ -206,61 +99,66 @@ module Shakha
|
|
|
206
99
|
same_site: :lax,
|
|
207
100
|
expires: Shakha.config.session_lifetime.from_now
|
|
208
101
|
}
|
|
209
|
-
|
|
210
|
-
redirect_to sanitize_return_to(return_to)
|
|
211
102
|
end
|
|
212
103
|
|
|
213
|
-
def
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
217
112
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
code: code,
|
|
222
|
-
client_id: client_id,
|
|
223
|
-
client_secret: client_secret,
|
|
224
|
-
redirect_uri: redirect_uri,
|
|
225
|
-
grant_type: "authorization_code",
|
|
226
|
-
code_verifier: verifier
|
|
227
|
-
}
|
|
228
|
-
)
|
|
113
|
+
def handle_auth_failure(exception, pkce_result)
|
|
114
|
+
return_to = pkce_result&.dig(:return_to) || "/"
|
|
229
115
|
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
232
121
|
end
|
|
233
122
|
|
|
234
|
-
def
|
|
235
|
-
|
|
123
|
+
def api_request?
|
|
124
|
+
request.headers["Accept"]&.include?("application/json")
|
|
236
125
|
end
|
|
237
126
|
|
|
238
|
-
def
|
|
239
|
-
|
|
240
|
-
|
|
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?("/")
|
|
241
134
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
http.use_ssl = uri.scheme == "https"
|
|
246
|
-
http.open_timeout = 5
|
|
247
|
-
http.read_timeout = 10
|
|
135
|
+
if uri.host.present? && uri.host != app_host && !allowed_origin?(uri.origin)
|
|
136
|
+
return "/"
|
|
137
|
+
end
|
|
248
138
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
139
|
+
raw
|
|
140
|
+
rescue URI::InvalidURIError
|
|
141
|
+
"/"
|
|
142
|
+
end
|
|
252
143
|
|
|
253
|
-
|
|
144
|
+
def allowed_origin?(origin)
|
|
145
|
+
Shakha.config.allowed_redirect_origins&.include?(origin) || false
|
|
146
|
+
end
|
|
254
147
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
258
153
|
end
|
|
154
|
+
end
|
|
259
155
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
264
162
|
end
|
|
265
163
|
end
|
|
266
|
-
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,43 +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
|
-
:signing_key,
|
|
13
|
-
:verification_key,
|
|
14
|
-
:key_id,
|
|
15
12
|
:rate_limiting_enabled,
|
|
16
13
|
:allowed_redirect_origins
|
|
17
14
|
|
|
18
15
|
def initialize
|
|
19
16
|
@session_lifetime = 30.days
|
|
20
|
-
@issuer = "https://shakha.dev"
|
|
21
17
|
@rate_limiting_enabled = false
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def embedded?
|
|
25
|
-
service_url.blank?
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def service_base_url
|
|
29
|
-
return app_origin if embedded?
|
|
30
|
-
|
|
31
|
-
service_url.chomp("/")
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def client_id
|
|
35
|
-
return @client_id if defined?(@client_id)
|
|
36
|
-
|
|
37
|
-
origin = URI.parse(app_origin).origin
|
|
38
|
-
@client_id = "origin:#{origin}"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def audience
|
|
42
|
-
client_id
|
|
18
|
+
@providers = [:google]
|
|
43
19
|
end
|
|
44
20
|
end
|
|
45
|
-
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(', ')}"
|