Skip to content

Instantly share code, notes, and snippets.

@ZipFile
Last active April 1, 2024 18:35
Show Gist options
  • Save ZipFile/c9ebedb224406f4f11845ab700124362 to your computer and use it in GitHub Desktop.
Save ZipFile/c9ebedb224406f4f11845ab700124362 to your computer and use it in GitHub Desktop.
Pixiv OAuth Flow

Retrieving Auth Token

  1. Run the command:

    python pixiv_auth.py login

    This will open the browser with Pixiv login page.

  2. Open dev console (F12) and switch to network tab.

  3. Enable persistent logging ("Preserve log").

  4. Type into the filter field: callback?

  5. Proceed with Pixiv login.

  6. After logging in you should see a blank page and request that looks like this: https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback?state=...&code=.... Copy value of the code param into the pixiv_auth.py's prompt and hit the Enter key.

If you did everything right and Pixiv did not change their auth flow, pair of auth_token and refresh_token should be displayed.

⚠️ code's lifetime is extremely short, so make sure to minimize delay between step 5 and 6. Otherwise, repeat everything starting step 1.

Refresh Tokens

python pixiv_auth.py refresh OLD_REFRESH_TOKEN
#!/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()
@dnslin
Copy link

dnslin commented Jul 24, 2022

I appreciate the author's login script and the detailed readme, but I have a question whether CLIENT_ID and CLIENT_SECRET can be obtained by themselves to prevent CLIENT_SECRET from failing and getting the token.

@Kevin-2106
Copy link

Traceback (most recent call last):
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\urllib3\connectionpool.py", line 703, in urlopen
httplib_response = self._make_request(
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\urllib3\connectionpool.py", line 386, in _make_request
self._validate_conn(conn)
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\urllib3\connectionpool.py", line 1042, in validate_conn
conn.connect()
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\urllib3\connection.py", line 414, in connect
self.sock = ssl_wrap_socket(
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\urllib3\util\ssl
.py", line 449, in ssl_wrap_socket
ssl_sock = ssl_wrap_socket_impl(
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\urllib3\util\ssl
.py", line 493, in _ssl_wrap_socket_impl
return ssl_context.wrap_socket(sock, server_hostname=server_hostname)
File "C:\Users\k2106\AppData\Local\Programs\Python\Python310\lib\ssl.py", line 512, in wrap_socket
return self.sslsocket_class._create(
File "C:\Users\k2106\AppData\Local\Programs\Python\Python310\lib\ssl.py", line 1070, in _create
self.do_handshake()
File "C:\Users\k2106\AppData\Local\Programs\Python\Python310\lib\ssl.py", line 1341, in do_handshake
self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\requests\adapters.py", line 489, in send
resp = conn.urlopen(
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
retries = retries.increment(
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\urllib3\util\retry.py", line 592, in increment
raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='oauth.secure.pixiv.net', port=443): Max retries exceeded with url: /auth/token (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)')))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "C:\Users\k2106\go-cqbot\HiBiAPI\pixiv_auth.py", line 158, in
main()
File "C:\Users\k2106\go-cqbot\HiBiAPI\pixiv_auth.py", line 154, in main
args.func(args)
File "C:\Users\k2106\go-cqbot\HiBiAPI\pixiv_auth.py", line 149, in
login_parser.set_defaults(func=lambda _: login())
File "C:\Users\k2106\go-cqbot\HiBiAPI\pixiv_auth.py", line 101, in login
response = requests.post(
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\requests\api.py", line 115, in post
return request("post", url, data=data, json=json, **kwargs)
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\requests\api.py", line 59, in request
return session.request(method=method, url=url, **kwargs)
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\requests\sessions.py", line 587, in request
resp = self.send(prep, **send_kwargs)
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\requests\sessions.py", line 701, in send
r = adapter.send(request, **kwargs)
File "C:\Users\k2106\go-cqbot\HiBiAPI\hibi\lib\site-packages\requests\adapters.py", line 563, in send
raise SSLError(e, request=request)
requests.exceptions.SSLError: HTTPSConnectionPool(host='oauth.secure.pixiv.net', port=443): Max retries exceeded with url: /auth/token (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)')))

I've set up a pixiv-nginx, which works as a Reverse Proxy, when done, I can access pixiv in my browser and get the code through F12, but when I run the script it raised the error above. How can I fix the problem?
我设置了pixiv-nginx,原理是对pixiv进行反代,设置完之后我可以再浏览器里访问pixiv并且从F12里找到code,但是我运行这个脚本的时候出现了如上报错。请问我如何解决这个问题?
(The meaning of the English and Chinese should be the same.)

@ZipFile
Copy link
Author

ZipFile commented Jul 25, 2022

@dnslin Client secret is not a secret at all in this kind of auth flow. You can extract it with debugger/traffic analyzer.

@Kevin-2106 I do not know how pixiv-nginx works, better ask the devs whether it proxies private apis used by andoid/ios app. If it does, just update LOGIN_URL and AUTH_TOKEN_URL to point to your reverse proxy. Or alternatively, use regular proxy (see discussion above).

@dnslin
Copy link

dnslin commented Jul 25, 2022

@dnslin这种类型的流程,可以在身份鉴别器中使用。

@Kevin-2106如果是这样,立即更新并指向您的pixiv-nginx代理代理。或者,使用代理代理(见)。LOGIN_URL``AUTH_TOKEN_URL

Thank you very much

@Kevin-2106
Copy link

@Kevin-2106 I do not know how pixiv-nginx works, better ask the devs whether it proxies private apis used by andoid/ios app. If it does, just update LOGIN_URL and AUTH_TOKEN_URL to point to your reverse proxy. Or alternatively, use regular proxy (see discussion above).

Solved. I've installed a certificate and edited Hosts to access pixiv.net through 127.0.0.1 , while requests can't verify the certificate and raised an SSL Error.
To solve it, just add "verify=False" to requests.post(), then the code should be:
解决了。我为了直接访问pixiv改了hosts,安装了证书,但是requests不能验证我的证书于是抛出了SSL Error,解决方法是在requests.post()中增加"verify=False"。改后代码如下。

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,
            "app-os-version": "14.6",
            "app-os": "ios",
        },
        verify=False,
        **REQUESTS_KWARGS
    )

Thanks anyway!

@EnderAvaritia
Copy link

i think i full the token truely
image
but it can't work
image

@GullfossLK
Copy link

I tried to refresh my refresh token but the same token was returned. How can I refresh it?
タイトルなし

@kaoshawn04
Copy link

Is this method still usable?

@SakiSakiSakiSakiSaki
Copy link

@GullfossLK It keeps returning the same refresh token for me as well. Not sure what to do now.

@Zaczero
Copy link

Zaczero commented Apr 1, 2024

Yes this method is still usable. About the refresh token staying the same: this is how OAuth2 protocol works, what is returned is the new access_token (that expires in 1 hour). To make authenticated requests you need access_token, refresh_token is used to get that access_token every hour and it should not change (it's a long-term key). I suppose this instruction is slightly confusing python pixiv_auth.py refresh OLD_REFRESH_TOKEN, should be python pixiv_auth.py access_token REFRESH_TOKEN or something along these lines.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment