cf-uaa-lib 1.3.1 → 1.3.2

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.
@@ -21,8 +21,8 @@ module CF::UAA
21
21
  # but they do not obtain them from the Authorization Server. This
22
22
  # class is for resource servers which accept bearer JWT tokens.
23
23
  #
24
- # For more on JWT, see the JSON Web \Token RFC here:
25
- # http://tools.ietf.org/id/draft-ietf-oauth-json-web-token-05.html
24
+ # For more on JWT, see the JSON Web Token RFC here:
25
+ # {http://tools.ietf.org/id/draft-ietf-oauth-json-web-token-05.html}
26
26
  #
27
27
  # An instance of this class can be used to decode and verify the contents
28
28
  # of a bearer token. Methods of this class can validate token signatures
@@ -30,19 +30,40 @@ module CF::UAA
30
30
  # is for a particular audience.
31
31
  class TokenCoder
32
32
 
33
- def self.init_digest(algo) # :nodoc:
33
+ def self.init_digest(algo) # @private
34
34
  OpenSSL::Digest::Digest.new(algo.sub('HS', 'sha').sub('RS', 'sha'))
35
35
  end
36
36
 
37
- # Takes a +token_body+ (the middle section of the JWT) and returns a signed
38
- # token string.
39
- def self.encode(token_body, skey, pkey = nil, algo = 'HS256')
37
+ def self.normalize_options(opts) # @private
38
+ opts = opts.dup
39
+ pk = opts[:pkey]
40
+ opts[:pkey] = OpenSSL::PKey::RSA.new(pk) if pk && !pk.is_a?(OpenSSL::PKey::PKey)
41
+ opts[:audience_ids] = Util.arglist(opts[:audience_ids])
42
+ opts[:algorithm] = 'HS256' unless opts[:algorithm]
43
+ opts[:verify] = true unless opts.key?(:verify)
44
+ opts
45
+ end
46
+
47
+ # Constructs a signed JWT.
48
+ # @param token_body Contents of the token in any object that can be converted to JSON.
49
+ # @param skey (see #initialize)
50
+ # @param pkey (see #initialize)
51
+ # @return [String] a signed JWT token string in the form "xxxx.xxxxx.xxxx".
52
+ def self.encode(token_body, options = {}, obsolete1 = nil, obsolete2 = nil)
53
+ unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
54
+ # deprecated: def self.encode(token_body, skey, pkey = nil, algo = 'HS256')
55
+ warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
56
+ options = {:skey => options }
57
+ options[:pkey], options[:algorithm] = obsolete1, obsolete2
58
+ end
59
+ options = normalize_options(options)
60
+ algo = options[:algorithm]
40
61
  segments = [Util.json_encode64("typ" => "JWT", "alg" => algo)]
41
62
  segments << Util.json_encode64(token_body)
42
63
  if ["HS256", "HS384", "HS512"].include?(algo)
43
- sig = OpenSSL::HMAC.digest(init_digest(algo), skey, segments.join('.'))
64
+ sig = OpenSSL::HMAC.digest(init_digest(algo), options[:skey], segments.join('.'))
44
65
  elsif ["RS256", "RS384", "RS512"].include?(algo)
45
- sig = pkey.sign(init_digest(algo), segments.join('.'))
66
+ sig = options[:pkey].sign(init_digest(algo), segments.join('.'))
46
67
  elsif algo == "none"
47
68
  sig = ""
48
69
  else
@@ -52,27 +73,37 @@ class TokenCoder
52
73
  segments.join('.')
53
74
  end
54
75
 
55
- # Decodes a +token+ and optionally verifies the signature. Both a secret key
56
- # and a public key can be provided for signature verification. The JWT
57
- # +token+ header indicates what signature algorithm was used and the
76
+ # Decodes a JWT token and optionally verifies the signature. Both a
77
+ # symmetrical key and a public key can be provided for signature verification.
78
+ # The JWT header indicates what signature algorithm was used and the
58
79
  # corresponding key is used to verify the signature (if +verify+ is true).
59
- # Returns a hash of the token contents or raises +DecodeError+.
60
- def self.decode(token, skey = nil, pkey = nil, verify = true)
61
- pkey = OpenSSL::PKey::RSA.new(pkey) unless pkey.nil? || pkey.is_a?(OpenSSL::PKey::PKey)
80
+ # @param [String] token A JWT token as returned by {TokenCoder.encode}
81
+ # @param skey (see #initialize)
82
+ # @param pkey (see #initialize)
83
+ # @param [Boolean] verify
84
+ # @return [Hash] the token contents
85
+ def self.decode(token, options = {}, obsolete1 = nil, obsolete2 = nil)
86
+ unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
87
+ # deprecated: def self.decode(token, skey = nil, pkey = nil, verify = true)
88
+ warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
89
+ options = {:skey => options }
90
+ options[:pkey], options[:verify] = obsolete1, obsolete2
91
+ end
92
+ options = normalize_options(options)
62
93
  segments = token.split('.')
63
94
  raise DecodeError, "Not enough or too many segments" unless [2,3].include? segments.length
64
95
  header_segment, payload_segment, crypto_segment = segments
65
96
  signing_input = [header_segment, payload_segment].join('.')
66
97
  header = Util.json_decode64(header_segment)
67
- payload = Util.json_decode64(payload_segment)
68
- return payload if !verify || (algo = header["alg"]) == "none"
98
+ payload = Util.json_decode64(payload_segment, (:sym if options[:symbolize_keys]))
99
+ return payload if !options[:verify] || (algo = header["alg"]) == "none"
69
100
  signature = Util.decode64(crypto_segment)
70
101
  if ["HS256", "HS384", "HS512"].include?(algo)
71
102
  raise DecodeError, "Signature verification failed" unless
72
- signature == OpenSSL::HMAC.digest(init_digest(algo), skey, signing_input)
103
+ signature == OpenSSL::HMAC.digest(init_digest(algo), options[:skey], signing_input)
73
104
  elsif ["RS256", "RS384", "RS512"].include?(algo)
74
105
  raise DecodeError, "Signature verification failed" unless
75
- pkey.verify(init_digest(algo), signature, signing_input)
106
+ options[:pkey].verify(init_digest(algo), signature, signing_input)
76
107
  else
77
108
  raise DecodeError, "Algorithm not supported"
78
109
  end
@@ -82,17 +113,24 @@ class TokenCoder
82
113
  # Creates a new token en/decoder for a service that is associated with
83
114
  # the the audience_ids, the symmetrical token validation key, and the
84
115
  # public and/or private keys. Parameters:
85
- # +audience_ids+:: an array or space separated strings. Should
86
- # indicate values which indicate the token is intended for this service
87
- # instance. It will be compared with tokens as they are decoded to
88
- # ensure that the token was intended for this audience.
89
- # +skey+:: is used to sign and validate tokens using symetrical key algoruthms
90
- # +pkey+:: may be a string or File which includes public and
91
- # optionally private key data in PEM or DER formats. The private key
92
- # is used to sign tokens and the public key is used to validate tokens.
93
- def initialize(audience_ids, skey, pkey = nil)
94
- @audience_ids, @skey, @pkey = Util.arglist(audience_ids), skey, pkey
95
- @pkey = OpenSSL::PKey::RSA.new(pkey) unless pkey.nil? || pkey.is_a?(OpenSSL::PKey::PKey)
116
+ # @param [Array<String>, String] audience_ids An array or space separated
117
+ # strings of values which indicate the token is intended for this service
118
+ # instance. It will be compared with tokens as they are decoded to ensure
119
+ # that the token was intended for this audience.
120
+ # @param [String] skey is used to sign and validate tokens using symmetrical
121
+ # key algoruthms
122
+ # @param [String, File, OpenSSL::PKey::PKey] pkey may be a String, File in
123
+ # PEM or DER formats. May include public and/or private key data. The
124
+ # private key is used to sign tokens and the public key is used to
125
+ # validate tokens.
126
+ def initialize(options = {}, obsolete1 = nil, obsolete2 = nil)
127
+ unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
128
+ # deprecated: def initialize(audience_ids, skey, pkey = nil)
129
+ warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
130
+ options = {:audience_ids => options }
131
+ options[:skey], options[:pkey] = obsolete1, obsolete2
132
+ end
133
+ @options = self.class.normalize_options(options)
96
134
  end
97
135
 
98
136
  # Encode a JWT token. Takes a hash of values to use as the token body.
@@ -100,26 +138,30 @@ class TokenCoder
100
138
  # Algorithm may be HS256, HS384, HS512, RS256, RS384, RS512, or none --
101
139
  # assuming the TokenCoder instance is configured with the appropriate
102
140
  # key -- i.e. pkey must include a private key for the RS algorithms.
103
- def encode(token_body = {}, algorithm = 'HS256')
104
- token_body['aud'] = @audience_ids unless token_body['aud']
105
- token_body['exp'] = Time.now.to_i + 7 * 24 * 60 * 60 unless token_body['exp']
106
- self.class.encode(token_body, @skey, @pkey, algorithm)
141
+ # @param token_body (see TokenCoder.encode)
142
+ # @return (see TokenCoder.encode)
143
+ def encode(token_body = {}, algorithm = nil)
144
+ token_body[:aud] = @options[:audience_ids] if @options[:audience_ids] && !token_body[:aud] && !token_body['aud']
145
+ token_body[:exp] = Time.now.to_i + 7 * 24 * 60 * 60 unless token_body[:exp] || token_body['exp']
146
+ self.class.encode(token_body, algorithm ? @options.merge(:algorithm => algorithm) : @options)
107
147
  end
108
148
 
109
149
  # Returns hash of values decoded from the token contents. If the
110
- # token contains audience ids in the +aud+ field and they do not contain one
111
- # or more of the +audience_ids+ in this instance, an AuthError will be raised.
112
- # AuthError is raised if the token has expired.
150
+ # audience_ids were specified in the options to this instance (see #initialize)
151
+ # and the token does not contain one or more of those audience_ids, an
152
+ # AuthError will be raised. AuthError is raised if the token has expired.
153
+ # @param [String] auth_header (see Scim.initialize#auth_header)
154
+ # @return (see TokenCoder.decode)
113
155
  def decode(auth_header)
114
- unless auth_header && (tkn = auth_header.split).length == 2 && tkn[0] =~ /^bearer$/i
156
+ unless auth_header && (tkn = auth_header.split(' ')).length == 2 && tkn[0] =~ /^bearer$/i
115
157
  raise DecodeError, "invalid authentication header: #{auth_header}"
116
158
  end
117
- reply = self.class.decode(tkn[1], @skey, @pkey)
118
- auds = Util.arglist(reply["aud"])
119
- if @audience_ids && (!auds || (auds & @audience_ids).empty?)
120
- raise AuthError, "invalid audience: #{auds.join(' ')}"
159
+ reply = self.class.decode(tkn[1], @options)
160
+ auds = Util.arglist(reply[:aud] || reply['aud'])
161
+ if @options[:audience_ids] && (!auds || (auds & @options[:audience_ids]).empty?)
162
+ raise AuthError, "invalid audience: #{auds}"
121
163
  end
122
- exp = reply["exp"]
164
+ exp = reply[:exp] || reply['exp']
123
165
  unless exp.is_a?(Integer) && exp > Time.now.to_i
124
166
  raise AuthError, "token expired"
125
167
  end
@@ -16,24 +16,28 @@ require 'uaa/http'
16
16
 
17
17
  module CF::UAA
18
18
 
19
- # The Token class is returned by various TokenIssuer methods. It holds access
19
+ # The TokenInfo class is returned by various TokenIssuer methods. It holds access
20
20
  # and refresh tokens as well as token meta-data such as token type and
21
- # expiration time. See Token#info for contents.
22
- class Token
21
+ # expiration time. See {TokenInfo#info} for contents.
22
+ class TokenInfo
23
23
 
24
- # Returns a hash of information about the current token. The info hash MUST include
24
+ # Information about the current token. The info hash MUST include
25
25
  # access_token, token_type and scope (if granted scope differs from requested
26
26
  # scope). It should include expires_in. It may include refresh_token, scope,
27
27
  # and other values from the auth server.
28
+ # @return [Hash]
28
29
  attr_reader :info
29
30
 
30
- def initialize(info) # :nodoc:
31
- @info = info
31
+ # Normally instantiated by {TokenIssuer}.
32
+ def initialize(info) @info = info end
33
+
34
+ # Constructs a string for use in an authorization header from the contents of
35
+ # the TokenInfo.
36
+ # @return [String] Typically a string such as "bearer xxxx.xxxx.xxxx".
37
+ def auth_header
38
+ "#{@info[:token_type] || @info['token_type']} #{@info[:access_token] || @info['access_token']}"
32
39
  end
33
40
 
34
- # Returns a string for use in an authorization header that is constructed
35
- # from contents of the Token. Typically a string such as "bearer xxxx.xxxx.xxxx".
36
- def auth_header; "#{info['token_type']} #{info['access_token']}" end
37
41
  end
38
42
 
39
43
  # Client Apps that want to get access to resource servers on behalf of their
@@ -42,51 +46,96 @@ end
42
46
  # class is for these use cases.
43
47
  #
44
48
  # In general most of this class is an implementation of the client pieces of
45
- # the OAuth2 protocol. See http://tools.ietf.org/html/rfc6749
49
+ # the OAuth2 protocol. See {http://tools.ietf.org/html/rfc6749}
46
50
  class TokenIssuer
47
51
 
48
52
  include Http
49
53
 
50
- # parameters:
51
- # [+target+] The base URL of a UAA's oauth authorize endpoint. For example
52
- # the target would be \https://login.cloudfoundry.com if the
53
- # endpoint is \https://login.cloudfoundry.com/oauth/authorize.
54
- # The target would be \http://localhost:8080/uaa if the endpoint
55
- # is \http://localhost:8080/uaa/oauth/authorize.
56
- # [+client_id+] The oauth2 client id. See http://tools.ietf.org/html/rfc6749#section-2.2
57
- # [+client_secret+] needed to authenticate the client for all grant types
58
- # except implicit.
59
- # [+token_target+] The base URL of the oauth token endpoint. If not specified,
60
- # +target+ is used.
61
- def initialize(target, client_id, client_secret = nil, token_target = nil)
54
+ private
55
+
56
+ def random_state; SecureRandom.hex end
57
+
58
+ def parse_implicit_params(encoded_params, state)
59
+ params = Util.decode_form(encoded_params)
60
+ raise BadResponse, "mismatched state" unless state && params.delete('state') == state
61
+ raise TargetError.new(params), "error response from #{@target}" if params['error']
62
+ raise BadResponse, "no type and token" unless params['token_type'] && params['access_token']
63
+ exp = params['expires_in'].to_i
64
+ params['expires_in'] = exp if exp.to_s == params['expires_in']
65
+ TokenInfo.new(Util.hash_keys!(params, @key_style))
66
+ rescue URI::InvalidURIError, ArgumentError
67
+ raise BadResponse, "received invalid response from target #{@target}"
68
+ end
69
+
70
+ # returns a CF::UAA::TokenInfo object which includes the access token and metadata.
71
+ def request_token(params)
72
+ if scope = Util.arglist(params.delete(:scope))
73
+ params[:scope] = Util.strlist(scope)
74
+ end
75
+ headers = {'content-type' => 'application/x-www-form-urlencoded',
76
+ 'accept' => 'application/json',
77
+ 'authorization' => Http.basic_auth(@client_id, @client_secret) }
78
+ reply = json_parse_reply(@key_style, *request(@token_target, :post,
79
+ '/oauth/token', Util.encode_form(params), headers))
80
+ raise BadResponse unless reply[jkey :token_type] && reply[jkey :access_token]
81
+ TokenInfo.new(reply)
82
+ end
83
+
84
+ def authorize_path_args(response_type, redirect_uri, scope, state = random_state, args = {})
85
+ params = args.merge(:client_id => @client_id, :response_type => response_type,
86
+ :redirect_uri => redirect_uri, :state => state)
87
+ params[:scope] = scope = Util.strlist(scope) if scope = Util.arglist(scope)
88
+ params[:nonce], params[:response_type] = state, "#{response_type} id_token" if scope && scope.include?('openid')
89
+ "/oauth/authorize?#{Util.encode_form(params)}"
90
+ end
91
+
92
+ def jkey(k) @key_style ? k : k.to_s end
93
+
94
+ public
95
+
96
+ # @param [String] target The base URL of a UAA's oauth authorize endpoint.
97
+ # For example the target would be {https://login.cloudfoundry.com} if the
98
+ # endpoint is {https://login.cloudfoundry.com/oauth/authorize}.
99
+ # The target would be {http://localhost:8080/uaa} if the endpoint
100
+ # is {http://localhost:8080/uaa/oauth/authorize}.
101
+ # @param [String] client_id The oauth2 client id, see
102
+ # {http://tools.ietf.org/html/rfc6749#section-2.2}
103
+ # @param [String] client_secret Needed to authenticate the client for all
104
+ # grant types except implicit.
105
+ # @param [Hash] options can be
106
+ # * +:token_target+, the base URL of the oauth token endpoint -- if
107
+ # not specified, +target+ is used.
108
+ # * +:symbolize_keys+, if true, returned hash keys are symbols.
109
+ def initialize(target, client_id, client_secret = nil, options = {})
62
110
  @target, @client_id, @client_secret = target, client_id, client_secret
63
- @token_target = token_target || target
111
+ @token_target = options[:token_target] || target
112
+ @key_style = options[:symbolize_keys] ? :sym : nil
64
113
  end
65
114
 
66
115
  # Allows an app to discover what credentials are required for
67
- # #implicit_grant_with_creds. Returns a hash of credential names with type
68
- # and suggested prompt value, e.g.
69
- # {"username":["text","Email"],"password":["password","Password"]}
116
+ # {#implicit_grant_with_creds}.
117
+ # @return [Hash] of credential names with type and suggested prompt value,
118
+ # e.g. !{"username":["text","Email"],"password":["password","Password"]}
70
119
  def prompts
71
- reply = json_get @target, '/login'
72
- return reply['prompts'] if reply && reply['prompts']
120
+ reply = json_get(@target, '/login')
121
+ return reply[jkey :prompts] if reply && reply[jkey :prompts]
73
122
  raise BadResponse, "No prompts in response from target #{@target}"
74
123
  end
75
124
 
76
125
  # Gets an access token in a single call to the UAA with the user
77
- # credentials used for authentication. The +credentials+ should
78
- # be an object such as a hash that can be converted to a json
79
- # representation of the credential name/value pairs
80
- # corresponding to the keys retrieved by #prompts.
81
- # Returns a Token.
126
+ # credentials used for authentication.
127
+ # @param credentials should be an object such as a hash that can be converted
128
+ # to a json representation of the credential name/value pairs corresponding to
129
+ # the keys retrieved by {#prompts}.
130
+ # @return [TokenInfo]
82
131
  def implicit_grant_with_creds(credentials, scope = nil)
83
132
  # this manufactured redirect_uri is a convention here, not part of OAuth2
84
133
  redir_uri = "https://uaa.cloudfoundry.com/redirect/#{@client_id}"
85
- uri = authorize_path_args("token", redir_uri, scope, state = SecureRandom.uuid)
134
+ uri = authorize_path_args("token", redir_uri, scope, state = random_state)
86
135
 
87
136
  # the accept header is only here so the uaa will issue error replies in json to aid debugging
88
137
  headers = {'content-type' => 'application/x-www-form-urlencoded', 'accept' => 'application/json' }
89
- body = URI.encode_www_form(credentials.merge('source' => 'credentials'))
138
+ body = Util.encode_form(credentials.merge(:source => 'credentials'))
90
139
  status, body, headers = request(@target, :post, uri, body, headers)
91
140
  raise BadResponse, "status #{status}" unless status == 302
92
141
  req_uri, reply_uri = URI.parse(redir_uri), URI.parse(headers['location'])
@@ -98,141 +147,109 @@ class TokenIssuer
98
147
  end
99
148
 
100
149
  # Constructs a uri that the client is to return to the browser to direct
101
- # the user to the authorization server to get an authcode. The +redirect_uri+
102
- # is embedded in the returned uri so the authorization server can redirect
103
- # the user back to the client app.
150
+ # the user to the authorization server to get an authcode.
151
+ # @param [String] redirect_uri (see #authcode_uri)
152
+ # @return [String]
104
153
  def implicit_uri(redirect_uri, scope = nil)
105
154
  @target + authorize_path_args("token", redirect_uri, scope)
106
155
  end
107
156
 
108
157
  # Gets a token via an implicit grant.
109
- # [+authcode_uri+] must be from a previous call to #implicit_uri and contains
110
- # state used to validate the contents of the reply from the
111
- # Authorization Server.
112
- # [+callback_fragment+] must be the fragment portion of the URL received by
113
- # user's browser after the Authorization Server
114
- # redirects back to the +redirect_uri+ that was given to
115
- # #implicit_uri. How the application get's the contents
116
- # of the fragment is application specific -- usually
117
- # some javascript in the page at the +redirect_uri+.
118
- #
119
- # See http://tools.ietf.org/html/rfc6749#section-4.2 .
120
- #
121
- # Returns a Token.
158
+ # @param [String] implicit_uri must be from a previous call to
159
+ # {#implicit_uri}, contains state used to validate the contents of the
160
+ # reply from the server.
161
+ # @param [String] callback_fragment must be the fragment portion of the URL
162
+ # received by the user's browser after the server redirects back to the
163
+ # +redirect_uri+ that was given to {#implicit_uri}. How the application
164
+ # gets the contents of the fragment is application specific -- usually
165
+ # some javascript in the page at the +redirect_uri+.
166
+ # @see http://tools.ietf.org/html/rfc6749#section-4.2
167
+ # @return [TokenInfo]
122
168
  def implicit_grant(implicit_uri, callback_fragment)
123
- in_params = Util.decode_form_to_hash(URI.parse(implicit_uri).query)
169
+ in_params = Util.decode_form(URI.parse(implicit_uri).query)
124
170
  unless in_params['state'] && in_params['redirect_uri']
125
171
  raise ArgumentError, "redirect must happen before implicit grant"
126
172
  end
127
- parse_implicit_params callback_fragment, in_params['state']
173
+ parse_implicit_params(callback_fragment, in_params['state'])
128
174
  end
129
175
 
130
176
  # A UAA extension to OAuth2 that allows a client to pre-authenticate a
131
177
  # user at the start of an authorization code flow. By passing in the
132
- # user's credentials (see #prompts) the Authorization Server can establish
133
- # a session with the user's browser without reprompting for authentication.
134
- # This is useful for user account management apps so that they can create
135
- # a user account, or reset a password for the user, without requiring the
136
- # user to type in their credentials again.
178
+ # user's credentials the server can establish a session with the user's
179
+ # browser without reprompting for authentication. This is useful for
180
+ # user account management apps so that they can create a user account,
181
+ # or reset a password for the user, without requiring the user to type
182
+ # in their credentials again.
183
+ # @param [String] credentials (see #implicit_grant_with_creds)
184
+ # @param [String] redirect_uri (see #authcode_uri)
185
+ # @return (see #authcode_uri)
137
186
  def autologin_uri(redirect_uri, credentials, scope = nil)
138
- headers = {'content_type' => 'application/x-www-form-urlencoded', 'accept' => 'application/json',
187
+ headers = {'content-type' => 'application/x-www-form-urlencoded',
188
+ 'accept' => 'application/json',
139
189
  'authorization' => Http.basic_auth(@client_id, @client_secret) }
140
- body = URI.encode_www_form(credentials)
141
- reply = json_parse_reply(*request(@target, :post, "/autologin", body, headers))
190
+ body = Util.encode_form(credentials)
191
+ reply = json_parse_reply(nil, *request(@target, :post, "/autologin", body, headers))
142
192
  raise BadResponse, "no autologin code in reply" unless reply['code']
143
- @target + authorize_path_args('code', redirect_uri, scope, SecureRandom.uuid, code: reply[:code])
193
+ @target + authorize_path_args('code', redirect_uri, scope,
194
+ random_state, :code => reply['code'])
144
195
  end
145
196
 
146
197
  # Constructs a uri that the client is to return to the browser to direct
147
- # the user to the authorization server to get an authcode. The redirect_uri
148
- # is embedded in the returned uri so the authorization server can redirect
149
- # the user back to the client app.
198
+ # the user to the authorization server to get an authcode.
199
+ # @param [String] redirect_uri is embedded in the returned uri so the server
200
+ # can redirect the user back to the caller's endpoint.
201
+ # @return [String] uri which
150
202
  def authcode_uri(redirect_uri, scope = nil)
151
203
  @target + authorize_path_args('code', redirect_uri, scope)
152
204
  end
153
205
 
154
206
  # Uses the instance client credentials in addition to +callback_query+
155
207
  # to get a token via the authorization code grant.
156
- # [+authcode_uri+] must be from a previous call to #authcode_uri and contains
157
- # state used to validate the contents of the reply from the
158
- # Authorization Server.
159
- # [callback_query] must be the query portion of the URL received by the
160
- # client after the user's browser is redirected back from
161
- # the Authorization server. It contains the authorization
162
- # code.
163
- #
164
- # See http://tools.ietf.org/html/rfc6749#section-4.1 .
165
- #
166
- # Returns a Token.
208
+ # @param [String] authcode_uri must be from a previous call to {#authcode_uri}
209
+ # and contains state used to validate the contents of the reply from the
210
+ # server.
211
+ # @param [String] callback_query must be the query portion of the URL
212
+ # received by the client after the user's browser is redirected back from
213
+ # the server. It contains the authorization code.
214
+ # @see http://tools.ietf.org/html/rfc6749#section-4.1
215
+ # @return [TokenInfo]
167
216
  def authcode_grant(authcode_uri, callback_query)
168
- ac_params = Util.decode_form_to_hash(URI.parse(authcode_uri).query)
217
+ ac_params = Util.decode_form(URI.parse(authcode_uri).query)
169
218
  unless ac_params['state'] && ac_params['redirect_uri']
170
219
  raise ArgumentError, "authcode redirect must happen before authcode grant"
171
220
  end
172
221
  begin
173
- params = Util.decode_form_to_hash(callback_query)
222
+ params = Util.decode_form(callback_query)
174
223
  authcode = params['code']
175
224
  raise BadResponse unless params['state'] == ac_params['state'] && authcode
176
225
  rescue URI::InvalidURIError, ArgumentError, BadResponse
177
226
  raise BadResponse, "received invalid response from target #{@target}"
178
227
  end
179
- request_token('grant_type' => 'authorization_code', 'code' => authcode, 'redirect_uri' => ac_params['redirect_uri'])
228
+ request_token(:grant_type => 'authorization_code', :code => authcode,
229
+ :redirect_uri => ac_params['redirect_uri'])
180
230
  end
181
231
 
182
232
  # Uses the instance client credentials in addition to the +username+
183
233
  # and +password+ to get a token via the owner password grant.
184
- # See http://tools.ietf.org/html/rfc6749#section-4.3 .
185
- # Returns a Token.
234
+ # See {http://tools.ietf.org/html/rfc6749#section-4.3}.
235
+ # @return [TokenInfo]
186
236
  def owner_password_grant(username, password, scope = nil)
187
- request_token('grant_type' => 'password', 'username' => username, 'password' => password, 'scope' => scope)
237
+ request_token(:grant_type => 'password', :username => username,
238
+ :password => password, :scope => scope)
188
239
  end
189
240
 
190
241
  # Uses the instance client credentials to get a token with a client
191
242
  # credentials grant. See http://tools.ietf.org/html/rfc6749#section-4.4
192
- # Returns a Token.
243
+ # @return [TokenInfo]
193
244
  def client_credentials_grant(scope = nil)
194
- request_token('grant_type' => 'client_credentials', 'scope' => scope)
245
+ request_token(:grant_type => 'client_credentials', :scope => scope)
195
246
  end
196
247
 
197
248
  # Uses the instance client credentials and the given +refresh_token+ to get
198
249
  # a new access token. See http://tools.ietf.org/html/rfc6749#section-6
199
- # Returns a Token, which may include a new refresh token as well as an access token.
250
+ # @return [TokenInfo] which may include a new refresh token as well as an access token.
200
251
  def refresh_token_grant(refresh_token, scope = nil)
201
- request_token('grant_type' => 'refresh_token', 'refresh_token' => refresh_token, 'scope' => scope)
202
- end
203
-
204
- private
205
-
206
- def parse_implicit_params(encoded_params, state)
207
- params = Util.decode_form_to_hash(encoded_params)
208
- raise BadResponse, "mismatched state" unless state && params.delete('state') == state
209
- raise TargetError.new(params), "error response from #{@target}" if params['error']
210
- raise BadResponse, "no type and token" unless params['token_type'] && params['access_token']
211
- exp = params['expires_in'].to_i
212
- params['expires_in'] = exp if exp.to_s == params['expires_in']
213
- Token.new params
214
- rescue URI::InvalidURIError, ArgumentError
215
- raise BadResponse, "received invalid response from target #{@target}"
216
- end
217
-
218
- # returns a CF::UAA::Token object which includes the access token and metadata.
219
- def request_token(params)
220
- if scope = Util.arglist(params.delete('scope'))
221
- params['scope'] = Util.strlist(scope)
222
- end
223
- headers = {'content-type' => 'application/x-www-form-urlencoded', 'accept' => 'application/json',
224
- 'authorization' => Http.basic_auth(@client_id, @client_secret) }
225
- body = URI.encode_www_form(params)
226
- reply = json_parse_reply(*request(@token_target, :post, '/oauth/token', body, headers))
227
- raise BadResponse unless reply['token_type'] && reply['access_token']
228
- Token.new reply
229
- end
230
-
231
- def authorize_path_args(response_type, redirect_uri, scope, state = SecureRandom.uuid, args = {})
232
- params = args.merge('client_id' => @client_id, 'response_type' => response_type, 'redirect_uri' => redirect_uri, 'state' => state)
233
- params['scope'] = scope = Util.strlist(scope) if scope = Util.arglist(scope)
234
- params['nonce'], params['response_type'] = state, "#{response_type} id_token" if scope && scope.include?('openid')
235
- "/oauth/authorize?#{URI.encode_www_form(params)}"
252
+ request_token(:grant_type => 'refresh_token', :refresh_token => refresh_token, :scope => scope)
236
253
  end
237
254
 
238
255
  end