Practical OAuth Part Two
Aug 04, 2022
This is part two on how to write an OAuth client and server. If you have not read Part One yet, you should take a look at it.
This post covers a lesser-known part of OAuth, which is console-based app authentication. You’ve probably seen it in action with the gcloud and heroku apps. Console-based app OAuth generally follows the same flow as that of a web app (auth code flow) with a few changes thrown in. I will explain how it works along with code examples. The code is written in Python and was tested on the 3.10 version. You can find it here.
So here’s how it works
Register the App
Providers that require a Redirect/Callback URL during registration require this step. You can skip this step if this is not a requirement by your provider. It will need to be done separately from the web app registration (if you’ve registered it previously).
For console-based apps, 127.0.0.1
or localhost
will be the host component of the redirect url because your app will have to run a server for a limited time to make OAuth work. You could also include a path in the url if you want. An example of a redirect url for a terminal-based app during registration is http://127.0.0.1/callback
.
The provider I used for this post (Github) does not need a port number during registration, but you can include it during the User Authorization step. This flexible port number requirement is dependent on the OAuth service you choose. When building an OAuth service, I recommend you have a flexible port number requirement to ease port number choice for apps that use your service.
User Authorization
Your app selects a port number to use. This port number can be a constant variable or selected on the fly. If your OAuth provider allows flexible port numbers on authorization, I recommend you select a random port number. In Python, one way to do this is:
import socket
sock = socket.socket()
sock.bind(('', 0))
port = sock.getsockname()[1]
sock.close()
The code above leaves it to the OS to select a port number that is not in use at the time. Most programming languages have support for this.
The next is to construct a url that looks like this.
https://authorization_server_domain/authorize_path?response_type=code&client_id=CLIENT_ID&scope=scopes&state=RANDOM_STRING&redirect_uri=http://127.0.0.1:port_number/callback_path
I explained the different components of the url in Part One. The only difference between that url and the above is the use of the ip address of the local computer.
After constructing the url, you can display it to the user in the terminal or open it directly in a browser. Most programming languages support opening directly in a browser. In Python, you can do this with the inbuilt webbrowser module.
Here’s a way to use it
import webbrowser
state = generate_state()
temp_cred['state'] = state
redirect_uri = f"http://127.0.0.1:{port}"
github_url = f"https://github.com/login/oauth/authorize?client_id={CLIENT_ID}&scope=read:user&state={state}&redirect_uri={redirect_uri}"
webbrowser.open(github_url)
The state
value is a random string, similar to the web app version.
Authorization Server Sends Authorization Code
When the user grants authentication to the app, the server will generate an authorization code and include it and the state
generated by the app in the callback url. This callback url structure will be like this
http://127.0.0.1:{port}/callback?code=RANDOM_STRING&state=RANDOM_STRING
After constructing the url, the server will redirect to it. One problem though, unlike the normal web app which is within the purview of the browser, this app is a terminal app and can’t just be redirected to it. Luckily, there is a solution! The solution is for the app to listen on its selected port until it gets the redirection request. The server shouldn’t run indefinitely else, it blocks. Once it receives the request, the server should close. Apart from allowing the rest of the code to execute, it also prevents subsequent, maybe harmful requests from coming in.
In Python, you can accomplish this with the inbuilt http.server module. Here’s an example
from http.server import HTTPServer, BaseHTTPRequestHandler
class OAuthServer(BaseHTTPRequestHandler):
def do_GET(self):
get_params(self.path) # validates state param value, then stores code param value
self.send_response(200)
self.end_headers()
self.wfile.write(bytes("<html><head><meta http-equiv='refresh' content='10;url=https://github.com'></head><body>Please return to the app.<script>window.close()</script></body></html>", "utf-8"))
raise KeyboardInterrupt
web_server = HTTPServer(("localhost", port), OAuthServer)
try:
web_server.serve_forever()
except KeyboardInterrupt:
pass
web_server.server_close()
After handling the redirection request, the do_GET
function raises an exception to force the server to close.
App Requests Access Token
The app sends a request for an access token from the authorization server using the code
param value stored by its short-lived server along with its credentials generated at registration time (client_id and client_secret). This request is commonly sent as a POST request in JSON format.
Here’s an example in Python
def get_access_token():
data = {
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'code': temp_cred['code']
}
data = parse.urlencode(data)
data = data.encode('utf-8')
req = request.Request('https://github.com/login/oauth/access_token', data=data)
req.add_header('Accept', 'application/json')
with request.urlopen(req) as f:
json_data = f.read().decode('utf-8')
try:
result = json.loads(json_data)
except json.JSONDecodeError as e:
print(e.msg)
temp_cred['access_token'] = result['access_token'] # store access token
The access token can now be used by the app to make requests with the identity of the user.
Conclusion
Terminal app OAuth is not as complex as it seems in theory. Finding it easy to implement was a lovely surprise. It’s as straightforward as they come.
You can find my implementation on Github. I hope you find it helpful!
Resources
Here are some of the awesome resources on this topic
- The OAuth spec especially, the section on Authorization Code should be read.
- The OAuth 2.0 for Native Apps spec is the resource for this part of OAuth. I covered just a bit of it in this article, mainly the Lookback Interface Redirection section. You should give that a read.
- I found this OAuth for Apps Sample code a great help.