Package: touchstone-auth
touchstone-authRationaleMIT itself and MIT-adjacent organizations offer many useful web services through
a Single-Sign-On (SSO) service called Touchstone, with two-factor logins provided
by Duo. This is great, and allows easy access to many functionalities, but because
MIT does not use a commercial SSO provider (like Okta and others), there is limited
ability to access Touchstone-protected sites without a web browser.Entertouchstone-auth, a Python package powered mostly by therequestspackage! This lets user authenticate themselves programmatically. Cookies are cached,
meaning that re-authentication is only needed once cookies expire.InstallThis package is on Pip, so you can just:pip install touchstone-authAlternatively, you can get built wheels from theReleases tab on Github.N.B. if installing manually,requests_pkcs12must be version v1.10 (pip installhandles this automatically).QuickstartThe classTouchstoneSessionis simply arequests.Sessionthat performs the Touchstone
authentication flow before returning a working session to you, the authenticated user.It is easiest to use as a context manager. Because Touchstone authentication requires a client-side certificate, remember tonot hard-codeyour credentials!
The example here loads credentials from a json file calledcredentials.json:{
    "certfile": "some_client_credential.p12",
    "password": "horse-battery-staple-correct"
}Then, in your Python file, you can do the following:import json
from touchstone_auth import TouchstoneSession, CertificateAuth

with open('credentials.json') as cred_file:
    credentials = json.load(cred_file)

with TouchstoneSession(
    base_url='https://atlas.mit.edu',
    auth_type=CertificateAuth(credentials['certfile'], credentials['password']),
    cookiejar_filename='cookies.pickle') as s:

    response = s.get('https://atlas.mit.edu/atlas/Main.action')When you call this the first time, your Python script will hang on the 2FA step until
the second-factor (by default, Duo push) is accepted. Subsequent requests should not block until the 30-day
"remember me" period is exceeded.If this blocking behavior is undesired, you can set the argumentshould_block=Falsein theTouchstoneSessionconstructor. If a blocking 2FA push is required, the errorWouldBlockErrorwill instead be raised.Finally, there is averboseargument; settingverbose=Truewill output extra
information about how processing is proceeding.Alternate authenticationYou can use other authentication methods as well.Certificate as a byte arrayIf you have your certificate as a byte string instead of a filename, just pass the bytes as your certificate:with TouchstoneSession(
    base_url='...',
    auth_type=CertificateAuth(cert_bytes, cert_password),
    cookiejar_filename='cookies.pickle') as s:Username/passwordTo use your username and password (donothard code your credentials in your code!), pass
aUsernamePassAuthinstead:with TouchstoneSession(
    base_url='...',
    auth_type=UsernamePassAuth(kerb_account, kerb_password),
    cookiejar_filename='cookies.pickle') as s:Kerberos ticketsTo authenticate using Kerberos tickets, passKerberosAuth()as theauth_typeparameter toTouchstoneSession, as in:with TouchstoneSession(
    base_url='...',
    auth_type=KerberosAuth(),
    cookiejar_filename='cookies.pickle') as s:Complete ExamplesGet your latest paystub from ADP:import json
from touchstone_auth import TouchstoneSession, CertificateAuth

with open('credentials.json') as cred_file:
    credentials = json.load(cred_file)

with TouchstoneSession(
    base_url='https://myadp.mit.edu',
    auth_type=CertificateAuth(credentials['certfile'], credentials['password']),
    cookiejar_filename='cookies.pickle') as s:

    response = s.get('https://my.adp.com/myadp_prefix/v1_0/O/A/payStatements?adjustments=yes&numberoflastpaydates=160')
    response_json = json.loads(response.text)
    latest = response_json['payStatements'][0]
    print('Latest paystub ({}): ${} net, ${} gross'.format(
        latest['payDate'],
        latest['netPayAmount']['amountValue'],
        latest['grossPayAmount']['amountValue']))which returnsLatest paystub (2021-08-13): $XXXX.XX net, $YYYY.YY grosswhen run.Check your Covidpass building access status:import json
from touchstone_auth import TouchstoneSession, CertificateAuth

with open('credentials.json') as cred_file:
    credentials = json.load(cred_file)

with TouchstoneSession(
    base_url=r'https://atlas-auth.mit.edu/oauth2/authorize?identity_provider=Touchstone&redirect_uri=https://covidpass.mit.edu&response_type=TOKEN&client_id=2ao42ccnajj7jpqd7h059n7eoc&scope=covid19/impersonate covid19/user digital-id/search digital-id/user openid profile',
    auth_type=CertificateAuth(credentials['certfile'], credentials['password']),
    cookiejar_filename='cookies.pickle') as s:

    response = json.loads(s.get('https://api.mit.edu/pass-v1/pass/access_status').text)
    print('Current Covidpass status: {}'.format(response['status']))This returnsCurrent Covidpass status: access_grantedif you are in fact up to date on Covidpass.For the various "new Atlas" OAUTH2 applications, you need to find the relevant authorization URL to put as the base URL.How did I find the proper URL for Covidpass? By looking in your browser's Developer Tools, you can locate the last GET request prior to redirect toidp.mit.edu, then remove the extraneousstateparameter.Get the registration list for a class, using Kerberos authentication:from touchstone_auth import TouchstoneSession, KerberosAuth
from bs4 import BeautifulSoup

with TouchstoneSession(base_url='https://student.mit.edu/',
                       auth_type=KerberosAuth(),
                       cookiejar_filename='cookies.pickle') as s:
    payload = {'termcode': '2023FA', 'SUBJECT01': '6.1600'}
    headers = {'Referer': 'https://student.mit.edu/cgi-bin/sfprwlst_sel.sh'}
    r = s.post('https://student.mit.edu/cgi-bin/sfprwlst.sh', data=payload, headers=headers)
    print(BeautifulSoup(r.text, 'html.parser').pre.text)Selecting two-factor methodWith version 0.3.0, you can also select between phone-call and Duo Push two factor
authentication.touchstone-authdefaults to Duo Push if you do not select one.To switch between the two, pass an additionaltwofactor_authargument. For example,
to use the phone-call two factor method in the above example, additionally import
the TwofactorType enum and pass it to the session constructor:import json
from touchstone_auth import TouchstoneSession, CertificateAuth, TwofactorType

with open('credentials.json') as cred_file:
    credentials = json.load(cred_file)

with TouchstoneSession(
    base_url=r'https://atlas-auth.mit.edu/oauth2/authorize?identity_provider=Touchstone&redirect_uri=https://covidpass.mit.edu&response_type=TOKEN&client_id=2ao42ccnajj7jpqd7h059n7eoc&scope=covid19/impersonate covid19/user digital-id/search digital-id/user openid profile',
    auth_type=CertificateAuth(credentials['certfile'], credentials['password']),
    cookiejar_filename='cookies.pickle',
    twofactor_type=TwofactorType.PHONE_CALL) as s:

    response = json.loads(s.get('https://api.mit.edu/pass-v1/pass/access_status').text)
    print('Current Covidpass status: {}'.format(response['status']))Developer installIf you'd like to hack locally ontouchstone-auth, after cloning this repository:$ git clone https://github.com/meson800/touchstone-auth.git
$ cd gityou can create a local virtual environment, and installtouchstone-authin "development mode"$ python -m venv env
$ .\env\Scripts\activate    (on Windows)
$ source env/bin/activate   (on Mac/Linux)
$ pip install -e .After this 'local install', you can use and importtouchstone-authfreely without
having to re-install after each update.ChangelogSee theCHANGELOGfor detailed changes.## [0.6.0] - 2023-09-15
### Updated
- Duo updated to use a second POST instead of a redirect to load some params.
  Library updated to be compatible with this new approachLicenseThis is licensed by the MIT license. Use freely!