KartikTalwar / Duolingo

Unofficial Duolingo API Written in Python
MIT License
822 stars 129 forks source link

unable to login with api, no returns #128

Open KBretz77 opened 1 year ago

KBretz77 commented 1 year ago

image

Ever since February 16th, I've been unable to access the API, even after verifying my username/password. I keep getting this exception:

Traceback (most recent call last): File "[USER SCRIPT] ", line 10, in lingo = duolingo.Duolingo('[USER], [PW]') File "[USER ENV] \lib\site-packages\duolingo.py", line 66, in init self._login() File "[USER ENV] \lib\site-packages\duolingo.py", line 105, in _login attempt = request.json() File "[USER ENV] \lib\site-packages\requests\models.py", line 975, in json raise RequestsJSONDecodeError(e.msg, e.doc, e.pos) requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

zener82 commented 1 year ago

Hello, I experience the same troubles since few days. Was working fine before.

File "/usr/lib/python3.10/site-packages/duolingo.py", line 66, in __init__ self._login() File "/usr/lib/python3.10/site-packages/duolingo.py", line 105, in _login attempt = request.json() File "/usr/lib/python3.10/site-packages/requests/models.py", line 910, in json return complexjson.loads(self.text, **kwargs) File "/usr/lib/python3.10/json/__init__.py", line 346, in loads return _default_decoder.decode(s) File "/usr/lib/python3.10/json/decoder.py", line 337, in decode obj, end = self.raw_decode(s, idx=_w(s, 0).end()) File "/usr/lib/python3.10/json/decoder.py", line 355, in raw_decode raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

From my understanding, it seems that the API cannot get any answer from the server with the API login function (or maybe bad formatted response? ). No change on my side with code that was working well before. One possibility is maybe a server protection, I was requesting data fro 3 accounts once an hour...

igorskh commented 1 year ago

I think they've changed login procedure, I've just checked the login procedure on the web version, there are some additional fields in the login request. I didn't look closer, but I assume it's some sort of protection.

JASchilz commented 1 year ago

My experience lately is that this library is abandoned (which I'm not complaining about), so we're probably on our own in working through this issue.

@igorskh mentioned additional fields in the login request. I also see additional fields (although I'm not really sure what was there before):

image

And to me this also looks like additional authentication details to exclude unauthorized clients. Accessing Duolingo via this library has always kind of felt like an unauthorized hack, and now it feels to me like Duolingo might be closing that door.

flyinggoatman commented 1 year ago

That really sucks!

I'm having the same issue.

sphanley commented 1 year ago

And to me this also looks like additional authentication details to exclude unauthorized clients. Accessing Duolingo via this library has always kind of felt like an unauthorized hack, and now it feels to me like Duolingo might be closing that door.

Yeah, if you look through the calls being made on the login page prior to that call to login?fields=, you'll see that they've added recapcha authentication, and the value provided from recapcha is required to log in. It seems clear that this is overtly intended to prevent logins except through the official clients. Unfortunately, projects dependent on this library are likely dead unless Duolingo decides to provide an official API down the road.

igorskh commented 1 year ago

JWT authentication still works.

sphanley commented 1 year ago

JWT authentication still works.

Is there a way to acquire a token other than manually extracting one from a browser session, though?

igorskh commented 1 year ago

JWT authentication still works.

Is there a way to acquire a token other than manually extracting one from a browser session, though?

No easy way that I know of. However, at the moment it's sufficient to get it only once, since they practically don't expire.

KBretz77 commented 1 year ago

Thanks so much everyone! I'm glad it wasn't just me and that it's sourced from Duolingo. I agree that the API call did seem like low-key hacking, so it doesn't surprise me that they have closed that loophole. I'll just grab the XP progress and other details from my user page for now and submit a request to Duolingo for them to create their own API.

flyinggoatman commented 1 year ago

JWT authentication still works.

How do you use this method?

JASchilz commented 1 year ago

@KBretz77 you might consider re-opening the issue. Even if you're not hoping for a solution, leaving the issue open serves as a helpful notice for anyone trying to use the library who runs into this problem. :)

KBretz77 commented 1 year ago

That's a good point, thanks @JASchilz

marvinscham commented 1 year ago

@flyinggoatman I fixed this for my usecase by removing self.jwt = None at https://github.com/KartikTalwar/Duolingo/blob/master/duolingo.py#L100, which then allowed me to instantiate like lingo = duolingo.Duolingo(username='myUsername', jwt='myJWT')

You can grab your JWT by logging in on duolingo.com and then running this JavaScript, which will output the token into the console: document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11);

AJRepo commented 1 year ago

The solution by @marvinscham worked for me. Since jwt=None by default - I don't see why L100 is there.

flyinggoatman commented 1 year ago

Has he committed the solution yet?

aandriella commented 1 year ago

@flyinggoatman I fixed this for my usecase by removing self.jwt = None at https://github.com/KartikTalwar/Duolingo/blob/master/duolingo.py#L100, which then allowed me to instantiate like lingo = duolingo.Duolingo(username='myUsername', jwt='myJWT')

You can grab your JWT by logging in on duolingo.com and then running this JavaScript, which will output the token into the console: document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11);

For me still does not work. I get "Login failed".

flyinggoatman commented 1 year ago

They’ve probably patched it again... Let me check my code.

From: Antonio @.> Sent: 23 May 2023 16:43 To: @.> Cc: @.>; @.> Subject: Re: [KartikTalwar/Duolingo] unable to login with api, no returns (Issue #128)

@flyinggoatmanhttps://github.com/flyinggoatman I fixed this for my usecase by removing self.jwt = None at https://github.com/KartikTalwar/Duolingo/blob/master/duolingo.py#L100, which then allowed me to instantiate like lingo = duolingo.Duolingo(username='myUsername', jwt='myJWT')

You can grab your JWT by logging in on duolingo.com and then running this JavaScript, which will output the token into the console: document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11);

For me still does not work. I got "Login failed".

— Reply to this email directly, view it on GitHubhttps://github.com/KartikTalwar/Duolingo/issues/128#issuecomment-1559698203, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AAZ2VUS2Q2IL244H4YSEJ6TXHTLLRANCNFSM6AAAAAAVAWDJ5E. You are receiving this because you were mentioned.Message ID: @.***>

Vag-Soft commented 1 year ago

I used this code to modify the Duolingo API to make JWT authorization work. It's still working for me to this day.

import inspect

source = inspect.getsource(duolingo)
new_source = source.replace('jwt=None', 'jwt')
new_source = source.replace('self.jwt = None', ' ')
exec(new_source, duolingo.__dict__)

@flyinggoatman I fixed this for my usecase by removing self.jwt = None at https://github.com/KartikTalwar/Duolingo/blob/master/duolingo.py#L100, which then allowed me to instantiate like lingo = duolingo.Duolingo(username='myUsername', jwt='myJWT') You can grab your JWT by logging in on duolingo.com and then running this JavaScript, which will output the token into the console: document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11);

For me still does not work. I get "Login failed".

aandriella commented 1 year ago

They’ve probably patched it again... Let me check my code. From: Antonio @.> Sent: 23 May 2023 16:43 To: @.> Cc: @.>; @.> Subject: Re: [KartikTalwar/Duolingo] unable to login with api, no returns (Issue #128) @flyinggoatmanhttps://github.com/flyinggoatman I fixed this for my usecase by removing self.jwt = None at https://github.com/KartikTalwar/Duolingo/blob/master/duolingo.py#L100, which then allowed me to instantiate like lingo = duolingo.Duolingo(username='myUsername', jwt='myJWT') You can grab your JWT by logging in on duolingo.com and then running this JavaScript, which will output the token into the console: document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11); For me still does not work. I got "Login failed". — Reply to this email directly, view it on GitHub<#128 (comment)>, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AAZ2VUS2Q2IL244H4YSEJ6TXHTLLRANCNFSM6AAAAAAVAWDJ5E. You are receiving this because you were mentioned.Message ID: @.***>

@flyinggoatman did you have the time to check if that is still working for you?

TiloGit commented 1 year ago

tested today and with the changes

replace('jwt=None', 'jwt')
replace('self.jwt = None', ' ')

and use jwt token worked.

here my main.py for the AWS Lambda function (username,pw and jwt token are Environment Variables of Lambda)

def item_already_equipped(lingo, item):
    if item == 'streak_freeze':
        return lingo.__dict__['user_data'].__dict__['tracking_properties']['num_item_streak_freeze'] > 0
    if item == 'rupee_wager':
        return lingo.__dict__['user_data'].__dict__['tracking_properties']['has_item_rupee_wager']

def main(a, b):
    import duolingo, os

    username = os.environ['usernames']
    password = os.environ['passwords']
    jwt = os.environ['jwt']    

    print("Test__Tilo__Here")
    print("environment variable: " + os.environ['usernames'])
    print("loaded jwt (first 10 char: " + jwt[:10])
    print("loaded one: " + username)

    try:
        #lingo = duolingo.Duolingo(username, password)
        lingo = duolingo.Duolingo(username=os.environ['usernames'], jwt=os.environ['jwt'])
    except ValueError:
        raise Exception("Username or login Invalid")

#here the test/report stuff
    print("---InfoPart---Start---")
    Mylanguages = lingo.get_languages()
    print(username + " get_languages for " + str(Mylanguages))
    streak_info = lingo.get_streak_info()
    print(username +" get_streak_info for " + str(streak_info))
    MyInfo = lingo.get_user_info()
    print(username +" Info ID: " + str(MyInfo["id"]))
    print(username +" Info fullname: " + str(MyInfo["fullname"]))
    print(username +" Info location: " + str(MyInfo["location"]))
    print(username +" Info contribution_points: " + str(MyInfo["contribution_points"]))
    print(username +" Info created: " + str.strip(MyInfo["created"]))
    print(username +" Info learning_language_string: " + str(MyInfo["learning_language_string"]))
    print(username +" streak_freeze: " + str(lingo.__dict__['user_data'].__dict__['tracking_properties']['num_item_streak_freeze']))
    print(username +" rupee_wager: " + str(lingo.__dict__['user_data'].__dict__['tracking_properties']['has_item_rupee_wager']))
    user_data_resp = lingo.get_data_by_user_id()
    print(username +" Info lingots: " + str(user_data_resp['lingots']))
    print(username +" Info totalXp: " + str(user_data_resp['totalXp']))
    print(username +" Info monthlyXp: " + str(user_data_resp['monthlyXp'])) 
    print(username +" Info weeklyXp: " + str(user_data_resp['weeklyXp'])) 
    print(username +" Info gems: " + str(user_data_resp['gems']))
    print(username +" Info currentCourse.crowns: " + str(user_data_resp['currentCourse']['crowns']))
    print("---InfoPart---End---")

#new buy stuff 2020-08-31
    stuff_to_purchase = ['streak_freeze', 'rupee_wager']

    for item in stuff_to_purchase:
        if(item_already_equipped(lingo, item)):
            print("Item "+ item + " already equipped! Skipping...")
            continue
        try:
            print("Trying to Buy " + item + " for " + username)
            lingo.buy_item(item, 'es')
            print("Bought " + item + " for " + username)
        except duolingo.AlreadyHaveStoreItemException: # no longer triggered AFAIK
            print("Item Already Equipped")
        except Exception:
            raise ValueError("Unable to buy " + item)

cheers.

tier61wro commented 1 year ago

Hello everyone,

Like you, I was quite disheartened when the Duolingo API library stopped working and was giving an authorization error. I am a volunteer. I use Duolingo to teach languages to refugee children who have fled from the war in Ukraine. Therefore, discovering that the library was broken was quite distressing.

By examining how Duolingo operates through the browser console, I managed to create a minor fix, and now everything is running smoothly on my end. Essentially, I changed the login_url and the parameters passed in the request to obtain the authorization token.

I have uploaded a version with the fix in this repository: https://github.com/tier61wro/Duolingo. Perhaps some of you will find this useful!

I'm not confident that my fix will work forever, so I don't think it makes sense to make a merge request into the main repository at this point.

Best regards, Alex

0xPorkchops commented 1 year ago

I've found that there is still an unauthenticated endpoint that allows you to retrieve user data. Perhaps there are more methods that can be called on this endpoint, but I am only interested in some public user data. https://www.duolingo.com/2017-06-30/users?username=USERNAME-HERE

flyinggoatman commented 1 year ago

I've found that there is still an unauthenticated endpoint that allows you to retrieve user data. Perhaps there are more methods that can be called on this endpoint, but I am only interested in some public user data. https://www.duolingo.com/2017-06-30/users?username=USERNAME-HERE

Fuck that's dangurous. My email is just... there...

filipre commented 1 year ago

Fuck that's dangurous. My email is just... there...

I played around with it and it seems like the endpoint returns different data for your own username depending on whether you are logged in or not. They use cookies to authenticate the API request. So your email and phone number are not really exposed, even though it looks like it.

mbrookes commented 11 months ago

@tier61wro Thanks for sharing it, but unfortunately your fix doesn't work for me:

Traceback (most recent call last):
  File "/Users/matt/Projects/duolingo/test.py", line 3, in <module>
    lingo  = duolingo.Duolingo('username', 'password')
  File "/Users/matt/Projects/duolingo/duolingo.py", line 66, in __init__
    self._login()
  File "/Users/matt/Projects/duolingo/duolingo.py", line 108, in _login
    self.jwt = request.headers['jwt']
  File "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/requests/structures.py", line 52, in __getitem__
    return self._store[key.lower()][1]
KeyError: 'jwt'

I was able to use a combination of @Vag-Soft's patch https://github.com/KartikTalwar/Duolingo/issues/128#issuecomment-1559963434 with @flyinggoatman's instantiation https://github.com/KartikTalwar/Duolingo/issues/128#issuecomment-1559710952 though, which is good enough for my personal use.

dlo commented 10 months ago

To make this super straightforward, here's the full fix based on @mbrookes's recommendation, which worked for me (thank you!):

  1. Log into Duolingo web.
  2. Open the console and paste in document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11); to retrieve your JWT.
  3. Use the following code to use the API:

    import duolingo
    import inspect
    
    source = inspect.getsource(duolingo)
    new_source = source.replace('jwt=None', 'jwt')
    new_source = source.replace('self.jwt = None', ' ')
    exec(new_source, duolingo.__dict__)
    
    lingo  = duolingo.Duolingo('YOUR_USERNAME', jwt='YOUR_JWT_FROM_ABOVE')
ExMacro commented 6 months ago

Hi! Nice work with this fix! I managed to download and started the UI setup. By running the code document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11); does return a long string of characters but unfortunately the integration doesn't login with that.

Any ideas?

ExMacro commented 6 months ago

Hi! Nice work with this fix! I managed to download and started the UI setup. By running the code document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)'))[0].slice(11); does return a long string of characters but unfortunately the integration doesn't login with that.

Any ideas?

I will answer to myself. The "username" in the login is not the login email address but the user profile -> user name just above "joined September 2022" etc. info in the Duolingo web site. Now working!