Authenticating with AWS Cognito from PHP¶
AWS Cognito is a service that handles user creation and authentication for your application, allowing you to focus on the key aspects of your application.
Use case¶
Many workloads are still running on PHP, and converting those workloads to cloud-native platforms may not happen overnight (or at all). Every so often we hear about new data breaches, where usernames and passwords are breached, and it would appear that developers are not very good at protecting credentials.
This is where Cognito comes in. By using this service from AWS, you'd be able to offload the authentication of your application onto Cognito, and let it handle the authentication of your application.
How it works¶
For this example, we are using the OATH2 feature of Cognito. OATH2 is a well-known protocol, so I won't delve too deep into how OATH2 works. For Cognito, let's walk through how this works.
Setting up Cognito¶
Start by creating a user pool, which is the collection of users. When creating the user pool, you need to decide how user accounts will be created. You can configure the parameters to suit your specific requirements.
For this example, we are going to focus on improved security. Where possible, we will ensure a high level of security integration between Cognito and your application.
A few key parameters that need to be configured :
- Provide a user pool name
- Select the "Use the Cognito Hosted UI" option
- Provide a domain name
- Provide an app client name
- Select "Generate a client secret"
- Provide a callback URL.
You can leave the remaining options as is. For this example, there is no need to configure MFA, or any special password policies. You're welcome to tweak these options as you need it in your application, it is not required for the example.
Once it is created, you will need the following information. Collect this from the AWS Cognito console.
- The full domain name
- The Client ID
- The client secret
- The redirect URI
A word about the "domain name"¶
Cognito allows you to use a domain name from AWS, or you could use your own domain name. For a production system, I would strongly recommend that you use your own domain name. Create a sub-domain in Route 53, and point that to Cognito. Refer to the AWS documentation on how to configure the custom domain.
Oath2 flow¶
Create the state¶
The state
field is a feature to prevent CSRF (Cross-site Request Forgery). We define this by simply generating a random string and storing it in the $_SESSION
variable.
if(!isset($_SESSION['state'])) {
$_SESSION['state'] = sha1(time().mt_rand());
}
Login¶
The browser starts by opening the Hosted UI page for the login endpoint.
print "<a href=\"$cognito_domain/login?" . http_build_query([
'client_id' => $client_id,
'redirect_uri' => $redirect_uri,
'response_type' => "code",
'state' => $_SESSION['state']
]). "\">Login</a>";
Validate the state¶
Validate that the state
we got is the same as what we provided Cognito in the beginning.
if(isset($_GET['code'])) {
if($_SESSION['state'] != $_GET['state']) {
print "access denied";
}
...
}
Issue the token¶
Once Cognito has performed the authentication, it returns a code
. This code is returned to the redirect_uri
. On receiving the code, we query /oath2/token.
Note that we also use the $client_id
and $client_secret
variables encoded as base64 as part of the authorization header.
$ch = curl_init();
// Get the token
$code = $_GET['code'];
curl_setopt_array($ch, [
CURLOPT_URL => "$cognito_domain/oauth2/token?" . http_build_query([
'grant_type' => "authorization_code",
'client_id' => $client_id,
'code' => $code,
'redirect_uri' => $redirect_uri
]),
CURLOPT_POSTFIELDS => $params,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Content-type: application/x-www-form-urlencoded",
"Authorization: Basic " . base64_encode("$client_id:$client_secret")
]
]);
$response = json_decode(curl_exec($ch),true);
Lookup the user information¶
Utilizing the oath2/userInfo endpoint, and using the token from the previous step, we can query the user information.
// Get the user info
curl_setopt_array($ch, [
CURLOPT_URL => "$cognito_domain/oauth2/userInfo",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Accept: application/json",
"Authorization: Bearer " . $response['access_token']
],
CURLOPT_POST => false
]);
$user = json_decode(curl_exec($ch),true);
User email address¶
Provided everything works as expected, we can now query the $user
variable, and retrieve the email address that was authenticated.
if(isset($user['email'])) {
print "You have been logged on!";
$_SESSION['emailaddress'] = $user['email'];
}
Caveats¶
- The Hosted UI only supports the signup, login, and password reset functions. Anything else (like updating the passwords, or updating attributes) is up to you - you have to build those interfaces yourself.
- You could store additional attributes within Cognito, however, I would recommend you store it in your own database. While you could use Cognito, when your application requirse additional parameters (like date of birth, address, etc), you may be better off using your database instead.
- The script in this example is only focussing on authentication. Session state is considered within the application.
Sample script¶
You can use this script to demonstrate how the authentication process works within PHP. Use the URL of the script as the redirect_uri
on Cognito.
<?php
session_start();
$cognito_domain = "https://<domain>.auth.ap-southeast-2.amazoncognito.com";
$client_id = "<client id>";
$client_secret = "<client secret>"; # DON'T HARD CODE THIS IN YOUR APP!!
$redirect_uri = "https://<redirect uri>";
if(!isset($_SESSION['state'])) {
$_SESSION['state'] = sha1(time().mt_rand());
}
if(isset($_SESSION['emailaddress'])) {
if(isset($_GET['logout'])) {
session_destroy();
session_start();
print "You have been logged out<br>\n";
} else {
print "You are logged on as " . $_SESSION['emailaddress'] . "<br>";;
print "<a href=\"?logout=true\">Logout</a>";
}
} else {
if(isset($_GET['code'])) {
if($_SESSION['state'] != $_GET['state']) {
print "access denied";
} else {
$ch = curl_init();
// Get the token
$code = $_GET['code'];
curl_setopt_array($ch, [
CURLOPT_URL => "$cognito_domain/oauth2/token?" . http_build_query([
'grant_type' => "authorization_code",
'client_id' => $client_id,
'code' => $code,
'redirect_uri' => $redirect_uri
]),
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Content-type: application/x-www-form-urlencoded",
"Authorization: Basic " . base64_encode("$client_id:$client_secret")
]
]);
$response = json_decode(curl_exec($ch),true);
// Get the user info
curl_setopt_array($ch, [
CURLOPT_URL => "$cognito_domain/oauth2/userInfo",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Accept: application/json",
"Authorization: Bearer " . $response['access_token']
],
CURLOPT_POST => false
]);
$user = json_decode(curl_exec($ch),true);
if(isset($user['email'])) {
print "You have been logged on!";
$_SESSION['emailaddress'] = $user['email'];
}
}
} else {
print "Not logged on - ";
print "<a href=\"$cognito_domain/login?" . http_build_query([
'client_id' => $client_id,
'redirect_uri' => $redirect_uri,
'response_type' => "code",
'state' => $_SESSION['state']
]). "\">Login</a>";
}
}
function json_curl($url,$headers,$params) {
$defaults = [
CURLOPT_URL => $url,
CURLOPT_POSTFIELDS => $params,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Content-type" => "application/x-www-form-urlencoded"
]
];
$ch = curl_init();
curl_setopt_array($ch, $defaults);
curl_setopt_array($ch, $headers);
return json_decode(curl_exec($ch),true);
}
?>