├── .gitignore ├── .vscode └── settings.json ├── tsconfig.json ├── .github └── FUNDING.yml ├── README.md ├── package.json └── src ├── test.ts ├── authenticate.ts ├── interfaces.ts ├── index.ts └── requests.ts /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | credential.json 4 | session.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.wordWrap": "on", 4 | "editor.tabSize": 2 5 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "target": "es2019" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [codler] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Avanza API 2 | 3 | ## Install 4 | 5 | ``` 6 | npm i avanza-api 7 | ``` 8 | 9 | ## Usage 10 | 11 | ```js 12 | import Avanza from "avanza-api"; 13 | const avanza = new Avanza(); 14 | 15 | await avanza.authenticate({ 16 | username: "", 17 | password: "", 18 | totpSecret: "" 19 | }); 20 | 21 | if (avanza.isAuthenticated) { 22 | const accounts = await avanza.getAccounts(); 23 | console.log(accounts); 24 | } 25 | ``` 26 | 27 | ### Login with BankId 28 | 29 | ```js 30 | avanza.authenticate({ 31 | personnummer: "123456789012" 32 | }); 33 | ``` 34 | 35 | If multiple username are connected to your BankId, you will need to choose which to login by adding username key. 36 | 37 | ```js 38 | avanza.authenticate({ 39 | username: "", 40 | personnummer: "123456789012" 41 | }); 42 | ``` 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "avanza-api", 3 | "version": "2.1.4", 4 | "description": "Avanza API", 5 | "main": "dist/index.js", 6 | "funding": { 7 | "type": "individual", 8 | "url": "http://yap.nu" 9 | }, 10 | "scripts": { 11 | "build": "tsc -p tsconfig.json", 12 | "test": "ts-node src/test.ts" 13 | }, 14 | "keywords": [ 15 | "Avanza" 16 | ], 17 | "author": "Han Lin Yap (http://yap.nu)", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/codler/avanza-api.git" 21 | }, 22 | "license": "ISC", 23 | "dependencies": { 24 | "fetch-register": "^1.3.2", 25 | "otp-client": "^1.0.2" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^13.7.4", 29 | "ts-node": "^8.6.2", 30 | "typescript": "^3.8.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import Avanza, { InstrumentType } from "./index"; 2 | const avanza = new Avanza({ 3 | preFetch: function(...rest) { 4 | console.log("Request", rest); 5 | return rest; 6 | } 7 | }); 8 | 9 | initPublic(); 10 | async function initPublic() { 11 | const search = await avanza.searchList("abb"); 12 | console.log(search); 13 | } 14 | 15 | // init(); 16 | async function init() { 17 | try { 18 | avanza.session = require("../session.json"); 19 | } catch {} 20 | if (!avanza.isAuthenticated) { 21 | try { 22 | await avanza.authenticate(require("../credential.json")); 23 | } catch (e) { 24 | console.log("Test: Catch:", e); 25 | } 26 | } 27 | 28 | if (avanza.isAuthenticated) { 29 | console.log("Session", avanza.session); 30 | 31 | const accounts = await avanza.getAccounts(); 32 | 33 | const positions = await avanza.getPositions(); 34 | 35 | const orderbookIds = positions 36 | .filter(position => position.instrumentType !== InstrumentType.UNKNOWN) 37 | .map(position => position.orderbookId); 38 | console.log("TCL: orderbookIds", orderbookIds); 39 | 40 | const ava = await avanza.getOrderbooks(orderbookIds); 41 | console.log(ava); 42 | } else { 43 | console.log("Test: Failed to authenticate"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/authenticate.ts: -------------------------------------------------------------------------------- 1 | import OTP from "otp-client"; 2 | 3 | import { 4 | getAuthenticationSessionsBankId, 5 | getAuthenticationSessionsBankIdCollect, 6 | getAuthenticationSessionsBankIdCollectCustomer, 7 | getAuthenticationSessionsUsercredentials, 8 | getAuthenticationSessionsTotp, 9 | AuthenticationSessionsTotp 10 | } from "./requests"; 11 | 12 | export interface Credentials { 13 | username?: string; 14 | password?: string; 15 | /** 16 | * Format 6 digit 17 | */ 18 | totp?: string; 19 | /** 20 | * Format Sha1 21 | */ 22 | totpSecret?: string; 23 | /** 24 | * Format XXXXXX-XXXX 25 | */ 26 | personnummer?: string; 27 | } 28 | 29 | async function authenticateCredential( 30 | options: Credentials 31 | ): Promise { 32 | const credential = await getAuthenticationSessionsUsercredentials( 33 | options.username, 34 | options.password 35 | ); 36 | const totpCode = options.totpSecret 37 | ? new OTP(options.totpSecret).getToken() 38 | : options.totp; 39 | const session = await getAuthenticationSessionsTotp( 40 | totpCode, 41 | credential.twoFactorLogin.transactionId 42 | ); 43 | return session; 44 | } 45 | 46 | async function authenticateBankId(options) { 47 | const attempt = await getAuthenticationSessionsBankId(options.personnummer); 48 | 49 | console.log("Open your mobile Bank ID"); 50 | 51 | const success = await getAuthenticationSessionsBankIdCollect( 52 | attempt.transactionId, 53 | new Date(attempt.expires) 54 | ); 55 | 56 | if (options.username) { 57 | const user = success.logins.find( 58 | login => login.username === options.username 59 | ); 60 | if (user) { 61 | return await getAuthenticationSessionsBankIdCollectCustomer( 62 | user.loginPath 63 | ); 64 | } else { 65 | throw "Username not found in login"; 66 | } 67 | } else if (success.logins.length) { 68 | return await getAuthenticationSessionsBankIdCollectCustomer( 69 | success.logins[0].loginPath 70 | ); 71 | } else { 72 | throw "No logins found"; 73 | } 74 | } 75 | 76 | async function authenticate( 77 | options: Credentials 78 | ): Promise { 79 | const useBankId = !!options.personnummer; 80 | 81 | if (useBankId) { 82 | return await authenticateBankId(options); 83 | } 84 | return await authenticateCredential(options); 85 | } 86 | 87 | export default authenticate; 88 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export enum AccountType { 2 | "AktieFondkonto" = "AktieFondkonto", 3 | "Investeringssparkonto" = "Investeringssparkonto", 4 | "KreditkontoISK" = "KreditkontoISK", 5 | "SparkontoPlus" = "SparkontoPlus", 6 | "Tjanstepension" = "Tjanstepension" 7 | } 8 | 9 | export enum InstrumentType { 10 | "STOCK" = "STOCK", 11 | "FUND" = "FUND", 12 | "CERTIFICATE" = "CERTIFICATE", 13 | "UNKNOWN" = "UNKNOWN" 14 | } 15 | 16 | export type SparkontoPlusType = "Collector" | "Klarna" | "Santander" | string; 17 | 18 | export interface Account { 19 | accountType: AccountType; 20 | interestRate: number; 21 | depositable: boolean; 22 | name: string; 23 | active: boolean; 24 | totalProfit: number; 25 | accountId: string; 26 | tradable: boolean; 27 | totalBalance: number; 28 | totalBalanceDue: number; 29 | ownCapital: number; 30 | accountPartlyOwned: boolean; 31 | buyingPower: number; 32 | totalProfitPercent: number; 33 | performance: number; 34 | performancePercent: number; 35 | sparkontoPlusType?: SparkontoPlusType; 36 | attorney: boolean; 37 | } 38 | 39 | export interface ResponseOverview { 40 | accounts: Account[]; 41 | numberOfOrders: number; 42 | numberOfDeals: number; 43 | totalBuyingPower: number; 44 | totalOwnCapital: number; 45 | totalBalance: number; 46 | numberOfTransfers: number; 47 | numberOfIntradayTransfers: number; 48 | totalPerformancePercent: number; 49 | totalPerformance: number; 50 | } 51 | 52 | export interface Position { 53 | instrumentType: InstrumentType; // Internal 54 | accountName: string; 55 | accountType: AccountType; 56 | depositable: boolean; 57 | accountId: string; 58 | changePercentPeriod?: number; // Fund 59 | changePercentThreeMonths?: number; // Fund 60 | value: number; 61 | profit: number; 62 | volume: number; 63 | collateralValue?: number; 64 | averageAcquiredPrice: number; 65 | profitPercent: number; 66 | acquiredValue: number; 67 | name: string; 68 | currency: string; 69 | flagCode?: string; // Not in Fund 70 | orderbookId?: string; // Not in Unknown 71 | tradable?: boolean; // Not in Unknown 72 | lastPrice?: number; // Not in Unknown 73 | lastPriceUpdated?: string; // Not in Unknown 74 | change?: number; // Not in Unknown 75 | changePercent?: number; // Not in Unknown 76 | } 77 | 78 | export interface InstrumentPosition { 79 | instrumentType: InstrumentType; 80 | positions: Position[]; 81 | totalValue: number; 82 | todaysProfitPercent: number; 83 | totalProfitValue: number; 84 | totalProfitPercent: number; 85 | } 86 | export interface ResponsePositions { 87 | instrumentPositions: InstrumentPosition[]; 88 | totalOwnCapital: number; 89 | totalBuyingPower: number; 90 | totalBalance: number; 91 | totalProfitPercent: number; 92 | totalProfit: number; 93 | } 94 | 95 | export interface TransactionFees { 96 | commission: number; 97 | marketFees: number; 98 | totalFees: number; 99 | totalSum: number; 100 | totalSumWithoutFees: number; 101 | } 102 | 103 | export interface Orderbook { 104 | currency: string; 105 | flagCode: string; 106 | name: string; 107 | id: string; 108 | type: InstrumentType; // InstrumentType?? 109 | marketPlace: string; // eg Stockholmsbörsen 110 | } 111 | export interface Order { 112 | transactionFees: TransactionFees; 113 | orderbook: Orderbook; 114 | account: { type: string; name: string; id: string }; 115 | status: string; // eg Marknaden 116 | statusDescription: string; 117 | rawStatus: string; // eg ACTIVE 118 | validUntil: string; 119 | openVolume: unknown; 120 | marketTransaction: boolean; 121 | type: string; // eg BUY 122 | orderId: string; 123 | deletable: boolean; 124 | price: number; 125 | modifyAllowed: boolean; 126 | orderDateTime: string; 127 | volume: number; 128 | sum: number; 129 | } 130 | 131 | export interface ResponseDealsAndOrders { 132 | orders: Order[]; 133 | deals: unknown[]; 134 | accounts: { type: string; name: string; id: string }[]; 135 | reservedAmount: number; 136 | } 137 | 138 | export interface ResponseOrderbook { 139 | currency: string; 140 | highestPrice?: number; // STOCK 141 | lowestPrice?: number; // STOCK 142 | lastPrice?: number; // STOCK 143 | change?: number; // STOCK 144 | changePercent?: number; // STOCK 145 | updated?: string; // STOCK 146 | totalVolumeTraded?: number; // STOCK 147 | flagCode?: string; // STOCK 148 | priceThreeMonthsAgo?: number; // STOCK 149 | managementFee?: number; // FUND 150 | prospectus?: string; // FUND 151 | rating?: number; // FUND 152 | changePercentOneYear?: number; // FUND 153 | minMonthlySavingAmount?: number; // FUND 154 | risk?: number; // FUND 155 | lastUpdated?: string; // FUND 156 | changePercentPeriod?: number; // FUND 157 | changePercentThreeMonths?: number; // FUND 158 | instrumentType: InstrumentType; 159 | tradable: boolean; 160 | name: string; 161 | id: string; 162 | } 163 | 164 | export interface Search { 165 | instrumentType: InstrumentType; // Internal 166 | currency: string; 167 | lastPrice: number; 168 | changePercent: number; 169 | tradable: boolean; 170 | tickerSymbol: string; 171 | flagCode: string; 172 | name: string; 173 | id: string; 174 | } 175 | 176 | export interface ResponseSearchHit { 177 | instrumentType: InstrumentType; 178 | numberOfHits: number; 179 | topHits: Search[]; 180 | } 181 | export interface ResponseSearch { 182 | totalNumberOfHits: number; 183 | hits: ResponseSearchHit[]; 184 | } 185 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "fetch-register"; 2 | import authenticate, { Credentials } from "./authenticate"; 3 | import { AuthenticationSessionsTotp } from "./requests"; 4 | import * as I from "./interfaces"; 5 | import { InstrumentType } from "./interfaces"; 6 | 7 | interface PreFetch { 8 | (path: string, options: RequestInit): { [0]: string; [1]: RequestInit }; 9 | } 10 | class Avanza { 11 | static BASE_URL = "https://www.avanza.se"; 12 | 13 | private preFetch: PreFetch; 14 | 15 | constructor(options: { preFetch?: PreFetch } = {}) { 16 | this.preFetch = 17 | options.preFetch || 18 | function(...rest) { 19 | return rest; 20 | }; 21 | } 22 | 23 | public get isAuthenticated(): boolean { 24 | return ( 25 | this.session && 26 | !!this.session.securityToken && 27 | !!this.session.authenticationSession 28 | ); 29 | } 30 | 31 | credentials: Credentials; 32 | session: AuthenticationSessionsTotp; 33 | 34 | expireSession() { 35 | this.session = undefined; 36 | } 37 | 38 | retryAuthenticate(): Promise { 39 | console.log("retryAuthenticate"); 40 | this.expireSession(); 41 | return this.authenticate(this.credentials); 42 | } 43 | 44 | async authenticate(options: Credentials): Promise { 45 | if (!options) { 46 | throw "Missing credentials"; 47 | } 48 | this.credentials = options; 49 | this.session = await authenticate(options); 50 | return this.isAuthenticated; 51 | } 52 | 53 | async authFetch(path: string, options: RequestInit = {}): Promise { 54 | if (!this.isAuthenticated) { 55 | throw "Call authenticate before"; 56 | } 57 | return this.fetch(path, options); 58 | } 59 | 60 | async fetch(path: string, options: RequestInit = {}): Promise { 61 | const requestPath = Avanza.BASE_URL + path; 62 | const requestOptions: RequestInit = Object.assign({}, options, { 63 | headers: { 64 | ...options.headers, 65 | "X-AuthenticationSession": 66 | this.session && this.session.authenticationSession, 67 | "X-SecurityToken": this.session && this.session.securityToken 68 | } 69 | }); 70 | 71 | try { 72 | const response = await fetch.apply( 73 | this, 74 | this.preFetch(requestPath, requestOptions) 75 | ); 76 | 77 | if (response.status === 401) { 78 | await this.retryAuthenticate(); 79 | if (this.isAuthenticated) { 80 | return this.fetch(path, options); 81 | } 82 | throw { code: 401 }; 83 | } else { 84 | return response.json().catch(e => { 85 | console.log(response); 86 | throw e; 87 | }); 88 | } 89 | } catch (e) { 90 | if (e) { 91 | if (e.code === "ETIMEDOUT") { 92 | console.log("Fetch: ETIMEDOUT"); 93 | throw e; 94 | } 95 | if (e.code === "ENOTFOUND") { 96 | console.log("Fetch: ENOTFOUND"); 97 | throw e; 98 | } 99 | if (e.code === 401) { 100 | console.log("Fetch: 401"); 101 | throw e; 102 | } 103 | } 104 | console.log(e); 105 | const processModuleName = "process"; 106 | require(processModuleName).exit(1); 107 | } 108 | } 109 | 110 | async getAccounts(): Promise { 111 | const overview: I.ResponseOverview = await this.getAccountsSummary(); 112 | return overview.accounts; 113 | } 114 | 115 | async getPositions(): Promise { 116 | const responsePositions: I.ResponsePositions = await this.getPositionsByInstrumentType(); 117 | 118 | const positions: I.Position[] = []; 119 | 120 | responsePositions.instrumentPositions.forEach(instrumentPosition => 121 | instrumentPosition.positions.forEach(position => { 122 | position.instrumentType = instrumentPosition.instrumentType; 123 | positions.push(position); 124 | }) 125 | ); 126 | 127 | return positions; 128 | } 129 | 130 | getPositionsByInstrumentType(): Promise { 131 | return this.authFetch("/_mobile/account/positions"); 132 | } 133 | 134 | getAccountsSummary(): Promise { 135 | return this.authFetch("/_mobile/account/overview"); 136 | } 137 | 138 | async getOrders(): Promise { 139 | const dealsandorders: I.ResponseDealsAndOrders = await this.getDealsAndOrders(); 140 | return dealsandorders.orders; 141 | } 142 | 143 | getDealsAndOrders(): Promise { 144 | return this.authFetch("/_mobile/account/dealsandorders"); 145 | } 146 | 147 | getOrderbooks(orderbookIds: string[]): Promise { 148 | if (!orderbookIds || orderbookIds.length === 0) { 149 | throw "Missing orderbookIds"; 150 | } 151 | const path = orderbookIds.join(","); 152 | return this.authFetch("/_mobile/market/orderbooklist/" + path); 153 | } 154 | 155 | async searchList( 156 | query: string, 157 | instrumentType: InstrumentType = InstrumentType.STOCK 158 | ): Promise { 159 | const responseSearch: I.ResponseSearch = await this.search( 160 | query, 161 | instrumentType 162 | ); 163 | 164 | const search: I.Search[] = []; 165 | 166 | responseSearch.hits.forEach(hit => 167 | hit.topHits.forEach(topHit => { 168 | topHit.instrumentType = hit.instrumentType; 169 | search.push(topHit); 170 | }) 171 | ); 172 | 173 | return search; 174 | } 175 | 176 | search( 177 | query: string, 178 | instrumentType: InstrumentType = InstrumentType.STOCK 179 | ): Promise { 180 | const qs = { 181 | limit: "1000", 182 | query: query 183 | }; 184 | return this.fetch( 185 | "/_mobile/market/search/" + instrumentType + "?" + new URLSearchParams(qs) 186 | ); 187 | } 188 | } 189 | 190 | export * from "./interfaces"; 191 | export default Avanza; 192 | -------------------------------------------------------------------------------- /src/requests.ts: -------------------------------------------------------------------------------- 1 | import Avanza from "./index"; 2 | interface ResponseAuthenticationSessionsBankId { 3 | transactionId: string; 4 | expires: string; 5 | autostartToken: string; 6 | } 7 | 8 | async function getAuthenticationSessionsBankId( 9 | personnummer 10 | ): Promise { 11 | const url = `${Avanza.BASE_URL}/_api/authentication/sessions/bankid`; 12 | const response = await fetch(url, { 13 | method: "POST", 14 | body: JSON.stringify({ 15 | identificationNumber: personnummer 16 | }), 17 | headers: { 18 | "Content-Type": "application/json" 19 | } 20 | }); 21 | 22 | const responseJson: ResponseAuthenticationSessionsBankId = await response.json(); 23 | if (typeof responseJson.transactionId === "undefined") { 24 | throw "Missing transactionId"; 25 | } 26 | if (new Date(responseJson.expires) < new Date()) { 27 | throw "Authentication attempt expired"; 28 | } 29 | return responseJson; 30 | } 31 | 32 | type ResponseAuthenticationSessionsBankIdCollectState = "OUTSTANDING_TRANSACTION"; 33 | 34 | interface ResponseAuthenticationSessionsBankIdCollectLoginAccount { 35 | accountName: string; 36 | accountType: string; 37 | } 38 | interface ResponseAuthenticationSessionsBankIdCollectLogin { 39 | customerId: string; 40 | username: string; 41 | accounts: ResponseAuthenticationSessionsBankIdCollectLoginAccount[]; 42 | loginPath: string; 43 | } 44 | interface ResponseAuthenticationSessionsBankIdCollect { 45 | transactionId: string; 46 | state: ResponseAuthenticationSessionsBankIdCollectState; 47 | name?: string; 48 | logins: ResponseAuthenticationSessionsBankIdCollectLogin[]; 49 | recommendedTargetCustomers: unknown[]; 50 | } 51 | 52 | async function getAuthenticationSessionsBankIdCollect( 53 | transactionId, 54 | expires: Date 55 | ): Promise { 56 | const url = `${Avanza.BASE_URL}/_api/authentication/sessions/bankid/collect`; 57 | const response = await fetch(url, { 58 | headers: { 59 | "Content-Type": "application/json", 60 | Cookie: `AZAMFATRANSACTION=${transactionId}` 61 | } 62 | }); 63 | 64 | const responseJson: ResponseAuthenticationSessionsBankIdCollect = await response.json(); 65 | 66 | if (responseJson.state === "OUTSTANDING_TRANSACTION") { 67 | return new Promise((resolve, reject) => { 68 | setTimeout(() => { 69 | if (new Date(expires) < new Date()) { 70 | reject("Authentication attempt expired"); 71 | return; 72 | } 73 | try { 74 | resolve( 75 | getAuthenticationSessionsBankIdCollect( 76 | responseJson.transactionId, 77 | expires 78 | ) 79 | ); 80 | } catch (e) { 81 | reject(e); 82 | } 83 | }, 2000); 84 | }); 85 | } else if (responseJson.state === "COMPLETE") { 86 | return responseJson; 87 | } else { 88 | console.log(responseJson); 89 | throw "Unknown state"; 90 | } 91 | } 92 | 93 | async function getAuthenticationSessionsBankIdCollectCustomer( 94 | loginPath: string 95 | ): Promise { 96 | const url = Avanza.BASE_URL + loginPath; 97 | const response = await fetch(url); 98 | 99 | const securityToken = response.headers.get("x-securitytoken"); 100 | if (!securityToken) { 101 | throw "Error getting security token"; 102 | } 103 | const responseJson: ResponseAuthenticationSessionsTotp = await response.json(); 104 | if (!responseJson.registrationComplete) { 105 | throw "Registration not complete"; 106 | } 107 | if ( 108 | responseJson.authenticationSession && 109 | responseJson.pushSubscriptionId && 110 | responseJson.customerId 111 | ) { 112 | return { 113 | ...responseJson, 114 | securityToken 115 | }; 116 | } else { 117 | console.error(responseJson); 118 | throw "Json missing keys"; 119 | } 120 | } 121 | interface ResponseAuthenticationSessionsUsercredentials { 122 | twoFactorLogin: { 123 | method: string; 124 | transactionId: string; 125 | }; 126 | } 127 | async function getAuthenticationSessionsUsercredentials( 128 | username, 129 | password 130 | ): Promise { 131 | const url = `${Avanza.BASE_URL}/_api/authentication/sessions/usercredentials`; 132 | const response = await fetch(url, { 133 | method: "POST", 134 | body: JSON.stringify({ 135 | username, 136 | password 137 | }), 138 | headers: { 139 | "Content-Type": "application/json" 140 | } 141 | }); 142 | 143 | const responseJson: ResponseAuthenticationSessionsUsercredentials = await response.json(); 144 | if (typeof responseJson.twoFactorLogin === "undefined") { 145 | console.error( 146 | "Request usercredentials: Failed to authenticate", 147 | responseJson 148 | ); 149 | throw "Failed to authenticate"; 150 | } 151 | if (responseJson.twoFactorLogin.method !== "TOTP") { 152 | throw `Unsupported second factor method ${responseJson.twoFactorLogin.method}`; 153 | } 154 | return responseJson; 155 | } 156 | 157 | interface ResponseAuthenticationSessionsTotp { 158 | authenticationSession: string; 159 | customerId: string; 160 | pushSubscriptionId: string; 161 | registrationComplete: boolean; 162 | } 163 | export interface AuthenticationSessionsTotp 164 | extends ResponseAuthenticationSessionsTotp { 165 | securityToken: string; 166 | } 167 | async function getAuthenticationSessionsTotp( 168 | totpCode: string, 169 | twoFactorLoginTransactionId: string 170 | ): Promise { 171 | const url = `${Avanza.BASE_URL}/_api/authentication/sessions/totp`; 172 | const response = await fetch(url, { 173 | method: "POST", 174 | body: JSON.stringify({ 175 | method: "TOTP", 176 | totpCode 177 | }), 178 | headers: { 179 | "Content-Type": "application/json", 180 | Cookie: `AZAMFATRANSACTION=${twoFactorLoginTransactionId}` 181 | } 182 | }); 183 | 184 | if (response.status === 401) { 185 | throw new Error("Unauthorized Totp"); 186 | } 187 | 188 | const securityToken = response.headers.get("x-securitytoken"); 189 | if (!securityToken) { 190 | throw "Error getting security token"; 191 | } 192 | const responseJson: ResponseAuthenticationSessionsTotp = await response.json(); 193 | if (!responseJson.registrationComplete) { 194 | throw "Registration not complete"; 195 | } 196 | if ( 197 | responseJson.authenticationSession && 198 | responseJson.pushSubscriptionId && 199 | responseJson.customerId 200 | ) { 201 | return { 202 | ...responseJson, 203 | securityToken 204 | }; 205 | } else { 206 | console.error(responseJson); 207 | throw "Json missing keys"; 208 | } 209 | } 210 | 211 | export { 212 | getAuthenticationSessionsBankId, 213 | getAuthenticationSessionsBankIdCollect, 214 | getAuthenticationSessionsBankIdCollectCustomer, 215 | getAuthenticationSessionsUsercredentials, 216 | getAuthenticationSessionsTotp 217 | }; 218 | --------------------------------------------------------------------------------