keycloak_oauth 0.1.6 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5372c486b89d09a51766f5ae6420c926f23d26d3f83ed30a9cd8b62c0c100a9
4
- data.tar.gz: fcc3d36bbcdcd9d3fda6d51ebfb74e227cbb1f3eb234dfabd98531362c0be8e2
3
+ metadata.gz: 4c8198edc1a928cd020798264f22e0ebcbbc6a4905aa4ebad578be3404ca92c4
4
+ data.tar.gz: a4cfff847382aba3b6b2757ca2e53da0dc1d21334ccaf21db2bd7b12083c8816
5
5
  SHA512:
6
- metadata.gz: ea374a1c1d6fa04d4473cc645be6af6bfe0d9bb3468924a238cae97ee990c339bbd16d99c8c0173c736b48f0cb5d3c8758cae059dd3d12713f872d6a12ca2dfd
7
- data.tar.gz: 95eb399aac689d154fb942521da92a557a0b5fc7f035c7798fb48b3c2e3e1c1a44a2548724cfcb5c96d689d8e6b491f75ca028fb6ad3e356174ae76a8b8562da
6
+ metadata.gz: 2e477873732dd5931bb3769d69831c77dbd9c18329c1c9391361a80e17bf1cdbfbf00847563bdbf32f3fe613121e98c89c8965f31adcefe71736d8d7a9e0cea3
7
+ data.tar.gz: 68d870d6d136be6e0f170bc1f1361c9325558829d52371f5708fbcd6043ed2e1f38680918fb44d31cf0b399f982c54389ff05e8a7ea071c57bbedf9b314dd8a9
@@ -7,8 +7,7 @@ module KeycloakOauth
7
7
  def oauth2
8
8
  authentication_service = KeycloakOauth::AuthenticationService.new(
9
9
  authentication_params: authentication_params,
10
- session: session,
11
- redirect_uri: current_uri_without_params
10
+ session: session
12
11
  )
13
12
  authentication_service.authenticate
14
13
  map_authenticatable_if_implemented(session)
@@ -19,7 +18,7 @@ module KeycloakOauth
19
18
  private
20
19
 
21
20
  def authentication_params
22
- params.permit(:code)
21
+ params.permit(:code).merge({ redirect_uri: current_uri_without_params })
23
22
  end
24
23
 
25
24
  def map_authenticatable_if_implemented(request)
@@ -1,59 +1,31 @@
1
- require 'net/http'
2
-
3
1
  module KeycloakOauth
4
- class AuthenticationError < StandardError; end
5
-
6
2
  class AuthenticationService
7
- GRANT_TYPE = 'authorization_code'.freeze
8
- CONTENT_TYPE = 'application/x-www-form-urlencoded'.freeze
9
3
  ACCESS_TOKEN_KEY = 'access_token'.freeze
10
4
  REFRESH_TOKEN_KEY = 'refresh_token'.freeze
11
5
 
12
- attr_reader :session
6
+ attr_reader :session, :authentication_params
13
7
 
14
- def initialize(authentication_params:, session:, redirect_uri:)
15
- @code = authentication_params[:code]
8
+ def initialize(authentication_params:, session:)
9
+ @authentication_params = authentication_params
16
10
  @session = session
17
- @redirect_uri = redirect_uri
18
11
  end
19
12
 
20
13
  def authenticate
21
- store_credentials(get_tokens)
14
+ post_token_service = KeycloakOauth::PostTokenService.new(
15
+ connection: KeycloakOauth.connection,
16
+ request_params: authentication_params
17
+ )
18
+ post_token_service.perform
19
+ store_credentials(post_token_service)
22
20
  end
23
21
 
24
22
  private
25
23
 
26
- attr_reader :code, :redirect_uri
27
-
28
- def get_tokens
29
- uri = URI.parse(KeycloakOauth.connection.authentication_endpoint)
30
- Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
31
- request = Net::HTTP::Post.new(uri)
32
- request.set_content_type(CONTENT_TYPE)
33
- request.set_form_data(token_request_params)
34
- http.request(request)
35
- end
36
- end
37
-
38
- def token_request_params
39
- {
40
- client_id: KeycloakOauth.connection.client_id,
41
- client_secret: KeycloakOauth.connection.client_secret,
42
- grant_type: GRANT_TYPE,
43
- code: code,
44
- redirect_uri: redirect_uri
45
- }
46
- end
47
-
48
- def store_credentials(http_response)
49
- response_hash = JSON.parse(http_response.body)
24
+ def store_credentials(post_token_service)
25
+ response_hash = post_token_service.parsed_response_body
50
26
 
51
- if http_response.code_type == Net::HTTPOK
52
- session[:access_token] = response_hash[ACCESS_TOKEN_KEY]
53
- session[:refresh_token] = response_hash[REFRESH_TOKEN_KEY]
54
- else
55
- raise KeycloakOauth::AuthenticationError.new(response_hash)
56
- end
27
+ session[:access_token] = response_hash[ACCESS_TOKEN_KEY]
28
+ session[:refresh_token] = response_hash[REFRESH_TOKEN_KEY]
57
29
  end
58
30
  end
59
31
  end
@@ -2,23 +2,73 @@ require 'net/http'
2
2
 
3
3
  module KeycloakOauth
4
4
  class AuthorizableError < StandardError; end
5
+ class NotFoundError < StandardError; end
5
6
 
6
7
  class AuthorizableService
7
- HTTP_SUCCESS_CODES = [Net::HTTPOK, Net::HTTPNoContent]
8
- DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded'.freeze
8
+ HTTP_SUCCESS_CODES = [Net::HTTPOK, Net::HTTPNoContent, Net::HTTPCreated]
9
+ CONTENT_TYPE_X_WWW_FORM_URLENCODED = 'application/x-www-form-urlencoded'.freeze
10
+ CONTENT_TYPE_JSON = 'application/json'.freeze
9
11
  AUTHORIZATION_HEADER = 'Authorization'.freeze
10
12
 
13
+ attr_reader :http_response, :parsed_response_body
14
+
15
+ def perform
16
+ @http_response = send_request
17
+ @parsed_response_body ||= parse_response_body(http_response)
18
+ end
19
+
20
+ def self.uri_with_supported_query_params(url, supported_params, given_params)
21
+ uri = URI.parse(url)
22
+
23
+ query_params = supported_params.inject({}) do |acc, query_param|
24
+ acc[query_param] = given_params[query_param] if given_params[query_param].present?
25
+ acc
26
+ end
27
+
28
+ log_unsupported_params(given_params.keys - supported_params)
29
+
30
+ uri.query = URI.encode_www_form(query_params) if query_params.values.any?
31
+ uri
32
+ end
33
+
11
34
  private
12
35
 
13
- def parsed_response(http_response)
14
- response = http_response.body.present? ? JSON.parse(http_response.body) : http_response.body
36
+ def parse_response_body(http_response)
37
+ response_body = http_response.body.present? ? JSON.parse(http_response.body) : http_response.body
15
38
 
16
- return response if HTTP_SUCCESS_CODES.include?(http_response.code_type)
39
+ return response_body if HTTP_SUCCESS_CODES.include?(http_response.code_type)
17
40
 
18
41
  # TODO: For now, we assume that the access token is always valid.
19
42
  # We do not yet handle the case where a refresh token is passed in and
20
43
  # used if the access token has expired.
21
- raise KeycloakOauth::AuthorizableError.new(response)
44
+ raise KeycloakOauth::AuthorizableError.new(error_message_from(response_body))
45
+ end
46
+
47
+ def error_message_from(response)
48
+ # Keycloak sometimes sends back a hash containing the "errorMessage" key,
49
+ # other times it returns a hash containing the "error_description key" key
50
+ # and other times it returns an empty string. There could be more cases,
51
+ # but for the moment we are only handling these three.
52
+ case response.class.to_s
53
+ when 'Hash'
54
+ if response.has_key?('errorMessage')
55
+ return response['errorMessage']
56
+ elsif response.has_key?('error_description')
57
+ return response['error_description']
58
+ elsif response.has_key?('error')
59
+ return response['error']
60
+ end
61
+ when 'String'
62
+ return response
63
+ else
64
+ 'Unexpected Keycloak error'
65
+ end
66
+ end
67
+
68
+ def self.log_unsupported_params(query_params)
69
+ query_params.each do |query_param|
70
+ Rails.logger.warn { "Unsupported query param was passed in: #{query_param}" }
71
+ end
22
72
  end
23
73
  end
24
74
  end
@@ -0,0 +1,42 @@
1
+ require 'net/http'
2
+
3
+ module KeycloakOauth
4
+ class GetUsersService < KeycloakOauth::AuthorizableService
5
+ SUPPORTED_QUERY_PARAMS = %i(briefRepresentation email first firstName lastName max search username)
6
+
7
+ attr_reader :connection, :options
8
+
9
+ def initialize(connection:, access_token:, refresh_token:, options: {})
10
+ @connection = connection
11
+ @access_token = access_token
12
+ @refresh_token = refresh_token
13
+ @options = options
14
+ end
15
+
16
+ def send_request
17
+ get_users
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :access_token, :refresh_token
23
+
24
+ def get_users
25
+ uri = build_uri
26
+
27
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
28
+ request = Net::HTTP::Get.new(uri)
29
+ request[AUTHORIZATION_HEADER] = "Bearer #{access_token}"
30
+ http.request(request)
31
+ end
32
+ end
33
+
34
+ def build_uri
35
+ self.class.uri_with_supported_query_params(
36
+ connection.users_endpoint,
37
+ SUPPORTED_QUERY_PARAMS,
38
+ options
39
+ )
40
+ end
41
+ end
42
+ end
@@ -6,8 +6,8 @@ module KeycloakOauth
6
6
  @session = session
7
7
  end
8
8
 
9
- def logout
10
- parsed_response(post_logout)
9
+ def send_request
10
+ post_logout
11
11
  end
12
12
 
13
13
  private
@@ -16,9 +16,9 @@ module KeycloakOauth
16
16
 
17
17
  def post_logout
18
18
  uri = URI.parse(KeycloakOauth.connection.logout_endpoint)
19
- Net::HTTP.start(uri.host, uri.port) do |http|
19
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
20
20
  request = Net::HTTP::Post.new(uri)
21
- request.set_content_type(DEFAULT_CONTENT_TYPE)
21
+ request.set_content_type(CONTENT_TYPE_X_WWW_FORM_URLENCODED)
22
22
  request.set_form_data(logout_request_params)
23
23
  request[AUTHORIZATION_HEADER] = "Bearer #{access_token}"
24
24
  http.request(request)
@@ -0,0 +1,43 @@
1
+ require 'net/http'
2
+
3
+ module KeycloakOauth
4
+ class PostTokenService < KeycloakOauth::AuthorizableService
5
+ DEFAULT_GRANT_TYPE = 'authorization_code'.freeze
6
+
7
+ attr_reader :request_params, :connection
8
+
9
+ def initialize(connection:, access_token: nil, refresh_token: nil, request_params:)
10
+ @connection = connection
11
+ @access_token = access_token
12
+ @refresh_token = refresh_token
13
+ @request_params = request_params
14
+ end
15
+
16
+ def send_request
17
+ post_token
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :code, :redirect_uri, :access_token, :refresh_token
23
+
24
+ def post_token
25
+ uri = URI.parse(connection.authentication_endpoint)
26
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
27
+ request = Net::HTTP::Post.new(uri)
28
+ request[AUTHORIZATION_HEADER] = "Bearer #{access_token}" if access_token.present?
29
+ request.set_content_type(CONTENT_TYPE_X_WWW_FORM_URLENCODED)
30
+ request.set_form_data(token_request_params)
31
+ http.request(request)
32
+ end
33
+ end
34
+
35
+ def token_request_params
36
+ {
37
+ client_id: connection.client_id,
38
+ client_secret: connection.client_secret,
39
+ grant_type: DEFAULT_GRANT_TYPE
40
+ }.merge(request_params)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,47 @@
1
+ require 'net/http'
2
+
3
+ module KeycloakOauth
4
+ class DuplicationError < StandardError; end
5
+
6
+ class PostUsersService < KeycloakOauth::AuthorizableService
7
+ attr_reader :request_params, :connection, :user_params
8
+
9
+ def initialize(connection:, access_token:, refresh_token:, user_params:)
10
+ @connection = connection
11
+ @access_token = access_token
12
+ @refresh_token = refresh_token
13
+ @user_params = user_params
14
+ end
15
+
16
+ def send_request
17
+ post_users
18
+ end
19
+
20
+ private
21
+
22
+ attr_accessor :access_token, :refresh_token
23
+
24
+ def post_users
25
+ uri = URI.parse(connection.users_endpoint)
26
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
27
+ request = Net::HTTP::Post.new(uri)
28
+ request.set_content_type(CONTENT_TYPE_JSON)
29
+ request[AUTHORIZATION_HEADER] = "Bearer #{access_token}"
30
+ request.body = user_params.to_json
31
+ http.request(request)
32
+ end
33
+ end
34
+
35
+ def parse_response_body(http_response)
36
+ super
37
+ rescue KeycloakOauth::AuthorizableError => exception
38
+ raise exception unless is_exception_a_duplication?(exception)
39
+ raise KeycloakOauth::DuplicationError.new(exception)
40
+ end
41
+
42
+ def is_exception_a_duplication?(exception)
43
+ exception.message == "User exists with same email" ||
44
+ exception.message == "User exists with same username"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,57 @@
1
+ require 'net/http'
2
+
3
+ module KeycloakOauth
4
+ class PutExecuteActionsEmailService < KeycloakOauth::AuthorizableService
5
+ SUPPORTED_QUERY_PARAMS = %i(client_id lifespan redirect_uri)
6
+
7
+ attr_reader :connection, :user_id, :actions, :options
8
+
9
+ def initialize(connection: KeycloakOauth.connection, access_token:, refresh_token:, user_id:, actions:, options: {})
10
+ @connection = connection
11
+ @access_token = access_token
12
+ @refresh_token = refresh_token
13
+ @user_id = user_id
14
+ @actions = actions
15
+ @options = options
16
+ end
17
+
18
+ def send_request
19
+ put_execute_actions_email
20
+ end
21
+
22
+ private
23
+
24
+ attr_accessor :access_token, :refresh_token
25
+
26
+ def put_execute_actions_email
27
+ uri = build_uri
28
+
29
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
30
+ request = Net::HTTP::Put.new(uri)
31
+ request.set_content_type(CONTENT_TYPE_JSON)
32
+ request[AUTHORIZATION_HEADER] = "Bearer #{access_token}"
33
+ request.body = actions.to_json
34
+ http.request(request)
35
+ end
36
+ end
37
+
38
+ def build_uri
39
+ self.class.uri_with_supported_query_params(
40
+ connection.put_execute_actions_email_endpoint(user_id),
41
+ SUPPORTED_QUERY_PARAMS,
42
+ options
43
+ )
44
+ end
45
+
46
+ def parse_response_body(http_response)
47
+ super
48
+ rescue KeycloakOauth::AuthorizableError => exception
49
+ raise exception unless not_found_error?(exception)
50
+ raise KeycloakOauth::NotFoundError.new(exception)
51
+ end
52
+
53
+ def not_found_error?(exception)
54
+ exception.message == "User not found"
55
+ end
56
+ end
57
+ end
@@ -9,8 +9,8 @@ module KeycloakOauth
9
9
  @refresh_token = refresh_token
10
10
  end
11
11
 
12
- def retrieve
13
- @user_information = parsed_response(get_user)
12
+ def send_request
13
+ get_user
14
14
  end
15
15
 
16
16
  private
@@ -21,7 +21,7 @@ module KeycloakOauth
21
21
  uri = URI.parse(KeycloakOauth.connection.user_info_endpoint)
22
22
  Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
23
23
  request = Net::HTTP::Get.new(uri)
24
- request.set_content_type(DEFAULT_CONTENT_TYPE)
24
+ request.set_content_type(CONTENT_TYPE_X_WWW_FORM_URLENCODED)
25
25
  request[AUTHORIZATION_HEADER] = "Bearer #{access_token}"
26
26
  http.request(request)
27
27
  end
@@ -19,13 +19,13 @@ module KeycloakOauth
19
19
  access_token: access_token,
20
20
  refresh_token: refresh_token
21
21
  )
22
- service.retrieve
23
- service.user_information
22
+ service.perform
23
+ service.parsed_response_body
24
24
  end
25
25
 
26
26
  def logout(session:)
27
27
  service = KeycloakOauth::LogoutService.new(session)
28
- service.logout
28
+ service.perform
29
29
  end
30
30
  end
31
31
  end
@@ -20,5 +20,13 @@ module KeycloakOauth
20
20
  def logout_endpoint
21
21
  "#{auth_url}/realms/#{realm}/protocol/openid-connect/logout"
22
22
  end
23
+
24
+ def users_endpoint
25
+ "#{auth_url}/admin/realms/#{realm}/users"
26
+ end
27
+
28
+ def put_execute_actions_email_endpoint(user_id)
29
+ "#{auth_url}/admin/realms/#{realm}/users/#{user_id}/execute-actions-email"
30
+ end
23
31
  end
24
32
  end
@@ -1,3 +1,3 @@
1
1
  module KeycloakOauth
2
- VERSION = "0.1.6"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycloak_oauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - simplificator
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-04 00:00:00.000000000 Z
11
+ date: 2021-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -58,7 +58,7 @@ dependencies:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
60
  version: '0'
61
- description:
61
+ description:
62
62
  email:
63
63
  - dev@simplificator.com
64
64
  executables: []
@@ -70,7 +70,11 @@ files:
70
70
  - app/controllers/keycloak_oauth/callbacks_controller.rb
71
71
  - app/services/keycloak_oauth/authentication_service.rb
72
72
  - app/services/keycloak_oauth/authorizable_service.rb
73
+ - app/services/keycloak_oauth/get_users_service.rb
73
74
  - app/services/keycloak_oauth/logout_service.rb
75
+ - app/services/keycloak_oauth/post_token_service.rb
76
+ - app/services/keycloak_oauth/post_users_service.rb
77
+ - app/services/keycloak_oauth/put_execute_actions_email_service.rb
74
78
  - app/services/keycloak_oauth/user_info_retrieval_service.rb
75
79
  - config/routes.rb
76
80
  - lib/keycloak_oauth.rb
@@ -86,7 +90,7 @@ metadata:
86
90
  homepage_uri: https://rubygems.org/gems/keycloak_oauth
87
91
  source_code_uri: https://github.com/simplificator/keycloak_oauth
88
92
  changelog_uri: https://github.com/simplificator/keycloak_oauth
89
- post_install_message:
93
+ post_install_message:
90
94
  rdoc_options: []
91
95
  require_paths:
92
96
  - lib
@@ -94,15 +98,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
98
  requirements:
95
99
  - - ">="
96
100
  - !ruby/object:Gem::Version
97
- version: 2.3.0
101
+ version: 2.6.0
98
102
  required_rubygems_version: !ruby/object:Gem::Requirement
99
103
  requirements:
100
104
  - - ">="
101
105
  - !ruby/object:Gem::Version
102
106
  version: '0'
103
107
  requirements: []
104
- rubygems_version: 3.0.8
105
- signing_key:
108
+ rubygems_version: 3.1.6
109
+ signing_key:
106
110
  specification_version: 4
107
111
  summary: Implementing OAuth with Keycloak in Ruby
108
112
  test_files: []