User Story

As a user I want to browse to an organization's web server and from it download a single-page application (SPA) that runs Javascript in the web browser. I want to click on a login button in the SPA and proceed through an OIDC authentication flow orchestrated by Javascript running as part of the SPA and where the OIDC client is a public client and does not possess a client secret. At the completion of the OIDC flow I want the SPA to receive a RFC 9068 compliant JSON web token (JWT) access token. I want the SPA to use the access token to authenticate me as a user to the Registry API as the SPA makes calls to the Registry API on my behalf.

I want the SPA to invoke a Registry API route and inspect the configuration for an enrollment flow to determine what information is required to sufficiently create a CO Person record. I want the SPA to collect the necessary information (name, email address, ...) and then invoke a Registry API route to create a CO Person record with the required information and leave the CO Person record in the appropriate state.

Approach: API JWT Authentication

The RegistryAuthComponent.php (app/src/Controller/Component/) controller component has the single function authenticateApiUser() where the current HTTP basic authentication is consumed. The proposal is to evolve that function so that it consults configuration state to determine which API Authentication plugins and in which order to use to attempt to authenticate the entity invoking the API call. The default for all new deployments and any upgraded deployments without additional configuration would be to
use a core and activated plugin that implements the current HTTP basic authentication method. An available plugin a deployer may configure will be able to consume the RFC 9068 compliant JWT token.

Additional Plugin Type

The current plugin types are detailed here. This design will add the row

PluginDescriptionAvailable SinceAdditional Requirements
apiauthAPI Auth Consumer PluginsTBDAPI Auth Consumer Plugins (PE)

ApiAuthConsumer

ApiAuthConsumer will be a Pluggable Model that understands Registry Plugins of the type apiauth.

api_auth_consumers table

The app/config/schema/schema.json file will be updated to add

"api_auth_consumers": {
  "columns": {
    "id": {},
    "co_id" : {},
    "description": {},
    "plugin": {},
    "status": {}
  }
}

Classes

class ApiAuthConsumersTable extends Table

class ApiAuthConsumer extends Entity

class ApiAuthConsumersController  extends StandardPluggableController

Plugins

HttpBasicAuth

plugin.json

{
  "types": {
    "apiauth": [
      "HttpBasicAuthConsumers"
    ]
  },
  "schema": {
    "tables": {
      "http_basic_auth_consumers": {
        "columns": {
          "id": {},
          "api_auth_consumer_id": {},
        },
        "indexes": {
        }
      }
    }
  }
}


Jwt

plugin.json

{
  "types": {
    "apiauth": [
      "JwtAuthConsumers"
    ]
  },
  "schema": {
    "tables": {
      "jwt_auth_consumers": {
        "columns": {
          "id": {},
          "api_auth_consumer_id": {},
          "jwks_uri": {},
          "jwks_uri_cache_ttl": {},
          "must_decrypt": {},
          "decryption_key": {},
          "decryption_algorithm": {},
          "allowed_issuer": {},
          "required_aud": {},
          "bearer_token_header": {},
          "identifier_claim": {}
        },
        "indexes": {
        }
      }
    }
  }
}

Requirements

RFC 9068 JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens

https://www.rfc-editor.org/rfc/rfc9068.html

See section 4 "Validating JWT Access Tokens" which includes "Resource servers receiving a JWT access token MUST validate it in the following manner":

  • The resource server MUST verify that the "typ" header value is "at+jwt" or "application/at+jwt" and reject tokens carrying any other value.
  •  If the JWT access token is encrypted, decrypt it using the keys and algorithms that the resource server specified during registration. If encryption was negotiated with the authorization server at registration time and the incoming JWT access token is not encrypted, the resource server SHOULD reject it.
  • The issuer identifier for the authorization server (which is typically obtained during discovery) MUST exactly match the value of the "iss" claim.
  • The resource server MUST validate that the "aud" claim contains a resource indicator value corresponding to an identifier the resource server expects for itself. The JWT access token MUST be rejected if "aud" does not contain a resource indicator of the current resource server as a valid audience.
  • The resource server MUST validate the signature of all incoming JWT access tokens according to [RFC7515] using the algorithm specified in the JWT "alg" Header Parameter. The resource server MUST reject any JWT in which the value of "alg" is "none". The resource server MUST use the keys provided by the authorization server.
  •  The current time MUST be before the time represented by the "exp" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew.

The PHP-JWT package validates the signature and checks the expiration. The other checks must be implemented by the plugin code.

This design includes the plugin downloading and caching the OP signing keys from a JWKS URL. Later evolutions of the plugin might also allow a deployer to  upload signing keys.

Approach: Authenticated User Onboarding via API

As a concrete use case, consider first a "self signup with approval" enrollment flow that is configured to collect

  • Official Name
  • Official Email Address
  • static COU (that is, when the enrollment flow is completed the CO Person record should have a CO Person Role with a specific COU configured as part of the enrollment flow configuration, with a static value for affiliation, e.g. "member")

Perhaps the enrollment flow configuration(s) can be "serialized" into a JSON schema object and returned to the  SPA?

Can the API help signal to the SPA that the user has already used a specific enrollment flow and therefore should not be able to go through it again?

Vendor Packages

PHP-JWT

https://github.com/firebase/php-jwt

"A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to RFC 7519."

BSD 3-Clause license.

GuzzleHttp

https://docs.guzzlephp.org/en/stable/

Provides PSR-7 compatible HTTP client and PSR-17 compatible HTTP request factory, used by the PHP-JWT code for downloading JWKS signing keys from the OP or OAuth2 server.

Note that the CakePHP 4.x HTTP client functionality does not currrently provide PSR-7 and PSR-17, but the CakePHP 5.x client does, so when PE is upgraded to CakePHP 5.x this package can be dropped.

MIT license.

Phpfastcache

https://www.phpfastcache.com/

Provides PSR-6 compatible cache item pool, used by the PHP-JWT code for caching the downloaded JWKS signing keys so they do not have to be fetched each time that a JWT is received and needs to be verified.

Note that it may be possible to use existing CakePHP 4.x or 5.x cache tools, but it is not immediately evident from the documentation, so more research is needed.

MIT license.


  • No labels