Class: CF::UAA::TokenCoder

Inherits:
Object
  • Object
show all
Defined in:
lib/uaa/token_coder.rb

Overview

This class is for OAuth Resource Servers. Resource Servers get tokens and need to validate and decode them, but they do not obtain them from the Authorization Server. This class is for resource servers which accept bearer JWT tokens.

For more on JWT, see the JSON Web Token RFC here: http://tools.ietf.org/id/draft-ietf-oauth-json-web-token-05.html

An instance of this class can be used to decode and verify the contents of a bearer token. Methods of this class can validate token signatures with a secret or public key, and they can also enforce that the token is for a particular audience.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}, obsolete1 = nil, obsolete2 = nil) ⇒ TokenCoder

Note:

the TokenCoder instance must be configured with the appropriate key material to support particular algorithm families and operations – i.e. :pkey must include a private key in order to sign tokens with the RS algorithms.

Creates a new token en/decoder for a service that is associated with the the audience_ids, the symmetrical token validation key, and the public and/or private keys.

Parameters:

  • options (Hash) (defaults to: {})

    Supported options:

    • :audience_ids [Array<String>, String] – An array or space separated string of values which indicate the token is intended for this service instance. It will be compared with tokens as they are decoded to ensure that the token was intended for this audience.

    • :skey [String] – used to sign and validate tokens using symmetrical key algoruthms

    • :pkey [String, File, OpenSSL::PKey::PKey] – may be a String or File in PEM or DER formats. May include public and/or private key data. The private key is used to sign tokens and the public key is used to validate tokens.

    • :algorithm [String] – Sets default used for encoding. May be HS256, HS384, HS512, RS256, RS384, RS512, or none.

    • :verify [String] – Verifies signatures when decoding tokens. Defaults to true.

    • :accept_algorithms [String, Array<String>] – An Array or space separated string of values which list what algorthms are accepted for token signatures. Defaults to all possible values of :algorithm except ‘none’.



151
152
153
154
155
156
157
158
159
# File 'lib/uaa/token_coder.rb', line 151

def initialize(options = {}, obsolete1 = nil, obsolete2 = nil)
  unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
    # deprecated: def initialize(audience_ids, skey, pkey = nil)
    warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
    options = {:audience_ids => options }
    options[:skey], options[:pkey] = obsolete1, obsolete2
  end
  @options = self.class.normalize_options(options)
end

Class Method Details

.decode(token, options = {}, obsolete1 = nil, obsolete2 = nil) ⇒ Hash

Decodes a JWT token and optionally verifies the signature. Both a symmetrical key and a public key can be provided for signature verification. The JWT header indicates what signature algorithm was used and the corresponding key is used to verify the signature (if verify is true).

Parameters:

  • token (String)

    A JWT token as returned by encode

  • options (Hash) (defaults to: {})

    Supported options:

    • :audience_ids [Array<String>, String] – An array or space separated string of values which indicate the token is intended for this service instance. It will be compared with tokens as they are decoded to ensure that the token was intended for this audience.

    • :skey [String] – used to sign and validate tokens using symmetrical key algoruthms

    • :pkey [String, File, OpenSSL::PKey::PKey] – may be a String or File in PEM or DER formats. May include public and/or private key data. The private key is used to sign tokens and the public key is used to validate tokens.

    • :algorithm [String] – Sets default used for encoding. May be HS256, HS384, HS512, RS256, RS384, RS512, or none.

    • :verify [String] – Verifies signatures when decoding tokens. Defaults to true.

    • :accept_algorithms [String, Array<String>] – An Array or space separated string of values which list what algorthms are accepted for token signatures. Defaults to all possible values of :algorithm except ‘none’.

Returns:

  • (Hash)

    the token contents

Raises:



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/uaa/token_coder.rb', line 95

def self.decode(token, options = {}, obsolete1 = nil, obsolete2 = nil)
  unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
    # deprecated: def self.decode(token, skey = nil, pkey = nil, verify = true)
    warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
    options = {:skey => options }
    options[:pkey], options[:verify] = obsolete1, obsolete2
  end
  options = normalize_options(options)
  segments = token.split('.')
  raise InvalidTokenFormat, "Not enough or too many segments" unless [2,3].include? segments.length
  header_segment, payload_segment, crypto_segment = segments
  signing_input = [header_segment, payload_segment].join('.')
  header = Util.json_decode64(header_segment)
  payload = Util.json_decode64(payload_segment, (:sym if options[:symbolize_keys]))
  return payload unless options[:verify]
  raise SignatureNotAccepted, "Signature algorithm not accepted" unless
      options[:accept_algorithms].include?(algo = header["alg"])
  return payload if algo == 'none'
  signature = Util.decode64(crypto_segment)
  if ["HS256", "HS384", "HS512"].include?(algo)
    raise InvalidSignature, "Signature verification failed" unless
        options[:skey] && signature == OpenSSL::HMAC.digest(init_digest(algo), options[:skey], signing_input)
  elsif ["RS256", "RS384", "RS512"].include?(algo)
    raise InvalidSignature, "Signature verification failed" unless
        options[:pkey] && options[:pkey].verify(init_digest(algo), signature, signing_input)
  else
    raise SignatureNotSupported, "Algorithm not supported"
  end
  payload
end

.encode(token_body, options = {}, obsolete1 = nil, obsolete2 = nil) ⇒ String

Constructs a signed JWT.

Parameters:

  • token_body

    Contents of the token in any object that can be converted to JSON.

  • options (Hash) (defaults to: {})

    Supported options:

    • :audience_ids [Array<String>, String] – An array or space separated string of values which indicate the token is intended for this service instance. It will be compared with tokens as they are decoded to ensure that the token was intended for this audience.

    • :skey [String] – used to sign and validate tokens using symmetrical key algoruthms

    • :pkey [String, File, OpenSSL::PKey::PKey] – may be a String or File in PEM or DER formats. May include public and/or private key data. The private key is used to sign tokens and the public key is used to validate tokens.

    • :algorithm [String] – Sets default used for encoding. May be HS256, HS384, HS512, RS256, RS384, RS512, or none.

    • :verify [String] – Verifies signatures when decoding tokens. Defaults to true.

    • :accept_algorithms [String, Array<String>] – An Array or space separated string of values which list what algorthms are accepted for token signatures. Defaults to all possible values of :algorithm except ‘none’.

Returns:

  • (String)

    a signed JWT token string in the form “xxxx.xxxxx.xxxx”.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/uaa/token_coder.rb', line 64

def self.encode(token_body, options = {}, obsolete1 = nil, obsolete2 = nil)
  unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
    # deprecated: def self.encode(token_body, skey, pkey = nil, algo = 'HS256')
    warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
    options = {:skey => options }
    options[:pkey], options[:algorithm] = obsolete1, obsolete2
  end
  options = normalize_options(options)
  algo = options[:algorithm]
  segments = [Util.json_encode64("typ" => "JWT", "alg" => algo)]
  segments << Util.json_encode64(token_body)
  if ["HS256", "HS384", "HS512"].include?(algo)
    sig = OpenSSL::HMAC.digest(init_digest(algo), options[:skey], segments.join('.'))
  elsif ["RS256", "RS384", "RS512"].include?(algo)
    sig = options[:pkey].sign(init_digest(algo), segments.join('.'))
  elsif algo == "none"
    sig = ""
  else
    raise SignatureNotSupported, "unsupported signing method"
  end
  segments << Util.encode64(sig)
  segments.join('.')
end

.init_digest(algo) ⇒ Object



44
45
46
# File 'lib/uaa/token_coder.rb', line 44

def self.init_digest(algo) # @private
  OpenSSL::Digest::Digest.new(algo.sub('HS', 'sha').sub('RS', 'sha'))
end

.normalize_options(opts) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/uaa/token_coder.rb', line 48

def self.normalize_options(opts) # @private
  opts = opts.dup
  pk = opts[:pkey]
  opts[:pkey] = OpenSSL::PKey::RSA.new(pk) if pk && !pk.is_a?(OpenSSL::PKey::PKey)
  opts[:audience_ids] = Util.arglist(opts[:audience_ids])
  opts[:algorithm] = 'HS256' unless opts[:algorithm]
  opts[:verify] = true unless opts.key?(:verify)
  opts[:accept_algorithms] = Util.arglist(opts[:accept_algorithms],
      ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512"])
  opts
end

Instance Method Details

#decode(auth_header) ⇒ Hash

Returns hash of values decoded from the token contents. If the audience_ids were specified in the options to this instance (see #initialize) and the token does not contain one or more of those audience_ids, an AuthError will be raised. AuthError is raised if the token has expired.

Parameters:

  • auth_header (String)

    (see Scim.initialize#auth_header)

Returns:

  • (Hash)

    the token contents



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/uaa/token_coder.rb', line 178

def decode(auth_header)
  unless auth_header && (tkn = auth_header.split(' ')).length == 2 && tkn[0] =~ /^bearer$/i
    raise InvalidTokenFormat, "invalid authentication header: #{auth_header}"
  end
  reply = self.class.decode(tkn[1], @options)
  auds = Util.arglist(reply[:aud] || reply['aud'])
  if @options[:audience_ids] && (!auds || (auds & @options[:audience_ids]).empty?)
    raise InvalidAudience, "invalid audience: #{auds}"
  end
  exp = reply[:exp] || reply['exp']
  unless exp.is_a?(Integer) && exp > Time.now.to_i
    raise TokenExpired, "token expired"
  end
  reply
end

#encode(token_body = {}, algorithm = nil) ⇒ String

Encode a JWT token. Takes a hash of values to use as the token body. Returns a signed token in JWT format (header, body, signature).

Parameters:

  • algorithm (String) (defaults to: nil)

    – overrides default. See #initialize for possible values.

  • token_body (defaults to: {})

    Contents of the token in any object that can be converted to JSON.

Returns:

  • (String)

    a signed JWT token string in the form “xxxx.xxxxx.xxxx”.



166
167
168
169
170
# File 'lib/uaa/token_coder.rb', line 166

def encode(token_body = {}, algorithm = nil)
  token_body[:aud] = @options[:audience_ids] if @options[:audience_ids] && !token_body[:aud] && !token_body['aud']
  token_body[:exp] = Time.now.to_i + 7 * 24 * 60 * 60 unless token_body[:exp] || token_body['exp']
  self.class.encode(token_body, algorithm ? @options.merge(:algorithm => algorithm) : @options)
end