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