Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auth support to [Reddit] badges #10790

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

PyvesB
Copy link
Member

@PyvesB PyvesB commented Jan 5, 2025

Fixes #9256.

@PyvesB PyvesB added the service-badge New or updated service badge label Jan 5, 2025
Copy link
Contributor

github-actions bot commented Jan 5, 2025

Warnings
⚠️ This PR modified the server but none of its tests.
That's okay so long as it's refactoring existing code.
Messages
📖 ✨ Thanks for your contribution to Shields, @PyvesB!
📖

Thanks for contributing to our documentation. We ❤️ our documentarians!

Generated by 🚫 dangerJS against 5a5e06e


// Abstract class for Reddit badges
// Authorization flow based on https://github.com/reddit-archive/reddit/wiki/OAuth2#application-only-oauth.
export default class RedditBase extends BaseJsonService {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is partly based on https://github.com/badges/shields/blob/master/services/twitch/twitch-base.js. However:

  • there are some annoying differences compared to Twitch's OAuth flow.
  • the logic in the Twitch code is partly broken. For example, the loop here does not refresh the request object, so we're querying new access tokens but still using the old one in our retries, which defeats the whole purpose of the retry loop. Also, I noticed whilst experimenting that err.response could be undefined in some cases, making the conditional blow up.

I decided to just try to make things work for Reddit as a fist iteration, and will came back to rework things at a later point.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah agreed. Last time I looked at the code for twitch I thought there are some issues that want fixing. Lets not copy the approach.

@@ -24,6 +24,7 @@ Production hosting is managed by the Shields ops team:
| Cloudflare (CDN) | Access management | @espadrine |
| Cloudflare (CDN) | Admin access | @calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB |
| Twitch | OAuth app | @PyvesB |
| Reddit | OAuth app | @chris48s, @PyvesB |
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reused the account that had been created alongside #9817 (comment) and shared the login details in Keybase.

@PyvesB
Copy link
Member Author

PyvesB commented Jan 5, 2025

The credentials are not being picked up as this is from a fork, so we're getting the existing state from the master branch, i.e. tests failing. They pass locally when credentials are configured, I guess we can simply merge this once the review is completed, and see what happens.

@chris48s
Copy link
Member

chris48s commented Jan 6, 2025

Couple of questions on the token:

  1. Do you know what rate limit we get if we authenticate with a token?

  2. Have you made separate credentials for CI/staging/prod or have you set the same ones on all of them. Where possible, I think its preferable to have different credentials for each environment. Can we set up 3 OAuth apps under one account?

@@ -29,8 +28,6 @@ export default class RedditSubredditSubscribers extends BaseJsonService {
},
}

static _cacheLength = 7200
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the question about what rate limit we get with authentication, are we confident that we'd have enough rate limit to get away with completely ditching this?

Comment on lines +4 to +9
const tokenSchema = Joi.object({
access_token: Joi.string().required(),
expires_in: Joi.number(),
scope: Joi.string(),
token_type: Joi.string(),
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expires_in value is used. The scope and token_type values are discarded. Suggest

const tokenSchema = Joi.object({
  access_token: Joi.string().required(),
  expires_in: Joi.number().required(),
}).required()

here.

Comment on lines +51 to +56
const timeout = setTimeout(() => {
RedditBase._redditToken = this._getNewToken()
}, replaceTokenMs)

// do not block program exit
timeout.unref()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach using setTimeout() isn't wrong, and I'm not saying we have to change it.
That said, just as a data point: We implemented something quite similar to this in the JWT auth helper.

static _isJwtValid(expiry) {
// we consider the token valid if the expiry
// datetime is later than (now + 1 minute)
return dayjs.unix(expiry).isAfter(dayjs().add(1, 'minutes'))
}
async _getJwt(loginEndpoint) {
const { _user: username, _pass: password } = this
// attempt to get JWT from cache
if (
jwtCache?.[loginEndpoint]?.[username]?.token &&
jwtCache?.[loginEndpoint]?.[username]?.expiry &&
this.constructor._isJwtValid(jwtCache[loginEndpoint][username].expiry)
) {
// cache hit
return jwtCache[loginEndpoint][username].token
}
// cache miss - request a new JWT
const originViolation = !this.isAllowedOrigin(loginEndpoint)
if (originViolation) {
throw new InvalidParameter({
prettyMessage: 'requested origin not authorized',
})
}
const { buffer } = await checkErrorResponse({})(
await fetch(loginEndpoint, {
method: 'POST',
form: { username, password },
}),
)
const json = validate(
{
ErrorClass: InvalidResponse,
prettyErrorMessage: 'invalid response data from auth endpoint',
},
parseJson(buffer),
Joi.object({ token: Joi.string().required() }).required(),
)
const token = json.token
const expiry = this.constructor._getJwtExpiry(token)
// store in the cache
if (!(loginEndpoint in jwtCache)) {
jwtCache[loginEndpoint] = {}
}
jwtCache[loginEndpoint][username] = { token, expiry }
return token
}
async _getJwtAuthHeader(loginEndpoint) {
if (!this.isConfigured) {
return undefined
}
const token = await this._getJwt(loginEndpoint)
return { Authorization: `Bearer ${token}` }
}

One thing that is quite nice about that approach is that the control flow is a bit more linear.
Dunno if you reckon that is any better/worse? If you just want to roll with this then I think it is ok.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
service-badge New or updated service badge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Reddit API Changes
2 participants