I'm working on implementing JWT verification in a client-side web-application that's using Webpack5. When a user is created on the backend running PHP, I create a public and private keypair for use in JWT like this and store them:
$keyPair = sodium_crypto_sign_keypair();
$privateKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
$publicKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
Then with firebase/php-jwt create the request like this:
$jwt = JWT::encode($payload, $privateKey, 'EdDSA');
However, using jose I get the following error when attempting to verify the JWT:
import * as jose from 'jose';
const { payload, protectedHeader } = await jose.jwtVerify(response.token, window.atob(response.key));
console.log(payload);
console.log(protectedHeader);
TypeError: Key must be of type CryptoKey.
I'm not understanding how it wants the key as both the base64 string and base64 presentation of the publicKey is met with the same TypeError in node. What am I doing wrong here?
Edwards-Curve Digital Signature Algorithm (EdDSA in JOSE) is not supported by WebCryptography API, as such you won't succeed with any library that uses native browser crypto. Sadly.
There's a proposal for Ed25519, Ed448, X25519, and X448 to be added in WebCrypto but it'll take a while before adoption and then implementation. When that happens jose will support this action. That being said you'll need to pass a CryptoKey. That's either imported via crypto.subtle.importKey directly, or via jose's import key functions. SubtleCrypto supports raw public key imports, jose supports PEM key imports in its wrappers only.
See Browser Support for the browser algorithm support matrix, as well as Algorithm Key Requirements for algorithm key requirements, and Type alias: KeyLike for how to obtain the right key representation.
As for the other (now deleted) suggestions, they do not take into account the algorithm you use, suggesting to use Uint8Array instead of a CryptoKey is wrong, Uint8Array use is exclusive for symmetric algorithms. Likewise, using transpiled jsonwebtoken is only remotely reliable for HMAC based algorithms.
jose only uses the runtime's native crypto layer which is your best bet at conformance and secure implementation but when running cross-runtimes you need the consider the intersection of supported algorithms on the platform.
I'm not understanding how it wants the key as both the base64 string and base64 presentation of the publicKey is met with the same TypeError in node
It's supposed to be KeyLike (interface) - in Node that's KeyObject (see crypto documentation), in Web-like environments it's CryptoKey (see WebCryptoAPI).
Related
We have an implementation of openid connect which returns an encoded id token, this works well and has been working for a while. However we are attempting to connect to it using cognito in aws and after a bit of trial and error we have found that we are missing a .well-known/openid-configuration file.
This file is meant to contain information about the calls to the openid-connect server we have including the JWK keys.
I don't understand JWK keys, this means:
1. How to generate them
2. Once generated what to do with them?
3. Does the exising code we have for the openid-connect need to change and use the JWK keys?
4. Is there any way of validating an openid-connect configuration?
I have asked something similar a while back but with no avail but revisiting.
Thanks
Kevin
What Ive already tried:
Been here: https://mkjwk.org/ and clicked on 'New Key', which returns what I assume is a web key.. however don't know what i'm meant to do with it or what the other tabs on that mean.
Expected Results:
Expecting AWS Cognito to continue and allow it to connect to our open id connect implementation. Currently just get an error with regards to a missing well known configuration file.
The openid connect server you have generates ID token that are JWS.
These tokens are signed using a private key (EC, RSA or OKP).
The well-known/openid-configuration is should contain a JSON object that indicates what algorithms are used to sign the tokens and the url to get the public keys associated to the private keys used to sign the token.
For example, the Google Account server configuration indicates the public keys can be found at https://www.googleapis.com/oauth2/v3/certs (jwks_uri parameter).
At this url, you will find a list of keys (JWKSet) formatted in JWK (JSON Web Keys). These formats (JWK and JWKSet) are the standrad representations for keys and key sets used in a JWT context (see RFC7517).
How to generate them
Normally you already have at least one private key to sign your tokens so you don’t have to generate a new one.
Once generated what to do with them?
The private key you have should be a PEM (starting with ------ PRIVATE RSA KEY ----- or similar) or DER key.
You just have to convert this key into JWK.
I created a small PHAR application (PHP) that will help to convert your keys. This app is part of the Web Token Framework but can be installed as a standalone app. Make sure you have PHP 7.1, OpenSSL and JSON extensions installed.
When the app is installed, you can execute the following command:
jose key:convert:public $(jose key:load:key PATH_TO_YOUR_KEY)
This command will convert the private key into JWK and then convert it from private to public.
The result can be shared through the jwks_uri endpoint.
Does the exising code we have for the openid-connect need to change and use the JWK keys?
No. The way you build your tokens does not means you have to change anything.
Is there any way of validating an openid-connect configuration?
As far as I know, there is no tools to validate the configuration object. You have to refer to the OpenID Connect specification.
While Florent's answer the usage of public key, I like to point you to the specification that defines the contents of JWK and specific implementation details.
OpenID Connect discovery define the discovery document. There you find jwks_uri URL as part of the metadata, which points to JSON Web Key Set
3. OpenID Provider Metadata
wks_uri
REQUIRED. URL of the OP's JSON Web Key Set [JWK] document. This
contains the signing key(s) the RP uses to validate signatures from
the OP
Now this points to RFC7517 - JSON Web Key (JWK). You can find exact details of each field of the JSON payload there. Refer its samples and appendix section to see sample contents. And your implementation must follow specification requirements defined by this.
For coding, if you are using JAVA, I suggest you to use Nimbus JOSE+JWT library. Sample can be found from this link.
For example, if you already have X509Certificate loaded, then following is the minimalistic code to generate JWK. It is embedded into a Servlet [Full source].
import Common.CertificateLoader;
import Common.Exceptions.FrameworkUncheckedException;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.JWK;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.cert.X509Certificate;
#WebServlet(urlPatterns = "/openid-configuration/jwks_uri")
public class JWKDocument extends HttpServlet {
#Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// NOTE - LOAD YOUR CERTIFICATE HERE
final X509Certificate certificate = CertificateLoader.getCertificateLoader().getX509Certificate();
final JWK jwk;
try {
jwk = JWK.parse(certificate);
} catch (JOSEException e) {
throw new FrameworkUncheckedException("Error while loading to JWK", e);
}
resp.getOutputStream().print(jwk.toJSONString());
}
}
from laravel docs
Application Key The next thing you should do after installing Laravel
is set your application key to a random string. If you installed
Laravel via Composer or the Laravel installer, this key has already
been set for you by the php artisan key:generate command.
Typically, this string should be 32 characters long. The key can be
set in the .env environment file. If you have not renamed the
.env.example file to .env, you should do that now. If the application
key is not set, your user sessions and other encrypted data will not
be secure!
What I know about application key is: If the application key is not set, generally I do get an exception.
How do this random string help to secure the session?
What are the other uses of this application key?
If I use the same application key everywhere (like staging, production etc..) does it make the application less secure?
what are some best practices for this key
As we can see its used in EncryptionServiceProvider:
public function register()
{
$this->app->singleton('encrypter', function ($app) {
$config = $app->make('config')->get('app');
// If the key starts with "base64:", we will need to decode the key before handing
// it off to the encrypter. Keys may be base-64 encoded for presentation and we
// want to make sure to convert them back to the raw bytes before encrypting.
if (Str::startsWith($key = $this->key($config), 'base64:')) {
$key = base64_decode(substr($key, 7));
}
return new Encrypter($key, $config['cipher']);
});
}
So every component that uses encryption: session, encryption (user scope), csrf token benefit from the app_key.
Rest of the questions can be answered by "how encryption" (AES) works, just open up Encrypter.php, and confirm that Laravel uses AES under the hood and encodes the result to base64.
Further more we can see how its all done by using tinker:
➜ laravel git:(staging) ✗ art tinker
Psy Shell v0.8.17 (PHP 7.1.14 — cli) by Justin Hileman
>>> encrypt('Hello World!')
=> "eyJpdiI6ImgzK08zSDQyMUE1T1NMVThERjQzdEE9PSIsInZhbHVlIjoiYzlZTk1td0JJZGtrS2luMlo0QzdGcVpKdTEzTWsxeFB6ME5pT1NmaGlQaz0iLCJtYWMiOiI3YTAzY2IxZjBiM2IyNDZiYzljZGJjNTczYzA3MGRjN2U3ZmFkMTVmMWRhMjcwMTRlODk5YTg5ZmM2YjBjMGNlIn0="
Note: I used this key: base64:Qc25VgXJ8CEkp790nqF+eEocRk1o7Yp0lM1jWPUuocQ= to encrypt Hello World!
After decoding the result we get (you can try decode your own cookie with session):
{"iv":"h3+O3H421A5OSLU8DF43tA==","value":"c9YNMmwBIdkkKin2Z4C7FqZJu13Mk1xPz0NiOSfhiPk=","mac":"7a03cb1f0b3b246bc9cdbc573c070dc7e7fad15f1da27014e899a89fc6b0c0ce"}
to understand above json (iv, value, mac) you need to understand AES:
https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
Best practices for application key
do store it in .env file only
do not store it in app.php, in fact in any git tracked file
do not change it unless you really want to
invalidate sessions/cookies (user logout)
invalidate password reset tokens
invalidate signed urls
Obvious Note: Changing application key has no effect on hashed passwords since hashing algorithms do not require encryption keys.
Is Laravel hashing differently request and response cookies?
I am using main domain and subdomains, and have set up CORS and CSRF and if I exclude cookies from EncryptCookies class I see the same cookies in the response headers and request headers.
If I leave them to encrypt however, I am getting different "encryption strings", and don't know if that is acceptable behaviour?
EncryptCookies class is only listed under web part in Kernel.php
For Laravel Encryption:
Laravel's encrypter uses OpenSSL to provide AES-256 and AES-128
encryption. You are strongly encouraged to use Laravel's built-in
encryption facilities and not attempt to roll your own "home grown"
encryption algorithms. All of Laravel's encrypted values are signed
using a message authentication code (MAC) so that their underlying
value can not be modified once encrypted.
For each encryption, the value are encrypted with AES-256 / AES-128 with different initialization vector and signed with different MAC, even if you encrypt the same value, the payload, returned value of encrypt always different. For easier understanding, you can check this example:
$value = Crypt::encrypt('foo');
// eyJpdiI6ImVoNEVlVWpnYUdwZ1JHRlJWSGlTZEE9PSIsInZhbHVlIjoiVThpWjJNWVBqZnVsWjhLVWNDXC85VHc9PSIsIm1hYyI6IjFjMDRhOTM5ZThhOWRmYjk3Mzk0OWFmNTM3YWE1NDAzNzMxNWY5YTJmODMwNmQxZDE4NDllZGJkMjc1Y2I3ZmYifQ==
base64_decode($value);
// {"iv":"eh4EeUjgaGpgRGFRVHiSdA==","value":"U8iZ2MYPjfulZ8KUcC\/9Tw==","mac":"1c04a939e8a9dfb973949af537aa54037315f9a2f8306d1d1849edbd275cb7ff"}
The second attempt:
$value = Crypt::encrypt('foo');
// eyJpdiI6Ill5MmZleG5ycTBaZmQ5NnRDT3N3dVE9PSIsInZhbHVlIjoiTmgrRnlqajJjUk9qTk1qeHJLU21LUT09IiwibWFjIjoiNWEzZDRjZWMwMjg0ZDhlMjhlZWRiODg3ZWQ5MTcxN2I5N2JjY2ZmMzc0NTYyOTI5MThmOTk4YjAyZjM1YTRjMyJ9
base64_decode($value);
// {"iv":"Yy2fexnrq0Zfd96tCOswuQ==","value":"Nh+Fyjj2cROjNMjxrKSmKQ==","mac":"5a3d4cec0284d8e28eedb887ed91717b97bccff37456292918f998b02f35a4c3"}
for a customer we are migrating a Laravel application to a Ruby application. We have some data stored in the database that we would like to decrypt in the ruby world.
This is the laravel part that was used to encrypt the data: https://laravel.com/docs/5.0/encryption
Now when importing the data to ruby we need a counter part that can decrypt the data.
In the laravel console I was able to decrypt the data like this:
>>> Crypt::decrypt('eyJpdiI6ImZyek9ZTjJNSW5ZYlhSa2ZYUldVbEE9PSIsInZhbHVlIjoia20zMTRLWEpCdXM2K05DZDBHSlE5SDlcL2pYVXk5aE5RWWR3dHFQT1dGQzA9IiwibWFjIjoiZWZlNGE3NTRhMDDlNzk2MjhlYjI1Mzc1NGNiYmRjNDMwZjM1NzdiMzkyZTU4ZjA4ZDNkMGE0YjUyOTBjMDAzOCJA')
=> "123123123123"
I am no laravel expert, but in the app.php file a secret key was set. So I need to be able to pass it somehow to the decrypt function.
The goal is to have a ruby function that takes the laravel password, encryption key and returns the decrypted value.
def decrypt_laravel_crypt(value, encryption_key)
end
Thanks for the help!
You can reverse engineer Laravel's Encrypter::decrypt method using mostly standard libs. Laravel will serialize data before encrypting it unless told explicitly not to. This means attempting to unserialize the decrypted string prior to using it.
require 'base64'
require 'openssl'
require 'json'
require 'php_serialize'
def lara_decrypt(encryptedString, appKey)
data = JSON.parse(Base64::decode64(encryptedString))
decipher = OpenSSL::Cipher.new('aes-256-cbc')
decipher.decrypt
decipher.key = Base64::decode64(appKey)
decipher.iv = Base64::decode64(data['iv'])
decrypted = decipher.update(Base64::decode64(data['value'])) + decipher.final
begin
PHP.unserialize(decrypted)
rescue
decrypted
end
end
Also when you set the app key be sure to drop base64: from the beginning of it (if it's there).
I have followed the instructions here to obtain an access token for a web API.
https://msdn.microsoft.com/en-us/library/azure/dn645542.aspx
I have this working but the documentation is vague when it comes to figuring out how to validate the token in PHP.
You can use the access token that is returned in the response to authenticate to a protected resources, such as a web API. Typically, the token is presented to the web API in an HTTP request using the Bearer scheme, which is described in RFC 6750. This specification explains how to use bearer tokens in HTTP requests to access protected resources.
When the web API receives and validates the token, it gives the native client application access to the web API.
How do I validate the JWT in application? I have a PHP framework which is using PHP openssl_verify() function with the token, signiture, key and algorithm but I receive error of when I use the private key from Azure with the SHA256 algorithm:
openssl_verify(): supplied key param cannot be coerced into a public key
This leads me to believe that the key I am using in PHP to validate is not correct. At the moment, I am using the private key I generated for the Active Directory Application, which happens to also be the same value I am using for the "client_secret" parameter when hitting the oauth2/token url (any other value causes no token to be generated so this is probably correct).
The key is similar to (BUT IT NOT ACTUALLY):
cLDQWERTYUI12asdqwezxctlkjpoiAn7yhjeutl8jsP=
Where I beleive openssl needs to have a certificate... if so I can't seem to find where this certificate is in the Azure portal.
What am I missing here? What is the key I should be using with openssl_verify() to verify the JWT and where do I find it in Azure?
Thanks
--
UPDATE:
I have found the public keys here: https://login.windows.net/common/discovery/keys
However I still cannot use the X5C provided to verify the signature. How do you do this in PHP?
--
UPDATE 2:
I used a converted to create a .pem file for the public key using both the 'e' and 'n' parameters. This received a public key.
Now I get OPEN SSL errors when decrypting with it:
error:0906D06C:PEM routines:PEM_read_bio:no start line
Closing this question as I have moved on from the origional issue. Updated my question with comments showing how I have progressed.
Created a new question for the new specific issue: How do I verify a JSON Web Token using a Public RSA key?
--
Just in case it helps anyone else:
For further information on a solution to obtaining a public key from Microsoft in PHP I did the following:
$string_microsoftPublicKeyURL = 'https://login.windows.net/common/discovery/keys';
$array_publicKeysWithKIDasArrayKey = loadKeysFromAzure($string_microsoftPublicKeyURL);
function loadKeysFromAzure($string_microsoftPublicKeyURL) {
$array_keys = array();
$jsonString_microsoftPublicKeys = file_get_contents($string_microsoftPublicKeyURL);
$array_microsoftPublicKeys = json_decode($jsonString_microsoftPublicKeys, true);
foreach($array_microsoftPublicKeys['keys'] as $array_publicKey) {
$string_certText = "-----BEGIN CERTIFICATE-----\r\n".chunk_split($array_publicKey['x5c'][0],64)."-----END CERTIFICATE-----\r\n";
$array_keys[$array_publicKey['kid']] = getPublicKeyFromX5C($string_certText);
}
return $array_keys;
}
function getPublicKeyFromX5C($string_certText) {
$object_cert = openssl_x509_read($string_certText);
$object_pubkey = openssl_pkey_get_public($object_cert);
$array_publicKey = openssl_pkey_get_details($object_pubkey);
return $array_publicKey['key'];
}
Its better however to cache these to disk so your not loading these them every time, but this is just a simple example of how to do this.
Then, using the array of public keys, check the JWT header for the 'kid' value to find the correct public cert to verify against and use this in parameter 3 within openssl_verify(). I used the JWT class to deal with this for me.
Using this public key array created above and the JWT class should allow you to validate microsoft JWTs.
Link to JWT class from firebase: https://github.com/firebase/php-jwt
Call JWT::Decode with param 1 of your JWT, param 2 of this public key array and param three of an array of just 'RS256'.
JWT::decode($string_JSONWebToken, $array_publicKeysWithKIDasArrayKey, array('RS256'));
This will throw an exception if the JWT is invalid or return a decrypted JWT for you to use (and check the claims).
If you want to verify the jwt then go to jwt.io
This will allow you to paste the JWT and it will then verify the header, claims, and if you add the Public key or private key (depending how the server verifies the signature) it will also verify the signature of the JWT.