Don't Fear the OAuth

PHPSW May 2020: Lockdown Security

Ian Littman / @iansltx

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

When we're done you'll understand how this works

Questions we'll Answer

  • What's an OAuth grant type?
  • What grant types are available, and when should I use each?
  • What does each grant-based flow look like, piece by piece?
  • How can I implement an OAuth 2 server (and client!) in PHP?

Questions we won't answer

  • How do I work with OAuth 1.x? (it's deprecated)
  • How can I build an OAuth server into my Symfony app?
  • How do I integrate OAuth with Laravel? (use Passport)

NOTE: This implementation isn't RFC-compliant
because it isn't running over HTTPS.

Grant Types

  • Resource Owner Password Credentials
  • Refresh Token
  • Client Credentials
  • Implicit
  • Authorization Code
  • Bonus: Device Authorization
  • Operating on behalf of a user
  • User can log in on the same device
  • Requesting service can obtain user's primary 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/oauth/token
 

grant_type=password&
client_id=first-party&
username=superuser&
password=super-secret

Password Grant

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

grant_type=password&
client_id=first-party&
username=superuser&
password=super-secret

Password Grant

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

grant_type=password&
client_id=first-party&
username=superuser&
password=super-secret

Password Grant

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

grant_type=password&
client_id=first-party&

username=superuser&
password=super-secret

Password Grant

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

grant_type=password&
client_id=first-party&
username=superuser&
password=super-secret&

client_secret=Es1JuAIbb7QRikQAN5wfbZPBLJA...

Password Grant

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

grant_type=password&
client_id=first-party&
username=superuser&
password=super-secret&
client_secret=Es1JuAIbb7QRikQAN5wfbZPBLJA...&

scope=me.name me.hash

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

Token Response

{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1N...",
    "refresh_token": "def502009dbc45718a2f75c4660f136e...",
}

Token response

{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1N...",
    "refresh_token": "def502009dbc45718a2f75c4660f136e...",
}

Token response

{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1N...",
    "refresh_token": "def502009dbc45718a2f75c4660f136e...",
}

Token response

{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1N...",
    "refresh_token": "def502009dbc45718a2f75c4660f136e...",
}

This happens to be a JWT

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

JWT Payload

{
  "aud": "first-party",
  "jti": "396485e81a10b18a26af53ea04bd672edb635c5c34d...",
  "iat": 1579872090,
  "nbf": 1579872090,
  "exp": 1579875690,
  "sub": "1",
  "scopes": ["me.name"]
}

JWT Payload

{
  "aud": "first-party",
  "jti": "396485e81a10b18a26af53ea04bd672edb635c5c34d...",
  "iat": 1579872090,
  "nbf": 1579872090,
  "exp": 1579875690,
  "sub": "1",
  "scopes": ["me.name"]
}

JWT Payload

{
  "aud": "first-party",
  "jti": "396485e81a10b18a26af53ea04bd672edb635c5c34d...",
  "iat": 1579872090,
  "nbf": 1579872090,
  "exp": 1579875690,
  "sub": "1",
  "scopes": ["me.name"]
}

JWT Payload

{
  "aud": "first-party",
  "jti": "396485e81a10b18a26af53ea04bd672edb635c5c34d...",
  "iat": 1579872090,
  "nbf": 1579872090,
  "exp": 1579875690,
  "sub": "1",
  "scopes": ["me.name"]
}

JWT Payload

{
  "aud": "first-party",
  "jti": "396485e81a10b18a26af53ea04bd672edb635c5c34d...",
  "iat": 1579872090,
  "nbf": 1579872090,
  "exp": 1579875690,
  "sub": "1",
  "scopes": ["me.name"]
}

JWT Payload

{
  "aud": "first-party",
  "jti": "396485e81a10b18a26af53ea04bd672edb635c5c34d...",
  "iat": 1579872090,
  "nbf": 1579872090,
  "exp": 1579875690,
  "sub": "1",
  "scopes": ["me.name"]
}

Token Response

{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1N...",
    "refresh_token": "def502009dbc45718a2f75c4660f136e...",
}

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

client_id=first-party&

grant_type=refresh_token&

refresh_token=def502009dbc45718a2f75c4660f136e...

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

client_id=first-party&

grant_type=refresh_token&

refresh_token=def502009dbc45718a2f75c4660f136e...

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
  • No need for a refresh token

Client Credentials Grant

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

grant_type=client_credentials&
client_id=machine-to-machine&
client_secret=super-secret-client-secret-string

Client Credentials Grant

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

grant_type=client_credentials&
client_id=machine-to-machine&
client_secret=super-secret-client-secret-string

Client Credentials Grant

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

grant_type=client_credentials&
client_id=machine-to-machine&
client_secret=super-secret-client-secret-string

Demo time

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
  • No refresh token here either

The old way: implicit grant

Implicit grant authorization request

https://my-auth-provider.com/oauth/authorize&
client_id=single-page-app&
redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&
response_type=token&
scope=me.name&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Implicit grant authorization request

https://my-auth-provider.com/oauth/authorize&
client_id=single-page-app&
redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&
response_type=token&
scope=me.name&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Implicit grant authorization request

https://my-auth-provider.com/oauth/authorize&
client_id=single-page-app&
redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&
response_type=token&
scope=me.name&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Implicit grant authorization request

https://my-auth-provider.com/oauth/authorize&
client_id=single-page-app&

redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&
response_type=token&
scope=me.name&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Implicit grant authorization request

https://my-auth-provider.com/oauth/authorize&
client_id=single-page-app&
redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&

response_type=token&
scope=me.name&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Implicit grant authorization request

https://my-auth-provider.com/oauth/authorize&
client_id=single-page-app&
redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&

response_type=token&
scope=me.name&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Implicit grant authorization request

https://my-auth-provider.com/oauth/authorize&
client_id=single-page-app&
redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&
response_type=token&
scope=me.name&

state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

...which redirects you to...

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

access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1N......&

token_type=Bearer&

expires_id=3600&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

...which redirects you to...

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

access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1N......&

token_type=Bearer&

expires_id=3600&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

...which redirects you to...

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

access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1N......&

token_type=Bearer&

expires_id=3600&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

...which redirects you to...

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

access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1N......&

token_type=Bearer&

expires_id=3600&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

Demo Time

Authorization 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

Request
Redirect
Redeem

Remember this?

Remember this?

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

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

We authenticate, and are 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

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

redeem Step (server-side)

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

grant_type=authorization_code&
client_id=835284083712.apps.googleusercontent.com&
client_secret=ifIhadThisIcouldImpersonateSlidesDotCom&
redirect_uri=https%3A%2F%2Fslides.com%2Fusers%2Fauth%2Fgoogle_oauth2%2Fcallback&code=4%2FvQFTLSQfFlG3jAzgmX2Dg...

Redeem Step (server-side)

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

grant_type=authorization_code&
client_id=835284083712.apps.googleusercontent.com&
client_secret=ifIhadThisIcouldImpersonateSlidesDotCom&
redirect_uri=https%3A%2F%2Fslides.com%2Fusers%2Fauth%2Fgoogle_oauth2%2Fcallback&code=4%2FvQFTLSQfFlG3jAzgmX2Dg...

Redeem step (server-side)

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

grant_type=authorization_code&
client_id=835284083712.apps.googleusercontent.com&

client_secret=ifIhadThisIcouldImpersonateSlidesDotCom&
redirect_uri=https%3A%2F%2Fslides.com%2Fusers%2Fauth%2Fgoogle_oauth2%2Fcallback&code=4%2FvQFTLSQfFlG3jAzgmX2Dg...

redeem step (server-side)

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

grant_type=authorization_code&
client_id=835284083712.apps.googleusercontent.com
&client_secret=ifIhadThisIcouldImpersonateSlidesDotCom&

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

Redeem step (server-side)

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

grant_type=authorization_code&
client_id=835284083712.apps.googleusercontent.com&
client_secret=ifIhadThisIcouldImpersonateSlidesDotCom&

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

Access Token Response

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

Access Token Response

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

A wild OpenID Connect JWT appeared!

{
  "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"
}

A wild OpenID Connect JWT appeared!

{
  "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"
}

A wild OpenID Connect JWT appeared!

{
  "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"
}

A wild OpenID Connect JWT appeared!

{
  "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"
}

A wild OpenID Connect JWT appeared!

{
  "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"
}

What if we tweaked the auth code grant
to work safely on client-side apps?

Proof Key for Code Exchange (PKCE)

PKCE authorization request

https://my-auth-provider.com/oauth/authorize&
client_id=single-page-app&
redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&
response_type=code&
code_challenge=XsgVoUAhKm9ZIDP3N53P2crEiz2X3KszLhIPKN...&
code_challenge_method=S256
scope=me.name me.hash&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

PKCE authorization request

https://my-auth-provider.com/oauth/authorize&
client_id=single-page-app&
redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&

response_type=code&
code_challenge=XsgVoUAhKm9ZIDP3N53P2crEiz2X3KszLhIPKN...&
code_challenge_method=S256
scope=me.name me.hash&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

PKCE authorization request

https://my-auth-provider.com/oauth/authorize&
client_id=single-page-app&

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

response_type=code&

code_challenge=XsgVoUAhKm9ZIDP3N53P2crEiz2X3KszLhIPKN...&
code_challenge_method=S256

scope=me.name me.hash&
state=214c1e90730143beca60feb6e9da0807fa68f7be82ef34fa

PKCE Code redemption

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

client_id=single-page-app&
redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&
grant_type=authorization_code&
code=4%2FvQFTLSQfFlG3jAzgmX2Dg...&
code_verifier=ovMQw176WZXkkm3uFQ4PtwKV...

PKCE Code redemption

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

client_id=single-page-app&
redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&

grant_type=authorization_code&
code=4%2FvQFTLSQfFlG3jAzgmX2Dg...&
code_verifier=ovMQw176WZXkkm3uFQ4PtwKV...

PKCE Code redemption

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

client_id=single-page-app&
redirect_uri=https%3A%2F%2Fspa.mysite.com%2Fauth&
grant_type=authorization_code&
code=4%2FvQFTLSQfFlG3jAzgmX2Dg...&

code_verifier=ovMQw176WZXkkm3uFQ4PtwKV...

Demo time!

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 in the request
  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 we covered

  • Components of OAuth 2.0 based authentication
  • Grant types, when to use them, and why
  • How to support these grants server-side in PHP, via League's oauth2-server package
  • How to interact with these grants client-side, from scratch

Further Reading

Thanks! Questions?

Don't Fear the OAuth - PHPSW May 2020

By Ian Littman

Don't Fear the OAuth - PHPSW May 2020

  • 1,669