#!/usr/bin/php-cgi -dcgi.force_redirect=0
<?php
# encoding: utf-8
# api: cgi
# type: store
# 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: -
# access: auth
#
# 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><link rel=token_endpoint href='https://$_SERVER[SERVER_NAME]$_SERVER[PHP_SELF]'></code>
</div>
HTML;
}
?>