pie-frost / client-auth
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7
- paragonie/certainty: ^2
- paragonie/paserk: ^2
- paragonie/paseto: ^3
- psr/http-message: ^1
- psr/http-server-handler: ^1
Requires (Dev)
- phpunit/phpunit: ^9
- vimeo/psalm: ^4
This package is auto-updated.
Last update: 2024-11-04 11:42:48 UTC
README
Client-side library for authenticating with the Bifrost Authentication Server.
Installation
Use Composer.
composer require pie-frost/client-auth
Configuration
The minimum configuration for the Authentication Client is as follows:
- The authentication server's public key
- Also, URL, but the hard-coded default is correct for our instance
- Your server's signing keypair (should be loaded as a PASERK sealing key)
- Your server's domain name
<?php declare(strict_types=1); // Recommended! /* Namespace imports */ use PIEFrost\ClientAuth\AuthServer; use PIEFrost\ClientAuth\Config; use ParagonIE\Paserk\Operations\Key\SealingSecretKey; use ParagonIE\Paseto\Keys\AsymmetricPublicKey; use ParagonIE\Paseto\Protocol\Version4; /* Get from Auth server; auto-discovery coming soon */ $serverPublicKey = new AsymmetricPublicKey( '', new Version4() ); /* The defaults are fine. */ $authServer = new AuthServer($serverPublicKey); /* Generate this once, then persist it. * * The corresponding Public Key needs to be added to the * authentication server. */ $mySecretKey = SealingSecretKey::generate(new Version4()); $myPublicKey = $mySecretKey->getPublicKey(); // To get a copy of this key in the format expected by the Auth Server: // echo $myPublicKey->encode(); $config = (new Config()) ->withAuthServer($authServer) ->withDomain('my-custom-service.foo.bar') ->withSecretKey($mySecretKey);
There are additional configuration options available, of course.
Usage
Once you have a configuration object loaded, the client can be loaded and used.
<?php declare(strict_types=1); // Recommended! /* Namespace imports */ use PIEFrost\ClientAuth\AuthServer; use PIEFrost\ClientAuth\Client; use PIEFrost\ClientAuth\Config; use ParagonIE\Paserk\Operations\Key\SealingSecretKey; use ParagonIE\Paseto\Keys\AsymmetricPublicKey; use ParagonIE\Paseto\Protocol\Version4; /** @var Config $config */ $client = Client::fromConfig($config);
Create Auth Request Token
To begin the authentication request workflow, you must have registered at least one Redirect URL with the authentication server.
The steps you will be performing are as follows:
- Generate (and persist, preferably in a PHP session) a 256-bit secret "challenge". The primary purpose of the challenge is to prevent replay and confused deputy attacks.
- Create a request token.
- Redirect the user to the auth server, making sure to specify the return URL and token (step 2).
<?php declare(strict_types=1); use PIEFrost\ClientAuth\Client; use ParagonIE\ConstantTime\Base64UrlSafe; /** * @var Client $client */ $redirectURL = 'https://example.com/bifrost-callback'; // Step 1. $_SESSION['challenges']['bifrost-auth'] = Base64UrlSafe::encodeUnpadded(random_bytes(32)); // Step 2. $token = $client->createAuthRequestToken( $_SESSION['challenges']['bifrost-auth'], $redirectURL ); // Step 3: header('Location: ' . $client->getAuthServer()->getAuthUrl([ 'challenge' => $_SESSION['challenges']['bifrost-auth'], 'url' => $redirectURL, 'paseto' => $token ])); exit;
Once your user is at the Authentication Server, they'll do the necessary steps to authenticate and then return to the callback URL.
Processing the Auth Server Response
<?php declare(strict_types=1); // Recommended! use PIEFrost\ClientAuth\Client; /** * @var Client $client */ if (!isset($_GET['paseto']) || !isset($_SESSION['challenges']['bifrost-auth'])) { // Invalid state, redirect user and terminate execution header('Location: /'); exit; } $userInfo = $client->processAuthResponse( $_GET['paseto'], $_SESSION['challenges']['bifrost-auth'] ); var_dump($userInfo);
Upon success, the var_dump()
will return the following information:
- Username for the authenticated user.
- Unique ID for the authenticated user.
- The domain name for the given user.
What you actually do with this information is up to you.
What Is Actually Happening?
The PIE-Frost project has an Authentication Server that implements an opinionated single sign-on protocol.
The workflow looks like this:
- Client generates a random challenge, then signs a
v4.public
PASETO that covers the challenge and the callback URL. - The user is redirected to the authentication server, with the PASETO from step 1.
(You can think of this initial token as a hall pass from your application.) - The authentication server validates the PASETO.
- If the user is already logged into the server, they move onto step 4.
- Otherwise, they're expected to sign in to the authentication server, which only permits hardware keys (WebAuthn).
- (There are server-side validation steps involved, but those aren't super important for clients to understand.)
- The server generates its response token.
- The server encrypts the user's information and challenge into a
v4.local
PASETO, using a random one-time key. - This random one-time key is then encrypted with your application's public key, using PASERK
k4.seal
. - Both of the above elements are bundled together and signed by the server into a
v4.public
PASETO. This gets provided to the suer.
- The server encrypts the user's information and challenge into a
- The user is redirected to your callback URL, with the token from step 4.
- The server response is verified and deserialized.
- The outer
v4.public
PASETO's signature is verified. - The one-time key is decrypted using your application's secret key.
- The inner
v4.local
PASETO is decrypted and verified.- The
challenge
claim is compared with the one generated in step 1. - The
org
claim is compared to the domain configured.
- The
- The outer
After step 6, you have a cryptographically authenticated data structure containing the user information provided by the Authentication Server.
Questions and Answers
Why Not Just Use SAML, OAuth, or OpenID Connect?
We wanted to completely avoid the complexity of XML, X.509, ASN.1, and DER/BER encoding. Additionally, we wanted to avoid using JWT.
This left us without any options, so we decided to build a minimalistic, opinionated authentication flow.
Design decisions made:
- The only digital signature algorithm supported in this workflow is Ed25519 (including WebAuthn).
- We constrained the token formats to
v4.public
(Ed25519) andv4.local
(XChaCha20 + BLAKE2b-MAC) PASETO. - For the asymmetric encryption (for sending user information from the authentication server to the application
server), we also permit one
k4.seal
PASERK (ephemeral-static X25519 + XChaCha20 + BLAKE2b-MAC). This prevents a malicious user from learning any useful information about their user account (i.e. unique ID). - We placed the challenge inside the
k4.seal
-encrypted PASERK to ensure the encryption was being respected in order for the challenge to be verified client-side. - All algorithm implementations are provided by libsodium.
- There is no runtime negotiation of any algorithm choices.
- The X25519 keys are derived from the Ed25519 keys (through birational equivalence), so only one keypair needs to be managed for each party.