@@ -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
10501208char * // O - Access token or `NULL` on error
10511209cupsOAuthGetTokens (
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 {
0 commit comments