duo_universal 0.1.1.pre → 0.1.2.pre

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3b0bf1000785f1035e413cff1024d0f20e292390de2cae8ba8a6ca366b07077
4
- data.tar.gz: 8d6585c3813c788dea07219c4e5ae1941f23b01e5bfc402fd8226652b02863e5
3
+ metadata.gz: ac75cce798a71c31075b9c4b05f8a3b74e08bcef5aeda635c4b873eaa0fa1186
4
+ data.tar.gz: 67b13d79bad4b6cbb8fb3228d87440989ee827c68af07067eb93f4c89fffdbad
5
5
  SHA512:
6
- metadata.gz: 47958ab6afaa0c09aae466284dc007b49e546290d7d05eb8fdf77ddc17f7c918d8eafaf4ab6f4f3a0d818b4d180f47e9e40ca76bf8a44d0f7dcab94a6acf9237
7
- data.tar.gz: 724318e4a0edfc3b2e0d9acb8066d8685f38a27f0627e8da595b01196852f225d6ab9962bae86bf495346ebfb9d884ea4fb538f14c05f11330a153753624c1cf
6
+ metadata.gz: 781ddd2d1724540f1df66827fa26fb583aa28bda5975e98c74a9ddd2d5b00e0ada013cc67b414738f0f9373cf8a56a4e85bae0eb6f5d5429d8ece186d52bd3c1
7
+ data.tar.gz: c1289b853fa84bcda7ee6bc3f5eab0aa73a2ee21f019f67a0bc561be4f2340eeb770e20505094bf770fbda1259cbb89853b88f0f538429a9ebe74e73f5aff151
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- duo_universal (0.1.1.pre)
4
+ duo_universal (0.1.2.pre)
5
5
  httparty
6
6
  jwt
7
7
 
data/README.md CHANGED
@@ -15,7 +15,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
15
15
 
16
16
  ## Usage
17
17
 
18
- TODO: Write usage instructions here
18
+ See the Sinatra-based demo app found in the `demo` folder, for an example of how to integrate this gem into your project.
19
19
 
20
20
  ## Development
21
21
 
@@ -1,175 +1,176 @@
1
- require 'jwt'
2
- require 'securerandom'
3
- require 'httparty'
4
-
5
- require 'byebug'
6
-
7
- module Duo
8
- class Client
9
- STATE_LENGTH = 36
10
- JTI_LENGTH = 36
11
- MINIMUM_STATE_LENGTH = 22
12
- MAXIMUM_STATE_LENGTH = 1024
13
- CLIENT_ID_LENGTH = 20
14
- CLIENT_SECRET_LENGTH = 40
15
- CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
16
- JWT_LEEWAY = 60
17
-
18
- attr_reader :client_id, :client_secret, :host, :redirect_uri, :use_duo_code_attribute
19
-
20
- def initialize(client_id, client_secret, host, redirect_uri, optional_args = {})
21
- raise Duo::ClientIDLengthError unless client_id && client_id.length == CLIENT_ID_LENGTH
22
- raise Duo::ClientSecretLengthError unless client_secret && client_secret.length == CLIENT_SECRET_LENGTH
23
- raise Duo::ApiHostRequiredError unless host
24
- raise Duo::RedirectUriRequiredError unless redirect_uri
25
-
26
- @client_id = client_id
27
- @client_secret = client_secret
28
- @host = host
29
- @redirect_uri = redirect_uri
30
- @use_duo_code_attribute = optional_args.fetch(:use_duo_code_attribute) { true }
31
- end
32
-
33
- def api_host_uri
34
- "https://#{host}"
35
- end
36
-
37
- def authorize_endpoint_uri
38
- "https://#{host}/oauth/v1/authorize"
39
- end
40
-
41
- def health_check_endpoint_uri
42
- "https://#{host}/oauth/v1/health_check"
43
- end
44
-
45
- def token_endpoint_uri
46
- "https://#{host}/oauth/v1/token"
47
- end
48
-
49
- def create_auth_url(username, state)
50
- raise Duo::StateLengthError unless state && state.length >= MINIMUM_STATE_LENGTH && state.length <= MAXIMUM_STATE_LENGTH
51
- raise Duo::UsernameRequiredError unless username && username.gsub(/\s*/, '').length > 0
52
-
53
- jwt_args = {
54
- scope: 'openid',
55
- redirect_uri: redirect_uri,
56
- client_id: client_id,
57
- iss: client_id,
58
- aud: api_host_uri,
59
- exp: (Time.now + (5*60)).to_i,
60
- state: state,
61
- response_type: 'code',
62
- duo_uname: username,
63
- use_duo_code_attribute: use_duo_code_attribute,
64
- }
65
-
66
- req_jwt = JWT.encode(jwt_args, client_secret, 'HS512')
67
-
68
- all_args = {
69
- response_type: 'code',
70
- client_id: client_id,
71
- request: req_jwt
72
- }
73
-
74
- query_string = URI.encode_www_form all_args
75
-
76
- "#{authorize_endpoint_uri}?#{query_string}"
77
- end
78
-
79
- def generate_state
80
- SecureRandom.alphanumeric(STATE_LENGTH)
81
- end
82
-
83
- # Checks whether Duo is available.
84
- # Returns:
85
- # {'response': {'timestamp': <int:unix timestamp>}, 'stat': 'OK'}
86
- # Raises:
87
- #
88
- def health_check
89
- req_payload = {
90
- client_assertion: JWT.encode(jwt_args_for(health_check_endpoint_uri), client_secret, 'HS512'),
91
- client_id: client_id
92
- }
93
-
94
- # ToDo: Add Support for verifying SSL certificates
95
- begin
96
- res = HTTParty.post(health_check_endpoint_uri, body: req_payload)
97
-
98
- json_resp = JSON.parse res.body
99
-
100
- raise Duo::Error.new(json_resp) unless json_resp['stat'] == 'OK'
101
-
102
- json_resp
103
- rescue => e
104
- raise e
105
- end
106
- end
107
-
108
- # Exchanges the duo_code for a token with Duo to determine
109
- # if the auth was successful.
110
- # Argument:
111
- # duo_code -- Authentication session transaction id
112
- # returned by Duo
113
- # username -- Name of the user authenticating with Duo
114
- # nonce -- Random 36B string used to associate
115
- # a session with an ID token
116
- # Returns:
117
- # A token with meta-data about the auth
118
- # Raises:
119
- # Duo::Error on error for invalid duo_codes, invalid credentials,
120
- # or problems connecting to Duo
121
- def exchange_authorization_code_for_2fa_result(duo_code, username, nonce = nil)
122
- raise Duo::DuoCodeRequiredError unless duo_code
123
-
124
- jwt_args = jwt_args_for(token_endpoint_uri)
125
-
126
- all_args = {
127
- grant_type: 'authorization_code',
128
- code: duo_code,
129
- redirect_uri: redirect_uri,
130
- client_id: client_id,
131
- client_assertion_type: CLIENT_ASSERTION_TYPE,
132
- client_assertion: JWT.encode(jwt_args, client_secret, 'HS512')
133
- }
134
-
135
- begin
136
- user_agent = "duo_universal_ruby/#{Duo::VERSION} ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} #{RUBY_PLATFORM}"
137
-
138
- resp = HTTParty.post(token_endpoint_uri, body: all_args, headers: { user_agent: user_agent })
139
-
140
- json_response_body = JSON.parse(resp.body)
141
-
142
- raise Duo::Error.new(json_response_body) unless resp.code == 200
143
-
144
- decoded_token = JWT.decode(json_response_body['id_token'], client_secret, true, {
145
- algorithm: 'HS512',
146
- iss: token_endpoint_uri,
147
- verify_iss: true,
148
- aud: client_id,
149
- verify_aud: true,
150
- exp_leeway: JWT_LEEWAY,
151
- required_claims: ['exp', 'iat'],
152
- verify_iat: true
153
- })
154
-
155
- # ToDo: finalise validation
156
-
157
- decoded_token
158
- rescue => e
159
- raise Duo::Error.new(e.message)
160
- end
161
- end
162
-
163
- private
164
-
165
- def jwt_args_for(endpoint_uri)
166
- {
167
- iss: client_id,
168
- sub: client_id,
169
- aud: endpoint_uri,
170
- exp: (Time.now + (5*60)).to_i,
171
- jti: SecureRandom.alphanumeric(JTI_LENGTH)
172
- }
173
- end
174
- end
1
+ require 'jwt'
2
+ require 'securerandom'
3
+ require 'httparty'
4
+
5
+ require 'byebug'
6
+
7
+ module Duo
8
+ class Client
9
+ STATE_LENGTH = 36
10
+ JTI_LENGTH = 36
11
+ MINIMUM_STATE_LENGTH = 22
12
+ MAXIMUM_STATE_LENGTH = 1024
13
+ CLIENT_ID_LENGTH = 20
14
+ CLIENT_SECRET_LENGTH = 40
15
+ CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
16
+ JWT_LEEWAY = 60
17
+
18
+ attr_reader :client_id, :client_secret, :host, :redirect_uri, :use_duo_code_attribute
19
+
20
+ def initialize(client_id, client_secret, host, redirect_uri, optional_args = {})
21
+ raise Duo::ClientIDLengthError unless client_id && client_id.length == CLIENT_ID_LENGTH
22
+ raise Duo::ClientSecretLengthError unless client_secret && client_secret.length == CLIENT_SECRET_LENGTH
23
+ raise Duo::ApiHostRequiredError unless host
24
+ raise Duo::RedirectUriRequiredError unless redirect_uri
25
+
26
+ @client_id = client_id
27
+ @client_secret = client_secret
28
+ @host = host
29
+ @redirect_uri = redirect_uri
30
+ @use_duo_code_attribute = optional_args.fetch(:use_duo_code_attribute) { true }
31
+ end
32
+
33
+ def api_host_uri
34
+ "https://#{host}"
35
+ end
36
+
37
+ def authorize_endpoint_uri
38
+ "https://#{host}/oauth/v1/authorize"
39
+ end
40
+
41
+ def health_check_endpoint_uri
42
+ "https://#{host}/oauth/v1/health_check"
43
+ end
44
+
45
+ def token_endpoint_uri
46
+ "https://#{host}/oauth/v1/token"
47
+ end
48
+
49
+ def create_auth_url(username, state)
50
+ raise Duo::StateLengthError unless state && state.length >= MINIMUM_STATE_LENGTH && state.length <= MAXIMUM_STATE_LENGTH
51
+ raise Duo::UsernameRequiredError unless username && username.gsub(/\s*/, '').length > 0
52
+
53
+ jwt_args = {
54
+ scope: 'openid',
55
+ redirect_uri: redirect_uri,
56
+ client_id: client_id,
57
+ iss: client_id,
58
+ aud: api_host_uri,
59
+ exp: (Time.now + (5*60)).to_i,
60
+ state: state,
61
+ response_type: 'code',
62
+ duo_uname: username,
63
+ use_duo_code_attribute: use_duo_code_attribute,
64
+ }
65
+
66
+ req_jwt = JWT.encode(jwt_args, client_secret, 'HS512')
67
+
68
+ all_args = {
69
+ response_type: 'code',
70
+ client_id: client_id,
71
+ request: req_jwt
72
+ }
73
+
74
+ query_string = URI.encode_www_form all_args
75
+
76
+ "#{authorize_endpoint_uri}?#{query_string}"
77
+ end
78
+
79
+ def generate_state
80
+ SecureRandom.alphanumeric(STATE_LENGTH)
81
+ end
82
+
83
+ # Checks whether Duo is available.
84
+ # Returns:
85
+ # {'response': {'timestamp': <int:unix timestamp>}, 'stat': 'OK'}
86
+ # Raises:
87
+ #
88
+ def health_check
89
+ req_payload = {
90
+ client_assertion: JWT.encode(jwt_args_for(health_check_endpoint_uri), client_secret, 'HS512'),
91
+ client_id: client_id
92
+ }
93
+
94
+ # ToDo: Add Support for verifying SSL certificates
95
+ begin
96
+ res = HTTParty.post(health_check_endpoint_uri, body: req_payload)
97
+
98
+ json_resp = JSON.parse res.body
99
+
100
+ raise Duo::Error.new(json_resp) unless json_resp['stat'] == 'OK'
101
+
102
+ json_resp
103
+ rescue => e
104
+ raise e
105
+ end
106
+ end
107
+
108
+ # Exchanges the duo_code for a token with Duo to determine
109
+ # if the auth was successful.
110
+ # Argument:
111
+ # duo_code -- Authentication session transaction id
112
+ # returned by Duo
113
+ # username -- Name of the user authenticating with Duo
114
+ # nonce -- Random 36B string used to associate
115
+ # a session with an ID token
116
+ # Returns:
117
+ # A token with meta-data about the auth
118
+ # Raises:
119
+ # Duo::Error on error for invalid duo_codes, invalid credentials,
120
+ # or problems connecting to Duo
121
+ def exchange_authorization_code_for_2fa_result(duo_code, username, nonce = nil)
122
+ raise Duo::DuoCodeRequiredError unless duo_code
123
+
124
+ jwt_args = jwt_args_for(token_endpoint_uri)
125
+
126
+ all_args = {
127
+ grant_type: 'authorization_code',
128
+ code: duo_code,
129
+ redirect_uri: redirect_uri,
130
+ client_id: client_id,
131
+ client_assertion_type: CLIENT_ASSERTION_TYPE,
132
+ client_assertion: JWT.encode(jwt_args, client_secret, 'HS512')
133
+ }
134
+
135
+ begin
136
+ user_agent = "duo_universal_ruby/#{Duo::VERSION} ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} #{RUBY_PLATFORM}"
137
+
138
+ resp = HTTParty.post(token_endpoint_uri, body: all_args, headers: { user_agent: user_agent })
139
+
140
+ json_response_body = JSON.parse(resp.body)
141
+
142
+ raise Duo::Error.new(json_response_body) unless resp.code == 200
143
+
144
+ decoded_token = JWT.decode(json_response_body['id_token'], client_secret, true, {
145
+ algorithm: 'HS512',
146
+ iss: token_endpoint_uri,
147
+ verify_iss: true,
148
+ aud: client_id,
149
+ verify_aud: true,
150
+ exp_leeway: JWT_LEEWAY,
151
+ required_claims: ['exp', 'iat'],
152
+ verify_iat: true
153
+ }).first
154
+
155
+ raise Duo::Error.new("The username is invalid.") unless decoded_token.has_key?('preferred_username') and decoded_token['preferred_username'] == username
156
+ raise Duo::Error.new("The nonce is invalid.") unless decoded_token.has_key?('nonce') and decoded_token['nonce'] == nonce
157
+
158
+ decoded_token
159
+ rescue => e
160
+ raise Duo::Error.new(e.message)
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ def jwt_args_for(endpoint_uri)
167
+ {
168
+ iss: client_id,
169
+ sub: client_id,
170
+ aud: endpoint_uri,
171
+ exp: (Time.now + (5*60)).to_i,
172
+ jti: SecureRandom.alphanumeric(JTI_LENGTH)
173
+ }
174
+ end
175
+ end
175
176
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Duo
4
- VERSION = "0.1.1.pre"
4
+ VERSION = "0.1.2.pre"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duo_universal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1.pre
4
+ version: 0.1.2.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Walter
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-23 00:00:00.000000000 Z
11
+ date: 2022-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt