2019-09-10 00:38:27 +00:00
< ? php
/** @ license MIT
* Copyright 2017 J . King , Dustin Wilson et al .
* See LICENSE and AUTHORS files for details */
declare ( strict_types = 1 );
namespace JKingWeb\Arsse\REST\Microsub ;
2019-09-10 04:02:11 +00:00
use JKingWeb\Arsse\Arsse ;
2019-09-10 00:38:27 +00:00
use JKingWeb\Arsse\Misc\URL ;
2019-09-10 21:48:38 +00:00
use JKingWeb\Arsse\Misc\Date ;
2019-09-10 00:38:27 +00:00
use Psr\Http\Message\ServerRequestInterface ;
use Psr\Http\Message\ResponseInterface ;
use Zend\Diactoros\Response\HtmlResponse as Response ;
use Zend\Diactoros\Response\EmptyResponse ;
class Auth extends \JKingWeb\Arsse\REST\AbstractHandler {
2019-09-13 15:02:56 +00:00
/** The scopes which we grant to Microsub clients. Mute and block are not included because they have no meaning in an RSS/Atom context; this may signal to clients to suppress muting and blocking in their UI */
const SCOPES = " read follow channels " ;
2019-09-10 00:38:27 +00:00
public function __construct () {
}
public function dispatch ( ServerRequestInterface $req ) : ResponseInterface {
// ensure that a user name is specified in the path
// if the path is empty or contains a slash, this is not a URL we handle
$id = parse_url ( $req -> getRequestTarget ())[ 'path' ] ? ? " " ;
if ( ! strlen ( $id ) || strpos ( $id , " / " ) !== false ) {
return new EmptyResponse ( 404 );
}
$id = rawurldecode ( $id );
// gather the query parameters and act on the "proc" parameter
$method = " do " . ucfirst ( strtolower ( $req -> getQueryParams ()[ 'proc' ] ? ? " discovery " ));
if ( ! method_exists ( $this , $method )) {
return new EmptyResponse ( 404 );
} else {
return $this -> $method ( $id , $req );
}
}
2019-09-13 15:02:56 +00:00
/** Produces a user - identifier URL consiustent with the request
*
* This involves reconstructing the scheme and authority based on $_SERVER
* variables ; it may fail depending on server configuration
*/
2019-09-10 04:02:11 +00:00
protected function buildIdentifier ( ServerRequestInterface $req , bool $baseOnly = false ) : string {
2019-09-10 00:38:27 +00:00
// construct the base user identifier URL; the user is never checked against the database
$s = $req -> getServerParams ();
2019-09-10 04:02:11 +00:00
$path = $req -> getRequestTarget ()[ 'path' ];
2019-09-10 00:38:27 +00:00
$https = ( strlen ( $s [ 'HTTPS' ] ? ? " " ) && $s [ 'HTTPS' ] !== " off " );
2019-09-13 15:02:56 +00:00
$port = ( int ) ( $s [ 'SERVER_PORT' ] ? ? 0 );
2019-09-10 00:38:27 +00:00
$port = ( ! $port || ( $https && $port == 443 ) || ( ! $https && $port == 80 )) ? " " : " : $port " ;
$base = URL :: normalize (( $https ? " https " : " http " ) . " :// " . $s [ 'HTTP_HOST' ] . $port . " / " );
2019-09-10 04:02:11 +00:00
return ! $baseOnly ? URL :: normalize ( $base . $path ) : $base ;
}
2019-09-13 15:02:56 +00:00
/** Presents a very basic user profile for discovery purposes
*
* The HTML document itself consists only of link elements and an
* encoding declaration ; Link header - fields are also included for
* HEAD requests
*
* Since discovery is publicly accessible , we produce a discovery
* page for all potential user name so as not to facilitate user
* enumeration
*/
2019-09-10 04:02:11 +00:00
protected function doDiscovery ( string $user , ServerRequestInterface $req ) : ResponseInterface {
$base = $this -> buildIdentifier ( $req , true );
$id = $this -> buildIdentifier ( $req );
2019-09-10 00:38:27 +00:00
$urlAuth = $id . " ?proc=login " ;
$urlToken = $id . " ?proc=issue " ;
$urlService = $base . " microsub " ;
// output an extremely basic identity resource
$html = '<meta charset="UTF-8"><link rel="authorization_endpoint" href="' . htmlspecialchars ( $urlAuth ) . '"><link rel="token_endpoint" href="' . htmlspecialchars ( $urlToken ) . '"><link rel="microsub" href="' . htmlspecialchars ( $urlService ) . '">' ;
return new Response ( $html , 200 , [
" Link: < $urlAuth >; rel= \" authorization_endpoint \" " ,
" Link: < $urlToken >; rel= \" token_endpoint \" " ,
" Link: < $urlService >; rel= \" microsub \" " ,
]);
}
2019-09-10 04:02:11 +00:00
2019-09-13 15:02:56 +00:00
/** Handles the authentication / authorization process
*
* Authentication is achieved via an HTTP Basic authentiation
* challenge ; once the user successfully logs in a code is issued
* and redirection occurs . Scopes are for all intents and purposes
* ignored and client information is not presented .
*/
2019-09-10 04:02:11 +00:00
protected function doLogin ( string $user , ServerRequestInterface $req ) : ResponseInterface {
if ( ! $req -> getAttribute ( " authenticated " , false )) {
// user has not yet logged in, or has failed to log in
return new EmptyResponse ( 401 );
} else {
// user has logged in
// ensure the logged-in user matches the IndieAuth identifier URL
$id = $req -> getAttribute ( " authenticatedUser " );
$query = $req -> getQueryParams ();
$url = buildIdentifier ( $req );
if ( $user !== $id || URL :: normalize ( $query [ 'me' ]) !== $url ) {
return new EmptyResponse ( 403 );
} else {
2019-09-10 21:48:38 +00:00
$redir = URL :: normalize ( rawurldecode ( $query [ 'redirect_uri' ]));
$state = $query [ 'state' ] ? ? " " ;
// check that the redirect URL is an absolute one
if ( ! URL :: absolute ( $redir )) {
return new EmptyResponse ( 400 );
}
// issue an authorization code and build the redirect URL
2019-09-13 15:02:56 +00:00
$code = Arsse :: $db -> tokenCreate ( $id , " microsub.auth " , null , Date :: add ( " PT2M " ), $query [ 'client_id' ]);
2019-09-10 21:48:38 +00:00
$next = URL :: queryAppend ( $redir , " code= $code &state= $state " );
return new EmptyResponse ( 302 , [ " Location: $next " ]);
2019-09-10 04:02:11 +00:00
}
}
}
2019-09-13 01:19:26 +00:00
protected function doIssue ( string $user , ServerRequestInterface $req ) : ResponseInterface {
2019-09-13 15:02:56 +00:00
2019-09-13 01:19:26 +00:00
}
2019-09-10 00:38:27 +00:00
}