PHP Authentication
Brian Retterer
@bretterer
brian@brianretterer.com
Lessons Learned
Who is Brian Retterer?
Developer Advocate
at Okta
Self Proclaimed Whovian
Laravel / WordPress / PHP Hacker

http://charisteapot.tumblr.com/post/116544339467/tardis-charisteapot-doctor-who-fanart-this-is
DR 11 Forever!
The
Early
Days
The If Checks!!!
<?php
$errors = array();
if ( !isset($_REQUEST['email']) ) {
$errors[] = 'Email is not set';
if ( !isset($_REQIEST['password']) ) {
$errors[] = 'Password is not set';
if ( $_REQUEST['email'] == '' ) {
$errors[] = 'Email can not be empty';
if ( $_REQUEST['password'] == '') {
$errors[] = 'Password can not be empty';
if ( !empty($user = get_user_by_email( $_REQUEST['email']) ) ) {
$errors[] = 'User Not Found';
if ( $user['password'] !== md5($user['salt'] . $_REQUEST['salt']) ) {
$errors[] = 'Password Is Incorrect';
}
}
}
}
}
}Security by Obscurity
CREATE TABLE Users (
id INT NOT NULL AUTO_INCREMENT,
email VARCHAR(80) NOT NULL,
display_name VARCHAR(50) NOT NULL,
favorite_phrase CHAR(41) NOT NULL, //NOT PASSWORD
PRIMARY KEY (user_id),
UNIQUE INDEX (email)
);The Hashing
<?php
md5('myPassword');<?php
$salt = 'GlobalSaltSecretPhrase';
md5( md5($salt) . md5('MyPassword') );<?php
$salt = 'GlobalSaltSecretPhrase';
md5( md5($salt) . md5('MyPassword') . md5($userCreatedAt) );Hall
Of
Shame
Don't Use Admin

Too Much Info

Too Much Info

WHAT?!?!?!

Errrr.. Gross

Login &
Registration
Guidelines
Keep it simple

Keep it simple

Don't give away too much

Username vs Email

Remember
Me
Option
Amazon

Amazon

Amazon




Password
Hashing
MD5
MD5
- Designed in 1991
- 128-bit hash
- RFC 1321
- Cracked in 2012
- Mostly used now for checksum
- and bad password storage
MCRYPT
MCRYPT
- Introduced in PHP 4
- Complex to implement
- Requires extension to pull random bytes
- mcrypt_create_iv
- PHP 7 has random_bytes
- openssl_encrypt instead
BCRYPT
BCRYPT
- 1999
- Incorporates a salt
- Adaptive
- Default hash algorithm for OpenBSD Passwords
- Defines itself
BCRYPT
$2y$10$HFVEs5GKR/mGj1pj2MJWg.HQ5TQjCQ1.IRGtXvOP7ofZTA1EAI9NG
$2y$ - Corrected Version of Bcrypt
$2a$ - Potentially Buggy
$2x$ - Known buggy hash
BCRYPT
$2y$10$HFVEs5GKR/mGj1pj2MJWg.HQ5TQjCQ1.IRGtXvOP7ofZTA1EAI9NG
10$ - Cost
$timeTarget = 0.05; // 50 milliseconds
$cost = 8;
do {
$cost++;
$start = microtime(true);
password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
$end = microtime(true);
} while (($end - $start) < $timeTarget);
echo "Appropriate Cost Found: " . $cost . "\n";BCRYPT
$2y$10$HFVEs5GKR/mGj1pj2MJWg.HQ5TQjCQ1.IRGtXvOP7ofZTA1EAI9NG
HFVEs5GKR/mGj1pj2MJWg. - Salt
BCRYPT
$2y$10$HFVEs5GKR/mGj1pj2MJWg.HQ5TQjCQ1.IRGtXvOP7ofZTA1EAI9NG
HQ5TQjCQ1.IRGtXvOP7ofZTA1EAI9NG - Hashed Text
PASSWORD_HASH
PASSWORD_HASH
password_hash('password', PASSWORD_DEFAULT, ['cost' => 10]);
// $2y$10$HFVEs5GKR/mGj1pj2MJWg.HQ5TQjCQ1.IRGtXvOP7ofZTA1EAI9NG
password_hash('password', PASSWORD_BCRYPT, ['cost' => 10]);
// $2y$10$OYicKVXdu0a5RTus6HuJfuDCun8caWE/PD18SwdGKJQVbWH0s.zd6
// PRE PHP 7.0
password_hash('password', PASSWORD_BCRYPT, ['salt' => 'somerandom22charstrng.']);
//$2y$10$somerandom22charstrng.pO7bKuthd0JmLDOsNPqVL13XmCqEs9eUsing PASSWORD_BCRYPT limits password to 72 chars
Always 60 characters total
PASSWORD_VERIFY
PASSWORD_VERIFY
password_verify('password', '$2y$10$somerandom22charstrng.pO7bKuthd0JmLDOsNPqVL13XmCqEs9e');
// trueAll Items needed to hash password is provided.
$hashed = '$2y$10$somerandom22charstrng.pO7bKuthd0JmLDOsNPqVL13XmCqEs9e';
$salt = substr($hashed, 7, 22);
$newHashed = password_hash('password', PASSWORD_BCRYPT, ['salt' => $salt]);
$hashed == $newHashed
// truePASSWORD_NEEDS_REHASH
password_needs_rehash("$2y$10$somerandom22charstrng.pO7bKuthd0JmLDOsNPqVL13XmCqEs9e", PASSWORD_DEFAULT));
// false
password_needs_rehash("$2x$10$somerandom22charstrng.pO7bKuthd0JmLDOsNPqVL13XmCqEs9e", PASSWORD_DEFAULT));
// trueif(password_needs_rehash(...)) {
$newHash = password_hash(...);
storePassword($newHash);
}Social
Integration
Don't overdo it

Don't reinvent the wheel

http://github.com/laravel/socialite
OAuth
2.0
Info on Oauth 2.0
- Released 2012
- RFC 6749
- Widely used
- 4 main grant types
Implicit Grant
The implicit grant type is used to obtain access tokens (it does not support the issuance of refresh tokens) and is optimized for public clients known to operate a particular redirection URI. These clients are typically implemented in a browser using a scripting language such as JavaScript.

Client Credentials Grant
The client can request an access token using only its client credentials (or other supported means of authentication) when the client is requesting access to the protected resources under its control, or those of another resource owner that have been previously arranged with the authorization server

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentialsHTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"example_parameter":"example_value"
}Authorization Code Grant
The authorization code grant type is used to obtain both access tokens and refresh tokens and is optimized for confidential clients. Since this is a redirection-based flow

Resource Owner Password Grant
The resource owner password credentials grant type is suitable in cases where the resource owner has a trust relationship with the client, such as the device operating system or a highly privileged application.

PHP League
http://oauth2-client.thephpleague.com/
$provider = new \League\OAuth2\Client\Provider\GenericProvider([
'clientId' => 'demoapp', // The client ID assigned to you by the provider
'clientSecret' => 'demopass', // The client password assigned to you by the provider
'redirectUri' => 'http://example.com/your-redirect-url/',
'urlAuthorize' => 'http://brentertainment.com/oauth2/lockdin/authorize',
'urlAccessToken' => 'http://brentertainment.com/oauth2/lockdin/token',
'urlResourceOwnerDetails' => 'http://brentertainment.com/oauth2/lockdin/resource'
]);
//Get Authorization Url
$authorizationUrl = $provider->getAuthorizationUrl();
//Verify Code and get access token
$accessToken = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
]);Multi
Factor
Auth
MFA Types
SMS
TOTP
Others



JSON
Web
Tokens
About JWT's
- RFC 7519
- URL Safe
- Signable
JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImM2NTcxZjg2LWNjODktNGY2Ny1iOTEzLTVjN2ZlMTcxM2Y1OSIsImlhdCI6MTQ3OTM5MDQ5OSwiZXhwIjoxNDc5Mzk0MDk5fQ.Yuy7VeMNKqKwmSCTXHQ_DdOrg2v42afKgZzmg8ISIIE
Header
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
{
"typ": "JWT",
"alg": "HS256"
}
JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImM2NTcxZjg2LWNjODktNGY2Ny1iOTEzLTVjN2ZlMTcxM2Y1OSIsImlhdCI6MTQ3OTM5MDQ5OSwiZXhwIjoxNDc5Mzk0MDk5fQ.Yuy7VeMNKqKwmSCTXHQ_DdOrg2v42afKgZzmg8ISIIE
Signature
Yuy7VeMNKqKwmSCTXHQ_DdOrg2v42afKgZzmg8ISIIE
JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImM2NTcxZjg2LWNjODktNGY2Ny1iOTEzLTVjN2ZlMTcxM2Y1OSIsImlhdCI6MTQ3OTM5MDQ5OSwiZXhwIjoxNDc5Mzk0MDk5fQ.Yuy7VeMNKqKwmSCTXHQ_DdOrg2v42afKgZzmg8ISIIE
Claims
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImM2NTcxZjg2LWNjODktNGY2Ny1iOTEzLTVjN2ZlMTcxM2Y1OSIsImlhdCI6MTQ3OTM5MDQ5OSwiZXhwIjoxNDc5Mzk0MDk5fQ
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"jti": "c6571f86-cc89-4f67-b913-5c7fe1713f59",
"iat": 1479390499,
"exp": 1479394099
}
Claims
| "iss" (Issuer) | Who issues this claim? |
| "sub" (Subject) | Who is the subject? |
| "aud" (Audience) | Who is the target of JWT? |
| "exp" (Expiration Time) | Seconds since epoch |
| "nbf" (Not Before) | seconds since epoch |
| "iat" (Issued At) | Seconds since epoch |
| "jti" (JWT ID) | Unique id for JWT |
How to generate?
composer require firebase/php-jwt
JWT::encode([
'sub' => '1234567890',
'jti' => Ramsey\Uuid\Uuid::uuid4(),
'iat' => 1479390499,
'exp' => 1479394099,
'nbf' => 1479390499,
'admin' => true,
'favorite_colors' => ['orange', 'blue']
], 'secret', 'HS256');
How to decode?
composer require firebase/php-jwt
try{
JWT::decode(
'eyJ0e...zI1NiJ9.eyJzdWIi...k0MDk5fQ.Yuy7...8ISIIE',
'secret',
['HS256']
);
} catch( SignatureInvalidException $e ) {
// Signature did not work, can use but with caution
} catch( BeforeValidException $e ) {
// This token is not yet valid
} catch( ExpiredException $e ) {
// This token has expired
}
JSON Web Tokens
https://jsonwebtoken.io/
Cookies
Vs
Session
Storing in Local Storage
<script>
window.sessionStorage.accessToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImM2NTcxZjg2LWNjODktNGY2Ny1iOTEzLTVjN2ZlMTcxM2Y1OSIsImlhdCI6MTQ3OTM5MDQ5OSwiZXhwIjoxNDc5Mzk0MDk5fQ.Yuy7VeMNKqKwmSCTXHQ_DdOrg2v42afKgZzmg8ISIIE';
</script>
Flaws of Local Storage
- Accessible via script
- Insecure packages
- XSS Attacks
PHP Session
- Not secure!
- Session Id Hacking
- Not scalable
- Who wants to use super global? ($_SESSION)
- Don't forget session_start()
Cookie Storage
composer require symfony/http-foundation
$cookie = new Cookie(
'accessToken', // name
'eyJ...J9.ey...fQ.Yu...IE', // payload
1479397344, //expire time
'/', // path
'example.io', // domain
true, // secure only
true, // http only
false // send without url encoding
);
$response->headers->setCookie($cookie);Cookies
- Immune to XSS
- Can be served over HTTPS only
- No State required
- Vulnerable to CSRF
- Access from JS can be blocked

PHP Authentication
Brian Retterer
@bretterer
brian@brianretterer.com
Lessons Learned
Thank You!
Slides: https://goo.gl/VT9thw
PHP Authentication - Lessons Learned - KCDC
By Brian Retterer
PHP Authentication - Lessons Learned - KCDC
KCDC 2017
- 1,323