1

this is something that has been bothering me for a while. I want to write a program so that I may automatically log into my PowerSchool portal, which will in the future potentially let me do things such as parse my schedule and grades. The first step to that is authenticating, which has become a problem for me.

import sys
import os
import requests
import lxml
import json
from bs4 import BeautifulSoup


def login(username, password):
    with requests.Session() as s:
        url = 'https://sisavrsb.ednet.ns.ca/guardian/home.html#sign-in-content'
        r = s.get(url)
        soup = BeautifulSoup(r.text, "lxml")
        token = soup.select_one("[name='pstoken']")['value']
        contextdata = soup.select_one("[name='contextData']")['value']
        headers = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Encoding': 'gzip, deflate, br',
            'Accept-Language': 'en-US,en;q=0.5',
            'Connection': 'keep-alive',
            #'Content-Length': '423',
            #'Content-Type': 'application/x-www-form-urlencoded',
            #'Cookie': 'JSESSIONID=0B1666C446234245CECC2983F1D6CA8A; PowerSchool_Cookie_K=2069644430.1.329063952.2221457792',
            'DNT': '1',
            #'Host': 'sisavrsb.ednet.ns.ca',
            'Referer': 'https://sisavrsb.ednet.ns.ca/public/',
            'Upgrade-Insecure-Requests': '1',
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0'
        }
        print(contextdata)
        data = json.dumps({
            'account': username,
            #'contextData': '30A7205567365DDB643E707E25B32D43578D70A04D9F407113CF640632082056',
            'contextData' : contextdata,
            'credentialType': 'User Id and Password Credential',
            #'dbpw': '61a2474517a2f79ae0da0781b9bdf57d',
            #'dbpw' : password,
            'pcasServerUrl': '\/',
            'pstoken': token,
            'pw': password,
            'returnUrl': '',
            'serviceName': 'PS Parent Portal',
            'serviceTicket': '',
            'translator_ldappassword': '',
            'translator_password': '',
            'translator_username': '',
            'translatorpw': ''

        })
        p = s.post(url, headers=headers, data=data, allow_redirects=True)
        soup = BeautifulSoup(p.text, "lxml")
        if p.status_code == 302:
            print('Success!')
        else:
            print('Authentication error', p.status_code)
            print('cookies', requests.utils.dict_from_cookiejar(s.cookies))
            print(p.history)
            print(p.headers)

def main():
    login('xxxxx', 'xxxxx')


if __name__ == '__main__':
    main()

At this point I've tried almost everything from Mechanize to (outdated) PowerSchool API. I've tried my best to replicate the headers and data, using requests.Session() so that the cookies work properly. After hours of fiddling with it, I finally got it so that p.history() is not blank. It now contains "<Response [302]>", which is extremely vague to me but better than nothing.

Here's my output

Authentication error 200
cookies {'JSESSIONID': 'B847F853CC373DC7EAA8800FA02EEC00', 'PowerSchool_Cookie_K': '2069644430.1.329063608.2225303936'}
[<Response [302]>]
{'Server': 'Apache-Coyote/1.1', 'Cache-control': 'no-store, no-cache, must-revalidate, post-check=0, check=0', 'Expires': 'Thu, 01 Dec 1994 16:00:00 GMT', 'Content-Type': 'text/html;charset=UTF-8', 'Content-Length': '8238', 'Date': 'Thu, 08 Feb 2018 01:01:05 GMT'}

I've left the website link so that you may test with POST requests and look at the headers and such. I'm out of ideas for how to resolve this, but I would really like to get this working. Obviously 302 is in the history which is a good sign for a POST code, but I still can't get past the login. If I did another requests.get() and printed the output it would be the login page again.

Mechanize (throws 500 internal server error):

import mechanize
import cookielib

br = mechanize.Browser()

cj = cookielib.LWPCookieJar()
br.set_cookiejar(cj)

br.set_handle_equiv(True)
br.set_handle_gzip(True)
br.set_handle_redirect(True)
br.set_handle_referer(True)
br.set_handle_robots(False)

br.set_handle_refresh(mechanize._http.HTTPRefreshProcessor(), max_time=1)

# Debugging
br.set_debug_http(True)
br.set_debug_redirects(True)
br.set_debug_responses(True)
br.set_handle_refresh(False)

# Fake User-Agent header
br.addheaders = [('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36')]
br.open('https://sisavrsb.ednet.ns.ca/public/home.html')

#
br.select_form(name='LoginForm')
br.form['account'] = 'xxxxx'
br.form['pw'] = 'xxxxxx'

br.method = 'POST'
response = br.submit()
print response.read()

EDIT: RoboBrowser gives me a 500 response too. Wondering if it's because of something I'm missing or simply a problem on their end.

Tim Dearborn
  • 1,178
  • 7
  • 18
  • This would require a fair deal of debugging. Have you considered using a more browser-like tool? I'm thinking about Selenium with a headless browser, for example. – Hubert Grzeskowiak Feb 08 '18 at 01:36
  • 1
    @HubertGrzeskowiak I've tried RoboBrowser which hasn't worked. –  Feb 08 '18 at 01:36
  • I'd also like to run this on my remote server, so it'd be preferable if it wasn't reliant on Firefox gecko drivers being installed like selenium is. –  Feb 08 '18 at 01:45

1 Answers1

2

Not sure if this will work for you or not, but I've done a similar thing recently.

All of the logic is done in the /admin/javascript/md5.js file (which seems to be a library that they modified and added their own functions to).

This is the python that I use in my own scripts

from bs4 import BeautifulSoup
import requests, base64, hashlib, hmac

POWERSCHOOL_BASE_URL = "https://powerschool.eips.ca/"

def initLoginPage(httpSession: requests.Session) -> [str, str]:
    response = httpSession.get(POWERSCHOOL_BASE_URL + "public/home.html")
    html_response = BeautifulSoup(response.content, "lxml")

    contextData = html_response.find('input', id='contextData').attrs['value']
    pstoken = html_response.find('input', attrs={'name': 'pstoken'}).attrs['value']

    return contextData, pstoken

def getPassword(contextData: str, password: str) -> str:
    return hmac.new(contextData.encode('UTF-8'), msg=base64.b64encode(hashlib.md5(password.encode('UTF-8')).digest()).strip(b'='), digestmod=hashlib.md5).hexdigest()

def login(httpSession: requests.Session, username: str, pw: str, pstoken: str) -> requests.Response:
    post_data = {
        'account': username,
        'pw': pw,
        'pstoken': pstoken,
    }
    return httpSession.post(POWERSCHOOL_BASE_URL + "guardian/home.html", data=post_data)

Side note: the entire PowerSchool system is really, really confusing and insecure (in case the monster of a password hashing one-liner wasn't enough).

sudoBash
  • 31
  • 1
  • 5