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.
- checksums.yaml +7 -0
- data/lib/rack-oidc-api/middleware.rb +173 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -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: []
|