# encoding: UTF-8 module Vines class Stream # Provides plain (username/password) and external (TLS certificate) SASL # authentication to client and server streams. class SASL include Vines::Log EMPTY = '='.freeze def initialize(stream) @stream = stream end # Authenticate server-to-server streams, comparing their domain to their # SSL certificate. # # http://xmpp.org/extensions/xep-0178.html#s2s # # encoded - The Base64 encoded remote domain name String sent by the # server stream. # # Returns true if the Base64 encoded domain matches the TLS certificate # presented earlier in stream negotiation. # # Raises a SaslError if authentication failed. 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 # Authenticate client-to-server streams using a username and password. # # encoded - The Base64 encoded jid and password String sent by the # client stream. # # Returns the authenticated User or raises SaslError if authentication failed. def plain_auth(encoded) jid, password = decode_credentials(encoded) user = authenticate(jid, password) user or raise SaslErrors::NotAuthorized end private # Storage backends should not raise errors, but if an unexpected error # occurs during authentication, convert it to a temporary-auth-failure. # # jid - The user's jid String. # password - The String password. # # Returns the authenticated User or nil if authentication failed. # # Raises TemoraryAuthFailure if the storage system failed. 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 # Return the JID and password decoded from the Base64 encoded SASL PLAIN # credentials formatted as authzid\0authcid\0password. # # http://tools.ietf.org/html/rfc6120#section-6.3.8 # http://tools.ietf.org/html/rfc4616 # # encoded - The Base64 encoded String from which to extract jid and password. # # Returns an Array of jid String and password String. 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 # 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. # # http://tools.ietf.org/html/rfc6120#section-6.3.8 # # authzid - The authzid String (may be nil). # jid - The username String. # # Returns nothing. 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 # Decode the Base64 encoded string, raising an error for invalid data. # # http://tools.ietf.org/html/rfc6120#section-13.9.1 # # encoded - The Base64 encoded String. # # Returns a UTF-8 String. def decode64(encoded) Base64.strict_decode64(encoded).tap do |decoded| decoded.force_encoding(Encoding::UTF_8) raise SaslErrors::IncorrectEncoding unless decoded.valid_encoding? end rescue raise SaslErrors::IncorrectEncoding end end end end