cerner-oauth1a 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 78e443a0c2b7b55bc787c7892ddd671af08769e4
4
+ data.tar.gz: 6631bda81224b2102408deae304611d63eb3b52b
5
+ SHA512:
6
+ metadata.gz: d5d68c6bfa9eae2007d5331ba60349a3c5e69c911f1d771bd0cae784136205e500cc727524f271854d5ef8d495b4aed5820cb4f3940aad00ef7528eed8612191
7
+ data.tar.gz: d8e086be1015712861286552827300d6bb72a1c8466b8d9f55962c797c44d5adcb9daaeaebd06dfe61a06ee89235b6f2f4a2f0276267d8116f4350007e711b77
@@ -0,0 +1,88 @@
1
+ # Cerner OAuth 1.0a Client Library
2
+
3
+ This RubyGem is a client library for interacting with the Cerner OAuth 1.0a provider to
4
+ participate in two-legged (B2B) authentication. The goal of this project is to provide a zero-dependency Ruby library that simply and compactly implements the client aspects of
5
+ Cerner OAuth 1.0a variant of the OAuth 1.0a B2B workflow.
6
+
7
+ # Usage
8
+
9
+ ## Install
10
+ This library can be installed using the `gem` command or added to a Gemfile for use with Bundler.
11
+
12
+ ### `gem` command
13
+
14
+ $ gem install cerner-oauth1a
15
+
16
+ ### Gemfile
17
+
18
+ gem 'cerner-oauth1a', '~> 1.0'
19
+
20
+ ## Basic Use
21
+
22
+ require 'cerner/oauth1a'
23
+ require 'net/http'
24
+
25
+ # Setup the AccessTokenAgent with an Access Token URL, Key and Secret
26
+ agent = Cerner::OAuth1a::AccessTokenAgent.new(
27
+ access_token_url: 'https://api.cernercare.com/oauth/access',
28
+ consumer_key: 'CONSUMER_KEY',
29
+ consumer_secret: 'CONSUMER_SECRET')
30
+
31
+ # Retrieve an AccessToken instance
32
+ access_token = agent.retrieve
33
+
34
+ # Setup the HTTP library to access the protected API you want to invoke
35
+ uri = URI('https://authz-demo-api.cerner.com/me')
36
+ http = Net::HTTP.new(uri.host, uri.port)
37
+ http.use_ssl = true if uri.scheme == 'https'
38
+
39
+ # Invoke the API's HTTP endpoint and use the AccessToken to generate an Authorization header
40
+ response = http.request_get(uri.path, Authorization: access_token.authorization_header)
41
+
42
+ ## Access Token Reuse
43
+ Generally, you'll want to use an Access Token more than once. Access Tokens can be reused, but
44
+ they do expire, so you'll need to acquire new tokens after one expires. All of the expiration
45
+ information is contained in the AccessToken class and you can easily determine if a token is
46
+ expired or about to by using the AccessToken#expired? method. Below is an example of you might
47
+ implement that:
48
+
49
+ uri = URI('https://authz-demo-api.cerner.com/me')
50
+ http = Net::HTTP.new(uri.host, uri.port)
51
+ http.use_ssl = true if uri.scheme == 'https'
52
+
53
+ access_token = agent.retrieve if access_token.expired?
54
+
55
+ response = http.request_get(uri.path, Authorization: access_token.authorization_header)
56
+
57
+ ## References
58
+ * https://wiki.ucern.com/display/public/reference/Cerner%27s+OAuth+Specification
59
+ * http://oauth.net/core/1.0a
60
+ * http://oauth.pbwiki.com/ProblemReporting
61
+ * https://wiki.ucern.com/display/public/reference/Accessing+Cerner%27s+Web+Services+Using+OAuth+1.0a
62
+
63
+ # Building
64
+
65
+ This project is built using Ruby 2.2+, Rake and Bundler. RSpec is used for unit tests and SimpleCov
66
+ is utilized for test coverage.
67
+
68
+ # Availability
69
+
70
+ This RubyGem will be available on https://rubygems.org/.
71
+
72
+ # Communication
73
+
74
+ All questions, bugs, enhancements and pull requests can be submitted here, on GitHub via Issues.
75
+
76
+ # Contributing
77
+
78
+ See [CONTRIBUTING.md](CONTRIBUTING.md)
79
+
80
+ # LICENSE
81
+
82
+ Copyright 2017 Cerner Innovation, Inc.
83
+
84
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
85
+
86
+     http://www.apache.org/licenses/LICENSE-2.0
87
+
88
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
@@ -0,0 +1,3 @@
1
+ require 'cerner/oauth1a/access_token'
2
+ require 'cerner/oauth1a/access_token_agent'
3
+ require 'cerner/oauth1a/version'
@@ -0,0 +1,138 @@
1
+ module Cerner
2
+ module OAuth1a
3
+
4
+ # Public: An OAuth 1.0a Access Token.
5
+ class AccessToken
6
+ # Returns the String Accessor Secret related to this token.
7
+ attr_reader :accessor_secret
8
+ # Returns the String Consumer Key related to this token.
9
+ attr_reader :consumer_key
10
+ # Returns the Time this token expires at.
11
+ attr_reader :expires_at
12
+ # Returns the String nonce related to this token.
13
+ attr_reader :nonce
14
+ # Returns the Time this token was created.
15
+ attr_reader :timestamp
16
+ # Returns the String Token.
17
+ attr_reader :token
18
+ # Returns the String Token Secret related to this token.
19
+ attr_reader :token_secret
20
+
21
+ # Public: Constructs an instance.
22
+ #
23
+ # arguments - The keyword arguments of the method:
24
+ # :accessor_secret - The String representing the accessor secret.
25
+ # :consumer_key - The String representing the consumer key.
26
+ # :expires_at - A Time representing the expiration moment or any object
27
+ # responding to to_i that represents the expiration moment
28
+ # as the number of seconds since the epoch.
29
+ # :nonce - The String representing the nonce.
30
+ # :expires_at - A Time representing the creation moment or any object
31
+ # responding to to_i that represents the creation moment
32
+ # as the number of seconds since the epoch.
33
+ # :token - The String representing the token.
34
+ # :token_secret - The String representing the token secret.
35
+ #
36
+ # Raises ArgumentError if any of the arguments is nil
37
+ def initialize(accessor_secret:, consumer_key:, expires_at:, nonce:, timestamp:, token:, token_secret:)
38
+ raise ArgumentError, 'accessor_secret is nil' unless accessor_secret
39
+ raise ArgumentError, 'consumer_key is nil' unless consumer_key
40
+ raise ArgumentError, 'expires_at is nil' unless expires_at
41
+ raise ArgumentError, 'nonce is nil' unless nonce
42
+ raise ArgumentError, 'timestamp is nil' unless timestamp
43
+ raise ArgumentError, 'token is nil' unless token
44
+ raise ArgumentError, 'token_secret is nil' unless token_secret
45
+
46
+ @accessor_secret = accessor_secret
47
+ @consumer_key = consumer_key
48
+ @expires_at = convert_to_time(expires_at)
49
+ @nonce = nonce
50
+ @timestamp = convert_to_time(timestamp)
51
+ @token = token
52
+ @token_secret = token_secret
53
+ @authorization_header = nil
54
+ end
55
+
56
+ # Public: Generates a value suitable for use as an HTTP Authorization header.
57
+ #
58
+ # Returns a String representation of the access token.
59
+ def authorization_header
60
+ return @authorization_header if @authorization_header
61
+
62
+ tuples = {
63
+ oauth_version: '1.0',
64
+ oauth_signature_method: 'PLAINTEXT',
65
+ oauth_signature: "#{@accessor_secret}&#{@token_secret}",
66
+ oauth_consumer_key: @consumer_key,
67
+ oauth_nonce: @nonce,
68
+ oauth_timestamp: @timestamp.tv_sec,
69
+ oauth_token: @token
70
+ }
71
+ @authorization_header = "OAuth " + tuples.map { |k, v| "#{k}=\"#{URI.encode_www_form_component(v)}\"" }.join(', ')
72
+ end
73
+
74
+ # Public: Check whether the access token has expired. By default (with no arguments),
75
+ # the method checks whether the token has expired based on the current time and a fudge
76
+ # factor of 300 seconds. Non-default argument values can be used to see whether the
77
+ # access token has expired at a different time and with a different fudge factor.
78
+ #
79
+ # now - A Time instance to check the expiration information against. Default is Time.now.
80
+ # fudge_sec - The number of seconds to remove from 'now' to adjust the comparion.
81
+ #
82
+ # Returns true if the access token as expired; false otherwise
83
+ def expired?(now: Time.now, fudge_sec: 300)
84
+ now = convert_to_time(now)
85
+ (now.tv_sec - fudge_sec) >= expires_at.tv_sec
86
+ end
87
+
88
+ # Public: Compare this to other based on attributes.
89
+ #
90
+ # Return true if equal; false otherwise
91
+ def ==(other)
92
+ accessor_secret == other.accessor_secret &&
93
+ consumer_key == other.consumer_key &&
94
+ expires_at == other.expires_at &&
95
+ nonce == other.nonce &&
96
+ timestamp == other.timestamp &&
97
+ token == other.token &&
98
+ token_secret == other.token_secret
99
+ end
100
+
101
+ # Public: Compare this to other based on the attributes. Equivalent to calling #==.
102
+ #
103
+ # Return true if equal; false otherwise
104
+ def eql?(other)
105
+ self == other
106
+ end
107
+
108
+ # Public: Generates a Hash of the attributes.
109
+ #
110
+ # Returns a Hash with keys for each attribute.
111
+ def to_h
112
+ {
113
+ accessor_secret: @accessor_secret,
114
+ consumer_key: @consumer_key,
115
+ expires_at: @expires_at,
116
+ nonce: @nonce,
117
+ timestamp: @timestamp,
118
+ token: @token,
119
+ token_secret: @token_secret
120
+ }
121
+ end
122
+
123
+ private
124
+
125
+ # Internal: Used by #initialize and #expired? to convert data into a Time instance.
126
+ # Returns a Time instance in the UTC time zone
127
+ def convert_to_time(time)
128
+ raise ArgumentError, 'time is nil' unless time
129
+ if time.is_a? Time
130
+ time.utc
131
+ else
132
+ Time.at(time.to_i).utc
133
+ end
134
+ end
135
+ end
136
+
137
+ end
138
+ end
@@ -0,0 +1,179 @@
1
+ require 'cerner/oauth1a/access_token'
2
+ require 'cerner/oauth1a/oauth_error'
3
+ require 'cerner/oauth1a/version'
4
+ require 'net/https'
5
+ require 'securerandom'
6
+ require 'uri'
7
+
8
+ module Cerner
9
+ module OAuth1a
10
+
11
+ # Public: A User Agent for interacting with the Access Token service to acquire
12
+ # Access Tokens.
13
+ class AccessTokenAgent
14
+ MIME_WWW_FORM_URL_ENCODED = 'application/x-www-form-urlencoded'
15
+
16
+ # Returns the URI Access Token URL.
17
+ attr_reader :access_token_url
18
+ # Returns the String Consumer Key.
19
+ attr_reader :consumer_key
20
+ # Returns the String Consumer Secret.
21
+ attr_reader :consumer_secret
22
+
23
+ # Public: Constructs an instance of the agent.
24
+ #
25
+ # arguments - The keyword arguments of the method:
26
+ # :access_token_url - The String or URI of the Access Token service endpoint.
27
+ # :consumer_key - The String of the Consumer Key of the account.
28
+ # :consumer_secret - The String of the Consumer Secret of the account.
29
+ # :open_timeout - An object responding to to_i. Used to set the timeout, in seconds,
30
+ # for opening HTTP connections to the Access Token service (optional, default: 5).
31
+ # :read_timeout - An object responding to to_i. Used to set the timeout, in seconds,
32
+ # for reading data from HTTP connections to the Access Token service (optional, default: 5).
33
+ #
34
+ # Raises ArgumentError if access_token_url, consumer_key or consumer_key is nil; if access_token_url is
35
+ # an invalid URI.
36
+ def initialize(access_token_url:, consumer_key:, consumer_secret:, open_timeout: 5, read_timeout: 5)
37
+ raise ArgumentError, 'consumer_key is nil' unless consumer_key
38
+ raise ArgumentError, 'consumer_secret is nil' unless consumer_secret
39
+
40
+ @consumer_key = consumer_key
41
+ @consumer_secret = consumer_secret
42
+
43
+ @access_token_url = convert_to_http_uri(access_token_url)
44
+
45
+ @open_timeout = (open_timeout ? open_timeout.to_i : 5)
46
+ @read_timeout = (read_timeout ? read_timeout.to_i : 5)
47
+ end
48
+
49
+ # Public: Retrives an AccessToken from the configured Access Token service endpoint (#access_token_url).
50
+ # This method will the #generate_accessor_secret, #generate_nonce and #generate_timestamp methods to
51
+ # interact with the service, which can be overridden via a sub-class, if desired.
52
+ #
53
+ # Returns a AccessToken upon success.
54
+ #
55
+ # Raises OAuthError unless the service returns a HTTP Status Code of 200.
56
+ # Raises StandardError sub-classes for any issues interacting with the service.
57
+ def retrieve
58
+ # construct a POST request
59
+ request = Net::HTTP::Post.new @access_token_url
60
+
61
+ # setup the data to construct the POST's message
62
+ accessor_secret = generate_accessor_secret
63
+ nonce = generate_nonce
64
+ timestamp = generate_timestamp
65
+ params = [
66
+ [:oauth_consumer_key, @consumer_key],
67
+ [:oauth_signature_method, 'PLAINTEXT'],
68
+ [:oauth_version, '1.0'],
69
+ [:oauth_timestamp, timestamp],
70
+ [:oauth_nonce, nonce],
71
+ [:oauth_signature, "#{@consumer_secret}&"],
72
+ [:oauth_accessor_secret, accessor_secret]
73
+ ]
74
+ # set the POST's body as a URL form-encoded string
75
+ request.set_form(params, MIME_WWW_FORM_URL_ENCODED, charset: 'UTF-8')
76
+
77
+ request['Accept'] = MIME_WWW_FORM_URL_ENCODED
78
+ # Set a custom User-Agent to help identify these invocation
79
+ request['User-Agent'] = "cerner-oauth1a #{VERSION} (Ruby #{RUBY_VERSION})"
80
+
81
+ http = Net::HTTP.new(@access_token_url.host, @access_token_url.port)
82
+ if @access_token_url.scheme == 'https'
83
+ # if the scheme is HTTPS, then enable SSL
84
+ http.use_ssl = true
85
+ # make sure to verify peers
86
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
87
+ # tweak the ciphers to eliminate unsafe options
88
+ http.ciphers = 'DEFAULT:!aNULL:!eNULL:!LOW:!SSLv2:!RC4'
89
+ end
90
+ http.open_timeout = @open_timeout
91
+ http.read_timeout = @read_timeout
92
+
93
+ response = http.request request
94
+
95
+ case response
96
+ when Net::HTTPSuccess
97
+ # Part the HTTP response and convert it into a Symbol-keyed Hash
98
+ tuples = Hash[URI.decode_www_form(response.body).map { |pair| [pair[0].to_sym, pair[1]] }]
99
+ # Use the parsed response to construct the AccessToken
100
+ access_token = AccessToken.new(accessor_secret: accessor_secret,
101
+ consumer_key: @consumer_key,
102
+ expires_at: timestamp + tuples[:oauth_expires_in].to_i,
103
+ nonce: nonce,
104
+ timestamp: timestamp,
105
+ token: tuples[:oauth_token],
106
+ token_secret: tuples[:oauth_token_secret])
107
+ access_token
108
+ else
109
+ # Extract any OAuth Problems reported in the response
110
+ oauth_data = parse_www_authenticate(response['WWW-Authenticate'])
111
+ # Raise an error for a failure to acquire a token
112
+ raise OAuthError.new('unable to acquire token', response.code, oauth_data['oauth_problem'])
113
+ end
114
+ end
115
+
116
+ # Public: Generate an Accessor Secret for invocations of the Access Token service.
117
+ #
118
+ # Returns a String containing the secret.
119
+ def generate_accessor_secret
120
+ SecureRandom.uuid
121
+ end
122
+
123
+ # Public: Generate a Nonce for invocations of the Access Token service.
124
+ #
125
+ # Returns a String containing the nonce.
126
+ def generate_nonce
127
+ SecureRandom.hex
128
+ end
129
+
130
+ # Public: Generate a Timestamp for invocations of the Access Token service.
131
+ #
132
+ # Returns an Integer representing the number of seconds since the epoch.
133
+ def generate_timestamp
134
+ Time.now.to_i
135
+ end
136
+
137
+ private
138
+
139
+ # Internal: Parse a WWW-Authenticate HTTP header for any OAuth
140
+ # information, which is indicated by a value starting with 'OAuth '.
141
+ #
142
+ # value - The String containing the header value.
143
+ #
144
+ # Returns a Hash containing any name-value pairs found in the value.
145
+ def parse_www_authenticate(value)
146
+ return {} unless value
147
+ value = value.strip
148
+ return {} unless value.start_with?('OAuth ')
149
+
150
+ Hash[value.scan(/([^\s=]*)=\"([^\"]*)\"/)]
151
+ end
152
+
153
+ # Internal: Convert an Access Token URL into a URI with some verification checks
154
+ #
155
+ # access_token_url - A String URL or a URI instance
156
+ # Returns a URI::HTTP or URI::HTTPS
157
+ #
158
+ # Raises ArgumentError if access_token_url is nil, invalid or not an HTTP/HTTPS URI
159
+ def convert_to_http_uri(access_token_url)
160
+ raise ArgumentError, 'access_token_url is nil' unless access_token_url
161
+ if access_token_url.is_a? URI
162
+ uri = access_token_url
163
+ else
164
+ begin
165
+ uri = URI(access_token_url)
166
+ rescue URI::InvalidURIError => e
167
+ # raise argument error with cause
168
+ raise ArgumentError, 'access_token_url is invalid'
169
+ end
170
+ end
171
+ unless uri.is_a? URI::HTTP
172
+ raise ArgumentError, 'access_token_url must be an HTTP or HTTPS URI'
173
+ end
174
+ uri
175
+ end
176
+ end
177
+
178
+ end
179
+ end
@@ -0,0 +1,26 @@
1
+ module Cerner
2
+ module OAuth1a
3
+ # Public: An OAuth-specific error.
4
+ class OAuthError < StandardError
5
+ # Returns the HTTP Response Code, if any, associated with this error.
6
+ attr_reader :http_response_code
7
+ # Returns the OAuth Problem string, if any, associated with this error.
8
+ # See http://oauth.pbwiki.com/ProblemReporting for more information.
9
+ attr_reader :oauth_problem
10
+
11
+ # Public: Construct an instance with a message, optional HTTP response code
12
+ # and optional OAuth Problem string.
13
+ #
14
+ # message - A descriptive message, passed to the super class.
15
+ # http_response_code - The HTTP response code associated with the error. Optional.
16
+ # oauth_problem - The OAuth Problem string associated with the error. Optional.
17
+ def initialize(message, http_response_code=nil, oauth_problem=nil)
18
+ @http_response_code = http_response_code
19
+ @oauth_problem = oauth_problem
20
+ message += " HTTP #{@http_response_code}" if @http_response_code
21
+ message += " OAuth Problem #{@oauth_problem}" if @oauth_problem
22
+ super message
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ module Cerner
2
+ module OAuth1a
3
+ VERSION = '1.0.0'
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cerner-oauth1a
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nathan Beyer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A minimal dependency client library for two-legged OAuth1.0a service
14
+ providers, such as Cerner's OAuth 1.0aprovider.
15
+ email:
16
+ - nbeyer@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - README.md
22
+ - lib/cerner/oauth1a.rb
23
+ - lib/cerner/oauth1a/access_token.rb
24
+ - lib/cerner/oauth1a/access_token_agent.rb
25
+ - lib/cerner/oauth1a/oauth_error.rb
26
+ - lib/cerner/oauth1a/version.rb
27
+ homepage: http://github.com/cerner/cerner-oauth1a
28
+ licenses: []
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '2.2'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 2.6.8
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: B2B/two-legged OAuth 1.0a service client.
50
+ test_files: []