cf-uaa-lib 1.3.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,43 +1,34 @@
1
1
  # CloudFoundry UAA Gem
2
2
 
3
- Client gem for interacting with the CloudFoundry UAA server.
3
+ Client gem for interacting with the [CloudFoundry UAA server](https://github.com/cloudfoundry/uaa)
4
4
 
5
- Set up a local ruby environment (so sudo not required):
5
+ ## Install from rubygems
6
6
 
7
- `$ rvm use 1.9.2`
7
+ $ gem install cf-uaa-lib
8
8
 
9
- or
9
+ ## Build from source
10
10
 
11
- `$ rbenv global 1.9.2-p180`
11
+ $ bundle install
12
+ $ gem build cf-uaa-lib.gemspec
13
+ $ gem install cf-uaa-lib<version>.gem
12
14
 
13
- see: https://rvm.io/ or http://rbenv.org/
15
+ ## Use the gem
14
16
 
15
- Build the gem
16
-
17
- `$ bundle install`
18
- `$ gem build cf-uaa-lib.gemspec`
19
-
20
- Install it
21
-
22
- `$ gem install cf-uaa-lib<version>.gem`
23
-
24
- Use the gem:
25
-
26
- `#!/usr/bin/env ruby`
27
- `require 'uaa'`
28
- `token_issuer = CF::UAA::TokenIssuer.new("https://uaa.cloudfoundry.com", "vmc")`
29
- `puts token\_issuer.prompts.inspect`
30
- `token = token_issuer.implicit_grant_with_creds(username: "<your_username>", password: "<your_password>")`
31
- `token_info = TokenCoder.decode(token.info["access_token"], nil, nil, false) #token signature not verified`
32
- `puts token_info["user_name"]`
17
+ #!/usr/bin/env ruby
18
+ require 'uaa'
19
+ token_issuer = CF::UAA::TokenIssuer.new("https://uaa.cloudfoundry.com", "vmc")
20
+ puts token\_issuer.prompts.inspect
21
+ token = token_issuer.implicit_grant_with_creds(username: "<your_username>", password: "<your_password>")
22
+ token_info = TokenCoder.decode(token.info["access_token"], nil, nil, false) #token signature not verified
23
+ puts token_info["user_name"]
33
24
 
34
25
  ## Tests
35
26
 
36
27
  Run the tests with rake:
37
28
 
38
- `$ bundle exec rake test`
29
+ $ bundle exec rake test
39
30
 
40
31
  Run the tests and see a fancy coverage report:
41
32
 
42
- `$ bundle exec rake cov`
33
+ $ bundle exec rake cov
43
34
 
data/lib/uaa/http.rb CHANGED
@@ -17,11 +17,22 @@ require 'uaa/util'
17
17
 
18
18
  module CF::UAA
19
19
 
20
+ # Indicates URL for the target is bad or not accessible
20
21
  class BadTarget < UAAError; end
22
+
23
+ # Error indicating the resource within the target server was not found
21
24
  class NotFound < UAAError; end
25
+
26
+ # Indicates a syntax error in a response from the UAA, e.g. missing required response field.
22
27
  class BadResponse < UAAError; end
28
+
29
+ # Indicates a token is malformed or expired
23
30
  class InvalidToken < UAAError; end
31
+
32
+ # Indicates an error from the http client stack
24
33
  class HTTPException < UAAError; end
34
+
35
+ # An application level error from the UAA which includes error info in the reply.
25
36
  class TargetError < UAAError
26
37
  attr_reader :info
27
38
  def initialize(error_info = {})
@@ -32,24 +43,34 @@ end
32
43
  # Utility accessors and methods for objects that want to access JSON web APIs.
33
44
  module Http
34
45
 
46
+ # Sets the current logger instance to recieve error messages
35
47
  def logger=(logr); @logger = logr end
48
+
49
+ # Returns the current logger or CF::UAA::Util.default_logger is none has been set.
36
50
  def logger ; @logger ||= Util.default_logger end
51
+
52
+ # Returns true if the current logger is set to +:trace+ level
37
53
  def trace? ; @logger && @logger.respond_to?(:trace?) && @logger.trace? end
38
54
 
39
- # sets handler for outgoing http requests. If not set, net/http is used. :yields: url, method, body, headers
55
+ # Sets handler for outgoing http requests. If not set, an internal cache of
56
+ # net/http connections is used.
57
+ # Arguments to handler are url, method, body, headers.
40
58
  def set_request_handler(&blk) @req_handler = blk end
41
59
 
60
+ # Returns a string for use in an http basic authentication header
42
61
  def self.basic_auth(name, password)
43
62
  "Basic " + Base64::strict_encode64("#{name}:#{password}")
44
63
  end
45
64
 
46
- def add_auth_json(auth, headers, jsonhdr = "content-type") # :nodoc:
65
+ private
66
+
67
+ def add_auth_json(auth, headers, jsonhdr = "content-type")
47
68
  headers["authorization"] = auth if auth
48
69
  headers.merge!(jsonhdr => "application/json")
49
70
  end
50
71
 
51
72
  def json_get(target, path = nil, authorization = nil, key_style = :none, headers = {})
52
- json_parse_reply(*http_get(target, path,
73
+ json_parse_reply(*http_get(target, path,
53
74
  add_auth_json(authorization, headers, "accept")), key_style)
54
75
  end
55
76
 
@@ -61,15 +82,11 @@ module Http
61
82
  http_put(target, path, Util.json(body), add_auth_json(authorization, headers))
62
83
  end
63
84
 
64
- def json_patch(target, path, body, authorization = nil, headers = {})
65
- http_patch(target, path, Util.json(body), add_auth_json(authorization, headers))
66
- end
67
-
68
85
  def json_parse_reply(status, body, headers, key_style = :none)
69
86
  unless [200, 201, 204, 400, 401, 403].include? status
70
87
  raise (status == 404 ? NotFound : BadResponse), "invalid status response: #{status}"
71
88
  end
72
- if body && !body.empty? && (status == 204 || headers.nil? ||
89
+ if body && !body.empty? && (status == 204 || headers.nil? ||
73
90
  headers["content-type"] !~ /application\/json/i)
74
91
  raise BadResponse, "received invalid response content or type"
75
92
  end
@@ -86,7 +103,6 @@ module Http
86
103
  def http_get(target, path = nil, headers = {}) request(target, :get, path, nil, headers) end
87
104
  def http_post(target, path, body, headers = {}) request(target, :post, path, body, headers) end
88
105
  def http_put(target, path, body, headers = {}) request(target, :put, path, body, headers) end
89
- def http_patch(target, path, body, headers = {}) request(target, :patch, path, body, headers) end
90
106
 
91
107
  def http_delete(target, path, authorization)
92
108
  status = request(target, :delete, path, nil, "authorization" => authorization)[0]
@@ -95,15 +111,13 @@ module Http
95
111
  end
96
112
  end
97
113
 
98
- private
99
-
100
114
  def request(target, method, path, body = nil, headers = {})
101
115
  headers["accept"] = headers["content-type"] if headers["content-type"] && !headers["accept"]
102
116
  url = "#{target}#{path}"
103
117
 
104
118
  logger.debug { "--->\nrequest: #{method} #{url}\n" +
105
119
  "headers: #{headers}\n#{'body: ' + Util.truncate(body.to_s, trace? ? 50000 : 50) if body}" }
106
- status, body, headers = @req_handler ? @req_handler.call(url, method, body, headers) :
120
+ status, body, headers = @req_handler ? @req_handler.call(url, method, body, headers) :
107
121
  net_http_request(url, method, body, headers)
108
122
  logger.debug { "<---\nresponse: #{status}\nheaders: #{headers}\n" +
109
123
  "#{'body: ' + Util.truncate(body.to_s, trace? ? 50000: 50) if body}" }
@@ -117,7 +131,7 @@ module Http
117
131
  end
118
132
 
119
133
  def net_http_request(url, method, body, headers)
120
- raise ArgumentError unless reqtype = {delete: Net::HTTP::Delete,
134
+ raise ArgumentError unless reqtype = {delete: Net::HTTP::Delete,
121
135
  get: Net::HTTP::Get, post: Net::HTTP::Post, put: Net::HTTP::Put}[method]
122
136
  headers["content-length"] = body.length if body
123
137
  uri = URI.parse(url)
data/lib/uaa/misc.rb CHANGED
@@ -11,30 +11,37 @@
11
11
  # subcomponent's license, as noted in the LICENSE file.
12
12
  #++
13
13
 
14
- # This class is for Web Client Apps (in the OAuth2 sense) that want
15
- # access to authenticated user information. Basically this class is
16
- # an OpenID Connect client.
17
-
18
14
  require 'uaa/http'
19
15
 
20
16
  module CF::UAA
21
17
 
22
- # everything is miscellaneous
23
- #
24
- # this class provides interfaces to UAA endpoints that are not in the context
25
- # of an overall class of operations, like "user accounts" or "tokens". It's
26
- # also for some apis like "change user password" or "change client secret" that
27
- # use different forms of authentication than other operations on those types
28
- # of resources.
18
+ # interfaces to UAA endpoints that are not in the context
19
+ # of an overall class of operations like SCIM resources or OAuth2 tokens.
29
20
  class Misc
30
21
 
31
22
  class << self
32
23
  include Http
33
24
  end
34
25
 
35
- def self.whoami(target, auth_header) json_get(target, "/userinfo?schema=openid", auth_header) end
36
- def self.varz(target, name, pwd) json_get(target, "/varz", Http.basic_auth(name, pwd)) end
26
+ # Returns a hash of information about the user authenticated by the token in
27
+ # the +auth_header+. It calls the +/userinfo+ endpoint and returns a hash of
28
+ # user information as specified by OpenID Connect.
29
+ # See: http://openid.net/connect/
30
+ # Specifically: http://openid.net/specs/openid-connect-standard-1_0.html#userinfo_ep
31
+ # and: http://openid.net/specs/openid-connect-messages-1_0.html#anchor9
32
+ def self.whoami(target, auth_header)
33
+ json_get(target, "/userinfo?schema=openid", auth_header)
34
+ end
35
+
36
+ # Returns a hash of various monitoring and status variables from the UAA.
37
+ # Authenticates to the UAA with basic authentication. Name and pwd
38
+ # must be configured in the UAA.
39
+ def self.varz(target, name, pwd)
40
+ json_get(target, "/varz", Http.basic_auth(name, pwd))
41
+ end
37
42
 
43
+ # returns a hash of basic information about the target server, including
44
+ # version number, commit ID, and links to API endpoints.
38
45
  def self.server(target)
39
46
  reply = json_get(target, '/login')
40
47
  return reply if reply && reply["prompts"]
@@ -45,8 +52,11 @@ class Misc
45
52
  json_get(target, "/token_key", (client_id && client_secret ? Http.basic_auth(client_id, client_secret) : nil))
46
53
  end
47
54
 
48
- # Returns hash of values from the Authorization Server that are associated
49
- # with the opaque token.
55
+ # Sends the token to the UAA to validate. Returns hash of values that are
56
+ # associated with the token. Authenticates with client_id and client_secret.
57
+ # If audience_ids are specified, raises AuthError token is not for this
58
+ # audience -- i.e. the token's 'aud' attribute does not contain one or more
59
+ # of the specified audience_ids.
50
60
  def self.decode_token(target, client_id, client_secret, token, token_type = "bearer", audience_ids = nil)
51
61
  reply = json_get(target, "/check_token?token_type=#{token_type}&token=#{token}",
52
62
  Http.basic_auth(client_id, client_secret))
@@ -57,6 +67,8 @@ class Misc
57
67
  reply
58
68
  end
59
69
 
70
+ # Returns a hash of information about the given password, including a
71
+ # strength score and an indication of what strength it required by the UAA.
60
72
  def self.password_strength(target, password)
61
73
  json_parse_reply(*request(target, :post, '/password/score', URI.encode_www_form("password" => password),
62
74
  "content-type" => "application/x-www-form-urlencoded", "accept" => "application/json"))
data/lib/uaa/scim.rb CHANGED
@@ -15,8 +15,11 @@ require 'uaa/http'
15
15
 
16
16
  module CF::UAA
17
17
 
18
- # This class is for apps that need to manage User Accounts, Groups, or OAuth Client Registrations.
19
- # It provides access to the SCIM endpoints on the UAA.
18
+ # This class is for apps that need to manage User Accounts, Groups, or OAuth
19
+ # Client Registrations. It provides access to the SCIM endpoints on the UAA.
20
+ # For more information about SCIM -- the IETF's System for Cross-domain
21
+ # Identity Management (formerly known as Simple Cloud Identity Management) --
22
+ # see http://www.simplecloud.info
20
23
  class Scim
21
24
 
22
25
  include Http
@@ -25,9 +28,9 @@ class Scim
25
28
 
26
29
  def force_attr(k)
27
30
  kd = k.to_s.downcase
28
- kc = {"username" => "userName", "familyname" => "familyName",
29
- "givenname" => "givenName", "middlename" => "middleName",
30
- "honorificprefix" => "honorificPrefix",
31
+ kc = {"username" => "userName", "familyname" => "familyName",
32
+ "givenname" => "givenName", "middlename" => "middleName",
33
+ "honorificprefix" => "honorificPrefix",
31
34
  "honorificsuffix" => "honorificSuffix", "displayname" => "displayName",
32
35
  "nickname" => "nickName", "profileurl" => "profileUrl",
33
36
  "streetaddress" => "streetAddress", "postalcode" => "postalCode",
@@ -62,19 +65,23 @@ class Scim
62
65
  ary[elem == :path ? 0 : 1]
63
66
  end
64
67
 
65
- def prep_request(type, info = nil)
66
- [type_info(type, :path), force_case(info)]
68
+ def prep_request(type, info = nil)
69
+ [type_info(type, :path), force_case(info)]
67
70
  end
68
71
 
69
72
  public
70
73
 
71
- # the auth_header parameter refers to a string that can be used in an
72
- # authorization header. For oauth with jwt tokens this would be something
73
- # like "bearer xxxx.xxxx.xxxx". The Token class returned by TokenIssuer
74
- # provides an auth_header method for this purpose.
74
+ # The +auth_header+ parameter refers to a string that can be used in an
75
+ # authorization header. For OAuth2 with JWT tokens this would be something
76
+ # like "bearer xxxx.xxxx.xxxx". The Token class provides
77
+ # CF::UAA::Token#auth_header for this purpose.
75
78
  def initialize(target, auth_header) @target, @auth_header = target, auth_header end
76
79
 
77
- # info is a hash structure converted to json and sent to the scim /Users endpoint
80
+ # creates a SCIM resource. For possible values for the +type+ parameter, and links
81
+ # to the schema of each type see #query
82
+ # info is a hash structure converted to json and sent to the scim endpoint
83
+ # A hash of the newly created object is returned, including its ID
84
+ # and meta data.
78
85
  def add(type, info)
79
86
  path, info = prep_request(type, info)
80
87
  reply = json_parse_reply(*json_post(@target, path, info, @auth_header), :down)
@@ -86,34 +93,45 @@ class Scim
86
93
  raise BadResponse, "no id returned by add request to #{@target}#{path}"
87
94
  end
88
95
 
89
- def delete(type, id)
96
+ # Deletes a SCIM resource identified by +id+. For possible values for type, see #query
97
+ def delete(type, id)
90
98
  path, _ = prep_request(type)
91
99
  http_delete @target, "#{path}/#{URI.encode(id)}", @auth_header
92
100
  end
93
101
 
94
- # info is a hash structure converted to json and sent to the scim /Users endpoint
102
+ # +info+ is a hash structure converted to json and sent to a scim endpoint
103
+ # For possible types, see #query
95
104
  def put(type, info)
96
105
  path, info = prep_request(type, info)
97
106
  ida = type == :client ? 'client_id' : 'id'
98
107
  raise ArgumentError, "scim info must include #{ida}" unless id = info[ida]
99
- hdrs = info && info["meta"] && info["meta"]["version"] ?
108
+ hdrs = info && info["meta"] && info["meta"]["version"] ?
100
109
  {'if-match' => info["meta"]["version"]} : {}
101
- reply = json_parse_reply(*json_put(@target, "#{path}/#{URI.encode(id)}",
110
+ reply = json_parse_reply(*json_put(@target, "#{path}/#{URI.encode(id)}",
102
111
  info, @auth_header, hdrs), :down)
103
112
 
104
113
  # hide client endpoints that are not scim compatible
105
114
  type == :client && !reply ? get(type, info["client_id"]): reply
106
115
  end
107
116
 
108
- # TODO: fix this when the UAA supports patch
109
- # info is a hash structure converted to json and sent to the scim /Users endpoint
110
- #def patch(path, id, info, attributes_to_delete = nil)
111
- # info = info.merge(meta: { attributes: Util.arglist(attributes_to_delete) }) if attributes_to_delete
112
- # json_parse_reply(*json_patch(@target, "#{path}/#{URI.encode(id)}", info, @auth_header))
113
- #end
114
-
115
- # supported query keys are: attributes, filter, startIndex, count
116
- # output hash keys are: resources, totalResults, itemsPerPage
117
+ # Queries for objects and returns a selected list of attributes for each
118
+ # a given filter. Possible values for +type+ and links to the schema of
119
+ # corresponding object type are:
120
+ # +:user+:: http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#user-resource
121
+ # :: http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#anchor8
122
+ # +:group+:: http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#group-resource
123
+ # :: http://www.simplecloud.info/specs/draft-scim-core-schema-01.html#anchor10
124
+ # +:client+::
125
+ # +:user_id+:: https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#converting-userids-to-names
126
+ #
127
+ # The +query+ hash may contain the following keys:
128
+ # attributes:: a comma or space separated list of attribute names to be
129
+ # returned for each object that matches the filter. If no attribute
130
+ # list is given, all attributes are returned.
131
+ # filter:: a filter to select which objects are returned. See
132
+ # http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources
133
+ # startIndex:: for paged output, start index of requested result set.
134
+ # count:: maximum number of results per reply
117
135
  def query(type, query = {})
118
136
  path, query = prep_request(type, query)
119
137
  query = query.reject {|k, v| v.nil? }
@@ -133,7 +151,10 @@ class Scim
133
151
  info
134
152
  end
135
153
 
136
- def get(type, id)
154
+ # Returns a hash of information about a specific object.
155
+ # [type] For possible values of type, see #add
156
+ # [id] the id attribute of the object assigned by the UAA
157
+ def get(type, id)
137
158
  path, _ = prep_request(type)
138
159
  info = json_get(@target, "#{path}/#{URI.encode(id)}", @auth_header, :down)
139
160
 
@@ -143,7 +164,7 @@ class Scim
143
164
  end
144
165
 
145
166
  # Collects all pages of entries from a query, returns array of results.
146
- # Type can be any scim resource type
167
+ # For descriptions of the +type+ and +query+ parameters, see #query.
147
168
  def all_pages(type, query = {})
148
169
  query = query.reject {|k, v| v.nil? }
149
170
  query["startindex"], info = 1, []
@@ -160,16 +181,16 @@ class Scim
160
181
  end
161
182
  end
162
183
 
163
- # Queries for objects by name. returns array of name/id hashes for each
164
- # name found.
184
+ # Queries for objects by name. Returns array of name/id hashes for each
185
+ # name found. For possible values of +type+, see #query
165
186
  def ids(type, *names)
166
187
  na = type_info(type, :name_attr)
167
188
  filter = names.each_with_object([]) { |n, o| o << "#{na} eq \"#{n}\""}
168
189
  all_pages(type, attributes: "id,#{na}", filter: filter.join(" or "))
169
190
  end
170
191
 
171
- # Convenience method to query for single object by name.
172
- # Returns its id. Raises error if not found.
192
+ # Convenience method to query for single object by name. Returns its id.
193
+ # Raises error if not found. For possible values of +type+, see #query
173
194
  def id(type, name)
174
195
  res = ids(type, name)
175
196
 
@@ -188,12 +209,26 @@ class Scim
188
209
  id
189
210
  end
190
211
 
212
+ # [For a user to change their own password] Token must contain "password.write" scope and the
213
+ # correct +old_password+ must be given.
214
+ # [For an admin to set a user's password] Token must contain "uaa.admin" scope.
215
+ #
216
+ # For more information see:
217
+ # https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-password-put-useridpassword
218
+ # or https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-Security.md#password-change
191
219
  def change_password(user_id, new_password, old_password = nil)
192
220
  password_request = {"password" => new_password}
193
221
  password_request["oldPassword"] = old_password if old_password
194
222
  json_parse_reply(*json_put(@target, "/Users/#{URI.encode(user_id)}/password", password_request, @auth_header))
195
223
  end
196
224
 
225
+ # [For a client to change its own secret] Token must contain "uaa.admin,client.secret" scope and the
226
+ # correct +old_secret+ must be given.
227
+ # [For an admin to set a client secret] Token must contain "uaa.admin" scope.
228
+ #
229
+ # For more information see:
230
+ # https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-APIs.rst#change-client-secret-put-oauthclientsclient_idsecret
231
+ # or https://github.com/cloudfoundry/uaa/blob/master/docs/UAA-Security.md#client-secret-mangagement
197
232
  def change_secret(client_id, new_secret, old_secret = nil)
198
233
  req = {"secret" => new_secret }
199
234
  req["oldSecret"] = old_secret if old_secret
@@ -19,20 +19,23 @@ module CF::UAA
19
19
  # This class is for OAuth Resource Servers.
20
20
  # Resource Servers get tokens and need to validate and decode them,
21
21
  # but they do not obtain them from the Authorization Server. This
22
- # class it for Resource Servers which accept Bearer JWT tokens. An
23
- # instance of this class can be used to decode and verify the contents
24
- # of a bearer token. The Authorization Server will have signed the
25
- # token and shared a secret key so that the Resource Server can verify
26
- # the signature. The Authorization Server may also have given the
27
- # Resource Server an id, in which case it must verify a matching value
28
- # is in the access token.
22
+ # class is for resource servers which accept bearer JWT tokens.
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
26
+ #
27
+ # An instance of this class can be used to decode and verify the contents
28
+ # of a bearer token. Methods of this class can validate token signatures
29
+ # with a secret or public key, and they can also enforce that the token
30
+ # is for a particular audience.
29
31
  class TokenCoder
30
32
 
31
33
  def self.init_digest(algo) # :nodoc:
32
34
  OpenSSL::Digest::Digest.new(algo.sub('HS', 'sha').sub('RS', 'sha'))
33
35
  end
34
36
 
35
- # takes a token_body (the middle section of the jwt) and returns a signed token
37
+ # Takes a +token_body+ (the middle section of the JWT) and returns a signed
38
+ # token string.
36
39
  def self.encode(token_body, skey, pkey = nil, algo = 'HS256')
37
40
  segments = [Util.json_encode64("typ" => "JWT", "alg" => algo)]
38
41
  segments << Util.json_encode64(token_body)
@@ -49,6 +52,11 @@ class TokenCoder
49
52
  segments.join('.')
50
53
  end
51
54
 
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
58
+ # corresponding key is used to verify the signature (if +verify+ is true).
59
+ # Returns a hash of the token contents or raises +DecodeError+.
52
60
  def self.decode(token, skey = nil, pkey = nil, verify = true)
53
61
  pkey = OpenSSL::PKey::RSA.new(pkey) unless pkey.nil? || pkey.is_a?(OpenSSL::PKey::PKey)
54
62
  segments = token.split('.')
@@ -71,18 +79,17 @@ class TokenCoder
71
79
  payload
72
80
  end
73
81
 
74
- # Create a new token en/decoder for a service that is associated with
82
+ # Creates a new token en/decoder for a service that is associated with
75
83
  # the the audience_ids, the symmetrical token validation key, and the
76
84
  # public and/or private keys. Parameters:
77
- # * audience_ids - an array or space separated strings and should
78
- # indicate values which indicate the token is intended for this service
79
- # instance. It will be compared with tokens as they are decoded to
80
- # ensure that the token was intended for this audience.
81
- # * skey - is used to sign and validate tokens using symetrical key
82
- # algoruthms
83
- # * pkey - pkey may be a string or File which includes public and
84
- # optionally private key data in PEM or DER formats. The private key
85
- # is used to sign tokens and the public key is used to validate tokens.
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.
86
93
  def initialize(audience_ids, skey, pkey = nil)
87
94
  @audience_ids, @skey, @pkey = Util.arglist(audience_ids), skey, pkey
88
95
  @pkey = OpenSSL::PKey::RSA.new(pkey) unless pkey.nil? || pkey.is_a?(OpenSSL::PKey::PKey)
@@ -100,9 +107,9 @@ class TokenCoder
100
107
  end
101
108
 
102
109
  # Returns hash of values decoded from the token contents. If the
103
- # token contains resource ids and they do not contain the id of the
104
- # caller there will be an AuthError. If the token has expired there
105
- # will also be an AuthError.
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.
106
113
  def decode(auth_header)
107
114
  unless auth_header && (tkn = auth_header.split).length == 2 && tkn[0] =~ /^bearer$/i
108
115
  raise DecodeError, "invalid authentication header: #{auth_header}"
@@ -11,51 +11,74 @@
11
11
  # subcomponent's license, as noted in the LICENSE file.
12
12
  #++
13
13
 
14
- # Web or Native Clients (in the OAuth2 sense) would use this class to get tokens
15
- # that they can use to get access to resources
16
-
17
- # Client Apps that want to get access on behalf of their users to
18
- # resource servers need to get tokens via authcode and implicit flows,
19
- # request scopes, etc., but they don't need to process tokens. This
20
- # class is for these use cases.
21
-
22
14
  require 'securerandom'
23
15
  require 'uaa/http'
24
16
 
25
17
  module CF::UAA
26
18
 
27
- # The Token class holds access and refresh tokens as well as token meta-data
28
- # such as token type and expiration time. The info hash MUST include
29
- # access_token, token_type and scope (if granted scope differs from requested
30
- # scope). It should include expires_in. It may include refresh_token, scope,
31
- # and other values from the auth server.
19
+ # The Token class is returned by various TokenIssuer methods. It holds access
20
+ # and refresh tokens as well as token meta-data such as token type and
21
+ # expiration time. See Token#info for contents.
32
22
  class Token
23
+
24
+ # Returns a hash of information about the current token. The info hash MUST include
25
+ # access_token, token_type and scope (if granted scope differs from requested
26
+ # scope). It should include expires_in. It may include refresh_token, scope,
27
+ # and other values from the auth server.
33
28
  attr_reader :info
34
- def initialize(info); @info = info end
29
+
30
+ def initialize(info) # :nodoc:
31
+ @info = info
32
+ end
33
+
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".
35
36
  def auth_header; "#{info['token_type']} #{info['access_token']}" end
36
37
  end
37
38
 
39
+ # Client Apps that want to get access to resource servers on behalf of their
40
+ # users need to get tokens via authcode and implicit flows,
41
+ # request scopes, etc., but they don't need to process tokens. This
42
+ # class is for these use cases.
43
+ #
44
+ # 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
38
46
  class TokenIssuer
39
47
 
40
48
  include Http
41
49
 
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.
42
61
  def initialize(target, client_id, client_secret = nil, token_target = nil)
43
62
  @target, @client_id, @client_secret = target, client_id, client_secret
44
63
  @token_target = token_target || target
45
64
  end
46
65
 
47
- # login prompts for use by app to collect credentials for implicit grant
66
+ # 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"]}
48
70
  def prompts
49
71
  reply = json_get @target, '/login'
50
72
  return reply['prompts'] if reply && reply['prompts']
51
73
  raise BadResponse, "No prompts in response from target #{@target}"
52
74
  end
53
75
 
54
- # gets an access token in a single call to the UAA with the client
55
- # credentials used for authentication. The credentials arg should
76
+ # Gets an access token in a single call to the UAA with the user
77
+ # credentials used for authentication. The +credentials+ should
56
78
  # be an object such as a hash that can be converted to a json
57
79
  # representation of the credential name/value pairs
58
- # as specified by the information retrieved by #prompts
80
+ # corresponding to the keys retrieved by #prompts.
81
+ # Returns a Token.
59
82
  def implicit_grant_with_creds(credentials, scope = nil)
60
83
  # this manufactured redirect_uri is a convention here, not part of OAuth2
61
84
  redir_uri = "https://uaa.cloudfoundry.com/redirect/#{@client_id}"
@@ -74,22 +97,43 @@ class TokenIssuer
74
97
  raise BadResponse, "bad location header in reply: #{e.message}"
75
98
  end
76
99
 
77
- # constructs a uri that the client is to return to the browser to direct
78
- # the user to the authorization server to get an authcode. The redirect_uri
100
+ # 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+
79
102
  # is embedded in the returned uri so the authorization server can redirect
80
103
  # the user back to the client app.
81
104
  def implicit_uri(redirect_uri, scope = nil)
82
105
  @target + authorize_path_args("token", redirect_uri, scope)
83
106
  end
84
107
 
85
- def implicit_grant(implicit_uri, callback_query)
108
+ # 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.
122
+ def implicit_grant(implicit_uri, callback_fragment)
86
123
  in_params = Util.decode_form_to_hash(URI.parse(implicit_uri).query)
87
124
  unless in_params['state'] && in_params['redirect_uri']
88
125
  raise ArgumentError, "redirect must happen before implicit grant"
89
126
  end
90
- parse_implicit_params callback_query, in_params['state']
127
+ parse_implicit_params callback_fragment, in_params['state']
91
128
  end
92
129
 
130
+ # A UAA extension to OAuth2 that allows a client to pre-authenticate a
131
+ # 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.
93
137
  def autologin_uri(redirect_uri, credentials, scope = nil)
94
138
  headers = {'content_type' => 'application/x-www-form-urlencoded', 'accept' => 'application/json',
95
139
  'authorization' => Http.basic_auth(@client_id, @client_secret) }
@@ -99,7 +143,7 @@ class TokenIssuer
99
143
  @target + authorize_path_args('code', redirect_uri, scope, SecureRandom.uuid, code: reply[:code])
100
144
  end
101
145
 
102
- # constructs a uri that the client is to return to the browser to direct
146
+ # Constructs a uri that the client is to return to the browser to direct
103
147
  # the user to the authorization server to get an authcode. The redirect_uri
104
148
  # is embedded in the returned uri so the authorization server can redirect
105
149
  # the user back to the client app.
@@ -107,6 +151,19 @@ class TokenIssuer
107
151
  @target + authorize_path_args('code', redirect_uri, scope)
108
152
  end
109
153
 
154
+ # Uses the instance client credentials in addition to +callback_query+
155
+ # 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.
110
167
  def authcode_grant(authcode_uri, callback_query)
111
168
  ac_params = Util.decode_form_to_hash(URI.parse(authcode_uri).query)
112
169
  unless ac_params['state'] && ac_params['redirect_uri']
@@ -122,14 +179,24 @@ class TokenIssuer
122
179
  request_token('grant_type' => 'authorization_code', 'code' => authcode, 'redirect_uri' => ac_params['redirect_uri'])
123
180
  end
124
181
 
182
+ # Uses the instance client credentials in addition to the +username+
183
+ # 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.
125
186
  def owner_password_grant(username, password, scope = nil)
126
187
  request_token('grant_type' => 'password', 'username' => username, 'password' => password, 'scope' => scope)
127
188
  end
128
189
 
190
+ # Uses the instance client credentials to get a token with a client
191
+ # credentials grant. See http://tools.ietf.org/html/rfc6749#section-4.4
192
+ # Returns a Token.
129
193
  def client_credentials_grant(scope = nil)
130
194
  request_token('grant_type' => 'client_credentials', 'scope' => scope)
131
195
  end
132
196
 
197
+ # Uses the instance client credentials and the given +refresh_token+ to get
198
+ # 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.
133
200
  def refresh_token_grant(refresh_token, scope = nil)
134
201
  request_token('grant_type' => 'refresh_token', 'refresh_token' => refresh_token, 'scope' => scope)
135
202
  end
data/lib/uaa/util.rb CHANGED
@@ -18,7 +18,7 @@ require 'uri'
18
18
 
19
19
  # :nodoc:
20
20
  module CF
21
- # Namespace for Cloudfoundry UAA
21
+ # Namespace for Cloudfoundry User Account and Authentication service Ruby APIs
22
22
  module UAA end
23
23
  end
24
24
 
@@ -30,30 +30,40 @@ end
30
30
 
31
31
  module CF::UAA
32
32
 
33
- # all CF::UAA exceptions are derived from UAAError
33
+ # Useful parent class. All CF::UAA exceptions are derived from this.
34
34
  class UAAError < RuntimeError; end
35
35
 
36
- # Authentication error
36
+ # Indicates an authentication error
37
37
  class AuthError < UAAError; end
38
38
 
39
- # error for decoding tokens, base64 encoding, or json
39
+ # Indicates an error occurred decoding a token, base64 decoding, or JSON
40
40
  class DecodeError < UAAError; end
41
41
 
42
- # low level helper functions useful to the UAA client APIs
42
+ # Low level helper functions useful to the UAA client APIs
43
43
  class Util
44
44
 
45
- # http headers and various protocol tags tend to contain '-' characters,
45
+ # HTTP headers and various protocol tags tend to contain '-' characters,
46
46
  # are intended to be case-insensitive, and often end up as keys in ruby
47
- # hashes. The :undash style converts to lowercase for at least
48
- # consistent case if not exactly case insensitive, and with '_' instead
49
- # of '-' for ruby convention. :todash reverses :undash (except for case).
50
- # :uncamel and :tocamel provide similar translations for camel-case keys.
47
+ # hashes. SCIM[http://www.simplecloud.info/] specifies that attribute
48
+ # names are case-insensitive and this code downcases such strings using
49
+ # this method.
50
+ #
51
+ # The various +styles+ convert +key+ as follows:
52
+ # [+:undash+] to lowercase, '-' to '_', and to a symbol
53
+ # [+:todash+] to string, '_' to '-'
54
+ # [+:uncamel+] uppercase to underscore-lowercase, to symbol
55
+ # [+:tocamel+] reverse of +uncamel+
56
+ # [+:tosym+] to symbol
57
+ # [+:tostr+] to string
58
+ # [+:down+] to lowercase
59
+ # [+:none+] leave the damn key alone
60
+ #
51
61
  # returns new key
52
62
  def self.hash_key(k, style)
53
63
  case style
54
64
  when :undash then k.to_s.downcase.tr('-', '_').to_sym
55
65
  when :todash then k.to_s.downcase.tr('_', '-')
56
- when :uncamel then k.to_s.downcase.gsub(/([A-Z])([^A-Z]*)/,'_\1\2').to_sym
66
+ when :uncamel then k.to_s.gsub(/([A-Z])([^A-Z]*)/,'_\1\2').downcase.to_sym
57
67
  when :tocamel then k.to_s.gsub(/(_[a-z])([^_]*)/) { $1[1].upcase + $2 }
58
68
  when :tosym then k.to_sym
59
69
  when :tostr then k.to_s
@@ -63,28 +73,28 @@ class Util
63
73
  end
64
74
  end
65
75
 
66
- # modifies obj in place changing any hash keys to style (see hash_key).
76
+ # Modifies obj in place changing any hash keys to style (see hash_key).
67
77
  # Recursively modifies subordinate hashes. Returns modified obj
68
78
  def self.hash_keys!(obj, style = :none)
69
79
  return obj if style == :none
70
80
  return obj.each {|o| hash_keys!(o, style)} if obj.is_a? Array
71
81
  return obj unless obj.is_a? Hash
72
82
  newkeys, nk = {}, nil
73
- obj.delete_if { |k, v|
83
+ obj.delete_if { |k, v|
74
84
  hash_keys!(v, style)
75
85
  newkeys[nk] = v unless (nk = hash_key(k, style)) == k
76
86
  nk != k
77
87
  }
78
88
  obj.merge!(newkeys)
79
89
  end
80
-
81
- # makes a new copy of obj with hash keys to style (see hash_key).
90
+
91
+ # Makes a new copy of obj with hash keys to style (see hash_key).
82
92
  # Recursively modifies subordinate hashes. Returns modified obj
83
93
  def self.hash_keys(obj, style = :none)
84
94
  return obj.collect {|o| hash_keys(o, style)} if obj.is_a? Array
85
95
  return obj unless obj.is_a? Hash
86
- obj.each_with_object({}) {|(k, v), h|
87
- h[hash_key(k, style)] = hash_keys(v, style)
96
+ obj.each_with_object({}) {|(k, v), h|
97
+ h[hash_key(k, style)] = hash_keys(v, style)
88
98
  }
89
99
  end
90
100
 
@@ -103,10 +113,22 @@ class Util
103
113
  raise ArgumentError, e.message
104
114
  end
105
115
 
116
+ # Converts +obj+ to JSON
106
117
  def self.json(obj) MultiJson.dump(obj) end
118
+
119
+ # Converts +obj+ to nicely formatted JSON
120
+ def self.json_pretty(obj) MultiJson.dump(obj, pretty: true) end
121
+
122
+ # Converts +obj+ to a URL-safe base 64 encoded string
107
123
  def self.json_encode64(obj = {}) encode64(json(obj)) end
124
+
125
+ # Converts +str+ from base64 encoding of a JSON string to a (returned) hash.
108
126
  def self.json_decode64(str) json_parse(decode64(str)) end
127
+
128
+ # encodes +obj+ as a URL-safe base 64 encoded string, with trailing padding removed.
109
129
  def self.encode64(obj) Base64::urlsafe_encode64(obj).gsub(/=*$/, '') end
130
+
131
+ # adds proper padding to a URL-safe base 64 encoded string, and then returns the decoded string.
110
132
  def self.decode64(str)
111
133
  return unless str
112
134
  pad = str.length % 4
@@ -116,20 +138,22 @@ class Util
116
138
  raise DecodeError, "invalid base64 encoding"
117
139
  end
118
140
 
141
+ # Parses a JSON string into the returned hash. For possible values of +style+
142
+ # see #hask_key
119
143
  def self.json_parse(str, style = :none)
120
144
  hash_keys!(MultiJson.load(str), style) if str && !str.empty?
121
145
  rescue MultiJson::DecodeError
122
146
  raise DecodeError, "json decoding error"
123
147
  end
124
148
 
125
- def self.truncate(obj, limit = 50)
149
+ def self.truncate(obj, limit = 50) # :nodoc:
126
150
  return obj.to_s if limit == 0
127
151
  limit = limit < 5 ? 1 : limit - 4
128
152
  str = obj.to_s[0..limit]
129
153
  str.length > limit ? str + '...': str
130
154
  end
131
155
 
132
- # many parameters in these classes can be given as arrays, or as a list of
156
+ # Many parameters in these classes can be given as arrays, or as a list of
133
157
  # arguments separated by spaces or commas. This method handles the possible
134
158
  # inputs and returns an array of arguments.
135
159
  def self.arglist(arg, default_arg = nil)
@@ -139,11 +163,12 @@ class Util
139
163
  arg.split(/[\s\,]+/).reject { |e| e.empty? }
140
164
  end
141
165
 
142
- # reverse of arglist, puts arrays of strings into a single, space-delimited string
166
+ # Reverse of arglist, puts arrays of strings into a single, space-delimited string
143
167
  def self.strlist(arg, delim = ' ')
144
168
  arg.respond_to?(:join) ? arg.join(delim) : arg.to_s if arg
145
169
  end
146
170
 
171
+ # Set the default logger used by the higher level classes.
147
172
  def self.default_logger(level = nil, sink = nil)
148
173
  if sink || !@default_logger
149
174
  @default_logger = Logger.new(sink || $stdout)
data/lib/uaa/version.rb CHANGED
@@ -13,6 +13,6 @@
13
13
 
14
14
  module CF
15
15
  module UAA
16
- VERSION = "1.3.0"
16
+ VERSION = "1.3.1"
17
17
  end
18
18
  end
data/spec/scim_spec.rb CHANGED
@@ -43,13 +43,13 @@ describe Scim do
43
43
  check_headers(headers, :json, :json)
44
44
  [200, '{"ID":"id12345"}', {"content-type" => "application/json"}]
45
45
  end
46
- result = subject.add(:user, hair: "brown", shoe_size: "large",
46
+ result = subject.add(:user, hair: "brown", shoe_size: "large",
47
47
  eye_color: ["blue", "green"], name: "fred")
48
48
  result["id"].should == "id12345"
49
49
  end
50
50
 
51
51
  it "replaces an object" do
52
- obj = {hair: "black", shoe_size: "medium", eye_color: ["hazel", "brown"],
52
+ obj = {hair: "black", shoe_size: "medium", eye_color: ["hazel", "brown"],
53
53
  name: "fredrick", meta: {version: 'v567'}, id: "id12345"}
54
54
  subject.set_request_handler do |url, method, body, headers|
55
55
  url.should == "#{@target}/Users/id12345"
@@ -78,7 +78,7 @@ describe Scim do
78
78
  url.should =~ %r{^#{@target}/Users\?attributes=id&startIndex=[12]$}
79
79
  method.should == :get
80
80
  check_headers(headers, nil, :json)
81
- reply = url =~ /1$/ ?
81
+ reply = url =~ /1$/ ?
82
82
  '{"TotalResults":2,"ItemsPerPage":1,"StartIndex":1,"RESOURCES":[{"id":"id12345"}]}' :
83
83
  '{"TotalResults":2,"ItemsPerPage":1,"StartIndex":2,"RESOURCES":[{"id":"id67890"}]}'
84
84
  [200, reply, {"content-type" => "application/json"}]
@@ -141,7 +141,7 @@ describe TokenIssuer do
141
141
  "expires_in=98765&scope=openid+logs.read&state=bad_state"
142
142
  [302, nil, {"content-type" => "application/json", "location" => location}]
143
143
  end
144
- expect {token = subject.implicit_grant_with_creds(username: "joe+admin",
144
+ expect {token = subject.implicit_grant_with_creds(username: "joe+admin",
145
145
  password: "?joe's%password$@ ")}.to raise_exception BadResponse
146
146
  end
147
147
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cf-uaa-lib
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.3.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2012-12-05 00:00:00.000000000 Z
16
+ date: 2012-12-08 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: multi_json
@@ -191,7 +191,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
191
191
  version: '0'
192
192
  segments:
193
193
  - 0
194
- hash: 2445597345483257892
194
+ hash: -3747087268217165493
195
195
  required_rubygems_version: !ruby/object:Gem::Requirement
196
196
  none: false
197
197
  requirements:
@@ -200,7 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
200
200
  version: '0'
201
201
  segments:
202
202
  - 0
203
- hash: 2445597345483257892
203
+ hash: -3747087268217165493
204
204
  requirements: []
205
205
  rubyforge_project: cf-uaa-lib
206
206
  rubygems_version: 1.8.21