Exclusive episodes on the RSS feed: <podcast:oauthEndpoints> #726
dmathewwws
started this conversation in
Tag Proposal
Replies: 1 comment 2 replies
-
Cool idea, how do player benefit? Like with secureRSS the player can set their own commission and it gets taken at purchase when the user pays in app. The biggest issue I have is that from my understanding the listeners detailed get passed to the hosting provider which then could allow them to launch their own app and spam all the listeners (I could be wrong - but a hosting company already did that once to podcast creators) |
Beta Was this translation helpful? Give feedback.
2 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
Currently an RSS feed is a public document and all enclosures links to audio or video files are publicly accessible as well. This proposal deals with how to have exclusive audio or video that is only accessible to paying users in a public RSS feed.
From a podcast creators’s perspective, I want to be able to earn money from exclusive episodes or early access to my episodes. Currently, I can use Patreon, Supercast etc and ask my users to leave their podcast player, signup and pay through their service, and then return with an exclusive RSS feed and add it to their podcast player.
From a podcast listener's perspective, I don’t enjoy the technical challenge and hassle of leaving my podcast player and adding a custom RSS feed.
Proposal
To allow for exclusive content on a public RSS feed:
What tags are needed:
<podcast:oauthEndpoints>
: Contains all information needed to get and exchange tokens needed to consume exclusive content<podcast:oauthSignupUrl>
: The url to sign up for a client_id and client_secret, and register your callback_url<podcast:oauthAuthorizeUrl>
: The 1st step in OAuth. The url to get an authorization code.<podcast:oauthTokenUrl>
: ****The 2nd step in OAuth. The url to exchange an authorization code for an access token and a refresh token.<podcast:oauthNewAccessTokenUrl>
: Once you have a refresh token, you can exchange it for a new access token. Access tokens are short lived ~ 2 hours.<podcast:oauthNewRefreshTokenUrl>
: Once you have a refresh token, you can exchange it for a new refresh token. Refresh tokens are longer lived. ~ >30 days. If a user does not have a valid refresh token, they will need go through the OAuth flow again to get a valid refresh token.<podcast:oauthNewContentTokenUrl>
: Once you have an access token, you can exchange it for a new content token. Content tokens are short lived ~ 2 hours and valid for a specific podcast. A content token for a specific podcast can access any of the exclusive episodes for a podcast.<podcast:oauthPublicKey>
: Public Key that you can verify that all tokens are from the oauth provider.<podcast:oauthExclusiveContent>
: Any episode that is exclusive to paying users is marked as true<podcast:oauthPaymentLink>
: A link to where a user can pay for exclusive contentExample:
How to get tokens needed to consume exclusive content?
signupUrl
Step 1:
POST
endpoint. A podcast player has to sign up with each podcast hosting service to access the exclusive content that is being hosted by them. You will get back a client_id, client_secret.Request:
Property
Type
Description
Response:
Property
Type
Description
Step 2:
PUT
endpoint using the same URL. Use your client_id and client_secret to add or update your callback_urlRequest:
Property
Type
Description
Response:
Property
Type
Description
authorizeUrl
GET
endpoint. The 1st step in OAuth. A podcast player sends a user to this URL, the user can authenticate with a payment provider (eg: Patreon) to show they have paid for access to exclusive content for this podcast. This URL is provided by the podcast hosting provider, who is responsible for hosting the exclusive content, building the integration with payment providers, and keeping track of whether the user should have access to exclusive content.Mockup of what the UI for an authorizeUrl would look like
Append these URL parameters to the end of authorizeUrl:
Property
Type
Description
code
If rss_url is not passed in, the end user sees a more generic “Get exclusive access to your favourite creators” page. |
| state | String (Optional) | You can pass in state, it will be returned back to you along with your tokens via the callback_url. This can be useful for security purposes. |
| code_challenge | String (Optional) | For PKCE. Recommended for security purposes. |
| code_challenge_method | String (Optional) | For PKCE. Recommended for security purposes. |
Example:
Once a user successfully completes authorization, your podcast player will get back an authorization code sent to your callback_url (returned as url parameters appended to your callback_url):
Property
Type
Description
Note: You can set your callback_url to be a route on your client side or a GET endpoint on your server side.
tokenUrl
POST
endpoint. This is the 2nd step in OAuth. With this endpoint, you can convert the one-time authorization code you got from the 1st step for an access and refresh token.Request:
Property
Type
Description
authorization_code
Response:
Property
Type
Description
Example:
Note: The second step of OAuth involves using your client_secret so it should be done on your server (or if you don’t have a server use an AWS Lambda or Cloudflare Worker to hide your client_secret / act as your server)
newAccessTokenUrl
POST
endpoint. Once you have a refresh token, you can exchange it for a new access token.Request:
Property
Type
Description
Response:
Property
Type
Description
Example:
newRefreshTokenUrl
POST
endpoint. Once you have a refresh token, you can exchange it for a new refresh token.Request:
Property
Type
Description
Response:
Property
Type
Description
Example:
newContentTokenUrl
POST
endpoint. Once you have an access token, you can exchange it for a new content token. Content tokens are used to view exclusive content for a specific podcast.Request:
Property
Type
Description
Response:
Property
Type
Description
Example:
Now you have a content token, you can append it to the end of an image via the url parameter ‘token’.
https://az1.taddy.org/96cc49d7-a95d-4266-b408-b57c7d26a62e/88eec86a-b7d5-4f33-ad89-1f91226dd1e1/9d8094e2-df3c-4795-bbda-d0f3e69add75/story.webp?token=123
Note: This token is only valid for episodes from this specific podcast.
Who determines access control
In this proposed workflow, podcast creators are trusting their podcast hosting provider to host their exclusive content, integrate with the payment provider of choice (eg: Patreon, Stripe etc), and deal with generating tokens which provide access to only paying users. Podcast players store and generate and refresh tokens on behalf of the paying user.
Podcast Player UX
Here are a couple mockups, the goal is to think about the different UX states that the podcast player will need to account for.
Mockup 1: When a user is viewing a podcast that has exclusive content, BUT they do have any tokens from the podcast hosting provider to show if they paid for any exclusive content, they need to authorize with the hosting provider.
Mockup 2: Once connected and a user is not a paying backer, they may want to know that there are exclusive episodes that they could have access to and have an easy way to become a paying backer.
Mockup 3: Once connected and a user is a paying backer, they may want to know that they are able to listen to this exclusive episode because they are a paying backer.
Mockup 4: One downside of this proposed workflow is that a user has to integrate with every podcasting hosting provider that is hosting exclusive content that they want access to. Because of this, the user may want to have a screen where they can see all the hosting providers they are connected or not yet connected to.
Podcast Hosting Provider UX
Mockup 1: (Same screenshot as above) A podcast listener can authenticate with the payment provider that the podcast creator has chosen gives exclusive access to their episodes.
Mockup 2: A user may want to revoke access to a podcast player from generating refresh and access token on their behalf. They can login with the payment provider they authorized and see all the podcast players that have access to that provider.
Better Security
Current State: Static RSS Feed URLs
Today, when you subscribe to exclusive content using something like Patreon, you will receive a private RSS feed URL like:
https://www.patreon.com/rss/YourFavoritePodcast?auth=a7b9c2d4e5f6789012345678901234567890abcd
Once generated, this url remains valid for anyone to view exclusive content indefinitely unless manually changed by the platform. You can use IP tracking, user-agent tracking, or rate limiting to try to make sure the url is not being abused, but these are stop gaps.
Proposed Solution: Rotating keys
Instead of using one permanent URL, OAuth uses short-lived, rotating tokens (which is best practice from a security perspective):
Refresh Token (30+ days) → Access Token (2 hours) → Content Token (2 hours per podcast).
A bad actor can still share a url with their token attached, but it becomes useless in hours..
Access Token vs Content Token
Access Token is a token that verfies that you have authorized one or more payment providers with a podcast hosting provider. This token can be used to generate content tokens. Content Tokens give access to exclusive episodes from one specific podcast.
JWT Format
Access, Refresh and Content Tokens return JWT tokens that can be decoded for additional details. This information is useful to the podcast player.
Example of a decoded access or refresh JWT token:
Example of a decoded content JWT token:
Public Key
One of the tags proposed is
<podcast:oauthPublicKey>
. This is useful to the podcast player because some of their endpoint urls may be public, meaning anyone could send JWT tokens to those urls. A podcast player should check that the podcast hosting provider who sent those JWT tokens signed them with the private key associated with the public key they provided. (Otherwise the UI of the podcast player may show a user that they have access to exclusive episodes even though you do not have valid tokens to do so)Bad actors
I am going to define a bad actor as someone who enables others to view exclusive content without paying for it.
A podcast player can be a bad actor, for example: They could generate tokens for a user that has genuinely paid for access to exclusive content, and then save those tokens and use it to spread content to users that have not paid for the content.
Podcast hosting providers should:
Regression on current podcast players
If a podcast player has not updated their app to account for this OAuth workflow, the UI of the podcast player will not show the episodes are exclusive, and when the user tries to play the episode it will look like the audio isn’t working.
Instead of throwing an error, one idea is to have a fallback audio where you can say: “Hey, you are trying to access an exclusive episode. This podcast player isn't compatible to access this audio file but please visit ABC.com or download XYZ compatible apps to this exclusive episode.“
App Store Rules
My belief is that this falls within the “Reader App” term for Apple’s App Store because:
3.1.3(a) “Reader” Apps: Apps may allow a user to access previously purchased content or content subscriptions (specifically: magazines, newspapers, books, audio, music, and video). Reader apps may offer account creation for free tiers, and account management functionality for existing customers. Reader app developers may apply for the External Link Account Entitlement to provide an informational link in their app to a web site the developer owns or maintains responsibility for in order to create or manage an account. This entitlement is not required for developers to include buttons, external links, or other calls to action in their United States storefront apps. Learn more about the External Link Account Entitlement.
https://developer.apple.com/app-store/review/guidelines/#reader-apps
Example & Code:
And if you want to see this in action! I've implemented this workflow in my comic app Inkverse. All comics on Inkverse are self-hosted by the creator (we don’t use RSS, we use a different open specification, but the idea and the ethos is the same as using RSS - creators are not locked into any one app because they own their feed).
Inkverse UX (Reader app)
A is for Alice is a comic (modern interpretation of Alice in Wonderland) where the latest 3 episodes are exclusive to their Patreon backers.
If you click on the Patreon Exclusive issue and:
→ you ARE NOT a Patreon backer you get a message that says you need to be a Patreon backer, plus a link to Patreon.
→ you ARE a Patreon backer you get to read the exclusive episode.
Inkverse is open-source and has implemented this OAuth workflow.
Here are the revenant bits:
Taddy (Hosting Provider)
I also built Taddy, and almost all the comics on Inkverse use Taddy to distribute their comics and do access control for Patreon exclusive episodes. I did Patreon as the initial payment integration as that was the most used payment system by comic creators on Taddy but I plan to add more payment providers in the future.
Taddy isn’t open-source - yet, but I did want to go over some architectural decisions I made that might be useful to you.
signupUrl
I did not follow the spec, ie) a POST and PUT and built a GUI for anyone to register their client and callback_url.
I wanted to have control over any comic app that uses exclusive content provided by Taddy. This solution may be useful for the bigger podcast hosting providers but the POST and PUT endpoints are useful as it will be impossible to register with every self-hosted feed.
Use a OAuth library
I offloaded a lot of the OAuth workflow to the
node-oauth/express-oauth-server
library but I created the JWT tokens myself.Architecture
I use Cloudflare R2 (AWS S3 alternative) to host my images. I have a public bucket for images from publicly accessible episodes and a private bucket for images from exclusive episodes. I have a Cloudflare Worker that runs and checks the JWT Token for if it has access to any asset in the bucket.
Beta Was this translation helpful? Give feedback.
All reactions