# Part 5 - OAuth Grant Types

Table of Contents

Introduction

Ever since the start of this blog series, we have used a single example and looked at it from different perspectives:

  • Resource Owner: you 🫵, the owner of the Facebook account.
  • Client: Strava, the running app that wants to post your workout to Facebook on your behalf.
  • Protected Resource: Facebook, the API that holds your account.
  • Authorization Server: the component that registers Strava, authenticates you, asks whether you are happy for Strava to post on your behalf, and hands Strava the tokens that Facebook will accept.

The OAuth flow has remained mostly the same: you open Strava, Strava sends you to Facebook’s Authorization Server, you log in and approve, and Strava gets back an Authorization Code it can trade for an Access Token. Then Strava can use that Access Token to make a post on your Facebook account.

This type of flow has a name. It is called the Authorization Code Grant Type. We have reused this flow in all posts and looked at it from different angles. We have looked at this flow from the client’s perspective, from the Protected Resource’s perspective, and the Authorization Server’s perspective.

In this post we’ll look at three grant types:

  • Authorization Code Grant — the flow we have used all series. A user delegates access from a device that has a browser.
  • Client Credentials Grant — no user at all. The client is acting on its own behalf.
  • Device Authorization Grant — there’s a user, but the device asking for access doesn’t have a way to show them a login page.

OAuth Grant Type

A grant type simply refers to a specific type of OAuth flow. For example, what happens if the user and browser are not involved in the OAuth flow and only the client and Authorization Server are involved? In that case, this grant type would be known as the Client Credentials Grant Type. It is simply a different flow of events under the OAuth umbrella.

By now, you should have a good understanding of the Authorization Code grant type.

In Part 4 we saw that “grant type” is literally a field the Authorization Server stores against every registered client. This is because the Authorization Server needs to know what grant types a client can support. For example, can a client operate without a user and browser present, or must both always be present? This creates the ceiling of capabilities for the client.

Client Credentials Grant

Strava’s marketing team runs its own official Facebook Page, separate from any individual user’s account. Every morning, a backend job at Strava publishes a scheduled post to that Page: “route of the day,” which is a leaderboard highlight.

The client is Strava, and it’s asking for access to a resource it already owns. You don’t own Strava’s Facebook Page. Strava (the client) does!

Broadly speaking, if the client already owns or has the necessary authorization to access the private resource, then the user becomes irrelevant.

When the resource owner and the client are the same party, the entire front channel disappears. Front-channel calls go through the browser, and with no user there is no browser. No redirect, no login screen, no consent screen, and no Authorization Code. The flow becomes very simple.

This means that the client just authenticates directly to the token endpoint and asks for a token in one request. This is how the OAuth flow is started:

Terminal window
POST /token HTTP/1.1
Host: auth-server.com
Authorization: Basic c3RyYXZhLW1hcmtldGluZzo4ZjNlMWMwMi1hOWI3
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=page_post

The Authorization Server checks the client credentials in the Authorization header exactly like it checks them at the token endpoint during an Authorization Code exchange.

The Authorization Server confirms client_credentials is a grant type this client is registered for, and also confirms page_post is within the scopes that the client was registered for.

If all the above checks out, the Authorization Server hands back an Access Token straight away, which ends the OAuth flow:

Terminal window
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token": "vN2vXLpQ4mZ7wRt1yHb3cJd6fGs0aV",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "page_post"
}

Finally, note that there is no Refresh Token. RFC 6749 states: “A refresh token SHOULD NOT be included.” A Refresh Token exists to spare a user from being dragged through consent again. There is no user here, so there is no need for a Refresh Token. The only credentials that are required are the client ID and client secret, which are readily available to the client. When the token expires, the client just sends the exact same client_credentials request again.

Device Authorization Grant

Now picture Strava running on a Garmin watch, or on the display inside a gym’s treadmill. You want that device to post your run to Facebook, same as your phone does. This time there is a resource owner — you — but your Garmin watch does not have a browser to redirect you to the Authorization Server and neither does the treadmill in your gym.

The Device Authorization Grant, defined in RFC 8628, solves this by moving the login step onto a different device that can actually handle a redirect, like your phone. This means you can delegate your authorization to Strava (on the Garmin watch) through your phone, so the OAuth flow can continue.

In other words, this grant type allows you to give consent on a different device from the one you started the OAuth flow with.

Here’s how the watch gets your Facebook Access Token:

  1. The Garmin watch directly asks the Authorization Server for a device code, instead of redirecting the user. It calls a new endpoint, conventionally /device_authorization, with just its client_id and the scope it wants. There’s no redirect_uri because nothing is ever going to redirect on this device.

    Terminal window
    POST /device_authorization HTTP/1.1
    Host: auth-server.com
    Content-Type: application/x-www-form-urlencoded
    client_id=strava-watch&scope=post
  2. The Authorization Server hands back two codes. A device_code that only the watch will ever see, and a short user_code that’s small enough for a human to type.

    Terminal window
    HTTP/1.1 200 OK
    Content-Type: application/json
    {
    "device_code": "8V1pr0rJ-4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
    "user_code": "WDJB-MJHT",
    "verification_uri": "https://facebook.com/device",
    "verification_uri_complete": "https://facebook.com/device?user_code=WDJB-MJHT",
    "expires_in": 600,
    "interval": 5
    }
  3. The watch shows you the code. Here is what is displayed on the screen of the Garmin watch:

    Go to facebook.com/device
    Enter code: WDJB-MJHT
  4. You approve on your phone. You open that URL in a browser on your phone, log into Facebook like normal, type in WDJB-MJHT, and see the same consent screen from Part 4 asking whether the watch can post on your behalf.

  5. The watch polls in the background while you do this. The watch intermittently polls /token on the Authorization Server every interval seconds (5, in our example) to check whether you’re done yet. Here is what the polling request from the Garmin watch looks like:

    Terminal window
    POST /token HTTP/1.1
    Host: auth-server.com
    Content-Type: application/x-www-form-urlencoded
    grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=8V1pr0rJ-4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk&client_id=strava-watch

    If you are still in the process of approving on your phone, then this is the response that your Garmin watch will get back from the poll:

    Terminal window
    HTTP/1.1 400 Bad Request
    Content-Type: application/json
    {
    "error": "authorization_pending"
    }
  6. The next poll succeeds. The moment you approve, the watch’s next poll gets back an Access Token. Same access_token / refresh_token shape we’ve seen since Part 4, no different from what your phone would have received.

Notice how short that user_code is. WDJB-MJHT has to be short enough for a human to read off a watch face and type into a phone, which also makes it short enough to guess. Section 5.1 of the spec calls this out and recommends that the Authorization Server rate-limit how many codes can be tried at the verification page.

Section 5.4 of the spec is written for exactly this attack. It tells the Authorization Server to confirm the device is genuinely in the user’s possession, which in practice means showing the code on the consent page and asking the user to check that it matches what the watch is displaying. It is the one place in this flow where the security depends on the user actually reading the screen.

Choosing a Grant Type

Pulling these three together, the decision mostly comes down to two questions: is there a specific person delegating access, and does the device that person is holding have a browser?

Grant TypeUser involved?Needs a browser on that device?Refresh Token?
Authorization Code (+ PKCE)YesYesYes
Client CredentialsNoN/ANo (re-request)
Device AuthorizationYes, on a second deviceNoYes

This is exactly what that grant_types array from client registration in Part 4 is for. strava-mobile gets registered with "grant_types": ["authorization_code", "refresh_token"]. The Authorization Server won’t hand out a token through a flow the client wasn’t registered for.

Conclusion

OAuth was designed to be extremely flexible. It accounts for users not being present, devices having no browser to redirect to, etc. There are still many flows that we did not cover, and new ones are being created even today. The three flows we have discussed in this series are the most important, in my opinion.

Whichever grant type a client uses, it lands in the exact same place every other post in this series has led to: an Access Token, checked by a protected resource, scoped to only what the user approved.

My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


OAuth Simplified Series

Comments