Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
zmarffy committed Jun 15, 2024
0 parents commit f32ad52
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 0 deletions.
15 changes: 15 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[flake8]
ignore = E203, E266, E501, W503, F403, F401
max-complexity = 18
select = B,C,E,F,W,T4,B9
exclude =
.venv
.svn
CVS
.bzr
.hg
.git
__pycache__
.tox
.eggs
*.egg
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.DS_Store
*.pyc
*.pyo
*.egg-info/
dist/
build/
.vscode/
__pycache__
.venv/
poetry.lock
test-report.xml
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Zeke Marffy

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# `ms-requests-session`

`ms-requests-session` is a tiny Python library that provides a way to login to your Microsoft account programatically in a `requests` `Session`. It is unknown why you would ever want to use this, but that's for you to determine. I just make the library.

## Usage

The usage is incredibly simple and straightforward. Just create a new `MSRequestsSession` object and pass it your creds and a user agent.

```python
from ms_requests_session import MSRequestsSession

session = MSRequestsSession("[email protected]", "password", "some user agent")
```

The returned object is a subclass of `requests.Session` and has you logged in to your Microsoft account.

## Known issues

- Rarely (it seems to occur for Microsoft accounts that have not been accessed for a very long time), a required key may be missing from an API response that is required to be passed to the next. This will prevent you from logging in using `ms-requests-session`. There may be a way further traverse the chain of requests needed to login, but I have found that logging in to that account manually once seems to get it back in shape to be used with `ms-requests-session`. If you figure anything out about combatting this, feel free to submit a PR
- Accounts protected by two-factor auth are not supported, and I have no plans to do so ever

## Special thanks

A huge thank you to [@Terrance](https://github.com/Terrance) and his [SkPy](https://github.com/Terrance/SkPy) library. He and his library were able to point me in the correct direction for understanding how tricky values such as `t` work.

The Chromium Dev Tools are your best friend when it comes to reversing services. Do not sleep on them.
3 changes: 3 additions & 0 deletions ms_requests_session/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .api import *

__version__ = "0.0.0"
121 changes: 121 additions & 0 deletions ms_requests_session/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import re

import requests
from bs4 import BeautifulSoup

POST_URL_RE = r"""urlPost: ?'([A-Za-z0-9:\?_\-\.&/=]+)"""
PPFT_URL_RE = r"""sFTTag: ?'.*value="(.*?)"/>"""
PPFT_URL_RE_2 = r"""sFT: ?'(.*?)'"""
PASSPORT_TEXT_RE = r"""sRandomBlob: ?'(.*?)'"""
PASSWORD_INCORRECT_TEXT_RE = r"""sErrTxt: ?'Your account or password is incorrect\."""
REQUIRED_LOGIN_POST_KEYS = {"pprid", "t", "NAP", "ANON"}
LIVE_URL = "https://login.live.com/"


class MSRequestsSessionLoginError(Exception):
pass


def _regex_search_and_get_first_group(pattern: str, string: str) -> str:
m = re.search(pattern, string)
if m:
return m.group(1)
else:
raise MSRequestsSessionLoginError(
f"Group 1 of {pattern} not found in input string"
)


class MSRequestsSession(requests.Session):
def __init__(self, email: str, password: str, user_agent: str) -> None:
"""A `requests.Session` object that logs you into your Microsoft account.
Args:
email (str): Microsoft account email.
password (str): Microsoft account password.
user_agent (str): The user agent to use to login.
"""
super().__init__()
self.headers.update({"User-Agent": user_agent}),
self._login(email, password)

def _login(self, email: str, password: str) -> None:
next_url = LIVE_URL
r = self.get(next_url)

# Retrieve the next URL specified by the login page
next_url = _regex_search_and_get_first_group(POST_URL_RE, r.text)
# Get the special PPFT value which is required for the next POST
ppft = _regex_search_and_get_first_group(PPFT_URL_RE, r.text)
# This is a thing for some reason. Attach some substring of the word "Passport" onto the... uh... passport
ppsx = _regex_search_and_get_first_group(PASSPORT_TEXT_RE, r.text)
data = {
"ps": 2,
"psRNGCDefaultType": None,
"psRNGCEntropy": None,
"psRNGCSLK": None,
"canary": None,
"ctx": None,
"hpgrequestid": None,
"PPFT": ppft,
"PPSX": ppsx,
"NewUser": 1,
"FoundMSAs": None,
"fspost": 0,
"i21": 0,
"CookieDisclosure": 0,
"IsFidoSupported": 1,
"isSignupPost": 0,
"isRecoveryAttemptPost": 0,
"i13": "1",
"i18": None,
"login": email,
"loginfmt": email,
"type": 11,
"LoginOptions": 3,
"lrt": None,
"lrtPartition": None,
"hisRegion": None,
"hisScaleUnit": None,
"passwd": password,
}
r = self.post(next_url, data=data, allow_redirects=True)
if re.search(PASSWORD_INCORRECT_TEXT_RE, r.text):
raise MSRequestsSessionLoginError("Incorrect creds")
next_url = _regex_search_and_get_first_group(POST_URL_RE, r.text)
ppft = _regex_search_and_get_first_group(PPFT_URL_RE_2, r.text)
data = {
"LoginOptions": 3,
"type": 28,
"ctx": None,
"hpgrequestid": None,
"PPFT": ppft,
"canary": None,
}
r = self.post(next_url, data=data, allow_redirects=True)
s = BeautifulSoup(r.text, "html.parser")
# POST to get the T value
next_url = s.find(id="fmHF")
if next_url is None:
raise MSRequestsSessionLoginError("fmHF key was missing from response")
next_url = next_url.get("action")
data = {}
# Collect the keys required for the post from the page source
for key in REQUIRED_LOGIN_POST_KEYS:
value = s.find(id=key)
if value is None:
raise MSRequestsSessionLoginError(
f"{key} value missing from response. Possibly try to manually login to your account, and then try again"
)
data[key] = value.get("value")

# Last POST to get your cookies!
self.post(
next_url,
data=data,
allow_redirects=True,
headers={
"Origin": LIVE_URL,
"Referer": LIVE_URL,
},
)
84 changes: 84 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
[tool.poetry]
name = "ms-requests-session"
version = "0.0.0"
description = "Provides a requests session logged in to your Microsoft account"
authors = ["Zeke Marffy <[email protected]>"]
packages = [{ include = "ms_requests_session" }]
readme = "README.md"
repository = "https://github.com/zmarffy/ms_requests_session"
homepage = "https://github.com/zmarffy/ms_requests_session"
license = "MIT"

[tool.poetry.dependencies]
python = "^3.8.1"
poetry = "^1.8.3"
requests = "^2.32.3"
beautifulsoup4 = "^4.12.3"

[tool.poetry.group.dev.dependencies]
black = "^23.3.0"
isort = "^5.12.0"
flake8 = "^6.0.0"
poethepoet = "^0.19.0"

[tool.poetry.group.test.dependencies]
pytest = "^7.3.1"

[build-system]
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
build-backend = "poetry_dynamic_versioning.backend"

[tool.black]
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| _build_files
)/
'''

[tool.isort]
profile = "black"
skip = [
".bzr",
".direnv",
".eggs",
".git",
".hg",
".mypy_cache",
".nox",
".pants.d",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
"_build_files",
]

[tool.poe.tasks]
clean = "rm -rf .pytest_cache dist ./**/__pycache__ test-report.xml"
_black = "black --diff . --check"
_isort = "isort . -c"
check-format = ["_black", "_isort"]
lint = "flake8"

[tool.poetry-dynamic-versioning]
enable = true
metadata = true
tagged-metadata = true
dirty = true
format-jinja = "{% if distance == 0 %}{{ base }}{% else %}{{ base }}+{{ distance }}.{{ commit }}{% endif %}"

0 comments on commit f32ad52

Please sign in to comment.