StandCloud third party authorization¶
This documentation describes how to authorize a HardPy application on a StandCloud system using the OAuth 2.0 Device Authorization Flow process.
Authorization process¶
- The application requests the device code from the authorization server.
- The server returns:
device_code
user_code
verification_uri
(authorization URL)verification_uri_complete
(the URL with the pre-filled code)expires_in
(code lifetime)interval
(status polling interval)
- The user follows the link and confirms authorization.
- The application periodically polls the token server.
- After confirmation, the server returns an access token and a refresh token.
Code example¶
The code samples are in Python, but they use simple constructs that are available in most programming languages. To implement the authorization process for a third-party application in a different language, use the general approach demonstrated in these scripts. Reading RFC6749, which describes the OAuth 2.0 device flow, is also recommended. The above examples do not display the QR code to the user; rather, they display a link for authorization.
Authorization example¶
auth_flow.py is an example of a simple authorization script written in Python.
The script requires the installation of the requests
package.
The only thing that needs to be changed to make it work is the
StandCloud address in the BASE_URL
variable.
# This script demonstrates the StandCloud authentication process.
# Change the URL to your StandCloud address, nothing else needs to be changed.
import json
import sys
import time
import requests
########################################
# Change URL to your StandCloud address
BASE_URL = "https://demo.standcloud.io"
########################################
# Confugirable parameters, False if the server is self-signed
SSL_VERIFY = True
# OAuth client configuration
# client_id must be "hardpy-report-uploader"
CLIENT_ID = "hardpy-report-uploader"
# API endpoints
API_URL = BASE_URL + "/hardpy/api/v1"
DEVICE_AUTHORIZATION_URL = BASE_URL + "/auth/api/oidc/device-authorization"
TOKEN_URL = BASE_URL + "/auth/api/oidc/token"
# Device authorization aequest
data = {
"client_id": CLIENT_ID,
"scope": "offline_access authelia.bearer.authz",
"audience": API_URL,
}
req = requests.post(DEVICE_AUTHORIZATION_URL, data=data, verify=SSL_VERIFY, timeout=10)
response = json.loads(req.content)
verification_uri_complete = response["verification_uri_complete"]
interval = response["interval"]
# Token request
if "error" in response:
error = response["error"]
error_description = response["error_description"]
print(f"{error}: {error_description}")
sys.exit(1)
print(f"\nVerification URI: {verification_uri_complete}")
data = {
"client_id": CLIENT_ID,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": response["device_code"],
}
# Token response
while True:
req = requests.post(TOKEN_URL, data=data, verify=SSL_VERIFY, timeout=10)
response = json.loads(req.content)
print(".", end="")
if "access_token" in response:
print("\nToken info:", response)
break
time.sleep(interval)
# Test API call
header = {
"Authorization": "Bearer {}".format(response["access_token"]),
"Content-type": "application/json",
"Accept": "text/plain",
}
print("\nheader: ", header)
USER_INFO_URL = API_URL + "/healthcheck"
response = requests.get(USER_INFO_URL, headers=header, verify=SSL_VERIFY, timeout=10)
if response.status_code != 200:
print(response.text)
sys.exit(1)
print("\nOAuth Authenticated and Authorized API access")
1. Introduction¶
The script facilitates a secure way for devices with limited input capabilities (like command-line applications) to authenticate with StandCloud. It leverages the OAuth 2.0 Device Flow, which allows a user to authorize a device by visiting a URL on a separate, input-rich device (e.g., a web browser on a computer or smartphone).
2. Key concepts: OAuth 2.0 device authorization grant flow¶
The OAuth 2.0 Device Authorization Grant Flow is designed for devices that cannot easily display a browser or accept direct user input (like typing a username and password). The flow generally involves the following steps:
- Device Authorization Request: The client (this script) requests a device code and a verification URI from the authorization server.
- User Interaction: The user takes the verification URI and a user code (provided by the authorization server) and enters them into a browser on a separate device to authorize the client.
- Token Request Polling: The client repeatedly polls the authorization server's token endpoint with the device code until the user completes the authorization.
- Token Response: Once authorized, the client receives access tokens and (optionally) refresh tokens.
- API Access: The client uses the obtained access token to make authenticated calls to the protected API resources.
For a deeper understanding of the OAuth 2.0 Device Authorization Grant, refer to the official specification: RFC 8628 - OAuth 2.0 Device Authorization Grant.
3. Script breakdown¶
The script performs the following sequence of operations:
3.1. Configuration¶
BASE_URL
: This variable must be changed to your specific StandCloud instance URL.- Example:
https://company_name.standcloud.io
- Example:
SSL_VERIFY
: A boolean flag (True
by default) indicating whether SSL certificate verification should be performed. Set toFalse
if your server uses a self-signed certificate.CLIENT_ID
: The OAuth client identifier, which is fixed ashardpy-report-uploader
. This identifies the client application to the StandCloud authentication system.
3.2. API endpoints¶
The script defines several critical API endpoints based on the BASE_URL
:
API_URL
: The base URL for the StandCloud API (/hardpy/api/v1
).DEVICE_AUTHORIZATION_URL
: The endpoint for initiating the device authorization flow (/auth/api/oidc/device-authorization
).TOKEN_URL
: The endpoint for requesting tokens after device authorization (/auth/api/oidc/token
).
3.3. Device authorization request¶
The script first makes a POST
request to the DEVICE_AUTHORIZATION_URL
with the following parameters:
client_id
: TheCLIENT_ID
defined above.scope
: Defines the permissions requested.offline_access
: Allows the client to request refresh tokens, enabling long-term access without re-authorization.authelia.bearer.authz
: A specific scope related to Authelia (an open-source authentication and authorization server often used with OpenID Connect) for bearer token authorization.
audience
: Specifies the intended recipient of the access token, which is theAPI_URL
.
Upon a successful response, the script extracts:
verification_uri_complete
: The full URL the user needs to visit in their browser to authorize the device.interval
: The recommended polling interval (in seconds) for subsequent token requests.device_code
: A code representing the authorization request, used in subsequent token polling.
The script then prints the verification_uri_complete
for the user to access.
It includes basic error handling for the device authorization request.
3.4. Token request (polling)¶
After initiating the device authorization, the script enters a loop to poll
the TOKEN_URL
until authorization is granted by the user.
Each POST
request to the TOKEN_URL
includes:
client_id
: TheCLIENT_ID
.grant_type
: Set to"urn:ietf:params:oauth:grant-type:device_code"
, indicating the Device Flow grant type.device_code
: Thedevice_code
obtained from the initial device authorization request.
The script pauses for the interval
period between each poll.
Once the access_token
is present in the response, the polling loop breaks, and the script proceeds.
3.5. Test API call¶
Finally, the script demonstrates how to use the obtained access_token
to make an authenticated API call.
- An
Authorization
header is constructed with the format"Bearer <access_token>"
. - A
GET
request is made to theAPI_URL
+/healthcheck
endpoint. This is a simple endpoint to verify successful authentication and authorization.
If the API call returns a 200 OK
status, it confirms that the OAuth authentication and
authorization process was successful, and the client can now access protected resources.
Any other status code indicates an issue, and the response text is printed for debugging.
4. Replicating in other languages¶
To implement this authentication flow in a different programming language, you will need to:
- HTTP Client Library: Use an HTTP client library available in your chosen language
(e.g.,
requests
for Python,HttpClient
for C#,fetch
for JavaScript,OkHttp
for Java,net/http
for Go). - JSON Parsing: Implement JSON parsing to handle the responses from the authorization and token endpoints.
- URL Construction: Construct the
DEVICE_AUTHORIZATION_URL
,TOKEN_URL
, andAPI_URL
correctly based on your StandCloud instance. - Request Parameters: Ensure that the
client_id
,scope
,audience
,grant_type
, anddevice_code
parameters are correctly included in yourPOST
request bodies, typically as form-urlencoded data. - Polling Logic: Implement a polling mechanism with appropriate delays to wait for user authorization.
- Bearer Token: Correctly format the
Authorization: Bearer <access_token>
header for protected API calls.
Authorization example with token update¶
token_update.py is a more complex version of the auth_flow.py script,
which rotates access and refresh tokens.
This script stores these tokens in a file.
However, this simplified token storage model should not be used in a production
environment for security reasons.
The only thing that needs to be changed to make it work is the
StandCloud address in the BASE_URL
variable.
# This script demonstrates the StandCloud authentication process with
# token updating. Don't use the save as file option, it's insecure.
# Change the URL to your StandCloud address, nothing else needs to be changed.
import json
import sys
import time
from pathlib import Path
import requests
########################################
# Change URL to your StandCloud address
BASE_URL = "https://demo.standcloud.io"
########################################
# Configurable parameters, False if the server is self-signed
SSL_VERIFY = True
# OAuth client configuration
# client_id must be "hardpy-report-uploader"
CLIENT_ID = "hardpy-report-uploader"
# API endpoints
API_URL = BASE_URL + "/hardpy/api/v1"
DEVICE_AUTHORIZATION_URL = BASE_URL + "/auth/api/oidc/device-authorization"
TOKEN_URL = BASE_URL + "/auth/api/oidc/token"
HEALTHCHECK_URL = API_URL + "/healthcheck"
TOKEN_FILE = Path(__file__).parent / "token_info.json"
def authenticate():
data = {
"client_id": CLIENT_ID,
"scope": "offline_access authelia.bearer.authz",
"audience": API_URL,
}
req = requests.post(
DEVICE_AUTHORIZATION_URL, data=data, verify=SSL_VERIFY, timeout=10
)
response = req.json()
if "error" in response:
error = response["error"]
error_description = response["error_description"]
print(f"{error}: {error_description}")
sys.exit(1)
verification_uri_complete = response["verification_uri_complete"]
interval = response["interval"]
print(f"\nVerification URI: {verification_uri_complete}")
data = {
"client_id": CLIENT_ID,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": response["device_code"],
}
while True:
req = requests.post(TOKEN_URL, data=data, verify=SSL_VERIFY, timeout=10)
response = req.json()
if "access_token" in response:
print("\nAuthentication successful")
break
time.sleep(interval)
# Calculate expiration time
expires_at = time.time() + response["expires_in"]
token_info = {
"access_token": response["access_token"],
"refresh_token": response["refresh_token"],
"expires_at": expires_at,
}
return token_info
def refresh_tokens(refresh_token_value):
data = {
"client_id": CLIENT_ID,
"grant_type": "refresh_token",
"refresh_token": refresh_token_value,
}
try:
req = requests.post(TOKEN_URL, data=data, verify=SSL_VERIFY, timeout=10)
response = req.json()
if "access_token" not in response:
print(
"Token refresh failed. Error:", response.get("error", "unknown error")
)
return None
expires_at = time.time() + response["expires_in"]
token_info = {
"access_token": response["access_token"],
"expires_at": expires_at,
}
# Update refresh token if new one is provided
if "refresh_token" in response:
token_info["refresh_token"] = response["refresh_token"]
print("Access and refresh tokens updated")
else:
print("Access token updated")
return token_info
except Exception as e:
print("Token refresh failed:", str(e))
return None
def healthcheck(access_token):
headers = {
"Authorization": f"Bearer {access_token}",
"Content-type": "application/json",
"Accept": "text/plain",
}
try:
response = requests.get(
HEALTHCHECK_URL, headers=headers, verify=SSL_VERIFY, timeout=10
)
if response.status_code == 200:
print("Healthcheck successful")
return True
print(f"Healthcheck failed: HTTP {response.status_code}")
return False
except Exception as e:
print("Healthcheck error:", str(e))
return False
def save_token_info(token_info):
with Path.open(TOKEN_FILE, "w") as f:
json.dump(token_info, f, indent=4)
def load_token_info():
try:
with Path.open(TOKEN_FILE, "r") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return None
def main():
token_info = load_token_info()
# If no tokens exist or file is corrupted
if (
not token_info
or "access_token" not in token_info
or "refresh_token" not in token_info
):
print("No valid tokens found. Starting authentication...")
token_info = authenticate()
save_token_info(token_info)
healthcheck(token_info["access_token"])
return
# Check access token expiration with 10-second buffer
current_time = time.time()
if current_time < token_info["expires_at"] - 10:
print("Access token is valid")
healthcheck(token_info["access_token"])
return
print("Access token expired. Refreshing tokens...")
new_token_info = refresh_tokens(token_info["refresh_token"])
if new_token_info:
# Preserve existing refresh token if not updated
if "refresh_token" not in new_token_info:
new_token_info["refresh_token"] = token_info["refresh_token"]
save_token_info(new_token_info)
healthcheck(new_token_info["access_token"])
else:
print("Refresh token invalid. Starting re-authentication...")
token_info = authenticate()
save_token_info(token_info)
healthcheck(token_info["access_token"])
if __name__ == "__main__":
main()
1. Script breakdown¶
This script extends the previous basic authentication by adding functions for token management.
1.1. Configuration and endpoints¶
Most configurations are similar to the previous script:
BASE_URL
: StandCloud instance URL.SSL_VERIFY
: For SSL certificate validation.CLIENT_ID
: Fixed as"hardpy-report-uploader"
.API_URL
: Base URL for API calls.DEVICE_AUTHORIZATION_URL
: Endpoint for initiating device flow.TOKEN_URL
: Endpoint for token requests (both initial and refresh).HEALTHCHECK_URL
: A specific endpoint used to test API access.TOKEN_FILE
: APath
object pointing totoken_info.json
in the same directory as the script. This file is used to store token information for persistence. (Remember the security warning regarding file storage).
1.2. authenticate()
function¶
This function encapsulates the initial OAuth 2.0 Device Authorization Grant Flow, identical to the previous script's main logic.
- It initiates a
POST
request toDEVICE_AUTHORIZATION_URL
withclient_id
,scope
(offline_access
is crucial here for getting a refresh token), andaudience
. - It handles the user interaction step, printing the
verification_uri_complete
. - It polls the
TOKEN_URL
using thedevice_code
until anaccess_token
(and importantly, arefresh_token
) is received. - It calculates the
expires_at
timestamp based onexpires_in
and the current time, then returns a dictionary (token_info
) containingaccess_token
,refresh_token
, andexpires_at
.
1.3. refresh_tokens()
function¶
This new function handles the refresh token grant type.
- It constructs a
POST
request to theTOKEN_URL
with:client_id
: Your client identifier.grant_type
: Set to"refresh_token"
.refresh_token
: Therefresh_token_value
obtained from a previous authentication or refresh.
- Upon a successful response, it extracts the
access_token
and its newexpires_at
timestamp. - Crucially, it checks if a new
refresh_token
is provided in the response. If so, it updates the stored refresh token. Some authorization servers issue new refresh tokens with each refresh; others do not. Best practice is to use the newest one if provided. - It returns the
token_info
dictionary. If the refresh fails, it prints an error and returnsNone
.
1.4. healthcheck()
function¶
This helper function tests the validity of an access_token
by making a GET
request to the HEALTHCHECK_URL
.
- It sets the
Authorization
header with theBearer
token. - It returns
True
if the response status is200 OK
, indicating successful API access, andFalse
otherwise.
1.5. save_token_info()
and load_token_info()
functions¶
These functions handle the persistence of token information to and from a local JSON file (token_info.json
).
save_token_info
: Writes thetoken_info
dictionary to theTOKEN_FILE
.load_token_info
: Reads thetoken_info
from theTOKEN_FILE
. It includes error handling forFileNotFoundError
orjson.JSONDecodeError
if the file doesn't exist or is corrupted, returningNone
in such cases.
1.6. main()
¶
This function orchestrates the script's logic.
- Load Existing Tokens: It first attempts to
load_token_info()
from the file. - Initial Authentication: If no valid tokens are found (file doesn't exist,
is corrupted, or essential keys are missing), it calls
authenticate()
to start a new device flow, thensave_token_info()
and performs ahealthcheck
. - Check Token Expiration: If tokens are loaded, it checks if the
access_token
is still valid (with a 10-second buffer before actual expiration).- If valid, it performs a
healthcheck
directly.
- If valid, it performs a
- Token Refresh: If the
access_token
has expired or is about to expire, it callsrefresh_tokens()
using the storedrefresh_token
.- If the refresh is successful, it updates the
token_info
(preserving the refresh token if a new one wasn't issued),save_token_info()
, and performs ahealthcheck
. - If the refresh fails (e.g., refresh token is revoked or expired),
it falls back to starting a new full
authenticate()
flow.
- If the refresh is successful, it updates the
2. Replicating in other languages¶
To implement this robust authentication and token refresh logic in another programming language, you'll need to:
- HTTP Client: Utilize your language's HTTP client for making
POST
andGET
requests. - JSON Handling: Parse JSON responses for token information and API results.
- Time Management: Accurately calculate token expiration times using timestamps.
- Token Storage: Implement a secure method to store
access_token
and especiallyrefresh_token
between application runs. Avoid plain text files in production. Consider:- Environment variables (for secrets, typically during deployment).
- Operating system's credential manager (e.g., macOS Keychain, Windows Credential Manager).
- Encrypted database or file storage.
- Secure vaults (e.g., HashiCorp Vault, AWS Secrets Manager).
- Flow Logic:
- Initial Authentication: Implement the Device Flow as described in the previous documentation.
- Token Expiration Check: Before making an API call, always check if the access token is near expiration.
- Token Refresh Logic: If expired, make a
POST
request to theTOKEN_URL
withgrant_type=refresh_token
and therefresh_token
. - Fallback to Re-authentication: If the refresh token fails, initiate the full Device Flow again.
- Error Handling: Implement robust error handling for network issues, invalid responses, and authentication failures at each step.