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.
@@ -1,127 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "jwt"
4
-
5
- module Shakha
6
- class ConfigurationError < StandardError; end
7
- class JWTError < StandardError; end
8
-
9
- class JwtHandler
10
- ALGORITHM = "ES256"
11
-
12
- class << self
13
- def encode(payload, exp: 24.hours.from_now)
14
- secret = signing_key || raise(ConfigurationError, "RSA/EC private key required for signing")
15
-
16
- header = {
17
- alg: ALGORITHM,
18
- typ: "JWT",
19
- kid: key_id
20
- }
21
-
22
- payload = payload.with_indifferent_access.merge(
23
- iss: Shakha.config.issuer,
24
- aud: Shakha.config.audience,
25
- iat: Time.current.to_i,
26
- exp: exp.to_i,
27
- jti: SecureRandom.uuid
28
- )
29
-
30
- JWT.encode(payload, secret, ALGORITHM, header)
31
- end
32
-
33
- def verify(token, audience: nil)
34
- public_key = verification_key || raise(ConfigurationError, "RSA/EC public key required for verification")
35
-
36
- decoded = JWT.decode(
37
- token,
38
- public_key,
39
- true,
40
- {
41
- algorithm: ALGORITHM,
42
- iss: Shakha.config.issuer,
43
- aud: audience || Shakha.config.audience,
44
- verify_iss: true,
45
- verify_aud: true,
46
- verify_expiration: true
47
- }
48
- )
49
-
50
- decoded[0].with_indifferent_access
51
- rescue JWT::DecodeError => e
52
- raise JWTError, e.message
53
- end
54
-
55
- def jwks
56
- {
57
- keys: [
58
- {
59
- kty: "EC",
60
- crv: "P-256",
61
- x: Base64.urlsafe_encode64(public_key_point&.x || public_key_raw_point[0..31], padding: false),
62
- y: Base64.urlsafe_encode64(public_key_point&.y || public_key_raw_point[32..63], padding: false),
63
- use: "sig",
64
- alg: ALGORITHM,
65
- kid: key_id
66
- }
67
- ]
68
- }.to_json
69
- end
70
-
71
- private
72
-
73
- def signing_key
74
- return @signing_key if defined?(@signing_key)
75
-
76
- key_material = Shakha.config.signing_key
77
- return @signing_key = nil unless key_material
78
-
79
- if key_material.is_a?(OpenSSL::PKey::EC)
80
- @signing_key = key_material
81
- elsif key_material.start_with?("-----BEGIN")
82
- @signing_key = OpenSSL::PKey::EC.new(key_material)
83
- else
84
- @signing_key = OpenSSL::PKey::EC.new(Base64.decode64(key_material))
85
- end
86
- end
87
-
88
- def verification_key
89
- return signing_key&.public_key if signing_key
90
-
91
- public_material = Shakha.config.verification_key
92
- return nil unless public_materiall
93
-
94
- if public_material.start_with?("-----BEGIN")
95
- OpenSSL::PKey::EC.new(public_material)
96
- else
97
- OpenSSL::PKey::EC.new(Base64.decode64(public_material))
98
- end
99
- end
100
-
101
- def public_key_point
102
- @public_key_point ||= begin
103
- key = verification_key || signing_key&.public_key
104
- return nil unless key
105
-
106
- group = key.group
107
- point = key.public_key
108
- { x: point.x, y: point.y }
109
- end
110
- end
111
-
112
- def public_key_raw_point
113
- @public_key_raw_point ||= begin
114
- key = verification_key || signing_key&.public_key
115
- return nil unless key
116
-
117
- point = key.public_key
118
- [point.x, point.y].map { |n| n.to_s(16).rjust(64, "0") }.join.scan(/../).map { |b| b.to_i(16).chr }.join
119
- end
120
- end
121
-
122
- def key_id
123
- Shakha.config.key_id || "default"
124
- end
125
- end
126
- end
127
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Shakha
4
- class Middleware
5
- def initialize(app)
6
- @app = app
7
- end
8
-
9
- def call(env)
10
- @env = env
11
- @request = ActionDispatch::Request.new(env)
12
-
13
- if verify_token_request?
14
- verify_token!
15
- else
16
- @app.call(env)
17
- end
18
- end
19
-
20
- private
21
-
22
- attr_reader :request
23
-
24
- def verify_token_request?
25
- request.path == "/auth/shakha/token" && request.post?
26
- end
27
-
28
- def verify_token!
29
- token = extract_token || raise(JWTError, "Missing token")
30
- payload = Shakha.verify_token(token)
31
-
32
- @env["shakha.user_id"] = payload[:pairwise_sub]
33
- @env["shakha.email"] = payload[:email]
34
- @env["shakha.name"] = payload[:name]
35
-
36
- @app.call(@env)
37
- rescue JWTError => e
38
- [401, { "Content-Type" => "application/json" }, [{ error: e.message }.to_json]]
39
- end
40
-
41
- def extract_token
42
- if request.content_type == "application/json"
43
- JSON.parse(request.body.read)["id_token"]
44
- elsif request.params["id_token"].present?
45
- request.params["id_token"]
46
- end
47
- end
48
- end
49
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "base64"
4
- require "openssl"
5
- require "jwt"
6
-
7
- module Shakha
8
- module Pairwise
9
- HMAC_DIGEST = OpenSSL::Digest::SHA256.new
10
-
11
- class << self
12
- def derive(google_sub, client_id)
13
- secret = secret_key
14
- input = "#{google_sub}:#{client_id}"
15
- digest = OpenSSL::HMAC.hexdigest(HMAC_DIGEST, secret, input)
16
- "ps_#{digest}"
17
- end
18
-
19
- private
20
-
21
- def secret_key
22
- Shakha.config.service_secret || raise(Shakha::ConfigurationError, "SHAKHA_SECRET not set")
23
- end
24
- end
25
- end
26
- end