123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- <?php
- function error_page($header, $body, $http = '400 Bad Request')
- {
- $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0';
- header($protocol . ' ' . $http);
- $html = <<<HTML
- <!doctype html>
- <html>
- <head>
- <style>
- .error{
- width:100%;
- text-align:center;
- margin-top:10%;
- }
- </style>
- <title>Error: $header</title>
- </head>
- <body>
- <div class='error'>
- <h1>Error: $header</h1>
- <p>$body</p>
- </div>
- </body>
- </html>
- HTML;
- die($html);
- }
- $configfile= __DIR__ . '/config.php';
- if (file_exists($configfile)) {
- include_once $configfile;
- } else {
- error_page(
- 'Configuration Error',
- 'Endpoint not yet configured, visit <a href="setup.php">setup.php</a> for instructions on how to set it up.'
- );
- }
- // Enable string comparison in constant time.
- if (!function_exists('hash_equals')) {
- function hash_equals($known_string, $user_string)
- {
- $known_length = strlen($known_string);
- if ($known_length !== strlen($user_string)) {
- return false;
- }
- $match = 0;
- for ($i = 0; $i < $known_length; $i++) {
- $match |= (ord($known_string[$i]) ^ ord($user_string[$i]));
- }
- return $match === 0;
- }
- }
- // Signed codes always have an time-to-live, by default 1 year (31536000 seconds).
- 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 verify_password($pass)
- {
- $hash_user = trim(preg_replace('/^https?:\/\//', '', USER_URL), '/');
- $hash = md5($hash_user . $pass . APP_KEY);
- return hash_equals(USER_HASH, $hash);
- }
- function filter_input_regexp($type, $variable, $regexp, $flags = null)
- {
- $options = array(
- 'options' => array('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(
- '',
- array(
- '/(?<=^|,)\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);
- }
- // URL Safe Base64 per https://tools.ietf.org/html/rfc7515#appendix-C
- 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;
- }
- if ((!defined('APP_URL') || APP_URL == '')
- || (!defined('APP_KEY') || APP_KEY == '')
- || (!defined('USER_HASH') || USER_HASH == '')
- || (!defined('USER_URL') || USER_URL == '')
- ) {
- error_page(
- 'Configuration Error',
- 'Endpoint not configured correctly, visit <a href="setup.php">setup.php</a> for instructions on how to set it up.'
- );
- }
- // First handle verification of codes.
- $code = filter_input_regexp(INPUT_POST, 'code', '@^[0-9a-f]+:[0-9a-f]{64}:@');
- if ($code !== null) {
- $redirect_uri = filter_input(INPUT_POST, 'redirect_uri', FILTER_VALIDATE_URL);
- $client_id = filter_input(INPUT_POST, 'client_id', FILTER_VALIDATE_URL);
- // Exit if there are errors in the client supplied data.
- 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_page('Verification Failed', 'Given Code Was Invalid');
- }
- $response = array('me' => USER_URL);
- $code_parts = explode(':', $code, 3);
- if ($code_parts[2] !== '') {
- $response['scope'] = base64_url_decode($code_parts[2]);
- }
- // Accept header
- $accept_header = '*/*';
- if (isset($_SERVER['HTTP_ACCEPT']) && strlen($_SERVER['HTTP_ACCEPT']) > 0) {
- $accept_header = $_SERVER['HTTP_ACCEPT'];
- }
- // Find the q value for application/json.
- $json = get_q_value('application/json', $accept_header);
- // Find the q value for application/x-www-form-urlencoded.
- $form = get_q_value('application/x-www-form-urlencoded', $accept_header);
- // Respond in the correct way.
- if ($json === 0 && $form === 0) {
- error_page(
- 'No Accepted Response Types',
- 'The client accepts neither JSON nor Form encoded responses.',
- '406 Not Acceptable'
- );
- } 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));
- }
- }
- // If this is not verification, collect all the client supplied data. Exit on errors.
- $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)) { // client_id is either omitted or not a valid URL.
- error_page(
- 'Faulty Request',
- 'There was an error with the request. The "client_id" field is invalid.'
- );
- }
- if (!is_string($redirect_uri)) { // redirect_uri is either omitted or not a valid URL.
- error_page(
- 'Faulty Request',
- 'There was an error with the request. The "redirect_uri" field is invalid.'
- );
- }
- if ($state === false) { // state contains invalid characters.
- error_page(
- 'Faulty Request',
- 'There was an error with the request. The "state" field contains invalid data.'
- );
- }
- if ($response_type === false) { // response_type is given as something other than id or code.
- error_page(
- 'Faulty Request',
- 'There was an error with the request. The "response_type" field must be "code".'
- );
- }
- if ($scope === false) { // scope contains invalid characters.
- error_page(
- 'Faulty Request',
- 'There was an error with the request. The "scope" field contains invalid data.'
- );
- }
- if ($scope === '') { // scope is left empty.
- // Treat empty parameters as if omitted.
- $scope = null;
- }
- // If the user submitted a password, get ready to redirect back to the callback.
- $pass_input = filter_input(INPUT_POST, 'password', FILTER_UNSAFE_RAW);
- if ($pass_input !== null) {
- $csrf_code = filter_input(INPUT_POST, '_csrf', FILTER_UNSAFE_RAW);
- // Exit if the CSRF does not verify.
- if ($csrf_code === null || !verify_signed_code(APP_KEY, $client_id . $redirect_uri . $state, $csrf_code)) {
- error_page(
- 'Invalid CSF Code',
- 'Usually this means you took too long to log in. Please try again.'
- );
- }
- // Exit if the password does not verify.
- if (!verify_password($pass_input)) {
- // Optional logging for failed logins.
- //
- // Enabling this on shared hosting may not be a good idea if syslog
- // isn't private and accessible. Enable with caution.
- if (function_exists('syslog') && defined('SYSLOG_FAILURE') && SYSLOG_FAILURE === 'I understand') {
- syslog(LOG_CRIT, sprintf(
- 'IndieAuth: login failure from %s for %s',
- $_SERVER['REMOTE_ADDR'],
- $me
- ));
- }
- error_page('Login Failed', 'Invalid password.');
- }
- $scope = filter_input_regexp(INPUT_POST, 'scopes', '@^[\x21\x23-\x5B\x5D-\x7E]+$@', FILTER_REQUIRE_ARRAY);
- // Scopes are defined.
- if ($scope !== null) {
- // Exit if the scopes ended up with illegal characters or were not supplied as array.
- if ($scope === false || in_array(false, $scope, true)) {
- error_page('Invalid Scopes', 'The scopes provided contained illegal characters.');
- }
- // Turn scopes into a single string again.
- $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 ($state !== null) {
- $parameters['state'] = $state;
- }
- $final_redir .= http_build_query($parameters);
- // Optional logging for successful logins.
- //
- // Enabling this on shared hosting may not be a good idea if syslog
- // isn't private and accessible. Enable with caution.
- if (function_exists('syslog') && defined('SYSLOG_SUCCESS') && SYSLOG_SUCCESS === 'I understand') {
- syslog(LOG_INFO, sprintf(
- 'IndieAuth: login from %s for %s',
- $_SERVER['REMOTE_ADDR'],
- $me
- ));
- }
- // Redirect back.
- header('Location: ' . $final_redir, true, 302);
- exit();
- }
- // If neither password nor a code was submitted, we need to ask the user to authenticate.
- $csrf_code = create_signed_code(APP_KEY, $client_id . $redirect_uri . $state, 2 * 60);
- ?><!doctype html>
- <html>
- <head>
- <title>Login</title>
- <style>
- h1{text-align:center;margin-top:3%;}
- body {text-align:center;}
- fieldset, pre {width:400px; margin-left:auto; margin-right:auto;margin-bottom:50px; background-color:#FFC; min-height:1em;}
- fieldset {text-align:left;}
- .form-login{
- margin-left:auto;
- width:300px;
- margin-right:auto;
- text-align:center;
- margin-top:20px;
- border:solid 1px black;
- padding:20px;
- }
- .form-line{ margin:5px 0 0 0;}
- .submit{width:100%}
- .yellow{background-color:#FFC}
- </style>
- </head>
- <body>
- <form method="POST" action="">
- <h1>Authenticate</h1>
- <div>You are attempting to login with client <pre><?php echo htmlspecialchars($client_id); ?></pre></div>
- <?php if (strlen($scope) > 0) : ?>
- <div>It is requesting the following scopes, uncheck any you do not wish to grant:</div>
- <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; ?>
- <div>After login you will be redirected to <pre><?php echo htmlspecialchars($redirect_uri); ?></pre></div>
- <div class="form-login">
- <input type="hidden" name="_csrf" value="<?php echo $csrf_code; ?>" />
- <p class="form-line">
- Logging in as:<br />
- <span class="yellow"><?php echo htmlspecialchars(USER_URL); ?></span>
- </p>
- <div class="form-line">
- <label for="password">Password:</label><br />
- <input type="password" name="password" id="password" />
- </div>
- <div class="form-line">
- <input class="submit" type="submit" name="submit" value="Submit" />
- </div>
- </div>
- </form>
- </body>
- </html>
|