Security in the web comes in several different forms. If you're engaged in using any kind of web-based API with REST protocol, chances are you would have to deal with authentication protocols such as OAuth. In such cases, a PKCE (Proof Key Certification Exchange) may be required.
Some further reading
here.
Suffice to say, you'll need to generate a Code Verifier and use the Code Verifier to generate a Code Challenge. We will use JavaScript for the first part and PHP for the second. We
could use PHP for both, but it's better for our purposes to use JavaScript.
What is a Code Verifier?
It's a string that has length between 43 to 128, and only with the characters "-", "_", ".", "~", a to z (and uppercase equivalents) and numbers 0 to 9. As mentioned earlier, we could easily use PHP to generate this code with just one line, but then the purpose of this web tutorial would be largely lost. I want to bring you through the logical process of creating such a string,
So without further ado, here's some bare-bones HTML in a PHP file. Note that there won't be much styling; this code isn't meant to be pretty.
<!DOCTYPE html>
<html>
<head>
<title>PKCE Generator</title>
<style>
</style>
<script>
</script>
</head>
<body>
</body>
</html>
We start with a fieldset tag in the HTML, and a legend. And a form. There's no
action attribute specified for the form, because it's just going to call itself.
<!DOCTYPE html>
<html>
<head>
<title>PKCE Generator</title>
<style>
</style>
<script>
</script>
</head>
<body>
<fieldset>
<legend>PKCE Generator</legend>
<form method="POST">
</form>
</fieldset>
</body>
</html>
In here, I am going to outline the steps needed for the PKCE. Take note of the buttons. The first button is not meant to submit the form, so we have to explicitly specify its type as "button". For the second button, we leave that specification out because it
is going to submit the form.
<!DOCTYPE html>
<html>
<head>
<title>PKCE Generator</title>
<style>
</style>
<script>
</script>
</head>
<body>
<fieldset>
<legend>PKCE Generator</legend>
<form method="POST">
1. Generate a Code Verifier. <button type="button">Generate</button><br />
<br />
<br />
2. Generate Code Challenge. <button>Generate</button><br />
</form>
</fieldset>
</body>
</html>
Then we insert two input tags, ids
txtVerifier and
txtChallenge, respectively. Also make sure they have
name attributes as well, because these will be needed when submitting the form. For
txtVerifier, the
maxlength attribute has been set to 128 just in case the user wants to enter a verifier manually. Both of these input fields have been styled using the CSS class
inputlength, which basically sets the width to 100 em.
<!DOCTYPE html>
<html>
<head>
<title>PKCE Generator</title>
<style>
.inputlength
{
width: 100em;
}
</style>
<script>
</script>
</head>
<body>
<fieldset>
<legend>PKCE Generator</legend>
<form method="POST">
1. Generate a Code Verifier. <button type="button">Generate</button><br />
<input id="txtVerifier" name="txtVerifier" maxlength="128" class="inputlength" value="" />
<br />
<br />
2. Generate Code Challenge. <button>Generate</button><br />
<input class="inputlength" id="txtChallenge" value="" />
</form>
</fieldset>
</body>
</html>
Now, we add some PHP, declaring
verifier and
challenge as empty strings. Then we make sure that the
value attribute of the input tags have these strings in them.
<!DOCTYPE html>
<html>
<head>
<title>PKCE Generator</title>
<style>
.inputlength
{
width: 100em;
}
</style>
<script>
</script>
</head>
<body>
<?php
$verifier="";
$challenge = "";
?>
<fieldset>
<legend>PKCE Generator</legend>
<form method="POST">
1. Generate a Code Verifier. <button type="button">Generate</button><br />
<input id="txtVerifier" name="txtVerifier" maxlength="128" class="inputlength" value="<?php echo $verifier;?>" />
<br />
<br />
2. Generate Code Challenge. <button>Generate</button><br />
<input class="inputlength" id="txtChallenge" value="<?php echo $challenge;?>" />
</form>
</fieldset>
</body>
</html>
Got all that? Cool. Visually, this is all the change we're going to make here.
Let's start writing JavaScript. We begin with a
pkcegen object to properly encapsulate things.
<script>
let pkcegen = {
};
</script>
In here, we want a nice useful random number generator that will give us a value between
min and
max. Nothing much to see here, but if you want more elaboration, check out this
previous blogpost!
<script>
let pkcegen = {
randomNumber: function(min, max)
{
return Math.floor(Math.random() * (max - min)) + min;
}
};
</script>
Now, let's create a method to generate a verifier! It will be called
genVerifier(). Yes, yes, not very creative. Blow me.
<script>
let pkcegen = {
genVerifier: function()
{
},
randomNumber: function(min, max)
{
return Math.floor(Math.random() * (max - min)) + min;
}
};
</script>
We declare a variable,
strLength, which will be the length of the string we are abut to generate. Using the
randomNumber() function, we make sure that it's a number between 43 and 128.
genVerifier: function()
{
var strLength = this.randomNumber(43, 128);
},
Then we declare
verifier as an empty string. At the end of this method,
verifier will be populated in the textbox
txtVerifier.
genVerifier: function()
{
var strLength = this.randomNumber(43, 128);
var verifier = "";
document.getElementById("txtVerifier").value = verifier;
},
Now, we create a
For loop to use
strLength's value to create the string.
genVerifier: function()
{
var strLength = this.randomNumber(43, 128);
var verifier = "";
for(let i = 0; i < strLength; i++)
{
}
document.getElementById("txtVerifier").value = verifier;
},
We declare
currentChar within the loop, then set it to the value returned by calling the
genVerifierValidChar() method. And then we concatenate
verifier with the value of
currentChar.
genVerifier: function()
{
var strLength = this.randomNumber(43, 128);
var verifier = "";
for(let i = 0; i < strLength; i++)
{
var currentChar;
currentChar = this.genVerifierValidChar();
verifier = verifier + currentChar;
}
document.getElementById("txtVerifier").value = verifier;
},
Let's create
genVerifierValidChar() next!
genVerifier: function()
{
var strLength = this.randomNumber(43, 128);
var verifier = "";
for(let i = 0; i < strLength; i++)
{
var currentChar;
currentChar = this.genVerifierValidChar();
verifier = verifier + currentChar;
}
document.getElementById("txtVerifier").value = verifier;
},
genVerifierValidChar: function()
{
},
randomNumber: function(min, max)
{
return Math.floor(Math.random() * (max - min)) + min;
}
We first declare
index, and set it to a random number between 0 and 5.
genVerifierValidChar: function()
{
var index = this.randomNumber(0, 5);
},
Then we use a
switch statement on
index.
genVerifierValidChar: function()
{
var index = this.randomNumber(0, 5);
switch(index)
{
}
},
So for the first four cases, we return different values. The
break statements are redundant since there are
return statements preceding them, but I like to leave them there. For the time being, we just return an empty string for the default case.
switch(index)
{
case 0: return "-"; break;
case 1: return "_"; break;
case 2: return "~"; break;
case 3: return "."; break;
default:
return "";
break;
}
Right now, all we have are these characters.
Now let's comment off the empty string. Instead, as a default, we declare
letterIndex as a number between 0 to 3.
switch(index)
{
case 0: return "-"; break;
case 1: return "_"; break;
case 2: return "~"; break;
case 3: return "."; break;
default:
//return "";
var letterIndex = this.randomNumber(0, 3);
break;
}
Then we run another
switch statement on
letterIndex.
switch(index)
{
case 0: return "-"; break;
case 1: return "_"; break;
case 2: return "~"; break;
case 3: return "."; break;
default:
//return "";
var letterIndex = this.randomNumber(0, 3);
switch(letterIndex)
{
}
break;
}
For the first case, we use the
fromCharCode() method of the
String class, passing in a random number from 48 to 57 to give us numbers from the ASCII character set.
switch(index)
{
case 0: return "-"; break;
case 1: return "_"; break;
case 2: return "~"; break;
case 3: return "."; break;
default:
//return "";
var letterIndex = this.randomNumber(0, 3);
switch(letterIndex)
{
case 0: return String.fromCharCode(this.randomNumber(48, 57)); break;
}
break;
}
The second case gives us lowercase letters.
switch(index)
{
case 0: return "-"; break;
case 1: return "_"; break;
case 2: return "~"; break;
case 3: return "."; break;
default:
//return "";
var letterIndex = this.randomNumber(0, 3);
switch(letterIndex)
{
case 0: return String.fromCharCode(this.randomNumber(48, 57)); break;
case 1: return String.fromCharCode(this.randomNumber(65, 90)); break;
}
break;
}
And by default, we return uppercase letters.
switch(index)
{
case 0: return "-"; break;
case 1: return "_"; break;
case 2: return "~"; break;
case 3: return "."; break;
default:
//return "";
var letterIndex = this.randomNumber(0, 3);
switch(letterIndex)
{
case 0: return String.fromCharCode(this.randomNumber(48, 57)); break;
case 1: return String.fromCharCode(this.randomNumber(65, 90)); break;
default: return String.fromCharCode(this.randomNumber(97, 122)); break;
}
break;
}
Click the first Generate button. You'll get a value!
But if there's a little too many special characters for your taste, hey, just adjust this value.
genVerifierValidChar: function()
{
var index = this.randomNumber(0, 50);
switch(index)
{
case 0: return "-"; break;
case 1: return "_"; break;
case 2: return "~"; break;
case 3: return "."; break;
default:
//return "";
var letterIndex = this.randomNumber(0, 3);
switch(letterIndex)
{
case 0: return String.fromCharCode(this.randomNumber(48, 57)); break;
case 1: return String.fromCharCode(this.randomNumber(65, 90)); break;
default: return String.fromCharCode(this.randomNumber(97, 122)); break;
}
break;
}
},
That's it for the Code Verifier...
What is a Code Challenge?
The Code Challenge is the Base64-encoded SHA-256 hash of the Code Verifier. Sounds complicated? Not really, it just means that you have to put the Code Verifier that you can now generate, through a few processes.
So let's write some PHP. If there's
POST data, set
verifier to the value sent by
txtVerifier. You know, the value inside the textbox you just filled by clicking the first Generate button?
<?php
$verifier="";
$challenge = "";
if (sizeof($_POST) > 0)
{
$verifier = $_POST["txtVerifier"];
}
?>
Now use the
hash() function to get the SHA-256 hash of verifier. Assign the value to the variable
hash.
<?php
$verifier="";
$challenge = "";
if (sizeof($_POST) > 0)
{
$verifier = $_POST["txtVerifier"];
$hash = hash("sha256", $verifier);
}
?>
We now use the
base64_encode() function on hash. However, due to the
limitations of PHP, we can't
just use it on
hash. We need to first put
hash through the
pack() function. We pass "H*" as the first argument. "H" is for Hex strings and the "*" just means you want to work on the entire string.
<?php
$verifier="";
$challenge = "";
if (sizeof($_POST) > 0)
{
$verifier = $_POST["txtVerifier"];
$hash = hash("sha256", $verifier);
$challenge = base64_encode(pack("H*", $hash));
}
?>
So far this is what you should get if you generate.
Your output should not contain slashes, the plus sign or the trailing equal sign. So here, we'll use the
strtr() function to replaces plusses and slashes with the hyphen and underscore characters respectively, and the
rtrim() function to remove the trailing equal signs.
<?php
$verifier="";
$challenge = "";
if (sizeof($_POST) > 0)
{
$verifier = $_POST["txtVerifier"];
$hash = hash("sha256", $verifier);
$challenge = base64_encode(pack("H*", $hash));
$challenge = strtr($challenge, "+/", "-_");
$challenge = rtrim($challenge, "=");
}
?>
There you have it.
Challenge accepted,
T___T