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
Source: RFC 6749 section 4.3
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
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...
Refreshing access
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
Server-side time
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...
Server-side time
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...
Server-side time
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...
Server-side time
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...
Server-side time
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 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!
Auth Code grant, server-side (Secret) and client-side (PKCE)
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
- Device asks auth server for a code, including its client_id in the request
- Auth server hands back a device code, user code, and verification URL
- Device tells user to go to the verification URL and enter the user code
- 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
- oauth.net - Easier to digest than RFCs
- jwt.io - More information on JWTs
- oauth2.thephpleague.com - PHP OAuth 2 Server Library
- laravel.com/docs/6.x/passport - Laravel Passport
- ian.im/oauthbnl20 - these slides
- github.com/iansltx/oauth-sample - code link
- joind.in/talk/d8ee9 - leave feedback!
- twitter.com/iansltx - me, online
Thanks! Questions?
Don't Fear the OAuth - PHPBenelux 2020
By Ian Littman
Don't Fear the OAuth - PHPBenelux 2020
- 1,814