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