Collection of themes/skins for the Fossil SCM

⌈⌋ ⎇ branch:  Fossil Skins Extra


Check-in [bbc582b007]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Enable code_verifier/challenge test, add `token` script to upgrade from auth code to access token
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: bbc582b0070743fe1a1f808ae142e6465d3e6402
User & Date: mario 2021-04-04 22:51:34
Context
2021-04-04
22:51
Enable upvar 1 for fx_stats query check-in: d933f24359 user: mario tags: trunk
22:51
Enable code_verifier/challenge test, add `token` script to upgrade from auth code to access token check-in: bbc582b007 user: mario tags: trunk
16:54
Add scope support, confirm page, and code_challenge fields (not verified yet) check-in: 20e5c47f73 user: mario tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to extroot/auth.

142
143
144
145
146
147
148



149
150
151
152
153
154
155
             return True;
        }
    }
}
function trim_url($url) {
    return strtolower(preg_replace("~^https?://|/+$~", "", $url));
}




#-- 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()]);







>
>
>







142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
             return True;
        }
    }
}
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()]);
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
        INSERT INTO fx_auth
        (`code`, `type`, `login`, `me`, `client_id`, `redirect_uri`, `state`, `code_challenge`, `code_challenge_m`, `expires`)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
        [$code, $response_type, $user, $me, $client_id, $redirect_uri, $state, $code_challenge, $code_challenge_m, time()+300]
    );

    # construct confirmation+redirect url
    $url = $redirect_uri;
    $url .= strstr($url, "?") ? "&" : "?";
    $url .= "code=" . urlencode($code) . "&state=" . urlencode($state);
    
    # output page
    if ($response_type == "code") {
        $scope = scope_list();
    }
    $html = <<<HTML








|
|
|







197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
        INSERT INTO fx_auth
        (`code`, `type`, `login`, `me`, `client_id`, `redirect_uri`, `state`, `code_challenge`, `code_challenge_m`, `expires`)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
        [$code, $response_type, $user, $me, $client_id, $redirect_uri, $state, $code_challenge, $code_challenge_m, time()+300]
    );

    # construct confirmation+redirect url
    $url = $redirect_uri
         . (strstr($redirect_uri, "?") ? "&" : "?")
         . "code=" . urlencode($code) . "&state=" . urlencode($state);
    
    # output page
    if ($response_type == "code") {
        $scope = scope_list();
    }
    $html = <<<HTML

237
238
239
240
241
242
243

244
245
246

247
248
249
250

251
252
253
254
255
256
257
258
259
260
261
262



263
264
265
266
267
268
269
270
271


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

    # input

    $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) or empty($client_id) or empty($redirect_uri)) {
        $ls = join(", ", array_diff(["code", "client_id", "redirect_uri"], array_keys($_REQUEST)));
        die(json_encode(["error" =>  "invalid_request", "error_description" => "missing parameters ($ls)"]));
    }
    elseif (empty($token)) {
        die(json_encode(["error" =>  "invalid_request", "error_description" => "code '$code' does not exist (possibly expired)"]));
    }
    elseif (($token["client_id"] != $client_id) or ($token["redirect_uri"] != $redirect_uri)) {
        die(json_encode(["error" =>  "invalid_scope", "error_description" => "code does not match previous params (client_id, redirect_uri)"]));
    }



    else {
        die(json_encode(["me" => $token["me"]]));
    }
}


#-- run
if (empty($_POST["redirect_target"]) and !empty($_REQUEST["code"])) {  # ?code=… when the remote app verifies the response
    verify_code();







>



>




>







|




>
>
>

|







240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280


#-- send ?code= verification response
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"];
    $code_verifier = $_REQUEST["code_verifier"];
    
    # find token
    clean_expired_token();
    $token = get_token_by_code($code)[0];
    $code_challenge = $token["code_challenge"];
    
    # check params
    if (empty($code) or empty($client_id) or empty($redirect_uri)) {
        $ls = join(", ", array_diff(["code", "client_id", "redirect_uri"], array_keys($_REQUEST)));
        die(json_encode(["error" =>  "invalid_request", "error_description" => "missing parameters ($ls)"]));
    }
    elseif (empty($token)) {
        die(json_encode(["error" =>  "access_denied", "error_description" => "code '$code' does not exist (possibly expired)"]));
    }
    elseif (($token["client_id"] != $client_id) or ($token["redirect_uri"] != $redirect_uri)) {
        die(json_encode(["error" =>  "invalid_scope", "error_description" => "code does not match previous params (client_id, redirect_uri)"]));
    }
    elseif ($code_challenge and base64_urlencode(hash("sha256", $code_verifier, 1)) != $code_challenge) {
        die(json_encode(["error" =>  "unauthorized_client", "error_description" => "code_challenge does not match code_verifier"]));
    }
    else {
        die(json_encode(["me" => $token["me"], "scope" => $token["scope"]]));
    }
}


#-- run
if (empty($_POST["redirect_target"]) and !empty($_REQUEST["code"])) {  # ?code=… when the remote app verifies the response
    verify_code();

Added extroot/token.























































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#!/usr/bin/php-cgi -dcgi.force_redirect=0
<?php
# encoding: utf-8
# api: cgi
# type: json
# 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.
#
# 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
function bearer() {
    if (empty($_SERVER["HTTP_AUTHORIZATION"])) {
         return "";
    }
    if (preg_match("/Bearer\s+(\S+)/i", $_SERVER["HTTP_AUTHORIZATION"], $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"])) {
        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`=?, expires=? WHERE code=?", ["token", time()+3600, $code]);
        json_response([
            "access_token" => $code,
            "token_type" => "Bearer",
            "scope" => $token["scope"],
            "me" => $token["me"],
        ]);
    }
}

#-- test validity of Authorization: Bearer TOKENCODE
function verify_bearer($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;
}


?>