Don't Fear the OAuth

AustinPHP + Austin Linux Meetup
January 2020

Ian Littman / @iansltx

follow along at https://ian.im/oauthatx20

We're talking about OAuth 2 here

OAuth 1.x is deprecated

How does this work?

Let's see where it redirects to

Let's See Where it redirects to

https://accounts.google.com/o/oauth2/auth?access_type=offline&

client_id=835284083712.apps.googleusercontent.com&

redirect_uri=https%3A%2F%2Fslides.com%2Fusers%2Fauth%2Fgoogle_oauth2%2Fcallback&

response_type=code&

scope=email+profile&

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Let's See Where it redirects to

https://accounts.google.com/o/oauth2/auth?access_type=offline&

client_id=835284083712.apps.googleusercontent.com&

redirect_uri=https%3A%2F%2Fslides.com%2Fusers%2Fauth%2Fgoogle_oauth2%2Fcallback&

response_type=code&

scope=email+profile&

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Auth Code Grant

  • Operating on behalf of a user
  • User can log in on the same device
  • Requesting service doesn't have user credentials
  • Requesting service may or may not be client-side
  • Requesting service may or may not be able to keep secrets secret

Let's See Where it redirects to

https://accounts.google.com/o/oauth2/auth?access_type=offline&

client_id=835284083712.apps.googleusercontent.com&

redirect_uri=https%3A%2F%2Fslides.com%2Fusers%2Fauth%2Fgoogle_oauth2%2Fcallback&

response_type=code&

scope=email+profile&

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Different behaviors for different clients

  • Confidential vs. public
  • Allowed grant types
  • User authorization for a given app
  • Preventing auth code hijacking
  • Revocation

Let's See Where it redirects to

https://accounts.google.com/o/oauth2/auth?access_type=offline&

client_id=835284083712.apps.googleusercontent.com&

redirect_uri=https%3A%2F%2Fslides.com%2Fusers%2Fauth%2Fgoogle_oauth2%2Fcallback&

response_type=code&

scope=email+profile&

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Let's See Where it redirects to

https://accounts.google.com/o/oauth2/auth?access_type=offline&

client_id=835284083712.apps.googleusercontent.com&

redirect_uri=https%3A%2F%2Fslides.com%2Fusers%2Fauth%2Fgoogle_oauth2%2Fcallback&

response_type=code&

scope=email+profile&

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Let's See Where it redirects to

https://accounts.google.com/o/oauth2/auth?access_type=offline&

client_id=835284083712.apps.googleusercontent.com&

redirect_uri=https%3A%2F%2Fslides.com%2Fusers%2Fauth%2Fgoogle_oauth2%2Fcallback&

response_type=code&

scope=email+profile&

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Let's See Where it redirects to

https://accounts.google.com/o/oauth2/auth?access_type=offline&

client_id=835284083712.apps.googleusercontent.com&

redirect_uri=https%3A%2F%2Fslides.com%2Fusers%2Fauth%2Fgoogle_oauth2%2Fcallback&

response_type=code&

scope=email+profile&

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Scopes

https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/drive.appdata
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.metadata
https://www.googleapis.com/auth/drive.metadata.readonly
https://www.googleapis.com/auth/drive.photos.readonly
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/drive.scripts

...and that's just for Google's Drive API

You probably don't want scopes
that map 1:1 to user roles

Let's authenticate and see
where we get redirected...

We're redirected back to...

https://slides.com/users/auth/google_oauth2/callback?

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa&
code=4%2FvQFTLSQfFlG3jAzgmX2Dg...&

scope=email+profile+openid+...&

authuser=0&

session_state=92238ad3186b0d73713263061d6f5a0aeddbf844..78ff&

prompt=none

We're redirected back to...

https://slides.com/users/auth/google_oauth2/callback?

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa&
code=4%2FvQFTLSQfFlG3jAzgmX2Dg...&

scope=email+profile+openid+...&

authuser=0&

session_state=92238ad3186b0d73713263061d6f5a0aeddbf844..78ff&

prompt=none

We're redirected back to...

https://slides.com/users/auth/google_oauth2/callback?

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa&
code=4%2FvQFTLSQfFlG3jAzgmX2Dg...&

scope=email+profile+openid+...&

authuser=0&

session_state=92238ad3186b0d73713263061d6f5a0aeddbf844..78ff&

prompt=none

We're redirected back to...

https://slides.com/users/auth/google_oauth2/callback?

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa&
code=4%2FvQFTLSQfFlG3jAzgmX2Dg...&

scope=email+profile+openid+...&

authuser=0&

session_state=92238ad3186b0d73713263061d6f5a0aeddbf844..78ff&

prompt=none

We're redirected back to...

https://slides.com/users/auth/google_oauth2/callback?

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa&
code=4%2FvQFTLSQfFlG3jAzgmX2Dg...&

scope=email+profile+openid+...&

authuser=0&

session_state=92238ad3186b0d73713263061d6f5a0aeddbf844..78ff&

prompt=none

Server-side time

POST https://www.googleapis.com/oauth2/v4/token
 

client_id=835284083712.apps.googleusercontent.com&

client_secret=ifIhadThisIcouldImpersonateSlidesDotCom&

redirect_uri=https%3A%2F%2Fslides.com%2Fusers%2Fauth%2Fgoogle_oauth2%2Fcallback&

grant_type=authorization_code&

code=4%2FvQFTLSQfFlG3jAzgmX2Dg...

Server-side time: Response

{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "xtnYoXsKMQSAR4kA0RlV4SxDERpcVTtYi...",
    "refresh_token": "o62ibFrTJmea3gTivWOih54Anujelg5A...",
    "id_token": "JZzEAMTgCcK3vPBkzby.KzwcoAd3Cji.TgK3vP..."
}

Server-side time: Response

{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "xtnYoXsKMQSAR4kA0RlV4SxDERpcVTtYi...",
    "refresh_token": "o62ibFrTJmea3gTivWOih54Anujelg5A...",
    "id_token": "JZzEAMTgCcK3vPBkzby.KzwcoAd3Cji.TgK3vP..."
}

Server-side time: Response

{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "xtnYoXsKMQSAR4kA0RlV4SxDERpcVTtYi...",
    "refresh_token": "o62ibFrTJmea3gTivWOih54Anujelg5A...",
    "id_token": "JZzEAMTgCcK3vPBkzby.KzwcoAd3Cji.TgK3vP..."
}

Refreshing access

POST https://www.googleapis.com/oauth2/v4/token
 

client_id=835284083712.apps.googleusercontent.com&

client_secret=ifIhadThisIcouldImpersonateSlidesDotCom&

grant_type=refresh_token&

refresh_token=o62ibFrTJmea3gTivWOih54Anujelg5A...

Server-side time: Response

{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "xtnYoXsKMQSAR4kA0RlV4SxDERpcVTtYi...",
    "refresh_token": "o62ibFrTJmea3gTivWOih54Anujelg5A...",
    "id_token": "JZzEAMTgCcK3vPBkzby.KzwcoAd3Cji.TgK3vP..."
}

This is a JWT

  • base64url(header). '.' . base64url(payload) . '.' . signature
  • Header: {"alg": "RS256", "typ": "JWT"}
  • Signature: sign(base64url(header) . '.' . base64url(payload))

JWT PAYLOAD FOR GOOGLE'S OPENID CONNECT IMPL

{
  "iss": "https://accounts.google.com",
  "azp": "1234987819200.apps.googleusercontent.com",
  "aud": "1234987819200.apps.googleusercontent.com",
  "sub": "10769150350006150715113082367",
  "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
  "hd": "example.com",
  "email": "jsmith@example.com",
  "email_verified": "true",
  "iat": 1353601026,
  "exp": 1353604926,
  "nonce": "0394852-3190485-2490358"
}

JWT Payload for Google's OpenID Connect Impl

{
  "iss": "https://accounts.google.com",
  "azp": "1234987819200.apps.googleusercontent.com",
  "aud": "1234987819200.apps.googleusercontent.com",
  "sub": "10769150350006150715113082367",
  "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
  "hd": "example.com",
  "email": "jsmith@example.com",
  "email_verified": "true",
  "iat": 1353601026,
  "exp": 1353604926,
  "nonce": "0394852-3190485-2490358"
}

OIDC Bonus: Discoverability

But What about my Single Page App?

  • Operating on behalf of a user
  • User can log in on the same device
  • Requesting service doesn't have user credentials
  • Requesting service is client-side
  • Requesting service is not able to keep secrets secret

The old way: implicit grant

The old way: implicit grant

https://my-auth-provider.com&

client_id=a123b123&

redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&

response_type=token&

scope=email+profile&

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

...which redirects you to...

https://spa.mysite.com/auth#

access_token=xtnYoXsKMQSAR4kA0RlV4SxDERpcVTtYi...&

token_type=Bearer&

expires_id=3600

The new way: PKCE

https://my-auth-provider.com&

client_id=a123b123&

redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&

response_type=code&
code_challenge=
XsgVoUAhKm9ZIDP3N53P2crEiz2X3KszLhIPKN...&
code_challenge_method=S256

scope=email+profile&

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

PKCE Code redemption

POST https://my-auth-provider.com/token
 

client_id=a123b123&

redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&

grant_type=authorization_code&

code=4%2FvQFTLSQfFlG3jAzgmX2Dg...&

code_verifier=ovMQw176WZXkkm3uFQ4PtwKV...

  • Operating on behalf of a user
  • User can log in on the same device
  • Requesting service can obtain user's credentials
  • Requesting service may or may not be client side
  • Requesting service may or may not be able to keep secrets secret
  • Caveat: Doesn't support special auth flows (e.g. 2FA) inline

Trusted Clients: Password Grant

Password Grant

POST https://my-auth-provider.com/token
 

client_id=a123b123&

client_secret=ovMQw176WZXkkm3uFQ4PtwKV...&

redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&

grant_type=password&

username=email@my.site&

password=hunter2

App-level access: Client Credentials Grant

  • Operating on behalf of an application, not a user
  • Requesting service is server-side
  • Requesting service is able to keep secrets secret

Client Credentials Grant

POST https://my-auth-provider.com/token
 

client_id=a123b123&

client_secret=ovMQw176WZXkkm3uFQ4PtwKV...&

redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&

grant_type=client_credentials

assisted access: Device authorization Grant

  • Operating on behalf of a user
  • User cannot log in on the same device
  • Requesting service is client side
  • Requesting service may or may not be able to keep secrets secret
  • Client is able to poll for an access token

assisted access: Device authorization Grant

  1. Device asks auth server for a code, including its client_id
  2. Auth server hands back a device code, user code, and verification URL
  3. Device tells user to go to the verification URL and enter the user code
  4. Device polls with its client ID and device code until one of the below:
    • User authorizes the device (device gets access/refresh tokens)
    • User declines authorization
    • Device code expires

What does it look like to serve this?

What does a full implementation look like?

Thanks! Questions?