keycloak-api-rails 1.1.1 → 1.1.2
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/CHANGELOG.md +5 -0
- data/MIT-LICENSE +1 -1
- data/README.md +1 -1
- data/keycloak-api-rails.gemspec +13 -2
- data/lib/keycloak-api-rails/version.rb +1 -1
- metadata +10 -22
- data/.github/workflows/ci.yml +0 -35
- data/.github/workflows/release.yml +0 -52
- data/.gitignore +0 -9
- data/.rspec +0 -2
- data/Dockerfile +0 -13
- data/Gemfile +0 -3
- data/Rakefile +0 -6
- data/spec/keycloak-api-rails/activesupport_free_spec.rb +0 -18
- data/spec/keycloak-api-rails/authentication_spec.rb +0 -70
- data/spec/keycloak-api-rails/configuration_spec.rb +0 -101
- data/spec/keycloak-api-rails/helper_spec.rb +0 -94
- data/spec/keycloak-api-rails/public_key_cached_resolver_spec.rb +0 -80
- data/spec/keycloak-api-rails/service_spec.rb +0 -281
- data/spec/keycloak-api-rails/testing_spec.rb +0 -102
- data/spec/spec_helper.rb +0 -14
- data/spec/support/activesupport_free_probe.rb +0 -131
- data/spec/support/public_key_cached_resolver_stub.rb +0 -11
- data/spec/support/public_key_resolver_stub.rb +0 -7
- data/spec/support/rails_helper.rb +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74dec51fa027308c2cf5b57ca67b7316b21677bf1592022f76988f232bbde911
|
|
4
|
+
data.tar.gz: 4836a7951bbf1559ff5eec0dfeb004ff8bcd2955cee7e06ee9e8a553743ad99e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48b3307f4dff316dfa063deddf0d106bbcc409856c20f0af5c441bdc3b1163e9cf93f0c3f7e53fd1a90fcb7946744c2b1b8863fd5a61a0c4012fe3d7df6b5aee
|
|
7
|
+
data.tar.gz: 81aca7224bcae644dd35f6fb715bdb447b0da549bc3da0bdd311a6bea06adfba468936aabcdaeb305a44839c6770a46644abfdf81f2f828246414880e9ab52dd
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.2] - 2026-08-01
|
|
9
|
+
|
|
10
|
+
* Gem metadata
|
|
11
|
+
* The published gem no longer ships non-production code
|
|
12
|
+
|
|
8
13
|
## [1.1.1] - 2026-07-31
|
|
9
14
|
|
|
10
15
|
* New test helpers, in `keycloak-api-rails/testing`: `KeycloakApiRails::Testing.stub_public_keys!` validates the tokens forged by `keycloak_token` and `keycloak_auth_headers`, so that controller and request tests can be authenticated without a Keycloak server, and without the boilerplate the README used to describe. This file is not loaded by `keycloak-api-rails` and has to be required explicitly
|
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
data/keycloak-api-rails.gemspec
CHANGED
|
@@ -9,10 +9,21 @@ Gem::Specification.new do |spec|
|
|
|
9
9
|
spec.email = ["lorent.lempereur.dev@gmail.com"]
|
|
10
10
|
spec.homepage = "https://github.com/looorent/keycloak-api-rails"
|
|
11
11
|
spec.summary = "Rails middleware that validates Authorization token emitted by Keycloak"
|
|
12
|
-
spec.description = "
|
|
12
|
+
spec.description = "Rack middleware that validates the Keycloak JWT access token carried by every " \
|
|
13
|
+
"request of a Ruby on Rails API. It verifies the token signature against the " \
|
|
14
|
+
"realm public keys, exposes the authenticated user, its roles and the custom " \
|
|
15
|
+
"attributes of the token to the controllers, and lets some routes opt out of " \
|
|
16
|
+
"authentication."
|
|
13
17
|
spec.license = "MIT"
|
|
14
18
|
|
|
15
|
-
spec.
|
|
19
|
+
spec.metadata = {
|
|
20
|
+
"source_code_uri" => spec.homepage,
|
|
21
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
|
|
22
|
+
"bug_tracker_uri" => "#{spec.homepage}/issues",
|
|
23
|
+
"rubygems_mfa_required" => "true"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
spec.files = Dir.glob(["lib/**/*.rb", "CHANGELOG.md", "MIT-LICENSE", "README.md", "keycloak-api-rails.gemspec"])
|
|
16
27
|
spec.require_paths = ["lib"]
|
|
17
28
|
|
|
18
29
|
spec.required_ruby_version = ">= 2.7"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: keycloak-api-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lorent Lempereur
|
|
@@ -93,23 +93,19 @@ dependencies:
|
|
|
93
93
|
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: 11.1.3
|
|
96
|
-
description:
|
|
96
|
+
description: Rack middleware that validates the Keycloak JWT access token carried
|
|
97
|
+
by every request of a Ruby on Rails API. It verifies the token signature against
|
|
98
|
+
the realm public keys, exposes the authenticated user, its roles and the custom
|
|
99
|
+
attributes of the token to the controllers, and lets some routes opt out of authentication.
|
|
97
100
|
email:
|
|
98
101
|
- lorent.lempereur.dev@gmail.com
|
|
99
102
|
executables: []
|
|
100
103
|
extensions: []
|
|
101
104
|
extra_rdoc_files: []
|
|
102
105
|
files:
|
|
103
|
-
- ".github/workflows/ci.yml"
|
|
104
|
-
- ".github/workflows/release.yml"
|
|
105
|
-
- ".gitignore"
|
|
106
|
-
- ".rspec"
|
|
107
106
|
- CHANGELOG.md
|
|
108
|
-
- Dockerfile
|
|
109
|
-
- Gemfile
|
|
110
107
|
- MIT-LICENSE
|
|
111
108
|
- README.md
|
|
112
|
-
- Rakefile
|
|
113
109
|
- keycloak-api-rails.gemspec
|
|
114
110
|
- lib/keycloak-api-rails.rb
|
|
115
111
|
- lib/keycloak-api-rails/authentication.rb
|
|
@@ -124,22 +120,14 @@ files:
|
|
|
124
120
|
- lib/keycloak-api-rails/testing.rb
|
|
125
121
|
- lib/keycloak-api-rails/token_error.rb
|
|
126
122
|
- lib/keycloak-api-rails/version.rb
|
|
127
|
-
- spec/keycloak-api-rails/activesupport_free_spec.rb
|
|
128
|
-
- spec/keycloak-api-rails/authentication_spec.rb
|
|
129
|
-
- spec/keycloak-api-rails/configuration_spec.rb
|
|
130
|
-
- spec/keycloak-api-rails/helper_spec.rb
|
|
131
|
-
- spec/keycloak-api-rails/public_key_cached_resolver_spec.rb
|
|
132
|
-
- spec/keycloak-api-rails/service_spec.rb
|
|
133
|
-
- spec/keycloak-api-rails/testing_spec.rb
|
|
134
|
-
- spec/spec_helper.rb
|
|
135
|
-
- spec/support/activesupport_free_probe.rb
|
|
136
|
-
- spec/support/public_key_cached_resolver_stub.rb
|
|
137
|
-
- spec/support/public_key_resolver_stub.rb
|
|
138
|
-
- spec/support/rails_helper.rb
|
|
139
123
|
homepage: https://github.com/looorent/keycloak-api-rails
|
|
140
124
|
licenses:
|
|
141
125
|
- MIT
|
|
142
|
-
metadata:
|
|
126
|
+
metadata:
|
|
127
|
+
source_code_uri: https://github.com/looorent/keycloak-api-rails
|
|
128
|
+
changelog_uri: https://github.com/looorent/keycloak-api-rails/blob/main/CHANGELOG.md
|
|
129
|
+
bug_tracker_uri: https://github.com/looorent/keycloak-api-rails/issues
|
|
130
|
+
rubygems_mfa_required: 'true'
|
|
143
131
|
rdoc_options: []
|
|
144
132
|
require_paths:
|
|
145
133
|
- lib
|
data/.github/workflows/ci.yml
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
name: Ruby
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ "main" ]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [ "main" ]
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: read
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
test:
|
|
14
|
-
name: Ruby ${{ matrix.ruby-version }}
|
|
15
|
-
# Pinned rather than 'ubuntu-latest': ruby/setup-ruby has no prebuilt Rubies for
|
|
16
|
-
# ubuntu-26.04 yet, so the oldest versions of the matrix would break once the
|
|
17
|
-
# 'latest' alias moves on.
|
|
18
|
-
runs-on: ubuntu-24.04
|
|
19
|
-
|
|
20
|
-
strategy:
|
|
21
|
-
fail-fast: false
|
|
22
|
-
matrix:
|
|
23
|
-
# Each version resolves the newest dependencies it supports: Rails 7.1 on Ruby 2.7
|
|
24
|
-
# and 3.0, Rails 7.2 on Ruby 3.1, Rails 8.1 on Ruby 3.2 and above.
|
|
25
|
-
ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
|
|
26
|
-
|
|
27
|
-
steps:
|
|
28
|
-
- uses: actions/checkout@v4
|
|
29
|
-
- name: Set up Ruby
|
|
30
|
-
uses: ruby/setup-ruby@v1
|
|
31
|
-
with:
|
|
32
|
-
ruby-version: ${{ matrix.ruby-version }}
|
|
33
|
-
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
|
34
|
-
- name: Run tests
|
|
35
|
-
run: bundle exec rspec
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
name: Release
|
|
2
|
-
|
|
3
|
-
# Publishing is triggered by pushing a version tag, e.g.:
|
|
4
|
-
# git tag -a v1.2.0 -m "Version 1.2.0" && git push origin v1.2.0
|
|
5
|
-
#
|
|
6
|
-
# No RubyGems API key is stored in this repository: the gem is pushed through
|
|
7
|
-
# RubyGems' Trusted Publishing, which exchanges a short-lived GitHub OIDC token
|
|
8
|
-
# for a scoped RubyGems credential. This requires a trusted publisher declared on
|
|
9
|
-
# https://rubygems.org/gems/keycloak-api-rails pointing to this repository, this
|
|
10
|
-
# workflow file name and the 'release' environment.
|
|
11
|
-
on:
|
|
12
|
-
push:
|
|
13
|
-
tags: [ "v*" ]
|
|
14
|
-
|
|
15
|
-
permissions:
|
|
16
|
-
contents: read
|
|
17
|
-
|
|
18
|
-
jobs:
|
|
19
|
-
release:
|
|
20
|
-
name: Push gem to RubyGems.org
|
|
21
|
-
# Pinned rather than 'ubuntu-latest', for the same reason as in ci.yml.
|
|
22
|
-
runs-on: ubuntu-24.04
|
|
23
|
-
|
|
24
|
-
environment: release
|
|
25
|
-
|
|
26
|
-
permissions:
|
|
27
|
-
contents: write # 'rake release' pushes the version tag when it does not exist yet
|
|
28
|
-
id-token: write # mandatory to request the OIDC token used by trusted publishing
|
|
29
|
-
|
|
30
|
-
steps:
|
|
31
|
-
- uses: actions/checkout@v4
|
|
32
|
-
|
|
33
|
-
- name: Set up Ruby
|
|
34
|
-
uses: ruby/setup-ruby@v1
|
|
35
|
-
with:
|
|
36
|
-
ruby-version: '3.4'
|
|
37
|
-
bundler-cache: true
|
|
38
|
-
|
|
39
|
-
- name: Check that the tag matches KeycloakApiRails::VERSION
|
|
40
|
-
run: |
|
|
41
|
-
tag="${GITHUB_REF_NAME#v}"
|
|
42
|
-
version="$(ruby -Ilib -r keycloak-api-rails/version -e 'print KeycloakApiRails::VERSION')"
|
|
43
|
-
if [ "$tag" != "$version" ]; then
|
|
44
|
-
echo "::error::Tag '${GITHUB_REF_NAME}' does not match KeycloakApiRails::VERSION '${version}'"
|
|
45
|
-
exit 1
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
|
-
- name: Run tests
|
|
49
|
-
run: bundle exec rspec
|
|
50
|
-
|
|
51
|
-
# Builds the gem, pushes it to RubyGems.org and waits for it to be available.
|
|
52
|
-
- uses: rubygems/release-gem@v1
|
data/.gitignore
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
.bundle/
|
|
2
|
-
log/*.log
|
|
3
|
-
.byebug_history
|
|
4
|
-
*.gem
|
|
5
|
-
pkg/
|
|
6
|
-
# This gem supports several Ruby versions, each of them resolving a different set of
|
|
7
|
-
# dependencies (Ruby 2.7 caps Rails to 7.1, Ruby 3.1 to 7.2, Ruby 3.2+ resolves Rails 8.1).
|
|
8
|
-
# A single committed lockfile cannot satisfy all of them.
|
|
9
|
-
Gemfile.lock
|
data/.rspec
DELETED
data/Dockerfile
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
FROM ruby:3.4.10-slim-bookworm
|
|
2
|
-
|
|
3
|
-
RUN apt-get update -qq && apt-get install -y build-essential git ruby-dev && apt-get clean && \
|
|
4
|
-
mkdir -p /usr/src/app/lib/keycloak-api-rails
|
|
5
|
-
|
|
6
|
-
WORKDIR /usr/src/app
|
|
7
|
-
|
|
8
|
-
COPY Gemfile /usr/src/app/
|
|
9
|
-
COPY keycloak-api-rails.gemspec /usr/src/app/
|
|
10
|
-
COPY lib/keycloak-api-rails/version.rb /usr/src/app/lib/keycloak-api-rails/
|
|
11
|
-
RUN bundle install
|
|
12
|
-
COPY . /usr/src/app
|
|
13
|
-
RUN bundle install
|
data/Gemfile
DELETED
data/Rakefile
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
require "rbconfig"
|
|
2
|
-
|
|
3
|
-
RSpec.describe "The library without ActiveSupport" do
|
|
4
|
-
|
|
5
|
-
let(:probe) { File.expand_path("../../support/activesupport_free_probe.rb", __FILE__) }
|
|
6
|
-
|
|
7
|
-
# RUBYOPT is cleared so that nothing set up by bundler is preloaded in the probe process.
|
|
8
|
-
def run_probe
|
|
9
|
-
output = IO.popen({ "RUBYOPT" => nil }, [RbConfig.ruby, probe], err: [:child, :out], &:read)
|
|
10
|
-
[$?.exitstatus, output]
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
it "loads and behaves the same way when ActiveSupport is not available" do
|
|
14
|
-
status, output = run_probe
|
|
15
|
-
|
|
16
|
-
expect(status).to eq(0), "The library relies on ActiveSupport:\n#{output}"
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
require "spec_helper"
|
|
2
|
-
|
|
3
|
-
describe KeycloakApiRails::Authentication do
|
|
4
|
-
class ExampleController < ActionController::Base
|
|
5
|
-
include KeycloakApiRails::Authentication
|
|
6
|
-
# Mark protected methods public so they may be called in tests
|
|
7
|
-
public(*KeycloakApiRails::Authentication.protected_instance_methods)
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
let(:controller) { ExampleController.new }
|
|
11
|
-
let(:token) { 'keycloak_valid_token'}
|
|
12
|
-
let(:headers) do
|
|
13
|
-
{
|
|
14
|
-
'REQUEST_METHOD' => :get,
|
|
15
|
-
'REQUEST_URI' => 'http://www.an-url.io',
|
|
16
|
-
'HTTP_AUTHORIZATION' => "Bearer #{token}"
|
|
17
|
-
}
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
describe "when included" do
|
|
21
|
-
it "declares keycloak_authenticate as a helper method when the base class supports helpers" do
|
|
22
|
-
declared_helper_methods = []
|
|
23
|
-
base = Class.new do
|
|
24
|
-
define_singleton_method(:helper_method) { |*names| declared_helper_methods.concat(names) }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
base.include(KeycloakApiRails::Authentication)
|
|
28
|
-
|
|
29
|
-
expect(declared_helper_methods).to eq [:keycloak_authenticate]
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
it "declares the helper method on a real controller" do
|
|
33
|
-
expect(ExampleController._helper_methods).to include :keycloak_authenticate
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
it "does not fail when the base class does not support helpers" do
|
|
37
|
-
expect {
|
|
38
|
-
Class.new.include(KeycloakApiRails::Authentication)
|
|
39
|
-
}.to_not raise_error
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
it "exposes its methods as protected instance methods" do
|
|
43
|
-
expect(KeycloakApiRails::Authentication.protected_instance_methods).to match_array %i[
|
|
44
|
-
keycloak_authenticate
|
|
45
|
-
authentication_failed
|
|
46
|
-
authentication_succeeded
|
|
47
|
-
]
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
it "does not add any public method to the including class" do
|
|
51
|
-
base = Class.new.include(KeycloakApiRails::Authentication)
|
|
52
|
-
|
|
53
|
-
expect(base.public_instance_methods).to_not include :keycloak_authenticate
|
|
54
|
-
expect(base.protected_instance_methods).to include :keycloak_authenticate
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
describe "#keycloak_authenticate" do
|
|
59
|
-
before do
|
|
60
|
-
# Mock request object because we aren't using real request spec
|
|
61
|
-
allow(controller).to receive(:request).and_return(double("request", env: headers ))
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
it "it authenticates with request header" do
|
|
65
|
-
expect_any_instance_of(KeycloakApiRails::Service).to receive(:decode_and_verify).with(token).and_return("A User")
|
|
66
|
-
expect(controller).to receive(:authentication_succeeded)
|
|
67
|
-
controller.keycloak_authenticate
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
RSpec.describe KeycloakApiRails::Configuration do
|
|
2
|
-
|
|
3
|
-
# Every attribute that used to be declared through ActiveSupport's `config_accessor`.
|
|
4
|
-
let(:attribute_names) do
|
|
5
|
-
%i[
|
|
6
|
-
server_url
|
|
7
|
-
realm_id
|
|
8
|
-
skip_paths
|
|
9
|
-
opt_in
|
|
10
|
-
token_expiration_tolerance_in_seconds
|
|
11
|
-
public_key_cache_ttl
|
|
12
|
-
custom_attributes
|
|
13
|
-
logger
|
|
14
|
-
ca_certificate_file
|
|
15
|
-
]
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
describe "attributes" do
|
|
19
|
-
let(:configuration) { KeycloakApiRails::Configuration.new }
|
|
20
|
-
|
|
21
|
-
it "exposes a reader and a writer for each attribute" do
|
|
22
|
-
attribute_names.each do |name|
|
|
23
|
-
expect(configuration).to respond_to name
|
|
24
|
-
expect(configuration).to respond_to "#{name}="
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
it "returns nil for each attribute that has not been assigned" do
|
|
29
|
-
attribute_names.each do |name|
|
|
30
|
-
expect(configuration.public_send(name)).to be_nil
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
it "returns the value that has been assigned" do
|
|
35
|
-
attribute_names.each do |name|
|
|
36
|
-
configuration.public_send("#{name}=", "a value for #{name}")
|
|
37
|
-
expect(configuration.public_send(name)).to eq "a value for #{name}"
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
it "does not respond to an unknown attribute" do
|
|
42
|
-
expect(configuration).to_not respond_to :not_a_configuration_attribute
|
|
43
|
-
expect {
|
|
44
|
-
configuration.not_a_configuration_attribute
|
|
45
|
-
}.to raise_error NoMethodError
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
describe "#configure" do
|
|
50
|
-
after(:each) do
|
|
51
|
-
KeycloakApiRails.load_configuration
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
it "yields the configuration" do
|
|
55
|
-
expect { |block| KeycloakApiRails.configure(&block) }.to yield_with_args KeycloakApiRails::Configuration
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
it "keeps the values assigned within the block" do
|
|
59
|
-
KeycloakApiRails.configure do |config|
|
|
60
|
-
config.server_url = "http://keycloak:8080"
|
|
61
|
-
config.realm_id = "a-realm"
|
|
62
|
-
config.ca_certificate_file = "/etc/ssl/a-certificate.pem"
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
expect(KeycloakApiRails.config.server_url).to eq "http://keycloak:8080"
|
|
66
|
-
expect(KeycloakApiRails.config.realm_id).to eq "a-realm"
|
|
67
|
-
expect(KeycloakApiRails.config.ca_certificate_file).to eq "/etc/ssl/a-certificate.pem"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
it "always returns the same configuration instance" do
|
|
71
|
-
expect(KeycloakApiRails.config).to be KeycloakApiRails.config
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
describe "#load_configuration" do
|
|
76
|
-
before(:each) do
|
|
77
|
-
KeycloakApiRails.load_configuration
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
after(:each) do
|
|
81
|
-
KeycloakApiRails.load_configuration
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
it "applies the documented default values" do
|
|
85
|
-
config = KeycloakApiRails.config
|
|
86
|
-
|
|
87
|
-
expect(config.server_url).to be_nil
|
|
88
|
-
expect(config.realm_id).to be_nil
|
|
89
|
-
expect(config.logger).to be_a ::Logger
|
|
90
|
-
expect(config.skip_paths).to eq({})
|
|
91
|
-
expect(config.opt_in).to be false
|
|
92
|
-
expect(config.token_expiration_tolerance_in_seconds).to eq 10
|
|
93
|
-
expect(config.public_key_cache_ttl).to eq 86400
|
|
94
|
-
expect(config.custom_attributes).to eq []
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
it "exposes the logger through KeycloakApiRails.logger" do
|
|
98
|
-
expect(KeycloakApiRails.logger).to be KeycloakApiRails.config.logger
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
RSpec.describe KeycloakApiRails::Helper do
|
|
2
|
-
describe "#read_token_from_query_string" do
|
|
3
|
-
|
|
4
|
-
subject { KeycloakApiRails::Helper.read_token_from_query_string(uri) }
|
|
5
|
-
|
|
6
|
-
context "when the uri is nil" do
|
|
7
|
-
let(:uri) { nil }
|
|
8
|
-
it "returns an empty token" do
|
|
9
|
-
expect(subject).to eq ""
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
context "when the uri is empty" do
|
|
14
|
-
let(:uri) { "" }
|
|
15
|
-
it "returns an empty token" do
|
|
16
|
-
expect(subject).to eq ""
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
context "when the uri only contains whitespaces" do
|
|
21
|
-
let(:uri) { " " }
|
|
22
|
-
it "returns an empty token" do
|
|
23
|
-
expect(subject).to eq ""
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
context "when the uri has no query string" do
|
|
28
|
-
let(:uri) { "http://www.an-url.io/health" }
|
|
29
|
-
it "returns no token" do
|
|
30
|
-
expect(subject).to be_nil
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
context "when the uri has a query string but no token" do
|
|
35
|
-
let(:uri) { "http://www.an-url.io/health?firstName=ouioui" }
|
|
36
|
-
it "returns no token" do
|
|
37
|
-
expect(subject).to be_nil
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
context "when the uri has a token in the query string" do
|
|
42
|
-
let(:uri) { "http://www.an-url.io/health?authorizationToken=aToken" }
|
|
43
|
-
it "returns the token" do
|
|
44
|
-
expect(subject).to eq "aToken"
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
context "when the uri has a token among other query string parameters" do
|
|
49
|
-
let(:uri) { "http://www.an-url.io/health?firstName=ouioui&authorizationToken=aToken&lastName=nonnon" }
|
|
50
|
-
it "returns the token" do
|
|
51
|
-
expect(subject).to eq "aToken"
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
context "when the uri has an empty token in the query string" do
|
|
56
|
-
let(:uri) { "http://www.an-url.io/health?authorizationToken=" }
|
|
57
|
-
it "returns an empty token" do
|
|
58
|
-
expect(subject).to eq ""
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
describe "#create_url_with_token" do
|
|
64
|
-
|
|
65
|
-
let(:uri) { "http://www.an-url.io" }
|
|
66
|
-
let(:token) { "aToken" }
|
|
67
|
-
|
|
68
|
-
before(:each) do
|
|
69
|
-
@url_with_token = KeycloakApiRails::Helper.create_url_with_token(uri, token)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
context "when the uri has no query string yet" do
|
|
73
|
-
it "returns an url with the provided token" do
|
|
74
|
-
expect(@url_with_token).to eq "#{uri}?authorizationToken=#{token}"
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
context "when the uri already has no query strings" do
|
|
79
|
-
context "but no token yet" do
|
|
80
|
-
let(:uri) { "http://www.an-url.io?firstName=ouioui&lastName=nonnon" }
|
|
81
|
-
it "returns an url with all the query string and the token" do
|
|
82
|
-
expect(@url_with_token).to eq "#{uri}&authorizationToken=#{token}"
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
context "including a token" do
|
|
87
|
-
let(:uri) { "http://www.an-url.io?authorizationToken=ouioui&lastName=nonnon" }
|
|
88
|
-
it "returns an url with all the query string and the new token" do
|
|
89
|
-
expect(@url_with_token).to eq "http://www.an-url.io?lastName=nonnon&authorizationToken=#{token}"
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
RSpec.describe KeycloakApiRails::Service do
|
|
2
|
-
|
|
3
|
-
let(:public_key_cache_ttl) { 86400 }
|
|
4
|
-
let(:server_url) { "whatever:8080" }
|
|
5
|
-
let(:realm_id) { "pouet" }
|
|
6
|
-
let!(:resolver) { KeycloakApiRails::PublicKeyCachedResolver.new(server_url, realm_id, public_key_cache_ttl) }
|
|
7
|
-
|
|
8
|
-
before(:each) do
|
|
9
|
-
resolver.instance_variable_set(:@resolver, KeycloakApiRails::PublicKeyResolverStub.new)
|
|
10
|
-
now = Time.local(2018, 1, 9, 12, 0, 0)
|
|
11
|
-
Timecop.freeze(now)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
after(:each) do
|
|
15
|
-
Timecop.return
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
describe "#find_public_key" do
|
|
19
|
-
context "when there is no public key in cache yet" do
|
|
20
|
-
before(:each) do
|
|
21
|
-
@public_key = resolver.find_public_keys
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
it "returns a valid public key" do
|
|
25
|
-
expect(@public_key).to_not be_nil
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
it "sets the current time to the resolver" do
|
|
29
|
-
expect(resolver.cached_public_key_retrieved_at).to eq Time.now
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
context "when there is already a public key in cache" do
|
|
34
|
-
before(:each) do
|
|
35
|
-
@first_public_key = resolver.find_public_keys
|
|
36
|
-
@first_cached_public_key_retrieved_at = resolver.cached_public_key_retrieved_at
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
context "and no need to refresh it" do
|
|
40
|
-
before(:each) do
|
|
41
|
-
Timecop.freeze(Time.now + public_key_cache_ttl.seconds - 10.seconds)
|
|
42
|
-
@second_public_key = resolver.find_public_keys
|
|
43
|
-
@second_cached_public_key_retrieved_at = resolver.cached_public_key_retrieved_at
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
it "returns a valid public key" do
|
|
47
|
-
expect(@second_public_key).to_not be_nil
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
it "does not refresh the public key" do
|
|
51
|
-
expect(@second_public_key).to eq @first_public_key
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
it "does not refresh the public key retrieval time" do
|
|
55
|
-
expect(@first_cached_public_key_retrieved_at).to eq @second_cached_public_key_retrieved_at
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
context "and its TTL has expired" do
|
|
60
|
-
before(:each) do
|
|
61
|
-
Timecop.freeze(Time.now + public_key_cache_ttl.seconds + 10.seconds)
|
|
62
|
-
@second_public_key = resolver.find_public_keys
|
|
63
|
-
@second_cached_public_key_retrieved_at = resolver.cached_public_key_retrieved_at
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
it "returns a valid public key" do
|
|
67
|
-
expect(@second_public_key).to_not be_nil
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
it "refreshes the public key" do
|
|
71
|
-
expect(@second_public_key).to_not eq @first_public_key
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
it "refreshes the public key retrieval time" do
|
|
75
|
-
expect(@first_cached_public_key_retrieved_at).to_not eq @second_cached_public_key_retrieved_at
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
RSpec.describe KeycloakApiRails::Service do
|
|
2
|
-
|
|
3
|
-
let!(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
|
|
4
|
-
let!(:public_key) { private_key.public_key }
|
|
5
|
-
let!(:key_resolver) { KeycloakApiRails::PublicKeyCachedResolverStub.new(public_key) }
|
|
6
|
-
let!(:service) { KeycloakApiRails::Service.new(key_resolver) }
|
|
7
|
-
|
|
8
|
-
before(:each) do
|
|
9
|
-
now = Time.local(2018, 1, 9, 12, 0, 0)
|
|
10
|
-
Timecop.freeze(now)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
after(:each) do
|
|
14
|
-
Timecop.return
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
describe "#decode_and_verify" do
|
|
18
|
-
def create_token(private_key, expiration_date, algorithm)
|
|
19
|
-
claim = {
|
|
20
|
-
iss: "KeycloakApiRails",
|
|
21
|
-
exp: expiration_date,
|
|
22
|
-
nbf: Time.local(2018, 1, 1, 0, 0, 0)
|
|
23
|
-
}
|
|
24
|
-
jws = JSON::JWT.new(claim).sign(private_key, algorithm)
|
|
25
|
-
jws.to_s
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
context "when token is nil" do
|
|
29
|
-
let(:token) { nil }
|
|
30
|
-
it "should raise an error :no_token" do
|
|
31
|
-
expect {
|
|
32
|
-
service.decode_and_verify(token)
|
|
33
|
-
}.to raise_error(TokenError, "No JWT token provided")
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
context "when token is an empty string" do
|
|
38
|
-
let(:token) { "" }
|
|
39
|
-
it "should raise an error :no_token" do
|
|
40
|
-
expect {
|
|
41
|
-
service.decode_and_verify(token)
|
|
42
|
-
}.to raise_error(TokenError, "No JWT token provided")
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
context "when token is in an invalid format" do
|
|
47
|
-
let(:token) { "coucou" }
|
|
48
|
-
it "should raise an error :invalid_format" do
|
|
49
|
-
expect {
|
|
50
|
-
service.decode_and_verify(token)
|
|
51
|
-
}.to raise_error(TokenError, "Wrong JWT Format")
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
context "when token is in a valid format" do
|
|
56
|
-
let(:algorithm) { :RS256 }
|
|
57
|
-
let(:expiration_date) { 1.week.from_now }
|
|
58
|
-
|
|
59
|
-
context "and token is generated by another private key" do
|
|
60
|
-
let(:another_private_key) { OpenSSL::PKey::RSA.generate(1024) }
|
|
61
|
-
let(:token) { create_token(another_private_key, expiration_date, algorithm) }
|
|
62
|
-
|
|
63
|
-
it "should raise an error :verification_failed" do
|
|
64
|
-
expect {
|
|
65
|
-
service.decode_and_verify(token)
|
|
66
|
-
}.to raise_error(TokenError, "Failed to verify JWT token")
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
context "and token is generated by the right private key" do
|
|
71
|
-
let(:token) { create_token(private_key, expiration_date, algorithm) }
|
|
72
|
-
|
|
73
|
-
context "and token is expired" do
|
|
74
|
-
let(:expiration_date) { Time.now - 2.days }
|
|
75
|
-
|
|
76
|
-
it "should raise an error :expiration_date" do
|
|
77
|
-
expect {
|
|
78
|
-
service.decode_and_verify(token)
|
|
79
|
-
}.to raise_error(TokenError, "JWT token is expired")
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# The service reads `token_expiration_tolerance_in_seconds` when it is instantiated:
|
|
84
|
-
# a token expiring within that tolerance is already considered as expired.
|
|
85
|
-
context "and token expires within the expiration tolerance" do
|
|
86
|
-
let(:expiration_date) { Time.now + 5 }
|
|
87
|
-
|
|
88
|
-
it "should raise an error :expiration_date" do
|
|
89
|
-
expect(KeycloakApiRails.config.token_expiration_tolerance_in_seconds).to eq 10
|
|
90
|
-
expect {
|
|
91
|
-
service.decode_and_verify(token)
|
|
92
|
-
}.to raise_error(TokenError, "JWT token is expired")
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
context "and token expires just after the expiration tolerance" do
|
|
97
|
-
let(:expiration_date) { Time.now + 30 }
|
|
98
|
-
|
|
99
|
-
it "should return a not-nil decoded token" do
|
|
100
|
-
expect(KeycloakApiRails.config.token_expiration_tolerance_in_seconds).to eq 10
|
|
101
|
-
expect(service.decode_and_verify(token)).to_not be_nil
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
context "and token is not expired" do
|
|
106
|
-
let(:expiration_date) { Time.now + 2.days }
|
|
107
|
-
|
|
108
|
-
context "and token is encrypted using RS256" do
|
|
109
|
-
let(:algorithm) { :RS256 }
|
|
110
|
-
|
|
111
|
-
it "should return a not-nil decoded token" do
|
|
112
|
-
expect(service.decode_and_verify(token)).to_not be_nil
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
context "and token is encrypted using RS512" do
|
|
117
|
-
let(:algorithm) { :RS512 }
|
|
118
|
-
|
|
119
|
-
it "should return a not-nil decoded token" do
|
|
120
|
-
expect(service.decode_and_verify(token)).to_not be_nil
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
describe "#need_middleware_authentication?" do
|
|
129
|
-
|
|
130
|
-
let(:method) { nil }
|
|
131
|
-
let(:path) { nil }
|
|
132
|
-
let(:headers) { {} }
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
before(:each) do
|
|
136
|
-
KeycloakApiRails.config.skip_paths = {
|
|
137
|
-
post: [/^\/skip/],
|
|
138
|
-
get: [/^\/skip/]
|
|
139
|
-
}
|
|
140
|
-
@result = service.need_middleware_authentication?(method, path, headers)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
context "when method is nil" do
|
|
144
|
-
let(:method) { nil }
|
|
145
|
-
let(:path) { "/do-not-skip" }
|
|
146
|
-
it "should return true" do
|
|
147
|
-
expect(@result).to be true
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
context "when path is nil" do
|
|
152
|
-
let(:method) { :get }
|
|
153
|
-
let(:path) { nil }
|
|
154
|
-
it "should return true" do
|
|
155
|
-
expect(@result).to be true
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
context "when method does not match the configuration" do
|
|
160
|
-
let(:method) { :put }
|
|
161
|
-
let(:path) { "/skip" }
|
|
162
|
-
it "should return true" do
|
|
163
|
-
expect(@result).to be true
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
context "when path does not match the configuration" do
|
|
168
|
-
let(:method) { :get }
|
|
169
|
-
let(:path) { "/do-not-skip" }
|
|
170
|
-
it "should return true" do
|
|
171
|
-
expect(@result).to be true
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
context "when method [get] and path do match the configuration" do
|
|
176
|
-
let(:method) { :get }
|
|
177
|
-
let(:path) { "/skip" }
|
|
178
|
-
it "should return false" do
|
|
179
|
-
expect(@result).to be false
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
context "when method [post] and path do match the configuration" do
|
|
185
|
-
let(:method) { :get }
|
|
186
|
-
let(:path) { "/skip" }
|
|
187
|
-
it "should return false" do
|
|
188
|
-
expect(@result).to be false
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
context "when the request is preflight" do
|
|
193
|
-
let(:method) { :options }
|
|
194
|
-
let(:headers) { { "HTTP_ACCESS_CONTROL_REQUEST_METHOD" => ["Authorization"] } }
|
|
195
|
-
let(:path) { "/do-not-skip" }
|
|
196
|
-
it "should return false" do
|
|
197
|
-
expect(@result).to be false
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
context "when configured as opt_in" do
|
|
202
|
-
before do
|
|
203
|
-
KeycloakApiRails.config.opt_in = true
|
|
204
|
-
service2 = KeycloakApiRails::Service.new(key_resolver)
|
|
205
|
-
@result = service2.need_middleware_authentication?(method, path, headers)
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# The configuration is global: leaving 'opt_in' enabled would disable the authentication
|
|
209
|
-
# of every example running afterwards.
|
|
210
|
-
after do
|
|
211
|
-
KeycloakApiRails.config.opt_in = false
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
it "should return false" do
|
|
215
|
-
expect(@result).to be false
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
describe "#read_token" do
|
|
221
|
-
let(:query_string) { "" }
|
|
222
|
-
let(:url) { "http://api.service.com/api/health?aParameter=true#{query_string}" }
|
|
223
|
-
let(:headers) { {} }
|
|
224
|
-
let(:header_token) { "header_token" }
|
|
225
|
-
let(:query_string_token) { "query_string_token" }
|
|
226
|
-
|
|
227
|
-
before(:each) do
|
|
228
|
-
@token = service.read_token(url, headers)
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
context "when the token is provided in the Authorization headers" do
|
|
232
|
-
let(:headers) do
|
|
233
|
-
{
|
|
234
|
-
"HTTP_AUTHORIZATION" => "Bearer #{header_token}"
|
|
235
|
-
}
|
|
236
|
-
end
|
|
237
|
-
context "and not in the query string" do
|
|
238
|
-
let(:query_string) { "" }
|
|
239
|
-
it "returns the header token" do
|
|
240
|
-
expect(@token).to eq header_token
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
context "and also in the query string" do
|
|
245
|
-
let(:query_string) { "&authorizationToken=#{query_string_token}" }
|
|
246
|
-
it "returns the query string token" do
|
|
247
|
-
expect(@token).to eq query_string_token
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
context "when the token is not provided in the Authorization headers" do
|
|
253
|
-
let(:headers) do
|
|
254
|
-
{
|
|
255
|
-
"ANOTHER_HEADER" => header_token
|
|
256
|
-
}
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
context "and not in the query string" do
|
|
260
|
-
let(:query_string) { "" }
|
|
261
|
-
it "returns an empty token" do
|
|
262
|
-
expect(@token).to eq ""
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
context "and query string is nil" do
|
|
267
|
-
let(:query_string) { nil }
|
|
268
|
-
it "returns an empty token" do
|
|
269
|
-
expect(@token).to eq ""
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
context "but in the query string" do
|
|
274
|
-
let(:query_string) { "&authorizationToken=#{query_string_token}" }
|
|
275
|
-
it "returns the query string token" do
|
|
276
|
-
expect(@token).to eq query_string_token
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
end
|
|
@@ -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
|