From 7877c8a75f9e3f342b49bf808fa69965377d60b5 Mon Sep 17 00:00:00 2001 From: Cameron Crockett Date: Sun, 16 Dec 2018 15:45:33 -0600 Subject: [PATCH] Initial commit for keycloak - opend id support --- .gitignore | 11 +++ .rspec | 3 + .travis.yml | 7 ++ .vscode/settings.json | 3 + CODE_OF_CONDUCT.md | 74 +++++++++++++++++ Gemfile | 6 ++ Gemfile.lock | 96 ++++++++++++++++++++++ LICENSE.txt | 21 +++++ README.md | 42 ++++++++++ Rakefile | 6 ++ lib/keycloak/version.rb | 5 ++ lib/omniauth-keycloak.rb | 2 + lib/omniauth/strategies/keycloak-openid.rb | 77 +++++++++++++++++ omniauth-keycloak.gemspec | 41 +++++++++ spec/omniauth/strategies/keycloak_spec.rb | 49 +++++++++++ spec/spec_helper.rb | 27 ++++++ 16 files changed, 470 insertions(+) create mode 100755 .gitignore create mode 100755 .rspec create mode 100755 .travis.yml create mode 100644 .vscode/settings.json create mode 100755 CODE_OF_CONDUCT.md create mode 100755 Gemfile create mode 100755 Gemfile.lock create mode 100755 LICENSE.txt create mode 100755 README.md create mode 100755 Rakefile create mode 100755 lib/keycloak/version.rb create mode 100755 lib/omniauth-keycloak.rb create mode 100755 lib/omniauth/strategies/keycloak-openid.rb create mode 100755 omniauth-keycloak.gemspec create mode 100755 spec/omniauth/strategies/keycloak_spec.rb create mode 100755 spec/spec_helper.rb diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..b04a8c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ + +# rspec failure tracking +.rspec_status diff --git a/.rspec b/.rspec new file mode 100755 index 0000000..34c5164 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/.travis.yml b/.travis.yml new file mode 100755 index 0000000..fbf48da --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +--- +sudo: false +language: ruby +cache: bundler +rvm: + - 2.5.1 +before_install: gem install bundler -v 1.16.5 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ff30c44 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.tabSize": 2 +} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100755 index 0000000..53e3e5d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at cameron.crockett@abcorp.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Gemfile b/Gemfile new file mode 100755 index 0000000..ff12d56 --- /dev/null +++ b/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +# Specify your gem's dependencies in omniauth-keycloak.gemspec +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100755 index 0000000..0934c8c --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,96 @@ +PATH + remote: . + specs: + omniauth-keycloak (1.0.0) + json-jwt (~> 1.9.4) + omniauth (~> 1.8.1) + omniauth-oauth2 (~> 1.5.0) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (5.2.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + aes_key_wrap (1.0.1) + bindata (2.4.4) + concurrent-ruby (1.0.5) + crack (0.4.3) + safe_yaml (~> 1.0.0) + diff-lcs (1.3) + docile (1.3.1) + faraday (0.15.3) + multipart-post (>= 1.2, < 3) + hashdiff (0.3.7) + hashie (3.5.7) + i18n (1.1.1) + concurrent-ruby (~> 1.0) + json (2.1.0) + json-jwt (1.9.4) + activesupport + aes_key_wrap + bindata + jwt (2.1.0) + minitest (5.11.3) + multi_json (1.13.1) + multi_xml (0.6.0) + multipart-post (2.0.0) + oauth2 (1.4.1) + faraday (>= 0.8, < 0.16.0) + jwt (>= 1.0, < 3.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) + omniauth (1.8.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) + omniauth-oauth2 (1.5.0) + oauth2 (~> 1.1) + omniauth (~> 1.2) + public_suffix (3.0.3) + rack (2.0.5) + rake (10.5.0) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-core (3.8.0) + rspec-support (~> 3.8.0) + rspec-expectations (3.8.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + safe_yaml (1.0.4) + simplecov (0.16.1) + docile (~> 1.1) + json (>= 1.8, < 3) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.2) + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) + webmock (3.4.2) + addressable (>= 2.3.6) + crack (>= 0.3.2) + hashdiff + +PLATFORMS + ruby + +DEPENDENCIES + bundler (~> 1.16) + omniauth-keycloak! + rake (~> 10.0) + rspec (~> 3.0) + simplecov (~> 0.16.1) + webmock (~> 3.4.2) + +BUNDLED WITH + 1.16.5 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100755 index 0000000..5c25395 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Cameron Crockett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100755 index 0000000..5a824ae --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Omniauth::Keycloak + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'omniauth-keycloak' +``` + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install omniauth-keycloak + +## Usage + +`OmniAuth::Strategies::Keycloak` is simply a Rack middleware. Read the OmniAuth docs for detailed instructions: https://github.com/intridea/omniauth. + +Here's a quick example, adding the middleware to a Rails app in `config/initializers/omniauth.rb`: + +```ruby +Rails.application.config.middleware.use OmniAuth::Builder do + provider :keycloak_openid, 'Example-Client', '19cca35f-dddd-473a-bdd5-03f00d61d884', + client_options: {site: 'https://example.keycloak-url.com', realm: 'example-realm'} +end +``` + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/ccrockett/omniauth-keycloak. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. + +## License + +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). + +## Code of Conduct + +Everyone interacting in the Omniauth::Keycloak project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ccrockett/omniauth-keycloak/blob/master/CODE_OF_CONDUCT.md). diff --git a/Rakefile b/Rakefile new file mode 100755 index 0000000..b7e9ed5 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +task :default => :spec diff --git a/lib/keycloak/version.rb b/lib/keycloak/version.rb new file mode 100755 index 0000000..5f4c5bf --- /dev/null +++ b/lib/keycloak/version.rb @@ -0,0 +1,5 @@ +module Omniauth + module Keycloak + VERSION = "1.0.0" + end +end diff --git a/lib/omniauth-keycloak.rb b/lib/omniauth-keycloak.rb new file mode 100755 index 0000000..ff0df4b --- /dev/null +++ b/lib/omniauth-keycloak.rb @@ -0,0 +1,2 @@ +require "keycloak/version" +require "omniauth/strategies/keycloak-openid" diff --git a/lib/omniauth/strategies/keycloak-openid.rb b/lib/omniauth/strategies/keycloak-openid.rb new file mode 100755 index 0000000..4dd2c4f --- /dev/null +++ b/lib/omniauth/strategies/keycloak-openid.rb @@ -0,0 +1,77 @@ +require 'omniauth' +require 'omniauth-oauth2' +require 'json/jwt' + +module OmniAuth + module Strategies + class KeycloakOpenId < OmniAuth::Strategies::OAuth2 + attr_reader :authorize_url + attr_reader :token_url + attr_reader :cert + + def setup_phase + if @authorize_url.nil? || @token_url.nil? + realm = options.client_options[:realm].nil? ? options.client_id : options.client_options[:realm] + site = options.client_options[:site] + response = Faraday.get "#{options.client_options[:site]}/auth/realms/#{realm}/.well-known/openid-configuration" + if (response.status == 200) + json = MultiJson.load(response.body) + @certs_endpoint = json["jwks_uri"] + @userinfo_endpoint = json["userinfo_endpoint"] + @authorize_url = json["authorization_endpoint"].gsub(site, "") + @token_url = json["token_endpoint"].gsub(site, "") + options.client_options.merge!({ + authorize_url: @authorize_url, + token_url: @token_url + }) + certs = Faraday.get @certs_endpoint + if (certs.status == 200) + json = MultiJson.load(certs.body) + @cert = json["keys"][0] + else + #TODO: Throw Error + puts "Couldn't get Cert" + end + else + #TODO: Throw Error + puts response.status + end + end + end + + def build_access_token + verifier = request.params["code"] + client.auth_code.get_token(verifier, + {:redirect_uri => callback_url.gsub(/\?.+\Z/, "")} + .merge(token_params.to_hash(:symbolize_keys => true)), + deep_symbolize(options.auth_token_params)) + end + + uid{ raw_info['sub'] } + + info do + { + :name => raw_info['name'], + :email => raw_info['email'], + :first_name => raw_info['given_name'], + :last_name => raw_info['family_name'] + } + end + + extra do + { + 'raw_info' => raw_info + } + end + + def raw_info + id_token_string = access_token.token + jwk = JSON::JWK.new(@cert) + id_token = JSON::JWT.decode id_token_string, jwk + id_token + end + + OmniAuth.config.add_camelization('keycloak_openid', 'KeycloakOpenId') + end + end +end \ No newline at end of file diff --git a/omniauth-keycloak.gemspec b/omniauth-keycloak.gemspec new file mode 100755 index 0000000..ba988ee --- /dev/null +++ b/omniauth-keycloak.gemspec @@ -0,0 +1,41 @@ +require File.expand_path("../lib/keycloak/version", __FILE__) +Gem::Specification.new do |spec| + spec.name = "omniauth-keycloak" + spec.version = Omniauth::Keycloak::VERSION + spec.authors = ["Cameron Crockett"] + spec.email = ["cameron.crockett@ccrockett.com"] + + spec.description = %q{"Omniauth strategy for Keycloak"} + spec.summary = spec.description + spec.homepage = "https://github.com/ccrockett/omniauth-keycloak" + spec.license = "MIT" + + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' + # to allow pushing to a single host or delete this section to allow pushing to any host. + if spec.respond_to?(:metadata) + spec.metadata["allowed_push_host"] = "http://gitlab.customcardsystems.com" + else + raise "RubyGems 2.0 or newer is required to protect against " \ + "public gem pushes." + end + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^lib/}) } + end + + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + + spec.add_dependency "omniauth", "~> 1.8.1" + spec.add_dependency "omniauth-oauth2", "~> 1.5.0" + spec.add_dependency "json-jwt", "~> 1.9.4" + spec.add_development_dependency "bundler", "~> 1.16" + spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "rspec", "~> 3.0" + spec.add_development_dependency 'simplecov', '~> 0.16.1' + spec.add_development_dependency 'webmock', '~> 3.4.2' +end diff --git a/spec/omniauth/strategies/keycloak_spec.rb b/spec/omniauth/strategies/keycloak_spec.rb new file mode 100755 index 0000000..cf09947 --- /dev/null +++ b/spec/omniauth/strategies/keycloak_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +RSpec.describe OmniAuth::Strategies::KeycloakOpenId do + body = '{"issuer": "http://localhost:8080/auth/realms/example-realm", + "authorization_endpoint": "http://localhost:8080/auth/realms/example-realm/protocol/openid-connect/auth", + "token_endpoint": "http://localhost:8080/auth/realms/example-realm/protocol/openid-connect/token", + "token_introspection_endpoint": "http://localhost:8080/auth/realms/example-realm/protocol/openid-connect/token/introspect", + "userinfo_endpoint": "http://localhost:8080/auth/realms/example-realm/protocol/openid-connect/userinfo", + "end_session_endpoint": "http://localhost:8080/auth/realms/example-realm/protocol/openid-connect/logout", + "jwks_uri": "http://localhost:8080/auth/realms/example-realm/protocol/openid-connect/certs", + "check_session_iframe": "http://localhost:8080/auth/realms/example-realm/protocol/openid-connect/login-status-iframe.html", + "grant_types_supported": ["authorization_code", "implicit", "refresh_token", "password", "client_credentials"], + "response_types_supported": ["code", "none", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token"], + "subject_types_supported": ["public", "pairwise"], + "id_token_signing_alg_values_supported": ["RS256"], + "userinfo_signing_alg_values_supported": ["RS256"], + "request_object_signing_alg_values_supported": ["none", "RS256"], + "response_modes_supported": ["query", "fragment", "form_post"], + "registration_endpoint": "http://localhost:8080/auth/realms/example-realm/clients-registrations/openid-connect", + "token_endpoint_auth_methods_supported": ["private_key_jwt", "client_secret_basic", "client_secret_post"], + "token_endpoint_auth_signing_alg_values_supported": ["RS256"], + "claims_supported": ["sub", "iss", "auth_time", "name", "given_name", "family_name", "preferred_username", "email"], + "claim_types_supported": ["normal"], + "claims_parameter_supported": false, + "scopes_supported": ["openid", "offline_access"], + "request_parameter_supported": true, + "request_uri_parameter_supported": true}' + + context 'client options' do + subject do + stub_request(:get, "http://localhost:8080/auth/realms/example-realm/.well-known/openid-configuration") + .to_return(status: 200, body: body, headers: {}) + stub_request(:get, "http://localhost:8080/auth/realms/example-realm/protocol/openid-connect/certs") + .to_return(status: 404, body: "", headers: {}) + OmniAuth::Strategies::KeycloakOpenId.new('keycloak-openid', 'Example-Client', 'b53c572b-9f3b-4e79-bf8b-f03c799ba6ec', + client_options: {site: 'http://localhost:8080', realm: 'example-realm'}) + end + + it 'should have the correct keycloak token url' do + subject.setup_phase + expect(subject.token_url).to eq('/auth/realms/example-realm/protocol/openid-connect/token') + end + + it 'should have the correct keycloak authorization url' do + subject.setup_phase + expect(subject.authorize_url).to eq('/auth/realms/example-realm/protocol/openid-connect/auth') + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100755 index 0000000..9f4e79b --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,27 @@ +require "bundler/setup" +require "omniauth/strategies/keycloak-openid" +require "webmock/rspec" + +if RUBY_VERSION >= "1.9" + require "simplecov" + + SimpleCov.start do + minimum_coverage(93.00) + end +end + +require "rspec" +require "omniauth" + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end + config.extend OmniAuth::Test::StrategyMacros, :type => :strategy +end