Skip to content
This repository has been archived by the owner on Oct 29, 2023. It is now read-only.

Authentification no longer working #48

Closed
GeoffreyFrogeye opened this issue Sep 14, 2019 · 43 comments · Fixed by #49
Closed

Authentification no longer working #48

GeoffreyFrogeye opened this issue Sep 14, 2019 · 43 comments · Fixed by #49
Assignees
Labels

Comments

@GeoffreyFrogeye
Copy link

From this day, 14th of September 2019, trying to log in with this client, whether from the module or with the CLI application, will result in a 403 error:

$ n26 balance
Traceback (most recent call last):
  File "/home/geoffrey/.local/bin/n26", line 11, in <module>
    load_entry_point('n26==1.2.0', 'console_scripts', 'n26')()
  File "/usr/lib/python3.7/site-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "/usr/lib/python3.7/site-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/usr/lib/python3.7/site-packages/click/core.py", line 1137, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/lib/python3.7/site-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/lib/python3.7/site-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "/home/geoffrey/.local/lib/python3.7/site-packages/n26/cli.py", line 94, in balance
    balance_data = API_CLIENT.get_balance()
  File "/home/geoffrey/.local/lib/python3.7/site-packages/n26/api.py", line 59, in get_balance
    return self._do_request(GET, BASE_URL + '/api/accounts')
  File "/home/geoffrey/.local/lib/python3.7/site-packages/n26/api.py", line 214, in _do_request
    access_token = self.get_token()
  File "/home/geoffrey/.local/lib/python3.7/site-packages/n26/api.py", line 272, in get_token
    self._token_data = self._request_token(self.config.username, self.config.password)
  File "/home/geoffrey/.local/lib/python3.7/site-packages/n26/api.py", line 296, in _request_token
    response.raise_for_status()
  File "/usr/lib/python3.7/site-packages/requests/models.py", line 940, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: https://api.tech26.de/oauth/token

I believe this is due to enforcement of the Revised Directive on Payment Services (PSD2) enforcing "strong" authentication. In fact, from now on, connecting to the N26 web application requires you to validate the login attempt from the phone application.

So I guess the big question is, can this be worked around, or should we say goodbye to the N26 API (without an validation from the phone application, which defeats the purpose)?

@markusressel
Copy link
Collaborator

markusressel commented Sep 14, 2019

Thx for reporting this so quickly.
The question is how long a login session can be kept alive. If it is possible to renew a token indefinitely it could - although not as convenient as before - still be doable. I will try to do some research about this asap.

If anyone is interested in having a look at f.ex. the N26 apk to provide information on how this could be done feel free to report in this thread.

@markusressel
Copy link
Collaborator

@GeoffreyFrogeye
Copy link
Author

By the way PSD2 also means that banks (including N26, which seems to comply already) must provide an API for allowing access to account transactions, balance and payment initiation.

However, if one want to access those API, they must be allowed to operate as a Payment Service Provider and having obtained certifications. A bit much for people that just want to check their account freely.

Moreover, if somehow anyone could gain access to this PSD2 API, they lack a lot of information the N26 API provides, notably on the opposed account of transactions (MCC, name, city, or IBAN/BIC...).

So if anyone (like me) was wondering the point of using the N26 API when there's a brand new one available, hopes this clears things up.

@markusressel
Copy link
Collaborator

Yeah they call it "open banking" yet you can't access the data as an individual and it is limited in the kind of data that is accessible 🤦‍♂️
I really hope N26 doesn't kill its own API for this 🙄

I don't really know how to proceed with this. I think the only acceptable way to continue this lib would be to try to reverse engineer the two-step authentication currently used by N26 and implement it here too. Although inconvenient it should be possible to refresh the token indefinitely, just like with the current approach. For the CLI usage the token should probably be temporarily stored on the file system for repeated access.

@janezkranjc
Copy link

I've been examining it a bit and it looks like they're using graphql now for all API calls (in the browser)

@limolitz
Copy link

I did some digging in the browser as well. The good news is, that if you copy the request a browser makes verbatim into curl, the request works, so there is no fingerprinting or nonce or something else blocking it.
There is some (apparently aes-) encrypted payload sent over, but it doesn't seem to be strictly needed. If you remove it, the request still gets answered, but the parameters (e.g. date limits in a transaction search) are gone. So for basic functionality, this might not be needed to be reverse-engineered.

@Sjord
Copy link

Sjord commented Sep 17, 2019

The login flow of the N26 Android app on a non-paired device works as follows:

  1. POST normal URL encoded form to oauth2/token, with username, password, and grant_type=password. Response contains error=mfa_required, and a GUID mfaToken.
  2. POST JSON to /api/mfa/challenge, with challengeType=oob, and the mfaToken.
  3. POST to oauth2/token again, with mfaToken and grant_type=mfa_oob. This responds with an access token and refresh token if the login has been accepted on the paired device.
  4. Access token is used in Authorization: Bearer header to authenticate following requests.

@janezkranjc
Copy link

Would it be somehow possible to extract something from a paired phone using a man in the middle proxy that could be used with the CLI interface without the need for 2fa?

@julian-klode
Copy link

julian-klode commented Sep 17, 2019

@Sjord Thanks, this workflow is working in my personal hack - well described.

Here's my entire flow:

data = [("username", user), ("password", password), ("grant_type", "password")]
USER_AGENT = ("Mozilla/5.0 (X11; Linux x86_64) "
              "AppleWebKit/537.36 (KHTML, like Gecko) "
              "Chrome/59.0.3071.86 Safari/537.36")


def authorize_mfa(mfa_token):
    req = urllib.request.Request("https://api.tech26.de/api/mfa/challenge",
                                 data=json.dumps({"challengeType": "oob", "mfaToken": mfa_token}).encode("utf-8"))
    req.add_header("Authorization",
                   "Basic bXktdHJ1c3RlZC13ZHBDbGllbnQ6c2VjcmV0")
    req.add_header("User-Agent", USER_AGENT)
    req.add_header('Content-Type', 'application/json')


    try:
        url = urllib.request.urlopen(req)
    except urllib.error.HTTPError as e:
        body = json.load(e)
        print(e, body)
        
    

def authorize():
    "Return the bearer."
    encoded_data = urllib.parse.urlencode(data).encode("utf-8")
    req = urllib.request.Request("https://api.tech26.de/oauth/token",
                                 data=encoded_data)
    req.add_header("Authorization",
                   "Basic bXktdHJ1c3RlZC13ZHBDbGllbnQ6c2VjcmV0")
    req.add_header("User-Agent", USER_AGENT)

    try:
        url = urllib.request.urlopen(req)
    except urllib.error.HTTPError as e:
        body = json.load(e)
        if body.get("error", "") == "mfa_required":
            mfa_token = body["mfaToken"]

    authorize_mfa(mfa_token)
    input()
    encoded_data = urllib.parse.urlencode([("grant_type", "mfa_oob"), ("mfaToken", mfa_token)]).encode("utf-8")
    req = urllib.request.Request("https://api.tech26.de/oauth/token",
                                 data=encoded_data)
    req.add_header("Authorization",
                   "Basic bXktdHJ1c3RlZC13ZHBDbGllbnQ6c2VjcmV0")
    req.add_header("User-Agent", USER_AGENT)
    url = urllib.request.urlopen(req)

    body = url.read().decode("utf-8")
    url.close()
    return json.loads(body)["access_token"]

@julian-klode
Copy link

One thing to figure out is how to get rid of the input() in there -basically if you do the final token POST before you have approved on the phone, it fails. I guess you could try in a loop, but it sounds bad.

@joylazari
Copy link

I guess there should be some kind of event that is been triggered on mobile approval.
Did someone tried to sniff the desktop website for something like an event listener?

@markusressel
Copy link
Collaborator

markusressel commented Sep 17, 2019

@julian-klode The website also has a considerable delay between approving the login in the app and continuing the website login. Is there any disadvantage to query in a loop other than that it feels bad? Any rate limit or something like that? Otherwise I would be fine with this solution - at least for now.

@julian-klode
Copy link

@markusressel I don't know if it gets flagged as unusual or something and triggers security lockouts eventually. I'm a bit scared.

@markusressel
Copy link
Collaborator

Ok so I played around with your code @julian-klode and modified it to match the code style of python-n26. After a couple of (successful!) test runs I now get a 429 http response for Too many log-in attempts. Please try again in 30 minutes..

I tried to do an automatic "retry" every 5 seconds for a maximum of 60 seconds in my last tests, which did not work as expected. Maybe this also contributes to the 429 lockout I got.

On the bright side the get_transactions and get_account_statuses api calls work just like before - after successful authentication of course - so I hope this is true for all other methods as well. Even if we might want/need to switch to graphql at some point (if what @janezkranjc said is true) they will probably keep the old API around for a while.

To keep the token around between processes I would like to store it somewhere on the system, but since this token data is very security relevant I am not sure where to do this. My initial thought was to simply store it in a file on the disk since the config file contained all relevant info anyway. But since access to the token data would also eliminate the mfa I am not so keen on actually publishing such a solution. Any input on that @femueller or maybe others that have solved a similar problem before?

@markusressel
Copy link
Collaborator

Just a little warning to anyone trying this out: Its 6 hours since I got the "30 minutes" timeout and I still cant login even via the official app when using the same Internet IP so it seems like they block based on the IP. When I use a 4G connection it works as normal. Im curious how long I have to wait before my home Internet IP works again...

@svez01
Copy link

svez01 commented Sep 18, 2019

Thank you @Sjord, I am able to authenticate using the steps you describe. Has anyone been successful in doing a man in the middle proxy on a paired device, to see the 2fa steps?

@bbastou
Copy link

bbastou commented Sep 19, 2019

There is the man in the middle on a paired device, with password authentification :

  1. First step
    POST api.tech26.global/oauth2/token
    Body : grant_type=password&password=***&username=***
    Response :
{"userMessage":{"title":"L'authentification à deux facteurs est nécessaire. ","detail":"Veuillez choisir la seconde méthode d'authentification. "},"mfaToken":"***","error_description":"MFA token is required","detail":"MFA token is required","hostUrl":"https://api.tech26.de","type":"mfa_required","error":"mfa_required","title":"L'authentification à deux facteurs est nécessaire. ","message":"Veuillez choisir la seconde méthode d'authentification. ","userId":"***","status":403}
  1. Second step
    POST api.tech26.de/oauth2/token
    Body :
challenge=27C9DB20CFD4C3CF3E25F8F55C39FB6DBB0E5AC010F3B8036F685D971551A767
grant_type=mfa_osc
mfaToken=***obfucated***
nonce=fa6a9aa0-682a-49c6-8f5d-048879604cb7
signature.oneStepPayload=%7B%22nonce%22%3A%22fa6a9aa0-682a-49c6-8f5d-048879604cb7%22%2C%22grant_type%22%3A%22mfa_osc%22%2C%22challenge%22%3A%2227C9DB20CFD4C3CF3E25F8F55C39FB6DBB0E5AC010F3B8036F685D971551A767%22%7D
signature.value=DLgelMPLpaf8qyfTqBnZbT7mtIYc3e7FtX1r8yIH%2B5Fu3362UWRk%2BiT7v8ZHPnwAz8GfTlFtV9CEBcdIS1hvBxL4In9xXPV6ljtTCujST5yrRyygHJAGxOVcqHJN26ub3Z8nh5TOWkCvQ/wgUORnD%2BvIvWK2qPfJkV7CMU/5gunjmp8ykVOcSDL%2BQbsos8YhMg2C5JHOngNnfadk6LVZ0XqemC7NJLhvGCOLVBqogwMpB%2BkakdKgYYkSE065XZOsvMz3eHtChGZbFPaSpBShhRDxrrn%2BagfY87E5RyMwwKjCG9NWaDf%2Byc%2B3EPbFXUePHcZypfUSH0b%2B2gPXxLSxTg%3D%3D

Response :

{"access_token":"***","token_type":"bearer","refresh_token":"***","expires_in":1799,"scope":"trust","host_url":"https://api.tech26.de"}

Refresh Token
The app seems never refresh the refresh_token, I tested on a non-paired device and a paired device (iOS both).
But in the decompiled android Apk, I see this :

    @e
    @o("oauth2/token")
    A<com.n26.base.network.adapter.a<SessionErrorRaw, SessionRaw>> a(@j Map<String, String> map, @c("refresh_token") String str, @c("grant_type") String str2);

When I try to refresh my token with "grant_type=refresh_token" on oauth2/token, I get this response :

{
    "status": 401,
    "detail": "Refresh token not found!",
    "type": "invalid_grant",
    "userMessage": {
        "title": "error.oauth2.invalid_refresh_token.title",
        "detail": "error.oauth2.invalid_refresh_token.detail"
    },
    "error": "invalid_grant",
    "error_description": "Refresh token not found!"
}

⚠️ UPDATE : refresh token is used when you try to loggin with fingerprint, to get a mfa_token :
POST api.tech26.global/oauth2/token
Body : grant_type=refresh_token&refresh_token=*your_refresh_token*
Header : device-token: A23E0A17-DBDD-4B88-B0B1-*********412
The server send us the same response as before, with the mfa_token, to process the second step of auth.
The fingerprint has to be refresh every 30 days. So if we want to have only one OTP every 30 days,
we need to know how the app generate : challenge, nonce, signature.value and device-token

@mvmisha
Copy link

mvmisha commented Sep 20, 2019

@bbastou in step 2, the mfaToken is the mfaToken from the step 1 response?¿

@bbastou
Copy link

bbastou commented Sep 20, 2019

@bbastou in step 2, the mfaToken is the mfaToken from the step 1 response?¿

Yes, the first POST on api.tech26.global/oauth2/token is always done, and it always return a mfa error and a mfaToken, which is used for others api calls. Even with paired devices (password and touch id).

@mvmisha
Copy link

mvmisha commented Sep 20, 2019

@bbastou in step 2, the mfaToken is the mfaToken from the step 1 response?¿

Yes, the first POST on api.tech26.global/oauth2/token is always done, and it always return a mfa error and a mfaToken, which is used for others api calls. Even with paired devices (password and touch id).

Ok, im testing in postman and im getting this response,

{
"userMessage": {
"title": "Application error",
"detail": "The app seems to have a error, please update to the latest version."
},
"error_description": "Bad request",
"detail": "Bad request",
"type": "invalid_grant",
"error": "invalid_grant",
"title": "invalid_grant",
"status": 400
}

@bbastou
Copy link

bbastou commented Sep 20, 2019

@mvmisha Maybe it's a headers problem ?

There is the postman collection I made for sms OTP auth :
https://gist.github.com/bbastou/b97d57393961c2be487bf471c1c973ac

Don't forget to add your email/password in the env variable.
Hope it will help.

@mvmisha
Copy link

mvmisha commented Sep 20, 2019

@mvmisha Maybe it's a headers problem ?

There is the postman collection I made for sms OTP auth :
https://gist.github.com/bbastou/b97d57393961c2be487bf471c1c973ac

Don't forget to add your email/password in the env variable.
Hope it will help.

Yep, headers it was

The request at "Auth - Password - Valid OTP" will return the token to get other account information?

Thanks!

edit: Yeah, works fine getting transactions and account info.. BUT, the message that the client recives is in english.. not much of a problem, but if i login within the website the message is in spanish (in this case im in spain), any idea how to fix that? 🤔

@bbastou
Copy link

bbastou commented Sep 20, 2019

You're welcome :)

For french message, I add this header : Accept-Language: fr-FR;q=1, en-FR;q=0.9
You can try to play with this header to force spanish responses.

@markusressel
Copy link
Collaborator

markusressel commented Sep 20, 2019

I feel dumb for realising this so late, but my "longer than 30 minutes" IP block was caused by Home Assistant still using the N26 component and constantly trying to login using the old, now unsupported, way... So there seems to be no IP ban afaik. After disabling the n26 component in my Home Assistant instance and waiting a bit I was able to start testing again.

@bbastou 30 day login would be really nice, but still there has to be a way to refresh the token. I guess they only refresh it after it has expired which it does after 900 (or 1800) seconds. Did you check what happens after this time?

@nikolamilekic
Copy link

@markusressel My tests show that tokens last at least an hour, and that they definitely expire after some time. 15 hours and not minutes maybe?

@markusressel
Copy link
Collaborator

@nikolamilekic the toke has an expires_in property, which is time in seconds. In my case it was around 900, in the example posted above its actually 1800 so 30 minutes.

@nikolamilekic
Copy link

@markusressel I have a small app that polls the API every hour. I only store the refresh_token and generate a new access_token before doing anything else. It's been running now for close to 48h without issues.

@markusressel
Copy link
Collaborator

markusressel commented Sep 21, 2019

@nikolamilekic could you please elaborate:

  • what requests do you do after the initial 2FA authentication has been successful?
  • why do you store the refresh_token and how do you use it afterwards?

@nikolamilekic
Copy link

nikolamilekic commented Sep 21, 2019

The result of the 2FA authentication is an access_token which I completely ignore and a refresh_token which I store.

Then every hour I:

  • POST to https://api.tech26.de/oauth/token with "grant_type" = "refresh_token" and the refresh_token I have stored. This gets me an access_token and a new refresh_token that I store for the future.
  • I use the access_token to GET account info and transactions

This has been working for 48 hours now so I guess the refresh tokens don't expire after 900 seconds.

Let me know if you'd like to see the code I have so far (it's in F#).

@mvmisha
Copy link

mvmisha commented Sep 21, 2019

I think refresh tokens would still work after 90 days.. but I’m not sure about it

Those 90 days are within PSD2 regulations

I don’t know about N26, but other banks don’t requiere SCA if you already did it in that 90 day period, and it would be required also if you need to access account transactions that are older than 90 days.. but yeah the whole PSD2 thing is confusing and not all banks follow the rules

@bbastou
Copy link

bbastou commented Sep 21, 2019

@nikolamilekic the grand_type = refresh_token result on an error for me. I'd like to see the code you made to help me understand what is wrong with mine

@nikolamilekic
Copy link

@bbastou
Have a look here nikolamilekic/N26DirectImport@9448859

It's all the changes I did for this. What's missing is code to get the initial token. I did that by hand using the steps described above. Let me know if there's anything I can clarify.

@bbastou
Copy link

bbastou commented Sep 21, 2019

Thanks a lot @nikolamilekic

So the refresh_token didn't worked for me because I used the same api calls than the iOs application did. First call on api.tech26.global/oauth2/token, with Authorization Basic bmF0aXZlaW9zOg==. And then others calls on api.tech26.global/oauth2/token.
With this, you have an acces_token that expires in 1799, but you can't refresh it.

If you make all your api call on api.tech26.global/oauth/token (oauth instead of oauth2), and Authorization Basic bXktdHJ1c3RlZC13ZHBDbGllbnQ6c2VjcmV0, your token expires in 899, but you can refresh it.

@markusressel
Copy link
Collaborator

Thx @bbastou, it is also a combination of using the *.global domain and the *.de domain (at least for me) but I got it functional now. We are trying to get this into a release asap but there are still some things to consider. Thx again for the GREAT community support, we would not have been able to achieve this without you 👍

@julian-klode
Copy link

I don't think you actually need the .global domain. It certainly works fine for me with https://api.tech26.de/oauth/token. I can do refresh tokens, even, and they last at least 1h40m (will see tomorrow if current one is still valid).

@markusressel
Copy link
Collaborator

@julian-klode hmm I may have confused an error I got due to a failed refresh of a token I had not refreshed for a couple of hours.

I assume your "last at least" refers to the access token that has been refreshed in the meantime? Or do you refer to the time the user has to refresh a token even after it has expired?

@julian-klode
Copy link

julian-klode commented Sep 24, 2019

@markusressel So, I stored the refresh token, waited 1h40m and then could get a new access token (and new refresh token) for it. It's not entirely clear how long the refresh token lasts, the last one from like 23:00 was expired at 10:00 this morning.

My login workflow is to try to do a refresh, and if that fails, I login again.

@markusressel
Copy link
Collaborator

@julian-klode thats how its supposed to be done. Afaik you dont have to refresh the token if it has not yet expired, but you will not be able to refresh it if it has expired for too long (like you said and I experienced too). But for a continuous application like home assistant this should not matter, since it will make API calls at least every 30 minutes, so the refresh should always work.

@julian-klode
Copy link

ack. In my case I have a script I run manually sometimes, hence it times out. But oh well, I could probably make it an hourly refresh Cron job.

@markusressel
Copy link
Collaborator

This is now part of the 2.0.0 release

@matthewmueller
Copy link

matthewmueller commented Oct 16, 2019

Thanks for all the hard work everyone – has anyone been able to get this working reliably without manual intervention? I've tried an hourly cron, but it seems like I eventually get:

Refresh token not found!

Any ideas?

@hajdbo
Copy link

hajdbo commented Oct 17, 2019

Same here, the token usually only works for 4 hours until I need to do the manual intervention again. This probably needs to be re-opened.

@markusressel
Copy link
Collaborator

I tested this in a terminal loop and can confirm that after 2 hours I had to reauthenticate. We will not reuse this issue for that Topic though and create a new one.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.