# frozen_string_literal: true require_relative "gs2_header" module Net class IMAP < Protocol module SASL # Abstract base class for the SASL mechanisms defined in # RFC7628[https://tools.ietf.org/html/rfc7628]: # * OAUTHBEARER[rdoc-ref:OAuthBearerAuthenticator] # (OAuthBearerAuthenticator) # * OAUTH10A class OAuthAuthenticator include GS2Header # Authorization identity: an identity to act as or on behalf of. # # If no explicit authorization identity is provided, it is usually # derived from the authentication identity. For the OAuth-based # mechanisms, the authentication identity is the identity established by # the OAuth credential. attr_reader :authzid # Hostname to which the client connected. attr_reader :host # Service port to which the client connected. attr_reader :port # HTTP method. (optional) attr_reader :mthd # HTTP path data. (optional) attr_reader :path # HTTP post data. (optional) attr_reader :post # The query string. (optional) attr_reader :qs # Stores the most recent server "challenge". When authentication fails, # this may hold information about the failure reason, as JSON. attr_reader :last_server_response # Creates an RFC7628[https://tools.ietf.org/html/rfc7628] OAuth # authenticator. # # === Options # # See child classes for required configuration parameter(s). The # following parameters are all optional, but protocols or servers may # add requirements for #authzid, #host, #port, or any other parameter. # # * #authzid ― Identity to act as or on behalf of. # * #host — Hostname to which the client connected. # * #port — Service port to which the client connected. # * #mthd — HTTP method # * #path — HTTP path data # * #post — HTTP post data # * #qs — HTTP query string # def initialize(authzid: nil, host: nil, port: nil, mthd: nil, path: nil, post: nil, qs: nil, **) @authzid = authzid @host = host @port = port @mthd = mthd @path = path @post = post @qs = qs @done = false end # The {RFC7628 §3.1}[https://www.rfc-editor.org/rfc/rfc7628#section-3.1] # formatted response. def initial_client_response kv_pairs = { host: host, port: port, mthd: mthd, path: path, post: post, qs: qs, auth: authorization, # authorization is implemented by subclasses }.compact [gs2_header, *kv_pairs.map {|kv| kv.join("=") }, "\1"].join("\1") end # Returns initial_client_response the first time, then "^A". def process(data) @last_server_response = data done? ? "\1" : initial_client_response ensure @done = true end # Returns true when the initial client response was sent. # # The authentication should not succeed unless this returns true, but it # does *not* indicate success. def done?; @done end # Value of the HTTP Authorization header # # Implemented by subclasses. def authorization; raise "must be implemented by subclass" end end # Authenticator for the "+OAUTHBEARER+" SASL mechanism, specified in # RFC7628[https://tools.ietf.org/html/rfc7628]. Authenticates using OAuth # 2.0 bearer tokens, as described in # RFC6750[https://tools.ietf.org/html/rfc6750]. Use via # Net::IMAP#authenticate. # # RFC6750[https://tools.ietf.org/html/rfc6750] requires Transport Layer # Security (TLS) to secure the protocol interaction between the client and # the resource server. TLS _MUST_ be used for +OAUTHBEARER+ to protect # the bearer token. class OAuthBearerAuthenticator < OAuthAuthenticator # An OAuth2 bearer token, generally the access token. attr_reader :oauth2_token # :call-seq: # new(oauth2_token, **options) -> authenticator # new(oauth2_token:, **options) -> authenticator # # Creates an Authenticator for the "+OAUTHBEARER+" SASL mechanism. # # Called by Net::IMAP#authenticate and similar methods on other clients. # # === Options # # Only +oauth2_token+ is required by the mechanism, however protocols # and servers may add requirements for #authzid, #host, #port, or any # other parameter. # # * #oauth2_token — An OAuth2 bearer token or access token. *Required.* # May be provided as either regular or keyword argument. # * #authzid ― Identity to act as or on behalf of. # * #host — Hostname to which the client connected. # * #port — Service port to which the client connected. # * See OAuthAuthenticator documentation for less common parameters. # def initialize(oauth2_token_arg = nil, oauth2_token: nil, **args, &blk) super(**args, &blk) # handles authzid, host, port, etc oauth2_token && oauth2_token_arg and raise ArgumentError, "conflicting values for oauth2_token" @oauth2_token = oauth2_token || oauth2_token_arg or raise ArgumentError, "missing oauth2_token" end # :call-seq: # initial_response? -> true # # +OAUTHBEARER+ sends an initial client response. def initial_response?; true end # Value of the HTTP Authorization header def authorization; "Bearer #{oauth2_token}" end end end end end