rack-oidc-api 0.0.2

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