From 6175ad184c66cbdbdc73d89719bfc601d5c9ba80 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Sun, 20 Dec 2015 11:44:15 -0800 Subject: [PATCH] implement indieauth sign-in --- README.md | 2 + composer.json | 6 +- composer.lock | 172 ++++++++++++++++++++++++++- controllers/API.php | 9 ++ controllers/Auth.php | 117 ++++++++++++++++++ controllers/Controller.php | 23 ++++ public/assets/telegraph-logo-256.png | Bin 0 -> 5527 bytes public/index.php | 9 ++ views/dashboard.php | 3 + views/index.php | 8 +- views/layout.php | 22 ++++ views/login.php | 54 +++++++++ 12 files changed, 418 insertions(+), 7 deletions(-) create mode 100644 controllers/API.php create mode 100644 controllers/Auth.php create mode 100644 public/assets/telegraph-logo-256.png create mode 100644 views/dashboard.php create mode 100644 views/layout.php create mode 100644 views/login.php diff --git a/README.md b/README.md index 0aeb8e6..090404f 100644 --- a/README.md +++ b/README.md @@ -68,3 +68,5 @@ A callback from Telegraph will include the following post body parameters: ## Credits Telegraph photo: https://www.flickr.com/photos/nostri-imago/3407786186 + +Telegraph icon: https://thenounproject.com/search/?q=telegraph&i=22058 diff --git a/composer.json b/composer.json index b85bc10..6bd3bd4 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,8 @@ "php": ">=5.5", "mf2/mf2": "0.2.*", "indieweb/mention-client": "1.*", + "indieauth/client": "0.1.*", + "firebase/php-jwt": "~3.0", "league/route": "~1.2", "league/plates": "~3.1" }, @@ -13,7 +15,9 @@ "files": [ "config.php", "lib/helpers.php", - "controllers/Controller.php" + "controllers/Controller.php", + "controllers/Auth.php", + "controllers/API.php" ] } } diff --git a/composer.lock b/composer.lock index 0921ad2..d64b131 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,177 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "5cc37d4bc843c6cbd92dc660ef9e71d8", - "content-hash": "07f934dcf7d5e224f6c980c51525aa81", + "hash": "5630557b773b8342de2ebfcfbe23f013", + "content-hash": "88dfc4a35925d318e92d4881b37d70a0", "packages": [ + { + "name": "barnabywalters/mf-cleaner", + "version": "v0.1.4", + "source": { + "type": "git", + "url": "https://github.com/barnabywalters/php-mf-cleaner.git", + "reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barnabywalters/php-mf-cleaner/zipball/ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4", + "reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4", + "shasum": "" + }, + "require-dev": { + "php": ">=5.3", + "phpunit/phpunit": "*" + }, + "suggest": { + "mf2/mf2": "To parse microformats2 structures from (X)HTML" + }, + "type": "library", + "autoload": { + "files": [ + "src/BarnabyWalters/Mf2/Functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barnaby Walters", + "email": "barnaby@waterpigs.co.uk" + } + ], + "description": "Cleans up microformats2 array structures", + "time": "2014-10-06 23:11:15" + }, + { + "name": "firebase/php-jwt", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1", + "reference": "fa8a06e96526eb7c0eeaa47e4f39be59d21f16e1", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "time": "2015-07-22 18:31:08" + }, + { + "name": "indieauth/client", + "version": "0.1.11", + "source": { + "type": "git", + "url": "https://github.com/indieweb/indieauth-client-php.git", + "reference": "6504ed0d4714084e9955f639d6e5cf4e976f9038" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/indieweb/indieauth-client-php/zipball/6504ed0d4714084e9955f639d6e5cf4e976f9038", + "reference": "6504ed0d4714084e9955f639d6e5cf4e976f9038", + "shasum": "" + }, + "require": { + "barnabywalters/mf-cleaner": "0.*", + "indieweb/link-rel-parser": "0.1.1", + "mf2/mf2": "0.2.*", + "php": ">5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "IndieAuth": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache 2.0" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "http://aaronparecki.com" + } + ], + "description": "IndieAuth Client Library", + "time": "2015-08-30 22:29:40" + }, + { + "name": "indieweb/link-rel-parser", + "version": "0.1.1", + "source": { + "type": "git", + "url": "https://github.com/indieweb/link-rel-parser-php.git", + "reference": "9e0e635fd301a8b1da7bc181f651f029c531dbb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/indieweb/link-rel-parser-php/zipball/9e0e635fd301a8b1da7bc181f651f029c531dbb6", + "reference": "9e0e635fd301a8b1da7bc181f651f029c531dbb6", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/IndieWeb/link_rel_parser.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "http://aaronparecki.com" + }, + { + "name": "Tantek Çelik", + "homepage": "http://tantek.com" + } + ], + "description": "Parse rel values from HTTP headers", + "homepage": "https://github.com/indieweb/link-rel-parser-php", + "keywords": [ + "http", + "indieweb", + "microformats2" + ], + "time": "2013-12-23 00:14:58" + }, { "name": "indieweb/mention-client", "version": "1.0.0", diff --git a/controllers/API.php b/controllers/API.php new file mode 100644 index 0000000..8794760 --- /dev/null +++ b/controllers/API.php @@ -0,0 +1,9 @@ +setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'return_to' => $request->get('return_to') + ])); + return $response; + } + + public function login_start(Request $request, Response $response) { + + if(!$request->get('url') || !($me = IndieAuth\Client::normalizeMeURL($request->get('url')))) { + $response->setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'error' => 'Invalid URL', + 'error_description' => 'The URL you entered, "' . htmlspecialchars($request->get('url')) . '" is not valid.' + ])); + return $response; + } + + $authorizationEndpoint = IndieAuth\Client::discoverAuthorizationEndpoint($me); + + $state = JWT::encode([ + 'me' => $me, + 'authorization_endpoint' => $authorizationEndpoint, + 'return_to' => $request->get('return_to'), + 'time' => time(), + 'exp' => time()+300 // verified by the JWT library + ], Config::$secretKey); + + if($authorizationEndpoint) { + // If the user specified only an authorization endpoint, use that + $authorizationURL = IndieAuth\Client::buildAuthorizationURL($authorizationEndpoint, $me, self::_buildRedirectURI(), Config::$clientID, $state); + } else { + // Otherwise, fall back to indieauth.com + $authorizationURL = IndieAuth\Client::buildAuthorizationURL(Config::$defaultAuthorizationEndpoint, $me, self::_buildRedirectURI(), Config::$clientID, $state); + } + + $response->setStatusCode(302); + $response->headers->set('Location', $authorizationURL); + return $response; + } + + public function login_callback(Request $request, Response $response) { + + if(!$request->get('state') || !$request->get('code') || !$request->get('me')) { + $response->setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'error' => 'Missing Parameters', + 'error_description' => 'The auth server did not return the necessary parameters, state and code and me.' + ])); + return $response; + } + + // Validate the "state" parameter to ensure this request originated at this client + try { + $state = JWT::decode($request->get('state'), Config::$secretKey, ['HS256']); + + if(!$state) { + $response->setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'error' => 'Invalid State', + 'error_description' => 'The state parameter was not valid.' + ])); + return $response; + } + } catch(Exception $e) { + $response->setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'error' => 'Invalid State', + 'error_description' => 'The state parameter was invalid:
'.htmlspecialchars($e->getMessage()) + ])); + return $response; + } + + // Discover the authorization endpoint from the "me" that was returned by the auth server + // This allows the auth server to return a different URL than the user originally entered, + // for example if the user enters multiusersite.example the auth server can return multiusersite.example/alice + if($state->authorization_endpoint) { // only discover the auth endpoint if one was originally found, otherwise use our fallback + $authorizationEndpoint = IndieAuth\Client::discoverAuthorizationEndpoint($request->get('me')); + } else { + $authorizationEndpoint = Config::$defaultAuthorizationEndpoint; + } + + // Verify the code with the auth server + $token = IndieAuth\Client::verifyIndieAuthCode($authorizationEndpoint, $request->get('code'), $request->get('me'), self::_buildRedirectURI(), Config::$clientID, $request->get('state'), true); + + if(!array_key_exists('auth', $token) || !array_key_exists('me', $token['auth'])) { + // The auth server didn't return a "me" URL + $response->setContent(view('login', [ + 'title' => 'Sign In to Telegraph', + 'error' => 'Invalid Auth Server Response', + 'error_description' => 'The authorization server did not return a valid response:
'.htmlspecialchars(json_encode($token)) + ])); + return $response; + } + + // Create or load the user + + session_start(); + $_SESSION['me'] = $token['auth']['me']; + $response->setStatusCode(302); + $response->headers->set('Location', ($state->return_to ?: '/dashboard')); + return $response; + } + + private static function _buildRedirectURI() { + return 'http' . (Config::$ssl ? 's' : '') . '://' . $_SERVER['SERVER_NAME'] . '/login/callback'; + } + +} diff --git a/controllers/Controller.php b/controllers/Controller.php index 292caf2..5714f1c 100644 --- a/controllers/Controller.php +++ b/controllers/Controller.php @@ -4,6 +4,18 @@ use Symfony\Component\HttpFoundation\Response; class Controller { + private function _is_logged_in(&$request, &$response) { + session_start(); + if(!array_key_exists('me', $_SESSION)) { + session_destroy(); + $response->setStatusCode(302); + $response->headers->set('Location', '/login?return_to='.$request->getPathInfo()); + return false; + } else { + return true; + } + } + public function index(Request $request, Response $response) { $response->setContent(view('index', [ 'title' => 'Telegraph' @@ -11,4 +23,15 @@ class Controller { return $response; } + public function dashboard(Request $request, Response $response) { + if(!$this->_is_logged_in($request, $response)) { + return $response; + } + + $response->setContent(view('dashboard', [ + 'title' => 'Dashboard' + ])); + return $response; + } + } diff --git a/public/assets/telegraph-logo-256.png b/public/assets/telegraph-logo-256.png new file mode 100644 index 0000000000000000000000000000000000000000..fcc1a636321157283bf3782e83c4c8cfffeb3c42 GIT binary patch literal 5527 zcmcgwc{tST+yBlOMIvNN+Z-b67|V>E8EbYi$V3?o#xkRsv1dsk6;ig76e>}+IF_-O zEQyfqFw8`jB-zQ9cRJ^Ee(xXW`n}in{&Bw7b6wy2zMuPZZ_j=I@jUU?md4x%L=OM} zz-@BIzy<(7ED{88>|_0$$oU?u-w~?eIjSwmgBpY*;{mig$rTSZ@xyuIZSXkvkV|jy zS^&W2MZlb+p0hC5a3lG_a6fcl!G8WMGyrJn2K(dOeDGALE8dep)D~N)Z4`qN+_l9V z5EgI?e|@|c;Y=tQZyRcfaSQcvQ+F5B)q!dSYp@LX;i))iu%9oHq7keu_J>^!7X3r4 zAO`(Ih3ca%_GeJ%EUcmWBr+a~fGNwn!4(yuNF+>A1%X5=%0ZRjib@J_)aU@o5EK*y3sQ!W$es#{>gwu0Y$z$ovoz!> zAw()JSe{4`|7pPhPjMp?{HX*I5&FX-&Xp8E)fQt#`ezD${uUO08zxfzDio_^3c)yk z1w|NK!O!nUUVms)s5bcjgz=Bs6ikReUcm-WAq9}#Sm)s({u9iq-G6uVLy;AYh83B> zIux9*0m&`E4^O0;7-);Jc3|!VcMZ6T8mo$`DsWXLI8sefQB6q+ZKQ|RMR#S!RDI*NkRn&fBO^6gKj_8K}>6gIr`xh4e zU$GkcWIT>aB4bD--=7^|?M0%JC|)FgsJ<-{DrJFlBM^TGq<yAhe> z2mPbM8iap{fR*n5k@=rk_y14T6j;G1{AiE=)h<6ptRDP9{@wg6=I`;r6InAuW(~)= z^fY$>;A=54Kx2YOzGQKxq~edXF*VuP*xGYrbD_->V3=Gk^hv6Rhq2jPr;g6Md3h&f zXT6SZ@Z_B~RrdR(zzLh;L^0z&)z|)-I|#kc+C%`2O2cJ@h3#GL4w|O8dwd-Y^ZdFo zwy>c&qaK=0yG&n+e7F(va3frq=JXDP(kPhV2cQ5D#hPz00B`{R8uRaZ`#Z(|B>Fgx zI3GSvGh8DM+^c}uCfEa~;+<><6UG9r%`t0(o-^M}l4;0D_hs4)jkXu@iZ+5R_QK~J zt+`v36mouMJ_KeG^8$A@6S0d8TFeMPkOX-9cvq@L2KZUQyp(2r5_Q_?M8^7RWnBH~ zqaTW1(svo}7T0;w3zfW_H`Z3tft`%Et)&Z6i!ziEbQv)FKD~MJ+Sa;l3rxr_XaUAOD<~id9KxdrnYr| zsI7frEBzxQujbHD&hcmML`mveKRx0$a z8WZ_Pc}onQ(z(5c`XC!{7W3C^XYYxDDBx7w1x8*=)8^jHygA8?KX7+n24!J>{_tSU zp{g{klaj&PH>&35*Qz19X8KOIxv;}T0G#}P-Ix`E4rWGHPW2CZ@T>I`mL&x^LrHt(Lx0ghZ$zCUeFp%_iOaQ z`eO}2M-r#f+V_}~^C~3GQ;UvUsZ|xMl2yNDH&kO%UiL6eZyx9mZFDW7-H)`JB=L9~ z0|Fg`uP1jxGbpk{m!ghWVYgo@s#VT1A`XYt_k3+hl{7^tMh2c*6m7jfK0kAO*9`*I z(Q?VNu2vl1AbL_3mj-JVa3vp=9{IkgiahsL*GKlD?(978;e1iy1-)!*U{G+GjRUpg zW&Zopdf8xjiu!gi*ZlsLFKX#wnIHY~s*LN4+CP#Qg{yJgps2*Mn-$aBj%tmy^@(oz zbtCKp5#+65mg309+SvGCzOwC)8A}O6727nEU8k1%L%b&PQSaoTrdMXbgCV&R-RmtUy!O%k= zBuE<}wPSRxHjRB8s2kPYTUF0Etre5`?Pbqz5?it4)UMdW?yvYq9@{m@FRz!ze4{v9 zi-R~&pQ|eqx+|;vBW`i!HGajvW9a70JQKY>JiWT-t^WxCote@;c)DFm_GDVmpu|oc zW_2~g=iLW|kM`mKZ%TfpTuv(BYYUpGe92zUKk#06az22l3cl^(JvwOS6^H3j_g=rJ z^v!f~Y%Y4)69w43u`H%tv#0bM{|aCx@($j6a5q|7^7m)>rI7AqyJh-=X{glndmQ%PH9rw*ZY#A7kFKkT6~2$WCVs-Cvzbn8nw|8e!O)>gZ#A#I_D@@zLo9i5v$oEF?=?o`atRF|YxeufJ7R`1^N_x(+ZGKpuip2dAzv5=EduR(#;4-i&SH^x|P> zym_R&1o!l$l7y=8y?UbM$%w&ZL2BZ9pq=bc$n{^--hanL)AnkkG^`+d>~8ULeY;c-Yjcx@ucws1A-$%#%gamxm^_cu!9$~nRr^cIF?#bU&>0A;=Yg~r#{CUVcHqRnm- zHnRHacR@jOu9e^XdcnLeCOC!j?z`LHs2}4(hYu(hab+h!H8ly9K5g=(eq#w+8)!8E=aIW*JvUa*iz-kcbBkSb}BV7F!%`6AnI*=|mx0(De)6`(5#Y z5eC9U$y?qr%#L2k+#K6DBz)rAuG98FvP_r!jAbKJ@#R)py}f(>8oQQMr@Ikmn8Cpd){-k4%-r3gfpezgqehW#@-=7GE{mhmnPMFSFrdK3X3xorRsda(y6|3p&|?0FX;%U-SBZeye@}Af1*8sHIbt5qzGen>^_9k9i%}~m zjM}PN`sYXB9>(HX2?Id;?dn3=3DnRF2IK>d)F&s*yX+{Oa?k8wG4?4Z7~R;1K?J9BeP&Cfbj8?=h*|pZJ&U$BG<+s~h9ogA^iaM9?o!$8 z9Cf_@_=o5g=`$xFL)kf5J~@yNlS;=Q9v~%N+DzJ)4=;#Qe`J3kBJW_^P8ruA-|f3T z94k%jo>D3wFEPkz2j*xmT$TexdatX#~ymUMq-&tEpD zm5y!eGdyacN7P#0Nf>bDc^y$o5Y=+gTRd|_yy(<4pz)hzb{^TAay@IQpg%hw-J(9i zR_f&Q-7Hw;Sa7W&J2ueQd~$^$G~WJTW8HI3ug+!|d|N(3r1YwKRJ>ZGxBAP}$iAci zp>Xek@ke!8m;vo*Wn|atB(mn$Y_qzjRi?LsYE) z)+=$W)tX@^`V?sJ)TfM!`OT{yBkCsN`{Od^r=nlwrg<6Q zPL@CVQlEDns3Y{Yx{}PCN=Meb%Mp@LX8};kGhV4i7%EHRJT&XeMD(R^X8fT$bMmj# zEf6!zK5T?V&Iyn1;CcJ+_AzI#c1ik9tsD{qmkxd}+Hfuvn(*GfTt9UZNt@n1ip(@Q z=aK$|%i7saF!A)W8qMNtG?%{S-4~{_N3Z58IoVxd_#O(Zn7hArkg`tCs=#31 z)s&NM2DnB&`7E<4cJ9gt-Cm%rDaul>Uw$zqzAQaD=P`Cay{`LR_srWOvx(LOV#|#U zp5kZEYC88a?4J}63<2+u^_LG4`Z`55&I0$fBc1EdefDs8@IH{UyOZa8m(fD`P(O7c z;);)3v2iBYf3PL}%XWWV$VR_!xrKrd!qeL>X{JiGu+sSW0gH2PfoB063NGn@j=yE&zcJ z^Pyso0f2hct6HFVHwOY-e*#vE1%Ygig9PeNuwLs>u55s@Fc1Yp;hcXE&|c>#S22JL z0D@U;`4cjr$HHO&z*=X-=Q&ZaW)N$d4r4klQ}ifvd<%3+c%gYgNDgS8i)DPdckl8s z6co5^5O`c?CpMb>Lk*$t>#y)j6Dh6SN3TPGJ2967JETu%1(VKS^ri@8JNOmN^*!yj z8%8`Q`FMGY8d(BRKB={f3xh@HYjDl2$i|E}70TxDy%^!K4m(+Ay8uMGAQbH$z7al} zp;&y8*$~4802o+5=wEw5`1?-(addRoute('GET', '/', 'Controller::index'); +$router->addRoute('GET', '/dashboard', 'Controller::dashboard'); +$router->addRoute('GET', '/api', 'Controller::api'); + +$router->addRoute('POST', '/webmention', 'API::webmention'); +$router->addRoute('GET', '/webmention/{code}', 'API::webmention_status'); + +$router->addRoute('GET', '/login', 'Auth::login'); +$router->addRoute('POST', '/login/start', 'Auth::login_start'); +$router->addRoute('GET', '/login/callback', 'Auth::login_callback'); diff --git a/views/dashboard.php b/views/dashboard.php new file mode 100644 index 0000000..2ca7e67 --- /dev/null +++ b/views/dashboard.php @@ -0,0 +1,3 @@ +layout('layout', ['title' => $title]); ?> + + diff --git a/views/index.php b/views/index.php index 153bb49..8d5485f 100644 --- a/views/index.php +++ b/views/index.php @@ -153,7 +153,7 @@ $menu = [ @@ -164,7 +164,7 @@ $menu = [ $name): ?> - Login + Login @@ -191,7 +191,7 @@ $menu = [ Telegraph

Easily send Webmentions from your website

-
Get Started
+ Get Started @@ -201,7 +201,7 @@ $menu = [

We send webmentions for you

-

Let Telegraph send webmentions for you. With a simple API, Telegraph will handle sending webmentions to other websites. Let Telegraph handle webmention discovery, and retrying on temporary failures. Telegraph will notify your site when a webmention was successfully sent.

+

Let Telegraph send webmentions for you. With a simple API, Telegraph will handle sending webmentions to other websites. Let us handle webmention discovery, and retrying on temporary failures. Telegraph will notify your site when a webmention was successfully sent.

Send webmentions automatically

You can even let Telegraph subscribe to your feed, and it will send webmentions whenever you publish a new post.

diff --git a/views/layout.php b/views/layout.php new file mode 100644 index 0000000..95fbe89 --- /dev/null +++ b/views/layout.php @@ -0,0 +1,22 @@ + + + + + + + + <?= $this->e($title) ?> + + + + + + + +section('content') ?> + +
+
+ + + diff --git a/views/login.php b/views/login.php new file mode 100644 index 0000000..1b4202b --- /dev/null +++ b/views/login.php @@ -0,0 +1,54 @@ +layout('layout', ['title' => $title]); ?> + + + +
+
+

+ +
+ Sign in to Telegraph +
+

+ + +
+
+ +
+ + +
+
+
+
+ + +
+
+ + +
+ +
+ +
+ +
+ What's this? About IndieAuth +
+
+