keycloak_oauth 0.1.4 → 0.1.9

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: 1700a23b09d7b728852b88e10c29de14be07c00c34a04f9342ec922c16b375b8
4
- data.tar.gz: ae133f07b837f475631206d6dd3e3fbaa1c0807d00adf073e20e178c042e64bd
3
+ metadata.gz: cd57edea266446ba41b1247a566ae9919ee9f5d26c8b4f77a915f568a085542d
4
+ data.tar.gz: 6334660d691bb5aeab2a8a789b7f2d57c4b8ff643ea29fb4a711f968d08bdc62
5
5
  SHA512:
6
- metadata.gz: 3528f127017bc5b92e01327483d7d455007dea1282620eb2b7e769550d63127c3f823104fc0bcf2f10482222ddc45816f6a84488e2dadcc2a864ea0ce1f256ea
7
- data.tar.gz: 7e41f37021b9663a1c3f50e3dc151b5891a23400ac90252c36b91a2869ea1754ee1ae72581d43199b0c217378cbdab39bfca503b88a68044a0e9db7e7a5dd0bb
6
+ metadata.gz: 6b472474aca15f56c3c7bbddbb5c135b11220add4f2cf0e6d52cc8398b5b3b8c755fa4d9517e103a68fe6ce86e676ac12860c077793a3094632e94da4b4a067d
7
+ data.tar.gz: aa7927e0ec6759a835123ae6d3f10ba59469558fe50081d2143c0274a94bfed11543527cc71b286436242e50588a7a5596b4bdad4152c90e80c385833f8a4f84
data/README.md CHANGED
@@ -88,6 +88,22 @@ def map_authenticatable(_request)
88
88
  end
89
89
  ```
90
90
 
91
+ **Logging out**
92
+ In order to log out, you can use the following API call:
93
+ `KeycloakOauth.connection.logout(session: session)`
94
+
95
+ Note that you need to pass in the session, as the gem needs to remove the Keycloak tokens from there.
96
+
97
+ e.g.
98
+ ```ruby
99
+ class SessionsController < ApplicationController
100
+ def destroy
101
+ KeycloakOauth.connection.logout(session: session)
102
+ redirect_to new_session_path
103
+ end
104
+ end
105
+ ```
106
+
91
107
  ## Development
92
108
 
93
109
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -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) 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
@@ -0,0 +1,74 @@
1
+ require 'net/http'
2
+
3
+ module KeycloakOauth
4
+ class AuthorizableError < StandardError; end
5
+ class NotFoundError < StandardError; end
6
+
7
+ class AuthorizableService
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
11
+ AUTHORIZATION_HEADER = 'Authorization'.freeze
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
+
34
+ private
35
+
36
+ def parse_response_body(http_response)
37
+ response_body = http_response.body.present? ? JSON.parse(http_response.body) : http_response.body
38
+
39
+ return response_body if HTTP_SUCCESS_CODES.include?(http_response.code_type)
40
+
41
+ # TODO: For now, we assume that the access token is always valid.
42
+ # We do not yet handle the case where a refresh token is passed in and
43
+ # used if the access token has expired.
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
72
+ end
73
+ end
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
@@ -0,0 +1,44 @@
1
+ require 'net/http'
2
+
3
+ module KeycloakOauth
4
+ class LogoutService < KeycloakOauth::AuthorizableService
5
+ def initialize(session)
6
+ @session = session
7
+ end
8
+
9
+ def send_request
10
+ post_logout
11
+ end
12
+
13
+ private
14
+
15
+ attr_accessor :session
16
+
17
+ def post_logout
18
+ uri = URI.parse(KeycloakOauth.connection.logout_endpoint)
19
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
20
+ request = Net::HTTP::Post.new(uri)
21
+ request.set_content_type(CONTENT_TYPE_X_WWW_FORM_URLENCODED)
22
+ request.set_form_data(logout_request_params)
23
+ request[AUTHORIZATION_HEADER] = "Bearer #{access_token}"
24
+ http.request(request)
25
+ end
26
+ end
27
+
28
+ def logout_request_params
29
+ {
30
+ client_id: KeycloakOauth.connection.client_id,
31
+ client_secret: KeycloakOauth.connection.client_secret,
32
+ refresh_token: refresh_token
33
+ }
34
+ end
35
+
36
+ def access_token
37
+ session[:access_token]
38
+ end
39
+
40
+ def refresh_token
41
+ session[:refresh_token]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
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:, request_params:)
10
+ @connection = connection
11
+ @request_params = request_params
12
+ end
13
+
14
+ def send_request
15
+ post_token
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :code, :redirect_uri
21
+
22
+ def post_token
23
+ uri = URI.parse(connection.authentication_endpoint)
24
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
25
+ request = Net::HTTP::Post.new(uri)
26
+ request.set_content_type(CONTENT_TYPE_X_WWW_FORM_URLENCODED)
27
+ request.set_form_data(token_request_params)
28
+ http.request(request)
29
+ end
30
+ end
31
+
32
+ def token_request_params
33
+ {
34
+ client_id: connection.client_id,
35
+ client_secret: connection.client_secret,
36
+ grant_type: DEFAULT_GRANT_TYPE
37
+ }.merge(request_params)
38
+ end
39
+ end
40
+ 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
@@ -1,45 +1,30 @@
1
1
  require 'net/http'
2
2
 
3
3
  module KeycloakOauth
4
- class UserInfoRetrievalError < StandardError; end
5
-
6
- class UserInfoRetrievalService
7
- AUTHORIZATION_HEADER = 'Authorization'.freeze
8
- CONTENT_TYPE = 'application/x-www-form-urlencoded'.freeze
9
-
4
+ class UserInfoRetrievalService < KeycloakOauth::AuthorizableService
10
5
  attr_reader :user_information
11
6
 
12
- def initialize(access_token:)
7
+ def initialize(access_token:, refresh_token:)
13
8
  @access_token = access_token
9
+ @refresh_token = refresh_token
14
10
  end
15
11
 
16
- def retrieve
17
- @user_information = parsed_user_information(get_user)
12
+ def send_request
13
+ get_user
18
14
  end
19
15
 
20
16
  private
21
17
 
22
- attr_accessor :access_token
18
+ attr_accessor :access_token, :refresh_token
23
19
 
24
20
  def get_user
25
21
  uri = URI.parse(KeycloakOauth.connection.user_info_endpoint)
26
- Net::HTTP.start(uri.host, uri.port) do |http|
22
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
27
23
  request = Net::HTTP::Get.new(uri)
28
- request.set_content_type(CONTENT_TYPE)
24
+ request.set_content_type(CONTENT_TYPE_X_WWW_FORM_URLENCODED)
29
25
  request[AUTHORIZATION_HEADER] = "Bearer #{access_token}"
30
26
  http.request(request)
31
27
  end
32
28
  end
33
-
34
- def parsed_user_information(http_response)
35
- response_hash = JSON.parse(http_response.body)
36
-
37
- return response_hash if http_response.code_type == Net::HTTPOK
38
-
39
- # TODO: For now, we assume that the access token is always valid.
40
- # We do not yet handle the case where a refresh token is passed in and
41
- # used if the access token has expired.
42
- raise KeycloakOauth::UserInfoRetrievalError.new(response_hash)
43
- end
44
29
  end
45
30
  end
@@ -14,10 +14,18 @@ module KeycloakOauth
14
14
  @callback_module = callback_module
15
15
  end
16
16
 
17
- def get_user_information(access_token:)
18
- service = KeycloakOauth::UserInfoRetrievalService.new(access_token: access_token)
19
- service.retrieve
20
- service.user_information
17
+ def get_user_information(access_token:, refresh_token:)
18
+ service = KeycloakOauth::UserInfoRetrievalService.new(
19
+ access_token: access_token,
20
+ refresh_token: refresh_token
21
+ )
22
+ service.perform
23
+ service.parsed_response_body
24
+ end
25
+
26
+ def logout(session:)
27
+ service = KeycloakOauth::LogoutService.new(session)
28
+ service.perform
21
29
  end
22
30
  end
23
31
  end
@@ -16,5 +16,17 @@ module KeycloakOauth
16
16
  def user_info_endpoint
17
17
  "#{auth_url}/realms/#{realm}/protocol/openid-connect/userinfo"
18
18
  end
19
+
20
+ def logout_endpoint
21
+ "#{auth_url}/realms/#{realm}/protocol/openid-connect/logout"
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
19
31
  end
20
32
  end
@@ -1,3 +1,3 @@
1
1
  module KeycloakOauth
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.9"
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.4
4
+ version: 0.1.9
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-02 00:00:00.000000000 Z
11
+ date: 2020-11-24 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: []
@@ -69,6 +69,12 @@ files:
69
69
  - Rakefile
70
70
  - app/controllers/keycloak_oauth/callbacks_controller.rb
71
71
  - app/services/keycloak_oauth/authentication_service.rb
72
+ - app/services/keycloak_oauth/authorizable_service.rb
73
+ - app/services/keycloak_oauth/get_users_service.rb
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
72
78
  - app/services/keycloak_oauth/user_info_retrieval_service.rb
73
79
  - config/routes.rb
74
80
  - lib/keycloak_oauth.rb
@@ -84,7 +90,7 @@ metadata:
84
90
  homepage_uri: https://rubygems.org/gems/keycloak_oauth
85
91
  source_code_uri: https://github.com/simplificator/keycloak_oauth
86
92
  changelog_uri: https://github.com/simplificator/keycloak_oauth
87
- post_install_message:
93
+ post_install_message:
88
94
  rdoc_options: []
89
95
  require_paths:
90
96
  - lib
@@ -99,8 +105,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
105
  - !ruby/object:Gem::Version
100
106
  version: '0'
101
107
  requirements: []
102
- rubygems_version: 3.0.8
103
- signing_key:
108
+ rubygems_version: 3.1.4
109
+ signing_key:
104
110
  specification_version: 4
105
111
  summary: Implementing OAuth with Keycloak in Ruby
106
112
  test_files: []