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 +4 -0
- data/README +3 -0
- data/lib/signet.rb +54 -0
- data/lib/signet/errors.rb +44 -9
- data/lib/signet/oauth_1.rb +20 -19
- data/lib/signet/oauth_1/client.rb +23 -13
- data/lib/signet/oauth_2.rb +148 -0
- data/lib/signet/oauth_2/client.rb +851 -0
- data/lib/signet/version.rb +4 -4
- data/spec/signet/oauth_1_spec.rb +25 -2
- data/spec/signet/oauth_2/client_spec.rb +70 -0
- data/spec/signet/oauth_2_spec.rb +164 -0
- data/spec/signet_spec.rb +80 -0
- data/tasks/gem.rake +1 -0
- data/tasks/spec.rake +4 -2
- metadata +38 -17
data/CHANGELOG
CHANGED
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
|
-
|
|
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 [
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
|
|
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
|
-
@
|
|
29
|
-
@
|
|
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
|
data/lib/signet/oauth_1.rb
CHANGED
|
@@ -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(
|
|
230
|
-
if !
|
|
231
|
-
raise TypeError, "Expected String, got #{
|
|
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
|
-
|
|
234
|
-
|
|
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
|
|
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> —
|
|
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
|
-
|
|
626
|
-
|
|
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
|
-
|
|
633
|
-
|
|
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
|
-
|
|
796
|
-
|
|
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
|
-
|
|
803
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
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
|