Skip to content

Instantly share code, notes, and snippets.

@mjameswh
Created April 15, 2019 19:40
Show Gist options
  • Save mjameswh/e3ed9420b98cdb6b5c1e76940af789f6 to your computer and use it in GitHub Desktop.
Save mjameswh/e3ed9420b98cdb6b5c1e76940af789f6 to your computer and use it in GitHub Desktop.
Use an existing PHP application with login as an OAuth2 Authorization Server
<?php
// ...
// Verify that provided username and password are valid
if ( checkLogin($_POST['username'], $_POST['password']) ) {
+ // If a target URL has been set, then redirect the user to that URL.
+ if (isset($_SESSION['login_success_target'])) {
+ header('Location: ' . $_SESSION['login_success_target']);
+ unset($_SESSION['login_success_target']);
+
+ } else {
// Redirect the user to the default application page
header('Location: application.php');
+ }
} else {
// Deal with login error...
}
<?php
require("shared.inc.php");
switch (true) { // Dummy switch to allow interrupting processing without using exit()
case true: // because PHP FPM doesn't like exit().
session_start();
$args = [];
if (isset($_GET["action"]) && ($_GET["action"] === "callback")) {
// "callback" was called but was not expected... Simply fail.
if (!isset($_SESSION['oauth2_callback_args']))
http_response_code(403);
$args = $_SESSION['oauth2_callback_args'];
unset($_SESSION['oauth2_callback_args']);
} else {
// response_type must be "code"
$args['response_type'] = $_GET["response_type"];
if ($args['response_type'] !== "code") {
http_response_code(400);
break;
}
// client_id must match one of predetermined names, and redirect_uri
// must start with a legitimate prefix for the specified client_id
$args['client_id'] = $_GET["client_id"];
$args['redirect_uri'] = $_GET["redirect_uri"];
if (($args['client_id'] === "shelfpublication") &&
(strncmp($args['redirect_uri'], "https://openresty.shelfpublication.com/", 39) === 0)) {
// Good
} else {
http_response_code(403);
break;
}
// We ignore "scope" for now; this parameter would allow the client to indicate what
// privileges are being requested.
$args['scope'] = $_GET["scope"];
// state is an opaque token; we simply copy it
$args['state'] = $_GET["state"];
}
if (isset($_SESSION['user_info']['id'])) {
// The user is authentified; generate an authorization code and return the user back
$authcode_token_object = [
'token_type' => 'authcode',
'user_id' => $_SESSION['user_info']['id'],
'expiration' => time() + (60 * 2), // 2 mins; authcode are expected to be exchanged almost immediately
'client_id' => $args['client_id']
];
$code = encrypt(json_encode($authcode_token_object));
header("Location: ${args['redirect_uri']}?code=${code}&state=${args['state']}");
break;
} else {
// The user is not yet authentified; forward him to the login page
$_SESSION['login_success_target'] = "/oauth2/authorize.php?action=callback";
$_SESSION['oauth2_callback_args'] = $args;
header("Location: /index.php");
break;
}
}
<?php
require("shared.inc.php");
switch (true) { // Dummy switch to allow interrupting processing without using exit()
case true: // because PHP FPM doesn't like exit().
// Extract bearer token
$auth_header = $_SERVER['HTTP_AUTHORIZATION'];
if (!preg_match('/Bearer\s(\S+)/', $auth_header, $matches)) {
error_log("oauth2-profile: no bearer token specified through HTTP authorization header");
http_response_code(403);
break;
}
$bearer_token = $matches[1];
// Decode and validate access token
$access_token_object = json_decode(decrypt($bearer_token));
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("oauth2-profile: bearer token could not be decrypted to a valid JSON document");
http_response_code(403);
break;
}
// Make sure the token is valid for this context
if ( ($access_token_object->token_type !== 'access')
|| ($access_token_object->expiration < time())) {
error_log("oauth2-profile: access token is not valid: ");
error_log(" token_type = " . $authcode_token_object->token_type);
error_log(" client_id = " . $authcode_token_object->client_id);
error_log(" expiration = " . $authcode_token_object->expiration);
error_log(" current time = " . time());
http_response_code(403);
break;
}
// Fetch user's profile from database
$user_id = $refresh_token_object->user_id;
$user_object = fetch_user_object($user_id);
if ($user_object === null) {
error_log("oauth2-profile: failed to retrieve user profile");
http_response_code(403);
break;
}
// Output the profile response
header("Content-Type: application/json");
header("Cache-Control: no-store");
header("Pragma: no-cache");
print('{' . "\n");
print(' "email": "' . $user_object['email'] . '",' . "\n");
print(' "lang": "' . $user_object['lang'] . '"' . "\n");
print('}' . "\n");
}
function fetch_user_object($user_id) {
// FIXME: fetch user object from database
return [
"email" => "user@server.com",
"lang" => "fr"
];
}
<?php
// Configuration
define('CRYPT_METHOD', "aes-128-cbc");
define('CRYPT_SECRET', "HRbFmgHTuZsZQtup"); // Random; must be 16 chars for AES 128; 32 chars for AES 256...
function encrypt($text) {
$ivlen = openssl_cipher_iv_length(CRYPT_METHOD);
$iv = openssl_random_pseudo_bytes($ivlen);
$cipher = openssl_encrypt($text, CRYPT_METHOD, CRYPT_SECRET, OPENSSL_RAW_DATA, $iv);
return base64url_encode($iv . $cipher);
}
function decrypt($cipher) {
$decoded = base64url_decode($cipher);
$ivlen = openssl_cipher_iv_length(CRYPT_METHOD);
$iv = substr($decoded, 0, $ivlen);
return openssl_decrypt(substr($decoded, $ivlen), CRYPT_METHOD, CRYPT_SECRET, OPENSSL_RAW_DATA, $iv);
}
// URL Safe variants of base64 encode/decode.
// From https://www.php.net/manual/fr/function.base64-encode.php#121767
function base64url_encode($data) {
return rtrim( strtr( base64_encode( $data ), '+/', '-_'), '=');
}
function base64url_decode($data) {
return base64_decode( strtr( $data, '-_', '+/') . str_repeat('=', 3 - ( 3 + strlen( $data )) % 4 ));
}
<?php
require("shared.inc.php");
switch (true) { // Dummy switch to allow interrupting processing without using exit()
case true: // because PHP FPM doesn't like exit().
// client_id must match one of predetermined names, and client_secret
// must match the presestablished secret for the specified client_id
$client_id = $_POST["client_id"];
$client_secret = $_POST["client_secret"];
if (($client_id === "shelfpublication") && ($client_secret === "secretsecret")) {
// Good
} else {
error_log("oauth2-token: invalid client_id or client_secret");
http_response_code(403);
break;
}
// check grant_type and associated parameters
$user_id = "";
$assign_refresh_token = true;
$grant_type = $_POST["grant_type"];
if ($grant_type === "authorization_code") {
// code is the code we generated earlier. It should match our formated syntax.
$authcode_token_object = json_decode(decrypt($_POST["code"]));
// If the token could not be decrypted to a valid JSON document, then it was probably junk
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("oauth2-token: authorization code could not be decrypted to a valid JSON document");
http_response_code(403);
break;
}
// Make sure the token is valid for this context
if ( ($authcode_token_object->token_type !== 'authcode')
|| ($authcode_token_object->client_id !== $client_id)
|| ($authcode_token_object->expiration < time())) {
error_log("oauth2-token: authorization code is not valid: ");
error_log(" token_type = " . $authcode_token_object->token_type);
error_log(" client_id = " . $authcode_token_object->client_id);
error_log(" expiration = " . $authcode_token_object->expiration);
error_log(" current time = " . time());
http_response_code(403);
break;
}
$user_id = $authcode_token_object->user_id;
error_log("oauth2-token: allocating access token based on authorization code");
} else if ($grant_type === "refresh_token") {
$refresh_token_object = json_decode(decrypt($_POST["refresh_token"]));
// If the token could not be decrypted to a valid JSON document, then it was probably junk
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("oauth2-token: refresh token could not be decrypted to a valid JSON document");
http_response_code(403);
break;
}
// Make sure the token is valid for this context
if ( ($refresh_token_object->token_type !== 'refresh')
|| ($refresh_token_object->client_id !== $client_id)
|| ($refresh_token_object->expiration < time())) {
error_log("oauth2-token: refresh token is not valid: ");
error_log(" token_type = " . $refresh_token_object->token_type);
error_log(" client_id = " . $refresh_token_object->client_id);
error_log(" expiration = " . $refresh_token_object->expiration);
error_log(" current time = " . time());
http_response_code(403);
break;
}
$user_id = $refresh_token_object->user_id;
$assign_refresh_token = false;
if (!user_exists_and_is_enabled($user_id)) {
error_log("oauth2-token: could not reauthorize based on refresh token because user no longer exists");
http_response_code(403);
break;
}
error_log("oauth2-token: allocating access token based on refresh token");
} else {
error_log("oauth2-token: unsupported grant type: " . $grant_type);
http_response_code(400);
break;
}
// Create an access token
$access_token_object = [
'token_type' => 'access',
'user_id' => $user_id,
'expiration' => time() + (60 * 30), // 30 mins
'client_id' => $client_id
];
$access_token = encrypt(json_encode($access_token_object));
// Create a refresh token, if appropriate
$refresh_token = "";
if ($assign_refresh_token) {
$refresh_token_object = [
'token_type' => 'refresh',
'user_id' => $user_id,
'expiration' => time() + (60 * 60 * 24 * 7), // 7 days
'client_id' => $client_id
];
$refresh_token = encrypt(json_encode($refresh_token_object));
}
// Output the access token response
header("Content-Type: application/json");
header("Cache-Control: no-store");
header("Pragma: no-cache");
print('{' . "\n");
print(' "access_token": "' . $access_token . '",' . "\n");
print(' "token_type": "bearer",' . "\n");
print(' "expires_in": 1800,' . "\n"); // 1800 sec == 30 min
if ($assign_refresh_token) {
print(' "refresh_token": "' . $refresh_token . '",' . "\n");
}
print(' "scope": "profile"' . "\n");
print('}' . "\n");
}
function user_exists_and_is_enabled($user_id) {
// FIXME: assert that user is still authorized to login
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment