diff options
Diffstat (limited to 'pyscripts/pixiv_auth.py')
-rwxr-xr-x | pyscripts/pixiv_auth.py | 115 |
1 files changed, 115 insertions, 0 deletions
diff --git a/pyscripts/pixiv_auth.py b/pyscripts/pixiv_auth.py new file mode 100755 index 0000000..f7dc5d1 --- /dev/null +++ b/pyscripts/pixiv_auth.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python + +from argparse import ArgumentParser +from base64 import urlsafe_b64encode +from hashlib import sha256 +from pprint import pprint +from secrets import token_urlsafe +from sys import exit +from urllib.parse import urlencode +from webbrowser import open as open_url + +import requests + +# Latest app version can be found using GET /v1/application-info/android +USER_AGENT = "PixivAndroidApp/5.0.234 (Android 11; Pixel 5)" +REDIRECT_URI = "https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback" +LOGIN_URL = "https://app-api.pixiv.net/web/v1/login" +AUTH_TOKEN_URL = "https://oauth.secure.pixiv.net/auth/token" +CLIENT_ID = "MOBrBDS8blbauoSck0ZfDbtuzpyT" +CLIENT_SECRET = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj" + + +def s256(data): + """S256 transformation method.""" + + return urlsafe_b64encode(sha256(data).digest()).rstrip(b"=").decode("ascii") + + +def oauth_pkce(transform): + """Proof Key for Code Exchange by OAuth Public Clients (RFC7636).""" + + code_verifier = token_urlsafe(32) + code_challenge = transform(code_verifier.encode("ascii")) + + return code_verifier, code_challenge + + +def print_auth_token_response(response): + data = response.json() + + try: + access_token = data["access_token"] + refresh_token = data["refresh_token"] + except KeyError: + print("error:") + pprint(data) + exit(1) + + print("access_token:", access_token) + print("refresh_token:", refresh_token) + print("expires_in:", data.get("expires_in", 0)) + + +def login(): + code_verifier, code_challenge = oauth_pkce(s256) + login_params = { + "code_challenge": code_challenge, + "code_challenge_method": "S256", + "client": "pixiv-android", + } + + open_url(f"{LOGIN_URL}?{urlencode(login_params)}") + + try: + code = input("code: ").strip() + except (EOFError, KeyboardInterrupt): + return + + response = requests.post( + AUTH_TOKEN_URL, + data={ + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "code": code, + "code_verifier": code_verifier, + "grant_type": "authorization_code", + "include_policy": "true", + "redirect_uri": REDIRECT_URI, + }, + headers={"User-Agent": USER_AGENT}, + ) + + print_auth_token_response(response) + + +def refresh(refresh_token): + response = requests.post( + AUTH_TOKEN_URL, + data={ + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "refresh_token", + "include_policy": "true", + "refresh_token": refresh_token, + }, + headers={"User-Agent": USER_AGENT}, + ) + print_auth_token_response(response) + + +def main(): + parser = ArgumentParser() + subparsers = parser.add_subparsers() + parser.set_defaults(func=lambda _: parser.print_usage()) + login_parser = subparsers.add_parser("login") + login_parser.set_defaults(func=lambda _: login()) + refresh_parser = subparsers.add_parser("refresh") + refresh_parser.add_argument("refresh_token") + refresh_parser.set_defaults(func=lambda ns: refresh(ns.refresh_token)) + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() |