keycloak_rack 1.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.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +68 -0
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.rubocop.yml +220 -0
- data/.ruby-version +1 -0
- data/.yardopts +7 -0
- data/Appraisals +16 -0
- data/CHANGELOG.md +10 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Gemfile +5 -0
- data/LICENSE +19 -0
- data/README.md +288 -0
- data/Rakefile +10 -0
- data/bin/appraisal +29 -0
- data/bin/console +6 -0
- data/bin/fix-appraisals +14 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/yard +29 -0
- data/bin/yardoc +29 -0
- data/bin/yri +29 -0
- data/gemfiles/rack_only.gemfile +5 -0
- data/gemfiles/rack_only.gemfile.lock +204 -0
- data/gemfiles/rails_6_0.gemfile +9 -0
- data/gemfiles/rails_6_0.gemfile.lock +323 -0
- data/gemfiles/rails_6_1.gemfile +9 -0
- data/gemfiles/rails_6_1.gemfile.lock +326 -0
- data/keycloak_rack.gemspec +56 -0
- data/lib/keycloak_rack.rb +59 -0
- data/lib/keycloak_rack/authenticate.rb +115 -0
- data/lib/keycloak_rack/authorize_realm.rb +53 -0
- data/lib/keycloak_rack/authorize_resource.rb +54 -0
- data/lib/keycloak_rack/config.rb +84 -0
- data/lib/keycloak_rack/container.rb +53 -0
- data/lib/keycloak_rack/decoded_token.rb +191 -0
- data/lib/keycloak_rack/flexible_struct.rb +20 -0
- data/lib/keycloak_rack/http_client.rb +86 -0
- data/lib/keycloak_rack/import.rb +9 -0
- data/lib/keycloak_rack/key_fetcher.rb +20 -0
- data/lib/keycloak_rack/key_resolver.rb +64 -0
- data/lib/keycloak_rack/middleware.rb +132 -0
- data/lib/keycloak_rack/railtie.rb +14 -0
- data/lib/keycloak_rack/read_token.rb +40 -0
- data/lib/keycloak_rack/resource_role_map.rb +8 -0
- data/lib/keycloak_rack/role_map.rb +15 -0
- data/lib/keycloak_rack/session.rb +44 -0
- data/lib/keycloak_rack/skip_authentication.rb +44 -0
- data/lib/keycloak_rack/types.rb +42 -0
- data/lib/keycloak_rack/version.rb +6 -0
- data/lib/keycloak_rack/with_config.rb +15 -0
- data/spec/dummy/.ruby-version +1 -0
- data/spec/dummy/README.md +24 -0
- data/spec/dummy/Rakefile +8 -0
- data/spec/dummy/app/controllers/application_controller.rb +22 -0
- data/spec/dummy/app/controllers/test_controller.rb +9 -0
- data/spec/dummy/config.ru +8 -0
- data/spec/dummy/config/application.rb +52 -0
- data/spec/dummy/config/boot.rb +3 -0
- data/spec/dummy/config/environment.rb +7 -0
- data/spec/dummy/config/environments/development.rb +51 -0
- data/spec/dummy/config/environments/test.rb +51 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +9 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +10 -0
- data/spec/dummy/config/initializers/cors.rb +17 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +8 -0
- data/spec/dummy/config/initializers/inflections.rb +17 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +11 -0
- data/spec/dummy/config/keycloak.yml +12 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/routes.rb +5 -0
- data/spec/dummy/public/robots.txt +1 -0
- data/spec/dummy/tmp/development_secret.txt +1 -0
- data/spec/factories/decoded_token.rb +18 -0
- data/spec/factories/session.rb +21 -0
- data/spec/factories/token_payload.rb +40 -0
- data/spec/keycloak_rack/authorize_realm_spec.rb +15 -0
- data/spec/keycloak_rack/authorize_resource_spec.rb +19 -0
- data/spec/keycloak_rack/decoded_token_spec.rb +31 -0
- data/spec/keycloak_rack/key_resolver_spec.rb +95 -0
- data/spec/keycloak_rack/middleware_spec.rb +172 -0
- data/spec/keycloak_rack/rails_integration_spec.rb +43 -0
- data/spec/keycloak_rack/session_spec.rb +37 -0
- data/spec/keycloak_rack/skip_authentication_spec.rb +55 -0
- data/spec/spec_helper.rb +101 -0
- data/spec/support/contexts/mocked_keycloak.rb +63 -0
- data/spec/support/contexts/mocked_rack_application.rb +41 -0
- data/spec/support/test_key.pem +27 -0
- data/spec/support/token_helper.rb +76 -0
- metadata +616 -0
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KeycloakRack
|
4
|
+
# The core service that handles authenticating a request from Keycloak.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# class ApplicationController < ActionController::API
|
8
|
+
# before_action :authenticate_user!
|
9
|
+
#
|
10
|
+
# # @return [void]
|
11
|
+
# def authenticate_user!
|
12
|
+
# # KeycloakRack::Session#authenticate! implements a Dry::Matcher::ResultMatcher
|
13
|
+
# request.env["keycloak:session"].authenticate! do |m|
|
14
|
+
# m.success(:authenticated) do |_, token|
|
15
|
+
# # this is the case when a user is successfully authenticated
|
16
|
+
#
|
17
|
+
# # token will be a KeycloakRack::DecodedToken instance, a
|
18
|
+
# # hash-like PORO that maps a number of values from the
|
19
|
+
# # decoded JWT that can be used to find or upsert a user
|
20
|
+
#
|
21
|
+
# attrs = decoded_token.slice(:keycloak_id, :email, :email_verified, :realm_access, :resource_access)
|
22
|
+
#
|
23
|
+
# result = User.upsert attrs, returning: %i[id], unique_by: %i[keycloak_id]
|
24
|
+
#
|
25
|
+
# @current_user = User.find result.first["id"]
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# m.success do
|
29
|
+
# # When allow_anonymous is true, or
|
30
|
+
# # a URI is skipped because of skip_paths, this
|
31
|
+
# # case will be reached. Requests from here on
|
32
|
+
# # out should be considered anonymous and treated
|
33
|
+
# # accordingly
|
34
|
+
#
|
35
|
+
# @current_user = AnonymousUser.new
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# m.failure do |code, reason|
|
39
|
+
# # All authentication failures are reached here,
|
40
|
+
# # assuming halt_on_auth_failure is set to false
|
41
|
+
# # This allows the application to decide how it
|
42
|
+
# # wants to respond
|
43
|
+
#
|
44
|
+
# render json: { errors: [{ message: "Auth Failure" }] }, status: :forbidden
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
class Authenticate
|
50
|
+
include Dry::Monads[:do, :result]
|
51
|
+
|
52
|
+
include Import[
|
53
|
+
config: "keycloak-rack.config",
|
54
|
+
key_resolver: "keycloak-rack.key_resolver",
|
55
|
+
read_token: "keycloak-rack.read_token",
|
56
|
+
skip_authentication: "keycloak-rack.skip_authentication"
|
57
|
+
]
|
58
|
+
|
59
|
+
delegate :token_leeway, to: :config
|
60
|
+
|
61
|
+
# @param [Hash] env the rack environment
|
62
|
+
# @return [Dry::Monads::Success(:authenticated, KeycloakRack::DecodedToken)]
|
63
|
+
# @return [Dry::Monads::Success(:skipped, String)]
|
64
|
+
# @return [Dry::Monads::Success(:unauthenticated)]
|
65
|
+
# @return [Dry::Monads::Failure(:expired, String, String, Exception)]
|
66
|
+
# @return [Dry::Monads::Failure(:decoding_failed, String, String, Exception)]
|
67
|
+
def call(env)
|
68
|
+
return Success[:skipped] if yield skip_authentication.call(env)
|
69
|
+
|
70
|
+
token = yield read_token.call env
|
71
|
+
|
72
|
+
return Success[:unauthenticated] if token.blank?
|
73
|
+
|
74
|
+
decoded_token = yield decode_and_verify token
|
75
|
+
|
76
|
+
Success[:authenticated, decoded_token]
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# @param [String] token
|
82
|
+
# @return [Dry::Monads::Success(KeycloakRack::DecodedToken)]
|
83
|
+
# @return [Dry::Monads::Failure(:expired, String, String, Exception)]
|
84
|
+
# @return [Dry::Monads::Failure(:decoding_failed, String, String, Exception)]
|
85
|
+
def decode_and_verify(token)
|
86
|
+
jwks = yield key_resolver.find_public_keys
|
87
|
+
|
88
|
+
algorithms = yield algorithms_for jwks
|
89
|
+
|
90
|
+
options = {
|
91
|
+
algorithms: algorithms,
|
92
|
+
leeway: token_leeway,
|
93
|
+
jwks: jwks
|
94
|
+
}
|
95
|
+
|
96
|
+
payload, headers = JWT.decode token, nil, true, options
|
97
|
+
rescue JWT::ExpiredSignature => e
|
98
|
+
Failure[:expired, "JWT is expired", token, e]
|
99
|
+
rescue JWT::DecodeError => e
|
100
|
+
Failure[:decoding_failed, "Failed to decode JWT", token, e]
|
101
|
+
else
|
102
|
+
Success DecodedToken.new payload.merge(original_payload: payload, headers: headers)
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param [{ Symbol => <{ Symbol => String }> }] jwks
|
106
|
+
# @return [<String>]
|
107
|
+
def algorithms_for(jwks)
|
108
|
+
jwks.fetch(:keys, []).map do |k|
|
109
|
+
k[:alg]
|
110
|
+
end.uniq.compact.then do |algs|
|
111
|
+
algs.present? ? Success(algs) : Failure[:no_algorithms, "Could not derive algorithms from JWKS"]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KeycloakRack
|
4
|
+
# A service that allows someone to check if the current token has a realm-level role.
|
5
|
+
#
|
6
|
+
# It is instantiated in `keycloak:authorize_realm` after the middleware runs.
|
7
|
+
#
|
8
|
+
# This can greatly simplify access control for rack services (for instance, to gate uploading files outside of Rails).
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# class UploadProcessor
|
12
|
+
# def initialize(app)
|
13
|
+
# @app = app
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# def call(env)
|
17
|
+
# env["keycloak.authorize_realm"].call("upload_permission") do |m|
|
18
|
+
# m.success do
|
19
|
+
# # allow the upload to proceed
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# m.failure do
|
23
|
+
# # fail the response, return 403, etc
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
class AuthorizeRealm
|
29
|
+
extend Dry::Initializer
|
30
|
+
|
31
|
+
include Dry::Monads[:result]
|
32
|
+
include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
|
33
|
+
|
34
|
+
param :session, Types.Instance(KeycloakRack::Session)
|
35
|
+
|
36
|
+
# Check to see if the current user session has a certain realm-level role.
|
37
|
+
#
|
38
|
+
# @see KeycloakRack::DecodedToken#has_realm_role?
|
39
|
+
# @param [String] role_name
|
40
|
+
# @return [Dry::Monads::Success(:authorized, String)]
|
41
|
+
# @return [Dry::Monads::Failure(:unauthorized, String)]
|
42
|
+
# @return [Dry::Monads::Failure(:unauthenticated, String)]
|
43
|
+
def call(role_name)
|
44
|
+
if session.has_realm_role?(role_name)
|
45
|
+
Success[:authorized, role_name]
|
46
|
+
elsif session.authenticated?
|
47
|
+
Failure[:unauthorized, "You do not have #{role_name.to_s.inspect} access"]
|
48
|
+
else
|
49
|
+
Failure[:unauthenticated, "You are not authenticated"]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KeycloakRack
|
4
|
+
# A service that allows someone to check if the current token has a resource-level role.
|
5
|
+
#
|
6
|
+
# It is instantiated in `keycloak:authorize_resource` after the middleware runs.
|
7
|
+
#
|
8
|
+
# This can greatly simplify access control for rack services (for instance, to gate modifications to a certain type of resource).
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# class WidgetCombobulator
|
12
|
+
# def initialize(app)
|
13
|
+
# @app = app
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# def call(env)
|
17
|
+
# env["keycloak.authorize_resource"].call("widgets", "recombobulate") do |m|
|
18
|
+
# m.success do
|
19
|
+
# # allow the user to recombobulate the widget
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# m.failure do
|
23
|
+
# # return forbidden, log the attempt, etc
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
class AuthorizeResource
|
29
|
+
extend Dry::Initializer
|
30
|
+
|
31
|
+
include Dry::Monads[:result]
|
32
|
+
include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
|
33
|
+
|
34
|
+
param :session, Types.Instance(KeycloakRack::Session)
|
35
|
+
|
36
|
+
# Check that the current session has a certain resource role.
|
37
|
+
#
|
38
|
+
# @see KeycloakRack::DecodedToken#has_resource_role?
|
39
|
+
# @param [String] resource_name
|
40
|
+
# @param [String] role_name
|
41
|
+
# @return [Dry::Monads::Success(:authorized, String)]
|
42
|
+
# @return [Dry::Monads::Failure(:unauthorized, String)]
|
43
|
+
# @return [Dry::Monads::Failure(:unauthenticated, String)]
|
44
|
+
def call(resource_name, role_name)
|
45
|
+
if session.has_resource_role?(resource_name, role_name)
|
46
|
+
Success[:authorized, resource_name, role_name]
|
47
|
+
elsif session.authenticated?
|
48
|
+
Failure[:unauthorized, "You do not have #{role_name.to_s.inspect} access on #{resource_name.to_s.inspect}"]
|
49
|
+
else
|
50
|
+
Failure[:unauthenticated, "You are not authenticated"]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KeycloakRack
|
4
|
+
# Configuration model for KeycloakRack.
|
5
|
+
#
|
6
|
+
# Uses [anyway_config](https://github.com/palkan/anyway_config)
|
7
|
+
# to permit flexible approaches to configuration.
|
8
|
+
class Config < Anyway::Config
|
9
|
+
config_name "keycloak"
|
10
|
+
|
11
|
+
env_prefix "KEYCLOAK"
|
12
|
+
|
13
|
+
# @!attribute [rw] server_url
|
14
|
+
#
|
15
|
+
# The URL of your Keycloak installation. Be sure to include `/auth` if necessary.
|
16
|
+
#
|
17
|
+
# @note Required config value
|
18
|
+
# @return [String]
|
19
|
+
attr_config :server_url
|
20
|
+
|
21
|
+
# @!attribute [rw] realm_id
|
22
|
+
#
|
23
|
+
# The ID of the realm used to authenticate requests.
|
24
|
+
#
|
25
|
+
# @note Required config value
|
26
|
+
# @return [String]
|
27
|
+
attr_config :realm_id
|
28
|
+
|
29
|
+
# @!attribute [rw] ca_certificate_file
|
30
|
+
# The optional path to the CA Certificate to validate connections to a keycloak server.
|
31
|
+
# @return [String, nil]
|
32
|
+
attr_config :ca_certificate_file
|
33
|
+
|
34
|
+
# @!attribute [r] skip_paths
|
35
|
+
# @return [{ #to_s => <String, Regexp> }]
|
36
|
+
attr_config skip_paths: {}
|
37
|
+
|
38
|
+
# @!attribute [rw] token_leeway
|
39
|
+
# The number of seconds to allow for tokens to be expired to allow for clock drift.
|
40
|
+
#
|
41
|
+
# @see https://github.com/jwt/ruby-jwt#expiration-time-claim
|
42
|
+
# @return [Integer]
|
43
|
+
attr_config token_leeway: 10
|
44
|
+
|
45
|
+
# @!attribute [rw] cache_ttl
|
46
|
+
# The interval (in seconds) that cached public keys in {KeycloakRack::KeyResolver} should be cached.
|
47
|
+
# @return [Integer]
|
48
|
+
attr_config cache_ttl: 86_400
|
49
|
+
|
50
|
+
# @!attribute [rw] halt_on_auth_failure
|
51
|
+
# @return [Boolean]
|
52
|
+
attr_config halt_on_auth_failure: true
|
53
|
+
|
54
|
+
# @!attribute [rw] allow_anonymous
|
55
|
+
# @return [Boolean]
|
56
|
+
attr_config allow_anonymous: false
|
57
|
+
|
58
|
+
# required :server_url, :realm_id
|
59
|
+
|
60
|
+
def cache_ttl=(value)
|
61
|
+
super Types::Coercible::Integer[value]
|
62
|
+
end
|
63
|
+
|
64
|
+
def skip_paths=(value)
|
65
|
+
super Types::SkipPaths[value]
|
66
|
+
end
|
67
|
+
|
68
|
+
def token_leeway=(value)
|
69
|
+
super Types::Coercible::Integer[value]
|
70
|
+
end
|
71
|
+
|
72
|
+
# @api private
|
73
|
+
# @!visibility private
|
74
|
+
# @return [OpenSSL::X509::Store]
|
75
|
+
def build_x509_store
|
76
|
+
# :nocov:
|
77
|
+
OpenSSL::X509::Store.new.tap do |store|
|
78
|
+
store.set_default_paths
|
79
|
+
store.add_file(ca_certificate_file) if ca_certificate_file.present?
|
80
|
+
end
|
81
|
+
# :nocov:
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KeycloakRack
|
4
|
+
# Dependency injection container for various `KeycloakRack` objects
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
# @!visibility private
|
8
|
+
class Container
|
9
|
+
extend Dry::Container::Mixin
|
10
|
+
|
11
|
+
namespace "keycloak-rack" do
|
12
|
+
register :config do
|
13
|
+
# :nocov:
|
14
|
+
KeycloakRack::Config.new
|
15
|
+
# :nocov:
|
16
|
+
end
|
17
|
+
|
18
|
+
register :authenticate do
|
19
|
+
KeycloakRack::Authenticate.new
|
20
|
+
end
|
21
|
+
|
22
|
+
register :http_client do
|
23
|
+
KeycloakRack::HTTPClient.new
|
24
|
+
end
|
25
|
+
|
26
|
+
register :key_fetcher do
|
27
|
+
KeycloakRack::KeyFetcher.new
|
28
|
+
end
|
29
|
+
|
30
|
+
register :key_resolver, memoize: true do
|
31
|
+
# :nocov:
|
32
|
+
KeycloakRack::KeyResolver.new
|
33
|
+
# :nocov:
|
34
|
+
end
|
35
|
+
|
36
|
+
register :read_token do
|
37
|
+
KeycloakRack::ReadToken.new
|
38
|
+
end
|
39
|
+
|
40
|
+
register :server_url do
|
41
|
+
resolve(:config).server_url
|
42
|
+
end
|
43
|
+
|
44
|
+
register :skip_authentication do
|
45
|
+
KeycloakRack::SkipAuthentication.new
|
46
|
+
end
|
47
|
+
|
48
|
+
register :x509_store do
|
49
|
+
resolve(:config).build_x509_store
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KeycloakRack
|
4
|
+
# PORO that wraps the result of decoding the JWT into something slightly more usable,
|
5
|
+
# with some type-safety and role checking features.
|
6
|
+
class DecodedToken < KeycloakRack::FlexibleStruct
|
7
|
+
# Mapping used to remap keys from a Keycloak JWT payload into something more legible.
|
8
|
+
# @api private
|
9
|
+
KEY_MAP = {
|
10
|
+
"allowed-origins" => :allowed_origins,
|
11
|
+
"auth_time" => :authorized_at,
|
12
|
+
"aud" => :audience,
|
13
|
+
"azp" => :authorized_party,
|
14
|
+
"exp" => :expires_at,
|
15
|
+
"iat" => :issued_at,
|
16
|
+
"typ" => :type,
|
17
|
+
}.with_indifferent_access.freeze
|
18
|
+
|
19
|
+
private_constant :KEY_MAP
|
20
|
+
|
21
|
+
transform_keys do |k|
|
22
|
+
KEY_MAP[k] || k.to_sym
|
23
|
+
end
|
24
|
+
|
25
|
+
# @!attribute [r] sub
|
26
|
+
# The user id / subject for the JWT. Corresponds to `user_id` in Keycloak's rest API,
|
27
|
+
# and suitable for linking your local user records to Keycloak's.
|
28
|
+
# @return [String]
|
29
|
+
attribute :sub, Types::String
|
30
|
+
|
31
|
+
# @!attribute [r] realm_access
|
32
|
+
# @return [KeycloakRack::RoleMap]
|
33
|
+
attribute :realm_access, RoleMap
|
34
|
+
|
35
|
+
# @!attribute [r] resource_access
|
36
|
+
# @return [{ String => KeycloakRack::RoleMap }]
|
37
|
+
attribute :resource_access, ResourceRoleMap
|
38
|
+
|
39
|
+
# @!attribute [r] email_verified
|
40
|
+
# @return [Boolean]
|
41
|
+
attribute? :email_verified, Types::Bool
|
42
|
+
|
43
|
+
# @!attribute [r] name
|
44
|
+
# @return [String, nil]
|
45
|
+
attribute? :name, Types::String.optional
|
46
|
+
|
47
|
+
# @!attribute [r] preferred_username
|
48
|
+
# @return [String, nil]
|
49
|
+
attribute? :preferred_username, Types::String.optional
|
50
|
+
|
51
|
+
# @!attribute [r] given_name
|
52
|
+
# @return [String, nil]
|
53
|
+
attribute? :given_name, Types::String.optional
|
54
|
+
|
55
|
+
# @!attribute [r] family_name
|
56
|
+
# @return [String, nil]
|
57
|
+
attribute? :family_name, Types::String.optional
|
58
|
+
|
59
|
+
# @!attribute [r] email
|
60
|
+
# @return [String, nil]
|
61
|
+
attribute? :email, Types::String.optional
|
62
|
+
|
63
|
+
# @!group Token Details
|
64
|
+
|
65
|
+
# @!attribute [r] expires_at
|
66
|
+
# The `exp` claim
|
67
|
+
# @return [Time]
|
68
|
+
attribute :expires_at, Types::Timestamp
|
69
|
+
|
70
|
+
# @!attribute [r] issued_at
|
71
|
+
# The `iat` claim
|
72
|
+
# @return [Time]
|
73
|
+
attribute :issued_at, Types::Timestamp
|
74
|
+
|
75
|
+
# @!attribute [r] authorized_at
|
76
|
+
# The `auth_time` value from Keycloak.
|
77
|
+
# @return [Time]
|
78
|
+
attribute :authorized_at, Types::Timestamp
|
79
|
+
|
80
|
+
# @!attribute [r] jti
|
81
|
+
# @return [String]
|
82
|
+
attribute :jti, Types::String
|
83
|
+
|
84
|
+
# @!attribute [r] audience
|
85
|
+
# @return [String]
|
86
|
+
attribute :audience, Types::String
|
87
|
+
|
88
|
+
# @!attribute [r] type
|
89
|
+
# The `typ` claim in the JWT. Keycloak sets this to `"JWT"`.
|
90
|
+
# @return [String]
|
91
|
+
attribute :type, Types::String
|
92
|
+
|
93
|
+
# @!attribute [r] authorized_party
|
94
|
+
# The `azp` claim
|
95
|
+
# @return [String]
|
96
|
+
attribute :authorized_party, Types::String
|
97
|
+
|
98
|
+
# @!attribute [r] nonce
|
99
|
+
# Cryptographic nonce for the token
|
100
|
+
# @return [String]
|
101
|
+
attribute :nonce, Types::String
|
102
|
+
|
103
|
+
# @!attribute [r] scope
|
104
|
+
# @return [String]
|
105
|
+
attribute :scope, Types::String
|
106
|
+
|
107
|
+
# @!attribute [r] session_state
|
108
|
+
# @return [String]
|
109
|
+
attribute :session_state, Types::String
|
110
|
+
|
111
|
+
# @!attribute [r] locale
|
112
|
+
# @return [String, nil]
|
113
|
+
attribute? :locale, Types::String.optional
|
114
|
+
|
115
|
+
# @!attribute [r] allowed_origins
|
116
|
+
# @return [<String>]
|
117
|
+
attribute :allowed_origins, Types::StringList
|
118
|
+
|
119
|
+
# @!attribute [r] headers
|
120
|
+
# The JWT headers, provided for debugging
|
121
|
+
# @return [ActiveSupport::HashWithindifferentAccess]
|
122
|
+
attribute? :headers, Types::IndifferentHash
|
123
|
+
|
124
|
+
# @!attribute [r] original_payload
|
125
|
+
# The original JWT payload, unmodified, for extracting potential additional attributes.
|
126
|
+
# @return [ActiveSupport::HashWithIndifferentAccess]
|
127
|
+
attribute? :original_payload, Types::IndifferentHash
|
128
|
+
|
129
|
+
# @!endgroup
|
130
|
+
|
131
|
+
alias keycloak_id sub
|
132
|
+
|
133
|
+
alias first_name given_name
|
134
|
+
|
135
|
+
alias last_name family_name
|
136
|
+
|
137
|
+
ALIASES = %i[keycloak_id first_name last_name].freeze
|
138
|
+
|
139
|
+
private_constant :ALIASES
|
140
|
+
|
141
|
+
delegate :attribute_names, to: :class
|
142
|
+
|
143
|
+
# @param [#to_sym] key
|
144
|
+
# @raise [KeycloakRack::DecodedToken::UnknownAttribute] if it is an unknown attribute
|
145
|
+
# @return [Object]
|
146
|
+
def fetch(key)
|
147
|
+
key = key.to_sym
|
148
|
+
|
149
|
+
if key.in?(attribute_names)
|
150
|
+
self[key]
|
151
|
+
elsif key.in?(ALIASES)
|
152
|
+
public_send(key)
|
153
|
+
elsif key.in?(original_payload)
|
154
|
+
original_payload[key]
|
155
|
+
else
|
156
|
+
raise UnknownAttribute, "Cannot fetch #{key.inspect}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Check if the current user has a certain realm role
|
161
|
+
#
|
162
|
+
# @param [#to_s] name
|
163
|
+
def has_realm_role?(name)
|
164
|
+
name.to_s.in? realm_access.roles
|
165
|
+
end
|
166
|
+
|
167
|
+
# Check if the user has a certain role on a certain resource.
|
168
|
+
#
|
169
|
+
# @param [#to_s] resource_name
|
170
|
+
# @param [#to_s] role_name
|
171
|
+
def has_resource_role?(resource_name, role_name)
|
172
|
+
resource_access[resource_name.to_s]&.has_role?(role_name)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Extract keys into something hash-like
|
176
|
+
#
|
177
|
+
# @param [<String, Symbol>] keys
|
178
|
+
# @return [ActiveSupport::HashWithIndifferentAccess]
|
179
|
+
def slice(*keys)
|
180
|
+
keys.flatten!
|
181
|
+
|
182
|
+
keys.each_with_object({}.with_indifferent_access) do |key, h|
|
183
|
+
h[key] = fetch(key)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# An error raised by {KeycloakRack::DecodedToken#fetch} when
|
188
|
+
# trying to fetch something the token doesn't know about
|
189
|
+
class UnknownAttribute < KeyError; end
|
190
|
+
end
|
191
|
+
end
|