keycloak-api-rails 1.1.1 → 2.0.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +51 -21
  5. data/keycloak-api-rails.gemspec +16 -6
  6. data/lib/keycloak-api-rails/authentication.rb +3 -11
  7. data/lib/keycloak-api-rails/configuration.rb +84 -0
  8. data/lib/keycloak-api-rails/helper.rb +31 -4
  9. data/lib/keycloak-api-rails/http_client.rb +54 -19
  10. data/lib/keycloak-api-rails/middleware.rb +17 -20
  11. data/lib/keycloak-api-rails/public_key_cached_resolver.rb +26 -7
  12. data/lib/keycloak-api-rails/railtie.rb +16 -1
  13. data/lib/keycloak-api-rails/service.rb +77 -28
  14. data/lib/keycloak-api-rails/testing.rb +5 -3
  15. data/lib/keycloak-api-rails/token_error.rb +41 -23
  16. data/lib/keycloak-api-rails/version.rb +1 -1
  17. data/lib/keycloak-api-rails.rb +21 -6
  18. metadata +39 -23
  19. data/.github/workflows/ci.yml +0 -35
  20. data/.github/workflows/release.yml +0 -52
  21. data/.gitignore +0 -9
  22. data/.rspec +0 -2
  23. data/Dockerfile +0 -13
  24. data/Gemfile +0 -3
  25. data/Rakefile +0 -6
  26. data/spec/keycloak-api-rails/activesupport_free_spec.rb +0 -18
  27. data/spec/keycloak-api-rails/authentication_spec.rb +0 -70
  28. data/spec/keycloak-api-rails/configuration_spec.rb +0 -101
  29. data/spec/keycloak-api-rails/helper_spec.rb +0 -94
  30. data/spec/keycloak-api-rails/public_key_cached_resolver_spec.rb +0 -80
  31. data/spec/keycloak-api-rails/service_spec.rb +0 -281
  32. data/spec/keycloak-api-rails/testing_spec.rb +0 -102
  33. data/spec/spec_helper.rb +0 -14
  34. data/spec/support/activesupport_free_probe.rb +0 -131
  35. data/spec/support/public_key_cached_resolver_stub.rb +0 -11
  36. data/spec/support/public_key_resolver_stub.rb +0 -7
  37. data/spec/support/rails_helper.rb +0 -4
@@ -1,102 +0,0 @@
1
- require_relative "../../lib/keycloak-api-rails/testing"
2
-
3
- RSpec.describe KeycloakApiRails::Testing do
4
- include KeycloakApiRails::Testing::Helpers
5
-
6
- # The tokens are validated by the real middleware and the real service, so that these examples
7
- # fail if what this module forges stops being what the library expects.
8
- let(:authenticated_env) { {} }
9
- let(:app) do
10
- downstream = lambda do |env|
11
- authenticated_env.replace(env)
12
- [200, {}, ["OK"]]
13
- end
14
- KeycloakApiRails::Middleware.new(downstream)
15
- end
16
-
17
- def get(headers)
18
- app.call({
19
- "REQUEST_METHOD" => "GET",
20
- "PATH_INFO" => "/me",
21
- "REQUEST_URI" => "http://example.org/me",
22
- "HTTP_AUTHORIZATION" => headers["Authorization"]
23
- })
24
- end
25
-
26
- before(:each) do
27
- # The middleware only authenticates a request when the configuration tells it to, and the
28
- # configuration is global: these examples cannot rely on what the other ones leave behind.
29
- KeycloakApiRails.config.opt_in = false
30
- KeycloakApiRails.config.skip_paths = {}
31
- KeycloakApiRails::Testing.stub_public_keys!
32
- end
33
-
34
- after(:each) do
35
- KeycloakApiRails.public_key_resolver = nil
36
- end
37
-
38
- describe ".stub_public_keys!" do
39
- it "should validate the tokens forged by this module" do
40
- status, _headers, _body = get(keycloak_auth_headers)
41
-
42
- expect(status).to eq(200)
43
- end
44
-
45
- it "should not require a Keycloak server to be configured" do
46
- expect(KeycloakApiRails.config.server_url).to be_nil
47
- expect(KeycloakApiRails.config.realm_id).to be_nil
48
-
49
- status, _headers, _body = get(keycloak_auth_headers)
50
-
51
- expect(status).to eq(200)
52
- end
53
- end
54
-
55
- describe ".token_for" do
56
- it "should authenticate the given user" do
57
- get(keycloak_auth_headers(sub: "a-keycloak-id"))
58
-
59
- expect(KeycloakApiRails::Helper.current_user_id(authenticated_env)).to eq("a-keycloak-id")
60
- end
61
-
62
- it "should generate a different user for each token" do
63
- first_token = JSON::JWT.decode(keycloak_token, KeycloakApiRails::Testing.public_keys)
64
- second_token = JSON::JWT.decode(keycloak_token, KeycloakApiRails::Testing.public_keys)
65
-
66
- expect(first_token["sub"]).not_to eq(second_token["sub"])
67
- end
68
-
69
- it "should expose every claim read by the library" do
70
- get(keycloak_auth_headers(sub: "a-keycloak-id",
71
- email: "an-email@keycloak.io",
72
- locale: "fr",
73
- authorized_party: "a-client",
74
- roles: ["admin", "user"],
75
- resource_roles: { "a-client" => ["reader"] }))
76
-
77
- expect(KeycloakApiRails::Helper.current_user_id(authenticated_env)).to eq("a-keycloak-id")
78
- expect(KeycloakApiRails::Helper.current_user_email(authenticated_env)).to eq("an-email@keycloak.io")
79
- expect(KeycloakApiRails::Helper.current_user_locale(authenticated_env)).to eq("fr")
80
- expect(KeycloakApiRails::Helper.current_authorized_party(authenticated_env)).to eq("a-client")
81
- expect(KeycloakApiRails::Helper.current_user_roles(authenticated_env)).to eq(["admin", "user"])
82
- expect(KeycloakApiRails::Helper.current_resource_roles(authenticated_env)).to eq({ "a-client" => ["reader"] })
83
- end
84
-
85
- it "should expose the claims declared as custom attributes" do
86
- KeycloakApiRails.config.custom_attributes = ["tenant_id"]
87
-
88
- get(keycloak_auth_headers(claims: { "tenant_id" => "a-tenant" }))
89
-
90
- expect(KeycloakApiRails::Helper.current_user_custom_attributes(authenticated_env)).to eq({ "tenant_id" => "a-tenant" })
91
- ensure
92
- KeycloakApiRails.config.custom_attributes = []
93
- end
94
-
95
- it "should forge a token that is expired when 'expires_at' is in the past" do
96
- status, _headers, body = get(keycloak_auth_headers(issued_at: Time.now - 7200, expires_at: Time.now - 3600))
97
-
98
- expect(status).to eq(401)
99
- expect(body.first).to include("JWT token is expired")
100
- end
101
- end
102
- end
data/spec/spec_helper.rb DELETED
@@ -1,14 +0,0 @@
1
- require_relative "../lib/keycloak-api-rails"
2
- require_relative "support/rails_helper"
3
- require_relative "support/public_key_cached_resolver_stub"
4
- require_relative "support/public_key_resolver_stub"
5
- require "timecop"
6
- require "byebug"
7
-
8
- RSpec.configure do |config|
9
- config.include RailsHelper
10
-
11
- config.expect_with :rspec do |expectations|
12
- expectations.include_chain_clauses_in_custom_matcher_descriptions = true
13
- end
14
- end
@@ -1,131 +0,0 @@
1
- # Loads the library and exercises every code path that used to rely on ActiveSupport,
2
- # in a process where ActiveSupport is *not* loaded.
3
- #
4
- # The regular test suite loads `rails/all`, which makes ActiveSupport available everywhere and
5
- # therefore cannot detect that the library depends on it again. This probe is executed in a
6
- # separate process by spec/keycloak-api-rails/activesupport_free_spec.rb.
7
- #
8
- # The library files are required one by one on purpose: `lib/keycloak-api-rails.rb` pulls in
9
- # `json/jwt`, which depends on ActiveSupport. The point of this probe is that *our* code does not
10
- # use ActiveSupport, not that it never gets loaded by a third party.
11
-
12
- require "uri"
13
- require "date"
14
- require "logger"
15
-
16
- LIB = File.expand_path("../../../lib/keycloak-api-rails", __FILE__)
17
-
18
- # Minimal stand-in for the parts of lib/keycloak-api-rails.rb the required files call at runtime.
19
- module KeycloakApiRails
20
- class << self
21
- attr_accessor :config
22
- end
23
- end
24
-
25
- require "#{LIB}/configuration"
26
- require "#{LIB}/helper"
27
- require "#{LIB}/authentication"
28
- require "#{LIB}/public_key_resolver"
29
- require "#{LIB}/public_key_cached_resolver"
30
- require "#{LIB}/service"
31
-
32
- $failures = []
33
-
34
- def assert(description)
35
- $failures << description unless yield
36
- rescue => e
37
- $failures << "#{description} (raised #{e.class}: #{e.message})"
38
- end
39
-
40
- assert("ActiveSupport is not loaded by the library") { !defined?(ActiveSupport) }
41
-
42
- ### Configuration -- used to be ActiveSupport::Configurable
43
- configuration = KeycloakApiRails::Configuration.new
44
- KeycloakApiRails.config = configuration
45
-
46
- %i[
47
- server_url realm_id skip_paths opt_in token_expiration_tolerance_in_seconds
48
- public_key_cache_ttl custom_attributes logger ca_certificate_file
49
- ].each do |name|
50
- assert("Configuration##{name} is nil until it is assigned") { configuration.public_send(name).nil? }
51
- configuration.public_send("#{name}=", "a value for #{name}")
52
- assert("Configuration##{name} returns what was assigned") { configuration.public_send(name) == "a value for #{name}" }
53
- end
54
-
55
- ### Authentication -- used to be ActiveSupport::Concern
56
- declared_helper_methods = []
57
- controller_with_helpers = Class.new do
58
- define_singleton_method(:helper_method) { |*names| declared_helper_methods.concat(names) }
59
- end
60
- controller_with_helpers.include(KeycloakApiRails::Authentication)
61
- assert("Authentication declares keycloak_authenticate as a helper method") do
62
- declared_helper_methods == [:keycloak_authenticate]
63
- end
64
- assert("Authentication can be included in a class without helper_method") do
65
- Class.new.include(KeycloakApiRails::Authentication)
66
- true
67
- end
68
- assert("Authentication keeps its methods protected") do
69
- KeycloakApiRails::Authentication.protected_instance_methods.sort ==
70
- %i[authentication_failed authentication_succeeded keycloak_authenticate]
71
- end
72
-
73
- ### Helper#read_token_from_query_string -- used to be `present?` and `second`
74
- {
75
- nil => "",
76
- "" => "",
77
- " " => "",
78
- "http://www.an-url.io/health" => nil,
79
- "http://www.an-url.io/health?firstName=ouioui" => nil,
80
- "http://www.an-url.io/health?authorizationToken=aToken" => "aToken",
81
- "http://www.an-url.io/health?a=1&authorizationToken=aToken&b=2" => "aToken",
82
- }.each do |uri, expected|
83
- assert("Helper.read_token_from_query_string(#{uri.inspect}) returns #{expected.inspect}") do
84
- KeycloakApiRails::Helper.read_token_from_query_string(uri) == expected
85
- end
86
- end
87
-
88
- ### Service#expired? -- used to be `to_datetime` and `seconds`
89
- configuration.token_expiration_tolerance_in_seconds = 10
90
- configuration.skip_paths = {}
91
- configuration.opt_in = false
92
- configuration.logger = ::Logger.new(File::NULL)
93
- service = KeycloakApiRails::Service.new(nil)
94
- now = Time.now
95
-
96
- {
97
- "expiring in one hour" => [(now + 3600).to_i, false],
98
- "expired two days ago" => [(now - 172800).to_i, true],
99
- "expiring within the tolerance (5s < 10s)" => [(now + 5).to_i, true],
100
- "expiring after the tolerance (30s > 10s)" => [(now + 30).to_i, false],
101
- }.each do |description, (exp, expected)|
102
- assert("Service#expired? returns #{expected} for a token #{description}") do
103
- service.send(:expired?, { "exp" => exp }) == expected
104
- end
105
- end
106
-
107
- ### PublicKeyCachedResolver -- used to be `seconds`
108
- ttl = 86400
109
- resolver = KeycloakApiRails::PublicKeyCachedResolver.new(nil, "a-realm", ttl)
110
- assert("PublicKeyCachedResolver considers an empty cache as outdated") do
111
- resolver.send(:public_keys_are_outdated?) == true
112
- end
113
- resolver.instance_variable_set(:@cached_public_keys, "a-public-key")
114
- resolver.instance_variable_set(:@cached_public_key_retrieved_at, Time.now - (ttl - 10))
115
- assert("PublicKeyCachedResolver keeps a cache that is within its TTL") do
116
- resolver.send(:public_keys_are_outdated?) == false
117
- end
118
- resolver.instance_variable_set(:@cached_public_key_retrieved_at, Time.now - (ttl + 10))
119
- assert("PublicKeyCachedResolver invalidates a cache that outlived its TTL") do
120
- resolver.send(:public_keys_are_outdated?) == true
121
- end
122
-
123
- assert("ActiveSupport has not been loaded while exercising the library") { !defined?(ActiveSupport) }
124
-
125
- if $failures.empty?
126
- puts "The library behaves correctly without ActiveSupport"
127
- exit 0
128
- else
129
- $failures.each { |failure| puts "FAILED: #{failure}" }
130
- exit 1
131
- end
@@ -1,11 +0,0 @@
1
- module KeycloakApiRails
2
- class PublicKeyCachedResolverStub
3
- def initialize(public_key)
4
- @public_key = public_key
5
- end
6
-
7
- def find_public_keys
8
- @public_key
9
- end
10
- end
11
- end
@@ -1,7 +0,0 @@
1
- module KeycloakApiRails
2
- class PublicKeyResolverStub
3
- def find_public_keys
4
- OpenSSL::PKey::RSA.generate(1024).public_key
5
- end
6
- end
7
- end
@@ -1,4 +0,0 @@
1
- require "rails/all"
2
-
3
- module RailsHelper
4
- end