Initial commit

This commit is contained in:
Mark Wane 2024-07-15 20:16:00 +01:00
commit 7812d56d02
Signed by: mark
GPG key ID: 406607E3C6A78C73
5 changed files with 247 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
config.php

1
.htaccess Normal file
View file

@ -0,0 +1 @@
PerlHeaderParserHandler Lemonldap::NG::Handler::ApacheMP2

33
form.php Normal file
View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>IndieAuth</title>
<link rel="stylesheet" type="text/css" href="/home.css" />
</head>
<body>
<h1>IndieAuth</h1>
<?php if ( ! is_null( $error ) ) : ?>
<h2 class="centre">Error</h2>
<p class="centre"><?php echo $error; ?></p>
<?php else : ?>
<h2 class="centre">Authenticate</h2>
<form method="POST" action="" class="centre">
<p>You are about to login with client <pre><?php echo htmlspecialchars($client_id); ?></pre></p>
<?php if ( strlen($scope) > 0) : ?>
<p>With the following scopes</p>
<fieldset>
<legend>Scopes</legend>
<?php foreach (explode(' ', $scope) as $n => $checkbox) : ?>
<div>
<input id="scope_<?php echo $n; ?>" type="checkbox" name="scopes[]" value="<?php echo htmlspecialchars($checkbox); ?>" checked />
<label for="scope_<?php echo $n; ?>"><?php echo $checkbox; ?></label>
</div>
<?php endforeach; ?>
</fieldset>
<?php endif; ?>
<p>After login you will be redirected to <pre><?php echo htmlspecialchars($redirect_uri); ?></pre></p>
<input type="hidden" name="_csrf" value="<?php echo $csrf_code; ?>" />
<input class="submit" type="submit" name="submit" value="Submit" />
</form>
<?php endif ?>
</body>

80
functions.php Normal file
View file

@ -0,0 +1,80 @@
<?php
function create_signed_code(
$key,
$message,
$ttl = 31536000,
$appended_data = ""
) {
$expires = time() + $ttl;
$body = $message . $expires . $appended_data;
$signature = hash_hmac("sha256", $body, $key);
return dechex($expires) .
":" .
$signature .
":" .
base64_url_encode($appended_data);
}
function verify_signed_code($key, $message, $code) {
$code_parts = explode(":", $code, 3);
if (count($code_parts) !== 3) {
return false;
}
$expires = hexdec($code_parts[0]);
if (time() > $expires) {
return false;
}
$body = $message . $expires . base64_url_decode($code_parts[2]);
$signature = hash_hmac("sha256", $body, $key);
return hash_equals($signature, $code_parts[1]);
}
function filter_input_regexp($type, $variable, $regexp, $flags = null) {
$options = [
"options" => ["regexp" => $regexp],
];
if ($flags !== null) {
$options["flags"] = $flags;
}
return filter_input($type, $variable, FILTER_VALIDATE_REGEXP, $options);
}
function get_q_value($mime, $accept) {
$fulltype = preg_replace('@^([^/]+\/).+$@', '$1*', $mime);
$regex = implode("", [
"/(?<=^|,)\s*(\*\/\*|",
preg_quote($fulltype, "/"),
"|",
preg_quote($mime, "/"),
')\s*(?:[^,]*?;\s*q\s*=\s*([0-9.]+))?\s*(?:,|$)/',
]);
$out = preg_match_all($regex, $accept, $matches);
$types = array_combine($matches[1], $matches[2]);
if (array_key_exists($mime, $types)) {
$q = $types[$mime];
} elseif (array_key_exists($fulltype, $types)) {
$q = $types[$fulltype];
} elseif (array_key_exists("*/*", $types)) {
$q = $types["*/*"];
} else {
return 0;
}
return $q === "" ? 1 : floatval($q);
}
function base64_url_encode($string) {
$string = base64_encode($string);
$string = rtrim($string, "=");
$string = strtr($string, "+/", "-_");
return $string;
}
function base64_url_decode($string) {
$string = strtr($string, "-_", "+/");
$padding = strlen($string) % 4;
if ($padding !== 0) {
$string .= str_repeat("=", 4 - $padding);
}
$string = base64_decode($string);
return $string;
}

132
index.php Normal file
View file

@ -0,0 +1,132 @@
<?php
/* Single user IndieAuth endpoint using server envvars from SSO
* Based on https://github.com/inklings-io/selfauth
*/
require_once 'functions.php';
require_once 'config.php';
$error = null;
// Verification of codes.
$code = filter_input_regexp( INPUT_POST, 'code', '@^[0-9a-f]+:[0-9a-f]{64}:@' );
if ( ! is_null($code) ){
$redirect_uri = filter_input( INPUT_POST, 'redirect_uri', FILTER_VALIDATE_URL );
$client_id = filter_input( INPUT_POST, 'client_id', FILTER_VALIDATE_URL );
if ( ! (
is_string($code) &&
is_string($redirect_uri) &&
is_string($client_id) &&
verify_signed_code( APP_KEY, USER_URL . $redirect_uri . $client_id, $code )
) ) {
$error = 'Invalid code';
http_response_code(400);
include 'form.php';
die();
}
$response = array('me' => USER_URL);
$code_parts = explode(':', $code, 3);
$accept_header = $_SERVER['HTTP_ACCEPT'] ?: '*/*';
if ( '' !== $code_parts[2] ) {
$response['scope'] = base64_url_decode($code_parts[2]);
}
$json = get_q_value('application/json', $accept_header);
$form = get_q_value('application/x-www-form-urlencoded', $accept_header);
if ( 0 === $json && 0 === $form ){
$error = 'Client does not accept JSON or Form encoded responses';
http_response_code(406);
include 'form.php';
die();
} elseif ( $json >= $form ){
header('Content-Type: application/json');
exit( json_encode($response) );
} else {
header('Content-Type: application/x-www-form-urlencoded');
exit( http_build_query($response) );
}
}
// No code submitted,
// Check login
if ( is_null($_SERVER["REMOTE_USER"]) ) {
$error = 'Not logged in. Login on the <a href="https://auth.cool110.xyz/">SSO portal</a>';
http_response_code(403);
include 'form.php';
die();
} elseif ( USER_NAME !== $_SERVER["REMOTE_USER"] ){
$error = 'This system is for ' . USER_NAME . ' only.';
http_response_code(403);
include 'form.php';
die();
}
// Filter client data
$me = filter_input( INPUT_GET, 'me', FILTER_VALIDATE_URL );
$client_id = filter_input( INPUT_GET, 'client_id', FILTER_VALIDATE_URL );
$redirect_uri = filter_input( INPUT_GET, 'redirect_uri', FILTER_VALIDATE_URL );
$state = filter_input_regexp( INPUT_GET, 'state', '@^[\x20-\x7E]*$@' );
$response_type = filter_input_regexp( INPUT_GET, 'response_type', '@^(id|code)?$@' );
$scope = filter_input_regexp( INPUT_GET, 'scope', '@^([\x21\x23-\x5B\x5D-\x7E]+( [\x21\x23-\x5B\x5D-\x7E]+)*)?$@' );
if ( ! is_string($client_id) ) {
$error .= 'Invalid Client ID<br />';
}
if ( ! is_string($redirect_uri) ){
$error .= 'Invalid redirect URI<br />';
}
if ( false === $state ) {
$error .= 'Invalid state<br />';
}
if ( false === $response_type ) {
$error .= 'Invalid response type<br />';
}
if ( $error ){
http_response_code(400);
include 'form.php';
die();
}
$csrf_code = filter_input( INPUT_POST, '_csrf', FILTER_UNSAFE_RAW );
// If form submitted
if ( ! is_null($csrf_code) ) {
if ( !verify_signed_code( APP_KEY, $client_id . $redirect_uri . $state, $csrf_code ) ){
$error = 'Invalid CSFR code';
http_response_code(400);
include 'form.php';
die();
}
$scope = filter_input_regexp( INPUT_POST, 'scopes', '@^[\x21\x23-\x5B\x5D-\x7E]+$@', FILTER_REQUIRE_ARRAY );
if ( ! is_null($scope) ) {
if ( false === $scope || in_array( false, $scope, true ) ) {
$error = 'Provided scopes contain illegal characters';
}
$scope = implode( ' ', $scope );
}
$code = create_signed_code( APP_KEY, USER_URL . $redirect_uri . $client_id, 5 * 60, $scope );
$final_redir = $redirect_uri;
if ( strpos($redirect_uri, '?') === false ) {
$final_redir .= '?';
} else {
$final_redir .= '&';
}
$parameters = array(
'code' => $code,
'me' => USER_URL
);
if ( ! is_null($state) ) {
$parameters['state'] = $state;
}
$final_redir .= http_build_query($parameters);
header('Location: ' . $final_redir, true, 302);
} else {
$csrf_code = create_signed_code(APP_KEY, $client_id . $redirect_uri . $state, 2 * 60);
include 'form.php';
}