module Sandal

A library for creating and reading JSON Web Tokens (JWT), supporting JSON Web Signatures (JWS) and JSON Web Encryption (JWE).

Currently supports draft-07 of the JWT spec, and draft-10 of the JWS and JWE specs.

Constants

DEFAULT_OPTIONS

The default options for token handling.

ignore_exp

Whether to ignore the expiry date of the token. This setting is just to help get things working and should always be false in real apps!

ignore_nbf

Whether to ignore the not-before date of the token. This setting is just to help get things working and should always be false in real apps!

ignore_signature

Whether to ignore the signature of signed (JWS) tokens. This setting is just tohelp get things working and should always be false in real apps!

max_clock_skew

The maximum clock skew, in seconds, when validating times. If your server time is out of sync with the token server then this can be increased to take that into account. It probably shouldn’t be more than about 300.

signature_policy

The policy for requiring signatures in tokens. The possible values are:

  • :strict (default) - The innermost token must be signed. This is the recommended policy.

  • :none - No signature is required. This really isn’t recommended.

valid_iss

A list of valid token issuers, if validation of the issuer claim is required.

valid_aud

A list of valid audiences, if validation of the audience claim is required.

VERSION

The semantic version of the library.

Public Class Methods

decode_token(token, depth = 16) { |header, options| ... } click to toggle source

Decodes and validates a signed and/or encrypted JSON Web Token, recursing into any nested tokens, and returns the payload.

The block is called with the token header as the first parameter, and should return the appropriate signature or decryption method to either validate the signature or decrypt the token as applicable. When the tokens are nested, this block will be called once per token. It can optionally have a second options parameter which can be used to override the {DEFAULT_OPTIONS} on a per-token basis; options are not persisted between yields.

@param token [String] The encoded JSON Web Token. @param depth [Integer] The maximum depth of token nesting to decode to. @yieldparam header [Hash] The JWT header values. @yieldparam options [Hash] (Optional) A hash that can be used to override the default options. @yieldreturn [#valid? or decrypt] The signature validator if the token is signed, or the token decrypter if the

token is encrypted.

@return [Hash or String] The payload of the token as a Hash if it was JSON, otherwise as a String. @raise [Sandal::TokenError] The token is invalid or not supported.

# File lib/sandal.rb, line 162
def self.decode_token(token, depth = 16)
  parts = token.split(".")
  decoded_parts = decode_token_parts(parts)
  header = decoded_parts[0]

  options = DEFAULT_OPTIONS.clone
  decoder = yield header, options if block_given?

  if is_encrypted?(parts)
    payload = decoder.decrypt(parts)
    if header.has_key?("zip")
      unless header["zip"] == "DEF"
        raise Sandal::InvalidTokenError, "Invalid zip algorithm."
      end
      payload = Zlib::Inflate.inflate(payload)
    end
  else
    payload = decoded_parts[1]
    unless options[:ignore_signature]
      validate_signature(parts, decoded_parts[2], decoder) 
    end
  end

  if header.has_key?("cty") && header["cty"] =~ /\AJWT\Z/i
    if depth > 0
      if block_given?
        decode_token(payload, depth - 1, &Proc.new)
      else 
        decode_token(payload, depth - 1)
      end
    else
      payload
    end
  else
    if options[:signature_policy] == :strict && !is_signed?(parts)
      raise Sandal::UnsupportedTokenError, "The innermost token is not signed."
    end
    parse_and_validate(payload, options)
  end
end
default!(defaults) click to toggle source

Overrides the default options.

@param defaults [Hash] The options to override (see {DEFAULT_OPTIONS} for details). @return [Hash] The new default options.

# File lib/sandal.rb, line 75
def self.default!(defaults)
  DEFAULT_OPTIONS.merge!(defaults)
end
encode_token(payload, signer, header_fields = nil) click to toggle source

Creates a signed JSON Web Token.

@param payload [String or Hash] The payload of the token. Hashes will be encoded as JSON. @param signer [#name,#sign] The token signer, which may be nil for an unsigned token. @param header_fields [Hash] Header fields for the token (note: do not include “alg”). @return [String] A signed JSON Web Token.

# File lib/sandal.rb, line 109
def self.encode_token(payload, signer, header_fields = nil)
  signer ||= Sandal::Sig::NONE

  header = {}
  header["alg"] = signer.name
  header = header_fields.merge(header) if header_fields
  header = Sandal::Json.dump(header)

  payload = Sandal::Json.dump(payload) unless payload.is_a?(String)

  sec_input = [header, payload].map { |p| Sandal::Util.jwt_base64_encode(p) }.join(".")
  signature = signer.sign(sec_input)
  [sec_input, Sandal::Util.jwt_base64_encode(signature)].join(".")
end
encrypt_token(payload, encrypter, header_fields = nil) click to toggle source

Creates an encrypted JSON Web Token.

@param payload [String] The payload of the token. @param encrypter [#name,#alg,#encrypt] The token encrypter. @param header_fields [Hash] Header fields for the token (note: do not include “alg” or “enc”). @return [String] An encrypted JSON Web Token.

# File lib/sandal.rb, line 130
def self.encrypt_token(payload, encrypter, header_fields = nil)
  header = {}
  header["enc"] = encrypter.name
  header["alg"] = encrypter.alg.name
  header = header_fields.merge(header) if header_fields

  if header.has_key?("zip")
    unless header["zip"] == "DEF"
      raise ArgumentError, "Invalid zip algorithm."
    end
    payload = Zlib::Deflate.deflate(payload, Zlib::BEST_COMPRESSION)
  end 

  encrypter.encrypt(Sandal::Json.dump(header), payload)
end
is_encrypted?(token) click to toggle source

Checks whether a token is encrypted.

@param token [String or Array] The token, or token parts. @return [Boolean] true if the token is encrypted; otherwise false.

# File lib/sandal.rb, line 83
def self.is_encrypted?(token)
  if token.is_a?(String)
    token.count(".") == 4
  else
    token.count == 5
  end
end
is_signed?(token) click to toggle source

Checks whether a token is signed.

@param token [String or Array] The token, or token parts. @return [Boolean] true if the token is signed; otherwise false.

# File lib/sandal.rb, line 95
def self.is_signed?(token)
  if token.is_a?(String)
    !token.end_with?(".") && token.count(".") == 2
  else
    token.count == 3 && !token[2].nil? && !token[2].empty?
  end
end

Private Class Methods

decode_token_parts(parts) click to toggle source

Decodes the parts of a token.

# File lib/sandal.rb, line 215
def self.decode_token_parts(parts)
  parts = parts.map { |part| Sandal::Util.jwt_base64_decode(part) }
  parts[0] = Sandal::Json.load(parts[0])
  parts
rescue
  raise InvalidTokenError, "Invalid token encoding."
end
parse_and_validate(payload, options) click to toggle source

Parses the content of a token and validates the claims if is JSON claims.

# File lib/sandal.rb, line 224
def self.parse_and_validate(payload, options)
  claims = Sandal::Json.load(payload) rescue nil
  if claims
    claims.extend(Sandal::Claims).validate_claims(options)
  else
    payload
  end
end
validate_signature(parts, signature, validator) click to toggle source

Decodes and validates a signed JSON Web Token.

# File lib/sandal.rb, line 206
def self.validate_signature(parts, signature, validator)
  raise UnsupportedTokenError, "Unsupported signature method." if validator.nil?
  secured_input = parts.take(2).join(".")
  unless validator.valid?(signature, secured_input)
    raise InvalidTokenError, "Invalid signature."
  end
end