signet 0.1.4 → 0.2.0

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