trakt / api-help

Trakt API docs at https://trakt.docs.apiary.io
182 stars 7 forks source link

Unable to refresh access tokens #48

Open XanderStrike opened 5 years ago

XanderStrike commented 5 years ago

I've been banging my head against the wall on this and I'm hoping y'all can look at logs on your side or something to illuminate the problem.

Locally, everything works fine and when a user's access key is too old I'm able to get a new key using the refresh_key.

However in production I'm getting the following:

2019/03/19 01:42:57 Got a 401 Unauthorized, full response:
&{401 Unauthorized 401 HTTP/2.0 2 0 map[X-Frame-Options:[SAMEORIGIN] Date:[Tue, 19 Mar 2019 01:43:37 GMT] Cache-Control:[no-store] Pragma:[no-cache] Cf-Ray:[4b9bc1ab7a1720ea-LAX] Vary:[Accept-Encoding] X-Xss-Protection:[1; mode=block] Www-Authenticate:[Bearer realm="Doorkeeper", error="invalid_grant", error_description="The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client."] X-Content-Type-Options:[nosniff] X-Request-Id:[7df331ea-d463-46dd-9a1a-6308ef477da1] X-Runtime:[0.003941] Content-Type:[application/json; charset=utf-8] Content-Length:[213] Set-Cookie:[__cfduid=d6df011396a30b21c3925e7d87338f2561552959817; expires=Wed, 18-Mar-20 01:43:37 GMT; path=/; domain=.trakt.tv; HttpOnly _traktsession=MmpXUjI3Y2w5TUoySXZTS1M2Vk14RjZsU0xhTEFUbnEzNHNLR1FoamNTT0N1cmczSldsenNwT0FXQ0JGN2plTmpWb2ZLUDdBRDV1TUsxaktka2tZK3c9PS0tb1ZGVFRvcUNKSVgvSkJWd1R6a2todz09--bd643d4fefa7f60632c5c5111f47955acc595ca3; path=/; HttpOnly] Cf-Railgun:[9be4ed67ab 99.99 0.085235 0030 57da] Expect-Ct:[max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"] Server:[cloudflare]] {0xc42052c000} 213 [] false false map[] 0xc4201e8300 0xc420112370}

It scrolls to the right for quite a ways but I like to be thorough. The important bit is "The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client."

Unfortunately, this doesn't really tell me anything useful. Since it works locally but not on production, one would think it's probably a redirection URI problem, but initial authorization requests work fine and they use the same code for determining redirect_uri.

Specifically here is where the initial auth's happen and here is where refreshes happen . You can find AuthRequest here.

Really it'd be super helpful just to know for what reason I'm getting the 401 unauthorized.

GameProgramer commented 5 years ago

I'm also getting the same error, starting about 3-4 days ago. Getting a new token works for that session, but fails the next time.

`[03-21 04:11:01] Failed. (Error sending data to URL [https://api.trakt.tv/oauth/token] (401) (HTTP/1.1 401 Unauthorized Date: Thu, 21 Mar 2019 11:11:01 GMT Content-Type: application/json; charset=utf-8 Content-Length: 213 Connection: keep-alive Cache-Control: no-store Cf-Railgun: 3833f389a5 1.87 0.177079 0030 57da Pragma: no-cache Set-Cookie: _traktsession=a0JlOXBaSkNFcGo0THZxbHJOTy9SQThJeGZYdmc1UUs0S3FydVB5RHJCbm1POGttSlhobDdJQ2hhWG5NUUJhd3ltMi9zT1A0S2ZHOXdpVU5NS0dQaEE9PS0tZmF4RXBzUTZJaUdURHpiNk1jWWlzZz09--7318f238f24f6847499b74713d414a50047877ed; path=/; HttpOnly Vary: Accept-Encoding Www-Authenticate: Bearer realm="Doorkeeper", error="invalid_grant", error_description="The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client." X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN X-Request-Id: 0fc40662-0ecd-4ed5-a2c1-eaf922ea8cbf X-Runtime: 0.004839 X-Xss-Protection: 1; mode=block Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" Server: cloudflare CF-RAY: 4baf7b8dcd7f6d9c-SJC

))`

rectifyer commented 5 years ago

This error means the original access_token has already been revoked, so the refresh_token is also invalid and can't be used. An access_token gets revoked if the user revokes it from the Trakt website or it gets revoked once the associated refresh_token is used (basically you can't refresh more than once).

To confirm this, I'd try creating a brand new token then refresh it and that should work. I haven't had any other reports about this, so hopefully it is just the scenario I mentioned above.

XanderStrike commented 5 years ago

Thanks for getting back to me! I've confirmed that brand new tokens refresh as expected.

Just to clarify, the refresh_token will also become invalid after 3 months? The service currently does a just-in-time refresh where if the token pair is beyond a certain age (IIRC it's 60 days). So if one of my users doesn't hit the service during that grace period, they'll have to go through the auth flow again and get a brand new pair. Is that correct?

That's a bummer because I'll need a scheduled job to get new tokens out of band with the requests coming in. Not a deal breaker at all, but the docs don't make that clear. Namely this section:

Use the refresh_token to get a new access_token without asking the user to re-authenticate. The access_token is valid for 3 months before it needs to be refreshed again.

makes it seem like the refresh should happen after the 3 month period. I suppose I got lucky by being overly cautious, but it'd be awesome if the docs could be updated to reflect that the whole pair becomes invalid after 3 months, and you must refresh before that period is over.

Also, would it be possible to get different errors for invalid, expired, and revoked so that I can key off those and take appropriate action? I'm burning a bunch of API requests on these revoked keys and I'm sure we'd both appreciate lightning the load a little 😄

XanderStrike commented 5 years ago

Oops

rectifyer commented 5 years ago

We use a library that follows the OAuth spec, so I'll need to look into that more and see what it does specifically in regards to the refresh token.

MaxHasADHD commented 4 years ago

I'm running into this issue as well. I've always read it as the refresh_token is always valid, and then after the expires_at date is matched or exceeded I'll refresh. I thought this worked fine but some users have reported my app not syncing and then in their logs I realize they aren't logged in. Just came to the forums to ask about that, glad this is here though already.

castaway commented 4 years ago

Can you not periodically call refresh (monthly maybe) to get a new access/refresh token?

XanderStrike commented 4 years ago

That requires a framework for doing scheduled jobs and a way to store the tokens. That's a lot more work and means that stateless or serverless "api glue" type applications can't work. My app was designed to refresh tokens during a request but that approach fails if a user doesn't hit it during the window where the tokens are valid. So now you have to add (and pay for!) some kind of database, and some code around background scheduled jobs.

Plus since I've added storage I've saved over 8k access tokens (user signups effectively), so once a month I hammer the trakt api with about 2k requests to refresh tokens I may never even use again. Just seems wasteful to me. Especially given how low-stakes the API all this security is protecting, what's someone gonna do if your key leaks? Mess up your watchlist?

castaway commented 4 years ago

That's true, nevertheless I think its how oauth was designed to be used.. you can only refresh within the not-expired period.

sttz commented 4 years ago

If you look at the OAuth 2.0 RFC, it's clear the expiration token is intended to be used after the access token expired. Look at the flow illustrated in figure 2: https://tools.ietf.org/html/rfc6749#section-1.5

But you can't rely on the refresh succeeding. E.g. a password change usually also invalidates all access and refresh tokens. In case the refresh fails, the user has to authorize again.

In my experience, refreshing has been working fine. I just tried and was able to refresh an expired access token. Here's the HAR of the request with sensitive and some irrelevant stuff removed:

Request JSON ```json { "startedDateTime": "2020-03-31T10:17:20.894Z", "time": 299.007999999958, "request": { "method": "POST", "url": "https://api.trakt.tv/oauth/token", "httpVersion": "http/2.0", "headers": [ { "name": ":method", "value": "POST" }, { "name": ":authority", "value": "api.trakt.tv" }, { "name": ":scheme", "value": "https" }, { "name": ":path", "value": "/oauth/token" }, { "name": "content-length", "value": "320" }, { "name": "dnt", "value": "1" }, { "name": "trakt-api-key", "value": "API_KEY" }, { "name": "trakt-api-version", "value": "2" }, { "name": "authorization", "value": "Bearer ACCESS_TOKEN" }, { "name": "content-type", "value": "application/json" }, { "name": "user-agent", "value": "USER_AGENT" }, { "name": "sec-fetch-dest", "value": "empty" }, { "name": "accept", "value": "*/*" }, { "name": "origin", "value": "https://www.crunchyroll.com" }, { "name": "sec-fetch-site", "value": "cross-site" }, { "name": "sec-fetch-mode", "value": "cors" }, { "name": "referer", "value": "https://www.crunchyroll.com/" }, { "name": "accept-encoding", "value": "gzip, deflate, br" }, { "name": "accept-language", "value": "en,en-US;q=0.9" } ], "queryString": [], "cookies": [], "headersSize": -1, "bodySize": 320, "postData": { "mimeType": "application/json", "text": "{\"refresh_token\":\"REFRESH_TOKEN\",\"client_id\":\"CLIENT_ID\",\"client_secret\":\"CLIENT_SECRET\",\"redirect_uri\":\"https://www.crunchyroll.com\",\"grant_type\":\"refresh_token\"}" } }, "response": { "status": 200, "statusText": "", "httpVersion": "http/2.0", "headers": [ { "name": "status", "value": "200" }, { "name": "date", "value": "Tue, 31 Mar 2020 10:17:21 GMT" }, { "name": "content-type", "value": "application/json; charset=utf-8" }, { "name": "set-cookie", "value": "_traktsession=SESSION; path=/; HttpOnly" }, { "name": "access-control-allow-origin", "value": "https://www.crunchyroll.com" }, { "name": "cache-control", "value": "no-store" }, { "name": "pragma", "value": "no-cache" }, { "name": "vary", "value": "Accept-Encoding" }, { "name": "x-content-type-options", "value": "nosniff" }, { "name": "x-frame-options", "value": "SAMEORIGIN" }, { "name": "x-xss-protection", "value": "1; mode=block" }, { "name": "server", "value": "cloudflare" }, { "name": "content-encoding", "value": "br" } ], "cookies": [ { "name": "_traktsession", "value": "SESSION", "path": "/", "expires": null, "httpOnly": true, "secure": false } ], "content": { "size": 250, "mimeType": "application/json" }, "redirectURL": "", "headersSize": -1, "bodySize": -1, "_transferSize": 691 }, "cache": {}, "timings": { "blocked": 139.02900000013364, "dns": -1, "ssl": -1, "connect": -1, "send": 0.119, "wait": 159.61699999941467, "receive": 0.24300000040966552, "_blocked_queueing": 138.90600000013364 }, "serverIPAddress": "104.20.81.229", "_priority": "High", "_resourceType": "fetch", "connection": "836", "pageref": "page_1" } ```

From a quick glance I can see two differences that shouldn't but maybe do matter?

lswee commented 1 year ago

Can someone from Trakt please confirm how long the refresh_token is valid and update the documentation accordingly?

tysonkerridge commented 1 year ago

@lswee The docs state

The access_token is valid for 3 months. Save and use the refresh_token to get a new access_token without asking the user to re-authenticate.

If you generate/refresh a token you get an expires_in value of ~7776000 (seconds) which is 90 days aka ~3 months.

mpfc75 commented 1 year ago

I think it's clear the access token is valid for 90 days.

What is a bit unclear is how long a refresh token is valid for? Is it 90 days too or longer? I think the ask is also for the doc to specify the refresh token validity time.

tysonkerridge commented 1 year ago

Sorry, my mistake. I believe it does not expire unless access is revoked, eg. by the user. That would kind of defeat the purpose, but I suppose I can't answer that for sure.

Edit: According to Oauth, as I understand it, it's meant to be unlimited but it's otherwise not necessary to know the refresh expiry as it makes no difference to how it's handled.

You might notice that the “expires_in” property refers to the access token, not the refresh token. The expiration time of the refresh token is intentionally never communicated to the client. This is because the client has no actionable steps it can take even if it were able to know when the refresh token would expire. There are also many reasons refresh tokens may expire prior to any expected lifetime of them as well.

If a refresh token expires for any reason, then the only action the application can take is to ask the user to log in again, starting a new OAuth flow from scratch, which will issue a new access token and refresh token to the application. That’s the reason it doesn’t matter whether the application knows the expected lifetime of the refresh token, because regardless of the reason it expires the outcome is always the same.

rectifyer commented 1 year ago

Yes, that should be how it works. The refresh token is valid until it is revoked. It could be revoked by the user or revoked once it is used and a new access token + refresh token is generated.

lswee commented 1 year ago

Fantastic, that's exactly what and why I was asking, thank you.