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