Skip to content

Commit 4c58b59

Browse files
committed
Add new cupsOAuthGetDeviceGrant API to support using device authorization grants
and fix cupsOAuthGetTokens with device authorization codes.
1 parent 3ae7d9f commit 4c58b59

3 files changed

Lines changed: 213 additions & 14 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Changes in libcups
44
libcups v3.0.0 (YYYY-MM-DD)
55
---------------------------
66

7-
- Added `cupsOAuthGetJWKS` and `cupsOAuthGetUserId` APIs.
7+
- Added `cupsOAuthGetDeviceGrant`, `cupsOAuthGetJWKS`, and `cupsOAuthGetUserId`
8+
APIs.
89
- Added `httpGetCookieValue` and `httpGetSecurity` APIs.
910
- Updated documentation (Issue #113)
1011
- Updated the `cupsOAuth` APIs to support sharing of some OAuth values between
@@ -18,6 +19,7 @@ libcups v3.0.0 (YYYY-MM-DD)
1819
- Fixed a bug in the Avahi implementation of `cupsDNSSDBrowseNew`.
1920
- Fixed a memory leak in `httpClose`.
2021
- Fixed some Coverity-detected issues.
22+
- Fixed support for device authorization grants.
2123

2224

2325
libcups v3.0rc4 (2025-03-18)

cups/oauth.c

Lines changed: 196 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,159 @@ cupsOAuthGetClientId(
783783
}
784784

785785

786+
//
787+
// 'cupsOAuthGetDeviceGrant()' - Get a device authorization grant for the specified resource and scope(s).
788+
//
789+
// This function requests a device authorization grant for the specified
790+
// resource and scope(s). Device authorization grants allow a user to open a
791+
// web page on any device to authorize access to the resource.
792+
//
793+
// The "auth_uri" parameter specifies the URI for the OAuth Authorization
794+
// Server. The "metadata" parameter specifies the Authorization Server metadata
795+
// as obtained using @link cupsOAuthCopyMetadata@ and/or
796+
// @link cupsOAuthGetMetadata@.
797+
//
798+
// The "resource_uri" parameter specifies the URI for a resource (printer, web
799+
// file, etc.) that you which to access.
800+
//
801+
// The "scopes" parameter specifies zero or more whitespace-delimited scope
802+
// names to request during authorization. The list of supported scope names are
803+
// available from the Authorization Server metadata, for example:
804+
//
805+
// ```
806+
// cups_json_t *metadata = cupsOAuthGetMetadata(auth_uri);
807+
// cups_json_t *scopes_supported = cupsJSONFind(metadata, "scopes_supported");
808+
// ```
809+
//
810+
// The returned JSON object must be freed using the @link cupsJSONDelete@
811+
// function and contains the following information:
812+
//
813+
// - `CUPS_ODEVGRANT_DEVICE_CODE`: The device code string to be used in
814+
// subsequent @link cupsOAuthGetTokens@ calls.
815+
// - `CUPS_ODEVGRANT_EXPIRES_IN`: The expiration date/time as a number of
816+
// seconds since the Unix epoch.
817+
// - `CUPS_ODEVGRANT_INTERVAL`: The number of seconds to wait between calls to
818+
// @link cupsOAuthGetTokens@.
819+
// - `CUPS_ODEVGRANT_USER_CODE`: The user code to enter on the verification
820+
// web page.
821+
// - `CUPS_ODEVGRANT_VERIFICATION_URL`: The URL for the verification web page.
822+
// - `CUPS_ODEVGRANT_VERIFICATION_URL_COMPLETE`: The URL for the verification
823+
// web page with the user code filled in.
824+
//
825+
// The values can be obtained using the @link cupsJSONFind@,
826+
// @cupsJSONGetNumber@, and @cupsJSONGetString@ functions, for example:
827+
//
828+
// ```
829+
// cups_json_t *grant = cupsOAuthGetDeviceGrant(...);
830+
//
831+
// const char *verification_url = cupsJSONGetString(cupsJSONFind(grant, CUPS_ODEVGRANT_VERIFICATION_URL));
832+
// double interval = cupsJSONGetNumber(cupsJSONFind(grant, CUPS_ODEVGRANT_INTERVAL));
833+
// ```
834+
//
835+
836+
cups_json_t * // O - Grant data or `NULL` on error
837+
cupsOAuthGetDeviceGrant(
838+
const char *auth_uri, // I - Authorization Server URI
839+
cups_json_t *metadata, // I - Authorization Server metadata
840+
const char *resource_uri, // I - Resource URI
841+
const char *scopes) // I - Space-delimited scopes
842+
{
843+
const char *device_ep; // Device authorization endpoint
844+
char *client_id = NULL, // `client_id` value
845+
*scopes_supported = NULL;
846+
// Supported scopes
847+
size_t num_form = 0; // Number of form variables
848+
cups_option_t *form = NULL; // Form variables
849+
char *request = NULL; // Form request data
850+
cups_json_t *grant = NULL; // Device grant
851+
852+
853+
// Range check input...
854+
DEBUG_printf("cupsOAuthGetDeviceGrant(auth_uri=\"%s\", metadata=%p, resource_uri=\"%s\", scopes=\"%s\")", auth_uri, (void *)metadata, resource_uri, scopes);
855+
856+
if (!auth_uri || !metadata || (device_ep = cupsJSONGetString(cupsJSONFind(metadata, "device_authorization_endpoint"))) == NULL)
857+
{
858+
_cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Device authorization grant is not supported by this server."), true);
859+
return (NULL);
860+
}
861+
862+
// Get the client_id value...
863+
if ((client_id = cupsOAuthCopyClientId(auth_uri, NULL)) == NULL)
864+
{
865+
_cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("The client ID is not configured for this server."), true);
866+
return (NULL);
867+
}
868+
869+
DEBUG_printf("1cupsOAuthGetDeviceGrant: client_id=\"%s\"", client_id);
870+
871+
// Get the scopes value(s)...
872+
if (!scopes)
873+
{
874+
scopes_supported = oauth_copy_scopes(metadata);
875+
scopes = scopes_supported;
876+
}
877+
878+
DEBUG_printf("1cupsOAuthGetDeviceGrant: scopes=\"%s\"", scopes);
879+
880+
// Build the request...
881+
num_form = cupsAddOption("client_id", client_id, num_form, &form);
882+
if (scopes)
883+
num_form = cupsAddOption("scope", scopes, num_form, &form);
884+
if (resource_uri)
885+
num_form = cupsAddOption("resource", resource_uri, num_form, &form);
886+
887+
if ((request = cupsFormEncode(/*url*/NULL, num_form, form)) != NULL)
888+
{
889+
// Send the device authorization grant request...
890+
if ((grant = oauth_do_post(device_ep, "application/x-www-form-urlencoded", request)) != NULL)
891+
{
892+
// Make sure we have any optional values in the returned JSON...
893+
const char *user_code = cupsJSONGetString(cupsJSONFind(grant, CUPS_ODEVGRANT_USER_CODE));
894+
const char *verification_url = cupsJSONGetString(cupsJSONFind(grant, CUPS_ODEVGRANT_VERIFICATION_URL));
895+
896+
if (!cupsJSONFind(grant, CUPS_ODEVGRANT_DEVICE_CODE) && !cupsJSONFind(grant, CUPS_ODEVGRANT_EXPIRES_IN) && !user_code && !verification_url)
897+
{
898+
// Missing required bits, treat this as an error...
899+
_cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(EINVAL), false);
900+
cupsJSONDelete(grant);
901+
grant = NULL;
902+
}
903+
else
904+
{
905+
// Add default 5 second interval...
906+
if (!cupsJSONFind(grant, CUPS_ODEVGRANT_INTERVAL))
907+
cupsJSONNewNumber(grant, cupsJSONNewKey(grant, /*after*/NULL, CUPS_ODEVGRANT_INTERVAL), 5.0);
908+
909+
// Add complete verification URL based on base URL
910+
if (!cupsJSONFind(grant, CUPS_ODEVGRANT_VERIFICATION_URL_COMPLETE))
911+
{
912+
char *complete_url; // Complete verification URL
913+
914+
cupsFreeOptions(num_form, form);
915+
form = NULL;
916+
num_form = cupsAddOption("user_code", user_code, 0, &form);
917+
918+
if ((complete_url = cupsFormEncode(verification_url, num_form, form)) != NULL)
919+
{
920+
cupsJSONNewString(grant, cupsJSONNewKey(grant, /*after*/NULL, CUPS_ODEVGRANT_VERIFICATION_URL_COMPLETE), complete_url);
921+
free(complete_url);
922+
}
923+
}
924+
}
925+
}
926+
}
927+
928+
// Free allocated stuff and return the device authorization grant, if any...
929+
cupsFreeOptions(num_form, form);
930+
931+
free(client_id);
932+
free(request);
933+
free(scopes_supported);
934+
935+
return (grant);
936+
}
937+
938+
786939
//
787940
// 'cupsOAuthGetJWKS()' - Get the JWT key set for an Authorization Server.
788941
//
@@ -1046,15 +1199,20 @@ cupsOAuthGetMetadata(
10461199
// @link cupsOAuthCopyRefreshToken@ and @link cupsOAuthCopyUserId@ functions
10471200
// respectively.
10481201
//
1202+
// When authorizing using a device code (`CUPS_OGRANT_DEVICE_CODE`) and a device
1203+
// access token is not yet ready, a `NULL` access token is returned with the
1204+
// expiration time set to the next recommended query time. If the
1205+
// "access_expires" value is set to `0` then the device authorization failed.
1206+
//
10491207

10501208
char * // O - Access token or `NULL` on error
10511209
cupsOAuthGetTokens(
10521210
const char *auth_uri, // I - Authorization Server URI
10531211
cups_json_t *metadata, // I - Authorization Server metadata
10541212
const char *resource_uri, // I - Resource URI
1055-
const char *grant_code, // I - Authorization code or refresh token
1213+
const char *grant_code, // I - Authorization code, device code, or refresh token
10561214
cups_ogrant_t grant_type, // I - Grant code type
1057-
const char *redirect_uri, // I - Redirect URI
1215+
const char *redirect_uri, // I - Redirect URI or `NULL` for device grants
10581216
time_t *access_expires) // O - Expiration time for access token
10591217
{
10601218
const char *token_ep; // Token endpoint
@@ -1065,6 +1223,7 @@ cupsOAuthGetTokens(
10651223
char *request = NULL; // Form request data
10661224
cups_json_t *response = NULL; // JSON response variables
10671225
const char *access_value = NULL, // access_token
1226+
*error, // Error code, if any
10681227
*id_value = NULL, // id_token
10691228
*refresh_value = NULL; // refresh_token
10701229
double expires_in; // expires_in value
@@ -1086,22 +1245,29 @@ cupsOAuthGetTokens(
10861245
if (access_expires)
10871246
*access_expires = 0;
10881247

1089-
if (!auth_uri || !metadata || (token_ep = cupsJSONGetString(cupsJSONFind(metadata, "token_endpoint"))) == NULL || !grant_code || !redirect_uri)
1248+
if (!auth_uri || !metadata || (token_ep = cupsJSONGetString(cupsJSONFind(metadata, "token_endpoint"))) == NULL || !grant_code || (!redirect_uri && grant_type != CUPS_OGRANT_DEVICE_CODE))
10901249
return (NULL);
10911250

10921251
// Prepare form data to get an access token...
10931252
num_form = cupsAddOption("grant_type", grant_types[grant_type], num_form, &form);
1094-
num_form = cupsAddOption("code", grant_code, num_form, &form);
1095-
1096-
if (!strcmp(redirect_uri, CUPS_OAUTH_REDIRECT_URI) && (value = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_REDIRECT_URI, /*try_sysconfig*/false)) != NULL)
1253+
if (grant_type == CUPS_OGRANT_DEVICE_CODE)
10971254
{
1098-
DEBUG_printf("1cupsOAuthGetTokens: redirect_uri=\"%s\"", value);
1099-
num_form = cupsAddOption("redirect_uri", value, num_form, &form);
1100-
free(value);
1255+
num_form = cupsAddOption("device_code", grant_code, num_form, &form);
11011256
}
11021257
else
11031258
{
1104-
num_form = cupsAddOption("redirect_uri", redirect_uri, num_form, &form);
1259+
num_form = cupsAddOption("code", grant_code, num_form, &form);
1260+
1261+
if (!strcmp(redirect_uri, CUPS_OAUTH_REDIRECT_URI) && (value = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_REDIRECT_URI, /*try_sysconfig*/false)) != NULL)
1262+
{
1263+
DEBUG_printf("1cupsOAuthGetTokens: redirect_uri=\"%s\"", value);
1264+
num_form = cupsAddOption("redirect_uri", value, num_form, &form);
1265+
free(value);
1266+
}
1267+
else
1268+
{
1269+
num_form = cupsAddOption("redirect_uri", redirect_uri, num_form, &form);
1270+
}
11051271
}
11061272

11071273
if ((value = cupsOAuthCopyClientId(auth_uri, redirect_uri)) != NULL)
@@ -1118,7 +1284,7 @@ cupsOAuthGetTokens(
11181284
free(value);
11191285
}
11201286

1121-
if ((value = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_CODE_VERIFIER, /*try_sysconfig*/false)) != NULL)
1287+
if (grant_type != CUPS_OGRANT_DEVICE_CODE && (value = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_CODE_VERIFIER, /*try_sysconfig*/false)) != NULL)
11221288
{
11231289
DEBUG_printf("1cupsOAuthGetTokens: code_verifier=\"%s\"", value);
11241290
num_form = cupsAddOption("code_verifier", value, num_form, &form);
@@ -1134,11 +1300,28 @@ cupsOAuthGetTokens(
11341300
if ((response = oauth_do_post(token_ep, "application/x-www-form-urlencoded", request)) == NULL)
11351301
goto done;
11361302

1303+
error = cupsJSONGetString(cupsJSONFind(response, "error"));
11371304
access_value = cupsJSONGetString(cupsJSONFind(response, "access_token"));
11381305
expires_in = cupsJSONGetNumber(cupsJSONFind(response, "expires_in"));
11391306
id_value = cupsJSONGetString(cupsJSONFind(response, "id_token"));
11401307
refresh_value = cupsJSONGetString(cupsJSONFind(response, "refresh_token"));
11411308

1309+
if (error)
1310+
{
1311+
// Handle "soft" device access token errors by setting access_expires to
1312+
// the next call time...
1313+
if (!strcmp(error, "slow_down"))
1314+
*access_expires = time(NULL) + 10;
1315+
else
1316+
*access_expires = time(NULL) + 5;
1317+
1318+
// Free memory and return...
1319+
cupsJSONDelete(response);
1320+
free(request);
1321+
1322+
return (NULL);
1323+
}
1324+
11421325
if (id_value)
11431326
{
11441327
// Validate the JWT
@@ -1720,7 +1903,7 @@ oauth_do_post(const char *ep, // I - Endpoint URI
17201903

17211904
// Check for errors...
17221905
resp_error = oauth_set_error(resp_json, /*num_form*/0, /*form*/NULL);
1723-
if (!resp_error && status != HTTP_STATUS_OK)
1906+
if (!resp_error && !cupsJSONFind(resp_json, "error") && status != HTTP_STATUS_OK)
17241907
{
17251908
_cupsSetError(IPP_STATUS_ERROR_INTERNAL, httpStatusString(status), false);
17261909
resp_error = true;
@@ -2125,7 +2308,7 @@ oauth_set_error(cups_json_t *json, // I - JSON response
21252308
error_desc = cupsGetOption("error_description", num_form, form);
21262309
}
21272310

2128-
if (error)
2311+
if (error && strcmp(error, "authorization_pending") && strcmp(error, "slow_down"))
21292312
{
21302313
if (error_desc)
21312314
{

cups/oauth.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ extern "C" {
2222
# define CUPS_OAUTH_REDIRECT_URI "http://127.0.0.1/"
2323
// Redirect URI for local authorization
2424

25+
# define CUPS_ODEVGRANT_DEVICE_CODE "device_code"
26+
// The device code string
27+
# define CUPS_ODEVGRANT_EXPIRES_IN "expires_in"
28+
// The expiration date/time of the device code
29+
# define CUPS_ODEVGRANT_INTERVAL "interval"
30+
// The requested number of seconds between token calls
31+
# define CUPS_ODEVGRANT_USER_CODE "user_code"
32+
// The user code string for authorization
33+
# define CUPS_ODEVGRANT_VERIFICATION_URL "verification_url"
34+
// The URL for the verification web page
35+
# define CUPS_ODEVGRANT_VERIFICATION_URL_COMPLETE "verification_url_complete"
36+
// The URL for the verifiction web page with the user code filled in
37+
2538

2639
//
2740
// Types...
@@ -47,6 +60,7 @@ extern cups_jwt_t *cupsOAuthCopyUserId(const char *auth_uri, const char *resourc
4760

4861
extern char *cupsOAuthGetAuthorizationCode(const char *auth_uri, cups_json_t *metadata, const char *resource_uri, const char *scopes, const char *redirect_uri) _CUPS_PUBLIC;
4962
extern char *cupsOAuthGetClientId(const char *auth_uri, cups_json_t *metadata, const char *redirect_uri, const char *logo_uri, const char *tos_uri) _CUPS_PUBLIC;
63+
extern cups_json_t *cupsOAuthGetDeviceGrant(const char *auth_uri, cups_json_t *metadata, const char *resource_uri, const char *scopes) _CUPS_PUBLIC;
5064
extern cups_json_t *cupsOAuthGetJWKS(const char *auth_uri, cups_json_t *metadata) _CUPS_PUBLIC;
5165
extern cups_json_t *cupsOAuthGetMetadata(const char *auth_uri) _CUPS_PUBLIC;
5266
extern char *cupsOAuthGetTokens(const char *auth_uri, cups_json_t *metadata, const char *resource_uri, const char *grant_code, cups_ogrant_t grant_type, const char *redirect_uri, time_t *access_expires) _CUPS_PUBLIC;

0 commit comments

Comments
 (0)