Using the IDCS’ OAuth Device Flow for Fun and Profit

Introduction

If you’ve been on the internet recently you’ve probably used OAuth and more specifically the “Authorization Code” grant type (or “AZ Code” if, like The Dude, you are into the whole brevity thing). For example if you’ve ever clicked a “Sign on with Facebook” button or used a Facebook app you’ve used OAuth’s AZ Code grant type, which is sometimes called a “flow”, to allow the site or app to get your identity from Facebook and possibly call Facebook back to get more information about you. The AZ Code flow is used for pretty much any federation on the internet that doesn’t rely on SAML, and it is basically the only means used to allow users to give apps an API tokens (called an Access Token in OAuth parlance). And because of that ubiquity when someone says OAuth they are usually thinking and talking about the AZ Code flow.

But the Authorization Code flow is only one of a bunch of ways to interact with an OAuth server to acquire a token. For example the Resource Owner grant type allows a trusted client to present a username + password and get an Access Token (or AT). And the Client Credentials grant type allows a client to present just its client ID and secret and get a AT. Other people have written about these grant types with IDCS in some depth but that’s not the purpose of this post. What I want to talk about is another, much lesser known flow called the “Device Flow for Browserless and Input Constrained Devices”. That name doesn’t roll off the tongue easily so I usually just call it the “Device Flow” (because I am into the whole brevity thing), and it’s actually a really cool and very useful flow for some very specific and interesting use cases. And it’s a flow you may have already used without knowing it was OAuth.

The draft RFC for the device flow explains it like this:

“This OAuth 2.0 authorization flow is designed for devices that either lack a browser to perform a user-agent based OAuth flow, or are input-constrained to the extent that requiring the user to input a lot of text (like their credentials to authenticate with the authorization server) is impractical. It enables OAuth clients on such devices (like smart TVs, media consoles, digital picture frames, and printers) to obtain user authorization to access protected resources without using an on-device user-agent, provided that they have an Internet connection.”

Or to put it another way: if you need an Access Token (and maybe a Refresh token too), and either there’s no browser OR users can’t easily type in a username & password, then the device flow is probably what you want.

Main Article

The very basics of what the device flow is are described above. But you’re probably not here for just that hand wavey overview, but because you want to know how it actually works.

As I said above the Device Code flow is used when there’s no browser, or it’s too hard to enter a username and password. Remember when I said “you may have already used without knowing it was OAuth”? If you’ve used a Smart TV or a game console to stream content from some online service and part of the process was the TV showing you a code and telling you to go to a URL on your computer and enter a code it was showing you then you have used this flow.

How the Device Flow works (on the wire)

Step 0: Connect to the Internet

Imagine it’s your birthday and you bought yourself a nice new electronic device of some sort. One that doesn’t come with a full keyboard. You open the box up, plug it in, and push the right buttons to get it to connect it to your WiFi network – perhaps you used the WiFi Protected Setup to get connected, or maybe you fought with an on-screen keyboard and 4 cursor keys to enter your (hopefully complicated) Wi-Fi password. Or perhaps the device has an LTE modem built in so as soon as you pop a SIM card in and turn it on it connects to the cellular carrier and thus the internet.

Up next is linking the device to your existing account on the OAuth server. The manufacturer could make you use an on screen keyboard to enter your username and password, and maybe even make you do some sort of strong authentication (OTP, MFA, KBA, etc). But if they want to make your experience actually reasonable they kick off the device flow instead.

Step 1: Get a code

As long as the device is on the internet it can send HTTP requests. So the first step of the flow is for it to send an HTTP POST to the OAuth server’s device endpoint with a few fields formatted as JSON. In IDCS that URL is /oauth2/v1/device and the POST body looks like this:

{
    "response_type": "device_code",
    "scope": "openid",
    "client_id": "3e880dd2af3341f0ae84c899016d38a7"
}

The response_type of “device_code” is the key here. It tells the OAuth server that we want to use the Device Code flow to acquire an Access Token (AT).

I’m using a scope of “openid” even though technically speaking my app isn’t doing OpenID Connect. You can use any valid scope string here, but you probably don’t want to use openid. But since this is just an example that’s fine for now.

The value for client_id comes from an OAuth client application registered in IDCS. That app should look something like this:

The two important bits are that Device Code is checked and the Client Type is set to Public.

After IDCS receives the request it does a bit of work, generates a couple of strings, and then sends back a response that looks like this:

{
    "device_code": "49f4a78f-79f8-4d6d-ae0c-93d3d244b859",
    "user_code": "934TXS",
    "verification_uri": "https://idcs-XXXXXXXX.identity.oraclecloud.com/ui/v1/device",
    "expires_in": 300,
}

The device_code is only known by the device and by IDCS and doesn’t need to be shown to the user. In fact it’s probably a bad idea to show it to the user, but since it’s only usable for a few minutes there is no harm in it appearing in something like a log file. It is used in the next step so the device does need to keep track of it for a few minutes.

The user_code is what is going to be shown to the user. If you recall the entire point of the Device Flow is that the device might not have an easy way to enter a username and password, or really information. So in a moment (step 4) the user is going to wind up entering this value by hand into a web site. So this code will be something short and easy to type in by hand – so 6 or 8 letters and/or numbers.

The verification_url is also shown to the user. The device should show that URL to the user, tell them to open a browser on their computer / phone / tablet to that URL, and enter the user_code. When the user goes to that URL if they’re not logged in they will be asked to do so in order to enter the user_code.

And finally the expires_in tells the device how long the device_code and user_code are valid. Most devices don’t show that information to the user but after the limit is reached it can show the user an error and give them an opportunity to get another code. Or it can just silently get another. I’ll talk more about that in a bit.

There are a couple of other fields that can come back from that request, but in the interest of brevity we can skip over them for now.

The key takeaway is that for this flow to work the device only needs to be able to show the user the user_code and the URL.

Step 2: Device Polls IDCS

After instructing the user to go to the URL and enter the user_code the device should immediately start polling IDCS – POSTing to it every second or so to the token endpoint (in IDCS that’s /oauth2/v1/token).

The POST payload for this contains 3 fields:

{
    "client_id": "3e880dd2af3341f0ae84c899016d38a7",
    "device_code": "49f4a78f-79f8-4d6d-ae0c-93d3d244b859",
    "grant_type": "urn:ietf:params:oauth:grant-type:device_code"
}

The client_id is the same value as above in the initial request.

The grant_type value is the fixed string shown.

And the device_code is the device_code from the initial response.

The first few of these calls are expected to return a message telling the device that IDCS is still waiting for the user to sign in and enter the code. That response looks like this:

{
    "error": "authorization_pending",
    "error_description": "The authorization for this token is pending."
}

The error string “authorization_pending” is described in the spec as a “soft error”. And it isn’t so much an error as the OAuth server (IDCS) saying “not yet, but keep checking in with me please.”

The error_description in this case is intended for the log file or possibly to be shown to the user. But it’s generally of no use and most clients silently discard it.

Step 3: User uses their browser to enter the code

After a second or two the human (you!) should finally get around to pointing a web browser at the URL. As I mentioned above, IDCS will prompt you to login (if you’re not already logged in), and then displays a screen asking for that code from above – remember this is the short (8 digit in this example) user_code:

Since the user might have to go find their laptop, maybe find the power cord, wait for it to boot up, open a browser, type in the URL, type in the URL a second time – this time without typos, and finally log in it’s possible that the device might be polling for a while. The device is going to keep on quietly polling IDCS (step 2) in the background all the while.

Once you enter the code and hit submit IDCS should respond with a screen something like this:

What happened under the covers was that IDCS took that user_code, found the associated device_code and the request that created it, and connected those things together with your account. User codes are one use, so it will also mark the user code as no longer valid so that nobody else can associate the device with their account (before expires_in is reached obviously).

The next time the device polls IDCS with the device code IDCS will be ready to cough up an Access Token with the user’s identity embedded.

Which should happen in a second or so…

Step 4: Polling finishes

Immediately after you hit submit in step 3 IDCS is ready to serve up an Access Token.

And since the device has been waking up every second or so to poll IDCS it’s only a matter of time until it does so again. And when it does it will get a different response than before (in step 2):

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJpb3VzbHk_ISI6IllvdSBkZWNvZGVkIHRoaXMgSldUIGp1c3QgdG8gc2VlIHdoYXQgd2FzIGhlcmU_ISBJIHByb21pc2UgeW91IG5vdGhpbmcgaW50ZXJlc3RpbmciLCJ1c2VyX3R6IjoiQW1lcmljYS9DaGljYWdvIiwic3ViIjoiY2hyaXN0b3BoZXIuam9obnNvbkBvcmFjbGUuY29tIiwidXNlcl9sb2NhbGUiOiJlbiIsInVzZXJfZGlzcGxheW5hbWUiOiJDaHJpc3RvcGhlciBKb2huc29uIiwidXNlci50ZW5hbnQubmFtZSI6InNvbWUgdGVuYW50IG5hbWUiLCJzdWJfbWFwcGluZ2F0dHIiOiJ1c2VyTmFtZSIsImlzcyI6Imh0dHBzOi8vaWRlbnRpdHkub3JhY2xlY2xvdWQuY29tLyIsInRva190eXBlIjoiQVQiLCJ1c2VyX3RlbmFudG5hbWUiOiJzb21lIHRlbmFudCBuYW1lIiwiY2xpZW50X2lkIjoiM2U4ODBkZDJhZjMzNDFmMGFlODRjODk5MDE2ZDM4YTciLCJ1c2VyX2lzQWRtaW4iOnRydWUsImF1ZCI6Imh0dHBzOi8vaWRjcy1YWFhYWFhYWC5pZGVudGl0eS5vcmFjbGVjbG91ZC5jb20iLCJ1c2VyX2lkIjoiOWNhMWQ5YjI2OTk2NDc1MDhiNWI0YmNmMTllNmUwZjAiLCJzdWJfdHlwZSI6InVzZXIiLCJzY29wZSI6Im9wZW5pZCIsImNsaWVudF90ZW5hbnRuYW1lIjoiaWRjcy1YWFhYWFhYWCIsInVzZXJfbGFuZyI6ImVuIiwiZXhwIjoxNTQzMzU1NDU3LCJpYXQiOjE1NDMzNTE4NTcsImNsaWVudF9ndWlkIjoiYWE3ZDY3Y2MxYWY1NDk5Yjg3ZjFjM2U5YmYxNzRiMDciLCJjbGllbnRfbmFtZSI6ImRldmljZSBjb2RlIHRlc3QiLCJ0ZW5hbnQiOiJpZGNzLVhYWFhYWFhYIiwianRpIjoiOTdiMDJkMTItODhiNi00ODA3LTk5MmUtMGNlOWVmZGVmZjY5In0.jsERIekF9OppQxsgPas2fx6UYVTpHZaTh737i4j2sME",
    "expires_in": 3600,
    "token_type": "Bearer"
}

That’s it. You’re done!

The device now has an Access Token that it can use to talk to the Resource Server (RS) as normal. The RS doesn’t even need to know that the AT came via the Device Code flow.

Possible errors

Back in Step 1 I mentioned that the user and device codes are only valid for a limited time, and that the server tells the device how long via the expires_in field.

If the user doesn’t sign in and enter the user_code and your device ignores the time limit and just keeps on polling for too long it will eventually get a response that says that the request was invalid:

{
    "error": "expired_token",
    "error_description": "The device code you provided is invalid."
}

A number of values for error are possible including these from the RFC:

access_denied: “The end-user denied the authorization request.” IDCS doesn’t (generally) do this, but after you enter the code IDCS could show a confirmation screen describing the scopes the device is requesting and allow the user to say yes or no. If the user were to choose no then the entire token acquisition process would be aborted. This error_code value is how IDCS would let the device know this happened. Again this isn’t something IDCS generally does so you probably won’t see this.

slow_down: “The client is polling too quickly and should back off at a reasonable rate.” If your device polls IDCS once a second you’ll never (or should never) see this response. But if you poll much more quickly, say without delaying between checks, then IDCS will tell you to slow down. The RFC requires you to slow down but it doesn’t say what the server should do if you don’t. I can’t tell you what IDCS does if you ignore this error value, but if it tells you to slow_down you really do need to do so.

expired_token: “The ‘device_code’ has expired. The client will need to make a new Device Authorization Request.” This one is self explanatory – it means that you are asking about a device_code after the expires_in clock has passed. At this point your device should stop polling and then either (A) ask the user if they want a new token or (B) just get a new token for the user. (A) is preferred by IDCS but if you have a use to do (B) you should feel free. It is possible to get this response even before the expires_in timer has elapsed but I can’t think of any time that this should happen.

invalid_grant: This one is a generic catch all error. For example if you presented a random string of characters to IDCS as your user_code in step 4 you would get this error.

The “fun and profit” part

The OAuth Device Code flow is used today, most commonly in smart TVs or for media players connected to dumb TVs (or to smart TVs that aren’t very good / are terrible). Entering a username and (strong) password via a remote and on screen QWERTY keyboard using only 4 arrows and an OK button is maddening; I know this because I had to do it last weekend! Thankfully the other services I use do rely on the Device Code flow, and so setting them up was much easier.

But there are plenty of other places you can use the device flow.

For example in a command line app that needs an AT but can’t be trusted to use the Resource Owner grant type; a command line app means no web browser and there’s no way use the AZ Code flow. Or where Resource Owner flow can’t work because users don’t login to IDCS with just a username + password; perhaps users login via federation, or your sign on policy requires strong authentication (TOTP, Push, email code, etc).

Another example is a thin IoT device like a temperature sensor or camera that has no input at all, but does have a small LCD display where it can show the code.

Or a cross platform game, for which you need an account. When you sign on on your console / computer it pops up a log in screen. But also under the covers it could kick off the device flow (step 1) to get a user_code and device_code, and generates a special QR code with that info in it. Then it puts up a message on screen that says “Already playing on your phone? Just scan this QR code to sign in!” Scanning the code would take care of step 3 and the app you’re immediately signed in… like magic!

 

As for the profit, I point you to Ferengi rule of acquisition #74: Knowledge equals profit!

 

Add Your Comment