ruby_oidc_client 0.0.1
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/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +12 -0
- data/examples/README.md +3 -0
- data/examples/client_secret_basic/Gemfile +6 -0
- data/examples/client_secret_basic/Gemfile.lock +26 -0
- data/examples/client_secret_basic/README.md +24 -0
- data/examples/client_secret_basic/app.rb +45 -0
- data/examples/client_secret_basic/config.json +7 -0
- data/examples/client_secret_basic/public/stylesheets/style.css +8 -0
- data/examples/client_secret_basic/views/index.erb +11 -0
- data/lib/id_partner.rb +260 -0
- data/lib/ruby_oidc_client/version.rb +5 -0
- data/ruby_oidc_client.gemspec +47 -0
- data/sig/ruby_oidc_client.rbs +40 -0
- metadata +155 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5b5f37ec00ec2675db46405e2ae66bbb0575039bdf0de00ac5f2a9f9420360de
|
4
|
+
data.tar.gz: 86093df21b39985900be9a4fcba7c831bdcb64331759c63f2acd13a8f93787a4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5eb4f9337b6f5dfde2a008c544fd2a706ab58d81ee23ab2618cc36cd5376ec4840c94c2169ec32ce37a85a5c9efec742749a575b2f75e5a0cc23b5ad9d7d9f88
|
7
|
+
data.tar.gz: 137967b68fddf32d29a0bcd69923297aec3eb5ef8ff89e347db096ce10b49ea116004a5380038023e273eec25df9b04bf55c5c0f145d20cb249bff3b9d713d76
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
0.1.0
|
2
|
+
|
3
|
+
- Flexible Configuration:
|
4
|
+
- Supports various authentication methods including client_secret_basic and private_key_jwt.
|
5
|
+
- Configurable token endpoint auth method.
|
6
|
+
- Configurable JWKS for signing and encryption.
|
7
|
+
- Authorization:
|
8
|
+
- Generation of authorization URL with support for different scopes, and parameters like scope and claims.
|
9
|
+
- Support for both standard and pushed authorization requests.
|
10
|
+
- Generation of proofs including state, nonce, and code verifier for PKCE.
|
11
|
+
- Token Acquisition:
|
12
|
+
- Token exchange functionality for obtaining tokens from an authorization code.
|
13
|
+
- User Information Retrieval:
|
14
|
+
- Userinfo endpoint interaction to retrieve user information using an access token.
|
15
|
+
- JWT Handling:
|
16
|
+
- JWT decoding support including handling of encrypted JWTs.
|
17
|
+
- JWT signing and encryption support.
|
18
|
+
- Utilities:
|
19
|
+
- Fetching provider configuration from the well-known configuration endpoint.
|
20
|
+
- Error Handling:
|
21
|
+
- Comprehensive error handling for various steps in the OIDC interaction process.
|
22
|
+
- Example:
|
23
|
+
- Sinatra example usage provided in the examples folder demonstrating how to use the IDPartner class for OIDC interactions.
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 IDPartner
|
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/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# RubyOidcClient
|
2
|
+
|
3
|
+
RubyOidcClient is a Ruby gem that simplifies OpenID Connect client operations. This gem provides a set of functionalities to interact with OIDC providers, facilitating authentication and authorization processes.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
You can install the `ruby_oidc_client` gem by executing the following command:
|
8
|
+
|
9
|
+
```bash
|
10
|
+
$ gem install ruby_oidc_client
|
11
|
+
```
|
12
|
+
|
13
|
+
Alternatively, if you are using Bundler, add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'ruby_oidc_client'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ bundle install
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Please check the [examples folder](./examples/)
|
28
|
+
|
29
|
+
### Running Tests
|
30
|
+
|
31
|
+
After checking out the repo, install the necessary dependencies and run the tests by executing:
|
32
|
+
|
33
|
+
```bash
|
34
|
+
$ rspec
|
35
|
+
```
|
36
|
+
|
37
|
+
### Running RuboCop
|
38
|
+
|
39
|
+
To check the code for styling issues according to the RuboCop guidelines, run:
|
40
|
+
|
41
|
+
```bash
|
42
|
+
$ rubocop
|
43
|
+
```
|
44
|
+
|
45
|
+
## Contributing
|
46
|
+
|
47
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/idpartner-app/ruby_oidc_client](https://github.com/idpartner-app/ruby_oidc_client).
|
48
|
+
|
49
|
+
## License
|
50
|
+
|
51
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
52
|
+
```
|
data/Rakefile
ADDED
data/examples/README.md
ADDED
@@ -0,0 +1,3 @@
|
|
1
|
+
# IDPartner examples
|
2
|
+
|
3
|
+
This contains the an example with the [ruby_oidc_client](https://github.com/idpartner-app/ruby_oidc_client) library, called [client-secret-basic](./client-secret-basic/README.md), which is a sinatra project using the client_secret_basic authentication method to obtain user info
|
@@ -0,0 +1,26 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
mustermann (3.0.0)
|
5
|
+
ruby2_keywords (~> 0.0.1)
|
6
|
+
rack (2.2.8)
|
7
|
+
rack-protection (3.1.0)
|
8
|
+
rack (~> 2.2, >= 2.2.4)
|
9
|
+
ruby2_keywords (0.0.5)
|
10
|
+
sinatra (3.1.0)
|
11
|
+
mustermann (~> 3.0)
|
12
|
+
rack (~> 2.2, >= 2.2.4)
|
13
|
+
rack-protection (= 3.1.0)
|
14
|
+
tilt (~> 2.0)
|
15
|
+
tilt (2.3.0)
|
16
|
+
webrick (1.8.1)
|
17
|
+
|
18
|
+
PLATFORMS
|
19
|
+
x86_64-darwin-23
|
20
|
+
|
21
|
+
DEPENDENCIES
|
22
|
+
sinatra
|
23
|
+
webrick
|
24
|
+
|
25
|
+
BUNDLED WITH
|
26
|
+
2.4.21
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# client_secret_basic flow using ruby_oidc_client lib
|
2
|
+
|
3
|
+
## Prerequisites
|
4
|
+
|
5
|
+
1. [Create an IDPartner Account](https://console.idpartner.com).
|
6
|
+
1. [Create an Application (Client Secret)](https://docs.idpartner.com/documentation/relying-party-user-guide/registering-your-app#create-an-application).
|
7
|
+
1. Ensure the following properties are set:
|
8
|
+
- Origin URL: http://localhost:3001/button/oauth
|
9
|
+
- Redirect URL: http://localhost:3001/button/oauth/callback
|
10
|
+
1. Grab the "Client ID" and the "Client Secret" to update the following parts in your code:
|
11
|
+
1. [Update CHANGE_ME_CLIENT_ID in the configuration file](./config.json)
|
12
|
+
1. [Update CHANGE_ME_CLIENT_SECRET in the configuration file](./config.json)
|
13
|
+
|
14
|
+
Aditionally you optionally can configure the next steps:
|
15
|
+
1. [Update the "redirect_uri" in the configuration file](./config.json)
|
16
|
+
|
17
|
+
## Running the project
|
18
|
+
|
19
|
+
1. Run: `bundle`
|
20
|
+
1. Run: `ruby app.rb`
|
21
|
+
1. Access http://localhost:3001
|
22
|
+
1. Click the "Choose your ID Partner" button
|
23
|
+
1. Search for "Mikomo Bank"
|
24
|
+
1. Use these test credentials `mikomo_10/mikomo_10`
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sinatra"
|
4
|
+
require_relative "../../lib/id_partner"
|
5
|
+
|
6
|
+
class App < Sinatra::Base
|
7
|
+
config = JSON.parse(File.read("config.json"))
|
8
|
+
|
9
|
+
set :bind, "0.0.0.0"
|
10
|
+
set :port, config["port"]
|
11
|
+
set :public_folder, "#{File.dirname(__FILE__)}/public"
|
12
|
+
enable :sessions
|
13
|
+
|
14
|
+
id_partner = RubyOidcClient::IDPartner.new({
|
15
|
+
client_id: config["client_id"],
|
16
|
+
client_secret: config["client_secret"],
|
17
|
+
redirect_uri: config["redirect_uri"]
|
18
|
+
})
|
19
|
+
|
20
|
+
get "/" do
|
21
|
+
erb :index, locals: { title: "RP Client Secret Example", config: config }
|
22
|
+
end
|
23
|
+
|
24
|
+
get "/button/oauth" do
|
25
|
+
scope = config["scope"]
|
26
|
+
proofs = id_partner.generate_proofs
|
27
|
+
session[:proofs] = proofs
|
28
|
+
session[:issuer] = params[:iss]
|
29
|
+
redirect id_partner.get_authorization_url(params, proofs, scope, { prompt: "consent" })
|
30
|
+
end
|
31
|
+
|
32
|
+
get "/button/oauth/callback" do
|
33
|
+
token = id_partner.token(params, session[:proofs])
|
34
|
+
userinfo = id_partner.userinfo(token["access_token"])
|
35
|
+
content_type :json
|
36
|
+
userinfo.to_json
|
37
|
+
end
|
38
|
+
|
39
|
+
get "/jwks" do
|
40
|
+
content_type :json
|
41
|
+
id_partner.public_jwks.to_json
|
42
|
+
end
|
43
|
+
|
44
|
+
run! if app_file == $PROGRAM_NAME
|
45
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<script src="https://install.idpartner.com/script.js"></script>
|
5
|
+
<title><%= title %></title>
|
6
|
+
<link rel='stylesheet' href='/stylesheets/style.css' />
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<id-partner id="<%= config["client_id"] %>"></id-partner>
|
10
|
+
</body>
|
11
|
+
</html>
|
data/lib/id_partner.rb
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "ruby_oidc_client/version"
|
4
|
+
require "json/jwt"
|
5
|
+
require "net/http"
|
6
|
+
require "uri"
|
7
|
+
require "base64"
|
8
|
+
|
9
|
+
module RubyOidcClient
|
10
|
+
class IDPartner
|
11
|
+
SUPPORTED_AUTH_METHODS = [
|
12
|
+
"client_secret_basic",
|
13
|
+
"tls_client_auth",
|
14
|
+
"private_key_jwt" # For backward compatibility
|
15
|
+
].freeze
|
16
|
+
SIGNING_ALG = "PS256"
|
17
|
+
ENCRYPTION_ALG = "RSA-OAEP"
|
18
|
+
ENCRYPTION_ENC = "A256CBC-HS512"
|
19
|
+
|
20
|
+
attr_reader :config, :endpoints, :provider_keys
|
21
|
+
|
22
|
+
def initialize(config)
|
23
|
+
raise ArgumentError, "Config missing." unless config
|
24
|
+
|
25
|
+
default_config = {
|
26
|
+
account_selector_service_url: "https://auth-api.idpartner.com/oidc-proxy",
|
27
|
+
token_endpoint_auth_method: "client_secret_basic",
|
28
|
+
jwks: nil,
|
29
|
+
client_secret: nil
|
30
|
+
}
|
31
|
+
|
32
|
+
@config = default_config.merge(config)
|
33
|
+
|
34
|
+
unless SUPPORTED_AUTH_METHODS.include?(@config[:token_endpoint_auth_method])
|
35
|
+
raise ArgumentError,
|
36
|
+
"Unsupported token_endpoint_auth_method '#{config[:token_endpoint_auth_method]}'. It must be one of (#{SUPPORTED_AUTH_METHODS.join(", ")})"
|
37
|
+
end
|
38
|
+
|
39
|
+
client_secret_config = @config[:token_endpoint_auth_method] == "client_secret_basic" ? { client_secret: @config[:client_secret] } : {}
|
40
|
+
|
41
|
+
jwks_config = if @config[:jwks]
|
42
|
+
{
|
43
|
+
authorization_encrypted_response_alg: ENCRYPTION_ALG,
|
44
|
+
authorization_encrypted_response_enc: ENCRYPTION_ENC,
|
45
|
+
id_token_encrypted_response_alg: ENCRYPTION_ALG,
|
46
|
+
id_token_encrypted_response_enc: ENCRYPTION_ENC,
|
47
|
+
request_object_signing_alg: SIGNING_ALG
|
48
|
+
}
|
49
|
+
else
|
50
|
+
{}
|
51
|
+
end
|
52
|
+
|
53
|
+
@config = @config.merge({
|
54
|
+
authorization_signed_response_alg: SIGNING_ALG,
|
55
|
+
id_token_signed_response_alg: SIGNING_ALG
|
56
|
+
}).merge(client_secret_config).merge(jwks_config)
|
57
|
+
end
|
58
|
+
|
59
|
+
def generate_proofs
|
60
|
+
{
|
61
|
+
state: SecureRandom.urlsafe_base64(64),
|
62
|
+
nonce: SecureRandom.urlsafe_base64(64),
|
63
|
+
code_verifier: SecureRandom.urlsafe_base64(64)
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_authorization_url(query, proofs, scope, extra_authorization_params = {})
|
68
|
+
raise ArgumentError, "The URL query parameter is required." unless query
|
69
|
+
raise ArgumentError, "The scope parameter is required." unless scope
|
70
|
+
raise ArgumentError, "The proofs parameter is required." unless proofs
|
71
|
+
|
72
|
+
if query[:iss].nil?
|
73
|
+
return "#{config[:account_selector_service_url]}/auth/select-accounts?client_id=#{config[:client_id]}&visitor_id=#{query[:visitor_id]}&scope=#{scope}&claims=#{extract_claims(extra_authorization_params[:claims]).join("+")}"
|
74
|
+
end
|
75
|
+
|
76
|
+
@config[:iss] = query[:iss]
|
77
|
+
obtain_well_known_config_endpoints
|
78
|
+
|
79
|
+
extra_authorization_params[:claims] = extra_authorization_params[:claims]&.to_json
|
80
|
+
extended_authorization_params = {
|
81
|
+
redirect_uri: config[:redirect_uri],
|
82
|
+
code_challenge_method: "S256",
|
83
|
+
code_challenge: generate_code_challenge(proofs[:code_verifier]),
|
84
|
+
state: proofs[:state],
|
85
|
+
nonce: proofs[:nonce],
|
86
|
+
scope: scope,
|
87
|
+
response_type: "code",
|
88
|
+
client_id: config[:client_id],
|
89
|
+
'x-fapi-interaction-id': SecureRandom.uuid,
|
90
|
+
identity_provider_id: query[:idp_id],
|
91
|
+
idpartner_token: query[:idpartner_token],
|
92
|
+
response_mode: "jwt"
|
93
|
+
}.merge(extra_authorization_params).compact
|
94
|
+
|
95
|
+
pushed_authorization_request_params = extended_authorization_params
|
96
|
+
pushed_authorization_request_params = { request: create_request_object(extended_authorization_params) } if config[:jwks]
|
97
|
+
|
98
|
+
request_uri = push_authorization_request(pushed_authorization_request_params)["request_uri"]
|
99
|
+
query_params = URI.encode_www_form(request_uri: request_uri)
|
100
|
+
"#{endpoints[:authorization_endpoint]}?#{query_params}"
|
101
|
+
end
|
102
|
+
|
103
|
+
def public_jwks
|
104
|
+
return {} unless config[:jwks]
|
105
|
+
|
106
|
+
jwk_set = JSON::JWK::Set.new(JSON.parse(config[:jwks]))
|
107
|
+
public_jwks = jwk_set.collect do |jwk|
|
108
|
+
public_jwk = jwk.to_key.public_key.to_jwk
|
109
|
+
public_jwk.merge("alg" => jwk["alg"], "use" => jwk["use"]).compact
|
110
|
+
end
|
111
|
+
|
112
|
+
{ "keys" => public_jwks }
|
113
|
+
end
|
114
|
+
|
115
|
+
def token(query, proofs)
|
116
|
+
decoded_jwt = decode_jwt(query[:response])
|
117
|
+
basic_auth_credentials = Base64.strict_encode64("#{config[:client_id]}:#{config[:client_secret]}")
|
118
|
+
payload = {
|
119
|
+
code: decoded_jwt["code"],
|
120
|
+
code_verifier: proofs[:code_verifier],
|
121
|
+
grant_type: "authorization_code",
|
122
|
+
redirect_uri: config[:redirect_uri]
|
123
|
+
}
|
124
|
+
|
125
|
+
uri = URI(endpoints[:token_endpoint])
|
126
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
127
|
+
headers = {
|
128
|
+
"Authorization" => "Basic #{basic_auth_credentials}",
|
129
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
130
|
+
"Accept" => "application/json"
|
131
|
+
}
|
132
|
+
token_request = Net::HTTP::Post.new(uri.request_uri, headers)
|
133
|
+
token_request.set_form_data(payload)
|
134
|
+
token_response = http.request(token_request)
|
135
|
+
raise "Failed to exchange token: #{token_response.body}" unless token_response.is_a?(Net::HTTPSuccess)
|
136
|
+
|
137
|
+
JSON.parse(token_response.body)
|
138
|
+
end
|
139
|
+
|
140
|
+
def userinfo(access_token)
|
141
|
+
uri = URI(endpoints[:userinfo_endpoint])
|
142
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
143
|
+
headers = {
|
144
|
+
"Authorization" => "Bearer #{access_token}",
|
145
|
+
"Accept" => "application/json"
|
146
|
+
}
|
147
|
+
|
148
|
+
userinfo_request = Net::HTTP::Get.new(uri.request_uri, headers)
|
149
|
+
userinfo_response = http.request(userinfo_request)
|
150
|
+
|
151
|
+
raise "Failed to retrieve userinfo: #{userinfo_response.body}" unless userinfo_response.is_a?(Net::HTTPSuccess)
|
152
|
+
|
153
|
+
JSON.parse(userinfo_response.body)
|
154
|
+
rescue StandardError => e
|
155
|
+
raise "Failed to fetch well-known config: #{e.message}"
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def extract_claims(claims_object)
|
161
|
+
return [] unless claims_object.is_a?(Hash)
|
162
|
+
|
163
|
+
userinfo_keys = claims_object[:userinfo]&.keys || []
|
164
|
+
id_token_keys = claims_object[:id_token]&.keys || []
|
165
|
+
|
166
|
+
(userinfo_keys + id_token_keys).uniq
|
167
|
+
end
|
168
|
+
|
169
|
+
def obtain_well_known_config_endpoints
|
170
|
+
uri = URI("#{config[:iss]}/.well-known/openid-configuration")
|
171
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
172
|
+
headers = { "Accept" => "application/json" }
|
173
|
+
request = Net::HTTP::Get.new(uri.request_uri, headers)
|
174
|
+
response = http.request(request)
|
175
|
+
raise "Failed to retrieve well-known: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
176
|
+
well_known_config = JSON.parse(response.body)
|
177
|
+
@endpoints = {
|
178
|
+
authorization_endpoint: well_known_config["authorization_endpoint"],
|
179
|
+
token_endpoint: well_known_config["token_endpoint"],
|
180
|
+
userinfo_endpoint: well_known_config["userinfo_endpoint"],
|
181
|
+
pushed_authorization_request_endpoint: well_known_config["pushed_authorization_request_endpoint"],
|
182
|
+
jwks_uri: well_known_config["jwks_uri"]
|
183
|
+
}
|
184
|
+
rescue StandardError => e
|
185
|
+
raise "Failed to fetch well-known config: #{e.message}"
|
186
|
+
end
|
187
|
+
|
188
|
+
def create_request_object(params)
|
189
|
+
extended_params = params.merge({
|
190
|
+
iss: config[:client_id],
|
191
|
+
aud: config[:iss],
|
192
|
+
exp: Time.now.to_i + 60,
|
193
|
+
iat: Time.now.to_i,
|
194
|
+
nbf: Time.now.to_i
|
195
|
+
})
|
196
|
+
|
197
|
+
sig_key = sig_key()
|
198
|
+
jwt = JSON::JWT.new(extended_params)
|
199
|
+
jwt.sign(sig_key, sig_key["alg"]).to_s
|
200
|
+
end
|
201
|
+
|
202
|
+
def push_authorization_request(request_object)
|
203
|
+
uri = URI(endpoints[:pushed_authorization_request_endpoint])
|
204
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
205
|
+
basic_auth_credentials = Base64.strict_encode64("#{config[:client_id]}:#{config[:client_secret]}")
|
206
|
+
headers = {
|
207
|
+
"Authorization" => "Basic #{basic_auth_credentials}",
|
208
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
209
|
+
"Accept" => "application/json"
|
210
|
+
}
|
211
|
+
par_request = Net::HTTP::Post.new(uri.request_uri, headers)
|
212
|
+
par_request.set_form_data(request_object)
|
213
|
+
par_response = http.request(par_request)
|
214
|
+
raise "Failed to push authorization request: #{par_response.body}" unless par_response.is_a?(Net::HTTPSuccess)
|
215
|
+
|
216
|
+
JSON.parse(par_response.body)
|
217
|
+
rescue StandardError => e
|
218
|
+
raise "An error occurred: #{e.message}"
|
219
|
+
end
|
220
|
+
|
221
|
+
def generate_code_challenge(code_verifier)
|
222
|
+
Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier)).gsub(/=+$/, "")
|
223
|
+
end
|
224
|
+
|
225
|
+
def sig_key
|
226
|
+
jwk_set = JSON::JWK::Set.new(JSON.parse(config[:jwks]))
|
227
|
+
jwk_set.find { |j| j[:use] == "sig" }
|
228
|
+
end
|
229
|
+
|
230
|
+
def enc_key
|
231
|
+
jwk_set = JSON::JWK::Set.new(JSON.parse(config[:jwks]))
|
232
|
+
jwk_set.find { |j| j[:use] == "enc" }
|
233
|
+
end
|
234
|
+
|
235
|
+
def decode_jwt(jwt_str)
|
236
|
+
if jwt_str.split(".").length == 5
|
237
|
+
jwe = JSON::JWT.decode(jwt_str, enc_key)
|
238
|
+
jwt_str = jwe.plain_text
|
239
|
+
end
|
240
|
+
|
241
|
+
kid = JSON::JWT.decode(jwt_str, :skip_verification).kid
|
242
|
+
sig_key = provider_key(kid)
|
243
|
+
JSON::JWT.decode(jwt_str, sig_key)
|
244
|
+
end
|
245
|
+
|
246
|
+
def provider_key(kid)
|
247
|
+
fetch_provider_keys unless provider_keys
|
248
|
+
@provider_keys[kid] || (raise "No provider key found for kid: #{kid}")
|
249
|
+
end
|
250
|
+
|
251
|
+
def fetch_provider_keys
|
252
|
+
jwks_uri = endpoints[:jwks_uri]
|
253
|
+
fetched_provider_keys = JSON::JWK::Set::Fetcher.fetch(jwks_uri, kid: nil, auto_detect: false)
|
254
|
+
@provider_keys = {}
|
255
|
+
fetched_provider_keys.each do |key|
|
256
|
+
@provider_keys[key["kid"]] = key
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/ruby_oidc_client/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "ruby_oidc_client"
|
7
|
+
spec.version = RubyOidcClient::VERSION
|
8
|
+
spec.authors = ["Giovanni Alberto"]
|
9
|
+
spec.email = ["delirable@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "A Ruby client for interacting with OpenID Connect providers using the IDPartner class."
|
12
|
+
spec.description = "The IDPartner gem provides a Ruby client for engaging with OpenID Connect providers, simplifying the process of authorization, token acquisition, and user information retrieval. It supports various authentication methods and handles endpoint discovery via well-known configuration. The gem encapsulates the complexities of generating, signing, and verifying JWTs, making OpenID Connect integration straightforward and secure."
|
13
|
+
spec.homepage = "https://github.com/idpartner-app/ruby_oidc_client"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.6.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/idpartner-app/ruby_oidc_client"
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/idpartner-app/ruby_oidc_client/blob/master/CHANGELOG.md"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
(File.expand_path(f) == __FILE__) ||
|
28
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
spec.bindir = "exe"
|
32
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
33
|
+
spec.require_paths = ["lib"]
|
34
|
+
|
35
|
+
# Uncomment to register a new dependency of your gem
|
36
|
+
spec.add_dependency "json-jwt", "~> 1.16.3"
|
37
|
+
|
38
|
+
# Optionally, within your gemspec file if you prefer to declare dev/test dependencies here
|
39
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
40
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
41
|
+
spec.add_development_dependency "rubocop", "~> 1.21"
|
42
|
+
spec.add_development_dependency "vcr", "~> 6.2.0"
|
43
|
+
spec.add_development_dependency "webmock", "~> 3.19.1"
|
44
|
+
|
45
|
+
# For more information and examples about making a new gem, check out our
|
46
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
47
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class IDPartner
|
2
|
+
type SUPPORTED_AUTH_METHODS: Array[String]
|
3
|
+
type SIGNING_ALG: String
|
4
|
+
type ENCRYPTION_ALG: String
|
5
|
+
type ENCRYPTION_ENC: String
|
6
|
+
|
7
|
+
attr_reader config: Hash[Symbol, String], endpoints: Hash[Symbol, String]
|
8
|
+
|
9
|
+
def initialize: (Hash[Symbol, String]) -> void
|
10
|
+
|
11
|
+
def generate_proofs: -> Hash[Symbol, String]
|
12
|
+
|
13
|
+
def get_authorization_url: (
|
14
|
+
query: Hash[Symbol, String],
|
15
|
+
proofs: Hash[Symbol, String],
|
16
|
+
scope: Array[String],
|
17
|
+
prompt: String,
|
18
|
+
claims_object: Hash[Symbol, String]
|
19
|
+
) -> String
|
20
|
+
|
21
|
+
def get_public_jwks: -> Hash[String, Array[untyped]]
|
22
|
+
|
23
|
+
def token: (query: Hash[Symbol, String], proofs: Hash[Symbol, String]) -> Hash[String, String]
|
24
|
+
|
25
|
+
def userinfo: (String) -> Hash[String, String]
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def get_endpoints_from_well_known_config: -> void
|
30
|
+
|
31
|
+
def create_request_object: (Hash[Symbol, untyped]) -> String
|
32
|
+
|
33
|
+
def push_authorization_request: (Hash[Symbol, untyped]) -> Hash[String, untyped]
|
34
|
+
|
35
|
+
def generate_code_challenge: (String) -> String
|
36
|
+
|
37
|
+
def parse_private_key: (String) -> untyped
|
38
|
+
|
39
|
+
def verify_jws: (String) -> Hash[Symbol, untyped]
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby_oidc_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Giovanni Alberto
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-11-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json-jwt
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.16.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.16.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '13.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '13.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.21'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.21'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: vcr
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 6.2.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 6.2.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: webmock
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.19.1
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.19.1
|
97
|
+
description: The IDPartner gem provides a Ruby client for engaging with OpenID Connect
|
98
|
+
providers, simplifying the process of authorization, token acquisition, and user
|
99
|
+
information retrieval. It supports various authentication methods and handles endpoint
|
100
|
+
discovery via well-known configuration. The gem encapsulates the complexities of
|
101
|
+
generating, signing, and verifying JWTs, making OpenID Connect integration straightforward
|
102
|
+
and secure.
|
103
|
+
email:
|
104
|
+
- delirable@gmail.com
|
105
|
+
executables: []
|
106
|
+
extensions: []
|
107
|
+
extra_rdoc_files: []
|
108
|
+
files:
|
109
|
+
- ".rspec"
|
110
|
+
- ".rubocop.yml"
|
111
|
+
- CHANGELOG.md
|
112
|
+
- LICENSE.txt
|
113
|
+
- README.md
|
114
|
+
- Rakefile
|
115
|
+
- examples/README.md
|
116
|
+
- examples/client_secret_basic/Gemfile
|
117
|
+
- examples/client_secret_basic/Gemfile.lock
|
118
|
+
- examples/client_secret_basic/README.md
|
119
|
+
- examples/client_secret_basic/app.rb
|
120
|
+
- examples/client_secret_basic/config.json
|
121
|
+
- examples/client_secret_basic/public/stylesheets/style.css
|
122
|
+
- examples/client_secret_basic/views/index.erb
|
123
|
+
- lib/id_partner.rb
|
124
|
+
- lib/ruby_oidc_client/version.rb
|
125
|
+
- ruby_oidc_client.gemspec
|
126
|
+
- sig/ruby_oidc_client.rbs
|
127
|
+
homepage: https://github.com/idpartner-app/ruby_oidc_client
|
128
|
+
licenses:
|
129
|
+
- MIT
|
130
|
+
metadata:
|
131
|
+
allowed_push_host: https://rubygems.org
|
132
|
+
homepage_uri: https://github.com/idpartner-app/ruby_oidc_client
|
133
|
+
source_code_uri: https://github.com/idpartner-app/ruby_oidc_client
|
134
|
+
changelog_uri: https://github.com/idpartner-app/ruby_oidc_client/blob/master/CHANGELOG.md
|
135
|
+
post_install_message:
|
136
|
+
rdoc_options: []
|
137
|
+
require_paths:
|
138
|
+
- lib
|
139
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: 2.6.0
|
144
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '0'
|
149
|
+
requirements: []
|
150
|
+
rubygems_version: 3.4.10
|
151
|
+
signing_key:
|
152
|
+
specification_version: 4
|
153
|
+
summary: A Ruby client for interacting with OpenID Connect providers using the IDPartner
|
154
|
+
class.
|
155
|
+
test_files: []
|