def get_response(self, request: _ProxyRequest): """Constructs a response packet to a given Radius request If the request is an 'initial' request (i.e. has no 'State' attribute, so it did not arrive in response to an AccessChallenge), then we simply call get_initial_response() If the request is a response to an AccessChallenge, then lookup the challenge state, perform some consistency checks (username, source ip), then we call self.get_challenge_response() Returns: packet.Packet """ try: challenge_state = self._find_challenge(request) if challenge_state: # sanity check on src ip expected_source = challenge_state.source if expected_source[0] != request.source[0]: raise BadStateError("Response to AccessChallenge from " "wrong source ip: %r" % request.source[0]) # sanity check on username if challenge_state.initial_request.username != request.username: raise BadStateError("Response to AccessChallenge with " "incorrect username: %r" % request.username) # everything checks out OK so far self.log_request( request, "Valid response to challenge issued at id %r" % challenge_state.initial_request.id, ) # clear challenge state (it has been consumed) self._cleanup_challenge(challenge_state) # build response ret = yield self.get_challenge_response( request, challenge_state.state) else: ret = yield self.get_initial_response(request) except BadStateError as e: msg = str(e) self.log_request(request, msg) log.auth_standard( msg=msg, username=request.username, auth_stage="Unknown", status=log.AUTH_REJECT, server_section=self.server_section_name, client_ip=request.client_ip, server_section_ikey=self.server_section_ikey, ) ret = yield self.create_reject_packet(request, "Unknown Challenge") defer.returnValue(ret)
def duo_auth(self, request, primary_res, factor=None, preauth_res=None): if not preauth_res: # Must call preauth even if the factor is known. response_packet, preauth_res = yield self.preauth( request, primary_res) if response_packet is not None: # E.g. enroll policy of deny or allow. defer.returnValue(response_packet) if factor is None: factor = util.factor_for_request(self.factors, preauth_res) if factor is None: msg = 'User has no Duo factors usable with this configuration' log.auth_standard(msg=msg, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_ERROR, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) self.log_request(request, msg) defer.returnValue(self.create_reject_packet(request, msg)) # Factor was either passed in or found above auto-push response_packet = yield super(DuoEAPRadiusServer, self).duo_auth( request, primary_res, factor, ) defer.returnValue(response_packet)
def add_incoming(self, msg: bytes): # Add a message to the EAP-TLS connection self.ssl_con.bio_write(msg) if not self.handshake: try: self.ssl_con.set_accept_state() self.ssl_con.do_handshake() except SSL.WantReadError: self.handshake = True try: # If there is a decrypted message ready, push it to inner eap decrypted_msg = self.ssl_con.recv(9999) # bytes # Inner eap lacks code, id, len # add them on so the packet is parsed the same header = (chr(2).encode() + msg[1:2] + struct.pack(">H", len(decrypted_msg) + 4)) # bytes if (decrypted_msg[0] == 2): # For some reason the MS-Auth-TLV has a header already header = b"" elif decrypted_msg[0] == 6: # GTC response gtc = yield self.gtc_received(self, decrypted_msg[1:], self.gtc_message) defer.returnValue(gtc) add_response = yield self.innerEAP.add_message(header + decrypted_msg) if add_response: error = yield self.errback(self, add_response.message) defer.returnValue(error) except SSL.WantReadError: # No decrypted message ready yet pass except SSL.Error as e: # Possibly retransmission or some other unknown error ssl_error = "SSL Error: {0}".format(e) log.auth_standard( msg=ssl_error, username=self.current_request.username, auth_stage=log.AUTH_UNKNOWN, status=log.AUTH_ERROR, client_ip=self.current_request.client_ip, server_section=self.server.server_section_name, server_section_ikey=self.server.server_section_ikey, ) error = yield self.errback(self, ssl_error) defer.returnValue(error) defer.returnValue(False)
def get_challenge_response(self, request, state): # Do not have access to factor - request.password is users cookie success = False self.log_request(request, 'Challenge Response: %r' % request.password) if state and 'primary_res' in state: radius_attrs = state['primary_res'].radius_attrs else: radius_attrs = {} auth_cookie = urllib.parse.unquote(self._get_authcookie(request)) try: finish_res = yield self.client.proxy_finish(auth_cookie) except duo_async.DuoAPIError as e: log.err(None, 'Duo proxy_finish call failed') response_packet = self.response_for_api_error( request, state['primary_res'], e, state['primary_res'].radius_attrs, ) defer.returnValue(response_packet) self.log_request(request, 'Authcookie validation result: %r' % finish_res) if (finish_res['valid_cookie'] and (finish_res['user'] == request.username)): success = True if success: log.auth_standard(msg='Valid login from iframe', username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_ALLOW, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, client_ip=request.client_ip) defer.returnValue( self.create_accept_packet( request, radius_attrs=radius_attrs, )) else: log.auth_standard(msg='Invalid login from iframe', username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_REJECT, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, client_ip=request.client_ip) defer.returnValue(self.create_reject_packet(request))
def response_for_api_error(self, request, primary_res, e, radius_reject_attrs=None): """Build and return a response packet for the given request, primary response, and api error. Assumes the primary response is success. Include the passed-in radius attributes when building a rejection packet. Returns a response ready to send to the client. """ if duo_async.should_server_fail_open(self.failmode, e.fail_open): msg = duo_async.get_fail_open_msg() self.log_request(request, msg) log.auth_standard( msg=msg, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_ALLOW, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, ) return self.create_accept_packet( request, msg, primary_res.radius_attrs, ) msg = duo_async.FAILMODE_SECURE_MSG self.log_request(request, msg) log.auth_standard( msg=msg, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_REJECT, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, ) if radius_reject_attrs is None: radius_reject_attrs = {} return self.create_reject_packet(request, msg, radius_attrs=radius_reject_attrs)
def get_challenge_response(self, request, state, session=None): """Overriden from abstract method in ChallengeResponseRadiusServer.""" if 'EAP-Message' in request.packet: request_message = b''.join(request.packet[79]) if session is None: session = state session.current_request = request add_response = yield session.add_message(request_message) if add_response: defer.returnValue(add_response) response_message = yield session.next_message() if isinstance(response_message, bytes): # Next EAP message response_packet = self.create_challenge(request, '', session) response_packet[79] = self.fragment(response_message) else: # response is a packet from kill_session or accept_session response_packet = response_message # EAP-Message (79) RADIUS packets MUST include a Message-Authenticator. response_packet.add_message_authenticator() defer.returnValue(response_packet) else: # If no EAP, treat it like a normal radius request try: password = request.password except Exception: password = None if not password: # Either PAP but blank or un-decryptable. (Not PAP? # Wrong shared secret?). Not EAP, either, or # EAP-Message would've been found. msg = 'Missing or improperly-formatted password' self.log_request(request, msg) log.auth_standard(msg=msg, username=request.username, auth_stage=log.AUTH_PRIMARY, status=log.AUTH_ERROR, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) defer.returnValue(self.create_reject_packet(request, msg)) else: result = yield self.auto_auth(request, password) defer.returnValue(result)
def duo_auth_only(self, request, factor): """ duo_auth and return its result. On failure, generate an appropriate response based on the configured failmode. """ try: client_ip = request.client_ip if not ip_util.is_valid_ip(client_ip): client_ip = None auth_res = yield self.client.auth(request.username, factor, client_ip) except duo_async.DuoAPIError as e: log.err(None, 'Duo auth call failed') if duo_async.should_server_fail_open(self.failmode, e.fail_open): msg = duo_async.get_fail_open_msg() self.log_request(request, msg) log.auth_standard(msg=msg, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_ALLOW, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) ret = {'result': duo_async.API_RESULT_ALLOW, 'status': msg} defer.returnValue(ret) else: msg = duo_async.FAILMODE_SECURE_MSG self.log_request(request, msg) log.auth_standard(msg=msg, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_ERROR, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) ret = {'result': duo_async.API_RESULT_DENY, 'status': msg} defer.returnValue(ret) if auth_res['result'] == duo_async.API_RESULT_ALLOW: log.auth_standard(msg=auth_res['status'], username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_ALLOW, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) else: log.auth_standard(msg=auth_res['status'], username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_REJECT, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) defer.returnValue(auth_res)
def primary_auth(self, request: _ProxyRequest, password: str = None): """ Perform primary authentication. request -- Radius Access Request message password -- Password included in the radius request, if any Returns: AuthResult: the RADIUS response to the primary authentication request. """ if self.pass_through_all: radius_attrs = dict(request.packet) else: radius_attrs = dict((attr, request[attr]) for attr in self._pass_through_attr_names + base.MS_CHAP2_REQUEST_ATTRS if attr in request) try: primary_res = yield self.primary_ator.authenticate( request.username, password, request.client_ip, radius_attrs) except AuthError as err: msg = "Error performing primary authentication: %s" % err self.log_request(request, msg) log.auth_standard( msg=msg, username=request.username, auth_stage=log.AUTH_PRIMARY, status=log.AUTH_ERROR, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, ) primary_res = AuthResult(False, msg) else: if not primary_res.success: primary_res_msg = "Primary credentials rejected - {0}".format( primary_res.msg) self.log_request(request, primary_res_msg) log.auth_standard( msg=primary_res_msg, username=request.username, auth_stage=log.AUTH_PRIMARY, status=log.AUTH_REJECT, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, ) else: log.auth_standard( msg="Primary authentication successful - {0}".format( primary_res.msg), username=request.username, auth_stage=log.AUTH_PRIMARY, status=log.AUTH_ALLOW, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, ) defer.returnValue(primary_res)
def duo_preauth(self, request): """ Get the user's duo auth status, and return it. On API error, this builds an appropriate failure return. request -- The user's RADIUS request Returns the preauth JSON result. """ try: if request.username in self.exempt_usernames: preauth_message = 'User exempted from 2FA' preauth_res = { 'result': duo_async.API_RESULT_ALLOW, 'status': preauth_message } else: preauth_res = yield self.client.preauth( request.username, request.client_ip, self.failmode) except duo_async.DuoAPIError as e: log.err(None, 'Duo preauth call failed') if duo_async.should_server_fail_open(self.failmode, e.fail_open): preauth_message = duo_async.get_fail_open_msg() preauth_res = { 'result': duo_async.API_RESULT_ALLOW, 'status': 'API call failed' } else: preauth_message = duo_async.FAILMODE_SECURE_MSG preauth_res = { 'result': duo_async.API_RESULT_DENY, 'status': 'API call failed' } else: if 'status' in preauth_res: preauth_message = 'Duo preauth returned \'%s\': \'%s\'' % ( preauth_res['result'], preauth_res['status']) self.log_request(request, preauth_message) else: preauth_message = 'Duo preauth returned \'%s\'' % preauth_res[ 'result'] self.log_request(request, preauth_message) if preauth_res['result'] == duo_async.API_RESULT_ALLOW: log.auth_standard(msg=preauth_message, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_ALLOW, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) elif preauth_res['result'] == duo_async.API_RESULT_DENY: log.auth_standard(msg=preauth_message, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_REJECT, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) elif preauth_res['result'] == duo_async.API_RESULT_ENROLL: log.auth_standard(msg=log.AUTH_ENROLL_MSG, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_REJECT, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) defer.returnValue(preauth_res)
def passcode_received(self, session, passcode: str, prompt): request = session.current_request if self.allow_concat: # In concat mode, attempt auth on first password result = yield self.auto_auth(request, passcode) if result.code == packet.AccessAccept: session.state = eap.EAP_SESSION_PEAP_ACCEPT else: session.state = eap.EAP_SESSION_DENY else: # If we didn't get any input, just resend the last prompt if passcode == '': return if not session.password: # otherwise, if this is the first credential we got # from the user, then do primary auth session.password = passcode session.primary_res = yield self.primary_auth( request, session.password) # if primary auth fails, send back a reject if not session.primary_res.success: session.state = eap.EAP_SESSION_DENY return # otherwise, do preauth and proceed according to the response resp, preauth_res = yield self.preauth(request, session.primary_res) session.state = eap.EAP_SESSION_PEAP_GTC if preauth_res is None: if resp.code == packet.AccessAccept: session.state = eap.EAP_SESSION_PEAP_ACCEPT else: session.state = eap.EAP_SESSION_DENY else: if preauth_res['result'] == duo_async.API_RESULT_ENROLL: session.enrolling = True gtc_msg = (preauth_res['status'] + ' then try again.').encode() session.innerEAP.gtc_message = gtc_msg session.state = eap.EAP_SESSION_PEAP_GTC elif preauth_res['result'] == duo_async.API_RESULT_AUTH: session.innerEAP.gtc_message = _format_short( preauth_res) session.gtc_message = session.innerEAP.gtc_message elif preauth_res['result'] == duo_async.API_RESULT_DENY: session.state = eap.EAP_SESSION_DENY elif preauth_res['result'] == duo_async.API_RESULT_ALLOW: session.state = eap.EAP_SESSION_PEAP_ACCEPT elif session.enrolling: # Always deny an auth attempt after displaying enrollment link session.state = eap.EAP_SESSION_DENY else: result = yield self.auto_auth(request, session.password, factor=passcode, primary_res=session.primary_res) if result.code == packet.AccessAccept: session.state = eap.EAP_SESSION_PEAP_ACCEPT elif passcode.rstrip('0123456789') == 'sms': # Let them enter an SMSed passcode if 'Reply-Message' in result: session.innerEAP.gtc_message = ( result['Reply-Message'][0] + '\n' + session.innerEAP.gtc_message).encode() else: # Reject result and no reply message means primary auth failed, no sms sent log.auth_standard( msg="Duo authentication failed", username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_REJECT, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) session.state = eap.EAP_SESSION_DENY else: log.auth_standard( msg="Unrecognized passcode", username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_REJECT, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) session.state = eap.EAP_SESSION_DENY
def add_message(self, msg: bytes): """Add a message to the EAP session. Returns False on success, returns errback on error, or gtc_received on receiving a token card response. """ code, identifier, length, eap_type = struct.unpack('>BBHB', msg[:5]) if not self.inner and self.id != 0 and identifier != self.id: # Silently discard response if ID does not match log.msg("Response ID %s does not match request ID %s" % (identifier, self.id)) defer.returnValue(False) if len(msg) != length: message = 'Malformed EAP packet: length ({0}) does not match actual length ({1})'.format( length, len(msg)) log.auth_standard( msg=message, username=self.current_request.username, auth_stage=log.AUTH_UNKNOWN, status=log.AUTH_ERROR, client_ip=self.current_request.client_ip, server_section=self.server.server_section_name, server_section_ikey=self.server.server_section_ikey) error = yield self.errback(self, message) defer.returnValue(error) if code == EAP_CODE_REQUEST: # EAP request, we are talking to RADIUS, this should not happen message = "EAP request received." log.auth_standard( msg=message, username=self.current_request.username, auth_stage=log.AUTH_UNKNOWN, status=log.AUTH_ERROR, client_ip=self.current_request.client_ip, server_section=self.server.server_section_name, server_section_ikey=self.server.server_section_ikey) error = yield self.errback(self, message) defer.returnValue(error) elif code == EAP_CODE_RESPONSE: # EAP response, we are talking to NetMotion self.id += 1 if eap_type == EAP_TYPE_IDENTITY: # Identity self.state = EAP_SESSION_REQUEST_GTC elif eap_type == EAP_TYPE_NAK: # Legacy Nak # Data element (element 5) will indicate the desired type desired_type = msg[5] if desired_type == EAP_TYPE_PEAP: # PEAP request self.state = EAP_SESSION_PEAP_REQUEST else: message = "Unknown Nak request %s." % desired_type log.auth_standard( msg=message, username=self.current_request.username, auth_stage=log.AUTH_UNKNOWN, status=log.AUTH_ERROR, client_ip=self.current_request.client_ip, server_section=self.server.server_section_name, server_section_ikey=self.server.server_section_ikey) error = yield self.errback(self, message) defer.returnValue(error) elif eap_type == EAP_TYPE_TLV: if msg[5:7] == MS_RESULT_HEADER and len( msg) == 11: # MS-Result-TLV if msg[-1] == 1 and self.state == EAP_SESSION_ACCEPT: # Success self.state = EAP_SESSION_SUCCESS else: # Failure self.state = EAP_SESSION_FAILURE else: # No support for cryptobinding or SoH TLV # At least exit with some relevant info message = "Unsupported TLV: {0!r}.".format(msg) log.auth_standard( msg=message, username=self.current_request.username, auth_stage=log.AUTH_UNKNOWN, status=log.AUTH_ERROR, client_ip=self.current_request.client_ip, server_section=self.server.server_section_name, server_section_ikey=self.server.server_section_ikey) error = yield self.errback(self, message) defer.returnValue(error) elif eap_type == EAP_TYPE_PEAP: # PEAP tls_flags = msg[5] if tls_flags & 0x80: # Length included self.tls_len = struct.unpack('>I', msg[6:10]) tls_msg: bytes = msg[10:] else: tls_msg = msg[6:] if self.state == EAP_SESSION_PEAP_MORE: self.tls_msg += tls_msg else: self.tls_msg = tls_msg if tls_flags & 0x40: # More fragments self.state = EAP_SESSION_PEAP_MORE else: self.state = EAP_SESSION_PEAP if self.tls_msg: if hasattr(self, 'ssl_con'): add_response = yield self.add_incoming(tls_msg) if add_response: # Errback or gtc callback occured, pass it through defer.returnValue(add_response) else: # We reset the EAP session but the peer is trying to continue self.state = EAP_SESSION_PEAP_REQUEST else: # Empty PEAP message, probably handshake over self.state = EAP_SESSION_PEAP_ID defer.returnValue(False)
def next_message(self) -> Iterator[bytes]: """Get the next outgoing message from the EAP session. Returns: str: contains message, if any pyrad.Packet: if authentication was successful. Result of calling this class' self.success() method. errback: if authentication was not successful """ if self.state == EAP_SESSION_START: # Request Identity packet = EAPPacket(EAP_CODE_REQUEST, self.id, EAP_TYPE_IDENTITY, chr(0).encode()) elif self.state == EAP_SESSION_REQUEST_PEAP: # Send a Nak PEAP request packet = EAPPacket(EAP_CODE_REQUEST, self.id, EAP_TYPE_NAK, EAP_TYPE_PEAP) elif self.state == EAP_SESSION_PEAP_REQUEST: # Start a TLS session and send TLS-start packet self.start_tls(self.pkey, self.certs, self.cipher_list, self.minimum_tls_version) packet = EAPPacket(EAP_CODE_REQUEST, self.id, EAP_TYPE_PEAP, b'', start=True) elif self.state == EAP_SESSION_PEAP: # Currently in a PEAP session # Check to see if the TLS connection has outgoing messages outgoing_msg = self.get_outgoing() if outgoing_msg: packet = EAPPacket(EAP_CODE_REQUEST, self.id, EAP_TYPE_PEAP, outgoing_msg) else: # The tls connection has nothing to send # Get a message from inner EAP and feed it to tls # Connection then retry to find something to send inner_msg = yield self.innerEAP.next_message() # Upon error or success, next_message will yield # an exception or a boolean respectively if isinstance(inner_msg, bytes): self.ssl_con.send(inner_msg) msg = yield self.next_message() defer.returnValue(msg) elif isinstance(inner_msg, Exception): log.auth_standard( msg=repr(inner_msg), username=self.current_request.username, auth_stage=log.AUTH_UNKNOWN, status=log.AUTH_ERROR, client_ip=self.current_request.client_ip, server_section=self.server.server_section_name, server_section_ikey=self.server.server_section_ikey) error = yield self.errback(self, repr(inner_msg)) defer.returnValue(error) elif inner_msg is True: success = yield self.success(self) defer.returnValue(success) else: peap_error_message = "Error in PEAP session" log.auth_standard( msg=peap_error_message, username=self.current_request.username, auth_stage=log.AUTH_UNKNOWN, status=log.AUTH_ERROR, client_ip=self.current_request.client_ip, server_section=self.server.server_section_name, server_section_ikey=self.server.server_section_ikey) error = self.errback(self, peap_error_message) defer.returnValue(error) elif self.state == EAP_SESSION_REQUEST_GTC: # Send a password (GTC) prompt packet = EAPPacket(EAP_CODE_REQUEST, self.id, EAP_TYPE_GTC, self.gtc_message) elif self.state == EAP_SESSION_PEAP_GTC: # Have inner EAP send GTC prompt self.innerEAP.state = EAP_SESSION_REQUEST_GTC self.state = EAP_SESSION_PEAP msg = yield self.next_message() defer.returnValue(msg) elif self.state == EAP_SESSION_PEAP_ID: # Have inner EAP request ID self.innerEAP.state = EAP_SESSION_START self.state = EAP_SESSION_PEAP msg = yield self.next_message() defer.returnValue(msg) elif self.state == EAP_SESSION_PEAP_ACCEPT: # Save the TLS key/randoms for generating MPPE keys self.mk = self.ssl_con.master_key() self.cr = self.ssl_con.client_random() self.sr = self.ssl_con.server_random() # Have inner EAP send accept message self.innerEAP.state = EAP_SESSION_ACCEPT self.state = EAP_SESSION_PEAP msg = yield self.next_message() defer.returnValue(msg) elif self.state == EAP_SESSION_PEAP_DENY: # Have inner EAP send reject message self.innerEAP.state = EAP_SESSION_DENY self.state = EAP_SESSION_PEAP msg = yield self.next_message() defer.returnValue(msg) elif self.state == EAP_SESSION_ACCEPT: # Send accept packet packet = EAPPacket(EAP_CODE_REQUEST, self.id, EAP_TYPE_TLV, True) elif self.state == EAP_SESSION_DENY: error = yield self.errback(self, 'Authentication denied.') defer.returnValue(error) elif self.state == EAP_SESSION_FAILURE: error = yield self.errback(self, 'EAP session failed.') defer.returnValue(error) elif self.state == EAP_SESSION_SUCCESS: success = yield self.success(self) defer.returnValue(success) if self.inner: defer.returnValue( packet.render()[4:]) # tunneled EAP skips code, id, length defer.returnValue(packet.render())
def _handle_request(self, datagram, host_port): host, port = host_port request_pkt = packet.AuthPacket(packet=datagram, dict=base.radius_dictionary()) request_pkt.source = (host, port) # make sure it's an AccessRequest if request_pkt.code != packet.AccessRequest: raise packet.PacketError("non-AccessRequest packet received") # lookup secret secret_for_host = self.secret_for_host(host) if secret_for_host is not None: request_pkt.secret = secret_for_host.encode() else: raise packet.PacketError("Unknown Client: %s" % host) # Validate Message-Authenticator, if any if request_pkt.message_authenticator: if not request_pkt.verify_message_authenticator(): raise packet.PacketError( "Invalid Message-Authenticator from {0}".format(host)) # check to see if it's a resend (i.e. client retry) old_request = self.requests.get((request_pkt.source, request_pkt.id)) if old_request: old_request_pkt = old_request.packet if _match_request_packets(request_pkt, old_request_pkt): # enough things (src, id, authenticator, username, # password) match that it's probably safe to assume # it's a resend. so send our result back (if we have # one); otherwise ignore self.log_request(old_request, "Received duplicate request") self._resend_response(old_request) defer.returnValue(old_request) else: self._cleanup_request(old_request) # create request state log.msg("Received new request id %r from %r" % (request_pkt.id, request_pkt.source)) request = _ProxyRequest(request_pkt, self.pw_codec, self.client_ip_attr) self.requests[(request.source, request.id)] = request try: # Check if password property can decrypt using the current secret. if self._can_decode_password(request): # authenticate the user request.response = yield self._get_response(request) else: self.log_request( request, "Cannot decode password using the configured" " radius_secret. Please ensure the client and" " Authentication Proxy use the same shared" " secret.", ) msg = "Cannot decode password" log.auth_standard( msg=msg, username=request.username, auth_stage=log.AUTH_UNKNOWN, status=log.AUTH_ERROR, server_section=self.server_section_name, client_ip=request.client_ip, server_section_ikey=self.server_section_ikey, ) response = self.create_reject_packet(request, msg) request.response = response.ReplyPacket() self._send_response(request) defer.returnValue(request) except Exception as e: # Something went wrong. Clean up the request and raise. self._cleanup_request(request) raise e
def get_initial_response(self, request): # make sure username, password were provided if request.username is None: msg = 'No username provided' self.log_request(request, msg) log.auth_standard(msg=msg, username=request.username, auth_stage="Unknown", status=log.AUTH_ERROR, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, client_ip=request.client_ip) defer.returnValue(self.create_reject_packet(request, msg)) self.log_request(request, 'login attempt for username %r' % request.username) if request.password is None: self.log_request( request, 'Only the PAP with a Shared Secret format is' ' supported. Is the system communicating with' ' the Authentication Proxy using CHAP or' ' MSCHAPv2 instead?') msg = 'No password provided' self.log_request(request, msg) log.auth_standard(msg=msg, username=request.username, auth_stage=log.AUTH_PRIMARY, status=log.AUTH_ERROR, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, client_ip=request.client_ip) defer.returnValue(self.create_reject_packet(request, msg)) # perform primary authentication primary_res = yield self.primary_auth(request, request.password) if not primary_res.success: defer.returnValue( self.create_reject_packet(request, primary_res.msg)) if request.username in self.exempt_usernames: msg = 'User exempted from 2FA' log.auth_standard(msg=msg, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_ALLOW, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, client_ip=request.client_ip) defer.returnValue( self.create_accept_packet( request, msg, radius_attrs=primary_res.radius_attrs)) # get a txid from duo service try: init_res = yield self.client.proxy_init(request.username) except duo_async.DuoAPIError as e: log.err(None, 'Duo proxy_init call failed') response_packet = self.response_for_api_error( request, primary_res, e, primary_res.radius_attrs, ) defer.returnValue(response_packet) # Note: MUST NOT YIELD between calling _create_challenge_id # and calling create_challenge() challenge_id = self._create_challenge_id() # build script injection with the txid params = { 'script_uri': self.script_uri, 'proxy_txid': init_res['proxy_txid'], 'api_host': self.client.host, 'state': challenge_id } challenge_msg = self.script_inject % params if len(challenge_msg) > 253: raise ValueError( 'response string is %d chars long, but cannot exceed 253 ' 'chars. If you specified a custom iframe_script_uri, you ' 'may need to shorten it by at least %d chars' % (len(challenge_msg), len(challenge_msg) - 253)) self.log_request(request, 'Sending authentication challenge packet') state = { 'primary_res': primary_res, } challenge_packet = self.create_challenge(request, challenge_msg, state=state, challenge_id=challenge_id) defer.returnValue(challenge_packet)
def preauth(self, request, primary_res, radius_reject_attrs=None): # perform preauth request if radius_reject_attrs is None: radius_reject_attrs = {} try: if request.username in self.exempt_usernames: preauth_res = { 'result': duo_async.API_RESULT_ALLOW, 'status': 'User exempted from 2FA' } else: preauth_res = yield self.client.preauth( request.username, request.client_ip, self.failmode) except duo_async.DuoAPIError as e: log.err(None, 'Duo preauth call failed') response = self.response_for_api_error(request, primary_res, e, radius_reject_attrs) defer.returnValue((response, None)) self.log_request( request, 'Got preauth result for: %r' % (preauth_res['result'], )) if preauth_res['result'] == duo_async.API_RESULT_ALLOW: msg = preauth_res['status'] log.auth_standard(msg=msg, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_ALLOW, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) response = self.create_accept_packet( request, msg, primary_res.radius_attrs, ) elif preauth_res['result'] == duo_async.API_RESULT_DENY: msg = preauth_res['status'] log.auth_standard(msg=msg, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_REJECT, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) response = self.create_reject_packet( request, msg, radius_attrs=radius_reject_attrs) elif preauth_res['result'] == duo_async.API_RESULT_ENROLL: msg = preauth_res['status'] log.auth_standard(msg=log.AUTH_ENROLL_MSG, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_REJECT, client_ip=request.client_ip, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey) response = self.create_reject_packet( request, msg, radius_attrs=radius_reject_attrs) else: # duo_async.API_RESULT_AUTH - return a sentinel value # saying we should continue response = None defer.returnValue((response, preauth_res))
def get_challenge_response(self, request: _ProxyRequest, state: Dict[str, Any]): """ Gets a challenge response for the given request Returns: packet.Packet """ # Do not have access to factor - request.password is users cookie success = False self.log_request(request, "Challenge Response: %r" % request.password) if state and "primary_res" in state: radius_attrs = state["primary_res"].radius_attrs else: radius_attrs = {} auth_cookie = urllib.parse.unquote(self._get_authcookie(request)) try: finish_res = yield self.client.proxy_finish(auth_cookie) except duo_async.DuoAPIError as e: log.failure("Duo proxy_finish call failed") response_packet = self.response_for_api_error( request, state["primary_res"], e, state["primary_res"].radius_attrs, ) defer.returnValue(response_packet) self.log_request(request, "Authcookie validation result: %r" % finish_res) if finish_res["valid_cookie"] and (finish_res["user"] == request.username): success = True if success: log.auth_standard( msg="Valid login from iframe", username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_ALLOW, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, client_ip=request.client_ip, ) defer.returnValue( self.create_accept_packet( request, radius_attrs=radius_attrs, )) else: log.auth_standard( msg="Invalid login from iframe", username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_REJECT, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, client_ip=request.client_ip, ) defer.returnValue(self.create_reject_packet(request))
def get_initial_response(self, request: _ProxyRequest): """ Gets a response for the first request over a connection from an appliance Returns: packet.Packet """ # make sure username, password were provided if request.username is None: msg = "No username provided" self.log_request(request, msg) log.auth_standard( msg=msg, username=request.username, auth_stage="Unknown", status=log.AUTH_ERROR, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, client_ip=request.client_ip, ) defer.returnValue(self.create_reject_packet(request, msg)) self.log_request(request, "login attempt for username %r" % request.username) if request.password is None: self.log_request( request, "Only the PAP with a Shared Secret format is" " supported. Is the system communicating with" " the Authentication Proxy using CHAP or" " MSCHAPv2 instead?", ) msg = "No password provided" self.log_request(request, msg) log.auth_standard( msg=msg, username=request.username, auth_stage=log.AUTH_PRIMARY, status=log.AUTH_ERROR, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, client_ip=request.client_ip, ) defer.returnValue(self.create_reject_packet(request, msg)) # perform primary authentication primary_res = yield self.primary_auth(request, request.password) if not primary_res.success: defer.returnValue( self.create_reject_packet(request, primary_res.msg)) if request.username in self.exempt_usernames: msg = "User exempted from 2FA" log.auth_standard( msg=msg, username=request.username, auth_stage=log.AUTH_SECONDARY, status=log.AUTH_ALLOW, server_section=self.server_section_name, server_section_ikey=self.server_section_ikey, client_ip=request.client_ip, ) defer.returnValue( self.create_accept_packet( request, msg, radius_attrs=primary_res.radius_attrs)) # get a txid from duo service try: init_res = yield self.client.proxy_init(request.username) except duo_async.DuoAPIError as e: log.failure("Duo proxy_init call failed") response_packet = self.response_for_api_error( request, primary_res, e, primary_res.radius_attrs, ) defer.returnValue(response_packet) # Note: MUST NOT YIELD between calling _create_challenge_id # and calling create_challenge() challenge_id = self._create_challenge_id() # build script injection with the txid params = { "script_uri": self.script_uri, "proxy_txid": init_res["proxy_txid"], "api_host": self.client.host, "state": challenge_id, } challenge_msg = self.script_inject % params if len(challenge_msg) > 253: raise ValueError( "response string is %d chars long, but cannot exceed 253 " "chars. If you specified a custom iframe_script_uri, you " "may need to shorten it by at least %d chars" % (len(challenge_msg), len(challenge_msg) - 253)) self.log_request(request, "Sending authentication challenge packet") state = { "primary_res": primary_res, } challenge_packet = self.create_challenge(request, challenge_msg, state=state, challenge_id=challenge_id) defer.returnValue(challenge_packet)