do not use state parameter on auth code verification
[indieauth-openid.git] / www / index.php
1 <?php
2 /**
3  * IndieAuth to OpenID proxy.
4  * Proxies IndieAuth authorization requests to one's OpenID server
5  *
6  * PHP version 5
7  *
8  * @package indieauth-openid
9  * @author  Christian Weiske <cweiske@cweiske.de>
10  * @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3
11  * @link    http://indiewebcamp.com/login-brainstorming
12  * @link    http://indiewebcamp.com/authorization-endpoint
13  * @link    http://indiewebcamp.com/auth-brainstorming
14  * @link    https://indieauth.com/developers
15  */
16 header('IndieAuth: authorization_endpoint');
17 if (($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')
18     && count($_GET) == 0
19 ) {
20     include 'about.php';
21     exit();
22 }
23
24 require_once 'Net/URL2.php';
25 require_once 'OpenID.php';
26 require_once 'OpenID/RelyingParty.php';
27 require_once 'OpenID/Message.php';
28 require_once 'OpenID/Exception.php';
29
30 function loadDb()
31 {
32     $pharFile = \Phar::running();
33     if ($pharFile == '') {
34         $dsn = 'sqlite:' . __DIR__ . '/../data/tokens.sq3';
35         $cfgFilePath = __DIR__ . '/config.php';
36     } else {
37         //remove phar:// from the path
38         $dir = dirname(substr($pharFile, 7)) . '/';
39         $dsn = 'sqlite:' . $dir . '/tokens.sq3';
40         $cfgFilePath = substr($pharFile, 7) . '.config.php';
41     }
42     //allow overriding DSN
43     if (file_exists($cfgFilePath)) {
44         include $cfgFilePath;
45     }
46
47     $db = new PDO($dsn);
48     $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
49     $db->exec("CREATE TABLE IF NOT EXISTS authtokens(
50 code TEXT,
51 me TEXT,
52 redirect_uri TEXT,
53 client_id TEXT,
54 state TEXT,
55 created DATE
56 )");
57     //clean old tokens
58     $stmt = $db->prepare('DELETE FROM authtokens WHERE created < :created');
59     $stmt->execute(array(':created' => date('c', time() - 60)));
60
61     return $db;
62 }
63
64 function create_token($me, $redirect_uri, $client_id, $state)
65 {
66     $code = base64_encode(openssl_random_pseudo_bytes(32));
67     $db = loadDb();
68     $db->prepare(
69         'INSERT INTO authtokens (code, me, redirect_uri, client_id, state, created)'
70         . ' VALUES(:code, :me, :redirect_uri, :client_id, :state, :created)'
71     )->execute(
72         array(
73             ':code' => $code,
74             ':me' => $me,
75             ':redirect_uri' => $redirect_uri,
76             ':client_id' => $client_id,
77             ':state' => (string) $state,
78             ':created' => date('c')
79         )
80     );
81     return $code;
82 }
83
84 function validate_token($code, $redirect_uri, $client_id)
85 {
86     $db = loadDb();
87     $stmt = $db->prepare(
88         'SELECT me FROM authtokens WHERE'
89         . ' code = :code'
90         . ' AND redirect_uri = :redirect_uri'
91         . ' AND client_id = :client_id'
92         . ' AND created >= :created'
93     );
94     $stmt->execute(
95         array(
96             ':code'         => $code,
97             ':redirect_uri' => $redirect_uri,
98             ':client_id'    => $client_id,
99             ':created'      => date('c', time() - 60)
100         )
101     );
102     $row = $stmt->fetch(PDO::FETCH_ASSOC);
103
104     $stmt = $db->prepare('DELETE FROM authtokens WHERE code = :code');
105     $stmt->execute(array(':code' => $code));
106
107     if ($row === false) {
108         return false;
109     }
110     return $row['me'];
111 }
112
113 function error($msg)
114 {
115     header('HTTP/1.0 400 Bad Request');
116     header('Content-type: text/plain; charset=utf-8');
117     echo $msg . "\n";
118     exit(1);
119 }
120
121 function verifyUrlParameter($givenParams, $paramName)
122 {
123     if (!isset($givenParams[$paramName])) {
124         error('"' . $paramName . '" parameter missing');
125     }
126     $url = parse_url($givenParams[$paramName]);
127     if (!isset($url['scheme'])) {
128         error('Invalid URL in "' . $paramName . '" parameter: scheme missing');
129     }
130     if (!isset($url['host'])) {
131         error('Invalid URL in "' . $paramName . '" parameter: host missing');
132     }
133
134     return $givenParams[$paramName];
135 }
136
137 function getBaseUrl()
138 {
139     if (!isset($_SERVER['REQUEST_SCHEME'])) {
140         $_SERVER['REQUEST_SCHEME'] = 'http';
141     }
142     $file = preg_replace('/[?#].*$/', '', $_SERVER['REQUEST_URI']);
143     return $_SERVER['REQUEST_SCHEME'] . '://'
144         . $_SERVER['HTTP_HOST']
145         . $file;
146 }
147
148 session_start();
149 $returnTo = getBaseUrl();
150 $realm    = getBaseUrl();
151
152 if (isset($_GET['openid_mode']) && $_GET['openid_mode'] != '') {
153     //verify openid response
154     if (!count($_POST)) {
155         list(, $queryString) = explode('?', $_SERVER['REQUEST_URI']);
156     } else {
157         $queryString = file_get_contents('php://input');
158     }
159
160     $message = new \OpenID_Message($queryString, \OpenID_Message::FORMAT_HTTP);
161     $id      = $message->get('openid.claimed_id');
162     if (OpenID::normalizeIdentifier($id) != OpenID::normalizeIdentifier($_SESSION['me'])) {
163         error(
164             sprintf(
165                 'Given identity URL "%s" and claimed OpenID "%s" do not match',
166                 $_SESSION['me'], $id
167             )
168         );
169     }
170     try {
171         $o = new \OpenID_RelyingParty($returnTo, $realm, $_SESSION['me']);
172         $result = $o->verify(new \Net_URL2($returnTo . '?' . $queryString), $message);
173
174         if ($result->success()) {
175             $token = create_token(
176                 $_SESSION['me'], $_SESSION['redirect_uri'],
177                 $_SESSION['client_id'], $_SESSION['state']
178             );
179             //redirect to indieauth
180             $url = new Net_URL2($_SESSION['redirect_uri']);
181             $url->setQueryVariable('code', $token);
182             $url->setQueryVariable('me', $_SESSION['me']);
183             $url->setQueryVariable('state', $_SESSION['state']);
184             header('Location: ' . $url->getURL());
185             exit();
186         } else {
187             error('Error verifying OpenID login: ' . $result->getAssertionMethod());
188         }
189     } catch (OpenID_Exception $e) {
190         error('Error verifying OpenID login: ' . $e->getMessage());
191     } catch (Exception $e) {
192         error(get_class($e) . ': ' . $e->getMessage());
193     }
194 }
195
196 if ($_SERVER['REQUEST_METHOD'] == 'GET') {
197     $me           = verifyUrlParameter($_GET, 'me');
198     $redirect_uri = verifyUrlParameter($_GET, 'redirect_uri');
199     $client_id    = verifyUrlParameter($_GET, 'client_id');
200     $state        = null;
201     if (isset($_GET['state'])) {
202         $state = $_GET['state'];
203     }
204     $response_type = 'id';
205     if (isset($_GET['response_type'])) {
206         $response_type = $_GET['response_type'];
207     }
208     if ($response_type != 'id') {
209         error('unsupported response_type: ' . $response_type);
210     }
211
212     $_SESSION['me']           = $me;
213     $_SESSION['redirect_uri'] = $redirect_uri;
214     $_SESSION['client_id']    = $client_id;
215     $_SESSION['state']        = $state;
216
217     try {
218         $o = new \OpenID_RelyingParty($returnTo, $realm, $me);
219         //if you get timeouts (errors like
220         // OpenID error: Request timed out after 3 second(s)
221         //) then uncomment the following line which disables
222         // all timeouts:
223         //$o->setRequestOptions(array('follow_redirects' => true));
224         $authRequest = $o->prepare();
225         $url = $authRequest->getAuthorizeURL();
226         header("Location: $url");
227         exit(0);
228     } catch (OpenID_Exception $e) {
229         error('OpenID error: ' . $e->getMessage());
230     } catch (Exception $e) {
231         error(get_class($e) . ': ' . $e->getMessage());
232     }
233 } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
234     $redirect_uri = verifyUrlParameter($_POST, 'redirect_uri');
235     $client_id    = verifyUrlParameter($_POST, 'client_id');
236     if (!isset($_POST['code'])) {
237         error('"code" parameter missing');
238     }
239     $token = $_POST['code'];
240
241     $me = validate_token($token, $redirect_uri, $client_id);
242     if ($me === false) {
243         error('Validating token failed');
244     }
245     header('Content-type: application/x-www-form-urlencoded');
246     echo 'me=' . urlencode($me);
247 }
248 ?>