keycloak_oauth 0.1.2 → 0.1.7

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: 36235d5a3f0e96e1b1ddac55c9825545ee11244ec785fff42c30029ee1ba33eb
4
- data.tar.gz: 7778aeab5443dcaa32dd301e2fe45a0b4c099e0ea9cd6fe289b6d2de8f1dc113
3
+ metadata.gz: 0121cf149dea17f792d3f007cfb5b55247e8e886c9a5d53ded44db08c0b7ebaa
4
+ data.tar.gz: c9a3cba0eb30bba3684be95b5270df88156d90a90b72889c3c6dc29c5b816bd7
5
5
  SHA512:
6
- metadata.gz: 4284790fc0cf71a7dd853d7e45f2b894500ff485b1e502857801ce5b453abb5658db95f078bb4c32b132a3f075e7ca7460b42093583a9eeba688cb3ebf1f6cf7
7
- data.tar.gz: 8d81ce99e61b047f6c1cd426d73e700b8c27dd9361ae5639994932e5b3c2ed06ba844fea3c5bf69ac1ca92a0206c19154fbe0a42e34a72bae9e07f65acba3b60
6
+ metadata.gz: 1db6f2a45c114e3360951aaa73a663364f94425f731c54581b8e0d713117b9cb8efa89dc8826962c58d2b578f581d6202bbb499249085d6d657c2f54cc1df061
7
+ data.tar.gz: 436dad25dd2d4987adb0abed086d23bc08a50e197d036b37df913414e1fe1bc84fddabc3e56532dadbb5d916050291be7d63266c28e7bac34ee798e2d0be24f1
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).
@@ -65,6 +75,35 @@ KeycloakOauth.configure do |config|
65
75
  end
66
76
  ```
67
77
 
78
+ **User mapping**
79
+ The host app is responsible for mapping a Keycloak session with a Rails user session. This can be achieved
80
+ by implementing the `map_authenticatable` method in the module configured above (e.g. `KeycloakOauthCallbacks` in our example).
81
+ You can get the user information by making a call to `KeycloakOauth.connection.get_user_information` to which you pass in the access token.
82
+ See here an example of retrieving the user information and saving the email address in the Rails session:
83
+
84
+ ```ruby
85
+ def map_authenticatable(_request)
86
+ service = KeycloakOauth.connection.get_user_information(access_token: session[:access_token])
87
+ session[:user_email_address] = service.user_information['email']
88
+ end
89
+ ```
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
+
68
107
  ## Development
69
108
 
70
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
@@ -7,9 +7,11 @@ module KeycloakOauth
7
7
  def oauth2
8
8
  authentication_service = KeycloakOauth::AuthenticationService.new(
9
9
  authentication_params: authentication_params,
10
- session: session
10
+ session: session,
11
+ redirect_uri: current_uri_without_params
11
12
  )
12
13
  authentication_service.authenticate
14
+ map_authenticatable_if_implemented(session)
13
15
 
14
16
  redirect_to self.class.method_defined?(:after_sign_in_path) ? after_sign_in_path(request) : '/'
15
17
  end
@@ -19,5 +21,23 @@ module KeycloakOauth
19
21
  def authentication_params
20
22
  params.permit(:code)
21
23
  end
24
+
25
+ def map_authenticatable_if_implemented(request)
26
+ if self.class.method_defined?(:map_authenticatable)
27
+ map_authenticatable(request)
28
+ else
29
+ raise NotImplementedError.new('User mapping must be handled by the host app. See README for more information.')
30
+ end
31
+ end
32
+
33
+ def current_uri_without_params
34
+ # If the host app has overwritten the route (e.g. to enable localised
35
+ # callbacks), this ensures we are using the path coming from the host app
36
+ # instead of the one coming from the engine.
37
+ main_app.url_for(only_path: false, overwrite_params: nil)
38
+ rescue ActionController::UrlGenerationError
39
+ # If the host app does not override the oauth2 path, use the engine's path.
40
+ oauth2_path
41
+ end
22
42
  end
23
43
  end
@@ -9,11 +9,12 @@ module KeycloakOauth
9
9
  ACCESS_TOKEN_KEY = 'access_token'.freeze
10
10
  REFRESH_TOKEN_KEY = 'refresh_token'.freeze
11
11
 
12
- attr_reader :code, :session
12
+ attr_reader :session
13
13
 
14
- def initialize(authentication_params:, session:)
14
+ def initialize(authentication_params:, session:, redirect_uri:)
15
15
  @code = authentication_params[:code]
16
16
  @session = session
17
+ @redirect_uri = redirect_uri
17
18
  end
18
19
 
19
20
  def authenticate
@@ -22,9 +23,11 @@ module KeycloakOauth
22
23
 
23
24
  private
24
25
 
26
+ attr_reader :code, :redirect_uri
27
+
25
28
  def get_tokens
26
29
  uri = URI.parse(KeycloakOauth.connection.authentication_endpoint)
27
- Net::HTTP.start(uri.host, uri.port) do |http|
30
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
28
31
  request = Net::HTTP::Post.new(uri)
29
32
  request.set_content_type(CONTENT_TYPE)
30
33
  request.set_form_data(token_request_params)
@@ -37,7 +40,8 @@ module KeycloakOauth
37
40
  client_id: KeycloakOauth.connection.client_id,
38
41
  client_secret: KeycloakOauth.connection.client_secret,
39
42
  grant_type: GRANT_TYPE,
40
- code: code
43
+ code: code,
44
+ redirect_uri: redirect_uri
41
45
  }
42
46
  end
43
47
 
@@ -0,0 +1,24 @@
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]
8
+ DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded'.freeze
9
+ AUTHORIZATION_HEADER = 'Authorization'.freeze
10
+
11
+ private
12
+
13
+ def parsed_response(http_response)
14
+ response = http_response.body.present? ? JSON.parse(http_response.body) : http_response.body
15
+
16
+ return response if HTTP_SUCCESS_CODES.include?(http_response.code_type)
17
+
18
+ # TODO: For now, we assume that the access token is always valid.
19
+ # We do not yet handle the case where a refresh token is passed in and
20
+ # used if the access token has expired.
21
+ raise KeycloakOauth::AuthorizableError.new(response)
22
+ end
23
+ end
24
+ 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 logout
10
+ parsed_response(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,30 @@
1
+ require 'net/http'
2
+
3
+ module KeycloakOauth
4
+ class UserInfoRetrievalService < KeycloakOauth::AuthorizableService
5
+ attr_reader :user_information
6
+
7
+ def initialize(access_token:, refresh_token:)
8
+ @access_token = access_token
9
+ @refresh_token = refresh_token
10
+ end
11
+
12
+ def retrieve
13
+ @user_information = parsed_response(get_user)
14
+ end
15
+
16
+ private
17
+
18
+ attr_accessor :access_token, :refresh_token
19
+
20
+ def get_user
21
+ uri = URI.parse(KeycloakOauth.connection.user_info_endpoint)
22
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
23
+ request = Net::HTTP::Get.new(uri)
24
+ request.set_content_type(DEFAULT_CONTENT_TYPE)
25
+ request[AUTHORIZATION_HEADER] = "Bearer #{access_token}"
26
+ http.request(request)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,8 +1,10 @@
1
+ require 'keycloak_oauth/endpoints'
2
+
1
3
  module KeycloakOauth
2
4
  class Connection
3
- attr_reader :auth_url, :realm, :client_id, :client_secret, :callback_module
5
+ include KeycloakOauth::Endpoints
4
6
 
5
- DEFAULT_RESPONSE_TYPE = 'code'.freeze
7
+ attr_reader :auth_url, :realm, :client_id, :client_secret, :callback_module
6
8
 
7
9
  def initialize(auth_url:, realm:, client_id:, client_secret:, callback_module: nil)
8
10
  @auth_url = auth_url
@@ -12,13 +14,18 @@ module KeycloakOauth
12
14
  @callback_module = callback_module
13
15
  end
14
16
 
15
- def authorization_endpoint(options: {})
16
- endpoint = "#{auth_url}/realms/#{realm}/protocol/openid-connect/auth?client_id=#{client_id}"
17
- endpoint += "&response_type=#{options[:response_type] || DEFAULT_RESPONSE_TYPE}"
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.retrieve
23
+ service.user_information
18
24
  end
19
25
 
20
- def authentication_endpoint
21
- "#{auth_url}/realms/#{realm}/protocol/openid-connect/token"
26
+ def logout(session:)
27
+ service = KeycloakOauth::LogoutService.new(session)
28
+ service.logout
22
29
  end
23
30
  end
24
31
  end
@@ -0,0 +1,24 @@
1
+ module KeycloakOauth
2
+ module Endpoints
3
+ DEFAULT_RESPONSE_TYPE = 'code'.freeze
4
+
5
+ def authorization_endpoint(options: {})
6
+ endpoint = "#{auth_url}/realms/#{realm}/protocol/openid-connect/auth?client_id=#{client_id}"
7
+ endpoint += "&response_type=#{options[:response_type] || DEFAULT_RESPONSE_TYPE}"
8
+ endpoint += "&redirect_uri=#{options[:redirect_uri]}" if options[:redirect_uri].present?
9
+ endpoint
10
+ end
11
+
12
+ def authentication_endpoint
13
+ "#{auth_url}/realms/#{realm}/protocol/openid-connect/token"
14
+ end
15
+
16
+ def user_info_endpoint
17
+ "#{auth_url}/realms/#{realm}/protocol/openid-connect/userinfo"
18
+ end
19
+
20
+ def logout_endpoint
21
+ "#{auth_url}/realms/#{realm}/protocol/openid-connect/logout"
22
+ end
23
+ end
24
+ end
@@ -1,3 +1,3 @@
1
1
  module KeycloakOauth
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.7"
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.2
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - simplificator
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-27 00:00:00.000000000 Z
11
+ date: 2020-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: 6.0.3
20
20
  - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: 6.0.3.3
22
+ version: 6.0.3.2
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: 6.0.3
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 6.0.3.3
32
+ version: 6.0.3.2
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: rspec-rails
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -67,13 +67,16 @@ 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/user_info_retrieval_service.rb
73
75
  - config/routes.rb
74
76
  - lib/keycloak_oauth.rb
75
77
  - lib/keycloak_oauth/configuration.rb
76
78
  - lib/keycloak_oauth/connection.rb
79
+ - lib/keycloak_oauth/endpoints.rb
77
80
  - lib/keycloak_oauth/engine.rb
78
81
  - lib/keycloak_oauth/version.rb
79
82
  homepage: https://rubygems.org/gems/keycloak_oauth
@@ -1,5 +0,0 @@
1
- module KeycloakOauth
2
- class ApplicationController < ActionController::Base
3
- protect_from_forgery with: :exception
4
- end
5
- end