openid-token-proxy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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