#!/usr/bin/php-cgi -dcgi.force_redirect=0
<?php
# encoding: utf-8
# api: cgi
# type: json
# category: auth
# title: IndieAuth token endpoint
# description: Turns an auth token into an access token (but not really)
# version: 0.2
# state: untested
# depends: php:sqlite
# doc: https://indieweb.org/obtaining-an-access-token
# config: -
#
# Counterpart to the `auth` cgi extension. This basically just
# upgrades the authorization code to an access token internally.
# The $2y$-hash token stays, the `fx_auth.type` becomes 'token'.
#
# Request: POST .../token
#    ?grant_type=authorization_code
#    &me=https://userwebid.example.org/
#    &code=$2y...
#    &redirect_uri=http://app.example.com/login/callback
#    &client_id=https://app.example.com
# Response:
#    {
#      "access_token": "$2y...",
#      "scope": "create ticket",
#      "me": "https://user.example.org/"
#    }
#
#

if ($_REQUEST["dbg"]) {
    error_reporting(E_ALL); ini_set("display_errors", 1);
}

#-- database (== fossil repo)
function db($sql="", $params=[]) {
    static $db;
    if (empty($db)) {
        $db = new PDO("sqlite:$_SERVER[FOSSIL_REPOSITORY]");
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
    }
    if ($params) {
        $stmt = $db->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }
    else {
        return $db->query($sql);
    }
}

#-- JSON/form-encoded output
function json_response($r) {
    if (stristr($_SERVER["HTTP_ACCEPT"], "/json")) {
        header("Content-Type: text/json");
        die(json_encode($r, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
    }
    elseif (stristr($_SERVER["HTTP_ACCEPT"], "/x-www-form")) {
        header("Content-Type: application/x-www-form-urlencoded");
        array_walk($r, function(&$v, $k) { $v = "$k=" . urlencode($v); });
        die(join("&", $r));
    }
    else {
        header("Content-Type: text/php-source");
        die(var_export($r, True));
    }
}


#-- utility functions
function trim_url($url) {
    return strtolower(preg_replace("~^https?://|/+$~", "", $url));
}
function base64_urlencode($raw) {
    return strtr(trim(base64_encode($raw), "="), "+/", "-_");
}

#-- load authorization properties by auth code
function get_token_by_code($code) {
    return db("SELECT * FROM fx_auth WHERE code=?", [$code]) ?: [[]];
}
function clean_expired_token() {
    db("DELETE FROM fx_auth WHERE expires < ?", [time()]);
}

#-- get code from Authorization: header (test different variations to circumvent fossil shortening the CGI env)
function bearer() {
    foreach (["HTTP_AUTHORIZATION", "HTTP_Authorization", "HTTP_COOKIE", "HTTP_AUTHORIZATION2"] as $h) {
        if (preg_match("/Bearer\s+(\S+)/i", $_SERVER[$h], $uu)) {
             return $uu[1];
        }
    }
}


#-- send ?code= verification response (basically what /auth already does)
function verify_code() {
    header("Content-Type: application/json");

    # input
    $grant_type = $_REQUEST["grant_type"];
    $code = $_REQUEST["code"];
    $client_id = $_REQUEST["client_id"];
    $redirect_uri = $_REQUEST["redirect_uri"];
    
    # find token
    clean_expired_token();
    $token = get_token_by_code($code)[0];
    
    # check params
    if (empty($code)) {
        json_response(["error" =>  "invalid_request", "error_description" => "missing code parameter"]);
    }
    elseif (empty($token)) {
        json_response(["error" =>  "access_denied", "error_description" => "access code / token expired"]);
    }
    elseif ($client_id != $token["client_id"]) {
        json_response(["error" =>  "access_denied", "error_description" => "wrong client_id"]);
    }
    elseif (in_array($token["type"], ["id", "revoked"]) or empty($token["scope"])) {
        json_response(["error" =>  "invalid_scope", "error_description" => "authorization code (response_type=id) not useable for access token upgrade"]);
    }
    else {
        db("UPDATE fx_auth SET `type`=?, mtime=?, expires=? WHERE code=?", ["token", time(), time()+3600, $code]);
        json_response([
            "access_token" => $code, #substr($code, 7),
            "token_type" => "Bearer",
            "scope" => $token["scope"],
            "me" => $token["me"],
        ]);
    }
}

function fix_code(&$code) {
    if (strpos($code, '$2y$10$') !== 0) { $code = '$2y$10$' . $code; }
}

#-- test validity of Authorization: Bearer TOKENCODE
function verify_bearer($code) {
    fix_code($code);
    # find token
    clean_expired_token();
    $token = get_token_by_code($code)[0];
    if (!$token or $token["type"] != "token") {
         die(header("Status: 403"));
    }
    json_response([
        "client_id" => $token["client_id"],
        "scope" => $token["scope"],
        "me" => $token["me"],
    ]);
}

#-- 7.1 Token Revocation Request
function revoke($code) {
    if ($token = get_token_by_code($code)[0]) {
        db("UPDATE fx_auth set `type`=? WHERE code=?", ["revoked", $code]);
    }
}


#-- run
if (!empty($_POST["grant_type"])) {
    verify_code();
}
elseif ($code = bearer()) {
    verify_bearer($code);
}
elseif ($_REQUEST["action"] == "revoke") {
    revoke($_REQUEST["token"]);
}
else {
    print<<<HTML
    <div class='fossil-doc' data-title='IndieAuth'>
       <svg height=270 width=215 style='float:left; margin-right: 30pt;' viewBox='0 0 42.967861 53.77858'> <g transform='translate(-26.926707,-72.244048)' id='layer1'>
        <path id='path828' d='m 58.667032,73.126526 c -6.35947,0.04444 -12.71895,0.08888 -19.078418,0.133327 -3.853683,17.29335 -7.707367,34.586687 -11.56105,51.880037 4.67086,-10e-4 9.341719,-0.002 14.012578,-0.003 0.17811,-6.32623 0.35623,-12.65246 0.53434,-18.97869 2.04552,-0.85886 4.09104,-1.71773 6.13657,-2.57659 2.13889,0.8959 4.27777,1.79179 6.41666,2.68769 -0.10594,5.96161 0.64059,11.93241 0.17825,17.88368 -0.82143,1.27915 1.29889,0.50748 2.05533,0.71827 3.82023,0.002 7.64045,0.005 11.46067,0.007 -3.38498,-17.25073 -6.76995,-34.501457 -10.15493,-51.752194 z m -9.48934,7.518922 c 4.76639,-0.180988 8.59732,5.026339 7.02166,9.521985 -1.23655,4.60395 -7.34109,6.7331 -11.17174,3.89414 -4.034474,-2.54196 -4.263284,-9.002574 -0.41875,-11.82358 1.28595,-1.025868 2.92405,-1.596645 4.56883,-1.592545 z m 5.68285,44.224172 c 0.32724,0 0.0986,0 0,0 z'
        style='fill:#aeea47;fill-opacity:1;stroke:#4a5848;stroke-width:1.76499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.99456518' />
       </g></svg>
       <h3>Token endpoint</h3>
       This is the access token grant point. It's not actually needed for IndieAuth requests, but for
       later MicroPub implementations.
       <p>
       Can be registered on a user homepage with:<br>
       <code>&lt;link rel=token_endpoint href='https://$_SERVER[SERVER_NAME]$_SERVER[PHP_SELF]'&gt;</code>
    </div>
HTML;
}


?>