OAuth 2.0
Motivation and explanation of OAuth 2.0
Third-Party APIs
Web APIs
- Many apps have APIs that let programs access user data with HTTP requests
GET api.github.com/user- The logged-in user's profileGET api.github.com/user/repos- Their repositories, including private onesPOST api.github.com/repos/<owner>/<repo>/issues- Create an issue
- Sometimes, you want your app to use this API on behalf of your users
- A scrum board app that creates GitHub issues when your users' create cards
- A "Login with GitHub" button (Which you'll implement on HW2)
- How can your users let your app access their GitHub accounts securely?
Bad Ideas: Ask for Their Password
Have your users give you their GitHub username and password- Never ask for a password to someone else's site
- You'd have to store it in plain text to reuse it
- Your app gets full access to their account. Not just what you need
- The user can't revoke your access without changing their password
- It trains users to type their passwords into random sites
Bad Idea: Ask for Their API Key
Have the user create an API key on GitHub and paste it into your app- Better than a password, but:
- The user has to handle a secret. Never trust your users, not even with their own security
- Requests from your app look exactly like requests from the user
- No accountability. If your app misbehaves, GitHub can't tell it was you
- If the key leaks, there's no way to tell who is using it
- Your usage counts against the user's rate limits (Or bill!)
OAuth 2.0
- OAuth 2.0 (Open Authorization) - The standard way for a user to grant an app limited access to their account on another service
- The user approves your app on GitHub's site
- Your app never sees their password
- GitHub issues an access token directly to your app
- The user never handles the access token
- The token is tied to your app. GitHub knows it's you (Accountability)
- The token is limited to the permissions (scopes) the user approved
- The user can revoke your app's access at any time
- The user still has to trust your app with the data they approve
Client Registration
- Before any user can log in, you register your app with GitHub
- You get/set at least 3 important values during registration
- Client ID - Identifies your app. Public
- Client Secret - Effectively a password for your app
- Only ever stored on your server (In your
.envfile). Never in your front end or your git repo
- Only ever stored on your server (In your
- Redirect URI - Where GitHub sends users after they approve your app
http://localhost:8080/authcallbackon the HW- GitHub only redirects to the URI you registered
OAuth Flows
- OAuth has several flows for different kinds of apps
- We'll use the Authorization Code Flow
- For apps with a server that can keep a secret
- The most common flow
State
Recall: Login XSRF
- Login XSRF: The attacker logs the victim into the attacker's account
- Your
/authcallbackendpoint is a login endpoint! - The usual XSRF defenses don't help here:
/authcallbackis designed to receive cross-site requests (A redirect from github.com)- It's a GET navigation, so
SameSite=Laxcookies are sent - GitHub builds the request, not your front end. There's no chance to add a XSRF token
The Attack
- The attacker starts "Login with GitHub" on your app using their own GitHub account
- They stop before following the final redirect and keep the unused code
- The attacker gets the victim to open a link to your callback with the attacker's code
- To your app, this looks exactly like the end of a normal login
- Your app redeems the code with its own client secret. Everything checks out
- The victim is now logged in to the attacker's account!
- Everything they save goes to an account the attacker controls
The State Parameter
- Before redirecting to GitHub (Step 1):
- Generate a random, unguessable
statevalue - Bind it to this browser
- Add it to the authorization request
- Generate a random, unguessable
- GitHub sends the same
stateback with the code (Step 4) - At
/authcallback:- Check that the
statematches the one bound to this browser - If it's missing or doesn't match:
400 Bad Requestand stop. Never redeem the code - Delete the state after checking. Each state is single use
- Check that the
- The state is a XSRF token for your OAuth callback
Binding State to the Browser
- The user usually isn't logged in yet, so there's no session to attach the state to
- One approach: Store it in a cookie before the redirect
Set-Cookie: oauth_state=Xk3fR9...; HttpOnly; Max-Age=600; SameSite=Lax- At the callback, compare the query string
stateto the cookie
- Attacker cannot set a cookie for our site
- Attacker can't read this cookie since our site never redirects to the attack site
- SOP will block responses so they can't fake a request and read the set-cookie header
- You have some freedom in how you implement this on the HW, as long as it stops the attack
Refresh Tokens
Refresh Tokens
- Many APIs make access tokens expire quickly (e.g. 1 hour)
- Along with the access token, they issue a long-lived refresh token
- When the access token expires, use the refresh token to obtain a new one:
- You get a new access token (And sometimes a new refresh token)
- The user doesn't have to do anything
- Why not just have the access tokens be long-lived?
- More secure when the auth server and resource server are not the same server
- More on this much later when we discus JWTs
- More secure when the auth server and resource server are not the same server