👋 Let’s talk about adding SSO to our CLIs, something really useful for different reasons. Single Sign-On (SSO) is not only a common practice nowadays in organisations but is almost a must.
Although it looks challenging for many people at first glance, nowadays the integration has been simplified thanks to the different services that help to abstract you from the complexity of the different flows. When it comes to the benefits of using SSO, there are many but some are very specific for CLI applications.
- Simplifies the authentication process. CLI applications usually run locally in the user’s computer which means they’re probably already using SSO for other services with an active session.
- Allow users to take advantage of a central identity provider (IdP), which could be any of the frequent mainstream social providers like Google, Microsoft, Github or any other over OIDC or SAML protocols.
- Increases security as the user doesn’t need to maintain long-lived secret keys, tokens or credentials for authentication.
- Allow better integration within other systems and services in the organisation.
those, among others, are some of the benefits but each use case it’s different, so what makes CLI session flow different?
Some context
Whereas in a regular application which lives in the cloud the authorisation flow is “simpler”, for a CLI application, where the source code lives in the user’s computer and you can’t exchange a secret, a relatively new flow was defined within the OAuth 2.0 protocol. The “Device authorisation grant”.
This new grant defines a secondary “device” apart from the client where the application is executed. The user manually grants access through that “device”.
+----------+ +----------------+
| |>---(A)-- Client Identifier --->| |
| | | |
| |<---(B)-- Device Code, ---<| |
| | User Code, | |
| Device | & Verification URI | |
| Client | | |
| | [polling] | |
| |>---(E)-- Device Code --->| |
| | & Client Identifier | |
| | | Authorization |
| |<---(F)-- Access Token ---<| Server |
+----------+ (& Optional Refresh Token) | |
v | |
: | |
(C) User Code & Verification URI | |
: | |
v | |
+----------+ | |
| End User | | |
| at |<---(D)-- End user reviews --->| |
| Browser | authorization request | |
+----------+ +----------------+
Figure 1: Device Authorization Flow
Coding time
For learning purpose, I created a new CLI tool with a minimum configuration, and for this specific example I used Auth0 as the Identity Provider (IdP), but you can easily configure it for use with other providers like Okta, Amazon Cognito or others.
If you want to run and test it you’d need at least the following requirements:
- to create you own Navite application in auth0
- Enable social or a database connection
- Register your API
With that, you’d have the parameters needed for the OAuth flow, like client_id or audience.
Request device code
As described in the flow diagram, the first step is to request a temporary device code that can be used to prompt the user for device activation. For that, the IdP give us an endpoint where we can request that device code using the audience and client_id parameters.
def request_device_code() -> DeviceCodeResponse:
headers = {"content-type": "application/x-www-form-urlencoded"}
payload = f"client_id={_APP_CLIENT_ID}&audience={_AUDIENCE}"
response = requests.post(
f"{_TENANT}/oauth/device/code", headers=headers, data=payload
)
response.raise_for_status()
device_code_info = DeviceCodeResponse(**response.json())
_user_authorization_flow(device_code_info=device_code_info)
return device_code_info
User device activation
The device code response
includes different information for enabling the user to activate and manage the CLI session,
@dataclass
class DeviceCodeResponse:
device_code: str
user_code: str
verification_uri: str
expires_in: int
interval: int
verification_uri_complete: str
For this use case, the most relevant is the user_code
along with the verification_uri
, the user can copy&paste and go the
URL or just directly open verification_uri_complete
in a browser to manually activate the CLI. For this step, the user
needs to be logged in with the appropriate credentials, either a social login (google, github, etc) or a database connection.
Request access token
The CLI needs to request and store the access token, meanwhile, the user is manually validating the device, and the CLI is making polling to the token URL provided by the IdP on a defined interval.
def _request_access_token(code: str):
headers = {"content-type": "application/x-www-form-urlencoded"}
payload = (
f"grant_type=urn:ietf:params:oauth:grant-type:device_code&"
f"device_code={code}&client_id={_APP_CLIENT_ID}"
)
response = requests.post(f"{_TENANT}/oauth/token", headers=headers, data=payload)
response.raise_for_status()
return response
def poll_user_verification(code: str, interval_seconds: int) -> ReceivedCredentials | None:
time.sleep(interval_seconds)
valid_response = False
while not valid_response:
try:
response = _request_access_token(code)
valid_response = True
return ReceivedCredentials(**response.json())
except requests.HTTPError as http_error:
if http_error.response.status_code == 403:
print("Retrying device code credentials request")
time.sleep(interval_seconds)
else:
raise http_error
Once the user has successfully authorised the CLI you get a 200 status code with the access_token
in the payload along with
additional parameters depending on the API configuration.
@dataclass
class ReceivedCredentials:
access_token: str
expires_in: int
token_type: str
Call your API with the credentials
The access_token
is a JWT (JSON Web Token) that follows the RFC7519
and you
can validate and verify for providing access to your APIs and resources. For that, you can call your API passing the access_token
as a bearer token in the request header.
Sumary
The code above implements a basic solution for SSO in your CLI, of course, there are different missing pieces like refresh_tokens
,
managing access through scopes
or handling profiles
which could help to understand better how this work in a corporate
environment.
For reference, if you want to follow up on the previous examples, all code snippets present in this post are available at: https://github.com/alexhermida/sample_sso_cli
Cheers!