Class: DuoUniversalRuby::Client
- Inherits:
-
Object
- Object
- DuoUniversalRuby::Client
- Defined in:
- lib/duo_universal_ruby/client.rb
Instance Method Summary collapse
- #clamped_expiry_duration ⇒ Object
- #create_auth_url(username:, state:, nonce: nil) ⇒ Object
- #exchange_authorization_code_for_2fa_result(duo_code:, username:, nonce: nil) ⇒ Object
- #generate_state ⇒ Object
- #health_check ⇒ Object
-
#initialize(client_id:, client_secret:, api_host:, redirect_uri:, duo_certs: DEFAULT_CA_CERT_PATH, use_duo_code_attribute: true, http_proxy: nil, exp_seconds: FIVE_MINUTES_IN_SECONDS) ⇒ Client
constructor
A new instance of Client.
Constructor Details
#initialize(client_id:, client_secret:, api_host:, redirect_uri:, duo_certs: DEFAULT_CA_CERT_PATH, use_duo_code_attribute: true, http_proxy: nil, exp_seconds: FIVE_MINUTES_IN_SECONDS) ⇒ Client
Returns a new instance of Client.
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/duo_universal_ruby/client.rb', line 10 def initialize(client_id:, client_secret:, api_host:, redirect_uri:, duo_certs: DEFAULT_CA_CERT_PATH, use_duo_code_attribute: true, http_proxy: nil, exp_seconds: FIVE_MINUTES_IN_SECONDS) # Initializes instance of Client class # Arguments: # client_id -- Client ID for the application in Duo # client_secret -- Client secret for the application in Duo # host -- Duo api host # redirect_uri -- Uri to redirect to after a successful auth # duo_certs -- (Optional: default is ca_certs.pem) Provide custom CA certs # use_duo_code_attribute -- (Optional: default true) Flag to use `duo_code` instead of `code` for returned authorization parameter # http_proxy -- (Optional) HTTP proxy to tunnel requests through # exp_seconds -- (Optional) The number of seconds used for JWT expiry. Must be be at most 5 minutes. validate_init_config(client_id, client_secret, api_host, redirect_uri, exp_seconds) @client_id = client_id @client_secret = client_secret @api_host = api_host @redirect_uri = redirect_uri @use_duo_code_attribute = use_duo_code_attribute @duo_certs = duo_certs == "DISABLE" ? false : (duo_certs || DEFAULT_CA_CERT_PATH) @http_proxy = http_proxy @exp_seconds = exp_seconds end |
Instance Method Details
#clamped_expiry_duration ⇒ Object
37 38 39 |
# File 'lib/duo_universal_ruby/client.rb', line 37 def clamped_expiry_duration [[FIVE_MINUTES_IN_SECONDS, @exp_seconds].min, 1].max end |
#create_auth_url(username:, state:, nonce: nil) ⇒ Object
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/duo_universal_ruby/client.rb', line 73 def create_auth_url(username:, state:, nonce: nil) # Generate uri to Duo's prompt # Arguments: # username -- username trying to authenticate with Duo # state -- Randomly generated character string of at least 16 # and at most 1024 characters returned to the integration by Duo after 2FA # nonce -- (Optional) Randomly generated character string of at least 16 # and at most 1024 characters used as the nonce for the underlying OIDC flow # Returns: # Authorization uri to redirect to for the Duo prompt raise Error, ERR_STATE_LEN unless state && state.length.between?(MINIMUM_STATE_LENGTH, MAXIMUM_STATE_LENGTH) raise Error, ERR_USERNAME unless username raise Error, ERR_NONCE_LEN if nonce && !nonce.length.between?(MINIMUM_STATE_LENGTH, MAXIMUM_STATE_LENGTH) endpoint = format(OAUTH_V1_AUTHORIZE_ENDPOINT, @api_host) jwt_payload = { 'scope' => 'openid', 'redirect_uri' => @redirect_uri, 'client_id' => @client_id, 'iss' => @client_id, 'aud' => format(API_HOST_URI_FORMAT, @api_host), 'exp' => Time.now.to_i + clamped_expiry_duration, 'state' => state, 'response_type' => 'code', 'duo_uname' => username, 'use_duo_code_attribute' => @use_duo_code_attribute } request_jwt = JWT.encode(jwt_payload, @client_secret, 'HS512') params = { 'response_type' => 'code', 'client_id' => @client_id, 'request' => request_jwt } params['nonce'] = nonce if nonce uri = URI(endpoint) uri.query = URI.encode_www_form(params) uri.to_s end |
#exchange_authorization_code_for_2fa_result(duo_code:, username:, nonce: nil) ⇒ Object
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/duo_universal_ruby/client.rb', line 117 def (duo_code:, username:, nonce: nil) # Exchange the duo_code for a token with Duo to determine # if the auth was successful. # Arguments: # duo_code -- Authentication session transaction id # returned by Duo # username -- Name of the user authenticating with Duo # nonce -- (Optional) Random 36B string used to associate # a session with an ID token # Return: # A token with meta-data about the auth # Raises: # DuoException on error for invalid duo_codes, invalid credentials, # or problems connecting to Duo raise Error, ERR_CODE unless duo_code endpoint = format(OAUTH_V1_TOKEN_ENDPOINT, @api_host) payload = create_jwt_payload(endpoint) request_data = { 'grant_type' => 'authorization_code', 'code' => duo_code, 'redirect_uri' => @redirect_uri, 'client_id' => @client_id, 'client_assertion_type' => CLIENT_ASSERT_TYPE, 'client_assertion' => JWT.encode(payload, @client_secret, 'HS512') } user_agent = "duo_universal_ruby/1.0 ruby/#{RUBY_VERSION} #{RUBY_PLATFORM}" headers = { 'User-Agent' => user_agent } response = post_form(endpoint, request_data, headers) raise Error, JSON.parse(response.body) unless response.code.to_i == SUCCESS_STATUS_CODE id_token = JSON.parse(response.body)['id_token'] decoded, = JWT.decode( id_token, @client_secret, true, { aud: @client_id, iss: format(OAUTH_V1_TOKEN_ENDPOINT, @api_host), leeway: LEEWAY, algorithm: 'HS512', verify_iss: true, verify_aud: true, verify_iat: true, require: ['exp', 'iat'] } ) # ID Token validation # https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation raise Error, ERR_USERNAME unless decoded['preferred_username'] == username raise Error, ERR_NONCE if nonce && decoded['nonce'] != nonce decoded rescue => e raise Error, e. end |
#generate_state ⇒ Object
41 42 43 44 45 46 |
# File 'lib/duo_universal_ruby/client.rb', line 41 def generate_state # Random value passed initially in the OAUTH_V1_AUTHORIZE_ENDPOINT and verfied throughout interactions. # It is up to the client to verify that it is the same value as a security measure. # This value specifically protects against CSRF attacks (see RFC 6749) generate_rand_alphanumeric(STATE_LENGTH) end |
#health_check ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/duo_universal_ruby/client.rb', line 48 def health_check # Checks whether Duo is available. # Returns: # {'response': {'timestamp': <int:unix timestamp>}, 'stat': 'OK'} # Raises: # DuoException on error for invalid credentials # or problem connecting to Duo endpoint = format(OAUTH_V1_HEALTH_CHECK_ENDPOINT, @api_host) payload = create_jwt_payload(endpoint) body = { 'client_assertion' => JWT.encode(payload, @client_secret, 'HS512'), 'client_id' => @client_id } response = post_form(endpoint, body) result = JSON.parse(response.body) raise Error, result unless result['stat'] == 'OK' result rescue => e raise Error, e. end |