Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*.env
|
||||||
|
smn-oauth.json
|
||||||
|
venv
|
||||||
21
close_page.html
Normal file
21
close_page.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Close Tab</title>
|
||||||
|
<script>
|
||||||
|
function closeTab() {
|
||||||
|
// Check if the window was opened by JavaScript
|
||||||
|
if (window.close) {
|
||||||
|
window.close();
|
||||||
|
} else {
|
||||||
|
alert("This tab cannot be closed automatically. Please close it manually.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onload="closeTab()">
|
||||||
|
<h1>The tab will now close.</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
173
get-balance.py
Executable file
173
get-balance.py
Executable file
@@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from typing import Final, Optional, cast
|
||||||
|
from uuid import uuid4
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pdb
|
||||||
|
import requests
|
||||||
|
import socketserver
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
# modifiable global variable due to library interface constraint
|
||||||
|
TOKEN: Optional[dict] = None
|
||||||
|
|
||||||
|
STATE: Final[str] = str(uuid4())
|
||||||
|
HOST: Final[str] = os.environ['SB1_HOST']
|
||||||
|
PORT: Final[int] = int(os.environ['SB1_PORT'])
|
||||||
|
CLIENT_ID: Final[str] = os.environ['SB1_CLIENT_ID']
|
||||||
|
CLIENT_SECRET: Final[str] = os.environ['SB1_CLIENT_SECRET']
|
||||||
|
FIN_INST: Final[str] = os.environ['SB1_FIN_INST']
|
||||||
|
REDIRECT_URI: Final[str] = os.environ['SB1_REDIRECT_URI']
|
||||||
|
|
||||||
|
def oauth_token(state: str, code: str, grant_type: str) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Gets a new OAuth token by either using an authorization code obtained
|
||||||
|
through BankID authentication or by using a refresh token.
|
||||||
|
Valid grant_type values:
|
||||||
|
- 'authorization_code'
|
||||||
|
- 'refresh_token'
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = 'https://api.sparebank1.no/oauth/token'
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
'client_id': CLIENT_ID,
|
||||||
|
'client_secret': CLIENT_SECRET,
|
||||||
|
'grant_type': grant_type,
|
||||||
|
}
|
||||||
|
if grant_type == 'authorization_code':
|
||||||
|
data['code'] = code
|
||||||
|
data['state'] = state
|
||||||
|
data['redirect_uri'] = REDIRECT_URI
|
||||||
|
elif grant_type == 'refresh_token':
|
||||||
|
data['refresh_token'] = code
|
||||||
|
else:
|
||||||
|
raise ValueError("grant_type must be 'authorization_code' or 'refresh_token'")
|
||||||
|
|
||||||
|
resp = requests.post('https://api.sparebank1.no/oauth/token', data=data, headers=headers)
|
||||||
|
if not resp.ok:
|
||||||
|
return None
|
||||||
|
|
||||||
|
token = json.loads(resp.text)
|
||||||
|
|
||||||
|
token['time'] = int(time.time())
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
def oauth_token_refresh(token: dict) -> Optional[dict]:
|
||||||
|
""" uses the `refresh_token` key in `token` to get a new `token` """
|
||||||
|
return oauth_token("0", token['refresh_token'], 'refresh_token')
|
||||||
|
|
||||||
|
def oauth_token_new(state: str, code: str) -> Optional[dict]:
|
||||||
|
""" uses an authentication code obtained through BankID authentication to obtain a new oauth token """
|
||||||
|
return oauth_token(state, code, 'authorization_code')
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
""" BankId authentication redirects to this handler with the url
|
||||||
|
parameters `code` and `state`, which are used to obtain an OAuth
|
||||||
|
code.
|
||||||
|
BaseHTTPRequestHandler handlers can't return any values so a global
|
||||||
|
variable `TOKEN` is set instead.
|
||||||
|
"""
|
||||||
|
parsed_path = urllib.parse.urlparse(self.path)
|
||||||
|
params = urllib.parse.parse_qs(parsed_path.query)
|
||||||
|
|
||||||
|
code = params.get("code", [None])[0]
|
||||||
|
state = params.get("state", [None])[0]
|
||||||
|
|
||||||
|
assert code
|
||||||
|
assert state
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'text/html')
|
||||||
|
self.end_headers()
|
||||||
|
with open('close_page.html', 'rb') as f:
|
||||||
|
self.wfile.write(f.read())
|
||||||
|
|
||||||
|
global TOKEN
|
||||||
|
TOKEN = oauth_token_new(state, code)
|
||||||
|
|
||||||
|
def browser_auth() -> Optional[dict]:
|
||||||
|
state = uuid4()
|
||||||
|
|
||||||
|
auth_url = 'https://api.sparebank1.no/oauth/authorize'
|
||||||
|
auth_url += f'?client_id={CLIENT_ID}'
|
||||||
|
auth_url += f'&state={state}'
|
||||||
|
auth_url += f'&redirect_uri={REDIRECT_URI}'
|
||||||
|
if FIN_INST:
|
||||||
|
auth_url += f'&finInst={FIN_INST}'
|
||||||
|
auth_url += f'&response_type=code'
|
||||||
|
|
||||||
|
print(f"opening {auth_url} in browser", file=sys.stderr)
|
||||||
|
webbrowser.open(auth_url)
|
||||||
|
|
||||||
|
with HTTPServer((HOST, PORT), Handler) as httpd:
|
||||||
|
# handle_request sets the global variable TOKEN
|
||||||
|
httpd.handle_request()
|
||||||
|
|
||||||
|
return TOKEN
|
||||||
|
|
||||||
|
def token_oauth_expired(token: dict) -> bool:
|
||||||
|
""" Returns true if access token is expired """
|
||||||
|
return token['time'] + token['expires_in'] < int(time.time())
|
||||||
|
|
||||||
|
def token_refresh_expired(token: dict) -> bool:
|
||||||
|
""" Return true if refresh token is expired """
|
||||||
|
return token['time'] + token['refresh_token_expires_in'] < int(time.time())
|
||||||
|
|
||||||
|
def authenticate() -> dict:
|
||||||
|
""" Main authentication function, returns a token dict containing `acess_token` """
|
||||||
|
try:
|
||||||
|
f = open('smn-oauth.json', 'r+')
|
||||||
|
with f:
|
||||||
|
token = json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
token = None
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
print(f"Token not found, BankID authentication required...", file=sys.stderr)
|
||||||
|
token = browser_auth()
|
||||||
|
elif token_refresh_expired(token):
|
||||||
|
print(f"Refresh token expired, authenticating...", file=sys.stderr)
|
||||||
|
token = browser_auth()
|
||||||
|
elif token_oauth_expired(token):
|
||||||
|
print(f"OAuth expired, refreshing...", file=sys.stderr)
|
||||||
|
token = oauth_token_refresh(token)
|
||||||
|
if not token:
|
||||||
|
print(f"Failed to refresh, authenticating", file=sys.stderr)
|
||||||
|
browser_auth()
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
print(f"Fatal error: failed to get token", file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
with open('smn-oauth.json', 'w') as f:
|
||||||
|
json.dump(token, f, indent=4)
|
||||||
|
f.truncate()
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
def main():
|
||||||
|
token = authenticate()
|
||||||
|
|
||||||
|
hw = requests.get(
|
||||||
|
'https://api.sparebank1.no/personal/banking/accounts?includeCreditCardAccounts=true&includeAskAccounts=true',
|
||||||
|
headers = {
|
||||||
|
'Authorization': f"Bearer {token['access_token']}",
|
||||||
|
'Accept': 'application/vnd.sparebank1.v1+json; charset=utf-8'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for acc in hw.json()['accounts']:
|
||||||
|
print(f'{(acc["description"] + ':').ljust(25)} {acc["balance"]} {acc["currencyCode"]}')
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
certifi==2024.8.30
|
||||||
|
charset-normalizer==3.4.0
|
||||||
|
idna==3.10
|
||||||
|
requests==2.32.3
|
||||||
|
types-requests==2.32.0.20241016
|
||||||
|
urllib3==2.2.3
|
||||||
Reference in New Issue
Block a user