Initial commit

This commit is contained in:
2024-11-11 22:01:18 +01:00
commit 3cd2d34634
4 changed files with 203 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*.env
smn-oauth.json
venv

21
close_page.html Normal file
View 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
View 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
View 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