duo_universal 0.1.1.pre → 0.1.2.pre

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