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 +17 -26
- data/lib/uaa/http.rb +27 -13
- data/lib/uaa/misc.rb +27 -15
- data/lib/uaa/scim.rb +66 -31
- data/lib/uaa/token_coder.rb +28 -21
- data/lib/uaa/token_issuer.rb +90 -23
- data/lib/uaa/util.rb +45 -20
- data/lib/uaa/version.rb +1 -1
- data/spec/scim_spec.rb +3 -3
- data/spec/token_issuer_spec.rb +1 -1
- metadata +4 -4
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
|
-
|
5
|
+
## Install from rubygems
|
6
6
|
|
7
|
-
|
7
|
+
$ gem install cf-uaa-lib
|
8
8
|
|
9
|
-
|
9
|
+
## Build from source
|
10
10
|
|
11
|
-
|
11
|
+
$ bundle install
|
12
|
+
$ gem build cf-uaa-lib.gemspec
|
13
|
+
$ gem install cf-uaa-lib<version>.gem
|
12
14
|
|
13
|
-
|
15
|
+
## Use the gem
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
29
|
+
$ bundle exec rake test
|
39
30
|
|
40
31
|
Run the tests and see a fancy coverage report:
|
41
32
|
|
42
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
-
|
36
|
-
|
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
|
49
|
-
# with the
|
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
|
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
|
-
#
|
72
|
-
# authorization header. For
|
73
|
-
# like "bearer xxxx.xxxx.xxxx". The Token class
|
74
|
-
#
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
#
|
109
|
-
#
|
110
|
-
#
|
111
|
-
#
|
112
|
-
#
|
113
|
-
#
|
114
|
-
|
115
|
-
#
|
116
|
-
#
|
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
|
-
|
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
|
-
#
|
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.
|
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
|
-
#
|
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
|
data/lib/uaa/token_coder.rb
CHANGED
@@ -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
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# token
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
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
|
104
|
-
#
|
105
|
-
#
|
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}"
|
data/lib/uaa/token_issuer.rb
CHANGED
@@ -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
|
28
|
-
#
|
29
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
55
|
-
# credentials used for authentication. The credentials
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
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
|
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
|
-
#
|
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
|
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
|
-
#
|
33
|
+
# Useful parent class. All CF::UAA exceptions are derived from this.
|
34
34
|
class UAAError < RuntimeError; end
|
35
35
|
|
36
|
-
#
|
36
|
+
# Indicates an authentication error
|
37
37
|
class AuthError < UAAError; end
|
38
38
|
|
39
|
-
# error
|
39
|
+
# Indicates an error occurred decoding a token, base64 decoding, or JSON
|
40
40
|
class DecodeError < UAAError; end
|
41
41
|
|
42
|
-
#
|
42
|
+
# Low level helper functions useful to the UAA client APIs
|
43
43
|
class Util
|
44
44
|
|
45
|
-
#
|
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.
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
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.
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
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"}]
|
data/spec/token_issuer_spec.rb
CHANGED
@@ -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.
|
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-
|
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:
|
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:
|
203
|
+
hash: -3747087268217165493
|
204
204
|
requirements: []
|
205
205
|
rubyforge_project: cf-uaa-lib
|
206
206
|
rubygems_version: 1.8.21
|