//
// OAuth API implementation for CUPS.
//
// Copyright © 2024-2025 by OpenPrinting.
// Copyright © 2017-2024 by Michael R Sweet
//
// Licensed under Apache License v2.0.  See the file "LICENSE" for more
// information.
//

#include "cups-private.h"
#include "oauth.h"
#include "form.h"
#include <sys/stat.h>
#ifdef _WIN32
#  include <process.h>
#  define O_NOFOLLOW 0			// Windows doesn't support this...
#else
#  include <poll.h>
#  ifdef __APPLE__
#    include <CoreFoundation/CoreFoundation.h>
#    include <CoreServices/CoreServices.h>
#  else
#    include <spawn.h>
#    include <sys/wait.h>
extern char **environ;			// @private@
#  endif // __APPLE__
#endif // _WIN32


//
// Overview
// ========
//
// The CUPS OAuth implementation follows the IEEE-ISTO Printer Working Group's
// IPP OAuth Extensions v1.0 (OAUTH) specification (pending publication), which
// in turn depends on a boatload of IETF RFCs and the OpenID Connect
// specifications.  In short, the IPP specification handles how to combine IPP
// (which is layered on top of HTTP) with OAuth and works to "consolidate" the
// different requirements of IETF OAuth 2.x and OpenID Connect so that we are as
// widely interoperable as possible.
//
//
// Compatibility
// -------------
//
// The intent is for CUPS to support using common OAuth implementations,
// including (but not limited to):
//
// - Amazon Cognito (<https://aws.amazon.com/cognito/>)
// - Github  (<https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps>)
// - Google (<https://developers.google.com/identity/openid-connect/openid-connect>)
// - Microsoft Account/Azure Active Directory/Entra ID (<https://learn.microsoft.com/en-us/entra/identity/>)
// - mOAuth (<https://www.msweet.org/moauth/>)
// - Okta Auth0 (<https://developer.auth0.com>)
//
//
// Security
// --------
//
// Security on the wire is as good as OAuth and TLS provides.
//
// The current OAuth cache implementation uses unencrypted files in your home
// directory with restricted permissions.  Ideally they should be encrypted
// "at rest" but Unix doesn't have a universal solution for this and the
// available options don't generally protect against malicious code running as
// the target user.  The code is setup to facilitate replacement with another
// storage "backend" (like the Keychain API on macOS), and adding conditional
// platform support code for this is planned.  This sort of issue is generally
// mitigated by access tokens having a limited life...
//
//
// Notes
// -----
//
// - Amazon and Microsoft require you to setup an Authorization Server for your
//   domain before you can play/test.  There is no public sandbox service.
// - Github support currently depends on hardcoded metadata
//   (<https://github.com/orgs/community/discussions/127556>) and has a few
//   authorization extensions that might require some special-handling.
// - Google implements OpenID Connect but not RFC 8414
//   (<https://accounts.google.com>) and seems to only allow a redirect URI of
//   "http://localhost" without a specified path.
// - Okta Auth0 provides a sample OpenID Connect Authorization Server
//   (<https://samples.auth0.com>) that also supports Device Connect and a few
//   other extensions that might be handy in the future.
//


//
// Local types...
//

typedef enum _cups_otype_e		// OAuth data type
{
  _CUPS_OTYPE_ACCESS,			// Access token
  _CUPS_OTYPE_CLIENT_ID,		// Client ID
  _CUPS_OTYPE_CLIENT_SECRET,		// Client secret
  _CUPS_OTYPE_CODE_VERIFIER,		// Client code_verifier
  _CUPS_OTYPE_USER_ID,			// (User) ID token
  _CUPS_OTYPE_JWKS,			// Server key store
  _CUPS_OTYPE_METADATA,			// Server metadata
  _CUPS_OTYPE_NONCE,			// Client nonce
  _CUPS_OTYPE_REDIRECT_URI,		// Redirect URI used
  _CUPS_OTYPE_REFRESH,			// Refresh token
  _CUPS_OTYPE_MAX			// Maximum number of types
} _cups_otype_t;


//
// Local constants...
//

#define _CUPS_CONNECTOR_CLIENT_ID	"a92130af-5eac-4a8a-8ba3-e17276b52ca1"
					// CUPS Universal Print Connector client ID
#define _CUPS_CONNECTOR_OAUTH_URI	"https://login.microsoftonline.com/"
					// OAuth URI prefix for MS Entrada ID
#define _CUPS_CONNECTOR_OAUTH_URILEN	34
					// Length of OAuth URI prefix

#define _CUPS_OAUTH_REDIRECT_FORMAT	"http://127.0.0.1:%d/"
					// Redirect URI with port
#define _CUPS_OAUTH_REDIRECT_PATH	"/?"
					// Redirect URI request path prefix
#define _CUPS_OAUTH_REDIRECT_PATHLEN	2
					// Redirect URI request path length

#ifdef DEBUG
static const char * const cups_otypes[_CUPS_OTYPE_MAX] =
{					// OAuth data types...
  "_CUPS_OTYPE_ACCESS",			// Access token
  "_CUPS_OTYPE_CLIENT_ID",		// Client ID
  "_CUPS_OTYPE_CLIENT_SECRET",		// Client secret
  "_CUPS_OTYPE_CODE_VERIFIER",		// Client code_verifier
  "_CUPS_OTYPE_USER_ID",		// (User) ID token
  "_CUPS_OTYPE_JWKS",			// Server key store
  "_CUPS_OTYPE_METADATA",		// Server metadata
  "_CUPS_OTYPE_NONCE",			// Client nonce
  "_CUPS_OTYPE_REDIRECT_URI",		// Redirect URI used
  "_CUPS_OTYPE_REFRESH"			// Refresh token
};
#endif // DEBUG

static const char *github_metadata =	// Github.com OAuth metadata
"{\
\"issuer\":\"https://github.com\",\
\"authorization_endpoint\":\"https://github.com/login/oauth/authorize\",\
\"token_endpoint\":\"https://github.com/login/oauth/access_token\",\
\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\"],\
\"scopes_supported\":[\"repo\",\"repo:status\",\"repo_deployment\",\"public_repo\",\"repo:invite\",\"security_events\",\"admin:repo_hook\",\"write:repo_hook\",\"read:repo_hook\",\"admin:org\",\"write:org\",\"read:org\",\"admin:public_key\",\"write:public_key\",\"read:public_key\",\"admin:org_hook\",\"gist\",\"notifications\",\"user\",\"read:user\",\"user:email\",\"user:follow\",\"project\",\"read:project\",\"delete_repo\",\"write:packages\",\"read:packages\",\"delete:packages\",\"admin.gpg_key\",\"write:gpg_key\",\"read:gpg_key\",\"codespace\",\"workflow\"],\
\"response_types_supported\":[\"code\"],\
\"grant_types_supported\":[\"authorization_code\",\"refresh_token\",\"\",\"urn:ietf:params:oauth:grant-type:device_code\"],\
\"device_authorization_endpoint\":\"https://github.com/login/device/code\",\
}";


//
// Local functions...
//

static char	*oauth_copy_response(http_t *http);
static char	*oauth_copy_scopes(cups_json_t *metadata);
static cups_json_t *oauth_do_post(const char *ep, const char *content_type, const char *data);
static char	*oauth_load_value(const char *auth_uri, const char *secondary_uri, _cups_otype_t otype, bool try_sysconfig);
static char	*oauth_make_access_uri(const char *access_token);
static char	*oauth_make_path(char *buffer, size_t bufsize, const char *auth_uri, const char *secondary_uri, _cups_otype_t otype, bool use_sysconfig);
static char	*oauth_make_software_id(char *buffer, size_t bufsize);
static bool	oauth_metadata_contains(cups_json_t *metadata, const char *parameter, const char *value);
static void	oauth_save_value(const char *auth_uri, const char *secondary_uri, _cups_otype_t otype, const char *value);
static bool	oauth_set_error(cups_json_t *json, size_t num_form, cups_option_t *form);


//
// 'cupsOAuthClearTokens()' - Clear any cached authorization information.
//
// This function clears cached authorization information for the given
// Authorization Server "auth_uri" and Resource "resource_uri" combination.

void
cupsOAuthClearTokens(
    const char *auth_uri,		// I - Authorization server URI
    const char *resource_uri)		// I - Resource server URI
{
  oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_ACCESS, /*value*/NULL);
  oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_CODE_VERIFIER, NULL);
  oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_USER_ID, /*value*/NULL);
  oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_NONCE, NULL);
  oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_REFRESH, /*value*/NULL);
}


//
// 'cupsOAuthCopyAccessToken()' - Get a cached access token.
//
// This function makes a copy of a cached access token and any
// associated expiration time for the given Authorization Server "auth_uri" and
// Resource "resource_uri" combination.  The returned access token must be freed
// using the `free` function.
//
// `NULL` is returned if no token is cached.
//

char *					// O - Access token
cupsOAuthCopyAccessToken(
    const char *auth_uri,		// I - Authorization Server URI
    const char *resource_uri,		// I - Resource URI
    time_t     *access_expires)		// O - Access expiration time or `NULL` for don't care
{
  char		*token,			// Access token
		*tokptr;		// Pointer into token


  // See if we have a token...
  if (access_expires)
    *access_expires = 0;

  if ((token = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_ACCESS, /*try_sysconfig*/false)) != NULL)
  {
    if ((tokptr = strchr(token, '\n')) != NULL)
    {
      *tokptr++ = '\0';

      if (access_expires)
        *access_expires = strtol(tokptr, NULL, 10);
    }
  }

  return (token);
}


//
// 'cupsOAuthCopyClientId()' - Get the cached `client_id` value.
//
// This function makes a copy of the cached `client_id` value for a given
// Authorization Server "auth_uri" and Redirection URI "resource_uri". The
// returned value must be freed using the `free` function.
//
// `NULL` is returned if no `client_id` is cached.
//

char *					// O - `client_id` value
cupsOAuthCopyClientId(
    const char *auth_uri,		// I - Authorization Server URI
    const char *redirect_uri)		// I - Redirection URI
{
  char	*client_id;			// Client ID value


  if ((client_id = oauth_load_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_ID, /*try_sysconfig*/true)) == NULL && !strncmp(auth_uri, _CUPS_CONNECTOR_OAUTH_URI, _CUPS_CONNECTOR_OAUTH_URILEN))
  {
    // Use the default CUPS Universal Print connector client ID with MS Entrada ID...
    client_id = strdup(_CUPS_CONNECTOR_CLIENT_ID);
  }

  return (client_id);
}


//
// 'cupsOAuthCopyRefreshToken()' - Get a cached refresh token.
//
// This function makes a copy of a cached refresh token for the given
// given Authorization Server "auth_uri" and Resource "resource_uri"
// combination.  The returned refresh token must be freed using the `free`
// function.
//
// `NULL` is returned if no refresh token is cached.
//

char *					// O - Refresh token
cupsOAuthCopyRefreshToken(
    const char *auth_uri,		// I - Authorization Server URI
    const char *resource_uri)		// I - Resource URI
{
  return (oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_REFRESH, /*try_sysconfig*/false));
}


//
// 'cupsOAuthCopyUserId()' - Get cached user identification information.
//
// This function makes a copy of cached user identification information for the
// given Authorization Server "auth_uri" and Resource "resource_uri"
// combination. The returned user information must be freed using the
// @link cupsJWTDelete@ function.
//
// `NULL` is returned if no identification information is cached.
//

cups_jwt_t *				// O - Identification information
cupsOAuthCopyUserId(
    const char *auth_uri,		// I - Authorization Server URI
    const char *resource_uri)		// I - Resource URI
{
  char		*value;			// ID token value
  cups_jwt_t	*jwt;			// JWT value


  value = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_USER_ID, /*try_sysconfig*/false);
  jwt   = cupsJWTImportString(value, CUPS_JWS_FORMAT_COMPACT);

  free(value);
  return (jwt);
}


//
// 'cupsOAuthGetAuthorizationCode()' - Authorize access using a web browser.
//
// This function performs a local/"native" OAuth authorization flow to obtain an
// authorization code for use with the @link cupsOAuthGetTokens@ function.
//
// The "auth_uri" argument specifies the URI for the OAuth Authorization
// Server.  The "metadata" argument specifies the Authorization Server metadata
// as obtained using @link cupsOAuthCopyMetadata@ and/or
// @link cupsOAuthGetMetadata@.
//
// The "resource_uri" argument specifies the URI for a resource (printer, web
// file, etc.) that you which to access.
//
// The "scopes" argument specifies zero or more whitespace-delimited scope
// names to request during authorization.  The list of supported scope names are
// available from the Authorization Server metadata, for example:
//
// ```
// cups_json_t *metadata = cupsOAuthGetMetadata(auth_uri);
// cups_json_t *scopes_supported = cupsJSONFind(metadata, "scopes_supported");
// ```
//
// The "redirect_uri" argument specifies a 'http:' URL with a listen address,
// port, and path to use.  If `NULL`, 127.0.0.1 on a random port is used with a
// path of "/".
//
// The returned authorization code must be freed using the `free` function.
//

char *					// O - Authorization code or `NULL` on error
cupsOAuthGetAuthorizationCode(
    const char  *auth_uri,		// I - Authorization Server URI
    cups_json_t *metadata,		// I - Authorization Server metadata
    const char  *resource_uri,		// I - Resource URI
    const char  *scopes,		// I - Space-delimited scopes
    const char  *redirect_uri)		// I - Redirect URI or `NULL` for default
{
  char		*client_id = NULL,	// `client_id` value
		*code_verifier = NULL,	// Code verifier string
		*nonce = NULL,		// Nonce string
		*state = NULL,		// State string
		*url = NULL,		// URL for authorization page
		*scopes_supported = NULL;
					// Supported scopes
  char		resource[256],		// Resource path
		final_uri[1024];	// Final redirect URI
  size_t	resourcelen;		// Length of resource path
  http_addr_t	addr;			// Loopback listen address
  int		port;			// Port number
  int		fd = -1;		// Listen file descriptor
  fd_set	input;			// Input file descriptors for select()
  struct timeval timeout;		// Timeout for select()
  time_t	endtime;		// End time
  http_t	*http;			// HTTP client
  char		*auth_code = NULL;	// Authorization code


  // Range check input...
  DEBUG_printf("cupsOAuthGetAuthorizationCode(auth_uri=\"%s\", metadata=%p, resource_uri=\"%s\", scopes=\"%s\", redirect_uri=\"%s\")", auth_uri, (void *)metadata, resource_uri, scopes, redirect_uri);

  if (!auth_uri || !metadata || cupsJSONGetString(cupsJSONFind(metadata, "authorization_endpoint")) == NULL)
    return (NULL);

  // Get the client_id value...
  if ((client_id = cupsOAuthCopyClientId(auth_uri, redirect_uri ? redirect_uri : CUPS_OAUTH_REDIRECT_URI)) == NULL)
    client_id = cupsOAuthGetClientId(auth_uri, metadata, redirect_uri ? redirect_uri : CUPS_OAUTH_REDIRECT_URI, /*logo_uri*/NULL, /*tos_uri*/NULL);

  DEBUG_printf("1cupsOAuthGetAuthorizationCode: client_id=\"%s\"", client_id);

  if (!client_id)
    return (NULL);

  // Listen on a local port...
  if (redirect_uri)
  {
    // Use the host/port/resource from the URI
    char	scheme[32],		// URL scheme
		userpass[256],		// Username:password (ignored)
		host[256];		// Hostname

    if (httpSeparateURI(HTTP_URI_CODING_ALL, redirect_uri, scheme, sizeof(scheme), userpass, sizeof(userpass), host, sizeof(host), &port, resource, sizeof(resource)) < HTTP_URI_STATUS_OK || strcmp(scheme, "http"))
    {
      DEBUG_printf("1cupsOAuthGetAuthorizationCode: Bad redirect_uri '%s'.", redirect_uri);
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(EINVAL), false);
      goto done;
    }

    memset(&addr, 0, sizeof(addr));
#ifdef __APPLE__
    addr.ipv4.sin_len    = sizeof(struct sockaddr_in);
#endif // __APPLE__
    addr.ipv4.sin_family = AF_INET;

    if (!strcmp(host, "localhost") || !strcmp(host, "127.0.0.1"))
      addr.ipv4.sin_addr.s_addr = htonl(0x7f000001);

    fd = httpAddrListen(&addr, port);

    cupsConcatString(resource, "?", sizeof(resource));
  }
  else
  {
    // Find the next available port on 127.0.0.1...
    memset(&addr, 0, sizeof(addr));
#ifdef __APPLE__
    addr.ipv4.sin_len         = sizeof(struct sockaddr_in);
#endif // __APPLE__
    addr.ipv4.sin_family      = AF_INET;
    addr.ipv4.sin_addr.s_addr = htonl(0x7f000001);

    for (port = 10000; port < 11000; port ++)
    {
      if ((fd = httpAddrListen(&addr, port)) >= 0)
	break;
    }

    // Save the redirect URI and resource...
    cupsCopyString(resource, _CUPS_OAUTH_REDIRECT_PATH, sizeof(resource));
    snprintf(final_uri, sizeof(final_uri), _CUPS_OAUTH_REDIRECT_FORMAT, port);
    redirect_uri = final_uri;
  }

  DEBUG_printf("1cupsOAuthGetAuthorizationCode: redirect_uri=\"%s\", resource=\"%s\"", redirect_uri, resource);
  DEBUG_printf("1cupsOAuthGetAuthorizationCode: Listen socket for port %d is %d (%s)", port, fd, strerror(errno));

  if (fd < 0)
    goto done;

  resourcelen = strlen(resource);

  // Point redirection to the local port...
  oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_REDIRECT_URI, redirect_uri);

  // Make state and code verification strings...
  if (oauth_metadata_contains(metadata, "code_challenge_methods_supported", "S256"))
    code_verifier = cupsOAuthMakeBase64Random(128);
  else
    code_verifier = NULL;

  DEBUG_printf("1cupsOAuthGetAuthorizationCode: code_verifier=\"%s\"", code_verifier);

  if (oauth_metadata_contains(metadata, "scopes_supported", "openid"))
    nonce = cupsOAuthMakeBase64Random(16);
  else
    nonce = NULL;

  DEBUG_printf("1cupsOAuthGetAuthorizationCode: nonce=\"%s\"", nonce);

  state = cupsOAuthMakeBase64Random(16);

  DEBUG_printf("1cupsOAuthGetAuthorizationCode: state=\"%s\"", state);

  if (!state)
    goto done;

  if (!scopes)
  {
    scopes_supported = oauth_copy_scopes(metadata);
    scopes           = scopes_supported;
  }

  DEBUG_printf("1cupsOAuthGetAuthorizationCode: scopes=\"%s\"", scopes);

  // Get the authorization URL...
  if ((url = cupsOAuthMakeAuthorizationURL(auth_uri, metadata, resource_uri, scopes, client_id, code_verifier, nonce, redirect_uri, state)) == NULL)
    goto done;

  DEBUG_printf("1cupsOAuthGetAuthorizationCode: url=\"%s\"", url);

  // Open a web browser with the authorization page...
#ifdef __APPLE__
  CFURLRef	cfurl;			// CoreFoundation URL
  int		error = 1;		// Open status

  if ((cfurl = CFURLCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)url, (CFIndex)strlen(url), kCFStringEncodingASCII, NULL)) != NULL)
  {
    error = LSOpenCFURLRef(cfurl, NULL);

    CFRelease(cfurl);
  }

  if (error != noErr)
    goto done;

#elif defined(_WIN32)
  if (_spawnl(_P_WAIT, "start", "", url, NULL))
    goto done;

#else
  pid_t		pid = 0;		// Process ID
  int		estatus;		// Exit status
  const char	*xdg_open_argv[3];	// xdg-open arguments

  xdg_open_argv[0] = "xdg-open";
  xdg_open_argv[1] = url;
  xdg_open_argv[2] = NULL;

  if (posix_spawnp(&pid, "xdg-open", NULL, NULL, (char * const *)xdg_open_argv, environ))
    goto done;				// Couldn't run xdg-open
  else if (waitpid(pid, &estatus, 0))
    goto done;				// Couldn't get exit status
  else if (estatus)
    goto done;				// Non-zero exit status
#endif // __APPLE__

  // Listen for connections up to 60 seconds...
  endtime = time(NULL) + 60;

  while (auth_code == NULL && time(NULL) < endtime)
  {
    timeout.tv_sec  = 1;
    timeout.tv_usec = 0;

    FD_ZERO(&input);
    FD_SET(fd, &input);

    if (select(fd + 1, &input, /*writefds*/NULL, /*errorfds*/NULL, &timeout) > 0 && FD_ISSET(fd, &input))
    {
      // Try accepting a connection...
      DEBUG_printf("1cupsOAuthGetAuthorizationCode: Accepting connection.");

      if ((http = httpAcceptConnection(fd, true)) != NULL)
      {
        // Respond to HTTP requests...
        while (auth_code == NULL && time(NULL) < endtime && httpWait(http, 1000))
        {
          char		reqres[4096],	// Resource path
			message[2048];	// Response message
	  http_state_t	hstate;		// HTTP request state
	  http_status_t	hstatus;	// HTTP request status
	  const char	*htype = NULL,	// HTTP response content type
			*hbody = NULL;	// HTTP response body

          // Get the request header...
          if ((hstate = httpReadRequest(http, reqres, sizeof(reqres))) == HTTP_STATE_WAITING)
            continue;
	  else if (hstate == HTTP_STATE_ERROR || hstate == HTTP_STATE_UNKNOWN_METHOD || hstate == HTTP_STATE_UNKNOWN_VERSION)
            break;

	  // Read incoming headers until the status changes...
	  do
	  {
	    hstatus = httpUpdate(http);
	  }
	  while (hstatus == HTTP_STATUS_CONTINUE && time(NULL) < endtime);

          // Stop on error...
          if (hstatus != HTTP_STATUS_OK)
            break;

          // Process the request...
          DEBUG_printf("1cupsOAuthGetAuthorizationCode: %s %s HTTP/1.1", httpStateString(hstate), reqres);

          switch (hstate)
          {
            default :
		hstatus = HTTP_STATUS_METHOD_NOT_ALLOWED;
                break;

            case HTTP_STATE_HEAD :
		if (!strncmp(reqres, resource, resourcelen))
		{
		  // Respond that the content will be HTML...
		  htype = "text/html";
		}
		else
		{
		  // Resource doesn't exist...
		  hstatus = HTTP_STATUS_NOT_FOUND;
		}
		break;

	    case HTTP_STATE_GET :
		if (!strncmp(reqres, resource, resourcelen))
		{
		  // Collect form parameters from resource...
		  const char	*code_value,		// Authoziation code value
				*error_code,		// Error code
				*error_desc,		// Error description
				*state_value;		// State value
		  size_t	num_form;		// Number of form variables
		  cups_option_t	*form = NULL;		// Form variables

		  num_form    = cupsFormDecode(reqres + resourcelen, &form);
                  code_value  = cupsGetOption("code", num_form, form);
                  error_code  = cupsGetOption("error", num_form, form);
                  error_desc  = cupsGetOption("error_description", num_form, form);
                  state_value = cupsGetOption("state", num_form, form);

                  if (code_value && state_value && !strcmp(state, state_value))
                  {
                    // Got a code and the correct state value, copy the code and
                    // save out code_verifier and nonce values...
                    auth_code = strdup(code_value);
		    hbody     = "<!DOCTYPE html>\n"
				"<html>\n"
				"  <head><title>Authorization Complete</title></head>\n"
				"  <body>\n"
				"    <h1>Authorization Complete</h1>\n"
				"    <p>You may now close this window/tab.</p>\n"
				"    <script>setTimeout(\"window.close()\", 5000)</script>\n"
				"  </body>\n"
				"</html>\n";
                  }
                  else
                  {
                    // Got an error...
		    hbody = message;
                    snprintf(message, sizeof(message),
                             "<!DOCTYPE html>\n"
			     "<html>\n"
			     "  <head><title>Authorization Failed</title></head>\n"
			     "  <body>\n"
			     "    <h1>Authorization Failed</h1>\n"
			     "    <p>%s: %s</p>\n"
			     "  </body>\n"
			     "</html>\n", error_code ? error_code : "bad_response", error_desc ? error_desc : "The authorization server's response was not understood.");
                  }
		  cupsFreeOptions(num_form, form);

                  // Respond accordingly...
		  htype = "text/html";
		}
		else
		{
		  // Resource doesn't exist...
		  hstatus = HTTP_STATUS_NOT_FOUND;
		  htype   = "text/plain";
		  hbody   = "This is not the resource you are looking for.\n";
		}
		break;
          }

	  // Send response...
          DEBUG_printf("1cupsOAuthGetAuthorizationCode: %d %s", hstatus, httpStatusString(hstatus));

	  httpClearFields(http);
	  if (hstatus >= HTTP_STATUS_BAD_REQUEST)
	    httpSetField(http, HTTP_FIELD_CONNECTION, "close");
	  if (htype)
	  {
	    httpSetField(http, HTTP_FIELD_CONTENT_TYPE, htype);
            DEBUG_printf("1cupsOAuthGetAuthorizationCode: Content-Type: %s", htype);
          }
	  if (hbody)
	  {
	    httpSetLength(http, strlen(hbody));
            DEBUG_printf("1cupsOAuthGetAuthorizationCode: Content-Length: %u", (unsigned)strlen(hbody));
          }
	  httpWriteResponse(http, hstatus);

	  if (hbody)
	  {
	    httpWrite(http, hbody, strlen(hbody));
            DEBUG_printf("1cupsOAuthGetAuthorizationCode: %s", hbody);
          }

          // Stop on error...
          if (hstatus != HTTP_STATUS_OK)
            break;
        }

	// Close the client connection...
        httpClose(http);
      }
    }
  }

  done:

  // Free strings, close the listen socket, and return...
  if (fd >= 0)
    httpAddrClose(&addr, fd);

  free(client_id);
  free(code_verifier);
  free(nonce);
  free(scopes_supported);
  free(state);
  free(url);

  DEBUG_printf("1cupsOAuthGetAuthCode: Returning \"%s\".", auth_code);

  return (auth_code);
}


//
// 'cupsOAuthGetClientId()' - Register a client application and get its ID.
//
// This function registers a client application with the specified OAuth
// Authorization Server.
//
// The "auth_uri" argument specifies the URI for the OAuth Authorization
// Server. The "metadata" argument specifies the Authorization Server metadata
// as obtained using @link cupsOAuthCopyMetadata@ and/or
// @link cupsOAuthGetMetadata@.
//
// The "redirect_uri" argument specifies the URL to use for providing
// authorization results to a WWW application.
//
// The "logo_uri" argument specifies a public URL for the logo of your
// application, while the "tos_uri" specifies a public URL for the terms of
// service for your application.
//
// The returned "client_id" string must be freed using the `free` function.
//
// *Note*: This function should only be used to register WWW applications. The
// @link cupsOAuthGetAuthorizationCode@ function handles registration of
// local/"native" applications for you.
//

char *					// O - `client_id` string or `NULL` on error
cupsOAuthGetClientId(
    const char  *auth_uri,		// I - Authorization Server URI
    cups_json_t *metadata,		// I - Authorization Server metadata
    const char  *redirect_uri,		// I - Redirection URL
    const char  *logo_uri,		// I - Logo URL or `NULL` for none
    const char  *tos_uri)		// I - Terms-of-service URL or `NULL` for none
{
  const char	*registration_ep;	// Registration endpoint
  char		software_id[37];	// `software_id` string
  char		*client_id = NULL;	// `client_id` string
  char		*req_data = NULL;	// JSON request data
  cups_json_t	*request,		// JSON request variables
		*response,		// JSON response
		*jarray;		// JSON array
  const char	*value;			// JSON value


  // Range check input...
  if (!auth_uri || !metadata || (registration_ep = cupsJSONGetString(cupsJSONFind(metadata, "registration_endpoint"))) == NULL || !redirect_uri)
    return (NULL);

  // Prepare JSON data to register the client application...
  request = cupsJSONNew(/*parent*/NULL, /*after*/NULL, CUPS_JTYPE_OBJECT);
  cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "client_name"), "CUPS");
  cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "client_uri"), "https://openprinting.github.io/cups/");
  if (logo_uri)
    cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "logo_uri"), logo_uri);
  cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "software_id"), oauth_make_software_id(software_id, sizeof(software_id)));
  cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "software_version"), LIBCUPS_VERSION);
  jarray = cupsJSONNew(request, cupsJSONNewKey(request, /*after*/NULL, "redirect_uris"), CUPS_JTYPE_ARRAY);
  cupsJSONNewString(jarray, /*after*/NULL, redirect_uri);
  if (tos_uri)
    cupsJSONNewString(request, cupsJSONNewKey(request, /*after*/NULL, "tos_uri"), tos_uri);

  req_data = cupsJSONExportString(request);
  cupsJSONDelete(request);

  if (!req_data)
    goto done;

  if ((response = oauth_do_post(registration_ep, "application/json", req_data)) == NULL)
    goto done;

  if ((value = cupsJSONGetString(cupsJSONFind(response, "client_id"))) != NULL)
  {
    if ((client_id = strdup(value)) != NULL)
    {
      // Save client_id and optional client_secret...
      oauth_save_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_ID, value);
      oauth_save_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_SECRET, cupsJSONGetString(cupsJSONFind(response, "client_secret")));
    }
  }

  cupsJSONDelete(response);

  // Return whatever we got...
  done:

  free(req_data);

  return (client_id);
}


//
// 'cupsOAuthGetDeviceGrant()' - Get a device authorization grant for the specified resource and scope(s).
//
// This function requests a device authorization grant for the specified
// resource and scope(s).  Device authorization grants allow a user to open a
// web page on any device to authorize access to the resource.
//
// The "auth_uri" argument specifies the URI for the OAuth Authorization
// Server.  The "metadata" argument specifies the Authorization Server metadata
// as obtained using @link cupsOAuthCopyMetadata@ and/or
// @link cupsOAuthGetMetadata@.
//
// The "resource_uri" argument specifies the URI for a resource (printer, web
// file, etc.) that you which to access.
//
// The "scopes" argument specifies zero or more whitespace-delimited scope
// names to request during authorization.  The list of supported scope names are
// available from the Authorization Server metadata, for example:
//
// ```
// cups_json_t *metadata = cupsOAuthGetMetadata(auth_uri);
// cups_json_t *scopes_supported = cupsJSONFind(metadata, "scopes_supported");
// ```
//
// The returned JSON object must be freed using the @link cupsJSONDelete@
// function and contains the following information:
//
// - `CUPS_ODEVGRANT_DEVICE_CODE`: The device code string to be used in
//   subsequent @link cupsOAuthGetTokens@ calls.
// - `CUPS_ODEVGRANT_EXPIRES_IN`: The expiration date/time as a number of
//   seconds since the Unix epoch.
// - `CUPS_ODEVGRANT_INTERVAL`: The number of seconds to wait between calls to
//   @link cupsOAuthGetTokens@.
// - `CUPS_ODEVGRANT_USER_CODE`: The user code to enter on the verification
//   web page.
// - `CUPS_ODEVGRANT_VERIFICATION_URL`: The URL for the verification web page.
// - `CUPS_ODEVGRANT_VERIFICATION_URL_COMPLETE`: The URL for the verification
//   web page with the user code filled in.
//
// The values can be obtained using the @link cupsJSONFind@,
// @link cupsJSONGetNumber@, and @link cupsJSONGetString@ functions, for
// example:
//
// ```
// cups_json_t *grant = cupsOAuthGetDeviceGrant(...);
//
// const char *verification_url = cupsJSONGetString(cupsJSONFind(grant, CUPS_ODEVGRANT_VERIFICATION_URL));
// double interval = cupsJSONGetNumber(cupsJSONFind(grant, CUPS_ODEVGRANT_INTERVAL));
// ```
//

cups_json_t *				// O - Grant data or `NULL` on error
cupsOAuthGetDeviceGrant(
    const char  *auth_uri,		// I - Authorization Server URI
    cups_json_t *metadata,		// I - Authorization Server metadata
    const char  *resource_uri,		// I - Resource URI
    const char  *scopes)		// I - Space-delimited scopes
{
  const char	*device_ep;		// Device authorization endpoint
  char		*client_id = NULL,	// `client_id` value
		*scopes_supported = NULL;
					// Supported scopes
  size_t	num_form = 0;		// Number of form variables
  cups_option_t	*form = NULL;		// Form variables
  char		*request = NULL;	// Form request data
  cups_json_t	*grant = NULL;		// Device grant


  // Range check input...
  DEBUG_printf("cupsOAuthGetDeviceGrant(auth_uri=\"%s\", metadata=%p, resource_uri=\"%s\", scopes=\"%s\")", auth_uri, (void *)metadata, resource_uri, scopes);

  if (!auth_uri || !metadata || (device_ep = cupsJSONGetString(cupsJSONFind(metadata, "device_authorization_endpoint"))) == NULL)
  {
    _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Device authorization grant is not supported by this server."), true);
    return (NULL);
  }

  // Get the client_id value...
  if ((client_id = cupsOAuthCopyClientId(auth_uri, NULL)) == NULL)
  {
    _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("The client ID is not configured for this server."), true);
    return (NULL);
  }

  DEBUG_printf("1cupsOAuthGetDeviceGrant: client_id=\"%s\"", client_id);

  // Get the scopes value(s)...
  if (!scopes)
  {
    scopes_supported = oauth_copy_scopes(metadata);
    scopes           = scopes_supported;
  }

  DEBUG_printf("1cupsOAuthGetDeviceGrant: scopes=\"%s\"", scopes);

  // Build the request...
  num_form = cupsAddOption("client_id", client_id, num_form, &form);
  if (scopes)
    num_form = cupsAddOption("scope", scopes, num_form, &form);
  if (resource_uri)
    num_form = cupsAddOption("resource", resource_uri, num_form, &form);

  if ((request = cupsFormEncode(/*url*/NULL, num_form, form)) != NULL)
  {
    // Send the device authorization grant request...
    if ((grant = oauth_do_post(device_ep, "application/x-www-form-urlencoded", request)) != NULL)
    {
      // Make sure we have any optional values in the returned JSON...
      const char *user_code = cupsJSONGetString(cupsJSONFind(grant, CUPS_ODEVGRANT_USER_CODE));
      const char *verification_url = cupsJSONGetString(cupsJSONFind(grant, CUPS_ODEVGRANT_VERIFICATION_URI));

      if (!cupsJSONFind(grant, CUPS_ODEVGRANT_DEVICE_CODE) || !cupsJSONFind(grant, CUPS_ODEVGRANT_EXPIRES_IN) || !user_code || !verification_url)
      {
        // Missing required bits, treat this as an error...
        _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(EINVAL), false);
        cupsJSONDelete(grant);
        grant = NULL;
      }
      else
      {
        // Add default 5 second interval...
        if (!cupsJSONFind(grant, CUPS_ODEVGRANT_INTERVAL))
          cupsJSONNewNumber(grant, cupsJSONNewKey(grant, /*after*/NULL, CUPS_ODEVGRANT_INTERVAL), 5.0);

        // Add complete verification URL based on base URL
        if (!cupsJSONFind(grant, CUPS_ODEVGRANT_VERIFICATION_URI_COMPLETE))
        {
          char *complete_url;		// Complete verification URL

          cupsFreeOptions(num_form, form);
          form     = NULL;
          num_form = cupsAddOption("user_code", user_code, 0, &form);

          if ((complete_url = cupsFormEncode(verification_url, num_form, form)) != NULL)
          {
            cupsJSONNewString(grant, cupsJSONNewKey(grant, /*after*/NULL, CUPS_ODEVGRANT_VERIFICATION_URI_COMPLETE), complete_url);
            free(complete_url);
          }
        }
      }
    }
  }

  // Free allocated stuff and return the device authorization grant, if any...
  cupsFreeOptions(num_form, form);

  free(client_id);
  free(request);
  free(scopes_supported);

  return (grant);
}


//
// '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.
//

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, /*use_sysconfig*/true))
    return (NULL);

  if (stat(filename, &fileinfo))
  {
    if (!oauth_make_path(filename, sizeof(filename), auth_uri, /*secondary_uri*/NULL, _CUPS_OTYPE_JWKS, /*use_sysconfig*/false))
      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 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.
//

cups_json_t *				// O - JSON metadata or `NULL` on error
cupsOAuthGetMetadata(
    const char *auth_uri)		// I - Authorization Server URI
{
  char		filename[1024];		// Local metadata filename
  struct stat	fileinfo;		// Local metadata file info
  char		filedate[256],		// Local metadata modification date
		host[256],		// Hostname
		resbase[256],		// Base resource path
		*resptr,		// Pointer to end of base
		resource[256];		// Resource path
  int		port;			// Port to use
  http_t	*http;			// Connection to server
  http_status_t	status = HTTP_STATUS_NOT_FOUND;
					// Request status
  size_t	i;			// Looping var
  static const char * const paths[] =	// Metadata paths
  {
    ".well-known/oauth-authorization-server",
    ".well-known/openid-configuration"
  };


  DEBUG_printf("cupsOAuthGetMetadata(auth_uri=\"%s\")", auth_uri);

  // Special-cases...
  if (!strncmp(auth_uri, "https://github.com", 18) && (!auth_uri[18] || auth_uri[18] == '/'))
    return (cupsJSONImportString(github_metadata));

  // Get existing metadata...
  if (!oauth_make_path(filename, sizeof(filename), auth_uri, /*secondary_uri*/NULL, _CUPS_OTYPE_METADATA, /*use_sysconfig*/true))
    return (NULL);

  if (stat(filename, &fileinfo))
  {
    if (!oauth_make_path(filename, sizeof(filename), auth_uri, /*secondary_uri*/NULL, _CUPS_OTYPE_METADATA, /*use_sysconfig*/false))
      return (NULL);

    if (stat(filename, &fileinfo))
      memset(&fileinfo, 0, sizeof(fileinfo));
  }

  if (fileinfo.st_mtime)
    httpGetDateString(fileinfo.st_mtime, filedate, sizeof(filedate));
  else
    filedate[0] = '\0';

  // Don't bother connecting if the metadata was updated recently...
  if ((time(NULL) - fileinfo.st_mtime) <= 60)
    goto load_metadata;

  // Try getting the metadata...
  if ((http = httpConnectURI(auth_uri, host, sizeof(host), &port, resbase, sizeof(resbase), /*blocking*/true, /*msec*/30000, /*cancel*/NULL, /*require_ca*/true)) == NULL)
    return (NULL);

  if (resbase[0])
    resptr = resbase + strlen(resbase) - 1;
  else
    resptr = resbase;

  for (i = 0; i < (sizeof(paths) / sizeof(paths[0])); i ++)
  {
    if (*resptr != '/')
      snprintf(resource, sizeof(resource), "%s/%s", resbase, paths[i]);
    else
      snprintf(resource, sizeof(resource), "%s%s", resbase, paths[i]);

    do
    {
      if (!_cups_strcasecmp(httpGetField(http, HTTP_FIELD_CONNECTION), "close"))
      {
        httpClearFields(http);
        if (!httpConnectAgain(http, /*msec*/30000, /*cancel*/NULL))
        {
	  status = HTTP_STATUS_ERROR;
	  break;
        }
      }

      httpClearFields(http);

      httpSetField(http, HTTP_FIELD_IF_MODIFIED_SINCE, filedate);
      if (!httpWriteRequest(http, "GET", resource))
      {
        if (!httpConnectAgain(http, 30000, NULL) || !httpWriteRequest(http, "GET", resource))
        {
          status = HTTP_STATUS_ERROR;
	  break;
        }
      }

      while ((status = httpUpdate(http)) == HTTP_STATUS_CONTINUE)
        ;

      if (status >= HTTP_STATUS_MULTIPLE_CHOICES && status <= HTTP_STATUS_SEE_OTHER)
      {
        // Redirect
	char	lscheme[32],		// Location scheme
		luserpass[256],		// Location user:password (not used)
		lhost[256],		// Location hostname
		lresource[256];		// Location resource path
	int	lport;			// Location port

        if (httpSeparateURI(HTTP_URI_CODING_ALL, httpGetField(http, HTTP_FIELD_LOCATION), lscheme, sizeof(lscheme), luserpass, sizeof(luserpass), lhost, sizeof(lhost), &lport, lresource, sizeof(lresource)) < HTTP_URI_STATUS_OK)
	  break;			// Don't redirect to an invalid URI

        if (_cups_strcasecmp(host, lhost) || port != lport)
	  break;			// Don't redirect off this host

        // Redirect to a local resource...
        cupsCopyString(resource, lresource, sizeof(resource));
      }
    }
    while (status >= HTTP_STATUS_MULTIPLE_CHOICES && status <= HTTP_STATUS_SEE_OTHER);

    if (status == HTTP_STATUS_NOT_MODIFIED)
    {
      // Metadata isn't changed, stop now...
      break;
    }
    else if (status == HTTP_STATUS_OK)
    {
      // Copy the metadata to the file...
      int	fd;			// Local metadata file
      char	buffer[8192];		// Copy buffer
      ssize_t	bytes;			// Bytes read

      if ((fd = open(filename, O_CREAT | O_TRUNC | O_WRONLY | O_NOFOLLOW, 0600)) < 0)
      {
        httpFlush(http);
        break;
      }

      while ((bytes = httpRead(http, buffer, sizeof(buffer))) > 0)
        write(fd, buffer, (size_t)bytes);

      close(fd);
      break;
    }
  }

  httpClose(http);

  if (status != HTTP_STATUS_OK && status != HTTP_STATUS_NOT_MODIFIED)
  {
    // Remove old cached data and return NULL...
    unlink(filename);
    return (NULL);
  }

  // Return the cached metadata, if any...
  load_metadata:

  return (cupsJSONImportFile(filename));
}


//
// 'cupsOAuthGetTokens()' - Obtain access and refresh tokens.
//
// This function obtains a access and refresh tokens from an OAuth Authorization
// Server. OpenID Authorization Servers also provide user identification
// information.
//
// The "auth_uri" argument specifies the URI for the OAuth Authorization
// Server.  The "metadata" argument specifies the Authorization Server metadata
// as obtained using @link cupsOAuthCopyMetadata@ and/or
// @link cupsOAuthGetMetadata@.
//
// The "resource_uri" argument specifies the URI for a resource (printer, web
// file, etc.) that you which to access.
//
// The "grant_code" argument specifies the code or token to use while the
// "grant_type" argument specifies the type of code:
//
// - `CUPS_OGRANT_AUTHORIZATION_CODE`: A user authorization grant code.
// - `CUPS_OGRANT_DEVICE_CODE`: A device authorization grant code.
// - `CUPS_OGRANT_REFRESH_TOKEN`: A refresh token.
//
// The "redirect_uri" specifies the redirection URI used to obtain the code. The
// constant `CUPS_OAUTH_REDIRECT_URI` should be used for codes obtained using
// the @link cupsOAuthGetAuthorizationCode@ function.
//
// When successful, the access token and expiration time are returned. The
// access token must be freed using the `free` function. The new refresh token
// and any user ID information can be obtained using the
// @link cupsOAuthCopyRefreshToken@ and @link cupsOAuthCopyUserId@ functions
// respectively.
//
// When authorizing using a device code (`CUPS_OGRANT_DEVICE_CODE`) and a device
// access token is not yet ready, a `NULL` access token is returned with the
// expiration time set to the next recommended query time.  If the
// "access_expires" value is set to `0` then the device authorization failed.
//

char *					// O - Access token or `NULL` on error
cupsOAuthGetTokens(
    const char    *auth_uri,		// I - Authorization Server URI
    cups_json_t   *metadata,		// I - Authorization Server metadata
    const char    *resource_uri,	// I - Resource URI
    const char    *grant_code,		// I - Authorization code, device code, or refresh token
    cups_ogrant_t grant_type,		// I - Grant code type
    const char    *redirect_uri,	// I - Redirect URI or `NULL` for device grants
    time_t        *access_expires)	// O - Expiration time for access token
{
  const char	*token_ep;		// Token endpoint
  char		*value,			// Prior value
		*nonce = NULL;		// Prior nonce value
  size_t	num_form = 0;		// Number of form variables
  cups_option_t	*form = NULL;		// Form variables
  char		*request = NULL;	// Form request data
  cups_json_t	*response = NULL;	// JSON response variables
  const char	*access_value = NULL,	// access_token
		*error,			// Error code, if any
		*id_value = NULL,	// id_token
		*refresh_value = NULL;	// refresh_token
  double	expires_in;		// expires_in value
  time_t	access_expvalue;	// Expiration time for access_token
  cups_jwt_t	*jwt = NULL;		// JWT of the id_token
  const char	*jnonce;		// Nonce value from the JWT
  char		*access_token = NULL;	// Access token
  static const char * const grant_types[] =
  {					// Grant type strings
    "authorization_code",
    "urn:ietf:params:oauth:grant-type:device_code",
    "refresh_token"
  };


  DEBUG_printf("cupsOAuthGetTokens(auth_uri=\"%s\", metadata=%p, resource_uri=\"%s\", grant_code=\"%s\", grant_type=%d, redirect_uri=\"%s\", access_expires=%p)", auth_uri, (void *)metadata, resource_uri, grant_code, grant_type, redirect_uri, (void *)access_expires);

  // Range check input...
  if (access_expires)
    *access_expires = 0;

  if (!auth_uri || !metadata || (token_ep = cupsJSONGetString(cupsJSONFind(metadata, "token_endpoint"))) == NULL || !grant_code || (!redirect_uri && grant_type != CUPS_OGRANT_DEVICE_CODE))
    return (NULL);

  // Prepare form data to get an access token...
  num_form = cupsAddOption("grant_type", grant_types[grant_type], num_form, &form);
  if (grant_type == CUPS_OGRANT_DEVICE_CODE)
  {
    num_form = cupsAddOption("device_code", grant_code, num_form, &form);
  }
  else
  {
    num_form = cupsAddOption("code", grant_code, num_form, &form);

    if (!strcmp(redirect_uri, CUPS_OAUTH_REDIRECT_URI) && (value = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_REDIRECT_URI, /*try_sysconfig*/false)) != NULL)
    {
      DEBUG_printf("1cupsOAuthGetTokens: redirect_uri=\"%s\"", value);
      num_form = cupsAddOption("redirect_uri", value, num_form, &form);
      free(value);
    }
    else
    {
      num_form = cupsAddOption("redirect_uri", redirect_uri, num_form, &form);
    }
  }

  if ((value = cupsOAuthCopyClientId(auth_uri, redirect_uri)) != NULL)
  {
    DEBUG_printf("1cupsOAuthGetTokens: client_id=\"%s\"", value);
    num_form = cupsAddOption("client_id", value, num_form, &form);
    free(value);
  }

  if ((value = oauth_load_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_SECRET, /*try_sysconfig*/true)) != NULL)
  {
    DEBUG_printf("1cupsOAuthGetTokens: client_secret=\"%s\"", value);
    num_form = cupsAddOption("client_secret", value, num_form, &form);
    free(value);
  }

  if (grant_type != CUPS_OGRANT_DEVICE_CODE && (value = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_CODE_VERIFIER, /*try_sysconfig*/false)) != NULL)
  {
    DEBUG_printf("1cupsOAuthGetTokens: code_verifier=\"%s\"", value);
    num_form = cupsAddOption("code_verifier", value, num_form, &form);
    free(value);
  }

  request = cupsFormEncode(/*url*/NULL, num_form, form);
  cupsFreeOptions(num_form, form);

  if (!request)
    goto done;

  if ((response = oauth_do_post(token_ep, "application/x-www-form-urlencoded", request)) == NULL)
    goto done;

  error         = cupsJSONGetString(cupsJSONFind(response, "error"));
  access_value  = cupsJSONGetString(cupsJSONFind(response, "access_token"));
  expires_in    = cupsJSONGetNumber(cupsJSONFind(response, "expires_in"));
  id_value      = cupsJSONGetString(cupsJSONFind(response, "id_token"));
  refresh_value = cupsJSONGetString(cupsJSONFind(response, "refresh_token"));

  if (error)
  {
    if (access_expires)
    {
      // Handle "soft" device access token errors by setting access_expires to
      // the next call time...
      if (!strcmp(error, "slow_down"))
	*access_expires = time(NULL) + 10;
      else
	*access_expires = time(NULL) + 5;
    }

    // Free memory and return...
    cupsJSONDelete(response);
    free(request);

    return (NULL);
  }

  if (id_value)
  {
    // Validate the JWT
    cups_json_t	*jwks;			// JWT key set
    bool	valid;			// Valid id_token?
    const char	*at_hash;		// at_hash claim value

    jwt    = cupsJWTImportString(id_value, CUPS_JWS_FORMAT_COMPACT);
    jnonce = cupsJWTGetClaimString(jwt, "nonce");
    nonce  = oauth_load_value(auth_uri, resource_uri, _CUPS_OTYPE_NONCE, /*try_sysconfig*/false);

    // Check nonce
    if (!jwt || (jnonce && nonce && strcmp(jnonce, nonce)))
      goto done;

    // Validate id_token against the Authorization Server's JWKS
    if ((jwks = cupsOAuthGetJWKS(auth_uri, metadata)) == NULL)
      goto done;

    valid = cupsJWTHasValidSignature(jwt, jwks);
    DEBUG_printf("1cupsOAuthGetTokens: valid=%s", valid ? "true" : "false");
    cupsJSONDelete(jwks);
    if (!valid)
      goto done;

    // Validate the at_hash claim string against access_token value
    if (access_value && (at_hash = cupsJWTGetClaimString(jwt, "at_hash")) != NULL)
    {
      unsigned char sha256[32],		// Hash of the access_token value
		at_hash_buffer[32];	// at_hash bytes
      size_t	at_hash_bytes = sizeof(at_hash_buffer);
		      			// Number of at_hash bytes

      cupsHashData("sha2-256", access_value, strlen(access_value), sha256, sizeof(sha256));
      httpDecode64((char *)at_hash_buffer, &at_hash_bytes, at_hash, /*end*/NULL);
      if (at_hash_bytes != 16 || memcmp(sha256, at_hash_buffer, 16))
      {
        DEBUG_puts("1cupsOAuthGetTokens: at_hash doesn't match SHA-256 of access_token.");
        goto done;
      }
    }
  }

  if (expires_in > 0.0)
    access_expvalue = time(NULL) + (long)expires_in;
  else
    access_expvalue = 0;

  cupsOAuthSaveTokens(auth_uri, resource_uri, access_value, access_expvalue, id_value, refresh_value);

  if (access_value)
    access_token = strdup(access_value);

  if (access_expires)
    *access_expires = access_expvalue;

  // Return whatever we got...
  done:

  // Clear code_verifier and nonce values...
  oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_CODE_VERIFIER, /*value*/NULL);
  oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_NONCE, /*value*/NULL);

  cupsJSONDelete(response);
  cupsJWTDelete(jwt);
  free(nonce);
  free(request);

  return (access_token);
}


//
// 'cupsOAuthGetUserId()' - Get the user ID token associated with the given access token.
//
// This function retrieves the user ID token associated with a given access
// token.  The user ID information is cached until the token expires to minimize
// the overhead of communicating with the Authorization Server.
//
// @since CUPS 2.5@
//

cups_jwt_t *				// O - Identification information or `NULL` if none
cupsOAuthGetUserId(
    const char  *auth_uri,		// I - Authorization Server URL
    cups_json_t *metadata,		// I - Authorization Server metadata
    const char  *access_token)		// I - Access (Bearer) token
{
  char		*access_uri,		// Access token URI
		*user_id_value;		// String value of user ID
  cups_jwt_t	*user_id = NULL;	// User ID JWT
  const char	*userinfo_ep;		// userinfo_endpoint value


  // Range check input...
  if (!auth_uri || !metadata || !access_token || !*access_token)
    return (NULL);

  // Convert the access token to a URI...
  if ((access_uri = oauth_make_access_uri(access_token)) == NULL)
    return (NULL);

  // See if we have this token cached already...
  if ((user_id_value = oauth_load_value(auth_uri, access_uri, _CUPS_OTYPE_USER_ID, /*try_sysconfig*/false)) == NULL && (userinfo_ep = cupsJSONGetString(cupsJSONFind(metadata, "userinfo_endpoint"))) != NULL)
  {
    // No, try to get the information from the Authorization Server...
    http_t	*http;			// Connection to endpoint
    char	host[256],		// Endpoint hostname
		resource[1024];		// Endpoint resource
    int		port;			// Endpoint port
    http_status_t status;		// GET status
    cups_json_t	*user_id_claims;	// Claims from userinfo response

    DEBUG_printf("1cupsOAuthGetUserId: Getting user ID from \"%s\".", userinfo_ep);

    // Connect to the endpoint...
    if ((http = httpConnectURI(userinfo_ep, host, sizeof(host), &port, resource, sizeof(resource), /*blocking*/true, /*msec*/30000, /*cancel*/NULL, /*require_ca*/true)) == NULL)
      return (NULL);

    // Send a GET request with the access token...
    httpSetAuthString(http, "Bearer", access_token);
    httpClearFields(http);
    httpSetField(http, HTTP_FIELD_ACCEPT, "application/json,text/json");

    if (!httpWriteRequest(http, "GET", resource))
    {
      if (!httpConnectAgain(http, 30000, NULL))
	goto done;

      if (!httpWriteRequest(http, "GET", resource))
	goto done;
    }

    // Get the response...
    do
    {
      status = httpUpdate(http);
    } while (status == HTTP_STATUS_CONTINUE);

    user_id_value  = oauth_copy_response(http);
    user_id_claims = cupsJSONImportString(user_id_value);

    // Verify that the access token hasn't expired...
    if (cupsJSONFind(user_id_claims, "error"))
    {
      // Expired or bad access token, get rid of this one...
      const char *error_desc = cupsJSONGetString(cupsJSONFind(user_id_claims, "error_description"));
					// Description of error

      DEBUG_printf("1cupsOAuthGetUserId: Error from userinfo - %s, %s.", cupsJSONGetString(cupsJSONFind(user_id_claims, "error")), error_desc);
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, error_desc, false);

      cupsJSONDelete(user_id_claims);
      oauth_save_value(auth_uri, access_uri, _CUPS_OTYPE_USER_ID, /*value*/NULL);
    }
    else if ((user_id = cupsJWTNew(/*type*/NULL, user_id_claims)) != NULL)
    {
      // Created a new JWT with the JSON user information, save it for future use...
      free(user_id_value);

      user_id_value = cupsJWTExportString(user_id, CUPS_JWS_FORMAT_COMPACT);
      oauth_save_value(auth_uri, access_uri, _CUPS_OTYPE_USER_ID, user_id_value);
    }
    else
    {
      // Free the JSON information since we couldn't create the JWT...
      cupsJSONDelete(user_id_claims);
    }
  }
  else if (user_id_value)
  {
    // Convert the string to a JWT...
    user_id = cupsJWTImportString(user_id_value, CUPS_JWS_FORMAT_COMPACT);
  }

  done:

  // Free strings and return...
  free(access_uri);
  free(user_id_value);

  return (user_id);
}


//
// 'cupsOAuthMakeAuthorizationURL()' - Make an authorization URL.
//
// This function makes an authorization URL for the specified authorization
// server and resource.
//
// The "auth_uri" argument specifies the URI for the OAuth Authorization
// Server.  The "metadata" argument specifies the Authorization Server metadata
// as obtained using @link cupsOAuthCopyMetadata@ and/or
// @link cupsOAuthGetMetadata@.
//
// The "resource_uri" argument specifies the URI for a resource (printer, web
// file, etc.) that you which to access.
//
// The "scopes" argument specifies zero or more whitespace-delimited scope
// names to request during authorization.  The list of supported scope names are
// available from the Authorization Server metadata, for example:
//
// ```
// cups_json_t *metadata = cupsOAuthGetMetadata(auth_uri);
// cups_json_t *scopes_supported = cupsJSONFind(metadata, "scopes_supported");
// ```
//
// The "client_id" argument specifies the client identifier obtained using
// @link cupsOAuthCopyClientId@ and/or @link cupsOAuthGetClientId@.
//
// The "client_id" argument is the string returned by
// @link cupsOAuthCopyClientId@ or @link cupsOAuthGetClientId@.
//
// The "code_verifier" argument specifies a random Base64URL-encoded string
// that is used by the Proof Key for Code Exchange [RFC7636] extension to help
// secure the authorization flow.  The @link cupsOAuthMakeBase64Random@ function
// can be used to generate this string.
//
// The "nonce" argument specifies a random Base64URL-encoded string that is
// used by OpenID to validate the ID token. The @link cupsOAuthMakeBase64Random@
// function can be used to generate this string.
//
// The "redirect_uri" argument specifies the URI that will receive the
// authorization grant code.
//
// The "state" argument is a unique (random) identifier for the authorization
// request.  It is provided to the redirection URI as a form parameter.
//

char *					// O - Authorization URL
cupsOAuthMakeAuthorizationURL(
    const char  *auth_uri,		// I - Authorization Server URI
    cups_json_t *metadata,		// I - Authorization Server metadata
    const char  *resource_uri,		// I - Resource URI
    const char  *scopes,		// I - Space-delimited scope(s)
    const char  *client_id,		// I - Client ID
    const char  *code_verifier,		// I - Code verifier string
    const char  *nonce,			// I - Nonce
    const char  *redirect_uri,		// I - Redirection URI
    const char  *state)			// I - State
{
  const char	*authorization_ep;	// Authorization endpoint
  unsigned char	sha256[32];		// SHA-256 hash of code verifier
  char		code_challenge[64];	// Hashed code verifier string
  size_t	num_vars = 0;		// Number of form variables
  cups_option_t	*vars = NULL;		// Form variables
  char		*url,			// URL for authorization page
		*scopes_supported;	// Supported scopes


  // Range check input...
  if (!auth_uri || !metadata || (authorization_ep = cupsJSONGetString(cupsJSONFind(metadata, "authorization_endpoint"))) == NULL || !redirect_uri || !client_id)
    return (NULL);

  // Make the authorization URL using the information supplied...
  if (oauth_metadata_contains(metadata, "response_type_supported", "code id_token"))
    num_vars = cupsAddOption("response_type", "code id_token", num_vars, &vars);
  else
    num_vars = cupsAddOption("response_type", "code", num_vars, &vars);

  num_vars = cupsAddOption("client_id", client_id, num_vars, &vars);
  num_vars = cupsAddOption("redirect_uri", redirect_uri, num_vars, &vars);

  if (code_verifier && oauth_metadata_contains(metadata, "code_challenge_methods_supported", "S256"))
  {
    cupsHashData("sha2-256", code_verifier, strlen(code_verifier), sha256, sizeof(sha256));
    httpEncode64(code_challenge, sizeof(code_challenge), (char *)sha256, sizeof(sha256), true);
    num_vars = cupsAddOption("code_challenge", code_challenge, num_vars, &vars);
    num_vars = cupsAddOption("code_challenge_method", "S256", num_vars, &vars);

    oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_CODE_VERIFIER, code_verifier);
  }

  if (nonce && oauth_metadata_contains(metadata, "scopes_supported", "openid"))
  {
    num_vars = cupsAddOption("nonce", nonce, num_vars, &vars);

    oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_NONCE, nonce);
  }

  if (resource_uri)
    num_vars = cupsAddOption("resource", resource_uri, num_vars, &vars);

  if (scopes)
  {
    num_vars = cupsAddOption("scope", scopes, num_vars, &vars);
  }
  else if ((scopes_supported = oauth_copy_scopes(metadata)) != NULL)
  {
    num_vars = cupsAddOption("scope", scopes_supported, num_vars, &vars);
    free(scopes_supported);
  }

  if (state)
    num_vars = cupsAddOption("state", state, num_vars, &vars);

  url = cupsFormEncode(authorization_ep, num_vars, vars);

  cupsFreeOptions(num_vars, vars);

  return (url);
}


//
// 'cupsOAuthMakeBase64Random()' - Make a random data string.
//
// This function creates a string containing random data that has been Base64URL
// encoded. "len" specifies the number of random bytes to include in the string.
// The returned string must be freed using the `free` function.
//

char *					// O - Random string
cupsOAuthMakeBase64Random(size_t len)	// I - Number of bytes
{
  size_t	i;			// Looping var
  char		bytes[768],		// Random bytes
		base64url[1025];	// Random string


  // Range check input...
  len = len * 3 / 4;

  if (len < 1)
    len = 1;
  else if (len > sizeof(bytes))
    len = sizeof(bytes);

  // Fill out random bytes and convert it to Base64URL...
  for (i = 0; i < len; i ++)
    bytes[i] = (char)cupsGetRand();

  httpEncode64(base64url, sizeof(base64url), bytes, len, /*url*/true);

  // Copy and return the random string...
  return (strdup(base64url));
}


//
// 'cupsOAuthSaveClientData()' - Save client_id and client_secret values.
//
// This function saves the "client_id" and "client_secret" values for the given
// Authorization Server "auth_uri" and redirection URI "redirect_uri". If the
// "client_id" is `NULL` then any saved values are deleted from the per-user
// store.
//

void
cupsOAuthSaveClientData(
    const char *auth_uri,		// I - Authorization Server URI
    const char *redirect_uri,		// I - Redirection URI
    const char *client_id,		// I - client_id or `NULL` to delete
    const char *client_secret)		// I - client_secret value or `NULL` for none
{
  oauth_save_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_ID, client_id);
  oauth_save_value(auth_uri, redirect_uri, _CUPS_OTYPE_CLIENT_SECRET, client_secret);
}


//
// 'cupsOAuthSaveTokens()' - Save authorization and refresh tokens.
//
// This function saves the access token "access_token", user ID "user_id", and
// refresh token "refresh_token" values for the given Authorization Server
// "auth_uri" and resource "resource_uri". Specifying `NULL` for any of the
// values will delete the corresponding saved values from the per-user store.
//

void
cupsOAuthSaveTokens(
    const char *auth_uri,		// I - Authorization Server URI
    const char *resource_uri,		// I - Resource URI
    const char *access_token,		// I - Access token or `NULL` to delete
    time_t     access_expires,		// I - Access expiration time
    const char *user_id,		// I - User ID or `NULL` to delete
    const char *refresh_token)		// I - Refresh token or `NULL` to delete
{
  char		temp[16384];		// Temporary string


  DEBUG_printf("cupsOAuthSaveTokens(auth_uri=\"%s\", resource_uri=\"%s\", access_token=\"%s\", access_expires=%ld, user_id=\"%s\", refresh_token=\"%s\")", auth_uri, resource_uri, access_token, (long)access_expires, user_id, refresh_token);

  // Access token...
  if (access_token)
  {
    // Save access token...
    char	*access_uri;		// Access token URI

    snprintf(temp, sizeof(temp), "%s\n%ld\n", access_token, (long)access_expires);
    oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_ACCESS, temp);

    if ((access_uri = oauth_make_access_uri(access_token)) != NULL)
    {
      oauth_save_value(auth_uri, access_uri, _CUPS_OTYPE_USER_ID, user_id);
      free(access_uri);
    }
  }
  else
  {
    // Remove access token
    oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_ACCESS, NULL);
  }

  // User ID...
  oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_USER_ID, user_id);

  // Refresh token...
  oauth_save_value(auth_uri, resource_uri, _CUPS_OTYPE_REFRESH, refresh_token);
}


//
// 'oauth_copy_response()' - Copy the response from a HTTP response.
//

static char *				// O - Response as a string
oauth_copy_response(http_t *http)	// I - HTTP connection
{
  char		*body,			// Message body data string
		*end,			// End of data
		*ptr;			// Pointer into string
  size_t	bodylen;		// Allocated length of string
  ssize_t	bytes;			// Bytes read
  http_state_t	initial_state;		// Initial HTTP state


  // Allocate memory for string...
  initial_state = httpGetState(http);

  if ((bytes = httpGetLength(http)) < 0)
    return (NULL);
  else if (bytes == 0 || bytes > 65536)
    bodylen = 65536;			// Accept up to 64k for GETs/POSTs
  else
    bodylen = (size_t)bytes;

  if ((body = calloc(1, bodylen + 1)) != NULL)
  {
    for (ptr = body, end = body + bodylen; ptr < end; ptr += bytes)
    {
      if ((bytes = httpRead(http, ptr, (size_t)(end - ptr))) <= 0)
        break;
    }
  }

  if (httpGetState(http) == initial_state)
    httpFlush(http);

  return (body);
}


//
// 'oauth_copy_scopes()' - Copy all supported scopes from the metadata.
//
// Caller must free returned string...
//

static char *				// O - Scopes
oauth_copy_scopes(
    cups_json_t *metadata)		// I - Metadata
{
  char		*scopes = NULL;		// Supported scopes
  cups_json_t	*values;		// Parameter values


  if ((values = cupsJSONFind(metadata, "scopes_supported")) != NULL)
  {
    // Convert scopes_supported to a string...
    size_t	i,			// Looping var
		count,			// Number of values
		length = 0;		// Length of string
    cups_json_t	*current;		// Current value
    const char	*scope;			// Scope string

    // Figure out the total length...
    for (i = 0, count = cupsJSONGetCount(values); i < count; i ++)
    {
      current = cupsJSONGetChild(values, i);

      if (cupsJSONGetType(current) == CUPS_JTYPE_STRING)
      {
        // Only copy common scopes...
        scope = cupsJSONGetString(current);

        if (!strcmp(scope, "email") || !strcmp(scope, "profile") || !strcmp(scope, "openid"))
          length += strlen(scope) + 1;
      }
    }

    if (length > 0 && (scopes = malloc(length)) != NULL)
    {
      // Copy the scopes to a string with spaces between them...
      char	*ptr;			// Pointer into value

      for (i = 0, ptr = scopes; i < count; i ++)
      {
        // Append each scope...
	current = cupsJSONGetChild(values, i);

	if (cupsJSONGetType(current) == CUPS_JTYPE_STRING)
	{
	  // Only copy common scopes...
          scope = cupsJSONGetString(current);

          if (!strcmp(scope, "email") || !strcmp(scope, "profile") || !strcmp(scope, "openid"))
          {
	    if (ptr > scopes)
	      *ptr++ = ' ';

	    cupsCopyString(ptr, scope, length - (size_t)(ptr - scopes));
	    ptr += strlen(ptr);
	  }
	}
      }
    }
  }

  return (scopes);
}


//
// 'oauth_do_post()' - Send a POST request with the specified data and do error
//                     handling, returning JSON when available.
//

static cups_json_t *			// O - JSON response
oauth_do_post(const char *ep,		// I - Endpoint URI
              const char *content_type,	// I - Content type
              const char *request)	// I - Request message body
{
  http_t	*http;			// Connection to endpoint
  char		host[256],		// Endpoint hostname
		resource[1024];		// Endpoint resource
  int		port;			// Endpoint port
  size_t	req_length;		// Length of request message body
  http_status_t	status;			// POST status
  char		*response;		// Response message body
  cups_json_t	*resp_json = NULL;	// Response JSON
  bool		resp_error;		// Is the response an error?


  DEBUG_printf("3oauth_do_post(ep=\"%s\", content_type=\"%s\", request=\"%s\")", ep, content_type, request);

  // Connect to the endpoint...
  if ((http = httpConnectURI(ep, host, sizeof(host), &port, resource, sizeof(resource), /*blocking*/true, /*msec*/30000, /*cancel*/NULL, /*require_ca*/true)) == NULL)
    return (NULL);

  // Send a POST request with the request data...
  req_length = strlen(request);

  httpClearFields(http);
  httpSetField(http, HTTP_FIELD_ACCEPT, "application/json,text/json");
  httpSetField(http, HTTP_FIELD_CONTENT_TYPE, content_type);
  httpSetLength(http, req_length);

  if (!httpWriteRequest(http, "POST", resource))
  {
    if (!httpConnectAgain(http, 30000, NULL))
      goto done;

    if (!httpWriteRequest(http, "POST", resource))
      goto done;
  }

  if (httpWrite(http, request, req_length) < (ssize_t)req_length)
    goto done;

  // Get the response...
  while ((status = httpUpdate(http)) == HTTP_STATUS_CONTINUE);

  response  = oauth_copy_response(http);
  resp_json = cupsJSONImportString(response);

  free(response);

  // Check for errors...
  resp_error = oauth_set_error(resp_json, /*num_form*/0, /*form*/NULL);
  if (!resp_error && !cupsJSONFind(resp_json, "error") && status != HTTP_STATUS_OK)
  {
    _cupsSetError(IPP_STATUS_ERROR_INTERNAL, httpStatusString(status), false);
    resp_error = true;
  }

  if (resp_error)
  {
    cupsJSONDelete(resp_json);
    resp_json = NULL;
  }

  // Close the HTTP connection and return any JSON we have...
  done:

  httpClose(http);

  return (resp_json);
}


//
// 'oauth_load_value()' - Load the contents of the specified value file.
//

static char *
oauth_load_value(
    const char    *auth_uri,		// I - Authorization Server URI
    const char    *secondary_uri,	// I - Resource or redirect URI
    _cups_otype_t otype,		// I - Type (_CUPS_OTYPE_xxx)
    bool          try_sysconfig)	// I - Try system configuration if no user config?
{
  char		filename[1024];		// Filename
  struct stat	fileinfo;		// File information
  int		fd;			// File descriptor
  char		*value = NULL;		// Value


  DEBUG_printf("3oauth_load_value(auth_uri=\"%s\", secondary_uri=\"%s\", otype=%s, try_sysconfig=%s)", auth_uri, secondary_uri, cups_otypes[otype], try_sysconfig ? "true" : "false");

  // Try to make the corresponding file path...
  if (!oauth_make_path(filename, sizeof(filename), auth_uri, secondary_uri, otype, /*use_sysconfig*/false))
    return (NULL);

  // Open the file...
  if ((fd = open(filename, O_RDONLY)) < 0 && try_sysconfig)
  {
    // Try the sysconfig version...

    if (!oauth_make_path(filename, sizeof(filename), auth_uri, secondary_uri, otype, /*use_sysconfig*/true))
      return (NULL);

    fd = open(filename, O_RDONLY);
  }

  if (fd >= 0)
  {
    // Opened, read up to 64k of data...
    if (!fstat(fd, &fileinfo) && fileinfo.st_size <= 65536 && (value = calloc(1, (size_t)fileinfo.st_size + 1)) != NULL)
      read(fd, value, (size_t)fileinfo.st_size);
    else
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), false);

    close(fd);
  }
  else
  {
    _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), false);
  }

  // Return whatever we have...
  DEBUG_printf("4oauth_load_value: Returning \"%s\".", value);

  return (value);
}


//
// 'oauth_make_access_uri()' - Make an access token URI.
//
// Note: You must free the returned pointer.
//

static char *				// O - Access token URI
oauth_make_access_uri(
    const char *access_token)		// I - Access token
{
  char		*access_uri;		// Access token URI
  size_t	access_uri_size;	// Size of access token URI (including nul)


  access_uri_size = strlen(access_token) + 11;
  if ((access_uri = malloc(access_uri_size)) != NULL)
    snprintf(access_uri, access_uri_size, "urn:token:%s", access_token);

  return (access_uri);
}


//
// 'oauth_make_path()' - Make an OAuth store filename.
//

static char *				// O - Filename
oauth_make_path(
    char          *buffer,		// I - Filename buffer
    size_t        bufsize,		// I - Size of filename buffer
    const char    *auth_uri,		// I - Authorization server URI
    const char    *secondary_uri,	// I - Resource/redirect URI
    _cups_otype_t otype,		// I - Type (_CUPS_OTYPE_xxx)
    bool          use_sysconfig)	// I - Use system configuration directory
{
  int		port;			// Port number from URI
  char		auth_temp[1024],	// Temporary copy of auth_uri
		secondary_temp[1024],	// Temporary copy of secondary_uri
		*ptr;			// Pointer into temporary strings
  unsigned char	auth_hash[32],		// SHA-256 hash of base auth_uri
		secondary_hash[32];	// SHA-256 hash of base secondary_uri
  _cups_globals_t *cg = _cupsGlobals();	// Global data
  static const char * const otypes[_CUPS_OTYPE_MAX] =
  {					// Filename extensions for each type
    "accs",				// Access token
    "clid",				// Client ID
    "csec",				// Client secret
    "cver",				// Code verifier
    "idtk",				// ID token
    "jwks",				// Key store
    "meta",				// Metadata
    "nonc",				// Nonce
    "ruri",				// Redirect URI
    "rfsh"				// Refresh token
  };


  DEBUG_printf("3oauth_make_path(buffer=%p, bufsize=%lu, auth_uri=\"%s\", secondary_uri=\"%s\", otype=%s, use_sysconfig=%s)", (void *)buffer, (unsigned long)bufsize, auth_uri, secondary_uri, cups_otypes[otype], use_sysconfig ? "true" : "false");

  // Range check input...
  if (!auth_uri || strncmp(auth_uri, "https://", 8) || auth_uri[8] == '[' || isdigit(auth_uri[8] & 255) || (secondary_uri && strncmp(secondary_uri, "http://", 7) && strncmp(secondary_uri, "https://", 8) && strncmp(secondary_uri, "urn:token:", 10)))
  {
    _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(EINVAL), false);
    *buffer = '\0';
    return (NULL);
  }

  // If the secondary URI is "http://127.0.0.1:port/" then map it to
  // CUPS_OAUTH_REDIRECT_URI to ensure we have a consistent hash...
  if (secondary_uri && sscanf(secondary_uri, "http://127.0.0.1:%d/", &port) == 1)
    secondary_uri = CUPS_OAUTH_REDIRECT_URI;

  // First make sure the "oauth" directory exists...
  if (use_sysconfig)
  {
    // Check and create in sysconfig directory, allowing for other users to read
    // common files as needed...
    snprintf(buffer, bufsize, "%s/oauth", cg->sysconfig);

    if (!_cupsDirCreate(buffer, 0711))
    {
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), false);
      *buffer = '\0';
      return (NULL);
    }
  }
  else
  {
    // Check and create in (private) userconfig directory...
    snprintf(buffer, bufsize, "%s/oauth", cg->userconfig);

    if (!_cupsDirCreate(buffer, 0700))
    {
      _cupsSetError(IPP_STATUS_ERROR_INTERNAL, strerror(errno), false);
      *buffer = '\0';
      return (NULL);
    }
  }

  // Build the hashed versions of the auth and resource URIs...
  cupsCopyString(auth_temp, auth_uri + 8, sizeof(auth_temp));
  if ((ptr = strchr(auth_temp, '/')) != NULL)
    *ptr = '\0';			// Strip resource path
  if (!strchr(auth_temp, ':'))		// Add :443 if no port is present
    cupsConcatString(auth_temp, ":443", sizeof(auth_temp));

  cupsHashData("sha2-256", auth_temp, strlen(auth_temp), auth_hash, sizeof(auth_hash));
  cupsHashString(auth_hash, sizeof(auth_hash), auth_temp, sizeof(auth_temp));

  if (secondary_uri)
  {
    if (!strncmp(secondary_uri, "http://", 7))
    {
      // HTTP URI
      cupsCopyString(secondary_temp, secondary_uri + 7, sizeof(secondary_temp));
      if ((ptr = strchr(secondary_temp, '/')) != NULL)
        *ptr = '\0';			// Strip resource path
      if (!strchr(secondary_temp, ':'))	// Add :80 if no port is present
        cupsConcatString(secondary_temp, ":80", sizeof(secondary_temp));
    }
    else if (!strncmp(secondary_uri, "https://", 8))
    {
      // HTTPS URI
      cupsCopyString(secondary_temp, secondary_uri + 8, sizeof(secondary_temp));
      if ((ptr = strchr(secondary_temp, '/')) != NULL)
        *ptr = '\0';			// Strip resource path
      if (!strchr(secondary_temp, ':'))	// Add :443 if no port is present
        cupsConcatString(secondary_temp, ":443", sizeof(secondary_temp));
    }
    else
    {
      // IPPS URI
      cupsCopyString(secondary_temp, secondary_uri + 7, sizeof(secondary_temp));
      if ((ptr = strchr(secondary_temp, '/')) != NULL)
        *ptr = '\0';			// Strip resource path
      if (!strchr(secondary_temp, ':'))	// Add :631 if no port is present
        cupsConcatString(secondary_temp, ":631", sizeof(secondary_temp));
    }

    cupsHashData("sha2-256", secondary_temp, strlen(secondary_temp), secondary_hash, sizeof(secondary_hash));
    cupsHashString(secondary_hash, sizeof(secondary_hash), secondary_temp, sizeof(secondary_temp));
  }
  else
  {
    // Leave an empty string for the resource portion
    secondary_temp[0] = '\0';
  }

  // Build the filename for the corresponding data...
  if (use_sysconfig)
  {
    if (secondary_temp[0])
      snprintf(buffer, bufsize, "%s/oauth/%s+%s.%s", cg->sysconfig, auth_temp, secondary_temp, otypes[otype]);
    else
      snprintf(buffer, bufsize, "%s/oauth/%s.%s", cg->sysconfig, auth_temp, otypes[otype]);
  }
  else if (secondary_temp[0])
  {
    snprintf(buffer, bufsize, "%s/oauth/%s+%s.%s", cg->userconfig, auth_temp, secondary_temp, otypes[otype]);
  }
  else
  {
    snprintf(buffer, bufsize, "%s/oauth/%s.%s", cg->userconfig, auth_temp, otypes[otype]);
  }

  DEBUG_printf("4oauth_make_path: Returning \"%s\".", buffer);

  return (buffer);
}


//
// 'oauth_make_software_id()' - Make the software_id UUID.
//
// The CUPS OAuth software_id is a format 8 (custom) UUID as defined in RFC 9562
// (replaces RFC 4122).  A certain amount of the UUID is "vanity" (RFC 8010 and
// RFC 8011 define the core IPP standard) with "CUPS" and "OAuth" in the UUID
// bytes as well, but this will be as unique as a regular random UUID will be.
//
// (Has the advantage of being easily identified, too...)
//
// For CUPS 3.0.x:
//
//   43555053-0300-8010-8011-4F4175746820
//

static char *				// O - UUID string
oauth_make_software_id(char   *buffer,	// I - UUID buffer
                       size_t bufsize)	// I - Size of UUID buffer
{
  unsigned char	uuid[16];		// UUID bytes


  uuid[ 0] = 'C';			// "CUPS"
  uuid[ 1] = 'U';
  uuid[ 2] = 'P';
  uuid[ 3] = 'S';
  uuid[ 4] = LIBCUPS_VERSION_MAJOR;	// CUPS major.minor
  uuid[ 5] = LIBCUPS_VERSION_MINOR;
  uuid[ 6] = 0x80;			// Custom UUID format 8
  uuid[ 7] = 0x10;			// "8010" for RFC 8010
  uuid[ 8] = 0x80;			// Variant 8
  uuid[ 9] = 0x11;			// "8011" for RFC 8011
  uuid[10] = 'O';			// "OAuth"
  uuid[11] = 'A';
  uuid[12] = 'u';
  uuid[13] = 't';
  uuid[14] = 'h';
  uuid[15] = 0x20;			// 2.0

  snprintf(buffer, bufsize, "%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X", uuid[0], uuid[1], uuid[2], uuid[3], uuid[4], uuid[5], uuid[6], uuid[7], uuid[8], uuid[9], uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]);

  return (buffer);
}


//
// 'oauth_metadata_contains()' - Determine whether a metadata parameter contains the specified value.
//

static bool				// O - `true` if present, `false` otherwise
oauth_metadata_contains(
    cups_json_t *metadata,		// I - Authorization server metadata
    const char  *parameter,		// I - Metadata parameter
    const char  *value)			// I - Parameter value
{
  size_t	i,			// Looping var
		count;			// Number of values
  cups_json_t	*values,		// Parameter values
		*current;		// Current value


  DEBUG_printf("3oauth_metadata_contains(metadata=%p, parameter=\"%s\", value=\"%s\")", (void *)metadata, parameter, value);

  if ((values = cupsJSONFind(metadata, parameter)) == NULL)
  {
    DEBUG_puts("4oauth_metadata: Returning false.");
    return (false);
  }

  for (i = 0, count = cupsJSONGetCount(values); i < count; i ++)
  {
    current = cupsJSONGetChild(values, i);

    if (cupsJSONGetType(current) == CUPS_JTYPE_STRING && !strcmp(value, cupsJSONGetString(current)))
      return (true);
  }

  return (false);
}


//
// 'oauth_save_value()' - Save a value string to the OAuth store.
//

static void
oauth_save_value(
    const char    *auth_uri,		// I - Authorization Server URI
    const char    *secondary_uri,	// I - Resource or redirect URI
    _cups_otype_t otype,		// I - Type (_CUPS_OTYPE_xxx)
    const char    *value)		// I - Value string or `NULL` to remove
{
  char	filename[1024];			// Filename
  int	fd;				// File descriptor
  static const int modes[_CUPS_OTYPE_MAX] =
  {					// File permissions for the various types
    0600,				// Access token
    0644,				// Client ID
    0644,				// Client secret
    0600,				// Client code_verifier
    0600,				// (User) ID token
    0644,				// Server key store
    0644,				// Server metadata
    0600,				// Client nonce
    0600,				// Redirect URI used
    0600				// Refresh token
  };


  DEBUG_printf("3oauth_save_value(auth_uri=\"%s\", secondary_uri=\"%s\", otype=%s, value=\"%s\")", auth_uri, secondary_uri, cups_otypes[otype], value);

  // Try making the filename...
  if (!oauth_make_path(filename, sizeof(filename), auth_uri, secondary_uri, otype, /*use_sysconfig*/false))
    return;

  if (value)
  {
    // Create the file...
    if ((fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW, modes[otype])) >= 0)
    {
      // Write the value and close...
      write(fd, value, strlen(value));
      close(fd);
    }
  }
  else
  {
    // Remove the file...
    unlink(filename);
  }
}


//
// 'oauth_set_error()' - Set the OAuth error message from a JSON response.
//

static bool				// O - `true` if there was an error, `false` otherwise
oauth_set_error(cups_json_t   *json,	// I - JSON response
                size_t        num_form,	// I - Number of form variables
                cups_option_t *form)	// I - Form variables
{
  const char	*error,			// error value
		*error_desc;		// error_description value


  if (json)
  {
    error      = cupsJSONGetString(cupsJSONFind(json, "error"));
    error_desc = cupsJSONGetString(cupsJSONFind(json, "error_description"));
  }
  else
  {
    error      = cupsGetOption("error", num_form, form);
    error_desc = cupsGetOption("error_description", num_form, form);
  }

  if (error && strcmp(error, "authorization_pending") && strcmp(error, "slow_down"))
  {
    if (error_desc)
    {
      char	message[1024];		// Message string

      snprintf(message, sizeof(message), "%s: %s", error, error_desc);
      _cupsSetError(IPP_STATUS_ERROR_CUPS_OAUTH, message, false);
    }
    else
    {
      _cupsSetError(IPP_STATUS_ERROR_CUPS_OAUTH, error, false);
    }

    return (true);
  }

  return (false);
}
