On a recent vacation, I did a personal hackathon with the goal of demystifying OAuth2 in a fun way. (My boss called this a vacation fail, but in between visiting dormant volcanoes and whale watching, this was the perfect downtime activity for me!)

The result is OZorkAuth. Zork was an early interactive fiction game that first appeared on a DEC PDP-10 in the late ’70s. I first played it on my Commodore 64 in 1983. Zork has been ported to dozens of platforms including the Sony PlayStation and even iPhone. If you can’t tell, I’m a little nostalgic about this game and this genre of games in general.

I’ve been noodling around for a while on the idea of playing these interactive fiction games via an API. The challenge is that these games are played synchronously. That is, in your terminal, you interact with the game and maybe you save the game along the way so you can come back to it.

Zork (and many many other games in this genre) are most commonly run in an interpreted environment called a Z-Machine. This is one of the reasons that these games have been easily ported to so many environments. Using the same game code, all you need is a Z-Machine interpreter that conforms to the standard and you can play the game.

So, I knew that I didn’t want lots of Z-Machines kicking around server side, eating up resources. I decided that on every interaction with the API, I would perform the following steps:

Fire up the Z-Machine Load Zork into it Restore game state (if any had been saved) Execute the game command that had been passed into the API Save game state Return a response to the user showing the result of their move in the game

The restoring and saving game state make it necessary to know who’s who, so authentication is a requirement of the API. Now, I had my in for using OAuth2 to secure the API.

Below, there’s a screencast where I walk you through interacting with the API, including working with two of the main OAuth2 authorization flows. Check that out if you want to see the OAuth2 mechanics in action. Read on if you want to delve into the code.



Getting Setup with Stormpath, Spring Boot and Spring Security

Stormpath’s Spring Boot and Spring Security integration make it dirt simple to have support for OAuth2 out of the box.

By default, Spring Security as well as Stormpath’s Spring Security integration locks down all paths. Additionally, Stormpath requires that all POST endpoints use CSRF protection. The first thing to do is to configure the app to allow certain paths and to disable CSRF protection on some of the POST endpoints. CSRF protection is very important in the context of browsers, but since we will be interacting with the API from the command line, we’ll disable it. Here’s the Spring Security configuration:

SpringSecurityWebAppConfig.java @Configuration public class SpringSecurityWebAppConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .apply(stormpath()).and() .authorizeRequests() .antMatchers("/").permitAll() .antMatchers("/v1/instructions").permitAll() .antMatchers("/v1/r").permitAll().and() .csrf().ignoringAntMatchers("/v1/c").and() .csrf().ignoringAntMatchers("/v1/r"); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration public class SpringSecurityWebAppConfig extends WebSecurityConfigurerAdapter { @Override protected void configure ( HttpSecurity http ) throws Exception { http . apply ( stormpath ( ) ) . and ( ) . authorizeRequests ( ) . antMatchers ( "/" ) . permitAll ( ) . antMatchers ( "/v1/instructions" ) . permitAll ( ) . antMatchers ( "/v1/r" ) . permitAll ( ) . and ( ) . csrf ( ) . ignoringAntMatchers ( "/v1/c" ) . and ( ) . csrf ( ) . ignoringAntMatchers ( "/v1/r" ) ; } }

Line 7 is all that is needed to enable the Stormpath Spring Security integration.

Lines 9 – 11 allow unauthenticated access to the / , /v1/instructions , /v1/r endpoints

Lines 12 & 13 disable CSRF protection for /v1/c and /v1/r . Those are the endpoints that we will be POST’ing to.

What are these endpoints? There’s 5 in total for the OZorkAuth API:

endpoint purpose / redirects to /v1/instructions /v1/instructions responds with JSON containing insturctions for how to interact with the API /v1/r register a new account (nothing to do with OAuth2) /v1/c issue a command to the game /v1/a auth endpoint for handling all supported OAuth2 flows

You may be wondering why there’s no /v1/a reference in the configuration above. It’s because that endpoint handles all of our OAuth2 flows and is supported by the Spring Boot and Spring Security integration without any additional coding.

By default, the OAuth2 endpoint for Stormpath enabled Spring Boot applications is /oauth/token . So, we just need to override the default in our application.properties file:

stormpath.web.accessToken.uri = /v1/a

Let’s take a look at the controller definition for the /v1/c endpoint. That’s what we hit to play the game:

GameController.java ... @RequestMapping(value = "/v1/c", method = RequestMethod.POST) public CommandResponse command(@RequestBody(required = false) CommandRequest commandRequest, HttpServletRequest req) throws IOException { Account account = AccountResolver.INSTANCE.getAccount(req); String zMachineRequest = (commandRequest != null) ? commandRequest.getRequest() : null; StringBuffer zMachineCommands = new StringBuffer(); //check for restart if ("restart".equals(zMachineRequest)) { gameService.restart(account); zMachineRequest = null; } else { // restore games state from customData, if it exists gameService.loadGameState(zMachineCommands, account); } // we always want to look zMachineCommands.append("look

"); // setup passed in command if (zMachineRequest != null) { zMachineCommands.append(zMachineRequest + "

"); zMachineCommands.append("save

"); } // execute game move (which may just be looking) String zMachineResponse = gameService.doZMachine(zMachineCommands, account); CommandResponse res = gameService.processZMachineResponse(zMachineRequest, zMachineResponse); if (zMachineRequest != null) { gameService.saveGameState(account); } gameService.cleanup(account); // return response return res; } ... 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 . . . @RequestMapping ( value = "/v1/c" , method = RequestMethod . POST ) public CommandResponse command ( @RequestBody ( required = false ) CommandRequest commandRequest , HttpServletRequest req ) throws IOException { Account account = AccountResolver . INSTANCE . getAccount ( req ) ; String zMachineRequest = ( commandRequest ! = null ) ? commandRequest . getRequest ( ) : null ; StringBuffer zMachineCommands = new StringBuffer ( ) ; //check for restart if ( "restart" . equals ( zMachineRequest ) ) { gameService . restart ( account ) ; zMachineRequest = null ; } else { // restore games state from customData, if it exists gameService . loadGameState ( zMachineCommands , account ) ; } // we always want to look zMachineCommands . append ( "look

" ) ; // setup passed in command if ( zMachineRequest ! = null ) { zMachineCommands . append ( zMachineRequest + "

" ) ; zMachineCommands . append ( "save

" ) ; } // execute game move (which may just be looking) String zMachineResponse = gameService . doZMachine ( zMachineCommands , account ) ; CommandResponse res = gameService . processZMachineResponse ( zMachineRequest , zMachineResponse ) ; if ( zMachineRequest ! = null ) { gameService . saveGameState ( account ) ; } gameService . cleanup ( account ) ; // return response return res ; } . . .

NOTE: in all the examples below, I use the command line HTTP tool called httpie. You can use any HTTP tool, including curl on the command line and Postman in the browser or desktop app.

If you’re on Mac, you can easily install httpie with: brew install httpie .

You may notice a lack of any authentication specific code above. We have Spring Security and the Stormpath integration to thank for that. Because we did not explicitly allow the /v1/c endpoint in our configuration above, that path requires that the user be authenticated. If they’re not authenticated, they get back an error:

➥http POST https://ozorkauth.herokuapp.com/v1/c HTTP/1.1 403 Forbidden { "error": "Forbidden", "message": "Access Denied", "path": "/v1/c", "status": 403, "timestamp": 1462400533833 } 1 2 3 4 5 6 7 8 9 10 ➥ http POST https : // ozorkauth .herokuapp .com / v1 / c HTTP / 1.1 403 Forbidden { "error" : "Forbidden" , "message" : "Access Denied" , "path" : "/v1/c" , "status" : 403 , "timestamp" : 1462400533833 }

Given that you can only enter the controller method if you’ve authenticated, line 5 of the controller code above ensures that we have access to the Stormpath Account of the authenticated user. We need that Account to restore and save game state to.

In the screencast above, I go into detail about the OAuth2 workflows involved in interacting with the game. Here, I will provide an overview of what it looks like.

In order to hit the /v1/c endpoint, you need an OAuth2 access token. This “counts” as being logged in, in the same way as if you’d gone to a login page and input your username and password.

OAuth2 Made Easy (and Fun)

The different OAuth2 flows covered here are all authorizations called grant types. To get an access token, we’ll use the password grant type.

Here’s what the command looks like:

➥http -v -f POST \ https://ozorkauth.herokuapp.com/v1/a \ Origin:https://ozorkauth.herokuapp.com \ grant_type=password username=zork@stormpath.com password=Passw0rd 1 2 3 4 ➥ http - v - f POST \ https : // ozorkauth .herokuapp .com / v1 / a \ Origin : https : // ozorkauth .herokuapp .com \ grant_type = password username = zork @ stormpath .com password = Passw0rd

On line 1, the -f command means that this will be a form submission. The impact of that is that the Content-Type in the request is set to: application/x-www-form-url-encoded . This is a requirement of the OAuth2 specification as outlined here. Line 1 also makes the HTTP method type POST – also a requirement of the spec.

Line 2 is the endpoint we are going to hit.

Line 3 specifies the Origin header. This is required by Stormpath as one of the tools to guard against CSRF attacks when using the browser.

Line 4 is the main event. We are indicating to our OAuth2 Service (Stormpath), that the authorization type is grant_type=password . And, we are passing in the username and password .

Here’s the request and response (some lines removed for brevity):

POST /v1/a HTTP/1.1 ... Content-Type: application/x-www-form-urlencoded; charset=utf-8 Origin: https://ozorkauth.herokuapp.com grant_type=password&username=zork@stormpath.com&password=Passw0rd HTTP/1.1 200 OK ... { "access_token": "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ...", "expires_in": 3600, "refresh_token": "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ...", "token_type": "Bearer" } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST / v1 / a HTTP / 1.1 . . . Content - Type : application / x - www - form - urlencoded ; charset = utf - 8 Origin : https : // ozorkauth .herokuapp .com grant_type = password &username = zork @ stormpath .com &password = Passw0rd HTTP / 1.1 200 OK . . . { "access_token" : "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ..." , "expires_in" : 3600 , "refresh_token" : "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ..." , "token_type" : "Bearer" }

As you can see, we get back an access token. We also get back a refresh token. I’ll talk about the use of the refresh token below. I can now use the access token to send commands to the protected /v1/c endpoint. First, I will save the tokens in environment variables to make the following httpie commands easier to work with.

ACCESS_TOKEN=eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ... REFRESH_TOKEN=eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ... 1 2 3 ACCESS_TOKEN = eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ . . . REFRESH_TOKEN = eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ . . .

Here’s the same httpie command we sent earlier that resulted in a 403 response. This time, we are including the access token in the Authorization header as a Bearer token. Bearer is the authorization keyword that, when followed by the access token, triggers Stormpath to lookup the access token, validate it and retrieve the Account associated with it.

➥http -v POST \ https://ozorkauth.herokuapp.com/v1/c \ Authorization:"Bearer $ACCESS_TOKEN" 1 2 3 ➥ http - v POST \ https : // ozorkauth .herokuapp .com / v1 / c \ Authorization : "Bearer $ACCESS_TOKEN"

Here’s the response:

HTTP/1.1 200 OK ... { "gameInfo": [ "ZORK I: The Great Underground Empire", "Copyright (c) 1981, 1982, 1983 Infocom, Inc. All rights reserved.", "ZORK is a registered trademark of Infocom, Inc.", "Revision 88 / Serial number 840726" ], "look": [ "West of House", "You are standing in an open field west of a white house, with a boarded front door.", "There is a small mailbox here." ], "status": "SUCCESS" } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 HTTP / 1.1 200 OK . . . { "gameInfo" : [ "ZORK I: The Great Underground Empire" , "Copyright (c) 1981, 1982, 1983 Infocom, Inc. All rights reserved." , "ZORK is a registered trademark of Infocom, Inc." , "Revision 88 / Serial number 840726" ] , "look" : [ "West of House" , "You are standing in an open field west of a white house, with a boarded front door." , "There is a small mailbox here." ] , "status" : "SUCCESS" }

Huzzah! We did it! We are now interacting with the game through its protected endpoint. Since I just hit the endpoint above with no other parameters, it just responds with a look at my surroundings in the game. This time, I will give it a command:

➥http -v POST \ https://ozorkauth.herokuapp.com/v1/c \ Authorization: "Bearer $ACCESS_TOKEN" \ request="go north" 1 2 3 4 ➥ http - v POST \ https : // ozorkauth .herokuapp .com / v1 / c \ Authorization : "Bearer $ACCESS_TOKEN" \ request = "go north"

Notice on line 4, there’s request="go north" . Here’s the response (some lines removed for brevity):

HTTP/1.1 200 OK ... { "gameInfo": [ ... ], "look": [ ... ], "request": "go north", "response": [ "North of House", "You are facing the north side of a white house. There is no door here, and all the windows are boarded up. To the north a narrow path winds through the trees." ], "status": "SUCCESS" } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 HTTP / 1.1 200 OK . . . { "gameInfo" : [ . . . ] , "look" : [ . . . ] , "request" : "go north" , "response" : [ "North of House" , "You are facing the north side of a white house. There is no door here, and all the windows are boarded up. To the north a narrow path winds through the trees." ] , "status" : "SUCCESS" }

Ok! Now you can play the game in earnest. However, at some point your access token is going to expire. You could just do the grant_type=password OAuth2 authorization flow again to get a new one. However, there’s another flow: grant_type=refresh_token . The sole purpose of the refresh token is to get a new access token. Using the refresh token is better for two reasons: 1) It greatly reduces the number of times you need to pass your password over the wire and 2) in conjunction with access tokens, it allows for a finer grain of control over a user’s session.

In a typical mobile app that uses OAuth2, when the access token expires, it will automatically use the refresh token to obtain a new one. This is done behind the scenes with you as a user none the wiser. When the refresh token finally expires, then you will have to log in again. It’s also common to validate the access token locally. That is, rather than having to hit the Stormpath API, the token can be validated in the Spring Boot app. This is because the tokens that are generated are cryptographically signed JWTs and can therefore be verified using the secret key, which your Spring Boot application is aware of.

This brings us back to the 2) above. Imagine a scenario where you have a user that’s misbehaving and you want to revoke their access. In this scenario, you have a long lived access token and no refresh token. If you are using local validation, you would have to wait until the access token expired before the user is kicked off the system (when they try to login again to get a new access token). Now, imagine the same scenario, only this time you have a short lived access token and a long lived refresh token. Just like before you revoke their access. Now, when their access token expires and they attempt to get a new one, they’ll get an error. So, at most they’ll have access to the system for the lifespan of their access token, which has been set to be short lived.

Ok. As our final delve into the OAuth2 flows in the OZorkAuth game, let’s use the refresh token we got earlier to get a new access token.

➥http -v -f POST \ https://ozorkauth.herokuapp.com/v1/a \ Origin:https://ozorkauth.herokuapp.com \ grant_type=refresh_token refresh_token=$REFRESH_TOKEN 1 2 3 4 ➥ http - v - f POST \ https : // ozorkauth .herokuapp .com / v1 / a \ Origin : https : // ozorkauth .herokuapp .com \ grant_type = refresh_token refresh_token = $ REFRESH_TOKEN

Notice on line 4 we use the grant_type=refresh_token and we pass in the refresh token we got earlier. Here’s the response:

HTTP/1.1 200 OK ... { "access_token": "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ...", "expires_in": 3600, "refresh_token": "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ...", "token_type": "Bearer" } 1 2 3 4 5 6 7 8 HTTP / 1.1 200 OK . . . { "access_token" : "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ..." , "expires_in" : 3600 , "refresh_token" : "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwiYWxnIjoiSFMyNTYifQ..." , "token_type" : "Bearer" }

This looks just like what we saw before when using the grant_type=password authorization flow. If you look at the full output, you will see that the refresh token has not changed while the access token is brand new.

Don’t Get Eaten by a Grue

I hope this has taken some of the mystery and complexity out of using OAuth2. All of the code in the OZorkAuth repo is focused on the gameplay. No coding at all was needed to support using the OAuth2 workflows as they are built into the Stormpath Spring Boot and Spring Security integrations.

Finally, you may be wondering just how game state is being saved. Stormpath has a feature called Custom Data that allows you to store up to 10MB of JSON data for each Account. Three lines of code save the game state each time you issue a command:

String saveFile = Base64.getEncoder().encodeToString(fileData); account.getCustomData().put("zMachineSaveData", saveFile); account.getCustomData().save(); 1 2 3 String saveFile = Base64 . getEncoder ( ) . encodeToString ( fileData ) ; account . getCustomData ( ) . put ( "zMachineSaveData" , saveFile ) ; account . getCustomData ( ) . save ( ) ;

The fileData variable is a byte array that is the game state produced by the Z-Machine. Line 1 Base64-encodes the byte array so it can be stored as plaintext. Line 2 puts the plaintext into the Custom Data associated with the Account and line 3 saves the Custom Data.

In this way what is usually a synchronous game is made asynchronous which is more suited to HTTP interactions. The Z-Machine is created and torn down on each request.

Further Reading

Have fun playing the game and drop me a line if you have any questions or comments!