cf-uaa-lib 1.3.0 → 1.3.1

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/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