standard_id 0.2.5 → 0.2.6
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/app/controllers/standard_id/api/well_known/jwks_controller.rb +21 -0
- data/config/routes/api.rb +4 -0
- data/lib/generators/standard_id/install/templates/standard_id.rb +30 -0
- data/lib/standard_id/config/schema.rb +10 -0
- data/lib/standard_id/jwt_service.rb +93 -6
- data/lib/standard_id/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '088c979a8207e9ed4cf6cdeec8a679985271bea03989b3feeaa187bf28df95f4'
|
|
4
|
+
data.tar.gz: 0aee93fe07e01a10c1c5133bffac61dae72f59933999c279f1fb54ee178e27e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ea65251e2b8a5599ee837d0e35ce8204ae5c98fd701fbba18fa2e53b34450af6d30fb54b89149c3539ffefbdadc60a93db9688b94469df08957a307ce62884a
|
|
7
|
+
data.tar.gz: bf4af5c23d299ff9488b44a4f5e3a30c59f892a155d4ca01491fb6c3b434946ba43d967f8e2e8c4d4ea4cdaa811d855bebfa8d4632430c7f779339475bdb6004
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StandardId
|
|
4
|
+
module Api
|
|
5
|
+
module WellKnown
|
|
6
|
+
class JwksController < ActionController::API
|
|
7
|
+
def show
|
|
8
|
+
jwks = StandardId::JwtService.jwks
|
|
9
|
+
|
|
10
|
+
if jwks.nil?
|
|
11
|
+
render json: { error: "JWKS not available" }, status: :not_found
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
response.headers["Cache-Control"] = "public, max-age=3600"
|
|
16
|
+
render json: jwks
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/config/routes/api.rb
CHANGED
|
@@ -8,6 +8,10 @@ StandardId.configure do |c|
|
|
|
8
8
|
# c.logger = Rails.logger
|
|
9
9
|
# c.web_layout = "application"
|
|
10
10
|
|
|
11
|
+
# JWT issuer claim (iss) - typically your application's URL
|
|
12
|
+
# Used in JWT tokens and verified on decode when set
|
|
13
|
+
# c.issuer = "https://auth.example.com"
|
|
14
|
+
|
|
11
15
|
# Inertia.js support (requires inertia_rails gem)
|
|
12
16
|
# When enabled, StandardId web controllers will render Inertia components
|
|
13
17
|
# instead of ERB views. You must create the corresponding components in your
|
|
@@ -51,6 +55,32 @@ StandardId.configure do |c|
|
|
|
51
55
|
# }
|
|
52
56
|
# c.oauth.allowed_audiences = %w[web mobile admin] # Empty = no validation
|
|
53
57
|
|
|
58
|
+
# JWT Signing Configuration (Asymmetric Algorithms)
|
|
59
|
+
# By default, JWTs are signed with HS256 using Rails.application.secret_key_base.
|
|
60
|
+
# For asymmetric signing, configure a private key and expose the public key
|
|
61
|
+
# via the JWKS endpoint at /.well-known/jwks.json
|
|
62
|
+
#
|
|
63
|
+
# Algorithm choices:
|
|
64
|
+
# ES256 (ECDSA) - Recommended for new projects. Smaller keys, faster, modern.
|
|
65
|
+
# RS256 (RSA) - Wider compatibility with older systems.
|
|
66
|
+
#
|
|
67
|
+
# Generate keys:
|
|
68
|
+
# ES256: openssl ecparam -name prime256v1 -genkey -noout -out signing_key.pem
|
|
69
|
+
# RS256: openssl genrsa -out signing_key.pem 2048
|
|
70
|
+
#
|
|
71
|
+
# Option 1: Rails credentials (recommended)
|
|
72
|
+
# c.oauth.signing_algorithm = :es256
|
|
73
|
+
# c.oauth.signing_key = Rails.application.credentials.dig(:standard_id, :signing_key)
|
|
74
|
+
#
|
|
75
|
+
# Option 2: Environment variable (replace literal \n with newlines)
|
|
76
|
+
# Convert PEM to env-safe format: awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' signing_key.pem
|
|
77
|
+
# c.oauth.signing_algorithm = :es256
|
|
78
|
+
# c.oauth.signing_key = ENV["JWT_SIGNING_KEY"]&.gsub('\n', "\n")
|
|
79
|
+
#
|
|
80
|
+
# Option 3: File path
|
|
81
|
+
# c.oauth.signing_algorithm = :es256
|
|
82
|
+
# c.oauth.signing_key = Rails.root.join("config/signing_key.pem")
|
|
83
|
+
|
|
54
84
|
# Events
|
|
55
85
|
# Enable or disable logging emitted via the internal event system
|
|
56
86
|
# c.events.enable_logging = false
|
|
@@ -51,6 +51,16 @@ StandardConfig.schema.draw do
|
|
|
51
51
|
field :scope_claims, type: :hash, default: -> { {} }
|
|
52
52
|
field :claim_resolvers, type: :hash, default: -> { {} }
|
|
53
53
|
field :allowed_audiences, type: :array, default: -> { [] } # Empty = no validation, any audience allowed
|
|
54
|
+
|
|
55
|
+
# JWT signing configuration (for asymmetric algorithms)
|
|
56
|
+
# If nil, uses HS256 with Rails.application.secret_key_base
|
|
57
|
+
field :signing_key, type: :any, default: nil
|
|
58
|
+
|
|
59
|
+
# Signing algorithm (see JwtService::SUPPORTED_ALGORITHMS for full list)
|
|
60
|
+
# Symmetric (HMAC): :hs256, :hs384, :hs512
|
|
61
|
+
# Asymmetric (RSA): :rs256, :rs384, :rs512
|
|
62
|
+
# Asymmetric (ECDSA): :es256, :es384, :es512
|
|
63
|
+
field :signing_algorithm, type: :symbol, default: :hs256
|
|
54
64
|
end
|
|
55
65
|
|
|
56
66
|
scope :social do
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
require "jwt"
|
|
2
2
|
require "concurrent/delay"
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "digest"
|
|
3
5
|
|
|
4
6
|
module StandardId
|
|
5
7
|
class JwtService
|
|
6
|
-
ALGORITHM = "HS256"
|
|
7
8
|
RESERVED_JWT_KEYS = %i[sub client_id scope grant_type exp iat aud iss nbf jti]
|
|
8
9
|
BASE_SESSION_FIELDS = %i[account_id client_id scopes grant_type aud]
|
|
9
10
|
|
|
11
|
+
# Supported signing algorithms categorized by type
|
|
12
|
+
# Symmetric: use shared secret (Rails.application.secret_key_base)
|
|
13
|
+
# Asymmetric: use key pairs (RSA or EC private key)
|
|
14
|
+
SUPPORTED_ALGORITHMS = {
|
|
15
|
+
# HMAC (symmetric)
|
|
16
|
+
"HS256" => { type: :symmetric },
|
|
17
|
+
"HS384" => { type: :symmetric },
|
|
18
|
+
"HS512" => { type: :symmetric },
|
|
19
|
+
# RSA (asymmetric)
|
|
20
|
+
"RS256" => { type: :asymmetric, key_class: OpenSSL::PKey::RSA },
|
|
21
|
+
"RS384" => { type: :asymmetric, key_class: OpenSSL::PKey::RSA },
|
|
22
|
+
"RS512" => { type: :asymmetric, key_class: OpenSSL::PKey::RSA },
|
|
23
|
+
# ECDSA (asymmetric)
|
|
24
|
+
"ES256" => { type: :asymmetric, key_class: OpenSSL::PKey::EC },
|
|
25
|
+
"ES384" => { type: :asymmetric, key_class: OpenSSL::PKey::EC },
|
|
26
|
+
"ES512" => { type: :asymmetric, key_class: OpenSSL::PKey::EC }
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
10
29
|
SESSION_CLASS = Concurrent::Delay.new do
|
|
11
30
|
Struct.new(*(BASE_SESSION_FIELDS + claim_resolver_keys), keyword_init: true) do
|
|
12
31
|
def active?
|
|
@@ -19,17 +38,73 @@ module StandardId
|
|
|
19
38
|
SESSION_CLASS.value
|
|
20
39
|
end
|
|
21
40
|
|
|
41
|
+
def self.algorithm
|
|
42
|
+
StandardId.config.oauth.signing_algorithm.to_s.upcase
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.algorithm_config
|
|
46
|
+
SUPPORTED_ALGORITHMS[algorithm] || raise(ArgumentError, "Unsupported algorithm: #{algorithm}. Supported: #{SUPPORTED_ALGORITHMS.keys.join(', ')}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.asymmetric?
|
|
50
|
+
algorithm_config[:type] == :asymmetric
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.signing_key
|
|
54
|
+
if asymmetric?
|
|
55
|
+
@signing_key_cache ||= parse_private_key(StandardId.config.oauth.signing_key)
|
|
56
|
+
else
|
|
57
|
+
Rails.application.secret_key_base
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.verification_key
|
|
62
|
+
if asymmetric?
|
|
63
|
+
key = signing_key
|
|
64
|
+
# For EC keys, the key itself can be used for verification
|
|
65
|
+
# For RSA keys, we extract the public key
|
|
66
|
+
key.is_a?(OpenSSL::PKey::EC) ? key : key.public_key
|
|
67
|
+
else
|
|
68
|
+
Rails.application.secret_key_base
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.key_id
|
|
73
|
+
return nil unless asymmetric?
|
|
74
|
+
|
|
75
|
+
# Generate stable key ID from public key fingerprint
|
|
76
|
+
# Use public_to_pem which works for both RSA and EC keys
|
|
77
|
+
@key_id ||= Digest::SHA256.hexdigest(signing_key.public_to_pem)[0..7]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.reset_cached_key!
|
|
81
|
+
@key_id = nil
|
|
82
|
+
@signing_key_cache = nil
|
|
83
|
+
@jwks = nil
|
|
84
|
+
end
|
|
85
|
+
|
|
22
86
|
def self.encode(payload, expires_in: 1.hour)
|
|
23
87
|
payload[:exp] = expires_in.from_now.to_i
|
|
24
88
|
payload[:iat] = Time.current.to_i
|
|
89
|
+
payload[:iss] ||= StandardId.config.issuer if StandardId.config.issuer.present?
|
|
90
|
+
|
|
91
|
+
headers = {}
|
|
92
|
+
headers[:kid] = key_id if asymmetric?
|
|
25
93
|
|
|
26
|
-
JWT.encode(payload,
|
|
94
|
+
JWT.encode(payload, signing_key, algorithm, headers)
|
|
27
95
|
end
|
|
28
96
|
|
|
29
97
|
def self.decode(token)
|
|
30
|
-
|
|
98
|
+
options = { algorithm: algorithm }
|
|
99
|
+
|
|
100
|
+
if StandardId.config.issuer.present?
|
|
101
|
+
options[:iss] = StandardId.config.issuer
|
|
102
|
+
options[:verify_iss] = true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
decoded = JWT.decode(token, verification_key, true, options)
|
|
31
106
|
decoded.first.with_indifferent_access
|
|
32
|
-
rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::InvalidIatError
|
|
107
|
+
rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::InvalidIatError, JWT::InvalidIssuerError
|
|
33
108
|
nil
|
|
34
109
|
end
|
|
35
110
|
|
|
@@ -53,10 +128,22 @@ module StandardId
|
|
|
53
128
|
)
|
|
54
129
|
end
|
|
55
130
|
|
|
131
|
+
def self.jwks
|
|
132
|
+
return nil unless asymmetric?
|
|
133
|
+
|
|
134
|
+
@jwks ||= begin
|
|
135
|
+
jwk = JWT::JWK.new(verification_key, kid: key_id)
|
|
136
|
+
{ keys: [jwk.export] }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
56
140
|
private
|
|
57
141
|
|
|
58
|
-
def self.
|
|
59
|
-
|
|
142
|
+
def self.parse_private_key(key_source)
|
|
143
|
+
pem = key_source.is_a?(Pathname) ? File.read(key_source) : key_source
|
|
144
|
+
key_class = algorithm_config[:key_class]
|
|
145
|
+
|
|
146
|
+
key_class.new(pem)
|
|
60
147
|
end
|
|
61
148
|
|
|
62
149
|
def self.claim_resolver_keys
|
data/lib/standard_id/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: standard_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -80,6 +80,7 @@ files:
|
|
|
80
80
|
- app/controllers/standard_id/api/oidc/logout_controller.rb
|
|
81
81
|
- app/controllers/standard_id/api/passwordless_controller.rb
|
|
82
82
|
- app/controllers/standard_id/api/userinfo_controller.rb
|
|
83
|
+
- app/controllers/standard_id/api/well_known/jwks_controller.rb
|
|
83
84
|
- app/controllers/standard_id/web/account_controller.rb
|
|
84
85
|
- app/controllers/standard_id/web/auth/callback/providers_controller.rb
|
|
85
86
|
- app/controllers/standard_id/web/base_controller.rb
|