class Vines::Stream::SASL

Provides plain (username/password) and external (TLS certificate) SASL authentication to client and server streams.

Constants

EMPTY

Public Class Methods

new(stream) click to toggle source
# File lib/vines/stream/sasl.rb, line 11
def initialize(stream)
  @stream = stream
end

Public Instance Methods

external_auth(encoded) click to toggle source

Authenticate s2s streams, comparing their domain to their SSL certificate. Return true if the base64 encoded domain matches the TLS certificate presented earlier in stream negotiation. Raise a SaslError if authentication failed. xmpp.org/extensions/xep-0178.html#s2s

# File lib/vines/stream/sasl.rb, line 20
def external_auth(encoded)
  unless encoded == EMPTY
    authzid = decode64(encoded)
    matches_from = (authzid == @stream.remote_domain)
    raise SaslErrors::InvalidAuthzid unless matches_from
  end
  matches_from = @stream.cert_domain_matches?(@stream.remote_domain)
  matches_from or raise SaslErrors::NotAuthorized
end
plain_auth(encoded) click to toggle source

Authenticate c2s streams using a username and password. Return the authenticated User or raise a SaslError if authentication failed.

# File lib/vines/stream/sasl.rb, line 32
def plain_auth(encoded)
  jid, password = decode_credentials(encoded)
  user = authenticate(jid, password)
  user or raise SaslErrors::NotAuthorized
end

Private Instance Methods

authenticate(jid, password) click to toggle source

Storage backends should not raise errors, but if an unexpected error occurs during authentication, convert it to a temporary-auth-failure. Return the authenticated User or nil if authentication failed.

# File lib/vines/stream/sasl.rb, line 43
def authenticate(jid, password)
  log.info("Authenticating user: %s" % jid)
  @stream.storage.authenticate(jid, password).tap do |user|
    log.info("Authentication succeeded: %s" % user.jid) if user
  end
rescue => e
  log.error("Failed to authenticate: #{e.to_s}")
  raise SaslErrors::TemporaryAuthFailure
end
decode64(encoded) click to toggle source

Decode the base64 encoded string, raising an error for invalid data. tools.ietf.org/html/rfc6120#section-13.9.1

# File lib/vines/stream/sasl.rb, line 85
def decode64(encoded)
  Base64.strict_decode64(encoded)
rescue
  raise SaslErrors::IncorrectEncoding
end
decode_credentials(encoded) click to toggle source

Return the JID and password decoded from the base64 encoded SASL PLAIN credentials formatted as authzid0authcid0password. tools.ietf.org/html/rfc6120#section-6.3.8 tools.ietf.org/html/rfc4616

# File lib/vines/stream/sasl.rb, line 57
def decode_credentials(encoded)
  authzid, node, password = decode64(encoded).split("\x00")
  raise SaslErrors::NotAuthorized if node.nil? || node.empty? || password.nil? || password.empty?
  jid = JID.new(node, @stream.domain) rescue (raise SaslErrors::NotAuthorized)
  validate_authzid!(authzid, jid)
  [jid, password]
end
validate_authzid!(authzid, jid) click to toggle source

An optional SASL authzid allows a user to authenticate with one user name and password and then have their connection authorized as a different ID (the authzid). We don't support that, so raise an error if the authzid is provided and different than the authcid.

Most clients don't send an authzid at all because it's optional and not widely supported. However, Strophe and Blather send a bare JID, in compliance with RFC 6120, but Smack sends just the user name as the authzid. So, take care to handle non-compliant clients here. tools.ietf.org/html/rfc6120#section-6.3.8

# File lib/vines/stream/sasl.rb, line 75
def validate_authzid!(authzid, jid)
  return if authzid.nil? || authzid.empty?
  authzid.downcase!
  smack = authzid == jid.node
  compliant = authzid == jid.to_s
  raise SaslErrors::InvalidAuthzid unless compliant || smack
end