ryanckulp / speedrail

Rails 7 app template: Devise auth, Stripe billing, Tailwind CSS, admin panel, SEO helpers, etc
https://founderhacker.com/24-hour-mvp
MIT License
225 stars 28 forks source link

YouTube OAuth2 demo #141

Open ryanckulp opened 5 months ago

ryanckulp commented 5 months ago

from a live coding session at Camp, Cohort 3.

what is OAuth2?

when Users want to connect your app (App 1) with another app (App 2), you need access to their data in App 2. if App 2 has an API, you need the User to grant you permission to their data with some form of API key.

the most common way to provide this API key was for App 2 to let users visit a "Developer" area in their account settings where they could generate and copy/paste a key into 3rd party tools, such as App 1 (your product). but this has several challenges.

  1. if App 2 only allows for 1 API key, you can't scope access between 3rd party products
  2. if you wish to revoke access to App 2 from a 3rd party app (say, App 3), then App 1 will also lose access
  3. copy/pasting API keys has a potential for user error, and App 1 always has to keep track of where App 2 manages keys
  4. in some cases, neither App 1 nor App 2 want the User to have access to their own API key

OAuth(2) addresses all these issues and more. Oauth is an authorization + authentication strategy that makes it a) easy for Users to connect applications and b) more secure + robust from a connection management and logging perspective.

How Oauth2 works

below is a simple flow of data between your application and the application on which you build an OAuth "client," which then enables your Users to share the other application's data with yours.

in this example we're building an app called "VideoGoals," which allows a YouTube creator to connect their channel and set goals for their content, such as likes or view counts on a per video basis.

Oauth2 flow example

Steps to building OAuth connections

  1. create Oauth2 Client + configure scopes (permissions)
  2. form an 'authorize url'
  3. configure the callback to swap the "code" for an API Key

Setting up an Oauth2 client

in this example we visit cloud.google.com, create a project, enable the "YouTube Data v3" api, and select the scope https://www.googleapis.com/auth/youtube.

on the OAuth consent screen we set our redirect URI to simply http://localhost:3000, then are granted a "client ID" and "client secret" which we'll need later.

a quick search for "Google OAuth2 authorize URL" reveals this snippet:

https://accounts.google.com/o/oauth2/v2/auth?
 scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly&
 access_type=offline&
 include_granted_scopes=true&
 response_type=code&
 state=state_parameter_passthrough_value&
 redirect_uri=https%3A//oauth2.example.com/code&
 client_id=client_id

simply swap out the scope, redirect_uri, and client_id from what you configured inside the Google Cloud UI, and visit this URL in your web browser.

this completes Step 1 of the diagram above, without writing any code.

it's worth noting that Google's cloud platform and OAuth2 client generator is much more complex than most. to check out simpler examples of OAuth clients, finding and setting scopes, and managing oauth consent screens, check out Gumroad and HubSpot's docs:

one of the most common issue with setting up OAuth clients is simply not paying attention to detail. it's very easy to have a small typo in your authorize URL that will prevent you from testing anything. an example is setting multiple scopes (permissions). some OAuth2 implementations require space delimited, others encoded (%20), others with commas, and so on like below:

image

Creating access tokens

after visiting a (working) authorization URL and following through the prompts (login + accept scopes), the User will be redirected to your "redirect URI," which in our examples above is simply localhost:3000. note that you do not need to have an actual server running, or any backend code configured at this point.

upon redirection your web browser will look something like this:

image

we want to extract the code= parameter, which in this example is sdfjasaskdfjsadkfsadf.

next, send a POST request to the "token" endpoint of your target Oauth2 client. for Google products it looks like this:

  require 'httparty'
  url = "https://oauth2.googleapis.com/token"

  body = {
    code: 'sdfjasaskdfjsadkfsadf',
    client_id: 'YOUR_CLIENT_ID_FROM_OAUTH2_CLIENT_DEVELOPER_PORTAL',
    client_secret: 'YOUR_CLIENT_SECRET_FROM_OAUTH2_CLIENT_DEVELOPER_PORTAL',
    redirect_uri: 'http://localhost:3000',
    grant_type: 'authorization_code'
  }

  resp = HTTParty.post(url, body: body)
  data = JSON.parse(resp.body)

the parsed JSON will look like this, and contain a valid access token with which to make API requests against that User's data on the target application.

  {"access_token"=>"ya29.b0Ad52N3-Fz2Na87OyRA8H0CHKHSbuurW4nM", "expires_in"=>3599, "refresh_token"=>"1//05XHw8RszAg5CCgYIArBAGaYSNwF", "scope"=>"https://www.googleapis.com/auth/youtube", "token_type"=>"Bearer"}

Making API requests

the Oauth2 standard is for access tokens to be included in API requests via a header called "authorization" and prefixed with "Bearer", like so:

headers = {
  .. other headers here, if applicable
  'authorization' => "Bearer ya29.b0Ad52N3-Fz2Na87OyRA8H0CHKHSbuurW4nM"
}

Refreshing access tokens

yet another OAuth2 standard is for access tokens to expire after some amount of time. as you can see in the "data" response above, Google tokens expire in 3600 seconds (1 hour). to request data from a User's account after the expiration, simply refresh the token like so:

require 'httparty'

url = "https://oauth2.googleapis.com/token"
body = {
      client_id: 'YOUR_CLIENT_ID_FROM_OAUTH2_CLIENT_DEVELOPER_PORTAL',
      client_secret: 'YOUR_CLIENT_SECRET_FROM_OAUTH2_CLIENT_DEVELOPER_PORTAL',
      refresh_token: '1//05XHw8RszAg5CCgYIArBAGaYSNwF',
      grant_type: grant_type
    }

    resp = HTTParty.post(url, body: body)

refreshing access tokens is done with the "refresh token" from earlier, when you exchanged the code for an access token. always save refresh tokens, because without them your access tokens will eventually be useless.

note that refresh tokens typically don't change, only access tokens. this is a debate amongst some engineers who claim that expiring access tokens is essentially a useless security feature, since refresh tokens act as permanent access tokens.

Next steps

see the code sample in this PR for an automated approach to swapping redirect URI "code" values for working access tokens.

ryanckulp commented 5 months ago

Below are live session notes from a student in attendance:

OAuth

Common errors in OAuth 1 solved by OAuth 2

OAuth 2 changes

  1. Grant explicit permission to APIs
  2. Scopes: permissions - gives only access to certain information with read or write. Customer names, phone, address would be accessible forever, even after user delete their data
  3. Security: API keys by default expire after set time (24 hours usually, 1 month max) Instead of pinging data every time, OAuth 2 needs to refresh API token if account is in good standing. Ex: If I reset Gmail password, can break connection to OAuth -> can be good news if I am hacked and you can’t refresh token to corrupt my data
  4. User experience: Gross to copy>paste keys and give them instructions to navigate them. Always stay in tune with particular API instructions

Historical context

Sensitive vs. non-sensitive scopes

Naming - why is it important?

Javascript origins - not typical OAuth (a Google thing)