openid-token-proxy 0.1.0

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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +4 -0
  7. data/Guardfile +41 -0
  8. data/LICENSE.md +22 -0
  9. data/README.md +211 -0
  10. data/Rakefile +16 -0
  11. data/app/controllers/openid_token_proxy/application_controller.rb +4 -0
  12. data/app/controllers/openid_token_proxy/callback_controller.rb +22 -0
  13. data/config/initializers/inflections.rb +3 -0
  14. data/config/routes.rb +3 -0
  15. data/docs/diagrams.sketch +0 -0
  16. data/docs/openid-token-proxy-flow.png +0 -0
  17. data/docs/regular-openid-flow.png +0 -0
  18. data/lib/openid-token-proxy.rb +1 -0
  19. data/lib/openid_token_proxy/client.rb +48 -0
  20. data/lib/openid_token_proxy/config.rb +56 -0
  21. data/lib/openid_token_proxy/engine.rb +5 -0
  22. data/lib/openid_token_proxy/error.rb +7 -0
  23. data/lib/openid_token_proxy/token/authentication.rb +54 -0
  24. data/lib/openid_token_proxy/token/expired.rb +12 -0
  25. data/lib/openid_token_proxy/token/invalid_application.rb +12 -0
  26. data/lib/openid_token_proxy/token/invalid_audience.rb +12 -0
  27. data/lib/openid_token_proxy/token/invalid_issuer.rb +12 -0
  28. data/lib/openid_token_proxy/token/malformed.rb +12 -0
  29. data/lib/openid_token_proxy/token/refresh.rb +30 -0
  30. data/lib/openid_token_proxy/token/required.rb +12 -0
  31. data/lib/openid_token_proxy/token/unverifiable_signature.rb +12 -0
  32. data/lib/openid_token_proxy/token.rb +80 -0
  33. data/lib/openid_token_proxy/version.rb +3 -0
  34. data/lib/openid_token_proxy.rb +40 -0
  35. data/openid-token-proxy.gemspec +35 -0
  36. data/spec/controllers/openid_token_proxy/callback_controller_spec.rb +72 -0
  37. data/spec/dummy/README.rdoc +28 -0
  38. data/spec/dummy/Rakefile +6 -0
  39. data/spec/dummy/app/assets/images/.keep +0 -0
  40. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  41. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  42. data/spec/dummy/app/controllers/accounts_controller.rb +10 -0
  43. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  44. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  45. data/spec/dummy/app/controllers/home_controller.rb +7 -0
  46. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  47. data/spec/dummy/app/mailers/.keep +0 -0
  48. data/spec/dummy/app/models/.keep +0 -0
  49. data/spec/dummy/app/models/concerns/.keep +0 -0
  50. data/spec/dummy/app/views/home/index.html.erb +25 -0
  51. data/spec/dummy/app/views/layouts/application.html.erb +54 -0
  52. data/spec/dummy/bin/bundle +3 -0
  53. data/spec/dummy/bin/rails +4 -0
  54. data/spec/dummy/bin/rake +4 -0
  55. data/spec/dummy/config/application.rb +27 -0
  56. data/spec/dummy/config/boot.rb +5 -0
  57. data/spec/dummy/config/environment.rb +5 -0
  58. data/spec/dummy/config/environments/development.rb +34 -0
  59. data/spec/dummy/config/environments/production.rb +75 -0
  60. data/spec/dummy/config/environments/test.rb +39 -0
  61. data/spec/dummy/config/initializers/assets.rb +8 -0
  62. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  63. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  64. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  65. data/spec/dummy/config/initializers/inflections.rb +16 -0
  66. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  67. data/spec/dummy/config/initializers/openid.rb +5 -0
  68. data/spec/dummy/config/initializers/session_store.rb +3 -0
  69. data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
  70. data/spec/dummy/config/locales/en.yml +23 -0
  71. data/spec/dummy/config/routes.rb +5 -0
  72. data/spec/dummy/config/secrets.yml +22 -0
  73. data/spec/dummy/config.ru +4 -0
  74. data/spec/dummy/lib/assets/.keep +0 -0
  75. data/spec/dummy/log/.keep +0 -0
  76. data/spec/dummy/public/404.html +67 -0
  77. data/spec/dummy/public/422.html +67 -0
  78. data/spec/dummy/public/500.html +66 -0
  79. data/spec/dummy/public/favicon.ico +0 -0
  80. data/spec/fixtures/keys.json +26 -0
  81. data/spec/fixtures/openid-configuration.json +30 -0
  82. data/spec/lib/openid_token_proxy/client_spec.rb +150 -0
  83. data/spec/lib/openid_token_proxy/config_spec.rb +201 -0
  84. data/spec/lib/openid_token_proxy/error_spec.rb +11 -0
  85. data/spec/lib/openid_token_proxy/token/authentication_spec.rb +67 -0
  86. data/spec/lib/openid_token_proxy/token/refresh_spec.rb +71 -0
  87. data/spec/lib/openid_token_proxy/token_spec.rb +138 -0
  88. data/spec/lib/openid_token_proxy_spec.rb +38 -0
  89. data/spec/spec_helper.rb +88 -0
  90. data/spec/support/env.rb +4 -0
  91. data/spec/support/fixture.rb +3 -0
  92. metadata +359 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 036360c10b330eabfbacd91009ab6ffd948caaaf
4
+ data.tar.gz: 5cc9438acbb74f17d8f4870ffd5899da09a542ae
5
+ SHA512:
6
+ metadata.gz: 82b2be465db4d1e0787dda423e88b434aaa9e4af60c7472d6ae9537fe3aea769275f0f0a8796470925f340b4c8d42f56dfb7fd3b54260cbea8946f6612fcc3fe
7
+ data.tar.gz: 6e82dd27d7e61e87a75d08663a3d4fb6052cc4f97b2b6774fe2a2359f006b15f3a0f4908dfdac3727e7533794911182922f20e7c944a0aa2876fdc1081dbc218
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ coverage/
2
+ Gemfile.lock
3
+ pkg/
4
+ spec/dummy/log/
5
+ tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1.0
6
+ script: bundle exec rake spec
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ### v0.1.0 - May 6, 2015
4
+
5
+ - Initial release.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # For the actual dependencies please see openid-token-proxy.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,41 @@
1
+ guard :rspec, cmd: 'bundle exec rspec' do
2
+ require 'guard/rspec/dsl'
3
+ dsl = Guard::RSpec::Dsl.new(self)
4
+
5
+ # RSpec files
6
+ rspec = dsl.rspec
7
+ watch(rspec.spec_helper) { rspec.spec_dir }
8
+ watch(rspec.spec_support) { rspec.spec_dir }
9
+ watch(rspec.spec_files)
10
+
11
+ # Ruby files
12
+ ruby = dsl.ruby
13
+ dsl.watch_spec_files_for(ruby.lib_files)
14
+
15
+ # Rails files
16
+ rails = dsl.rails(view_extensions: %w(erb haml slim))
17
+ dsl.watch_spec_files_for(rails.app_files)
18
+ dsl.watch_spec_files_for(rails.views)
19
+
20
+ watch(rails.controllers) do |m|
21
+ [
22
+ rspec.spec.("routing/#{m[1]}_routing"),
23
+ rspec.spec.("controllers/#{m[1]}_controller"),
24
+ rspec.spec.("acceptance/#{m[1]}")
25
+ ]
26
+ end
27
+
28
+ # Rails config changes
29
+ watch(rails.spec_helper) { rspec.spec_dir }
30
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
31
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
32
+
33
+ # Capybara features specs
34
+ watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") }
35
+
36
+ # Turnip features and steps
37
+ watch(%r{^spec/acceptance/(.+)\.feature$})
38
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
39
+ Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance'
40
+ end
41
+ end
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ Licensed under the **MIT** license
2
+
3
+ > Copyright (c) 2015 Hyper Interaktiv AS
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 NONINFRINGEMENT.
19
+ > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ > CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ > SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # OpenID token proxy
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/openid-token-proxy.svg?style=flat)](https://rubygems.org/gems/openid-token-proxy)
4
+ [![Build Status](https://img.shields.io/travis/hyperoslo/openid-token-proxy.svg?style=flat)](https://travis-ci.org/hyperoslo/openid-token-proxy)
5
+ [![Dependency Status](https://img.shields.io/gemnasium/hyperoslo/openid-token-proxy.svg?style=flat)](https://gemnasium.com/hyperoslo/openid-token-proxy)
6
+ [![Code Climate](https://img.shields.io/codeclimate/github/hyperoslo/openid-token-proxy.svg?style=flat)](https://codeclimate.com/github/hyperoslo/openid-token-proxy)
7
+ [![Coverage Status](https://img.shields.io/coveralls/hyperoslo/openid-token-proxy.svg?style=flat)](https://coveralls.io/r/hyperoslo/openid-token-proxy)
8
+
9
+ Retrieves and refreshes OpenID tokens on behalf of a user when dealing with complex
10
+ authentication schemes, such as client-side certificates.
11
+
12
+ **Note: Under development, not for production usage just yet**
13
+
14
+ **Supported Ruby versions: 2.0.0 or higher**
15
+
16
+ Licensed under the **MIT** license, see LICENSE for more information.
17
+
18
+
19
+ ## Background
20
+
21
+ When using [OpenID](http://openid.net/specs/openid-connect-core-1_0.html) in
22
+ native applications, the most common approach is to open the identity provider's
23
+ authorization page in a web view, let the user authenticate and have the application
24
+ hold on to access, identity and refresh tokens.
25
+
26
+ ![Regular OpenID flow](docs/regular-openid-flow.png?raw=1)
27
+
28
+ However, the above flow may be unusable if the identity provider provides complex
29
+ authentication schemes, such as client-side certificates.
30
+
31
+ On iOS, client-side certificates stored in the system keychain [cannot be obtained due to application sandboxing](http://stackoverflow.com/questions/7648487/how-to-list-certificates-from-the-iphone-keychain-inside-my-app).
32
+
33
+ On Android, one can obtain system certificates but these [can not be used within a web view](http://stackoverflow.com/questions/15588851/android-webview-with-client-certificate).
34
+
35
+ ![OpenID token proxy flow](docs/openid-token-proxy-flow.png?raw=1)
36
+
37
+ When using OpenID token proxy, the application opens a web browser - which has
38
+ access to client-side certificates regardless of storage location - and lets the
39
+ user authenticate. The identity provider redirects to the OpenID token proxy,
40
+ which in turn passes along any obtained tokens to the application.
41
+
42
+
43
+ ## Installation
44
+
45
+ Add this line to your application's Gemfile:
46
+
47
+ ```ruby
48
+ gem 'openid-token-proxy'
49
+ ```
50
+
51
+ Or install it yourself:
52
+
53
+ $ gem install openid-token-proxy
54
+
55
+
56
+ ## Usage
57
+
58
+ ### Configuration
59
+
60
+ ```ruby
61
+ OpenIDTokenProxy.configure do |config|
62
+ config.client_id = 'xxx'
63
+ config.client_secret = 'xxx'
64
+ config.issuer = 'https://login.windows.net/common'
65
+ config.redirect_uri = 'https://example.com/auth/callback'
66
+ config.resource = 'https://graph.windows.net'
67
+
68
+ # Indicates which domain users will presumably be signing in with
69
+ config.domain_hint = 'example.com'
70
+
71
+ # Whether to force authentication in case a session is already established
72
+ config.prompt = 'login'
73
+
74
+ # If these endpoints or public keys are not configured explicitly, they will be
75
+ # discovered automatically by contacting the issuer (see above)
76
+ config.authorization_endpoint = 'https://login.windows.net/common/oauth2/authorize'
77
+ config.token_endpoint = 'https://login.windows.net/common/oauth2/token'
78
+ config.userinfo_endpoint = 'https://login.windows.net/common/openid/userinfo'
79
+ config.public_keys = [
80
+ OpenSSL::PKey::RSA.new("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9...")
81
+ ]
82
+
83
+ # Alternatively, you can override the authorization URI in its entirety:
84
+ config.authorization_uri = 'https://id.hyper.no/authorize?prompt=login'
85
+ end
86
+ ```
87
+
88
+ Alternatively, these environment variables will be picked up automatically:
89
+
90
+ - `OPENID_AUTHORIZATION_ENDPOINT`
91
+ - `OPENID_AUTHORIZATION_URI`
92
+ - `OPENID_CLIENT_ID`
93
+ - `OPENID_CLIENT_SECRET`
94
+ - `OPENID_DOMAIN_HINT`
95
+ - `OPENID_ISSUER`
96
+ - `OPENID_PROMPT`
97
+ - `OPENID_REDIRECT_URI`
98
+ - `OPENID_RESOURCE`
99
+ - `OPENID_TOKEN_ENDPOINT`
100
+ - `OPENID_USERINFO_ENDPOINT`
101
+
102
+
103
+ ### Token acquirement
104
+
105
+ OpenID token proxy's main task is to obtain tokens on behalf of users. To allow it
106
+ to do so, start by mounting the engine in your Rails application:
107
+
108
+ ```ruby
109
+ Rails.application.routes.draw do
110
+ mount OpenIDTokenProxy::Engine, at: '/auth'
111
+ end
112
+ ```
113
+
114
+ Next, register the engine's callback - `https://example.com/auth/callback` - as
115
+ the redirect URL of your OpenID application on the issuer so that any authorization
116
+ requests are routed back to your application.
117
+
118
+ The proxy itself also needs to be configured with a redirect URL in order for it
119
+ to know what to do with any newly obtained tokens. To boot back into a native
120
+ applicaton one could use custom URL schemes or intents:
121
+
122
+ ```ruby
123
+ OpenIDTokenProxy.configure do |config|
124
+ config.token_acquirement_hook = proc { |token|
125
+ "my-app://?token=#{token}&refresh_token=#{token.refresh_token}"
126
+ }
127
+ end
128
+ ```
129
+
130
+
131
+ ### Token authentication
132
+
133
+ Additionally, OpenID token proxy ships with an authentication module simplifying
134
+ token validation for use in APIs:
135
+
136
+ ```ruby
137
+ class AccountsController < ApplicationController
138
+ include OpenIDTokenProxy::Token::Authentication
139
+
140
+ require_valid_token
141
+
142
+ ...
143
+ end
144
+ ```
145
+
146
+ Access tokens may be provided with one of the following:
147
+
148
+ - `X-Token` header.
149
+ - `Authorization: Bearer <token>` header.
150
+ - Query string parameter `token`.
151
+
152
+
153
+ #### Identity / claims
154
+
155
+ A valid token is exposed to a controller as `current_token` and identity information
156
+ can be extracted by providing a claim name through hash-syntax:
157
+
158
+ ```ruby
159
+ current_token['email']
160
+ ```
161
+
162
+ Identity providers may support additional claims beyond the [standard OpenID ones](http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims).
163
+
164
+
165
+ ### Token refreshing
166
+
167
+ Most identity providers issue access tokens [with short lifespans](http://openid.net/specs/openid-connect-core-1_0.html#TokenLifetime).
168
+ To prevent users from having to authenticate often, refresh tokens are used to
169
+ obtain new access tokens without user intervention.
170
+
171
+ OpenID token proxy's token refresh module does just that:
172
+
173
+ ```ruby
174
+ class AccountsController < ApplicationController
175
+ include OpenIDTokenProxy::Token::Authentication
176
+ include OpenIDTokenProxy::Token::Refresh
177
+
178
+ require_valid_token
179
+
180
+ ...
181
+ end
182
+ ```
183
+
184
+ Refresh tokens may be provided with one of the following:
185
+
186
+ - `X-Refresh-Token` header.
187
+ - Query string parameter `refresh_token`.
188
+
189
+ Whenever an access token has expired and a refresh token is given, the module will
190
+ attempt to obtain a new token transparently.
191
+
192
+ The following headers will be present on the API response if, **and only if**, a new
193
+ token was obtained:
194
+
195
+ - `X-Token` header containing the new access token to be used in future requests.
196
+ - `X-Refresh-Token` header containing the new refresh token.
197
+
198
+
199
+ ## Contributing
200
+
201
+ 1. Fork it
202
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
203
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
204
+ 4. Push to the branch (`git push origin my-new-feature`)
205
+ 5. Create a pull request
206
+
207
+
208
+ ## Credits
209
+
210
+ Hyper made this. We're a digital communications agency with a passion for good code,
211
+ and if you're using this library we probably [want to hire you](http://hyper.no/jobs).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ Bundler::GemHelper.install_tasks
8
+
9
+ require 'rspec/core/rake_task'
10
+
11
+ RSpec::Core::RakeTask.new(:spec) do |spec|
12
+ spec.pattern = 'spec/**/*_spec.rb'
13
+ end
14
+
15
+ task test: :spec
16
+ task default: :spec
@@ -0,0 +1,4 @@
1
+ module OpenIDTokenProxy
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,22 @@
1
+ module OpenIDTokenProxy
2
+ class CallbackController < ApplicationController
3
+ def handle
4
+ unless code = params[:code]
5
+ render text: "Required parameter 'code' missing.", status: :bad_request
6
+ return
7
+ end
8
+
9
+ begin
10
+ token = OpenIDTokenProxy.client.retrieve_token!(auth_code: code)
11
+ rescue OpenIDTokenProxy::Client::AuthCodeError => error
12
+ render text: "Could not exchange authorization code: #{error.message}.",
13
+ status: :bad_request
14
+ return
15
+ end
16
+
17
+ config = OpenIDTokenProxy.config
18
+ uri = instance_exec token, &config.token_acquirement_hook
19
+ redirect_to uri || main_app.root_url unless performed?
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
2
+ inflect.acronym 'OpenID'
3
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ OpenIDTokenProxy::Engine.routes.draw do
2
+ get :callback, to: 'callback#handle'
3
+ end
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ require 'openid_token_proxy'
@@ -0,0 +1,48 @@
1
+ module OpenIDTokenProxy
2
+ class Client
3
+ attr_accessor :config
4
+
5
+ def initialize(config = OpenIDTokenProxy.config)
6
+ @config = config
7
+ end
8
+
9
+ def authorization_uri
10
+ config.authorization_uri || new_client.authorization_uri(
11
+ domain_hint: config.domain_hint,
12
+ prompt: config.prompt,
13
+ resource: config.resource
14
+ )
15
+ end
16
+
17
+ # Raised when auth code could not be exchanged
18
+ class AuthCodeError < Error; end
19
+
20
+ # Raised when refresh token could not be exchanged
21
+ class RefreshTokenError < Error; end
22
+
23
+ # Retrieves a token for given authorization code or refresh token
24
+ def retrieve_token!(params)
25
+ client = new_client
26
+ client.authorization_code = params[:auth_code] if params[:auth_code]
27
+ client.refresh_token = params[:refresh_token] if params[:refresh_token]
28
+ response = client.access_token!(:query_string)
29
+ token = Token.decode!(response.access_token)
30
+ token.refresh_token = response.refresh_token
31
+ token
32
+ rescue Rack::OAuth2::Client::Error => e
33
+ raise AuthCodeError.new(e.message) if params[:auth_code]
34
+ raise RefreshTokenError.new(e.message) if params[:refresh_token]
35
+ end
36
+
37
+ def new_client
38
+ OpenIDConnect::Client.new(
39
+ identifier: config.client_id,
40
+ secret: config.client_secret,
41
+ authorization_endpoint: config.authorization_endpoint,
42
+ token_endpoint: config.token_endpoint,
43
+ userinfo_endpoint: config.userinfo_endpoint,
44
+ redirect_uri: config.redirect_uri
45
+ )
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,56 @@
1
+ require 'openid_connect'
2
+
3
+ module OpenIDTokenProxy
4
+ class Config
5
+ attr_accessor :client_id, :client_secret, :issuer
6
+ attr_accessor :domain_hint, :prompt, :redirect_uri, :resource
7
+ attr_accessor :authorization_uri
8
+ attr_accessor :authorization_endpoint, :token_endpoint, :userinfo_endpoint
9
+ attr_accessor :token_acquirement_hook
10
+ attr_accessor :public_keys
11
+
12
+ def initialize
13
+ @client_id = ENV['OPENID_CLIENT_ID']
14
+ @client_secret = ENV['OPENID_CLIENT_SECRET']
15
+ @issuer = ENV['OPENID_ISSUER']
16
+
17
+ @domain_hint = ENV['OPENID_DOMAIN_HINT']
18
+ @prompt = ENV['OPENID_PROMPT']
19
+ @redirect_uri = ENV['OPENID_REDIRECT_URI']
20
+ @resource = ENV['OPENID_RESOURCE']
21
+
22
+ @authorization_uri = ENV['OPENID_AUTHORIZATION_URI']
23
+
24
+ @authorization_endpoint = ENV['OPENID_AUTHORIZATION_ENDPOINT']
25
+ @token_endpoint = ENV['OPENID_TOKEN_ENDPOINT']
26
+ @userinfo_endpoint = ENV['OPENID_USERINFO_ENDPOINT']
27
+
28
+ @token_acquirement_hook = proc { }
29
+
30
+ yield self if block_given?
31
+ end
32
+
33
+ def provider_config
34
+ # TODO: Add support for refreshing provider configuration
35
+ @provider_config ||= begin
36
+ OpenIDConnect::Discovery::Provider::Config.discover! issuer
37
+ end
38
+ end
39
+
40
+ def authorization_endpoint
41
+ @authorization_endpoint || provider_config.authorization_endpoint
42
+ end
43
+
44
+ def token_endpoint
45
+ @token_endpoint || provider_config.token_endpoint
46
+ end
47
+
48
+ def userinfo_endpoint
49
+ @userinfo_endpoint || provider_config.userinfo_endpoint
50
+ end
51
+
52
+ def public_keys
53
+ @public_keys ||= provider_config.public_keys
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ module OpenIDTokenProxy
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace OpenIDTokenProxy
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ module OpenIDTokenProxy
2
+ class Error < StandardError
3
+ def to_json
4
+ { error: message }
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,54 @@
1
+ require 'active_support/concern'
2
+
3
+ module OpenIDTokenProxy
4
+ class Token
5
+ module Authentication
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ rescue_from OpenIDTokenProxy::Error, with: :require_authorization
10
+
11
+ helper_method :current_token, :raw_token
12
+ end
13
+
14
+ module ClassMethods
15
+ def require_valid_token(*args)
16
+ before_action :require_valid_token, *args
17
+ end
18
+ end
19
+
20
+ def set_authentication_url!
21
+ uri = OpenIDTokenProxy.client.authorization_uri
22
+ response.headers['X-Authentication-URL'] = uri
23
+ end
24
+
25
+ def require_authorization(exception)
26
+ set_authentication_url!
27
+ render json: exception.to_json, status: :unauthorized
28
+ end
29
+
30
+ def require_valid_token
31
+ config = OpenIDTokenProxy.config
32
+ current_token.validate! audience: config.resource,
33
+ client_id: config.client_id
34
+ end
35
+
36
+ def current_token
37
+ @current_token ||= OpenIDTokenProxy::Token.decode!(raw_token)
38
+ end
39
+
40
+ def raw_token
41
+ token = params[:token]
42
+ return token if token
43
+
44
+ authorization = request.headers['Authorization']
45
+ if authorization
46
+ token = authorization[/Bearer (.+)/, 1]
47
+ return token if token
48
+ end
49
+
50
+ request.headers['X-Token']
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+ module OpenIDTokenProxy
2
+ class Token
3
+
4
+ # Raised when a token has expired
5
+ class Expired < Error
6
+ def initialize
7
+ super 'Token has expired.'
8
+ end
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module OpenIDTokenProxy
2
+ class Token
3
+
4
+ # Raised when a token's application did not match
5
+ class InvalidApplication < Error
6
+ def initialize
7
+ super 'Token is not intended for this application.'
8
+ end
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module OpenIDTokenProxy
2
+ class Token
3
+
4
+ # Raised when a token's audience did not match
5
+ class InvalidAudience < Error
6
+ def initialize
7
+ super 'Token was issued for an unexpected audience/resource.'
8
+ end
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module OpenIDTokenProxy
2
+ class Token
3
+
4
+ # Raised when a token's issuer did not match
5
+ class InvalidIssuer < Error
6
+ def initialize
7
+ super 'Token was issued by an unexpected issuer.'
8
+ end
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module OpenIDTokenProxy
2
+ class Token
3
+
4
+ # Raised when a token could not be decoded
5
+ class Malformed < Error
6
+ def initialize(message)
7
+ super "Token is malformed: #{message}."
8
+ end
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,30 @@
1
+ require 'active_support/concern'
2
+
3
+ module OpenIDTokenProxy
4
+ class Token
5
+ module Refresh
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include OpenIDTokenProxy::Token::Authentication
10
+
11
+ helper_method :raw_refresh_token
12
+
13
+ def require_valid_token
14
+ super
15
+ rescue OpenIDTokenProxy::Token::Expired
16
+ raise unless raw_refresh_token.present?
17
+ @current_token = OpenIDTokenProxy.client.retrieve_token!(
18
+ refresh_token: raw_refresh_token
19
+ )
20
+ response.headers['X-Token'] = current_token.access_token
21
+ response.headers['X-Refresh-Token'] = current_token.refresh_token
22
+ end
23
+ end
24
+
25
+ def raw_refresh_token
26
+ params[:refresh_token] || request.headers['X-Refresh-Token']
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ module OpenIDTokenProxy
2
+ class Token
3
+
4
+ # Raised when a token was not provided
5
+ class Required < Error
6
+ def initialize
7
+ super 'Token must be provided.'
8
+ end
9
+ end
10
+
11
+ end
12
+ end