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);
//
// This function clears cached authorization information for the given
// Authorization Server "auth_uri" and Resource "resource_uri" combination.
+//
+// @since CUPS 2.5@
+//
void
cupsOAuthClearTokens(
//
// `NULL` is returned if no token is cached.
//
+// @since CUPS 2.5@
+//
char * // O - Access token
cupsOAuthCopyAccessToken(
//
// `NULL` is returned if no `client_id` is cached.
//
+// @since CUPS 2.5@
+//
char * // O - `client_id` value
cupsOAuthCopyClientId(
//
// `NULL` is returned if no refresh token is cached.
//
+// @since CUPS 2.5@
+//
char * // O - Refresh token
cupsOAuthCopyRefreshToken(
//
// `NULL` is returned if no identification information is cached.
//
+// @since CUPS 2.5@
+//
cups_jwt_t * // O - Identification information
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(
// @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(
}
+//
+// '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
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...
}
}
+ 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:
// @link cupsOAuthCopyRefreshToken@ and @link cupsOAuthCopyUserId@ functions
// respectively.
//
+// @since CUPS 2.5@
+//
char * // O - Access token or `NULL` on error
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);
// 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(
// 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
// "client_id" is `NULL` then any saved values are deleted from the per-user
// store.
//
+// @since CUPS 2.5@
+//
void
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(
}
-//
-// '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.
//
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...
int userlen; /* Username:password length */
-
authorization += 5;
while (isspace(*authorization & 255))
authorization ++;
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 */