keycloak_oauth 0.1.3 → 0.1.8

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: 45b0a220afa0ca575a22ed7fd5cf958dc4d7ad2450a116f3bdf7e5e3546d6eac
4
- data.tar.gz: eeb8fcb58d29e1cf18d4cf3fadddbf184029576b97bb9194e4218f4889636f05
3
+ metadata.gz: 5b84ab145e8572eebc11e31faff2080a82ec5f157fe5579c7076f32149f4b2b7
4
+ data.tar.gz: '0308c1a7e95dbd136e1cb1c659d203b355d78faecfefa4bd56d31fc222040245'
5
5
  SHA512:
6
- metadata.gz: 58a27c6c046d783c0f83db3a8857d06aab9402e85d00a6d880eb0ad637d2b5f8a4e4c4e4df03065f03097bfda98439f735b8257d10c52046be85d0c210aeef3e
7
- data.tar.gz: dc6cc4e9a90c8f78a09e3b2b90d19ca7973aea125678170ba2be0902d490eba1ff17cbcfcce9a22e78f9b85324f3bce77f5bb878a722dfc8113cf72b01505581
6
+ metadata.gz: a9c847ab0c59e5d6c4d28c51bbc5ad8ae515874cd680b312b31e264edc97748e813a319eb83f51c75ee463edaa02a9eba65ad6a444b82c59bc805b1a6eb1cfbf
7
+ data.tar.gz: fab61c415c9a09a4cc66a6e9986998dfc1db3c126205698938ef4f76c59a3571d1078f74456ab97201f7fa578b323430767fedd5687315a6233404f3610fb01d
data/README.md CHANGED
@@ -44,6 +44,16 @@ e.g.
44
44
 
45
45
  Once authentication is performed, the access and refresh tokens are stored in the session and can be used in your app as wished.
46
46
 
47
+ ***Customising redirect URIs***
48
+ There are situations where you would want to customise the oauth2 route (e.g. to use a localised version of the callback URL).
49
+ In this case, you can do the following:
50
+ - add a controller to your app: e.g. `CallbackOverrides`
51
+ - add the following to your routes.rb file: `get 'oauth2', to: 'callback_overrides#oauth2'`
52
+ - add whatever logic you need in the controller, e.g. a `skip_before_action`; it can also be blank
53
+ - add redirect URI to the authorization link:
54
+ e.g.
55
+ `<%= link_to 'Login with Keycloak', KeycloakOauth.connection.authorization_endpoint(options: {redirect_uri: 'http://myapp.com/en/oauth2'}) %>`
56
+
47
57
  **Keycloak callback URL**
48
58
  Keycloak needs a callback URL to send the authorization code to once a user logs in.
49
59
  By default, once authentication is performed, we redirect to the `/` path (i.e. whatever the root path is set to in the host app).
@@ -78,6 +88,22 @@ def map_authenticatable(_request)
78
88
  end
79
89
  ```
80
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
+
81
107
  ## Development
82
108
 
83
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.
@@ -1,5 +1,5 @@
1
1
  module KeycloakOauth
2
- class CallbacksController < ApplicationController
2
+ class CallbacksController < ::ApplicationController
3
3
  if KeycloakOauth.connection.callback_module.present?
4
4
  include KeycloakOauth.connection.callback_module
5
5
  end
@@ -18,7 +18,7 @@ module KeycloakOauth
18
18
  private
19
19
 
20
20
  def authentication_params
21
- params.permit(:code)
21
+ params.permit(:code).merge({ redirect_uri: current_uri_without_params })
22
22
  end
23
23
 
24
24
  def map_authenticatable_if_implemented(request)
@@ -28,5 +28,15 @@ module KeycloakOauth
28
28
  raise NotImplementedError.new('User mapping must be handled by the host app. See README for more information.')
29
29
  end
30
30
  end
31
+
32
+ def current_uri_without_params
33
+ # If the host app has overwritten the route (e.g. to enable localised
34
+ # callbacks), this ensures we are using the path coming from the host app
35
+ # instead of the one coming from the engine.
36
+ main_app.url_for(only_path: false, overwrite_params: nil)
37
+ rescue ActionController::UrlGenerationError
38
+ # If the host app does not override the oauth2 path, use the engine's path.
39
+ oauth2_path
40
+ end
31
41
  end
32
42
  end
@@ -1,55 +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 :code, :session
6
+ attr_reader :session, :authentication_params
13
7
 
14
8
  def initialize(authentication_params:, session:)
15
- @code = authentication_params[:code]
9
+ @authentication_params = authentication_params
16
10
  @session = session
17
11
  end
18
12
 
19
13
  def authenticate
20
- 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)
21
20
  end
22
21
 
23
22
  private
24
23
 
25
- def get_tokens
26
- uri = URI.parse(KeycloakOauth.connection.authentication_endpoint)
27
- Net::HTTP.start(uri.host, uri.port) do |http|
28
- request = Net::HTTP::Post.new(uri)
29
- request.set_content_type(CONTENT_TYPE)
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: KeycloakOauth.connection.client_id,
38
- client_secret: KeycloakOauth.connection.client_secret,
39
- grant_type: GRANT_TYPE,
40
- code: code
41
- }
42
- end
43
-
44
- def store_credentials(http_response)
45
- response_hash = JSON.parse(http_response.body)
24
+ def store_credentials(post_token_service)
25
+ response_hash = post_token_service.parsed_response_body
46
26
 
47
- if http_response.code_type == Net::HTTPOK
48
- session[:access_token] = response_hash[ACCESS_TOKEN_KEY]
49
- session[:refresh_token] = response_hash[REFRESH_TOKEN_KEY]
50
- else
51
- raise KeycloakOauth::AuthenticationError.new(response_hash)
52
- end
27
+ session[:access_token] = response_hash[ACCESS_TOKEN_KEY]
28
+ session[:refresh_token] = response_hash[REFRESH_TOKEN_KEY]
53
29
  end
54
30
  end
55
31
  end
@@ -0,0 +1,50 @@
1
+ require 'net/http'
2
+
3
+ module KeycloakOauth
4
+ class AuthorizableError < StandardError; end
5
+
6
+ class AuthorizableService
7
+ HTTP_SUCCESS_CODES = [Net::HTTPOK, Net::HTTPNoContent, Net::HTTPCreated]
8
+ DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded'.freeze
9
+ AUTHORIZATION_HEADER = 'Authorization'.freeze
10
+
11
+ attr_reader :http_response, :parsed_response_body
12
+
13
+ def perform
14
+ @http_response = send_request
15
+ @parsed_response_body ||= parse_response_body(http_response)
16
+ end
17
+
18
+ private
19
+
20
+ def parse_response_body(http_response)
21
+ response_body = http_response.body.present? ? JSON.parse(http_response.body) : http_response.body
22
+
23
+ return response_body if HTTP_SUCCESS_CODES.include?(http_response.code_type)
24
+
25
+ # TODO: For now, we assume that the access token is always valid.
26
+ # We do not yet handle the case where a refresh token is passed in and
27
+ # used if the access token has expired.
28
+ raise KeycloakOauth::AuthorizableError.new(error_message_from(response_body))
29
+ end
30
+
31
+ def error_message_from(response)
32
+ # Keycloak sometimes sends back a hash containing the "errorMessage" key,
33
+ # other times it returns a hash containing the "error_description key" key
34
+ # and other times it returns an empty string. There could be more cases,
35
+ # but for the moment we are only handling these three.
36
+ case response.class.to_s
37
+ when 'Hash'
38
+ if response.has_key?('errorMessage')
39
+ return response['errorMessage']
40
+ elsif response.has_key?('error_description')
41
+ return response['error_description']
42
+ end
43
+ when 'String'
44
+ return response
45
+ else
46
+ 'Unexpected Keycloak error'
47
+ end
48
+ end
49
+ end
50
+ 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(DEFAULT_CONTENT_TYPE)
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(DEFAULT_CONTENT_TYPE)
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,49 @@
1
+ require 'net/http'
2
+
3
+ module KeycloakOauth
4
+ class DuplicationError < StandardError; end
5
+
6
+ class PostUsersService < KeycloakOauth::AuthorizableService
7
+ CONTENT_TYPE = 'application/json'.freeze
8
+
9
+ attr_reader :request_params, :connection, :user_params
10
+
11
+ def initialize(connection:, access_token:, refresh_token:, user_params:)
12
+ @connection = connection
13
+ @access_token = access_token
14
+ @refresh_token = refresh_token
15
+ @user_params = user_params
16
+ end
17
+
18
+ def send_request
19
+ post_users
20
+ end
21
+
22
+ private
23
+
24
+ attr_accessor :access_token, :refresh_token
25
+
26
+ def post_users
27
+ uri = URI.parse(connection.post_users_endpoint)
28
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
29
+ request = Net::HTTP::Post.new(uri)
30
+ request.set_content_type(CONTENT_TYPE)
31
+ request[AUTHORIZATION_HEADER] = "Bearer #{access_token}"
32
+ request.body = user_params.to_json
33
+ http.request(request)
34
+ end
35
+ end
36
+
37
+ def parse_response_body(http_response)
38
+ super
39
+ rescue KeycloakOauth::AuthorizableError => exception
40
+ raise exception unless is_exception_a_duplication?(exception)
41
+ raise KeycloakOauth::DuplicationError.new(exception)
42
+ end
43
+
44
+ def is_exception_a_duplication?(exception)
45
+ exception.message == "User exists with same email" ||
46
+ exception.message == "User exists with same username"
47
+ end
48
+ end
49
+ 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(DEFAULT_CONTENT_TYPE)
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
@@ -5,6 +5,8 @@ module KeycloakOauth
5
5
  def authorization_endpoint(options: {})
6
6
  endpoint = "#{auth_url}/realms/#{realm}/protocol/openid-connect/auth?client_id=#{client_id}"
7
7
  endpoint += "&response_type=#{options[:response_type] || DEFAULT_RESPONSE_TYPE}"
8
+ endpoint += "&redirect_uri=#{options[:redirect_uri]}" if options[:redirect_uri].present?
9
+ endpoint
8
10
  end
9
11
 
10
12
  def authentication_endpoint
@@ -14,5 +16,13 @@ module KeycloakOauth
14
16
  def user_info_endpoint
15
17
  "#{auth_url}/realms/#{realm}/protocol/openid-connect/userinfo"
16
18
  end
19
+
20
+ def logout_endpoint
21
+ "#{auth_url}/realms/#{realm}/protocol/openid-connect/logout"
22
+ end
23
+
24
+ def post_users_endpoint
25
+ "#{auth_url}/admin/realms/#{realm}/users"
26
+ end
17
27
  end
18
28
  end
@@ -1,3 +1,3 @@
1
1
  module KeycloakOauth
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.8"
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.3
4
+ version: 0.1.8
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-10-27 00:00:00.000000000 Z
11
+ date: 2020-11-17 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: []
@@ -67,9 +67,12 @@ extra_rdoc_files: []
67
67
  files:
68
68
  - README.md
69
69
  - Rakefile
70
- - app/controllers/keycloak_oauth/application_controller.rb
71
70
  - app/controllers/keycloak_oauth/callbacks_controller.rb
72
71
  - app/services/keycloak_oauth/authentication_service.rb
72
+ - app/services/keycloak_oauth/authorizable_service.rb
73
+ - app/services/keycloak_oauth/logout_service.rb
74
+ - app/services/keycloak_oauth/post_token_service.rb
75
+ - app/services/keycloak_oauth/post_users_service.rb
73
76
  - app/services/keycloak_oauth/user_info_retrieval_service.rb
74
77
  - config/routes.rb
75
78
  - lib/keycloak_oauth.rb
@@ -85,7 +88,7 @@ metadata:
85
88
  homepage_uri: https://rubygems.org/gems/keycloak_oauth
86
89
  source_code_uri: https://github.com/simplificator/keycloak_oauth
87
90
  changelog_uri: https://github.com/simplificator/keycloak_oauth
88
- post_install_message:
91
+ post_install_message:
89
92
  rdoc_options: []
90
93
  require_paths:
91
94
  - lib
@@ -100,8 +103,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
103
  - !ruby/object:Gem::Version
101
104
  version: '0'
102
105
  requirements: []
103
- rubygems_version: 3.0.8
104
- signing_key:
106
+ rubygems_version: 3.1.4
107
+ signing_key:
105
108
  specification_version: 4
106
109
  summary: Implementing OAuth with Keycloak in Ruby
107
110
  test_files: []
@@ -1,5 +0,0 @@
1
- module KeycloakOauth
2
- class ApplicationController < ActionController::Base
3
- protect_from_forgery with: :exception
4
- end
5
- end