himari 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.lock +152 -0
  5. data/LICENSE.txt +21 -0
  6. data/Rakefile +8 -0
  7. data/himari.gemspec +44 -0
  8. data/lib/himari/access_token.rb +119 -0
  9. data/lib/himari/app.rb +193 -0
  10. data/lib/himari/authorization_code.rb +83 -0
  11. data/lib/himari/client_registration.rb +47 -0
  12. data/lib/himari/config.rb +39 -0
  13. data/lib/himari/decisions/authentication.rb +16 -0
  14. data/lib/himari/decisions/authorization.rb +48 -0
  15. data/lib/himari/decisions/base.rb +63 -0
  16. data/lib/himari/decisions/claims.rb +58 -0
  17. data/lib/himari/id_token.rb +57 -0
  18. data/lib/himari/item_provider.rb +11 -0
  19. data/lib/himari/item_providers/static.rb +20 -0
  20. data/lib/himari/log_line.rb +9 -0
  21. data/lib/himari/middlewares/authentication_rule.rb +24 -0
  22. data/lib/himari/middlewares/authorization_rule.rb +24 -0
  23. data/lib/himari/middlewares/claims_rule.rb +24 -0
  24. data/lib/himari/middlewares/client.rb +24 -0
  25. data/lib/himari/middlewares/config.rb +24 -0
  26. data/lib/himari/middlewares/signing_key.rb +24 -0
  27. data/lib/himari/provider_chain.rb +26 -0
  28. data/lib/himari/rule.rb +7 -0
  29. data/lib/himari/rule_processor.rb +81 -0
  30. data/lib/himari/services/downstream_authorization.rb +73 -0
  31. data/lib/himari/services/jwks_endpoint.rb +40 -0
  32. data/lib/himari/services/oidc_authorization_endpoint.rb +82 -0
  33. data/lib/himari/services/oidc_provider_metadata_endpoint.rb +56 -0
  34. data/lib/himari/services/oidc_token_endpoint.rb +86 -0
  35. data/lib/himari/services/oidc_userinfo_endpoint.rb +73 -0
  36. data/lib/himari/services/upstream_authentication.rb +106 -0
  37. data/lib/himari/session_data.rb +7 -0
  38. data/lib/himari/signing_key.rb +128 -0
  39. data/lib/himari/storages/base.rb +57 -0
  40. data/lib/himari/storages/filesystem.rb +36 -0
  41. data/lib/himari/storages/memory.rb +31 -0
  42. data/lib/himari/version.rb +5 -0
  43. data/lib/himari.rb +4 -0
  44. data/public/public/index.css +74 -0
  45. data/sig/himari.rbs +4 -0
  46. data/views/login.erb +37 -0
  47. metadata +174 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9534716186569cef82a629dd4c15fdcc2f30b6b79712445ef67156b33c99a299
4
+ data.tar.gz: 8243f15a8bdf914c8b73447aab430a243b0dffea2ea2b84af032d92de3fcdc49
5
+ SHA512:
6
+ metadata.gz: 30386ab57b39997a8634f63a99466f7efad64d0596767c8ad5f2123e877a1cd3b85f97f5ab29eb6a40d534a223582f56294220c5b3b9b5a873a5a61e928885d5
7
+ data.tar.gz: 207828253833d92b746ed271a8118e3389b9bbc51302ad57cf99bfb7dd339e337b27a770d6c63ebaa02e31c21264ca0b923fdee9ac3d28e5b55280cfd033aadf
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'rake'
5
+
6
+ group :test do
7
+ gem 'rspec'
8
+ gem 'simplecov'
9
+ gem 'simplecov-html'
10
+ gem 'rack-test'
11
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,152 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ himari (0.1.0)
5
+ addressable
6
+ omniauth (>= 2.0)
7
+ openid_connect
8
+ rack-oauth2
9
+ rack-protection
10
+ sinatra (>= 3.0)
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ activemodel (7.0.4.3)
16
+ activesupport (= 7.0.4.3)
17
+ activesupport (7.0.4.3)
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ i18n (>= 1.6, < 2)
20
+ minitest (>= 5.1)
21
+ tzinfo (~> 2.0)
22
+ addressable (2.8.1)
23
+ public_suffix (>= 2.0.2, < 6.0)
24
+ aes_key_wrap (1.1.0)
25
+ attr_required (1.0.1)
26
+ bindata (2.4.15)
27
+ concurrent-ruby (1.2.2)
28
+ date (3.3.3)
29
+ diff-lcs (1.5.0)
30
+ docile (1.4.0)
31
+ faraday (2.7.4)
32
+ faraday-net_http (>= 2.0, < 3.1)
33
+ ruby2_keywords (>= 0.0.4)
34
+ faraday-follow_redirects (0.3.0)
35
+ faraday (>= 1, < 3)
36
+ faraday-net_http (3.0.2)
37
+ hashie (5.0.0)
38
+ i18n (1.12.0)
39
+ concurrent-ruby (~> 1.0)
40
+ json-jwt (1.16.3)
41
+ activesupport (>= 4.2)
42
+ aes_key_wrap
43
+ bindata
44
+ faraday (~> 2.0)
45
+ faraday-follow_redirects
46
+ mail (2.8.1)
47
+ mini_mime (>= 0.1.1)
48
+ net-imap
49
+ net-pop
50
+ net-smtp
51
+ mini_mime (1.1.2)
52
+ minitest (5.18.0)
53
+ mustermann (3.0.0)
54
+ ruby2_keywords (~> 0.0.1)
55
+ net-imap (0.3.4)
56
+ date
57
+ net-protocol
58
+ net-pop (0.1.2)
59
+ net-protocol
60
+ net-protocol (0.2.1)
61
+ timeout
62
+ net-smtp (0.3.3)
63
+ net-protocol
64
+ omniauth (2.1.1)
65
+ hashie (>= 3.4.6)
66
+ rack (>= 2.2.3)
67
+ rack-protection
68
+ openid_connect (2.2.0)
69
+ activemodel
70
+ attr_required (>= 1.0.0)
71
+ faraday (~> 2.0)
72
+ faraday-follow_redirects
73
+ json-jwt (>= 1.16)
74
+ net-smtp
75
+ rack-oauth2 (~> 2.2)
76
+ swd (~> 2.0)
77
+ tzinfo
78
+ validate_email
79
+ validate_url
80
+ webfinger (~> 2.0)
81
+ public_suffix (5.0.1)
82
+ rack (2.2.6.4)
83
+ rack-oauth2 (2.2.0)
84
+ activesupport
85
+ attr_required
86
+ faraday (~> 2.0)
87
+ faraday-follow_redirects
88
+ json-jwt (>= 1.11.0)
89
+ rack (>= 2.1.0)
90
+ rack-protection (3.0.5)
91
+ rack
92
+ rack-test (2.1.0)
93
+ rack (>= 1.3)
94
+ rake (13.0.6)
95
+ rspec (3.12.0)
96
+ rspec-core (~> 3.12.0)
97
+ rspec-expectations (~> 3.12.0)
98
+ rspec-mocks (~> 3.12.0)
99
+ rspec-core (3.12.1)
100
+ rspec-support (~> 3.12.0)
101
+ rspec-expectations (3.12.2)
102
+ diff-lcs (>= 1.2.0, < 2.0)
103
+ rspec-support (~> 3.12.0)
104
+ rspec-mocks (3.12.4)
105
+ diff-lcs (>= 1.2.0, < 2.0)
106
+ rspec-support (~> 3.12.0)
107
+ rspec-support (3.12.0)
108
+ ruby2_keywords (0.0.5)
109
+ simplecov (0.22.0)
110
+ docile (~> 1.1)
111
+ simplecov-html (~> 0.11)
112
+ simplecov_json_formatter (~> 0.1)
113
+ simplecov-html (0.12.3)
114
+ simplecov_json_formatter (0.1.4)
115
+ sinatra (3.0.5)
116
+ mustermann (~> 3.0)
117
+ rack (~> 2.2, >= 2.2.4)
118
+ rack-protection (= 3.0.5)
119
+ tilt (~> 2.0)
120
+ swd (2.0.2)
121
+ activesupport (>= 3)
122
+ attr_required (>= 0.0.5)
123
+ faraday (~> 2.0)
124
+ faraday-follow_redirects
125
+ tilt (2.1.0)
126
+ timeout (0.3.2)
127
+ tzinfo (2.0.6)
128
+ concurrent-ruby (~> 1.0)
129
+ validate_email (0.1.6)
130
+ activemodel (>= 3.0)
131
+ mail (>= 2.2.5)
132
+ validate_url (1.0.15)
133
+ activemodel (>= 3.0.0)
134
+ public_suffix
135
+ webfinger (2.1.2)
136
+ activesupport
137
+ faraday (~> 2.0)
138
+ faraday-follow_redirects
139
+
140
+ PLATFORMS
141
+ ruby
142
+
143
+ DEPENDENCIES
144
+ himari!
145
+ rack-test
146
+ rake
147
+ rspec
148
+ simplecov
149
+ simplecov-html
150
+
151
+ BUNDLED WITH
152
+ 2.4.8
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Sorah Fukumori
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/himari.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/himari/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "himari"
7
+ spec.version = Himari::VERSION
8
+ spec.authors = ["Sorah Fukumori"]
9
+ spec.email = ["her@sorah.jp"]
10
+
11
+ spec.summary = "Small OIDC IdP for small teams - Omniauth to OIDC"
12
+ spec.homepage = "https://github.com/sorah/himari"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.7.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/sorah/himari"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
24
+ end
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "sinatra", '>= 3.0'
31
+ spec.add_dependency 'rack-protection'
32
+ spec.add_dependency "omniauth", ">= 2.0"
33
+
34
+ spec.add_dependency 'addressable'
35
+
36
+ spec.add_dependency "rack-oauth2"
37
+ spec.add_dependency "openid_connect"
38
+
39
+ # Uncomment to register a new dependency of your gem
40
+ # spec.add_dependency "example-gem", "~> 1.0"
41
+
42
+ # For more information and examples about making a new gem, check out our
43
+ # guide at: https://bundler.io/guides/creating_gem.html
44
+ end
@@ -0,0 +1,119 @@
1
+ require 'securerandom'
2
+ require 'base64'
3
+ require 'digest/sha2'
4
+ require 'rack/utils'
5
+
6
+ require 'rack/oauth2'
7
+ require 'openid_connect'
8
+
9
+ module Himari
10
+ class AccessToken
11
+ class SecretMissing < StandardError; end
12
+ class SecretIncorrect < StandardError; end
13
+ class TokenExpired < StandardError; end
14
+ class InvalidFormat < StandardError; end
15
+
16
+ Format = Struct.new(:handler, :secret, keyword_init: true) do
17
+ HEADER = 'hmat'
18
+
19
+ def self.parse(str)
20
+ parts = str.split('.')
21
+ raise InvalidFormat unless parts.size == 3
22
+ raise InvalidFormat unless parts[0] == HEADER
23
+ new(handler: parts[1], secret: parts[2])
24
+ end
25
+
26
+ def to_s
27
+ "#{HEADER}.#{handler}.#{secret}"
28
+ end
29
+ end
30
+
31
+ class Bearer < Rack::OAuth2::AccessToken::Bearer
32
+ def token_response(options = {})
33
+ super.tap do |r|
34
+ r[:token_type] = 'Bearer' # https://github.com/nov/openid_connect_sample/blob/a5b7ee5b63508d99a3a36b4537809dfa64ba3b1f/lib/token_endpoint.rb#L37
35
+ end
36
+ end
37
+ end
38
+
39
+ def self.make(**kwargs)
40
+ new(
41
+ handler: SecureRandom.urlsafe_base64(32),
42
+ secret: SecureRandom.urlsafe_base64(32),
43
+ expiry: Time.now.to_i + 3600,
44
+ **kwargs
45
+ )
46
+ end
47
+
48
+ # @param authz [Himari::AuthorizationCode]
49
+ def self.from_authz(authz)
50
+ make(
51
+ client_id: authz.client_id,
52
+ claims: authz.claims,
53
+ )
54
+ end
55
+
56
+ def initialize(handler:, client_id:, claims:, expiry:, secret: nil, secret_hash: nil)
57
+ @handler = handler
58
+ @client_id = client_id
59
+ @claims = claims
60
+ @expiry = expiry
61
+
62
+ @secret = secret
63
+ @secret_hash = secret_hash
64
+ end
65
+
66
+ attr_reader :handler, :client_id, :claims, :expiry
67
+
68
+ def secret
69
+ raise SecretMissing unless @secret
70
+ @secret
71
+ end
72
+
73
+ def secret_hash
74
+ @secret_hash ||= Base64.urlsafe_encode64(Digest::SHA384.digest(secret), padding: false)
75
+ end
76
+
77
+ def verify_secret!(given_secret)
78
+ dgst = Base64.urlsafe_decode64(secret_hash)
79
+ given_dgst = Digest::SHA384.digest(given_secret)
80
+ raise SecretIncorrect unless Rack::Utils.secure_compare(dgst, given_dgst)
81
+ @secret = given_secret
82
+ true
83
+ end
84
+
85
+ def verify_expiry!(now = Time.now)
86
+ raise TokenExpired if @expiry <= now.to_i
87
+ end
88
+
89
+ def format
90
+ Format.new(handler: handler, secret: secret)
91
+ end
92
+
93
+ def to_bearer
94
+ Bearer.new(
95
+ access_token: format.to_s,
96
+ expires_in: (expiry - Time.now.to_i).to_i,
97
+ )
98
+ end
99
+
100
+ def as_log
101
+ {
102
+ handler_dgst: Digest::SHA256.hexdigest(handler),
103
+ client_id: client_id,
104
+ claims: claims,
105
+ expiry: expiry,
106
+ }
107
+ end
108
+
109
+ def as_json
110
+ {
111
+ handler: handler,
112
+ secret_hash: secret_hash,
113
+ client_id: client_id,
114
+ claims: claims,
115
+ expiry: expiry.to_i,
116
+ }
117
+ end
118
+ end
119
+ end
data/lib/himari/app.rb ADDED
@@ -0,0 +1,193 @@
1
+ require 'sinatra/base'
2
+ require 'addressable'
3
+
4
+ require 'himari/log_line'
5
+
6
+ require 'himari/provider_chain'
7
+ require 'himari/authorization_code'
8
+
9
+ require 'himari/middlewares/client'
10
+ require 'himari/middlewares/config'
11
+ require 'himari/middlewares/signing_key'
12
+
13
+ require 'himari/services/downstream_authorization'
14
+ require 'himari/services/upstream_authentication'
15
+
16
+ require 'himari/services/jwks_endpoint'
17
+ require 'himari/services/oidc_authorization_endpoint'
18
+ require 'himari/services/oidc_provider_metadata_endpoint'
19
+ require 'himari/services/oidc_token_endpoint'
20
+ require 'himari/services/oidc_userinfo_endpoint'
21
+
22
+ module Himari
23
+ class App < Sinatra::Base
24
+ set :root, File.expand_path(File.join(__dir__, '..', '..'))
25
+
26
+ set :protection, use: %i(authenticity_token), except: %i(remote_token)
27
+ set :logging, nil
28
+
29
+ ProviderCandidate = Struct.new(:name, :button, :action, keyword_init: true)
30
+
31
+ helpers do
32
+ def current_user
33
+ session[:session_data]
34
+ end
35
+
36
+ def config
37
+ env[Himari::Middlewares::Config::RACK_KEY]
38
+ end
39
+
40
+ def signing_key_provider
41
+ Himari::ProviderChain.new(request.env[Himari::Middlewares::SigningKey::RACK_KEY] || [])
42
+ end
43
+
44
+ def client_provider
45
+ Himari::ProviderChain.new(request.env[Himari::Middlewares::Client::RACK_KEY] || [])
46
+ end
47
+
48
+ def known_providers
49
+ query = Addressable::URI.form_encode(back_to: request.fullpath)
50
+ config.providers.map do |pr|
51
+ name = pr.fetch(:name)
52
+ ProviderCandidate.new(
53
+ name: name,
54
+ button: pr[:button] || "Log in with #{name}",
55
+ action: "/auth/#{name}?#{query}",
56
+ )
57
+ end
58
+ end
59
+
60
+ def csrf_token_value
61
+ Rack::Protection::AuthenticityToken.token(session)
62
+ end
63
+
64
+ def csrf_token_name
65
+ 'authenticity_token'
66
+ end
67
+
68
+ def cachebuster
69
+ env['himari.cachebuster'] || "#{Process.pid}"
70
+ end
71
+
72
+ def request_id
73
+ env['HTTP_X_REQUEST_ID'] ||= SecureRandom.uuid
74
+ end
75
+
76
+ def request_as_log
77
+ env['himari.request_as_log'] ||= {
78
+ id: request_id,
79
+ method: request.request_method,
80
+ path: request.path,
81
+ ip: request.ip,
82
+ cip: env['REMOTE_ADDR'],
83
+ xff: env['HTTP_X_FORWARDED_FOR'],
84
+ }
85
+ end
86
+ end
87
+
88
+ before do
89
+ request_as_log()
90
+ end
91
+
92
+ get '/' do
93
+ content_type :text
94
+ "Himari\n"
95
+ end
96
+
97
+ get '/oidc/authorize' do
98
+ client = client_provider.find(id: params[:client_id])
99
+ unless client
100
+ logger&.warn(Himari::LogLine.new('authorize: no client registration found', req: request_as_log, client_id: params[:client_id]))
101
+ next halt 401, 'unknown client'
102
+ end
103
+ if current_user
104
+ # do downstream authz and process oidc request
105
+ decision = Himari::Services::DownstreamAuthorization.from_request(session: current_user, client: client, request: request).perform
106
+ logger&.info(Himari::LogLine.new('authorize: downstream authorized', req: request_as_log, allowed: decision.authz_result.allowed, result: decision.as_log))
107
+ raise unless decision.authz_result.allowed # sanity check
108
+
109
+ authz = AuthorizationCode.make(
110
+ client_id: decision.client.id,
111
+ claims: decision.claims,
112
+ )
113
+
114
+ Himari::Services::OidcAuthorizationEndpoint.new(
115
+ authz: authz,
116
+ client: client,
117
+ storage: config.storage,
118
+ logger: logger,
119
+ ).call(env)
120
+ else
121
+ logger&.info(Himari::LogLine.new('authorize: prompt login', req: request_as_log, client_id: params[:client_id]))
122
+ erb :login
123
+ end
124
+ rescue Himari::Services::DownstreamAuthorization::ForbiddenError => e
125
+ logger&.warn(Himari::LogLine.new('authorize: downstream forbidden', req: request_as_log, allowed: e.result.authz_result.allowed, err: e.class.inspect, result: e.as_log))
126
+ halt 403, "Forbidden"
127
+ end
128
+
129
+ token_ep = proc do
130
+ Himari::Services::OidcTokenEndpoint.new(
131
+ client_provider: client_provider,
132
+ signing_key_provider: signing_key_provider,
133
+ storage: config.storage,
134
+ issuer: config.issuer,
135
+ logger: logger,
136
+ ).call(env)
137
+ end
138
+ post '/oidc/token', &token_ep
139
+ post '/public/oidc/token', &token_ep
140
+
141
+ userinfo_ep = proc do
142
+ Himari::Services::OidcUserinfoEndpoint.new(
143
+ storage: config.storage,
144
+ logger: logger,
145
+ ).call(env)
146
+ end
147
+ get '/oidc/userinfo', &userinfo_ep
148
+ get '/public/oidc/userinfo', &userinfo_ep
149
+
150
+
151
+ jwks_ep = proc do
152
+ Himari::Services::JwksEndpoint.new(
153
+ signing_key_provider: signing_key_provider,
154
+ ).call(env)
155
+ end
156
+ get '/jwks', &jwks_ep
157
+ get '/public/jwks', &jwks_ep
158
+
159
+ get '/.well-known/openid-configuration' do
160
+ Himari::Services::OidcProviderMetadataEndpoint.new(
161
+ signing_key_provider: signing_key_provider,
162
+ issuer: config.issuer,
163
+ ).call(env)
164
+ end
165
+
166
+ omniauth_callback = proc do
167
+ # do upstream auth
168
+ authn = Himari::Services::UpstreamAuthentication.from_request(request).perform
169
+ logger&.info(Himari::LogLine.new('authentication allowed', req: request_as_log, allowed: authn.authn_result.allowed, uid: request.env.fetch('omniauth.auth')[:uid], provider: request.env.fetch('omniauth.auth')[:provider], result: authn.as_log))
170
+ raise unless authn.authn_result.allowed # sanity check
171
+
172
+ given_back_to = request.env['omniauth.params']&.fetch('back_to', nil)
173
+ back_to = if given_back_to
174
+ uri = Addressable::URI.parse(given_back_to)
175
+ if uri && uri.host.nil? && uri.scheme.nil? && uri.path.start_with?('/')
176
+ given_back_to
177
+ else
178
+ logger&.warn(Himari::LogLine.new('invalid back_to', req: request_as_log, given_back_to: given_back_to))
179
+ nil
180
+ end
181
+ end || '/'
182
+
183
+ session.destroy
184
+ session[:session_data] = authn.session_data
185
+ redirect back_to
186
+ rescue Himari::Services::UpstreamAuthentication::UnauthorizedError => e
187
+ logger&.warn(Himari::LogLine.new('authentication denied', req: request_as_log, err: e.class.inspect, allowed: e.result.authn_result.allowed, uid: request.env.fetch('omniauth.auth')[:uid], provider: request.env.fetch('omniauth.auth')[:provider], result: e.as_log))
188
+ halt(401, 'Unauthorized')
189
+ end
190
+ get '/auth/:provider/callback', &omniauth_callback
191
+ post '/auth/:provider/callback', &omniauth_callback
192
+ end
193
+ end
@@ -0,0 +1,83 @@
1
+ require 'digest/sha2'
2
+
3
+ module Himari
4
+ authz_attrs = %i(
5
+ code
6
+ client_id
7
+ claims
8
+ openid
9
+ redirect_uri
10
+ nonce
11
+ code_challenge
12
+ code_challenge_method
13
+ expiry
14
+ )
15
+ AuthorizationCode = Struct.new(*authz_attrs, keyword_init: true) do
16
+ def self.make(**kwargs)
17
+ new(
18
+ code: SecureRandom.urlsafe_base64(32),
19
+ expiry: Time.now.to_i + 900,
20
+ **kwargs,
21
+ )
22
+ end
23
+
24
+ def valid_redirect_uri?(given_uri)
25
+ redirect_uri == given_uri
26
+ end
27
+
28
+ def pkce?
29
+ !!(code_challenge && code_challenge_method)
30
+ end
31
+
32
+ def pkce_known_method?
33
+ # https://datatracker.ietf.org/doc/html/rfc7636#section-4.2
34
+ %w(S256 plain).include?(code_challenge_method.to_s)
35
+ end
36
+
37
+ def pkce_valid_challenge?
38
+ # https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
39
+ case code_challenge_method.to_s
40
+ when 'plain'
41
+ (43..128).cover?(code_challenge.size)
42
+ when 'S256'
43
+ (43..45).cover?(code_challenge.size)
44
+ end
45
+ end
46
+
47
+ def pkce_valid_request?
48
+ pkce? && pkce_known_method? && pkce_valid_challenge?
49
+ end
50
+
51
+ def code_dgst_for_log
52
+ @code_dgst_for_log ||= code ? Digest::SHA256.hexdigest(code) : nil
53
+ end
54
+
55
+ def as_log
56
+ {
57
+ code_dgst: code_dgst_for_log,
58
+ client_id: client_id,
59
+ claims: claims,
60
+ nonce: nonce,
61
+ openid: openid,
62
+ expiry: expiry.to_i,
63
+ pkce: pkce?,
64
+ pkce_method: code_challenge_method,
65
+ pkce_valid_chal: pkce_valid_challenge?,
66
+ }
67
+ end
68
+
69
+ def as_json
70
+ {
71
+ code: code,
72
+ client_id: client_id,
73
+ claims: claims,
74
+ openid: openid,
75
+ redirect_uri: redirect_uri,
76
+ nonce: nonce,
77
+ code_challenge: code_challenge,
78
+ code_challenge_method: code_challenge_method,
79
+ expiry: expiry.to_i,
80
+ }
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,47 @@
1
+ require 'digest/sha2'
2
+
3
+ module Himari
4
+ class ClientRegistration
5
+ def initialize(name:, id:, secret: nil, secret_hash: nil, redirect_uris:, preferred_key_group: nil)
6
+ @name = name
7
+ @id = id
8
+ @secret = secret
9
+ @secret_hash = secret_hash
10
+ @redirect_uris = redirect_uris
11
+ @preferred_key_group = preferred_key_group
12
+
13
+ raise ArgumentError, "either secret or secret_hash must be present" if !@secret && !@secret_hash
14
+ end
15
+
16
+ attr_reader :name, :id, :redirect_uris, :preferred_key_group
17
+
18
+ def secret_hash
19
+ @secret_hash ||= Digest::SHA384.hexdigest(secret)
20
+ end
21
+
22
+ def match_secret?(given_secret)
23
+ if @secret
24
+ Rack::Utils.secure_compare(@secret, given_secret)
25
+ else
26
+ dgst = [secret_hash].pack('H*')
27
+ Rack::Utils.secure_compare(dgst, Digest::SHA384.digest(given_secret))
28
+ end
29
+ end
30
+
31
+ def as_log
32
+ {name: name, id: id}
33
+ end
34
+
35
+ def match_hint?(id: nil)
36
+ result = true
37
+
38
+ result &&= if id
39
+ id == self.id
40
+ else
41
+ true
42
+ end
43
+
44
+ result
45
+ end
46
+ end
47
+ end