keycloak-api-rails 0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/Dockerfile +11 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +145 -0
- data/MIT-LICENSE +20 -0
- data/README.md +165 -0
- data/keycloak-api-rails.gemspec +23 -0
- data/lib/keycloak-api-rails.rb +49 -0
- data/lib/keycloak-api-rails/configuration.rb +11 -0
- data/lib/keycloak-api-rails/helper.rb +43 -0
- data/lib/keycloak-api-rails/middleware.rb +45 -0
- data/lib/keycloak-api-rails/public_key_cached_resolver.rb +30 -0
- data/lib/keycloak-api-rails/public_key_resolver.rb +21 -0
- data/lib/keycloak-api-rails/railtie.rb +9 -0
- data/lib/keycloak-api-rails/service.rb +59 -0
- data/lib/keycloak-api-rails/token_error.rb +30 -0
- data/lib/keycloak-api-rails/version.rb +3 -0
- data/spec/keycloak-api-rails/helper_spec.rb +33 -0
- data/spec/keycloak-api-rails/public_key_cached_resolver_spec.rb +80 -0
- data/spec/keycloak-api-rails/service_spec.rb +234 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/public_key_cached_resolver_stub.rb +11 -0
- data/spec/support/public_key_resolver_stub.rb +7 -0
- data/spec/support/rails_helper.rb +4 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1fb2c6f355e3fc27fbb0110609f34fc4d17e34f0
|
4
|
+
data.tar.gz: 3f338ecbe4d68ecccf65904434399394b7f51a80
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 869df876da34f6cbb957a7c12f88975efc5147ddb4282813b30997aa5e9b229045f5e3fee056429a6990193909b3e864893d69787f67e09ade924816f5696b96
|
7
|
+
data.tar.gz: 1a1e5f069ad76e2eb83653bddff7da10b019097f06277f91793b6839e2ccf3fe91e1df3ff25a5a12caf32fdc4668570e853a964a64fee0d85c4c1eb8aabd009e
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Dockerfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
FROM ruby:2.3
|
2
|
+
RUN mkdir -p /usr/src/app/lib/keycloak-api-rails
|
3
|
+
WORKDIR /usr/src/app
|
4
|
+
|
5
|
+
COPY Gemfile /usr/src/app/
|
6
|
+
COPY Gemfile.lock /usr/src/app/
|
7
|
+
COPY keycloak-api-rails.gemspec /usr/src/app/
|
8
|
+
COPY lib/keycloak-api-rails/version.rb /usr/src/app/lib/keycloak-api-rails/
|
9
|
+
RUN bundle install
|
10
|
+
COPY . /usr/src/app
|
11
|
+
RUN bundle install
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
keycloak-api-rails (0.6)
|
5
|
+
json-jwt (~> 1.8, >= 1.8.3)
|
6
|
+
rails (>= 4.2)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
actioncable (5.1.4)
|
12
|
+
actionpack (= 5.1.4)
|
13
|
+
nio4r (~> 2.0)
|
14
|
+
websocket-driver (~> 0.6.1)
|
15
|
+
actionmailer (5.1.4)
|
16
|
+
actionpack (= 5.1.4)
|
17
|
+
actionview (= 5.1.4)
|
18
|
+
activejob (= 5.1.4)
|
19
|
+
mail (~> 2.5, >= 2.5.4)
|
20
|
+
rails-dom-testing (~> 2.0)
|
21
|
+
actionpack (5.1.4)
|
22
|
+
actionview (= 5.1.4)
|
23
|
+
activesupport (= 5.1.4)
|
24
|
+
rack (~> 2.0)
|
25
|
+
rack-test (>= 0.6.3)
|
26
|
+
rails-dom-testing (~> 2.0)
|
27
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
28
|
+
actionview (5.1.4)
|
29
|
+
activesupport (= 5.1.4)
|
30
|
+
builder (~> 3.1)
|
31
|
+
erubi (~> 1.4)
|
32
|
+
rails-dom-testing (~> 2.0)
|
33
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
34
|
+
activejob (5.1.4)
|
35
|
+
activesupport (= 5.1.4)
|
36
|
+
globalid (>= 0.3.6)
|
37
|
+
activemodel (5.1.4)
|
38
|
+
activesupport (= 5.1.4)
|
39
|
+
activerecord (5.1.4)
|
40
|
+
activemodel (= 5.1.4)
|
41
|
+
activesupport (= 5.1.4)
|
42
|
+
arel (~> 8.0)
|
43
|
+
activesupport (5.1.4)
|
44
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
45
|
+
i18n (~> 0.7)
|
46
|
+
minitest (~> 5.1)
|
47
|
+
tzinfo (~> 1.1)
|
48
|
+
arel (8.0.0)
|
49
|
+
bindata (2.4.1)
|
50
|
+
builder (3.2.3)
|
51
|
+
byebug (9.1.0)
|
52
|
+
concurrent-ruby (1.0.5)
|
53
|
+
crass (1.0.3)
|
54
|
+
diff-lcs (1.3)
|
55
|
+
erubi (1.7.0)
|
56
|
+
globalid (0.4.1)
|
57
|
+
activesupport (>= 4.2.0)
|
58
|
+
i18n (0.9.1)
|
59
|
+
concurrent-ruby (~> 1.0)
|
60
|
+
json-jwt (1.8.3)
|
61
|
+
activesupport
|
62
|
+
bindata
|
63
|
+
securecompare
|
64
|
+
url_safe_base64
|
65
|
+
loofah (2.1.1)
|
66
|
+
crass (~> 1.0.2)
|
67
|
+
nokogiri (>= 1.5.9)
|
68
|
+
mail (2.7.0)
|
69
|
+
mini_mime (>= 0.1.1)
|
70
|
+
method_source (0.9.0)
|
71
|
+
mini_mime (1.0.0)
|
72
|
+
mini_portile2 (2.3.0)
|
73
|
+
minitest (5.11.1)
|
74
|
+
nio4r (2.2.0)
|
75
|
+
nokogiri (1.8.1)
|
76
|
+
mini_portile2 (~> 2.3.0)
|
77
|
+
rack (2.0.3)
|
78
|
+
rack-test (0.8.2)
|
79
|
+
rack (>= 1.0, < 3)
|
80
|
+
rails (5.1.4)
|
81
|
+
actioncable (= 5.1.4)
|
82
|
+
actionmailer (= 5.1.4)
|
83
|
+
actionpack (= 5.1.4)
|
84
|
+
actionview (= 5.1.4)
|
85
|
+
activejob (= 5.1.4)
|
86
|
+
activemodel (= 5.1.4)
|
87
|
+
activerecord (= 5.1.4)
|
88
|
+
activesupport (= 5.1.4)
|
89
|
+
bundler (>= 1.3.0)
|
90
|
+
railties (= 5.1.4)
|
91
|
+
sprockets-rails (>= 2.0.0)
|
92
|
+
rails-dom-testing (2.0.3)
|
93
|
+
activesupport (>= 4.2.0)
|
94
|
+
nokogiri (>= 1.6)
|
95
|
+
rails-html-sanitizer (1.0.3)
|
96
|
+
loofah (~> 2.0)
|
97
|
+
railties (5.1.4)
|
98
|
+
actionpack (= 5.1.4)
|
99
|
+
activesupport (= 5.1.4)
|
100
|
+
method_source
|
101
|
+
rake (>= 0.8.7)
|
102
|
+
thor (>= 0.18.1, < 2.0)
|
103
|
+
rake (12.3.0)
|
104
|
+
rspec (3.7.0)
|
105
|
+
rspec-core (~> 3.7.0)
|
106
|
+
rspec-expectations (~> 3.7.0)
|
107
|
+
rspec-mocks (~> 3.7.0)
|
108
|
+
rspec-core (3.7.1)
|
109
|
+
rspec-support (~> 3.7.0)
|
110
|
+
rspec-expectations (3.7.0)
|
111
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
112
|
+
rspec-support (~> 3.7.0)
|
113
|
+
rspec-mocks (3.7.0)
|
114
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
115
|
+
rspec-support (~> 3.7.0)
|
116
|
+
rspec-support (3.7.0)
|
117
|
+
securecompare (1.0.0)
|
118
|
+
sprockets (3.7.1)
|
119
|
+
concurrent-ruby (~> 1.0)
|
120
|
+
rack (> 1, < 3)
|
121
|
+
sprockets-rails (3.2.1)
|
122
|
+
actionpack (>= 4.0)
|
123
|
+
activesupport (>= 4.0)
|
124
|
+
sprockets (>= 3.0.0)
|
125
|
+
thor (0.20.0)
|
126
|
+
thread_safe (0.3.6)
|
127
|
+
timecop (0.9.1)
|
128
|
+
tzinfo (1.2.4)
|
129
|
+
thread_safe (~> 0.1)
|
130
|
+
url_safe_base64 (0.2.2)
|
131
|
+
websocket-driver (0.6.5)
|
132
|
+
websocket-extensions (>= 0.1.0)
|
133
|
+
websocket-extensions (0.1.3)
|
134
|
+
|
135
|
+
PLATFORMS
|
136
|
+
ruby
|
137
|
+
|
138
|
+
DEPENDENCIES
|
139
|
+
byebug (= 9.1.0)
|
140
|
+
keycloak-api-rails!
|
141
|
+
rspec (= 3.7.0)
|
142
|
+
timecop (= 0.9.1)
|
143
|
+
|
144
|
+
BUNDLED WITH
|
145
|
+
1.16.1
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2018
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# Keycloak-Rails-Api
|
2
|
+
|
3
|
+
This gem aims at validates Keycloak JWT token in Ruby On Rails APIs.
|
4
|
+
|
5
|
+
## Token validation
|
6
|
+
|
7
|
+
Tokens send (through query strings or Authorization headers) to this Railtie Middleware are validated against a Keycloak public key. This public key is downloaded every day by default (this interval can be changed through `public_key_cache_ttl`).
|
8
|
+
|
9
|
+
## Pass token to the API
|
10
|
+
|
11
|
+
* Method 1: By adding an `Authorization` HTTP Header with its value set to `Bearer <your token>`.
|
12
|
+
_e.g_ using curl: `curl -H "Authorization: Bearer <your-token>" https://api.pouet.io/api/more-pouets`
|
13
|
+
* Method 2: By providing the token via query string, especially via the parameter named `authorizationToken`. Keep in mind that this method is less secure (url are kept intact in your browser history, and so on...)
|
14
|
+
_e.g._ using curl: `curl https://api.pouet.io/api/more-pouets?authorizationToken<your-token>`
|
15
|
+
|
16
|
+
_If both method are used at the same time, The query string as a higher priority when reading given tokens._
|
17
|
+
|
18
|
+
## When a token is validated
|
19
|
+
|
20
|
+
In Rails controller, the request `env` variables has two more properties:
|
21
|
+
* `keycloak:keycloak_id`
|
22
|
+
* `keycloak:roles`
|
23
|
+
|
24
|
+
They can be accessed using `Keycloak::Helper` methods.
|
25
|
+
|
26
|
+
## Overall configuration options
|
27
|
+
|
28
|
+
All options have a default value. However, all of them can be changed in your initializer file.
|
29
|
+
|
30
|
+
| Option | Default Value | Type | Required? | Description | Example |
|
31
|
+
| ---- | ----- | ------ | ----- | ------ | ----- |
|
32
|
+
| `server_url` | `nil`| String | Required | The base url where your Keycloak server is located. This value can be retrieved in your Keycloak client configuration. | `auth:8080` |
|
33
|
+
| `realm_id` | `nil`| String | Required | Realm's name (not id, actually) | `master` |
|
34
|
+
| `logger` | `Logger.new(STDOUT)`| Logger | Optional | The logger used by `keycloak-api-rails` | `Rails.logger` |
|
35
|
+
| `skip_paths` | `{}`| Hash of methods and paths regexp | Optional | Paths whose the token must not be validatefd | `{ get: [/^\/health\/.+/] }`|
|
36
|
+
| `token_expiration_tolerance_in_seconds` | `10`| Logger | Optional | Number of seconds a token can expire before being rejected by the API. | `15` |
|
37
|
+
| `public_key_cache_ttl` | `86400`| Integer | Optional | Amount of time, in seconds, specifying maximum interval between two requests to {project_name} to retrieve new public keys. It is 86400 seconds (1 day) by default. At least once per this configured interval (1 day by default) will be new public key always downloaded. | `Rails.logger` |
|
38
|
+
|
39
|
+
## Configure it
|
40
|
+
|
41
|
+
Create a `keycloak.rb` file in your Rails `config/initializers` folder. For instance:
|
42
|
+
|
43
|
+
```
|
44
|
+
Keycloak.configure do |config|
|
45
|
+
config.server_url = ENV["KEYCLOAK_SERVER_URL"]
|
46
|
+
config.realm_id = ENV["KEYCLOAK_REALM_ID"]
|
47
|
+
config.logger = Rails.logger
|
48
|
+
config.skip_paths = {
|
49
|
+
post: [/^\/message/],
|
50
|
+
get: [/^\/locales/, /^\/health\/.+/]
|
51
|
+
}
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
## Use cases
|
56
|
+
|
57
|
+
Once this gem is configured in your Rails project, you can read, validate and use tokens in your controllers.
|
58
|
+
|
59
|
+
### Keycloak Id
|
60
|
+
|
61
|
+
If you identify users using their Keycloak Id, this value can be read from your controllers using `Keycloak::Helper.current_user_id(request.env)`.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class AuthenticatedController < ApplicationController
|
65
|
+
|
66
|
+
def user
|
67
|
+
keycloak_id = Keycloak::Helper.current_user_id(request.env)
|
68
|
+
User.active.find_by(keycloak_id: keycloak_id)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
### Roles
|
74
|
+
|
75
|
+
`Keycloak::Helper.current_user_roles` can be use against a Rails request to read user's roles.
|
76
|
+
|
77
|
+
For example, a controller can require users to be administrator (considering you defined an `application-admin` role):
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
class AdminController < ApplicationController
|
81
|
+
|
82
|
+
before_action :require_to_be_admin!
|
83
|
+
|
84
|
+
def require_to_be_admin!
|
85
|
+
if !current_user_roles.include?("application-admin")
|
86
|
+
render(json: { reason: "admin", message: "You have to be an administrator to access that endpoint." }, status: :forbidden)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def current_user_roles
|
93
|
+
Keycloak::Helper.current_user_roles(request.env)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
### Create an URL where the token must be passed via query string
|
99
|
+
|
100
|
+
`Keycloak::Helper.create_url_with_token` method can be used to build an url from another, by adding a token as query string.
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
def example
|
104
|
+
Keycloak::Helper.create_url_with_token("https://api.pouet.io/api/more-pouets", "myToken")
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
This should output `https://api.pouet.io/api/more-pouets?authorizationToken=myToken`.
|
109
|
+
|
110
|
+
|
111
|
+
### Accessing Keycloak Service
|
112
|
+
|
113
|
+
A lazy-loaded service Keycloak::Service can be accessed using `Keycloak.service`.
|
114
|
+
For instance, to read a provided token:
|
115
|
+
```ruby
|
116
|
+
class RenderTokenController < ApplicationController
|
117
|
+
def show
|
118
|
+
uri = request.env["REQUEST_URI"]
|
119
|
+
headers = request.env
|
120
|
+
token = Keycloak.service.read_token(uri, headers)
|
121
|
+
render json: { token: token }, status: :ok
|
122
|
+
end
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
## Writing integration tests
|
127
|
+
|
128
|
+
If you want to write controller tests in your codebase and that Keycloak is configured for these controllers, here is how to mock it.
|
129
|
+
These lines are based on tests written using `rspec`.
|
130
|
+
|
131
|
+
* First, create a private key. This key should be created once per test suite for performance matters.
|
132
|
+
```ruby
|
133
|
+
config.before(:suite) do
|
134
|
+
$private_key = OpenSSL::PKey::RSA.generate(1024)
|
135
|
+
end
|
136
|
+
```
|
137
|
+
* Then, in a `shared_context`, configure a lazy token based on your main user. (here, we assume you have a `user` variable with a `keycloak_id` property)
|
138
|
+
```ruby
|
139
|
+
let(:jwt) do
|
140
|
+
claims = {
|
141
|
+
iat: Time.zone.now.to_i,
|
142
|
+
exp: (Time.zone.now + 1.day).to_i,
|
143
|
+
sub: user.keycloak_id,
|
144
|
+
}
|
145
|
+
token = JSON::JWT.new(claims)
|
146
|
+
token.kid = "default"
|
147
|
+
token.sign($private_key, :RS256).to_s
|
148
|
+
end
|
149
|
+
```
|
150
|
+
* Finally, in the same `shared_context`, stub `Keycloak.public_key_resolver` to use a valid public key that is able to validate `jwt`:
|
151
|
+
```ruby
|
152
|
+
before(:each) do
|
153
|
+
public_key_resolver = Keycloak.public_key_resolver
|
154
|
+
allow(public_key_resolver).to receive(:find_public_keys) { JSON::JWK::Set.new(JSON::JWK.new($private_key, kid: "default")) }
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
## How to execute library tests
|
159
|
+
|
160
|
+
From the `keycloak-rails-api` directory:
|
161
|
+
|
162
|
+
```
|
163
|
+
$ docker build . -t keycloak-rails-api:test
|
164
|
+
$ docker run -v `pwd`:/usr/src/app/ keycloak-rails-api:test bundle exec rspec spec
|
165
|
+
```
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
require "keycloak-api-rails/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "keycloak-api-rails"
|
7
|
+
spec.version = Keycloak::VERSION
|
8
|
+
spec.authors = ["Lorent Lempereur"]
|
9
|
+
spec.email = ["lorent.lempereur.dev@gmail.com"]
|
10
|
+
spec.homepage = "https://github.com/looorent/keycloak-api-rails"
|
11
|
+
spec.summary = "Rails middleware that validates Authorization token emitted by Keycloak"
|
12
|
+
spec.description = "Rails middleware that validates Authorization token emitted by Keycloak"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.require_paths = ["lib"]
|
17
|
+
|
18
|
+
spec.add_dependency "rails", ">= 4.2"
|
19
|
+
spec.add_dependency "json-jwt", "~> 1.8", ">= 1.8.3"
|
20
|
+
spec.add_development_dependency "rspec", "3.7.0"
|
21
|
+
spec.add_development_dependency "timecop", "0.9.1"
|
22
|
+
spec.add_development_dependency "byebug", "9.1.0"
|
23
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "json/jwt"
|
3
|
+
require "uri"
|
4
|
+
require "date"
|
5
|
+
|
6
|
+
require_relative "keycloak-api-rails/configuration"
|
7
|
+
require_relative "keycloak-api-rails/token_error"
|
8
|
+
require_relative "keycloak-api-rails/helper"
|
9
|
+
require_relative "keycloak-api-rails/public_key_resolver"
|
10
|
+
require_relative "keycloak-api-rails/public_key_cached_resolver"
|
11
|
+
require_relative "keycloak-api-rails/service"
|
12
|
+
require_relative "keycloak-api-rails/middleware"
|
13
|
+
require_relative "keycloak-api-rails/railtie" if defined?(Rails)
|
14
|
+
|
15
|
+
module Keycloak
|
16
|
+
|
17
|
+
def self.configure
|
18
|
+
yield @configuration ||= Keycloak::Configuration.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.config
|
22
|
+
@configuration
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.public_key_resolver
|
26
|
+
@public_key_resolver ||= PublicKeyCachedResolver.from_configuration(config)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.service
|
30
|
+
@service ||= Keycloak::Service.new(public_key_resolver)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.logger
|
34
|
+
config.logger
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.load_configuration
|
38
|
+
configure do |config|
|
39
|
+
config.server_url = nil
|
40
|
+
config.realm_id = nil
|
41
|
+
config.logger = ::Logger.new(STDOUT)
|
42
|
+
config.skip_paths = {}
|
43
|
+
config.token_expiration_tolerance_in_seconds = 10
|
44
|
+
config.public_key_cache_ttl = 86400
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
load_configuration
|
49
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Keycloak
|
2
|
+
class Configuration
|
3
|
+
include ActiveSupport::Configurable
|
4
|
+
config_accessor :server_url
|
5
|
+
config_accessor :realm_id
|
6
|
+
config_accessor :skip_paths
|
7
|
+
config_accessor :token_expiration_tolerance_in_seconds
|
8
|
+
config_accessor :public_key_cache_ttl
|
9
|
+
config_accessor :logger
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Keycloak
|
2
|
+
class Helper
|
3
|
+
|
4
|
+
CURRENT_USER_ID_KEY = "keycloak:keycloak_id"
|
5
|
+
ROLES_KEY = "keycloak:roles"
|
6
|
+
QUERY_STRING_TOKEN_KEY = "authorizationToken"
|
7
|
+
|
8
|
+
def self.current_user_id(env)
|
9
|
+
env[CURRENT_USER_ID_KEY]
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.assign_current_user_id(env, token)
|
13
|
+
env[CURRENT_USER_ID_KEY] = token["sub"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.current_user_roles(env)
|
17
|
+
env[ROLES_KEY]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.assign_realm_roles(env, token)
|
21
|
+
env[ROLES_KEY] = token.dig("realm_access", "roles")
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.read_token_from_query_string(uri)
|
25
|
+
parsed_uri = URI.parse(uri)
|
26
|
+
query = URI.decode_www_form(parsed_uri.query || "")
|
27
|
+
query_string_token = query.detect { |param| param.first == QUERY_STRING_TOKEN_KEY }
|
28
|
+
query_string_token&.second
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.create_url_with_token(uri, token)
|
32
|
+
uri = URI(uri)
|
33
|
+
params = URI.decode_www_form(uri.query || "").reject { |query_string| query_string.first == QUERY_STRING_TOKEN_KEY }
|
34
|
+
params << [QUERY_STRING_TOKEN_KEY, token]
|
35
|
+
uri.query = URI.encode_www_form(params)
|
36
|
+
uri.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.read_token_from_headers(headers)
|
40
|
+
headers["HTTP_AUTHORIZATION"]&.gsub(/^Bearer /, "") || ""
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Keycloak
|
2
|
+
|
3
|
+
class Middleware
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
method = env["REQUEST_METHOD"]
|
10
|
+
path = env["PATH_INFO"]
|
11
|
+
uri = env["REQUEST_URI"]
|
12
|
+
|
13
|
+
if service.need_authentication?(method, path, env)
|
14
|
+
logger.debug("Start authentication for #{method} : #{path}")
|
15
|
+
token = service.read_token(uri, env)
|
16
|
+
decoded_token = service.decode_and_verify(token)
|
17
|
+
authentication_succeeded(env, decoded_token)
|
18
|
+
else
|
19
|
+
logger.debug("Skip authentication for #{method} : #{path}")
|
20
|
+
@app.call(env)
|
21
|
+
end
|
22
|
+
rescue TokenError => e
|
23
|
+
authentication_failed(e.message)
|
24
|
+
end
|
25
|
+
|
26
|
+
def authentication_failed(message)
|
27
|
+
logger.warn(message)
|
28
|
+
[401, {"Content-Type" => "application/json"}, [ { error: message }.to_json]]
|
29
|
+
end
|
30
|
+
|
31
|
+
def authentication_succeeded(env, decoded_token)
|
32
|
+
Helper.assign_current_user_id(env, decoded_token)
|
33
|
+
Helper.assign_realm_roles(env, decoded_token)
|
34
|
+
@app.call(env)
|
35
|
+
end
|
36
|
+
|
37
|
+
def service
|
38
|
+
Keycloak.service
|
39
|
+
end
|
40
|
+
|
41
|
+
def logger
|
42
|
+
Keycloak.logger
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Keycloak
|
2
|
+
class PublicKeyCachedResolver
|
3
|
+
attr_reader :cached_public_key_retrieved_at
|
4
|
+
|
5
|
+
def initialize(server_url, realm_id, public_key_cache_ttl)
|
6
|
+
@resolver = PublicKeyResolver.new(server_url, realm_id)
|
7
|
+
@public_key_cache_ttl = public_key_cache_ttl
|
8
|
+
@cached_public_keys = nil
|
9
|
+
@cached_public_key_retrieved_at = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from_configuration(configuration)
|
13
|
+
PublicKeyCachedResolver.new(configuration.server_url, configuration.realm_id, configuration.public_key_cache_ttl)
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_public_keys
|
17
|
+
if public_keys_are_outdated?
|
18
|
+
@cached_public_keys = @resolver.find_public_keys
|
19
|
+
@cached_public_key_retrieved_at = Time.now
|
20
|
+
end
|
21
|
+
@cached_public_keys
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def public_keys_are_outdated?
|
27
|
+
@cached_public_keys.nil? || @cached_public_key_retrieved_at.nil? || Time.now > (@cached_public_key_retrieved_at + @public_key_cache_ttl.seconds)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Keycloak
|
2
|
+
class PublicKeyResolver
|
3
|
+
def initialize(server_url, realm_id)
|
4
|
+
@public_certificate_url = create_public_certificate_url(server_url, realm_id)
|
5
|
+
end
|
6
|
+
|
7
|
+
def find_public_keys
|
8
|
+
JSON::JWK::Set.new(JSON.parse(RestClient.get(@public_certificate_url).body)["keys"])
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def create_realm_url(server_url, realm_id)
|
14
|
+
"#{server_url}/realms/#{realm_id}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_public_certificate_url(server_url, realm_id)
|
18
|
+
"#{create_realm_url(server_url, realm_id)}/protocol/openid-connect/certs"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Keycloak
|
2
|
+
class Service
|
3
|
+
|
4
|
+
def initialize(key_resolver)
|
5
|
+
@key_resolver = key_resolver
|
6
|
+
@skip_paths = Keycloak.config.skip_paths
|
7
|
+
@logger = Keycloak.config.logger
|
8
|
+
@token_expiration_tolerance_in_seconds = Keycloak.config.token_expiration_tolerance_in_seconds
|
9
|
+
end
|
10
|
+
|
11
|
+
def decode_and_verify(token)
|
12
|
+
unless token.nil? || token&.empty?
|
13
|
+
public_key = @key_resolver.find_public_keys
|
14
|
+
decoded_token = JSON::JWT.decode(token, public_key)
|
15
|
+
|
16
|
+
unless expired?(decoded_token)
|
17
|
+
decoded_token.verify!(public_key)
|
18
|
+
decoded_token
|
19
|
+
else
|
20
|
+
raise TokenError.expired(token)
|
21
|
+
end
|
22
|
+
else
|
23
|
+
raise TokenError.no_token(token)
|
24
|
+
end
|
25
|
+
rescue JSON::JWT::VerificationFailed => e
|
26
|
+
raise TokenError.verification_failed(token, e)
|
27
|
+
rescue JSON::JWK::Set::KidNotFound => e
|
28
|
+
raise TokenError.verification_failed(token, e)
|
29
|
+
rescue JSON::JWT::InvalidFormat
|
30
|
+
raise TokenError.invalid_format(token, e)
|
31
|
+
end
|
32
|
+
|
33
|
+
def read_token(uri, headers)
|
34
|
+
Helper.read_token_from_query_string(uri) || Helper.read_token_from_headers(headers)
|
35
|
+
end
|
36
|
+
|
37
|
+
def need_authentication?(method, path, headers)
|
38
|
+
!should_skip?(method, path) && !is_preflight?(method, headers)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def should_skip?(method, path)
|
44
|
+
method_symbol = method&.downcase&.to_sym
|
45
|
+
skip_paths = @skip_paths[method_symbol]
|
46
|
+
!skip_paths.nil? && !skip_paths.empty? && !skip_paths.find_index { |skip_path| skip_path.match(path) }.nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_preflight?(method, headers)
|
50
|
+
method_symbol = method&.downcase&.to_sym
|
51
|
+
method_symbol == :options && !headers["HTTP_ACCESS_CONTROL_REQUEST_METHOD"].nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
def expired?(token)
|
55
|
+
token_expiration = Time.at(token["exp"]).to_datetime
|
56
|
+
token_expiration < Time.now + @token_expiration_tolerance_in_seconds.seconds
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class TokenError < StandardError
|
2
|
+
attr_reader :token, :reason, :original_error
|
3
|
+
|
4
|
+
def initialize(token, reason, message, original_error)
|
5
|
+
super(message)
|
6
|
+
@token = token
|
7
|
+
@reason = reason
|
8
|
+
@original_error = original_error
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.verification_failed(token, original_error)
|
12
|
+
TokenError.new(token, :verification_failed, "Failed to verify JWT token", original_error)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.invalid_format(token, original_error)
|
16
|
+
TokenError.new(token, :invalid_format, "Wrong JWT Format", original_error)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.no_token(token)
|
20
|
+
TokenError.new(token, :no_token, "No JWT token provided", nil)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.expired(token)
|
24
|
+
TokenError.new(token, :expired, "JWT token is expired", nil)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.unknown(token)
|
28
|
+
TokenError.new
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
RSpec.describe Keycloak::Helper do
|
2
|
+
describe "#create_url_with_token" do
|
3
|
+
|
4
|
+
let(:uri) { "http://www.an-url.io" }
|
5
|
+
let(:token) { "aToken" }
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
@url_with_token = Keycloak::Helper.create_url_with_token(uri, token)
|
9
|
+
end
|
10
|
+
|
11
|
+
context "when the uri has no query string yet" do
|
12
|
+
it "returns an url with the provided token" do
|
13
|
+
expect(@url_with_token).to eq "#{uri}?authorizationToken=#{token}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "when the uri already has no query strings" do
|
18
|
+
context "but no token yet" do
|
19
|
+
let(:uri) { "http://www.an-url.io?firstName=ouioui&lastName=nonnon" }
|
20
|
+
it "returns an url with all the query string and the token" do
|
21
|
+
expect(@url_with_token).to eq "#{uri}&authorizationToken=#{token}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "including a token" do
|
26
|
+
let(:uri) { "http://www.an-url.io?authorizationToken=ouioui&lastName=nonnon" }
|
27
|
+
it "returns an url with all the query string and the new token" do
|
28
|
+
expect(@url_with_token).to eq "http://www.an-url.io?lastName=nonnon&authorizationToken=#{token}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
RSpec.describe Keycloak::Service do
|
2
|
+
|
3
|
+
let(:public_key_cache_ttl) { 86400 }
|
4
|
+
let(:server_url) { "whatever:8080" }
|
5
|
+
let(:realm_id) { "pouet" }
|
6
|
+
let!(:resolver) { Keycloak::PublicKeyCachedResolver.new(server_url, realm_id, public_key_cache_ttl) }
|
7
|
+
|
8
|
+
before(:each) do
|
9
|
+
resolver.instance_variable_set(:@resolver, Keycloak::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
|
@@ -0,0 +1,234 @@
|
|
1
|
+
RSpec.describe Keycloak::Service do
|
2
|
+
|
3
|
+
let!(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
|
4
|
+
let!(:public_key) { private_key.public_key }
|
5
|
+
let!(:key_resolver) { Keycloak::PublicKeyCachedResolverStub.new(public_key) }
|
6
|
+
let!(:service) { Keycloak::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: "Keycloak",
|
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
|
+
context "and token is not expired" do
|
84
|
+
let(:expiration_date) { Time.now + 2.days }
|
85
|
+
|
86
|
+
context "and token is encrypted using RS256" do
|
87
|
+
let(:algorithm) { :RS256 }
|
88
|
+
|
89
|
+
it "should return a not-nil decoded token" do
|
90
|
+
expect(service.decode_and_verify(token)).to_not be_nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context "and token is encrypted using RS512" do
|
95
|
+
let(:algorithm) { :RS512 }
|
96
|
+
|
97
|
+
it "should return a not-nil decoded token" do
|
98
|
+
expect(service.decode_and_verify(token)).to_not be_nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "#need_authentication?" do
|
107
|
+
|
108
|
+
let(:method) { nil }
|
109
|
+
let(:path) { nil }
|
110
|
+
let(:headers) { {} }
|
111
|
+
|
112
|
+
|
113
|
+
before(:each) do
|
114
|
+
Keycloak.config.skip_paths = {
|
115
|
+
post: [/^\/skip/],
|
116
|
+
get: [/^\/skip/]
|
117
|
+
}
|
118
|
+
@result = service.need_authentication?(method, path, headers)
|
119
|
+
end
|
120
|
+
|
121
|
+
context "when method is nil" do
|
122
|
+
let(:method) { nil }
|
123
|
+
let(:path) { "/do-not-skip" }
|
124
|
+
it "should return true" do
|
125
|
+
expect(@result).to be true
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context "when path is nil" do
|
130
|
+
let(:method) { :get }
|
131
|
+
let(:path) { nil }
|
132
|
+
it "should return true" do
|
133
|
+
expect(@result).to be true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context "when method does not match the configuration" do
|
138
|
+
let(:method) { :put }
|
139
|
+
let(:path) { "/skip" }
|
140
|
+
it "should return true" do
|
141
|
+
expect(@result).to be true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
context "when path does not match the configuration" do
|
146
|
+
let(:method) { :get }
|
147
|
+
let(:path) { "/do-not-skip" }
|
148
|
+
it "should return true" do
|
149
|
+
expect(@result).to be true
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context "when method [get] and path do match the configuration" do
|
154
|
+
let(:method) { :get }
|
155
|
+
let(:path) { "/skip" }
|
156
|
+
it "should return false" do
|
157
|
+
expect(@result).to be false
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
context "when method [post] and path do match the configuration" do
|
163
|
+
let(:method) { :get }
|
164
|
+
let(:path) { "/skip" }
|
165
|
+
it "should return false" do
|
166
|
+
expect(@result).to be false
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "when the request is preflight" do
|
171
|
+
let(:method) { :options }
|
172
|
+
let(:headers) { { "HTTP_ACCESS_CONTROL_REQUEST_METHOD" => ["Authorization"] } }
|
173
|
+
let(:path) { "/do-not-skip" }
|
174
|
+
it "should return false" do
|
175
|
+
expect(@result).to be false
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
describe "#read_token" do
|
181
|
+
let(:query_string) { "" }
|
182
|
+
let(:url) { "http://api.service.com/api/health?aParameter=true#{query_string}" }
|
183
|
+
let(:headers) { {} }
|
184
|
+
let(:header_token) { "header_token" }
|
185
|
+
let(:query_string_token) { "query_string_token" }
|
186
|
+
|
187
|
+
before(:each) do
|
188
|
+
@token = service.read_token(url, headers)
|
189
|
+
end
|
190
|
+
|
191
|
+
context "when the token is provided in the Authorization headers" do
|
192
|
+
let(:headers) do
|
193
|
+
{
|
194
|
+
"HTTP_AUTHORIZATION" => "Bearer #{header_token}"
|
195
|
+
}
|
196
|
+
end
|
197
|
+
context "and not in the query string" do
|
198
|
+
let(:query_string) { "" }
|
199
|
+
it "returns the header token" do
|
200
|
+
expect(@token).to eq header_token
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
context "and also in the query string" do
|
205
|
+
let(:query_string) { "&authorizationToken=#{query_string_token}" }
|
206
|
+
it "returns the query string token" do
|
207
|
+
expect(@token).to eq query_string_token
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
context "when the token is not provided in the Authorization headers" do
|
213
|
+
let(:headers) do
|
214
|
+
{
|
215
|
+
"ANOTHER_HEADER" => header_token
|
216
|
+
}
|
217
|
+
end
|
218
|
+
|
219
|
+
context "and not in the query string" do
|
220
|
+
let(:query_string) { "" }
|
221
|
+
it "returns an empty token" do
|
222
|
+
expect(@token).to eq ""
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
context "but in the query string" do
|
227
|
+
let(:query_string) { "&authorizationToken=#{query_string_token}" }
|
228
|
+
it "returns the query string token" do
|
229
|
+
expect(@token).to eq query_string_token
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
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
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: keycloak-api-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.6'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lorent Lempereur
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-01-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: json-jwt
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.8'
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 1.8.3
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '1.8'
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.8.3
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - '='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 3.7.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - '='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 3.7.0
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: timecop
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - '='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.9.1
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - '='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 0.9.1
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: byebug
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - '='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 9.1.0
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - '='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 9.1.0
|
89
|
+
description: Rails middleware that validates Authorization token emitted by Keycloak
|
90
|
+
email:
|
91
|
+
- lorent.lempereur.dev@gmail.com
|
92
|
+
executables: []
|
93
|
+
extensions: []
|
94
|
+
extra_rdoc_files: []
|
95
|
+
files:
|
96
|
+
- ".gitignore"
|
97
|
+
- ".rspec"
|
98
|
+
- Dockerfile
|
99
|
+
- Gemfile
|
100
|
+
- Gemfile.lock
|
101
|
+
- MIT-LICENSE
|
102
|
+
- README.md
|
103
|
+
- keycloak-api-rails.gemspec
|
104
|
+
- lib/keycloak-api-rails.rb
|
105
|
+
- lib/keycloak-api-rails/configuration.rb
|
106
|
+
- lib/keycloak-api-rails/helper.rb
|
107
|
+
- lib/keycloak-api-rails/middleware.rb
|
108
|
+
- lib/keycloak-api-rails/public_key_cached_resolver.rb
|
109
|
+
- lib/keycloak-api-rails/public_key_resolver.rb
|
110
|
+
- lib/keycloak-api-rails/railtie.rb
|
111
|
+
- lib/keycloak-api-rails/service.rb
|
112
|
+
- lib/keycloak-api-rails/token_error.rb
|
113
|
+
- lib/keycloak-api-rails/version.rb
|
114
|
+
- spec/keycloak-api-rails/helper_spec.rb
|
115
|
+
- spec/keycloak-api-rails/public_key_cached_resolver_spec.rb
|
116
|
+
- spec/keycloak-api-rails/service_spec.rb
|
117
|
+
- spec/spec_helper.rb
|
118
|
+
- spec/support/public_key_cached_resolver_stub.rb
|
119
|
+
- spec/support/public_key_resolver_stub.rb
|
120
|
+
- spec/support/rails_helper.rb
|
121
|
+
homepage: https://github.com/looorent/keycloak-api-rails
|
122
|
+
licenses:
|
123
|
+
- MIT
|
124
|
+
metadata: {}
|
125
|
+
post_install_message:
|
126
|
+
rdoc_options: []
|
127
|
+
require_paths:
|
128
|
+
- lib
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 2.6.4
|
142
|
+
signing_key:
|
143
|
+
specification_version: 4
|
144
|
+
summary: Rails middleware that validates Authorization token emitted by Keycloak
|
145
|
+
test_files: []
|