]> git.ipfire.org Git - thirdparty/cups.git/commitdiff
More OAuth additions (Issue #246):
authorMichael R Sweet <msweet@msweet.org>
Wed, 16 Apr 2025 15:59:38 +0000 (11:59 -0400)
committerMichael R Sweet <msweet@msweet.org>
Wed, 16 Apr 2025 15:59:38 +0000 (11:59 -0400)
- Make cupsOAuthGetJWKS a public API (previously just a static function) since
  any service will need it for validating JWTs.
- Implement OAuth support in cupsdAuthorize().
- Add OAuthJWKS global and manage it in cupsdReadConfiguration().

cups/oauth.c
cups/oauth.h
scheduler/auth.c
scheduler/auth.h
scheduler/conf.c

index b7f426fb6675bf9487d180b4c807a2f034a994f4..9d6fbe82872448c4a67a7a4238f9337e3e42bb45 100644 (file)
@@ -159,7 +159,6 @@ static const char *github_metadata =        // Github.com OAuth metadata
 
 static char    *oauth_copy_response(http_t *http);
 static cups_json_t *oauth_do_post(const char *ep, const char *content_type, const char *data);
-static cups_json_t *oauth_get_jwks(const char *auth_uri, cups_json_t *metadata);
 static char    *oauth_load_value(const char *auth_uri, const char *secondary_uri, _cups_otype_t otype);
 static char    *oauth_make_path(char *buffer, size_t bufsize, const char *auth_uri, const char *secondary_uri, _cups_otype_t otype);
 static char    *oauth_make_software_id(char *buffer, size_t bufsize);
@@ -173,6 +172,9 @@ static bool oauth_set_error(cups_json_t *json, size_t num_form, cups_option_t *f
 //
 // This function clears cached authorization information for the given
 // Authorization Server "auth_uri" and Resource "resource_uri" combination.
+//
+// @since CUPS 2.5@
+//
 
 void
 cupsOAuthClearTokens(
@@ -197,6 +199,8 @@ cupsOAuthClearTokens(
 //
 // `NULL` is returned if no token is cached.
 //
+// @since CUPS 2.5@
+//
 
 char *                                 // O - Access token
 cupsOAuthCopyAccessToken(
@@ -236,6 +240,8 @@ cupsOAuthCopyAccessToken(
 //
 // `NULL` is returned if no `client_id` is cached.
 //
+// @since CUPS 2.5@
+//
 
 char *                                 // O - `client_id` value
 cupsOAuthCopyClientId(
@@ -265,6 +271,8 @@ cupsOAuthCopyClientId(
 //
 // `NULL` is returned if no refresh token is cached.
 //
+// @since CUPS 2.5@
+//
 
 char *                                 // O - Refresh token
 cupsOAuthCopyRefreshToken(
@@ -285,6 +293,8 @@ cupsOAuthCopyRefreshToken(
 //
 // `NULL` is returned if no identification information is cached.
 //
+// @since CUPS 2.5@
+//
 
 cups_jwt_t *                           // O - Identification information
 cupsOAuthCopyUserId(
@@ -332,6 +342,8 @@ cupsOAuthCopyUserId(
 //
 // The returned authorization code must be freed using the `free` function.
 //
+// @since CUPS 2.5@
+//
 
 char *                                 // O - Authorization code or `NULL` on error
 cupsOAuthGetAuthorizationCode(
@@ -758,6 +770,8 @@ cupsOAuthGetAuthorizationCode(
 // @link cupsOAuthGetAuthorizationCode@ function handles registration of
 // local/"native" applications for you.
 //
+// @since CUPS 2.5@
+//
 
 char *                                 // O - `client_id` string or `NULL` on error
 cupsOAuthGetClientId(
@@ -824,13 +838,76 @@ cupsOAuthGetClientId(
 }
 
 
+//
+// 'cupsOAuthGetJWKS()' - Get the JWT key set for an Authorization Server.
+//
+// This function gets the JWT key set for the specified Authorization Server
+// "auth_uri".  The "metadata" value is obtained using the
+// @link cupsOAuthGetMetadata@ function.  The returned key set is cached
+// per-user for better performance and must be freed using the
+// @link cupsJSONDelete@ function.
+//
+// The key set is typically used to validate JWT bearer tokens using the
+// @link cupsJWTHasValidSignature@ function.
+//
+// @since CUPS 2.5@
+//
+
+cups_json_t *                          // O - JWKS or `NULL` on error
+cupsOAuthGetJWKS(const char  *auth_uri,        // I - Authorization server URI
+                 cups_json_t *metadata)        // I - Server metadata
+{
+  const char   *jwks_uri;              // URI of key set
+  cups_json_t  *jwks;                  // JWT key set
+  char         filename[1024];         // Local metadata filename
+  struct stat  fileinfo;               // Local metadata file info
+
+
+  DEBUG_printf("cupsOAuthGetJWKS(auth_uri=\"%s\", metadata=%p)", auth_uri, (void *)metadata);
+
+  // Get existing key set...
+  if (!oauth_make_path(filename, sizeof(filename), auth_uri, /*secondary_uri*/NULL, _CUPS_OTYPE_JWKS))
+    return (NULL);
+
+  if (stat(filename, &fileinfo))
+    memset(&fileinfo, 0, sizeof(fileinfo));
+
+  // Don't bother connecting if the key set was updated recently...
+  if ((time(NULL) - fileinfo.st_mtime) <= 60)
+    return (cupsJSONImportFile(filename));
+
+  // Try getting the key set...
+  if ((jwks_uri = cupsJSONGetString(cupsJSONFind(metadata, "jwks_uri"))) == NULL)
+  {
+    _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("No JWKS URI in authorization server metadata."), true);
+    return (NULL);
+  }
+
+  if ((jwks = cupsJSONImportURL(jwks_uri, &fileinfo.st_mtime)) != NULL)
+  {
+    // Save the key set...
+    char *s = cupsJSONExportString(jwks);
+                                       // JSON string
+
+    oauth_save_value(auth_uri, /*secondary_uri*/NULL, _CUPS_OTYPE_JWKS, s);
+    free(s);
+  }
+
+  // Return what we got...
+  return (jwks);
+}
+
+
 //
 // 'cupsOAuthGetMetadata()' - Get the metadata for an Authorization Server.
 //
-// This function gets the metadata for the specified Authorization Server URI
-// "auth_uri". Metadata is cached per-user for better performance.
+// This function gets the RFC 8414 or Open ID Connect metadata for the specified
+// OAuth Authorization Server URI "auth_uri".
+//
+// The returned metadata is cached per-user for better performance and must be
+// freed using the @link cupsJSONDelete@ function.
 //
-// The returned metadata must be freed using the @link cupsJSONDelete@ function.
+// @since CUPS 2.5@
 //
 
 cups_json_t *                          // O - JSON metadata or `NULL` on error
@@ -859,7 +936,7 @@ cupsOAuthGetMetadata(
   DEBUG_printf("cupsOAuthGetMetadata(auth_uri=\"%s\")", auth_uri);
 
   // Special-cases...
-  if (!strcmp(auth_uri, "https://github.com"))
+  if (!strncmp(auth_uri, "https://github.com", 18) && (!auth_uri[18] || auth_uri[18] == '/'))
     return (cupsJSONImportString(github_metadata));
 
   // Get existing metadata...
@@ -968,14 +1045,15 @@ cupsOAuthGetMetadata(
     }
   }
 
+  httpClose(http);
+
   if (status != HTTP_STATUS_OK && status != HTTP_STATUS_NOT_MODIFIED)
   {
-    // Remove old cached data...
+    // Remove old cached data and return NULL...
     unlink(filename);
+    return (NULL);
   }
 
-  httpClose(http);
-
   // Return the cached metadata, if any...
   load_metadata:
 
@@ -1015,6 +1093,8 @@ cupsOAuthGetMetadata(
 // @link cupsOAuthCopyRefreshToken@ and @link cupsOAuthCopyUserId@ functions
 // respectively.
 //
+// @since CUPS 2.5@
+//
 
 char *                                 // O - Access token or `NULL` on error
 cupsOAuthGetTokens(
@@ -1124,7 +1204,7 @@ cupsOAuthGetTokens(
       goto done;
 
     // Validate id_token against the Authorization Server's JWKS
-    if ((jwks = oauth_get_jwks(auth_uri, metadata)) == NULL)
+    if ((jwks = cupsOAuthGetJWKS(auth_uri, metadata)) == NULL)
       goto done;
 
     valid = cupsJWTHasValidSignature(jwt, jwks);
@@ -1220,6 +1300,8 @@ cupsOAuthGetTokens(
 // The "state" parameter is a unique (random) identifier for the authorization
 // request.  It is provided to the redirection URI as a form parameter.
 //
+// @since CUPS 2.5@
+//
 
 char *                                 // O - Authorization URL
 cupsOAuthMakeAuthorizationURL(
@@ -1295,6 +1377,8 @@ cupsOAuthMakeAuthorizationURL(
 // encoded. "len" specifies the number of random bytes to include in the string.
 // The returned string must be freed using the `free` function.
 //
+// @since CUPS 2.5@
+//
 
 char *                                 // O - Random string
 cupsOAuthMakeBase64Random(size_t len)  // I - Number of bytes
@@ -1331,6 +1415,8 @@ cupsOAuthMakeBase64Random(size_t len)     // I - Number of bytes
 // "client_id" is `NULL` then any saved values are deleted from the per-user
 // store.
 //
+// @since CUPS 2.5@
+//
 
 void
 cupsOAuthSaveClientData(
@@ -1352,6 +1438,8 @@ cupsOAuthSaveClientData(
 // "auth_uri" and resource "resource_uri". Specifying `NULL` for any of the
 // values will delete the corresponding saved values from the per-user store.
 //
+// @since CUPS 2.5@
+//
 
 void
 cupsOAuthSaveTokens(
@@ -1503,52 +1591,6 @@ oauth_do_post(const char *ep,            // I - Endpoint URI
 }
 
 
-//
-// 'oauth_get_jwks()' - Get the JWT key set for an Authorization Server.
-//
-
-static cups_json_t *                   // O - JWKS or `NULL` on error
-oauth_get_jwks(const char  *auth_uri,  // I - Authorization server URI
-               cups_json_t *metadata)  // I - Server metadata
-{
-  const char   *jwks_uri;              // URI of key set
-  cups_json_t  *jwks;                  // JWT key set
-  char         filename[1024];         // Local metadata filename
-  struct stat  fileinfo;               // Local metadata file info
-
-
-  DEBUG_printf("oauth_get_jwks(auth_uri=\"%s\", metadata=%p)", auth_uri, (void *)metadata);
-
-  // Get existing key set...
-  if (!oauth_make_path(filename, sizeof(filename), auth_uri, /*secondary_uri*/NULL, _CUPS_OTYPE_JWKS))
-    return (NULL);
-
-  if (stat(filename, &fileinfo))
-    memset(&fileinfo, 0, sizeof(fileinfo));
-
-  // Don't bother connecting if the key set was updated recently...
-  if ((time(NULL) - fileinfo.st_mtime) <= 60)
-    return (cupsJSONImportFile(filename));
-
-  // Try getting the key set...
-  if ((jwks_uri = cupsJSONGetString(cupsJSONFind(metadata, "jwks_uri"))) == NULL)
-    return (NULL);
-
-  if ((jwks = cupsJSONImportURL(jwks_uri, &fileinfo.st_mtime)) != NULL)
-  {
-    // Save the key set...
-    char *s = cupsJSONExportString(jwks);
-                                       // JSON string
-
-    oauth_save_value(auth_uri, /*secondary_uri*/NULL, _CUPS_OTYPE_JWKS, s);
-    free(s);
-  }
-
-  // Return what we got...
-  return (jwks);
-}
-
-
 //
 // 'oauth_load_value()' - Load the contents of the specified value file.
 //
index a1625feda8b29d90c59749873cd5b274d6cac50a..f4275280a7ae0cb7869d26f0d3e938cf53dd4696 100644 (file)
@@ -1,7 +1,7 @@
 //
 // OAuth API definitions for CUPS.
 //
-// Copyright © 2024 by OpenPrinting.
+// Copyright © 2024-2025 by OpenPrinting.
 //
 // Licensed under Apache License v2.0.  See the file "LICENSE" for more
 // information.
@@ -47,6 +47,7 @@ extern cups_jwt_t     *cupsOAuthCopyUserId(const char *auth_uri, const char *resourc
 
 extern char            *cupsOAuthGetAuthorizationCode(const char *auth_uri, cups_json_t *metadata, const char *resource_uri, const char *scopes, const char *redirect_uri) _CUPS_PUBLIC;
 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;
+extern cups_json_t     *cupsOAuthGetJWKS(const char *auth_uri, cups_json_t *metadata) _CUPS_PUBLIC;
 extern cups_json_t     *cupsOAuthGetMetadata(const char *auth_uri) _CUPS_PUBLIC;
 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;
 
index c767f10f22602607df92a354b8edb4999e4280f5..1ac00f5679336203540b96eb2b30dcbb192e4f3f 100644 (file)
@@ -562,7 +562,7 @@ cupsdAuthorize(cupsd_client_t *con) /* I - Client connection */
 
     cupsdLogClient(con, CUPSD_LOG_DEBUG, "Authorized as %s using Local.", username);
   }
-  else if (!strncmp(authorization, "Basic", 5))
+  else if (!strncmp(authorization, "Basic ", 6))
   {
    /*
     * Get the Basic authentication data...
@@ -570,7 +570,6 @@ cupsdAuthorize(cupsd_client_t *con) /* I - Client connection */
 
     int        userlen;                        /* Username:password length */
 
-
     authorization += 5;
     while (isspace(*authorization & 255))
       authorization ++;
@@ -695,8 +694,66 @@ cupsdAuthorize(cupsd_client_t *con)        /* I - Client connection */
     cupsdLogClient(con, CUPSD_LOG_DEBUG, "Authorized as \"%s\" using Basic.", username);
     con->type = type;
   }
+  else if (!strncmp(authorization, "Bearer ", 7))
+  {
+    // OAuth/OpenID authorization using JWT bearer tokens...
+    cups_jwt_t *jwt;                   // JWT decoded from bearer token...
+    const char *sub,                   // Subject/user ID
+               *name,                  // Real name
+               *email;                 // Email address
+
+    // Skip whitespace after "Bearer"...
+    authorization += 7;
+    while (isspace(*authorization & 255))
+      authorization ++;
+
+    // Decode and validate the JWT...
+    if ((jwt = cupsJWTImportString(authorization, CUPS_JWS_FORMAT_COMPACT)) == NULL)
+    {
+      cupsdLogClient(con, CUPSD_LOG_ERROR, "Unable to import JWT Bearer token: %s", cupsGetErrorString());
+      cupsCopyString(con->autherror, cupsGetErrorString(), sizeof(con->autherror));
+      return;
+    }
+    else if (!cupsJWTHasValidSignature(jwt, OAuthJWKS))
+    {
+      cupsdLogClient(con, CUPSD_LOG_ERROR, "JWT Bearer token signature is bad.");
+      cupsCopyString(con->autherror, "Invalid JWT signature.", sizeof(con->autherror));
+      cupsJWTDelete(jwt);
+      return;
+    }
+    else if (cupsJWTGetClaimNumber(jwt, CUPS_JWT_EXP) < time(NULL))
+    {
+      cupsdLogClient(con, CUPSD_LOG_ERROR, "JWT Bearer token is expired.");
+      cupsCopyString(con->autherror, "Expired JWT.", sizeof(con->autherror));
+      cupsJWTDelete(jwt);
+      return;
+    }
+    else if ((sub = cupsJWTGetClaimString(jwt, CUPS_JWT_SUB)) == NULL)
+    {
+      cupsdLogClient(con, CUPSD_LOG_ERROR, "Missing subject name in JWT Bearer token.");
+      cupsCopyString(con->autherror, "Missing subject name.", sizeof(con->autherror));
+      cupsJWTDelete(jwt);
+      return;
+    }
+
+    // Good JWT, grab information from it and return...
+    con->autherror[0] = '\0';
+    con->password[0]  = '\0';
+
+    httpSetAuthString(con->http, "Bearer", authorization);
+    cupsCopyString(con->username, sub, sizeof(con->username));
+    if ((name = cupsJWTGetClaimString(jwt, CUPS_JWT_NAME)) != NULL)
+      cupsCopyString(con->realname, name, sizeof(con->realname));
+    if ((email = cupsJWTGetClaimString(jwt, "email")) != NULL)
+      cupsCopyString(con->email, email, sizeof(con->email));
+
+    cupsJWTDelete(jwt);
+
+    cupsdLogClient(con, CUPSD_LOG_DEBUG, "Authorized as \"%s\" (%s <%s>) using OAuth/OpenID.", con->username, con->realname, con->email);
+    return;
+  }
 #ifdef HAVE_GSSAPI
-  else if (!strncmp(authorization, "Negotiate", 9))
+  else if (!strncmp(authorization, "Negotiate ", 10))
   {
     int                        len;            /* Length of authorization string */
     gss_ctx_id_t       context;        /* Authorization context */
index ea9cb9fb69f52651dd501e097a9250028178ecfa..adc0ccdea43ef580e91c1a500fc1f54ed321a6e5 100644 (file)
@@ -117,9 +117,9 @@ VAR cups_array_t    *Locations      VALUE(NULL);
 
 VAR cups_array_t       *OAuthGroups    VALUE(NULL);
                                        /* OAuthGroup entries */
-VAR http_t             *OAuthHTTP      VALUE(NULL);
-                                       /* Connection to server */
-VAR cups_json_t                *OAuthMetadata  VALUE(NULL);
+VAR cups_json_t                *OAuthJWKS      VALUE(NULL),
+                                       /* Public keys for JWT validation */
+                       *OAuthMetadata  VALUE(NULL);
                                        /* Metadata from the server */
 VAR char               *OAuthScopes    VALUE(NULL),
                                        /* OAuthScopes value */
index 390c8732ebd50035b58c2097f4a8dae54aa73655..aec274448c3778ba6d16bb48985c7b105491b5b8 100644 (file)
@@ -9,11 +9,8 @@
  * information.
  */
 
-/*
- * Include necessary headers...
- */
-
 #include "cupsd.h"
+#include <cups/oauth.h>
 #include <stdarg.h>
 #include <grp.h>
 #include <sys/utsname.h>
@@ -609,8 +606,8 @@ cupsdReadConfiguration(void)
   cupsArrayDelete(OAuthGroups);
   OAuthGroups = NULL;
 
-  httpClose(OAuthHTTP);
-  OAuthHTTP = NULL;
+  cupsJSONDelete(OAuthJWKS);
+  OAuthJWKS = NULL;
 
   cupsJSONDelete(OAuthMetadata);
   OAuthMetadata = NULL;
@@ -875,6 +872,22 @@ cupsdReadConfiguration(void)
 
   cupsFileClose(fp);
 
+  if (OAuthServer)
+  {
+    if ((OAuthMetadata = cupsOAuthGetMetadata(OAuthServer)) == NULL)
+    {
+      cupsdLogMessage(CUPSD_LOG_ERROR, "Unable to get metadata from OAUth server \"%s\": %s", OAuthServer, cupsGetErrorString());
+      if (FatalErrors & CUPSD_FATAL_CONFIG)
+        status = 0;
+    }
+    else if ((OAuthJWKS = cupsOAuthGetJWKS(OAuthServer, OAuthMetadata)) == NULL)
+    {
+      cupsdLogMessage(CUPSD_LOG_ERROR, "Unable to load OAuth JWKS for validation: %s", cupsGetErrorString());
+      if (FatalErrors & CUPSD_FATAL_CONFIG)
+        status = 0;
+    }
+  }
+
   if (!status)
   {
     if (TestConfigFile)