From b5dc6af793bc8ba949859b70177598ddf9e98564 Mon Sep 17 00:00:00 2001 From: Michael R Sweet Date: Wed, 16 Apr 2025 11:59:38 -0400 Subject: [PATCH] More OAuth additions (Issue #246): - 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 | 152 ++++++++++++++++++++++++++++++----------------- cups/oauth.h | 3 +- scheduler/auth.c | 63 +++++++++++++++++++- scheduler/auth.h | 6 +- scheduler/conf.c | 25 ++++++-- 5 files changed, 181 insertions(+), 68 deletions(-) diff --git a/cups/oauth.c b/cups/oauth.c index b7f426fb66..9d6fbe8287 100644 --- a/cups/oauth.c +++ b/cups/oauth.c @@ -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. // diff --git a/cups/oauth.h b/cups/oauth.h index a1625feda8..f4275280a7 100644 --- a/cups/oauth.h +++ b/cups/oauth.h @@ -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; diff --git a/scheduler/auth.c b/scheduler/auth.c index c767f10f22..1ac00f5679 100644 --- a/scheduler/auth.c +++ b/scheduler/auth.c @@ -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 */ diff --git a/scheduler/auth.h b/scheduler/auth.h index ea9cb9fb69..adc0ccdea4 100644 --- a/scheduler/auth.h +++ b/scheduler/auth.h @@ -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 */ diff --git a/scheduler/conf.c b/scheduler/conf.c index 390c8732eb..aec274448c 100644 --- a/scheduler/conf.c +++ b/scheduler/conf.c @@ -9,11 +9,8 @@ * information. */ -/* - * Include necessary headers... - */ - #include "cupsd.h" +#include #include #include #include @@ -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) -- 2.47.2