├── .gitignore ├── .travis.yml ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── omniauth-openid-connect.rb └── omniauth │ ├── openid_connect.rb │ ├── openid_connect │ ├── errors.rb │ └── version.rb │ └── strategies │ └── openid_connect.rb ├── omniauth-openid-connect.gemspec └── test ├── fixtures ├── id_token.txt ├── jwks.json └── test.crt ├── lib └── omniauth │ ├── openid_connect │ └── version_test.rb │ └── strategies │ └── openid_connect_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | InstalledFiles 7 | _yardoc 8 | coverage 9 | doc/ 10 | lib/bundler/man 11 | pkg 12 | rdoc 13 | spec/reports 14 | test/tmp 15 | test/version_tmp 16 | tmp 17 | .ruby-version 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - gem update bundler 3 | rvm: 4 | - 1.9.3 5 | - 2.0.0 6 | - 2.1.0 7 | - 2.2.0 8 | - 2.3.0 9 | - rbx 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'minitest' do 5 | # with Minitest::Unit 6 | watch(%r|^test/(.*)\/(.*)_test\.rb|) 7 | watch(%r|^lib/(.*)\.rb|) { |m| "test/lib/#{m[1]}_test.rb" } 8 | watch(%r|^test/test_helper\.rb|) { "test" } 9 | end 10 | 11 | guard :bundler do 12 | watch('Gemfile') 13 | watch(/^.+\.gemspec/) 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 John Bohn 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This project is no longer maintained. Please see https://github.com/m0n9oose/omniauth_openid_connect for a maintained version. 2 | 3 | # OmniAuth::OpenIDConnect 4 | 5 | OpenID Connect strategy for OmniAuth 6 | [![Gem Version](https://badge.fury.io/rb/omniauth-openid-connect.png)](http://badge.fury.io/rb/omniauth-openid-connect) 7 | [![Build Status](https://travis-ci.org/jjbohn/omniauth-openid-connect.png?branch=master)](https://travis-ci.org/jjbohn/omniauth-openid-connect) 8 | [![Coverage Status](https://coveralls.io/repos/jjbohn/omniauth-openid-connect/badge.png?branch=master)](https://coveralls.io/r/jjbohn/omniauth-openid-connect?branch=master) 9 | [![Code Climate](https://codeclimate.com/github/jjbohn/omniauth-openid-connect.png)](https://codeclimate.com/github/jjbohn/omniauth-openid-connect) 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | gem 'omniauth-openid-connect' 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install omniauth-openid-connect 24 | 25 | ## Usage 26 | 27 | Example configuration 28 | ```ruby 29 | config.omniauth :openid_connect, { 30 | name: :my_provider, 31 | scope: [:openid, :email, :profile, :address], 32 | response_type: :code, 33 | client_options: { 34 | port: 443, 35 | scheme: "https", 36 | host: "myprovider.com", 37 | identifier: ENV["OP_CLIENT_ID"], 38 | secret: ENV["OP_SECRET_KEY"], 39 | redirect_uri: "http://myapp.com/users/auth/openid_connect/callback", 40 | }, 41 | } 42 | ``` 43 | 44 | Configuration details: 45 | * `name` is arbitrary, I recommend using the name of your provider. The name 46 | configuration exists because you could be using multiple OpenID Connect 47 | providers in a single app. 48 | * Although `response_type` is an available option, currently, only `:code` 49 | is valid. There are plans to bring in implicit flow and hybrid flow at some 50 | point, but it hasn't come up yet for me. Those flows aren't best practive for 51 | server side web apps anyway and are designed more for native/mobile apps. 52 | * If you want to pass `state` paramete by yourself. You can set Proc Object. 53 | e.g. `state: Proc.new{ SecureRandom.hex(32) }` 54 | * `nonce` is optional. If don't want to pass "nonce" parameter to provider, You should specify 55 | `false` to `send_nonce` option. (default true) 56 | * Support for other client authentication methods. If don't specified 57 | `:client_auth_method` option, automatically set `:basic`. 58 | * Use "OpenID Connect Discovery", You should specify `true` to `discovery` option. (default false) 59 | * In "OpenID Connect Discovery", generally provider should have Webfinger endpoint. 60 | If provider does not have Webfinger endpoint, You can specify "Issuer" to option. 61 | e.g. `issuer: "https://myprovider.com"` 62 | It means to get configuration from "https://myprovider.com/.well-known/openid-configuration". 63 | 64 | For the full low down on OpenID Connect, please check out 65 | [the spec](http://openid.net/specs/openid-connect-core-1_0.html). 66 | 67 | ## Contributing 68 | 69 | 1. Fork it ( http://github.com/jjbohn/omniauth-openid-connect/fork ) 70 | 2. Create your feature branch (`git checkout -b my-new-feature`) 71 | 3. Commit your changes (`git commit -am 'Add some feature'`) 72 | 4. Push to the branch (`git push origin my-new-feature`) 73 | 5. Create new Pull Request 74 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << 'lib/omniauth-openid-connect' 6 | t.test_files = FileList['test/lib/omniauth/**/*_test.rb'] 7 | t.verbose = true 8 | end 9 | 10 | task default: :test 11 | -------------------------------------------------------------------------------- /lib/omniauth-openid-connect.rb: -------------------------------------------------------------------------------- 1 | require "omniauth/openid_connect" 2 | -------------------------------------------------------------------------------- /lib/omniauth/openid_connect.rb: -------------------------------------------------------------------------------- 1 | require "omniauth/openid_connect/errors" 2 | require "omniauth/openid_connect/version" 3 | require "omniauth/strategies/openid_connect" 4 | -------------------------------------------------------------------------------- /lib/omniauth/openid_connect/errors.rb: -------------------------------------------------------------------------------- 1 | module OmniAuth 2 | module OpenIDConnect 3 | class Error < RuntimeError; end 4 | class MissingCodeError < Error; end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/omniauth/openid_connect/version.rb: -------------------------------------------------------------------------------- 1 | module OmniAuth 2 | module OpenIDConnect 3 | VERSION = "0.2.2" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/omniauth/strategies/openid_connect.rb: -------------------------------------------------------------------------------- 1 | require 'addressable/uri' 2 | require 'timeout' 3 | require 'net/http' 4 | require 'open-uri' 5 | require 'omniauth' 6 | require 'openid_connect' 7 | 8 | module OmniAuth 9 | module Strategies 10 | class OpenIDConnect 11 | include OmniAuth::Strategy 12 | 13 | option :client_options, { 14 | identifier: nil, 15 | secret: nil, 16 | redirect_uri: nil, 17 | scheme: "https", 18 | host: nil, 19 | port: 443, 20 | authorization_endpoint: "/authorize", 21 | token_endpoint: "/token", 22 | userinfo_endpoint: "/userinfo", 23 | jwks_uri: '/jwk' 24 | } 25 | option :issuer 26 | option :discovery, false 27 | option :client_signing_alg 28 | option :client_jwk_signing_key 29 | option :client_x509_signing_key 30 | option :scope, [:openid] 31 | option :response_type, "code" 32 | option :state 33 | option :response_mode 34 | option :display, nil #, [:page, :popup, :touch, :wap] 35 | option :prompt, nil #, [:none, :login, :consent, :select_account] 36 | option :hd, nil 37 | option :max_age 38 | option :ui_locales 39 | option :id_token_hint 40 | option :login_hint 41 | option :acr_values 42 | option :send_nonce, true 43 | option :send_scope_to_token_endpoint, true 44 | option :client_auth_method 45 | 46 | uid { user_info.sub } 47 | 48 | info do 49 | { 50 | name: user_info.name, 51 | email: user_info.email, 52 | nickname: user_info.preferred_username, 53 | first_name: user_info.given_name, 54 | last_name: user_info.family_name, 55 | gender: user_info.gender, 56 | image: user_info.picture, 57 | phone: user_info.phone_number, 58 | urls: { website: user_info.website } 59 | } 60 | end 61 | 62 | extra do 63 | {raw_info: user_info.raw_attributes} 64 | end 65 | 66 | credentials do 67 | { 68 | id_token: access_token.id_token, 69 | token: access_token.access_token, 70 | refresh_token: access_token.refresh_token, 71 | expires_in: access_token.expires_in, 72 | scope: access_token.scope 73 | } 74 | end 75 | 76 | def client 77 | @client ||= ::OpenIDConnect::Client.new(client_options) 78 | end 79 | 80 | def config 81 | @config ||= ::OpenIDConnect::Discovery::Provider::Config.discover!(options.issuer) 82 | end 83 | 84 | def request_phase 85 | options.issuer = issuer if options.issuer.blank? 86 | discover! if options.discovery 87 | redirect authorize_uri 88 | end 89 | 90 | def callback_phase 91 | error = request.params['error_reason'] || request.params['error'] 92 | if error 93 | raise CallbackError.new(request.params['error'], request.params['error_description'] || request.params['error_reason'], request.params['error_uri']) 94 | elsif request.params['state'].to_s.empty? || request.params['state'] != stored_state 95 | return Rack::Response.new(['401 Unauthorized'], 401).finish 96 | elsif !request.params["code"] 97 | return fail!(:missing_code, OmniAuth::OpenIDConnect::MissingCodeError.new(request.params["error"])) 98 | else 99 | options.issuer = issuer if options.issuer.blank? 100 | discover! if options.discovery 101 | client.redirect_uri = client_options.redirect_uri 102 | client.authorization_code = authorization_code 103 | access_token 104 | super 105 | end 106 | rescue CallbackError => e 107 | fail!(:invalid_credentials, e) 108 | rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e 109 | fail!(:timeout, e) 110 | rescue ::SocketError => e 111 | fail!(:failed_to_connect, e) 112 | end 113 | 114 | 115 | def authorization_code 116 | request.params["code"] 117 | end 118 | 119 | def authorize_uri 120 | client.redirect_uri = client_options.redirect_uri 121 | opts = { 122 | response_type: options.response_type, 123 | scope: options.scope, 124 | state: new_state, 125 | nonce: (new_nonce if options.send_nonce), 126 | hd: options.hd, 127 | } 128 | client.authorization_uri(opts.reject{|k,v| v.nil?}) 129 | end 130 | 131 | def public_key 132 | if options.discovery 133 | config.jwks 134 | else 135 | key_or_secret 136 | end 137 | end 138 | 139 | private 140 | 141 | def issuer 142 | resource = "#{client_options.scheme}://#{client_options.host}" + ((client_options.port) ? ":#{client_options.port.to_s}" : '') 143 | ::OpenIDConnect::Discovery::Provider.discover!(resource).issuer 144 | end 145 | 146 | def discover! 147 | client_options.authorization_endpoint = config.authorization_endpoint 148 | client_options.token_endpoint = config.token_endpoint 149 | client_options.userinfo_endpoint = config.userinfo_endpoint 150 | client_options.jwks_uri = config.jwks_uri 151 | end 152 | 153 | def user_info 154 | @user_info ||= access_token.userinfo! 155 | end 156 | 157 | def access_token 158 | @access_token ||= lambda { 159 | _access_token = client.access_token!( 160 | scope: (options.scope if options.send_scope_to_token_endpoint), 161 | client_auth_method: options.client_auth_method 162 | ) 163 | _id_token = decode_id_token _access_token.id_token 164 | _id_token.verify!( 165 | issuer: options.issuer, 166 | client_id: client_options.identifier, 167 | nonce: stored_nonce 168 | ) 169 | _access_token 170 | }.call() 171 | end 172 | 173 | def decode_id_token(id_token) 174 | ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, public_key) 175 | end 176 | 177 | 178 | def client_options 179 | options.client_options 180 | end 181 | 182 | def new_state 183 | state = options.state.call if options.state.respond_to? :call 184 | session['omniauth.state'] = state || SecureRandom.hex(16) 185 | end 186 | 187 | def stored_state 188 | session.delete('omniauth.state') 189 | end 190 | 191 | def new_nonce 192 | session['omniauth.nonce'] = SecureRandom.hex(16) 193 | end 194 | 195 | def stored_nonce 196 | session.delete('omniauth.nonce') 197 | end 198 | 199 | def session 200 | @env.nil? ? {} : super 201 | end 202 | 203 | def key_or_secret 204 | case options.client_signing_alg 205 | when :HS256, :HS384, :HS512 206 | return client_options.secret 207 | when :RS256, :RS384, :RS512 208 | if options.client_jwk_signing_key 209 | return parse_jwk_key(options.client_jwk_signing_key) 210 | elsif options.client_x509_signing_key 211 | return parse_x509_key(options.client_x509_signing_key) 212 | end 213 | else 214 | end 215 | end 216 | 217 | def parse_x509_key(key) 218 | OpenSSL::X509::Certificate.new(key).public_key 219 | end 220 | 221 | def parse_jwk_key(key) 222 | json = JSON.parse(key) 223 | if json.has_key?('keys') 224 | JSON::JWK::Set.new json['keys'] 225 | else 226 | JSON::JWK.new json 227 | end 228 | end 229 | 230 | def decode(str) 231 | UrlSafeBase64.decode64(str).unpack('B*').first.to_i(2).to_s 232 | end 233 | 234 | class CallbackError < StandardError 235 | attr_accessor :error, :error_reason, :error_uri 236 | 237 | def initialize(error, error_reason=nil, error_uri=nil) 238 | self.error = error 239 | self.error_reason = error_reason 240 | self.error_uri = error_uri 241 | end 242 | 243 | def message 244 | [error, error_reason, error_uri].compact.join(' | ') 245 | end 246 | end 247 | end 248 | end 249 | end 250 | 251 | OmniAuth.config.add_camelization 'openid_connect', 'OpenIDConnect' 252 | -------------------------------------------------------------------------------- /omniauth-openid-connect.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'omniauth/openid_connect/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "omniauth-openid-connect" 8 | spec.version = OmniAuth::OpenIDConnect::VERSION 9 | spec.authors = ["John Bohn"] 10 | spec.email = ["jjbohn@gmail.com"] 11 | spec.summary = %q{OpenID Connect Strategy for OmniAuth} 12 | spec.description = %q{OpenID Connect Strategy for OmniAuth} 13 | spec.homepage = "https://github.com/jjbohn/omniauth-openid-connect" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency 'omniauth', '~> 1.1' 22 | spec.add_dependency 'openid_connect', '~> 0.9.2' 23 | spec.add_dependency 'addressable', '~> 2.3' 24 | spec.add_development_dependency "bundler", "~> 1.5" 25 | spec.add_development_dependency "minitest" 26 | spec.add_development_dependency "mocha" 27 | spec.add_development_dependency "guard" 28 | spec.add_development_dependency "guard-minitest" 29 | spec.add_development_dependency "guard-bundler" 30 | spec.add_development_dependency "rake" 31 | spec.add_development_dependency "simplecov" 32 | spec.add_development_dependency "pry" 33 | spec.add_development_dependency "coveralls" 34 | spec.add_development_dependency "faker" 35 | end 36 | -------------------------------------------------------------------------------- /test/fixtures/id_token.txt: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg 2 | -------------------------------------------------------------------------------- /test/fixtures/jwks.json: -------------------------------------------------------------------------------- 1 | {"keys": [{ 2 | "kty": "RSA", 3 | "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 4 | "e": "AQAB", 5 | "alg": "RS256", 6 | "kid": "1e9gdk7" 7 | }] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/test.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDJDCCAgwCCQC57Ob2JfXb+DANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJK 3 | UDEOMAwGA1UECBMFVG9reW8xITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5 4 | IEx0ZDESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDgwMTA4NTAxM1oXDTE1MDgw 5 | MTA4NTAxM1owVDELMAkGA1UEBhMCSlAxDjAMBgNVBAgTBVRva3lvMSEwHwYDVQQK 6 | ExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDCC 7 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+7czSGHN2087T+oX2kBCY/ 8 | XN6UOS/mdU2Gn//omZlyxsQXIqvgBLNWeCVt4QdlFUbgPLggfXUelECV/RUOCIIi 9 | F2Th4t3x1LviN2XkUiva0DZBnOycqEaJdkyreEuGL1CLVZgZjKmSzNqLl0Yci3D0 10 | zgVsXFZSadQebietm4CCmfJYREt9NJxXcrLxVDgat/Xm/KJBsohs3f+cbBT8EXer 11 | 7+2oZjZoVUgw1hu0alaOvAfE4mxsVwjn3g2mjDqRJLbbuWqgDobjMHah+d4zwJvN 12 | ePK8E0hfaz/XBLsJ4e6bQA3M3bANEgSvsicup/qb/0th4gUdc/kj4aJGj0RP7oEC 13 | AwEAATANBgkqhkiG9w0BAQUFAAOCAQEADuVec/8u2qJiq6K2W/gSLGYCBZq64OrA 14 | s7L2+S82m9/3gAb62wGcDNZjIGFDQubXmO6RhHv7JUT5YZqv9/kRGTJcHDUrwwoN 15 | IE99CIPizp7VfnrZ6GsYeszSsw3m+mKTETm+6ELmaSDbYAsrCg4IpGwUF0L88ATv 16 | CJ8QzW4X7b9dYVc7UAYyCie2N65GXfesBbRlSwFLuVqIzZfMdNpNijTIUwUqGSME 17 | b8IjLYzvekP53CO4wEBRrAVIPNXgftorxIE30OLWua2Qw3y6Pn+Qp5fLe47025S7 18 | Lcec18/FbHG0Vbq0qO9cKQw80XyK31N6z556wr2GN2WyixkzVRddXA== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/lib/omniauth/openid_connect/version_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../test_helper' 2 | 3 | class OmniAuth::OpenIDConnect::VersionTest < MiniTest::Test 4 | def test_version_defined 5 | refute_nil OmniAuth::OpenIDConnect::VERSION 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/lib/omniauth/strategies/openid_connect_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../test_helper' 2 | 3 | class OmniAuth::Strategies::OpenIDConnectTest < StrategyTestCase 4 | def test_client_options_defaults 5 | assert_equal 'https', strategy.options.client_options.scheme 6 | assert_equal 443, strategy.options.client_options.port 7 | assert_equal '/authorize', strategy.options.client_options.authorization_endpoint 8 | assert_equal '/token', strategy.options.client_options.token_endpoint 9 | end 10 | 11 | def test_request_phase 12 | expected_redirect = /^https:\/\/example\.com\/authorize\?client_id=1234&nonce=[\w\d]{32}&response_type=code&scope=openid&state=[\w\d]{32}$/ 13 | strategy.options.issuer = 'example.com' 14 | strategy.options.client_options.host = 'example.com' 15 | strategy.expects(:redirect).with(regexp_matches(expected_redirect)) 16 | strategy.request_phase 17 | end 18 | 19 | def test_request_phase_with_discovery 20 | expected_redirect = /^https:\/\/example\.com\/authorization\?client_id=1234&nonce=[\w\d]{32}&response_type=code&scope=openid&state=[\w\d]{32}$/ 21 | strategy.options.client_options.host = 'example.com' 22 | strategy.options.discovery = true 23 | 24 | issuer = stub('OpenIDConnect::Discovery::Issuer') 25 | issuer.stubs(:issuer).returns('https://example.com/') 26 | ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) 27 | 28 | config = stub('OpenIDConnect::Discovery::Provder::Config') 29 | config.stubs(:authorization_endpoint).returns('https://example.com/authorization') 30 | config.stubs(:token_endpoint).returns('https://example.com/token') 31 | config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') 32 | config.stubs(:jwks_uri).returns('https://example.com/jwks') 33 | ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) 34 | 35 | strategy.expects(:redirect).with(regexp_matches(expected_redirect)) 36 | strategy.request_phase 37 | 38 | assert_equal strategy.options.issuer, 'https://example.com/' 39 | assert_equal strategy.options.client_options.authorization_endpoint, 'https://example.com/authorization' 40 | assert_equal strategy.options.client_options.token_endpoint, 'https://example.com/token' 41 | assert_equal strategy.options.client_options.userinfo_endpoint, 'https://example.com/userinfo' 42 | assert_equal strategy.options.client_options.jwks_uri, 'https://example.com/jwks' 43 | end 44 | 45 | def test_uid 46 | assert_equal user_info.sub, strategy.uid 47 | end 48 | 49 | def test_callback_phase(session = {}, params = {}) 50 | code = SecureRandom.hex(16) 51 | state = SecureRandom.hex(16) 52 | nonce = SecureRandom.hex(16) 53 | request.stubs(:params).returns({'code' => code,'state' => state}) 54 | request.stubs(:path_info).returns('') 55 | 56 | strategy.options.issuer = 'example.com' 57 | strategy.options.client_signing_alg = :RS256 58 | strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json') 59 | 60 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 61 | id_token.stubs(:verify!).with({:issuer => strategy.options.issuer, :client_id => @identifier, :nonce => nonce}).returns(true) 62 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 63 | 64 | strategy.unstub(:user_info) 65 | access_token = stub('OpenIDConnect::AccessToken') 66 | access_token.stubs(:access_token) 67 | access_token.stubs(:refresh_token) 68 | access_token.stubs(:expires_in) 69 | access_token.stubs(:scope) 70 | access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt')) 71 | client.expects(:access_token!).at_least_once.returns(access_token) 72 | access_token.expects(:userinfo!).returns(user_info) 73 | 74 | strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) 75 | strategy.callback_phase 76 | end 77 | 78 | def test_callback_phase_with_discovery 79 | code = SecureRandom.hex(16) 80 | state = SecureRandom.hex(16) 81 | nonce = SecureRandom.hex(16) 82 | jwks = JSON::JWK::Set.new(JSON.parse(File.read('test/fixtures/jwks.json'))['keys']) 83 | 84 | request.stubs(:params).returns({'code' => code,'state' => state}) 85 | request.stubs(:path_info).returns('') 86 | 87 | strategy.options.client_options.host = 'example.com' 88 | strategy.options.discovery = true 89 | 90 | issuer = stub('OpenIDConnect::Discovery::Issuer') 91 | issuer.stubs(:issuer).returns('https://example.com/') 92 | ::OpenIDConnect::Discovery::Provider.stubs(:discover!).returns(issuer) 93 | 94 | config = stub('OpenIDConnect::Discovery::Provder::Config') 95 | config.stubs(:authorization_endpoint).returns('https://example.com/authorization') 96 | config.stubs(:token_endpoint).returns('https://example.com/token') 97 | config.stubs(:userinfo_endpoint).returns('https://example.com/userinfo') 98 | config.stubs(:jwks_uri).returns('https://example.com/jwks') 99 | config.stubs(:jwks).returns(jwks) 100 | 101 | ::OpenIDConnect::Discovery::Provider::Config.stubs(:discover!).with('https://example.com/').returns(config) 102 | 103 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 104 | id_token.stubs(:verify!).with({:issuer => 'https://example.com/', :client_id => @identifier, :nonce => nonce}).returns(true) 105 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 106 | 107 | strategy.unstub(:user_info) 108 | access_token = stub('OpenIDConnect::AccessToken') 109 | access_token.stubs(:access_token) 110 | access_token.stubs(:refresh_token) 111 | access_token.stubs(:expires_in) 112 | access_token.stubs(:scope) 113 | access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt')) 114 | client.expects(:access_token!).at_least_once.returns(access_token) 115 | access_token.expects(:userinfo!).returns(user_info) 116 | 117 | strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) 118 | strategy.callback_phase 119 | end 120 | 121 | def test_callback_phase_with_error 122 | state = SecureRandom.hex(16) 123 | nonce = SecureRandom.hex(16) 124 | request.stubs(:params).returns({'error' => 'invalid_request'}) 125 | request.stubs(:path_info).returns('') 126 | 127 | strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) 128 | strategy.expects(:fail!) 129 | strategy.callback_phase 130 | end 131 | 132 | def test_callback_phase_with_invalid_state 133 | code = SecureRandom.hex(16) 134 | state = SecureRandom.hex(16) 135 | nonce = SecureRandom.hex(16) 136 | request.stubs(:params).returns({'code' => code,'state' => 'foobar'}) 137 | request.stubs(:path_info).returns('') 138 | 139 | strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) 140 | result = strategy.callback_phase 141 | 142 | assert result.kind_of?(Array) 143 | assert result.first == 401, "Expecting unauthorized" 144 | end 145 | 146 | def test_callback_phase_with_timeout 147 | code = SecureRandom.hex(16) 148 | state = SecureRandom.hex(16) 149 | nonce = SecureRandom.hex(16) 150 | request.stubs(:params).returns({'code' => code,'state' => state}) 151 | request.stubs(:path_info).returns('') 152 | 153 | strategy.options.issuer = 'example.com' 154 | 155 | strategy.stubs(:access_token).raises(::Timeout::Error.new('error')) 156 | strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) 157 | strategy.expects(:fail!) 158 | strategy.callback_phase 159 | end 160 | 161 | def test_callback_phase_with_etimeout 162 | code = SecureRandom.hex(16) 163 | state = SecureRandom.hex(16) 164 | nonce = SecureRandom.hex(16) 165 | request.stubs(:params).returns({'code' => code,'state' => state}) 166 | request.stubs(:path_info).returns('') 167 | 168 | strategy.options.issuer = 'example.com' 169 | 170 | strategy.stubs(:access_token).raises(::Errno::ETIMEDOUT.new('error')) 171 | strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) 172 | strategy.expects(:fail!) 173 | strategy.callback_phase 174 | end 175 | 176 | def test_callback_phase_with_socket_error 177 | code = SecureRandom.hex(16) 178 | state = SecureRandom.hex(16) 179 | nonce = SecureRandom.hex(16) 180 | request.stubs(:params).returns({'code' => code,'state' => state}) 181 | request.stubs(:path_info).returns('') 182 | 183 | strategy.options.issuer = 'example.com' 184 | 185 | strategy.stubs(:access_token).raises(::SocketError.new('error')) 186 | strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) 187 | strategy.expects(:fail!) 188 | strategy.callback_phase 189 | end 190 | 191 | def test_info 192 | info = strategy.info 193 | assert_equal user_info.name, info[:name] 194 | assert_equal user_info.email, info[:email] 195 | assert_equal user_info.preferred_username, info[:nickname] 196 | assert_equal user_info.given_name, info[:first_name] 197 | assert_equal user_info.family_name, info[:last_name] 198 | assert_equal user_info.gender, info[:gender] 199 | assert_equal user_info.picture, info[:image] 200 | assert_equal user_info.phone_number, info[:phone] 201 | assert_equal({ website: user_info.website }, info[:urls]) 202 | end 203 | 204 | def test_extra 205 | assert_equal({ raw_info: user_info.as_json }, strategy.extra) 206 | end 207 | 208 | def test_credentials 209 | strategy.options.issuer = 'example.com' 210 | strategy.options.client_signing_alg = :RS256 211 | strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json') 212 | 213 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 214 | id_token.stubs(:verify!).returns(true) 215 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 216 | 217 | access_token = stub('OpenIDConnect::AccessToken') 218 | access_token.stubs(:access_token).returns(SecureRandom.hex(16)) 219 | access_token.stubs(:refresh_token).returns(SecureRandom.hex(16)) 220 | access_token.stubs(:expires_in).returns(Time.now) 221 | access_token.stubs(:scope).returns('openidconnect') 222 | access_token.stubs(:id_token).returns(File.read('test/fixtures/id_token.txt')) 223 | 224 | client.expects(:access_token!).returns(access_token) 225 | access_token.expects(:refresh_token).returns(access_token.refresh_token) 226 | access_token.expects(:expires_in).returns(access_token.expires_in) 227 | 228 | assert_equal({ id_token: access_token.id_token, 229 | token: access_token.access_token, 230 | refresh_token: access_token.refresh_token, 231 | expires_in: access_token.expires_in, 232 | scope: access_token.scope 233 | }, strategy.credentials) 234 | end 235 | 236 | def test_option_send_nonce 237 | strategy.options.client_options[:host] = "foobar.com" 238 | 239 | assert(strategy.authorize_uri =~ /nonce=/, "URI must contain nonce") 240 | 241 | strategy.options.send_nonce = false 242 | assert(!(strategy.authorize_uri =~ /nonce=/), "URI must not contain nonce") 243 | end 244 | 245 | def test_failure_endpoint_redirect 246 | OmniAuth.config.stubs(:failure_raise_out_environments).returns([]) 247 | strategy.stubs(:env).returns({}) 248 | request.stubs(:params).returns({"error" => "access denied"}) 249 | 250 | result = strategy.callback_phase 251 | 252 | assert(result.is_a? Array) 253 | assert(result[0] == 302, "Redirect") 254 | assert(result[1]["Location"] =~ /\/auth\/failure/) 255 | end 256 | 257 | def test_state 258 | strategy.options.state = lambda { 42 } 259 | session = { "state" => 42 } 260 | 261 | expected_redirect = /&state=/ 262 | strategy.options.issuer = 'example.com' 263 | strategy.options.client_options.host = "example.com" 264 | strategy.expects(:redirect).with(regexp_matches(expected_redirect)) 265 | strategy.request_phase 266 | 267 | # this should succeed as the correct state is passed with the request 268 | test_callback_phase(session, { "state" => 42 }) 269 | 270 | # the following should fail because the wrong state is passed to the callback 271 | code = SecureRandom.hex(16) 272 | request.stubs(:params).returns({"code" => code, "state" => 43}) 273 | request.stubs(:path_info).returns("") 274 | strategy.call!({"rack.session" => session}) 275 | 276 | result = strategy.callback_phase 277 | 278 | assert result.kind_of?(Array) 279 | assert result.first == 401, "Expecting unauthorized" 280 | end 281 | 282 | def test_option_client_auth_method 283 | code = SecureRandom.hex(16) 284 | state = SecureRandom.hex(16) 285 | nonce = SecureRandom.hex(16) 286 | 287 | opts = strategy.options.client_options 288 | opts[:host] = "foobar.com" 289 | strategy.options.issuer = "foobar.com" 290 | strategy.options.client_auth_method = :not_basic 291 | strategy.options.client_signing_alg = :RS256 292 | strategy.options.client_jwk_signing_key = File.read('test/fixtures/jwks.json') 293 | 294 | json_response = {access_token: 'test_access_token', 295 | id_token: File.read('test/fixtures/id_token.txt'), 296 | token_type: 'Bearer', 297 | }.to_json 298 | success = Struct.new(:status, :body).new(200, json_response) 299 | 300 | request.stubs(:path_info).returns('') 301 | strategy.call!({'rack.session' => {'omniauth.state' => state, 'omniauth.nonce' => nonce}}) 302 | 303 | id_token = stub('OpenIDConnect::ResponseObject::IdToken') 304 | id_token.stubs(:verify!).with({:issuer => strategy.options.issuer, :client_id => @identifier, :nonce => nonce}).returns(true) 305 | ::OpenIDConnect::ResponseObject::IdToken.stubs(:decode).returns(id_token) 306 | 307 | HTTPClient.any_instance.stubs(:post).with( 308 | "#{opts.scheme}://#{opts.host}:#{opts.port}#{opts.token_endpoint}", 309 | {scope: 'openid', :grant_type => :client_credentials, :client_id => @identifier, :client_secret => @secret}, 310 | {} 311 | ).returns(success) 312 | 313 | assert(strategy.send :access_token) 314 | end 315 | 316 | def test_public_key_with_jwks 317 | strategy.options.client_signing_alg = :RS256 318 | strategy.options.client_jwk_signing_key = File.read('./test/fixtures/jwks.json') 319 | 320 | assert_equal JSON::JWK::Set, strategy.public_key.class 321 | end 322 | 323 | def test_public_key_with_jwk 324 | strategy.options.client_signing_alg = :RS256 325 | jwks_str = File.read('./test/fixtures/jwks.json') 326 | jwks = JSON.parse(jwks_str) 327 | jwk = jwks['keys'].first 328 | strategy.options.client_jwk_signing_key = jwk.to_json 329 | 330 | assert_equal JSON::JWK, strategy.public_key.class 331 | end 332 | 333 | def test_public_key_with_x509 334 | strategy.options.client_signing_alg = :RS256 335 | strategy.options.client_x509_signing_key = File.read('./test/fixtures/test.crt') 336 | assert_equal OpenSSL::PKey::RSA, strategy.public_key.class 337 | end 338 | 339 | def test_public_key_with_hmac 340 | strategy.options.client_options.secret = 'secret' 341 | strategy.options.client_signing_alg = :HS256 342 | assert_equal strategy.options.client_options.secret, strategy.public_key 343 | end 344 | end 345 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.command_name 'test' 3 | SimpleCov.start 4 | 5 | require 'coveralls' 6 | Coveralls.wear! 7 | 8 | require 'minitest/autorun' 9 | require 'mocha/mini_test' 10 | require 'faker' 11 | require 'active_support' 12 | require_relative '../lib/omniauth-openid-connect' 13 | 14 | OmniAuth.config.test_mode = true 15 | 16 | class StrategyTestCase < MiniTest::Test 17 | class DummyApp 18 | def call(env); end 19 | end 20 | 21 | attr_accessor :identifier, :secret 22 | 23 | def setup 24 | @identifier = "1234" 25 | @secret = "1234asdgat3" 26 | end 27 | 28 | def client 29 | strategy.client 30 | end 31 | 32 | def user_info 33 | @user_info ||= OpenIDConnect::ResponseObject::UserInfo.new( 34 | sub: SecureRandom.hex(16), 35 | name: Faker::Name.name, 36 | email: Faker::Internet.email, 37 | nickname: Faker::Name.first_name, 38 | preferred_username: Faker::Internet.user_name, 39 | given_name: Faker::Name.first_name, 40 | family_name: Faker::Name.last_name, 41 | gender: 'female', 42 | picture: Faker::Internet.url + ".png", 43 | phone_number: Faker::PhoneNumber.phone_number, 44 | website: Faker::Internet.url, 45 | ) 46 | end 47 | 48 | def request 49 | @request ||= stub('Request').tap do |request| 50 | request.stubs(:params).returns({}) 51 | request.stubs(:cookies).returns({}) 52 | request.stubs(:env).returns({}) 53 | request.stubs(:scheme).returns({}) 54 | request.stubs(:ssl?).returns(false) 55 | end 56 | end 57 | 58 | def strategy 59 | @strategy ||= OmniAuth::Strategies::OpenIDConnect.new(DummyApp.new).tap do |strategy| 60 | strategy.options.client_options.identifier = @identifier 61 | strategy.options.client_options.secret = @secret 62 | strategy.stubs(:request).returns(request) 63 | strategy.stubs(:user_info).returns(user_info) 64 | end 65 | end 66 | end 67 | --------------------------------------------------------------------------------