Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Radius: How to handle the request for passcode - {"ErrorCode":"ITATS542I","ErrorMessage":"Enter PASSCODE"} #3

Closed
BKFlister opened this issue Jan 11, 2023 · 14 comments

Comments

@BKFlister
Copy link

Hi,

Testing with Radius as Authentication Method.

After the username and password is verified, the Radius server sends a passcode for the user to enter.
Cyberark returns {"ErrorCode":"ITATS542I","ErrorMessage":"Enter PASSCODE"} to get passcode from the user.

Do you have an example on how to catch this,
and get (wait for) the passcode from the user and finish the authentication?

BR
Bjorn-Kare

@gleveille-lbp
Copy link
Collaborator

Hi,

Could you try to catch this exception and then reauthenticate with the passcode ?
https://cyberark-customers.force.com/s/question/0D52J00008otNrXSAU/rest-api-compatibility-with-radius-authentication (look at last comment).

@BKFlister
Copy link
Author

Hi,

I am struggling a bit on the re-authenticate with the Passcode.
I am not an experienced python-coder :-)

I am able to catch the exception, but after reauth it fails with invalid credential.

code:

import aiobastion
import asyncio

config = {'api_host': 'pvaa_host'}

async def main():
    vault = aiobastion.EPV(serialized=config)
    
    try:
        await vault.login(username, password, 'radius')
    except Exception as err:
        if 'ITATS542I' in str(err):
            passcode = input("Enter passcode: ")
            try:
                await vault.login(username, passcode, 'radius')
            except Exception as err:
                print(str(err))

    async with vault as epv:
        print(await epv.safe.list())

if __name__ == '__main__':
    asyncio.run(main())

I suspect that it might be that the session is closed/cleared because of the 500 error?
If I catch the exception inside cyberark.py and do a new session.post with a passcode, I am able to login and get the session token..
The Radius Authentication depends on the cookie CA22222

code added for test in cyberark.py, at line 118

...
    elif req.status == 500: #"ITATS542I"
        try:
            request_data = {"username": username, "password": 'passcode', "concurrentSession": True}
            async with session.post(url, json=request_data, **self.request_params) as req:
                tok = await req.text()
                print(tok)
        except Exception as err:
            print(str(err))
        else:
...

@BKFlister
Copy link
Author

Have tested some more, and I am able to get a successfull login when the Cookies are set when sending the second authentication for the passcode.

Store the Cookies from the first login with username and password.
Set Cookies CA22222 and mobilestate from first login before sending the second login with username and passCode.

BR
Bjorn-Kare

@BKFlister
Copy link
Author

Hi,
Here is a "working" Radius authentication with request for token.
I have added code to __login_cyberark in Cyberark.py
It is probably not the best code or method to handle this, but it works.
Might need a check/clearing of the temporary radiussession variable if the session is timed out.

Please see if it is something that can be added to the framework.

BR
Bjorn-Kare

__login_cyberark - line 105

async def __login_cyberark(self, username: str, password: str, auth_type: str) -> str:
        assert self.__token is None
        assert auth_type.upper() in ("CYBERARK", "WINDOWS", "LDAP", "RADIUS")
        url, head = self.get_url("API/Auth/" + auth_type + "/Logon")
        request_data = {"username": username, "password": password, "concurrentSession": True}
        try:
            if auth_type.upper() == "RADIUS":
                global radiussession
                try: 
                    radiussession
                except NameError:
                    session = self.get_session()
                else:
                    session = radiussession
            else:
                session = self.get_session()
            
            async with session.post(url, json=request_data, **self.request_params) as req:
                if req.status != 200:
                    if req.status == 403:
                        raise CyberarkException("Invalid credentials ! ")
                    elif req.status == 409:
                        raise CyberarkException("Password expired !")
                    else:
                        try:
                            raise CyberarkException(await req.text())
                        except Exception as err:
                            if 'ITATS542I' in str(err): # and req.status == 500
                                radiussession = session
                                raise CyberarkException(req.status, str(err))
                            else:
                                await self.close_session()
                                # Old session reused ?
                                raise CyberarkException(f"Unknown error, HTTP {str(req.status)}, ERR : {str(err)}")

                tok = await req.text()
                # Closing session because now we are connected and we need to update headers which can be done
                # only by recreating a new session (or passing the headers on each request)
                await session.close()
                radiussession = None
                return tok.replace('"', '')

            # Cleaning password after authentication
            self.__password = ""
        except (ConnectionError, TimeoutError):
            raise CyberarkException("Network problem connecting to PVWA")
        except Exception as err:
            raise CyberarkException(err)

Test code

import aiobastion
import asyncio

pvwa_host = 'pam-host'
username = 'user'
password = 'password'
authtype = 'Radius'
config = {'api_host': pvwa_host}

async def main():
    vault = aiobastion.EPV(serialized=config) 
    try:
        await vault.login(username, password, authtype)
    except Exception as err:
        if 'ITATS542I' in str(err):
            passcode = input("Enter passcode: ")
            try:
                await vault.login(username, passcode, authtype)
            except Exception as err:
                print('Error:',str(err))
        else:
            print('Error:',str(err))

    try:
        async with vault as epv:
            print('Token:',await epv.check_token())
            try:
                safes = await epv.safe.list(details=True)
                print(safes)
            except Exception as err:
                print('Error:',str(err))
    except Exception as err:
        print('Error:',str(err))

if __name__ == '__main__':
    asyncio.run(main())

@gleveille-lbp
Copy link
Collaborator

Hi Bjorn-Kare,

I prefer not handling "normal use cases" with exceptions if possible, it doesn't feel natural for me.

I have created a new branch called "radius", could you try with the cyberark.py in it ?

The idea is to call the login function with your pin code and your passcode. The login function the redirects you to the "__chall_response_login" function to achieve the authent with both requests.

await login(username, pincode, "RADIUS", passcode):

Try it and tell me what happens, if you have troubles and if you are stuck i will try to reproduce the "challenge response" login in my lab.

Greetings,
Gautier

@BKFlister
Copy link
Author

Hi Gautier,

Thank you.

You are of-course correct in not handling normal use cases with exceptions, that is what I would like . Just did a "how can I get it to work first", and then maybe try to improve the code afterwards. And I am not an experienced Python-coder :-)

The new radius branch works when I have the passcode up-front. I use my password as pincode.
await vault.login(username, password, authtype, passcode) -> OK
I have one test account that has a preset passcode, to not have to wait for the passode sent as a sms message, or push message to an app.
The Radius authentication also works when the Radius server sends an push-message where the user only needs to confirm directly in an app on a mobile phone, iphone/android. Cyberark then waits on the repsonse from the Radius server.

It seems that it will not work when the passcode is sent/requested as a challenge/response to the user.
I then get the "ITATS5421" response. If I have understood the updated use-case correctly.

Even if we are moving towards using apps with confirmation for the Radius, there will still be use cases where the user needs to wait for and enter the passcode as a challenge/response.

Thanks
BR
Bjorn-kare

@BKFlister
Copy link
Author

Hi Gautier,

Tested a bit more where I catch the 'ITATS542I' exception and then enter the passcode.
First login: await vault.login(username, password, authtype)
Second login: await vault.login(username, password, authtype, passcode)

For the simple test user that has a predefined passcode this also works, but the Cyberark logs it as an 'Authentication failure', twice. I can still list the user Safe, and get a "Event loop is closed" error.

For a user that has a challenge/response passcode it fails with "Invalid credentials !". the Cyberark logs "Radius authentication failure... (Radius server reply: Passcode is incorrect or has expired)."

A test with a fake passcode for the first login, and enter the challenge/response passcode inside __chall_response_login works.
__chall_response_login - line 116

                    if "ITATS542I" in response:
                    # Update request data
                        passcode = input("Enter passcode: ")
                        request_data = {"username": username, "password": passcode, "concurrentSession": True}

This will work for running the scripts manually, but will not work when calling the login where the user enters the credentials in a web-page.

BR
Bjorn-Kare

@gleveille-lbp
Copy link
Collaborator

Hi Bjorn-Kare,

Sorry for the late reply, I was having trouble handling sessions correctly with issues that happen when calling asynchronous functions in synchronous functions.

I've just published a new version (0.0.14) that raise a ChallengeResponseException at the first login.
The session is not closed at this moment.

So you can catch it and reauthenticate with the passcode.
When the token is received, the session is closed.

Could you please try it and tell me if it's ok ?

Best regards,
Gautier

@BKFlister
Copy link
Author

Hi Gautier,

Thank you, it seems to work as wanted.
Tested three Radius scenarios, predefined passcode, passcode sent as sms or an in-app message, and user acknowledge in-app. Have not tested with the good old physical token, but that should also work.
Perfect!

I get an error after login and getting the safes (see at end of post), but that might be the test-code I use.
I catch the "getting passcode" as an exception.
You might have a better example-code to test with?

I might have some other suggestions/requests expanding aiobastion, will get back to them later.

Thank you
BR
Bjorn-kare

Code used for testing:

import aiobastion
import asyncio
import getpass

pvwa_host = 'pam-host'
authtype = 'Radius'
username = 'username'
password = getpass.getpass()


config = {'api_host': pvwa_host}

async def main():
    vault = aiobastion.EPV(serialized=config) 
    try:
        await vault.login(username, password, authtype)
    except Exception as err:
        passcode = input("Enter passcode: ")
        await vault.login(username, passcode, authtype)

    async with vault as epv:
        safes = await epv.safe.list(details=True)
        print(safes)

if __name__ == '__main__':
    asyncio.run(main())

Error after running code:

Exception ignored in: <function _ProactorBasePipeTransport.__del__ at 0x0000022D3838F940>
Traceback (most recent call last):
  File "C:\Program Files\Python39\lib\asyncio\proactor_events.py", line 116, in __del__
    self.close()
  File "C:\Program Files\Python39\lib\asyncio\proactor_events.py", line 108, in close
    self._loop.call_soon(self._call_connection_lost, None)
  File "C:\Program Files\Python39\lib\asyncio\base_events.py", line 751, in call_soon
    self._check_closed()
  File "C:\Program Files\Python39\lib\asyncio\base_events.py", line 515, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

@gleveille-lbp
Copy link
Collaborator

gleveille-lbp commented Feb 1, 2023

Glad to hear that it's working as expected!

For your error, try this => encode/httpx#914 (comment)

if __name__ == '__main__':
    if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    asyncio.run(main())

I tried with your code and it's working fine on my end but i'm running python3.11 where this bug may have been fixed.

I am listening to your suggestions / requests, please do not hesitate to send me your criticism!

Best regards,
Gautier

@BKFlister
Copy link
Author

Thank you, that solved the error.

Thank you for getting the Radius to work.

Br
Bjorn-Kare

@BKFlister
Copy link
Author

BKFlister commented Mar 8, 2023

Hi,
I am testing some more with radius and Challenge Response, planning to use it in a small Quart web-app.
Radius authentication with Challenge Response works

To catch the Challenge Response I have to check for an empty exception error message?
If I understand correctly, the other messages that will be returned are Invalid credentials ! , Password expired !, Unable to get error message... or the text from req.text()

The only exception I seem to get is GetTokenException

Or have I misunderstood?
Is there a better method catching the Challenge Response?

Best Regards
Bjorn-Kare

@gleveille-lbp
Copy link
Collaborator

Hi,
You are true the "ChallengeResponseException" was not raised properly, I hopefully fixed it now.

Please have a try and tell me.
Regards,
Gautier

@BKFlister
Copy link
Author

Thank you,

I can use the "ChallengeResponseException" now.

BR
Bjorn-Kare

safepost pushed a commit that referenced this issue May 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants