rack-oidc-api 0.0.2

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 (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/rack-oidc-api/middleware.rb +173 -0
  3. metadata +74 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7873572d744e42c94d3de3fee764ed8da0bbb195
4
+ data.tar.gz: 6280ec7b6e5dbd384569d74616f12723cfea9161
5
+ SHA512:
6
+ metadata.gz: 663cd76417eb3b1c66d98671cdb81aa428e2db9584026071959bba36b3b3dc084bed894c01bc641d639ba89df895792c5713bd9358ef2a89db2fad4b9af45b8e
7
+ data.tar.gz: 0dbb6c08bbb125b4fea95012267637b82c35cbb9bdba1b5dd5d077e6f1250d314c7957f0b20ca2a77254f61a0f6843eb4bcc059bfb4f5fe3bfc834ec742be99c
@@ -0,0 +1,173 @@
1
+ require 'json'
2
+ require 'thread'
3
+ require 'net/http'
4
+ require 'jwt'
5
+
6
+ module RackOidcApi
7
+ BEARER_TOKEN_REGEX = %r{
8
+ \ABearer\s{1}( # starts with Bearer and a single space
9
+ [a-zA-Z0-9\-\_]+\. # 1 or more chars followed by a single period
10
+ [a-zA-Z0-9\-\_]+\. # 1 or more chars followed by a single period
11
+ [a-zA-Z0-9\-\_]+ # 1 or more chars
12
+ )\z # nothing trailing
13
+ }ix
14
+
15
+ ALGORITHMS = %w(RS256 RS384 RS512)
16
+
17
+ class Middleware
18
+ def initialize(app, opts)
19
+ @app = app
20
+
21
+ raise "provider must be specified" if !opts[:provider]
22
+ raise "audience must be specified" if !opts[:audience]
23
+
24
+ @provider = opts[:provider].gsub(/\/\z/, '')
25
+ @audience = opts[:audience]
26
+ @pinned_cert = opts[:pinned_provider_ssl]
27
+ @lock = Mutex.new
28
+
29
+ reload_options
30
+ end
31
+
32
+ def reload_options
33
+ begin
34
+ oidc_config_uri = URI("#{@provider}/.well-known/openid-configuration")
35
+ oidc_config_raw = http_get(oidc_config_uri)
36
+ raise "Failed to retrieve OIDC Discovery Data" unless oidc_config_raw
37
+ oidc_config = JSON.parse(oidc_config_raw)
38
+ raise "Invalid or missing OIDC Discovery Data" unless oidc_config
39
+
40
+ jwks_uri = oidc_config['jwks_uri']
41
+ raise "No JWKS URI in OIDC Discovery" unless jwks_uri
42
+
43
+ # Do not allow JWKS from a different origin (scheme, host, port)
44
+ jwks_uri = URI(jwks_uri)
45
+ jwks_uri.scheme = oidc_config_uri.scheme
46
+ jwks_uri.host = oidc_config_uri.host
47
+ jwks_uri.port = oidc_config_uri.port
48
+
49
+ jwks_raw = http_get(jwks_uri)
50
+ raise "Failed to retrieve JWKS File" unless jwks_raw
51
+
52
+ jwks = JSON.parse(jwks_raw)
53
+ algorithms = ALGORITHMS - (ALGORITHMS - oidc_config['id_token_signing_alg_values_supported'] || [])
54
+
55
+ keys = []
56
+ jwks['keys'].each do |key|
57
+ rec = {}
58
+ key.each do |k, v|
59
+ rec[k.to_sym] = v
60
+ end
61
+ keys << rec
62
+ end
63
+
64
+ @jwks = {keys: keys}
65
+ @algorithms = algorithms
66
+ @valid = Time.now + 300
67
+ @avail = true
68
+ rescue JSON::JSONError
69
+ @avail = false
70
+ @valid = Time.now + 60
71
+ rescue StandardError => e
72
+ STDERR.puts(e.message)
73
+ @avail = false
74
+ @valid = Time.now + 60
75
+ rescue URI::InvalidURIError => e
76
+ STDERR.puts(e.message)
77
+ @avail = false
78
+ @valid = Time.now + 60
79
+ end
80
+ end
81
+
82
+ def check_reload
83
+ locked = @lock.try_lock
84
+ return unless locked # Only have one reload checking thread at once
85
+ begin
86
+ if @valid < Time.now
87
+ reload_options
88
+ end
89
+ ensure
90
+ @lock.unlock
91
+ end
92
+ end
93
+
94
+ def call(env)
95
+ header = env['HTTP_AUTHORIZATION']
96
+ if !header
97
+ return mkerror("Missing Authorization Header")
98
+ end
99
+
100
+ if !header.match(BEARER_TOKEN_REGEX)
101
+ return mkerror("Invalid Bearer token")
102
+ end
103
+
104
+ _, token = header.split(/\s/, 2)
105
+
106
+ check_reload
107
+ if !@avail
108
+ return mkerror("OIDC provider unavailable")
109
+ end
110
+
111
+ jwk_loader = proc do |options|
112
+ @jwks
113
+ end
114
+
115
+ begin
116
+ jwt = JWT.decode(token, nil, true, {
117
+ algorithms: @algorithms,
118
+ jwks: jwk_loader,
119
+ aud: @audience,
120
+ verify_aud: true,
121
+ nbf_leeway: 30,
122
+ exp_leeway: 30
123
+ })
124
+ env[:identity_token] = jwt
125
+ rescue JWT::JWKError => e
126
+ # Handle problems with the provided JWKs
127
+ return mkerror("Invalid Bearer token")
128
+ rescue JWT::DecodeError => e
129
+ # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
130
+ return mkerror(e.message)
131
+ end
132
+
133
+ @app.call(env)
134
+ end
135
+
136
+ private
137
+
138
+ def mkerror(message)
139
+ body = { error: message }.to_json
140
+ headers = { 'Content-Type' => 'application/json' }
141
+
142
+ [401, headers, [body]]
143
+ end
144
+
145
+ def http_get(uri)
146
+ http = Net::HTTP.new(uri.host, uri.port)
147
+ http.use_ssl = uri.scheme != 'http'
148
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER # Make sure we're verifying
149
+ if @pinned_cert
150
+ http.verify_callback = lambda do | preverify_ok, cert_store |
151
+ return false unless preverify_ok
152
+
153
+ # We only want to verify once, and fail the first time the callback
154
+ # is invoked (as opposed to checking only the last time it's called).
155
+ # Therefore we get at the whole authorization chain.
156
+ # The end certificate is at the beginning of the chain (the certificate
157
+ # for the host we are talking to)
158
+ end_cert = cert_store.chain[0]
159
+
160
+ # Only perform the checks if the current cert is the end certificate
161
+ # in the chain. We can compare using the DER representation
162
+ # (OpenSSL::X509::Certificate objects are not comparable, and for
163
+ # a good reason). If we don't we are going to perform the verification
164
+ # many times - once per certificate in the chain of trust, which is wasteful
165
+ return true unless end_cert.to_der == cert_store.current_cert.to_der
166
+
167
+ public_key = end_cert.public_key.to_pem
168
+ @pinned_cert == public_key || @pinned_cert == OpenSSL::Digest::SHA256.hexdigest(public_key)
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-oidc-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Tim Goddard <tim@goddard.nz>
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ description:
42
+ email:
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/rack-oidc-api/middleware.rb
48
+ homepage:
49
+ licenses:
50
+ - MIT
51
+ metadata: {}
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 2.5.2.1
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: rack-oidc-api provides a JWT validation middleware which automatically discovers
72
+ the settings supported by a given OIDC provider, and validates the token against
73
+ the published keys.
74
+ test_files: []