FIDO2 in Oracle APEX
There are a couple of APEX projects that I have written down but never got around to implementing. For most of them, I procrastinate because I tell myself: “Oracle will surely implement this in the next release anyway.” Usually, that is not the case, but releases used to come frequently enough to keep me from ever starting these projects. However, we have not seen a new APEX release for more than a year now, so I finally took the chance to start at least one of them: integrating FIDO2 tokens directly into APEX. You can find the repo on GitHub. In this blog post, I want to talk about my motivation and how the implementation works.
What is FIDO2
I don’t want to bore you with the details, but I am a bit of a security nerd, so I get really excited about things like FIDO2. It is an initiative to make multi-factor authentication easier and less cumbersome. You can even replace passwords entirely if you do it right. FIDO2 consists of different building blocks, such as CTAP and WebAuthn, but I said I don’t want to bore you ;)
The key takeaway is that you can have MFA that is immune to phishing. Traditionally, MFA uses a code sent via SMS or a TOTP generated by an app that the user has to enter. The problem with this is that an attacker could clone the website and trick you into entering the MFA code. They could then use it to start a new session on your behalf. FIDO2 eliminates this attack vector by only allowing authentication for the correct website. If you registered your FIDO2 security key for cloudflare.com, it can only be used on cloudflare.com and will fail to work on cl0udflare.com or cloudflare.spam. Another benefit is ease of use. You can just insert a small USB device (or even use NFC) and tap it once for each authentication. No need to type MFA codes.
I already mentioned the downside: you need a physical FIDO2 security key (and I would recommend at least 2 for failover). You can also use software passkeys nowadays, but they are theoretically clonable.
There is a lot of content on how FIDO2 or WebAuthn works, and it would take too much time to explain every detail here. So, if you are interested, visit Wikipedia or Yubico.
Workflow
The way FIDO2 works is that you first start an enrollment process, which registers the security token with the user account. This includes a unique ID that can be used by the server to request a specific credential later on and a public key to verify future signatures. To request authentication, the server sends a challenge and valid credential IDs to the browser. The browser then relays this to any security key connected to the device, and the security key creates a signature for the challenge using its internal private key. This signature can then be validated by the server to ensure that the security key is indeed in possession of the private key and signed the correct challenge.
Implementation
I decided to have a central webauthn package that handles registration and verification. Registered credentials are stored in fido_credentials and are always tied to a user. For now, I decided to skip verification of newly registered credentials entirely. In theory, you should verify that the security key did in fact register with the challenge you provided. This requires a CBOR parser, which I will probably implement in the future (with the power of AI…). Verification is, of course, performed when a user tries to authenticate with a credential.
Sending a challenge to an already registered security key works with the navigator.credentials.get API. The request looks like this:
Where P1_WEBAUTHN_CHALLENGE is a page item that contains a challenge I generated on the server with webauthn.generate_challenge_base64(). This challenge is inspired by Frederik Tolf’s thoughts on providing a challenge with an expiration date without keeping track of various challenges on the server. It starts with the initialization vector (IV) and then appends AES-XTS-encrypted JSON containing the server timestamp and a monotonically increasing challenge counter. The allowed credentials are a list of registered credential IDs for the current user. Any one of them will be accepted.
The security key response looks like this:
Most interestingly, clientDataJSON contains the following information in Base64URL encoding:
Our challenge can be found in challenge and should contain the IV in the first 16 bytes if it has not been tampered with. You can also see that the origin of the web server has been included in the client data. This way, the server can verify that the signature was calculated for the correct server. The security key calculated the signature over response.authenticatorData and the SHA-256 hash of response.clientDataJSON. So, we can verify it with the corresponding public key from fido_credentials like this:
dbms_crypto.verify(
src => response.authenticatorData || dbms_crypto.hash(response.clientDataJSON, dbms_crypto.HASH_SH256)
,sign => response.signature
,pub_key => correct_public_key
,pubkey_alg => dbms_crypto.KEY_TYPE_ECDSA
,sign_alg => dbms_crypto.SIGN_SHA256_ECDSA
);
This will return a boolean, and we are already good to go. Note that SHA-256 ECDSA is only one possible way of generating the signature. Different security keys will prefer other algorithms, and even more will be supported in the future. So you should also save the algorithm associated with the credential ID in fido_credentials to be future-proof (I decided to postpone that).
Proof-of-concept
You can find an APEX integration of WebAuthn in the repo. It does not use FIDO2 for authentication, but only provides a way to enroll and verify credentials. You need to run the install.sql script to use the app. The app itself will only work on localhost or over HTTPS. navigator.credentials requires this secure context.
I decided to implement this with dynamic actions on button presses that trigger navigator.credentials.create or navigator.credentials.get and send the response to the server with an Ajax callback. For example, the callback for verification is a really simple call to webauthn.verify:
declare
l_json varchar2(9000) := apex_application.g_x01;
l_result boolean;
begin
l_result := webauthn.verify(l_json);
if l_result then
htp.p('{"status": "success"}');
else
htp.p('{"status": "failure"}');
end if;
exception
when others then
console.error;
htp.p('{"status": "error", "message": "' || sqlerrm || '"}');
end;
Next steps
I outlined some next steps in my README.md. An obvious next step would be to create an APEX plugin that can create and verify credentials without the need to create several processes and items. Also, as I said, you should really save the algorithm associated with the public key and allow for different algorithms in the verification step. I will keep you updated once any of this happens.