signet 0.1.4 → 0.2.0

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.
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ == 0.2.0
2
+
3
+ * Added support for OAuth 2.0 draft 10
4
+
1
5
  == 0.1.4
2
6
 
3
7
  * Added support for a two-legged authorization flow
data/README CHANGED
@@ -14,6 +14,8 @@ Signet is an OAuth 1.0 / OAuth 2.0 implementation.
14
14
  - {Signet::OAuth1}
15
15
  - {Signet::OAuth1::Client}
16
16
  - {Signet::OAuth1::Credential}
17
+ - {Signet::OAuth2}
18
+ - {Signet::OAuth2::Client}
17
19
 
18
20
  == Example Usage for Google
19
21
 
@@ -36,6 +38,7 @@ Signet is an OAuth 1.0 / OAuth 2.0 implementation.
36
38
  response = client.fetch_protected_resource(
37
39
  :uri => 'https://mail.google.com/mail/feed/atom'
38
40
  )
41
+ # The Rack response format is used here
39
42
  status, headers, body = response
40
43
 
41
44
  == Install
data/lib/signet.rb CHANGED
@@ -15,4 +15,58 @@
15
15
  require 'signet/version'
16
16
 
17
17
  module Signet #:nodoc:
18
+ def self.parse_auth_param_list(auth_param_string)
19
+ # Production rules from:
20
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-12
21
+ token = /[-!#$\%&'*+.^_`|~0-9a-zA-Z]+/
22
+ d_qdtext = /[\s\x21\x23-\x5B\x5D-\x7E\x80-\xFF]/
23
+ d_quoted_pair = /\\[\s\x21-\x7E\x80-\xFF]/
24
+ d_qs = /"(?:#{d_qdtext}|#{d_quoted_pair})*"/
25
+ # Production rules that allow for more liberal parsing, i.e. single quotes
26
+ s_qdtext = /[\s\x21-\x26\x28-\x5B\x5D-\x7E\x80-\xFF]/
27
+ s_quoted_pair = /\\[\s\x21-\x7E\x80-\xFF]/
28
+ s_qs = /'(?:#{s_qdtext}|#{s_quoted_pair})*'/
29
+ # Combine the above production rules to find valid auth-param pairs.
30
+ auth_param = /((?:#{token})\s*=\s*(?:#{d_qs}|#{s_qs}|#{token}))/
31
+ auth_param_pairs = []
32
+ position = 0
33
+ last_match = nil
34
+ remainder = auth_param_string
35
+ # Iterate over the string, consuming pair matches as we go. Verify that
36
+ # pre-matches and post-matches contain only allowable characters.
37
+ #
38
+ # This would be way easier in Ruby 1.9, but we want backwards
39
+ # compatibility.
40
+ while (match = remainder.match(auth_param))
41
+ if match.pre_match && match.pre_match !~ /^[\s,]*$/
42
+ raise ParseError,
43
+ "Unexpected auth param format: '#{auth_param_string}'."
44
+ end
45
+ pair = match.captures[0]
46
+ auth_param_pairs << pair
47
+ remainder = match.post_match
48
+ last_match = match
49
+ end
50
+ if last_match.post_match && last_match.post_match !~ /^[\s,]*$/
51
+ raise ParseError,
52
+ "Unexpected auth param format: '#{auth_param_string}'."
53
+ end
54
+ # Now parse the auth-param pair strings & turn them into key-value pairs.
55
+ return (auth_param_pairs.inject([]) do |accu, pair|
56
+ name, value = pair.split('=', 2)
57
+ if value =~ /^".*"$/
58
+ value = value.gsub(/^"(.*)"$/, '\1').gsub(/\\(.)/, '\1')
59
+ elsif value =~ /^'.*'$/
60
+ value = value.gsub(/^'(.*)'$/, '\1').gsub(/\\(.)/, '\1')
61
+ elsif value =~ /[\(\)<>@,;:\\\"\/\[\]?={}]/
62
+ # Certain special characters are not allowed
63
+ raise ParseError, (
64
+ "Unexpected characters in auth param " +
65
+ "list: '#{auth_param_string}'."
66
+ )
67
+ end
68
+ accu << [name, value]
69
+ accu
70
+ end)
71
+ end
18
72
  end
data/lib/signet/errors.rb CHANGED
@@ -12,27 +12,62 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- module Signet #:nodoc:
15
+ require 'addressable/uri'
16
+
17
+ module Signet
18
+ ##
19
+ # An error indicating that the client has aborted an operation that
20
+ # would have been unsafe to perform.
21
+ class UnsafeOperationError < StandardError
22
+ end
23
+
24
+ ##
25
+ # An error indicating the client failed to parse a value.
26
+ class ParseError < StandardError
27
+ end
28
+
29
+ ##
30
+ # An error indicating the server refused to authorize the client.
16
31
  class AuthorizationError < StandardError
17
32
  ##
18
33
  # Creates a new authentication error.
19
34
  #
20
35
  # @param [String] message
21
36
  # A message describing the error.
22
- # @param [Array] request
23
- # A tuple of method, uri, headers, and body. Optional.
24
- # @param [Array] response
25
- # A tuple of status, headers, and body. Optional.
26
- def initialize(message, request=nil, response=nil)
37
+ # @param [Hash] options
38
+ # The configuration parameters for the request.
39
+ # - <code>:request</code>
40
+ # A tuple of method, uri, headers, and body. Optional.
41
+ # - <code>:response</code>
42
+ # A tuple of status, headers, and body. Optional.
43
+ # - <code>:code</code> —
44
+ # An error code.
45
+ # - <code>:description</code> —
46
+ # Human-readable text intended to be used to assist in resolving the
47
+ # error condition.
48
+ # - <code>:uri</code> —
49
+ # A URI identifying a human-readable web page with additional
50
+ # information about the error, indended for the resource owner.
51
+ def initialize(message, options={})
27
52
  super(message)
28
- @request = request
29
- @response = response
53
+ @options = options
54
+ @request = options[:request]
55
+ @response = options[:response]
56
+ @code = options[:code]
57
+ @description = options[:description]
58
+ @uri = Addressable::URI.parse(options[:uri])
30
59
  end
31
60
 
61
+ ##
62
+ # The HTTP request that triggered this authentication error.
63
+ #
64
+ # @return [Array] A tuple of method, uri, headers, and body.
65
+ attr_reader :request
66
+
32
67
  ##
33
68
  # The HTTP response that triggered this authentication error.
34
69
  #
35
70
  # @return [Array] A tuple of status, headers, and body.
36
71
  attr_reader :response
37
72
  end
38
- end
73
+ end
@@ -1,4 +1,5 @@
1
1
  require 'addressable/uri'
2
+ require 'signet'
2
3
 
3
4
  begin
4
5
  require 'securerandom'
@@ -226,27 +227,27 @@ module Signet #:nodoc:
226
227
  # Parses an <code>Authorization</code> header into its component
227
228
  # parameters. Parameter keys and values are decoded according to the
228
229
  # rules given in RFC 5849.
229
- def self.parse_authorization_header(header)
230
- if !header.kind_of?(String)
231
- raise TypeError, "Expected String, got #{header.class}."
230
+ def self.parse_authorization_header(field_value)
231
+ if !field_value.kind_of?(String)
232
+ raise TypeError, "Expected String, got #{field_value.class}."
232
233
  end
233
- unless header[0...6] == 'OAuth '
234
- raise ArgumentError,
234
+ auth_scheme = field_value[/^([-._0-9a-zA-Z]+)/, 1]
235
+ case auth_scheme
236
+ when /^OAuth$/i
237
+ # Other token types may be supported eventually
238
+ pairs = Signet.parse_auth_param_list(field_value[/^OAuth\s+(.*)$/i, 1])
239
+ return (pairs.inject([]) do |accu, (k, v)|
240
+ if k != 'realm'
241
+ k = self.unencode(k)
242
+ v = self.unencode(v)
243
+ end
244
+ accu << [k, v]
245
+ accu
246
+ end)
247
+ else
248
+ raise ParseError,
235
249
  'Parsing non-OAuth Authorization headers is out of scope.'
236
250
  end
237
- header = header.gsub(/^OAuth /, '')
238
- return header.split(/,\s*/).inject([]) do |accu, pair|
239
- k = pair[/^(.*?)=\"[^\"]*\"/, 1]
240
- v = pair[/^.*?=\"([^\"]*)\"/, 1]
241
- if k != 'realm'
242
- k = self.unencode(k)
243
- v = self.unencode(v)
244
- else
245
- v = v.gsub('\"', '"')
246
- end
247
- accu << [k, v]
248
- accu
249
- end
250
251
  end
251
252
 
252
253
  ##
@@ -356,7 +357,7 @@ module Signet #:nodoc:
356
357
  }.merge(options)
357
358
  temporary_credential_key =
358
359
  self.extract_credential_key_option(:temporary, options)
359
- parsed_uri = Addressable::URI.parse(authorization_uri)
360
+ parsed_uri = Addressable::URI.parse(authorization_uri).dup
360
361
  query_values = parsed_uri.query_values || {}
361
362
  if options[:additional_parameters]
362
363
  query_values = query_values.merge(
@@ -14,11 +14,12 @@
14
14
 
15
15
  require 'stringio'
16
16
  require 'addressable/uri'
17
+ require 'signet'
18
+ require 'signet/errors'
17
19
  require 'signet/oauth_1'
18
20
  require 'signet/oauth_1/credential'
19
- require 'signet/errors'
20
21
 
21
- module Signet #:nodoc:
22
+ module Signet
22
23
  module OAuth1
23
24
  class Client
24
25
  ##
@@ -28,7 +29,8 @@ module Signet #:nodoc:
28
29
  # The configuration parameters for the client.
29
30
  # - <code>:temporary_credential_uri</code> —
30
31
  # The OAuth temporary credentials URI.
31
- # - <code>:authorization_uri</code> — The OAuth authorization URI.
32
+ # - <code>:authorization_uri</code> —
33
+ # The OAuth authorization URI.
32
34
  # - <code>:token_credential_uri</code> —
33
35
  # The OAuth token credentials URI.
34
36
  # - <code>:client_credential_key</code> —
@@ -555,6 +557,7 @@ module Signet #:nodoc:
555
557
  )
556
558
  ]
557
559
  headers = [authorization_header]
560
+ headers << ['Cache-Control', 'no-store']
558
561
  if method == 'POST'
559
562
  headers << ['Content-Type', 'application/x-www-form-urlencoded']
560
563
  end
@@ -622,15 +625,17 @@ module Signet #:nodoc:
622
625
  if body.strip.length > 0
623
626
  message += " Server message:\n#{body.strip}"
624
627
  end
625
- error = ::Signet::AuthorizationError.new(message, request, response)
626
- raise error
628
+ raise ::Signet::AuthorizationError.new(
629
+ message, :request => request, :response => response
630
+ )
627
631
  else
628
632
  message = "Unexpected status code: #{status}."
629
633
  if body.strip.length > 0
630
634
  message += " Server message:\n#{body.strip}"
631
635
  end
632
- error = ::Signet::AuthorizationError.new(message, request, response)
633
- raise error
636
+ raise ::Signet::AuthorizationError.new(
637
+ message, :request => request, :response => response
638
+ )
634
639
  end
635
640
  end
636
641
  alias_method(
@@ -727,6 +732,7 @@ module Signet #:nodoc:
727
732
  )
728
733
  ]
729
734
  headers = [authorization_header]
735
+ headers << ['Cache-Control', 'no-store']
730
736
  if method == 'POST'
731
737
  headers << ['Content-Type', 'application/x-www-form-urlencoded']
732
738
  end
@@ -792,15 +798,17 @@ module Signet #:nodoc:
792
798
  if body.strip.length > 0
793
799
  message += " Server message:\n#{body.strip}"
794
800
  end
795
- error = ::Signet::AuthorizationError.new(message, request, response)
796
- raise error
801
+ raise ::Signet::AuthorizationError.new(
802
+ message, :request => request, :response => response
803
+ )
797
804
  else
798
805
  message = "Unexpected status code: #{status}."
799
806
  if body.strip.length > 0
800
807
  message += " Server message:\n#{body.strip}"
801
808
  end
802
- error = ::Signet::AuthorizationError.new(message, request, response)
803
- raise error
809
+ raise ::Signet::AuthorizationError.new(
810
+ message, :request => request, :response => response
811
+ )
804
812
  end
805
813
  end
806
814
  alias_method(
@@ -959,6 +967,7 @@ module Signet #:nodoc:
959
967
  )
960
968
  ]
961
969
  headers << authorization_header
970
+ headers << ['Cache-Control', 'no-store']
962
971
  return [method, uri.to_str, headers, [body]]
963
972
  end
964
973
 
@@ -1032,8 +1041,9 @@ module Signet #:nodoc:
1032
1041
  if body.strip.length > 0
1033
1042
  message += " Server message:\n#{body.strip}"
1034
1043
  end
1035
- error = ::Signet::AuthorizationError.new(message, request, response)
1036
- raise error
1044
+ raise ::Signet::AuthorizationError.new(
1045
+ message, :request => request, :response => response
1046
+ )
1037
1047
  else
1038
1048
  return response
1039
1049
  end
@@ -0,0 +1,148 @@
1
+ # Copyright (C) 2010 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'base64'
16
+ require 'signet'
17
+ require 'json'
18
+
19
+ module Signet #:nodoc:
20
+ ##
21
+ # An implementation of http://tools.ietf.org/html/draft-ietf-oauth-v2-10
22
+ #
23
+ # This module will be updated periodically to support newer drafts of the
24
+ # specification, as they become widely deployed.
25
+ module OAuth2
26
+ def self.parse_authorization_header(field_value)
27
+ auth_scheme = field_value[/^([-._0-9a-zA-Z]+)/, 1]
28
+ case auth_scheme
29
+ when /^Basic$/i
30
+ # HTTP Basic is allowed in OAuth 2
31
+ return self.parse_basic_credentials(field_value[/^Basic\s+(.*)$/i, 1])
32
+ when /^OAuth$/i
33
+ # Other token types may be supported eventually
34
+ return self.parse_bearer_credentials(field_value[/^OAuth\s+(.*)$/i, 1])
35
+ else
36
+ raise ParseError,
37
+ 'Parsing non-OAuth Authorization headers is out of scope.'
38
+ end
39
+ end
40
+
41
+ def self.parse_www_authenticate_header(field_value)
42
+ auth_scheme = field_value[/^([-._0-9a-zA-Z]+)/, 1]
43
+ case auth_scheme
44
+ when /^OAuth$/i
45
+ # Other token types may be supported eventually
46
+ return self.parse_oauth_challenge(field_value[/^OAuth\s+(.*)$/i, 1])
47
+ else
48
+ raise ParseError,
49
+ 'Parsing non-OAuth WWW-Authenticate headers is out of scope.'
50
+ end
51
+ end
52
+
53
+ def self.parse_basic_credentials(credential_string)
54
+ decoded = Base64.decode64(credential_string)
55
+ client_id, client_secret = decoded.split(':', 2)
56
+ return [['client_id', client_id], ['client_secret', client_secret]]
57
+ end
58
+
59
+ def self.parse_bearer_credentials(credential_string)
60
+ access_token = credential_string[/^([^,\s]+)(?:\s|,|$)/i, 1]
61
+ parameters = []
62
+ parameters << ['access_token', access_token]
63
+ auth_param_string = credential_string[/^(?:[^,\s]+)\s*,\s*(.*)$/i, 1]
64
+ if auth_param_string
65
+ # This code will rarely get called, but is included for completeness
66
+ parameters.concat(Signet.parse_auth_param_list(auth_param_string))
67
+ end
68
+ return parameters
69
+ end
70
+
71
+ def self.parse_oauth_challenge(challenge_string)
72
+ return Signet.parse_auth_param_list(challenge_string)
73
+ end
74
+
75
+ def self.parse_json_credentials(body)
76
+ if !body.kind_of?(String)
77
+ raise TypeError, "Expected String, got #{body.class}."
78
+ end
79
+ return JSON.parse(body)
80
+ end
81
+
82
+ ##
83
+ # Generates a Basic Authorization header from a client identifier and a
84
+ # client password.
85
+ #
86
+ # @param [String] client_id
87
+ # The client identifier.
88
+ # @param [String] client_password
89
+ # The client password.
90
+ #
91
+ # @return [String]
92
+ # The value for the HTTP Basic Authorization header.
93
+ def self.generate_basic_authorization_header(client_id, client_password)
94
+ if client_id =~ /:/
95
+ raise ArgumentError,
96
+ "A client identifier may not contain a ':' character."
97
+ end
98
+ return 'Basic ' + Base64.encode64(
99
+ client_id + ':' + client_password
100
+ ).gsub(/\n/, '')
101
+ end
102
+
103
+ ##
104
+ # Generates a Basic Authorization header from a client identifier and a
105
+ # client password.
106
+ #
107
+ # @param [String] client_id
108
+ # The client identifier.
109
+ # @param [String] client_password
110
+ # The client password.
111
+ #
112
+ # @return [String]
113
+ # The value for the HTTP Basic Authorization header.
114
+ def self.generate_bearer_authorization_header(
115
+ access_token, auth_params=nil)
116
+ # TODO: escaping?
117
+ header = "OAuth #{access_token}"
118
+ if auth_params && !auth_params.empty?
119
+ header += (", " +
120
+ auth_params.inject('') do |accu, (key, value)|
121
+ accu += "#{key}=\"#{value}\""
122
+ accu
123
+ end
124
+ )
125
+ end
126
+ return header
127
+ end
128
+
129
+ ##
130
+ # Appends the necessary OAuth parameters to
131
+ # the base authorization endpoint URI.
132
+ #
133
+ # @param [Addressable::URI, String, #to_str] authorization_uri
134
+ # The base authorization endpoint URI.
135
+ #
136
+ # @return [String] The authorization URI to redirect the user to.
137
+ def self.generate_authorization_uri(authorization_uri, parameters={})
138
+ for key, value in parameters
139
+ parameters.delete(key) if value == nil
140
+ end
141
+ parsed_uri = Addressable::URI.parse(authorization_uri).dup
142
+ query_values = parsed_uri.query_values || {}
143
+ query_values = query_values.merge(parameters)
144
+ parsed_uri.query_values = query_values
145
+ return parsed_uri.normalize.to_s
146
+ end
147
+ end
148
+ end