From 60d05195838d19f5b6e1145683808539892b7326 Mon Sep 17 00:00:00 2001 From: Deon George Date: Wed, 12 Aug 2020 16:03:50 +1000 Subject: [PATCH] Initial commit with query/ident implemented --- README.md | 43 ++++ SQRL_logo_vector_outline.svg | 18 ++ composer.json | 29 +++ src/Models/Nonce.php | 20 ++ src/Models/Pubkey.php | 15 ++ src/SQRL.php | 216 ++++++++++++++++++ src/SQRL/Auth.php | 141 ++++++++++++ src/SQRL/Nonce.php | 102 +++++++++ src/SQRL/Pubkey.php | 39 ++++ src/SQRL/Response.php | 86 +++++++ src/SQRLController.php | 189 +++++++++++++++ src/SQRLServiceProvider.php | 30 +++ src/config/database.php | 14 ++ src/config/sqrl.php | 10 + .../2020_08_01_000000_create_sqrl_tables.php | 57 +++++ src/routes.php | 39 ++++ src/views/login.blade.php | 157 +++++++++++++ src/views/login_form.blade.php | 24 ++ 18 files changed, 1229 insertions(+) create mode 100644 README.md create mode 100644 SQRL_logo_vector_outline.svg create mode 100644 composer.json create mode 100644 src/Models/Nonce.php create mode 100644 src/Models/Pubkey.php create mode 100644 src/SQRL.php create mode 100644 src/SQRL/Auth.php create mode 100644 src/SQRL/Nonce.php create mode 100644 src/SQRL/Pubkey.php create mode 100644 src/SQRL/Response.php create mode 100644 src/SQRLController.php create mode 100644 src/SQRLServiceProvider.php create mode 100644 src/config/database.php create mode 100644 src/config/sqrl.php create mode 100644 src/database/migrations/2020_08_01_000000_create_sqrl_tables.php create mode 100644 src/routes.php create mode 100644 src/views/login.blade.php create mode 100644 src/views/login_form.blade.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..ab84440 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Leenooks\SQRL +Leenooks\SQRL is a laravel module aimed to enable SQRL login to your application. + +## Installation +To install, run + +`composer require leenooks/sqrl` + +## Configuration +### Laravel +[Not Documented Yet] + +### Lumen + +Add to your bootstrap.app + +`$app->register(Leenooks\SQRL\SQRLServiceProvider::class);` + +### Both +You'll then need to configure the following: +1. `.env` + + add the following variables: + + ``` + DB_CONNECTION=sqlite # Your databsae configuration (must the same as SQRL_DATABASE below) + ... + SQRL_DATABASE=sqlite # Points to the SQRL database connection + SQRL_URL_LOGIN=https://site/sqrl/login # URL to your login page (not used with LUMEN + SQRL_KEY_DOMAIN=site # URL to yours SQRL Server without http:// and https:// SQRL_API_ROUTE=/index.php/api/sqrl # Route to SQRL Server API + SQRL_NONCE_MAX_AGE_MINUTES=5 # Max age in minutes of the valid nonce + SQRL_NONCE_SALT=RANDOM # Generate a random salt value to calculate the nonce + ``` + +2. Create your SQLITE database + + If you havent done so already, create your SQLITE database (unless you are using own database configuration) + +3. run migrations + + `php artisan migrate` + +4. enjoy! diff --git a/SQRL_logo_vector_outline.svg b/SQRL_logo_vector_outline.svg new file mode 100644 index 0000000..e3370f2 --- /dev/null +++ b/SQRL_logo_vector_outline.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a23265c --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "leenooks/sqrl", + "description": "SQRL Authentication", + "type": "library", + "authors": [ + { + "name": "Deon George", + "email": "deon@leenooks.net" + } + ], + "require": { + "php": ">=7.2", + "ext-xml": "*", + "ext-sqlite": "*", + "simplesoftwareio/simple-qrcode": ">=2.0" + }, + "autoload": { + "psr-4": { + "Leenooks\\SQRL\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Leenooks\\SQRL\\LaravelServiceProvider" + ], + } + } +} diff --git a/src/Models/Nonce.php b/src/Models/Nonce.php new file mode 100644 index 0000000..c4d2770 --- /dev/null +++ b/src/Models/Nonce.php @@ -0,0 +1,20 @@ +connection = config('sqrl.database'); + + parent::__construct($attributes); + } + + public function pubkey() + { + return $this->belongsTo(Pubkey::class); + } +} diff --git a/src/Models/Pubkey.php b/src/Models/Pubkey.php new file mode 100644 index 0000000..1b9cfa3 --- /dev/null +++ b/src/Models/Pubkey.php @@ -0,0 +1,15 @@ +connection = config('sqrl.database'); + + parent::__construct($attributes); + } +} diff --git a/src/SQRL.php b/src/SQRL.php new file mode 100644 index 0000000..44f31e7 --- /dev/null +++ b/src/SQRL.php @@ -0,0 +1,216 @@ + 0x01, + 'PREV_ID_MATCH' => 0x02, + 'IP_MATCH' => 0x04, + 'SQRL_DISABLED' => 0x08, + 'UNSUPPORTED_FUNCTION' => 0x10, + 'TRANSIENT_ERROR' => 0x20, + 'COMMAND_FAILED' => 0x40, + 'CLIENT_FAILURE' => 0x80, + 'BAD_ID' => 0x100, + 'ID_SUPERSEDED' => 0x200, + ]; + + /** + * Base 64 Decode a URL encoded string + * + * @param string $url + * @return string + */ + public static function base64_decode_url(string $url): string + { + return base64_decode(str_replace(['-','_'],['+','/'],$url)); + } + + /** + * Base 64 Encode a URL + * + * @param string $url + * @return string|string[] + */ + public static function base64_encode_url(string $url): string + { + return str_replace(['+','/','='],['-','_',''],base64_encode($url)); + } + + /** + * Return a nonce used for authentication + * + * @return array + */ + public static function authNonce(): array + { + $url = config('sqrl.url'); + + $o = Nonce::createAuth('',$url); + $o->url = self::base64_encode_url(sprintf('%s?nut=%s',$url,$o->nonce)); + $o->save(); + + $route = sprintf('%s?nut=%s',config('sqrl.api_route'),$o->nonce); + $sqrl_url = sprintf('sqrl://%s',config('sqrl.domain').$route); + + return [ + 'nonce'=>$o->nonce, + 'check_state_on'=>$route, + 'sqrl_url'=>$sqrl_url, + 'sqrl_url_encoded'=>self::base64_encode_url(sprintf('%s&can=%s',$sqrl_url,$o->can)), + 'qrcode'=>SQRL::qrCode($sqrl_url,100), + ]; + } + + protected static function clientDecode(string $clientInput): array + { + $inputAsArray = explode("\n",self::base64_decode_url($clientInput)); + $result = collect(); + + foreach (array_filter($inputAsArray) as $individualInputs) { + if (strpos($individualInputs,'=') === FALSE) { + continue; + } + + list($key,$val) = explode("=", $individualInputs); + $val = trim($val); // Trim \r + + switch ($key) { + case 'cmd': + case 'btn': + case 'ver': + $result->put($key,$val); + break; + + case 'idk': + case 'ins': + case 'pidk': + case 'suk': + case 'vuk': + $result->put($key,self::base64_decode_url($val)); + break; + + case 'opt': + $result->put($key,explode('~',$val)); + break; + } + + } + + return $result->toArray(); + } + + public static function decodeData(array $data_encoded): array + { + $result = collect(); + + if (($x=Arr::get($data_encoded,'client'))) { + $result->put('client',self::clientDecode($x)); + } + + if (($x=Arr::get($data_encoded,'server'))) { + $result->put('server',self::serverDecode($x)); + } + + foreach (['ids','urs','pids'] as $key) { + if (($x=Arr::get($data_encoded,$key))) { + $result->put($key,self::base64_decode_url($x)); + } + } + + return $result->toArray(); + } + + protected static function serverDecode(string $serverData) + { + $decoded = self::base64_decode_url($serverData); + + if ((substr($decoded,0,7) === 'sqrl://') || (substr($decoded,0,6) === 'qrl://')) { + return $decoded; + } + + $parsedResult = collect(); + $serverValues = explode("\r\n",$decoded); + + foreach ($serverValues as $value) { + $splitStop = strpos($value,'='); + + $key = substr($value,0,$splitStop); + $val = substr($value,$splitStop+1); + + $parsedResult->put($key,$val); + } + + return $parsedResult->toArray(); + } + + /** + * Return a QRCode image + * + * @param string $nonce + * @param int $size + * @return string + */ + protected static function qrCode(string $nonce,int $size=100): string + { + return (new QRCode)->size($size)->generate($nonce); + } + + /** + * Get the TIF Code + * + * @param string $code + * @return int + */ + public static function tifcode(string $code): int + { + return Arr::get(self::$tif_codes,$code); + } + + public static function validateSignature(string $orig,string $pk,string $sig): bool + { + return (sodium_crypto_sign_open($sig.$orig, $pk) !== FALSE); + } + + public static function validateSignatures(array $data_received,array $decode_request): bool + { + $data_to_validate = Arr::get($data_received,'client').Arr::get($data_received,'server'); + + if (! self::validateSignature($data_to_validate,Arr::get($decode_request,'client.idk'),Arr::get($decode_request,'ids'))) { + return FALSE; + } + + if (Arr::get($decode_request,'urs') && Arr::get($decode_request,'client.vuk')) { + if (Arr::get($decode_request,'client.pidk')) { + $sqrl_pubkey = Pubkey::check(Arr::get($decode_request,'client.pidk')); + + if ($sqrl_pubkey && ! self::validateSignature($data_to_validate,$sqrl_pubkey->vuk,Arr::get($decode_request,'urs'))) { + return FALSE; + } + + } else if (! Arr::get($decode_request,'client.pidk') + && ! self::validateSignature($data_to_validate,Arr::get($decode_request,'client.vuk'),Arr::get($decode_request,'urs'))) + { + return FALSE; + } + } + + if (Arr::get($decode_request,'pids') + && Arr::get($decode_request,'client.pidk') + && ! self::validateSignature($data_to_validate,Arr::get($decode_request,'client.pidk'),Arr::get($decode_request,'pids'))) + { + return FALSE; + } + + return TRUE; + } +} diff --git a/src/SQRL/Auth.php b/src/SQRL/Auth.php new file mode 100644 index 0000000..fe24cc8 --- /dev/null +++ b/src/SQRL/Auth.php @@ -0,0 +1,141 @@ +nonce, $tif|SQRL::tifcode('UNSUPPORTED_FUNCTION'), $log_register); + break; + } + + return $response; + } + + /** + * SQRL Query from client + * + * @param array $client_decode + * @param string $server_decode + * @param Nonce $sqrl_nonce + * @param int $tif + * @return string + */ + protected static function processQuery(array $client_decode,string $server_decode,Nonce $sqrl_nonce,int $tif): string + { + $sqrl_pubkey = Pubkey::check(Arr::get($client_decode,'idk')); + + if ($sqrl_pubkey) { + $sqrl_nonce->pubkey_id = $sqrl_pubkey->id; + $sqrl_nonce->save(); + + Log::debug(__METHOD__,['pubkey'=>$sqrl_pubkey->id,'nonce'=>$sqrl_nonce->id]); + + if ($sqrl_pubkey->disabled == 1) { + return SQRL\Response::problem($sqrl_nonce->nonce,$tif|SQRL::tifcode('SQRL_DISABLED')); + } + + //@todo - extra options Arr::get($client_decode,'opt') + + $suk = in_array('suk',Arr::get($client_decode,'opt',[])) ? $sqrl_pubkey->suk : ''; + + return Response::respond( + $sqrl_nonce->nonce, + $tif|SQRL::tifcode('ID_MATCH'), + sprintf('%s?nut=%s',config('sqrl.api_route'),$sqrl_nonce->nonce), + '', + '', + 0, + $suk, + '', + ); + } + + return Response::respond( + $sqrl_nonce->nonce, + $tif, + sprintf('%s?nut=%s',config('sqrl.api_route'),$sqrl_nonce->nonce), + '', + '', + 0, + '', + '', + ); + } + + /** + * SQRL Identify request from client + * + * @param array $client_decode + * @param array $server_decode + * @param Nonce $sqrl_nonce + * @param int $tif + * @return string + */ + protected static function processIdent(array $client_decode,array $server_decode,Nonce $sqrl_nonce,int $tif): string + { + $sqrl_pubkey = Pubkey::check(Arr::get($client_decode,'idk')); + + if (! $sqrl_pubkey) { + + if (! Arr::get($client_decode,'suk') && ! Arr::get($client_decode,'vuk')) { + return SQRL\Response::problem($sqrl_nonce->nonce,$tif|SQRL::tifcode('CLIENT_FAILURE')); + } + + $sqrl_pubkey = Pubkey::create(Arr::get($client_decode,'idk'),Arr::get($client_decode,'suk'),Arr::get($client_decode,'vuk')); + + if (! $sqrl_pubkey) { + return SQRL\Response::problem($sqrl_nonce->nonce,$tif|SQRL::tifcode('TRANSIENT_ERROR')); + } + + } else { + $tif |= SQRL::tifcode('ID_MATCH'); + } + + // Check if the user has disabled their key previously. + if ($sqrl_pubkey->disabled == 1) { + return SQRL\Response::problem($sqrl_nonce->nonce,$tif|SQRL::tifcode('SQRL_DISABLED')); + } + + // Link this user to the nonce + if ((! $sqrl_nonce->pubkey_id) || $sqrl_nonce->pubkey_id !== $sqrl_pubkey->id) { + $sqrl_nonce->pubkey_id = $sqrl_pubkey->id; + } + + $sqrl_nonce->verified = 1; + $sqrl_nonce->save(); + + // @todo suk may need to be included in disabled response? + $suk = in_array('suk',Arr::get($client_decode,'opt',[])) ? $sqrl_pubkey->suk : ''; + $url = in_array('cps',Arr::get($client_decode,'opt',[])) ? SQRL::base64_decode_url($sqrl_nonce->url) : ''; + + //@todo - extra options Arr::get($client_decode,'opt') + + return Response::respond( + $sqrl_nonce->nonce, + $tif, + sprintf('%s?nut=%s',config('sqrl.api_route'),$sqrl_nonce->nonce), + $url, + '', + 0, + $suk, + '', + ); + } +} diff --git a/src/SQRL/Nonce.php b/src/SQRL/Nonce.php new file mode 100644 index 0000000..30b5a24 --- /dev/null +++ b/src/SQRL/Nonce.php @@ -0,0 +1,102 @@ +count() > 0); + + return $nonce; + } + + /** + * Create and Store a nonce for auth + * + * @param string $url + * @param string $can + * @return Model + */ + public static function createAuth(string $url,string $can): Model + { + $ip = Request::ip(); + $nonce = static::create($ip.'auth'); + + $o = new Model; + $o->type = 'auth'; + $o->nonce = $nonce; + $o->orig_nonce = $nonce; + $o->ip = $ip; + $o->url = SQRL::base64_encode_url($url); + $o->can = SQRL::base64_encode_url($can); + $o->save(); + + return $o; + } + + /** + * Check if our nonce is valid by checking if its in the DB + * + * @param string $nonce + * @return Model|null + */ + public static function check(string $nonce,string $key): ?Model + { + try { + $o = Model::where($key,$nonce)->firstOrFail(); + + } catch (ModelNotFoundException $e) { + return NULL; + } + + Log::debug(sprintf('A: %s, B: %s',$o->created_at->diffInMinutes(Carbon::now()),config('sqrl.nonce_age'))); + // Delete the old nonce + if ($o->created_at->diffInMinutes(Carbon::now()) > config('sqrl.nonce_age')) { + $o->delete(); + + return NULL; + } + + return $o; + } + + /** + * During auth, check if our nonce is valid and update it + * + * @param string $nonce + * @return Model|null + */ + public static function checkNonceValid(string $nonce): ?Model + { + $o = self::check($nonce,'nonce'); + + if ($o) { + $o->nonce = self::create(Request::ip().$o->id.$o->type); + + $o->save(); + } + + return $o; + } +} diff --git a/src/SQRL/Pubkey.php b/src/SQRL/Pubkey.php new file mode 100644 index 0000000..11a1e78 --- /dev/null +++ b/src/SQRL/Pubkey.php @@ -0,0 +1,39 @@ +first(); + } + + /** + * Store Public Key in DB + * + * @param $public_key + * @param $suk + * @param $vuk + * @return mixed + */ + public static function create(string $public_key,string $suk,string $vuk): Model + { + $o = new Model; + $o->public_key = SQRL::base64_encode_url($public_key); + $o->suk = SQRL::base64_encode_url($suk); + $o->vuk = SQRL::base64_encode_url($vuk); + + $o->save(); + return $o; + } +} diff --git a/src/SQRL/Response.php b/src/SQRL/Response.php new file mode 100644 index 0000000..cfbf55a --- /dev/null +++ b/src/SQRL/Response.php @@ -0,0 +1,86 @@ +header('Content-Length',strlen($response->getOriginalContent())) + ->header('Content-Type','application/x-www-form-urlencoded') + ->header('User-Agent','SQRL Server') + ->header('host',config('sqrl.domain')); + } + + /** + * Send back a problem response to the Client + * + * @param string $nonce + * @param int $tif + * @return string + */ + public static function problem(string $nonce,int $tif): string + { + return self::respond( + $nonce, + $tif, + sprintf('%s?nut=%s',config('sqrl.api_route'),$nonce), + '', + '', + 0, + '', + '' + ); + } + + /** + * Send back a response to the Client + * + * @param string $nonce + * @param int $tif + * @param string $query + * @param string $url + * @param string $can + * @param int $sin + * @param string $suk + * @param string $ask + * @return string + */ + public static function respond(string $nonce,int $tif,string $query,string $url='',string $can='',int $sin=0,string $suk='',string $ask=''): string + { + $result = "ver=1\r\n"; + $result .= sprintf("nut=%s\r\n",$nonce); + $result .= sprintf("tif=%s\r\n",strtoupper(dechex($tif))); + $result .= sprintf("qry=%s\r\n",$query); + + if ($url) + $result .= sprintf("url=%s\r\n",$url); + + if ($can) + $result .= sprintf("can=%s\r\n",$can); + + $result .= sprintf("sin=%d\r\n",$sin); + + if ($suk) + $result .= sprintf("suk=%s\r\n",SQRL::base64_encode_url($suk)); + + if ($ask) + $result .= sprintf("ask=%s\r\n",$ask); + + Log::debug(sprintf('Response [%s]',str_replace("\r\n",'.',$result))); + + return SQRL::base64_encode_url($result); + } +} diff --git a/src/SQRLController.php b/src/SQRLController.php new file mode 100644 index 0000000..29b7a89 --- /dev/null +++ b/src/SQRLController.php @@ -0,0 +1,189 @@ +get('nut')) { + + } + + // If this laravel, check if the user has been logged in + if (app() instanceof \Illuminate\Foundation\Application) { + // Laravel - check if the user is actually logged in + if (Auth::check()) { + return redirect()->intended(RouteServiceProvider::HOME); + + } else { + return view('login',SQRLAuth::authNonce()); + } + + } else { + // For JSON we just need the SQRL login + if ($request->expectsJson()) + return Arr::get(SQRLAuth::authNonce(),'sqrl_url'); + + return view('sqrl::login',SQRLAuth::authNonce()) + ->with('LUMEN',TRUE); + } + } + + public function api(Request $request): Response + { + Log::info(sprintf('API-start [%s]',$request->get('nut'))); + + $validatedData = $this->validate($request,[ + 'client' => 'required|string', + 'server' => 'required|string', + 'ids' => 'required|string', + 'nut' => sprintf('required|string|exists:%s.nonces,nonce',config('sqrl.database')), + 'urs' => 'string', + 'pids' => 'string' + ]); + + Log::debug(sprintf('API-client [%s]',$validatedData['client'])); + Log::debug(sprintf('API-server [%s]',$validatedData['server'])); + Log::debug(sprintf('API-ids [%s]',$validatedData['ids'])); + Log::debug(sprintf('API-urs [%s]',$validatedData['urs'] ?? '')); + Log::debug(sprintf('API-pids [%s]',$validatedData['pids'] ?? '')); + + $decode_request = SQRL::decodeData($validatedData); + $sqrl_nonce = SQRLAuth\Nonce::checkNonceValid($validatedData['nut']); + $tif = in_array('noiptest',Arr::get($decode_request,'client.opt')) ? 0 : SQRL::tifcode('IP_MATCH'); + + if (! $sqrl_nonce) { + Log::error('API:Nonce not valid',['n'=>$validatedData['nut'],'tif'=>SQRL::tifcode('CLIENT_FAILURE')]); + $response = SQRLAuth\Response::problem($validatedData['nut'],SQRL::tifcode('CLIENT_FAILURE')); + + } elseif (($sqrl_nonce->ip !== $request->ip()) && (! in_array('noiptest',Arr::get($decode_request,'client.opt')))) { + Log::error('API::IP Doesnt Match',['n'=>$validatedData['nut'],'tif'=>SQRL::tifcode('COMMAND_FAILED')]); + $response = SQRLAuth\Response::problem($sqrl_nonce->nonce,SQRL::tifcode('COMMAND_FAILED')); + + } elseif (SQRL::validateSignatures($validatedData,$decode_request) === FALSE) { + Log::error('API::Signature Failed',['n'=>$validatedData['nut'],'tif'=>$tif|SQRL::tifcode('CLIENT_FAILURE')]); + $response = SQRLAuth\Response::problem($sqrl_nonce->nonce,$tif|SQRL::tifcode('CLIENT_FAILURE')); + + } elseif (! Arr::get($decode_request,'client.cmd')) { + Log::error('API::Invalid Request',['n'=>$validatedData['nut'],'tif'=>$tif|SQRL::tifcode('CLIENT_FAILURE')]); + $response = SQRLAuth\Response::problem($sqrl_nonce->nonce,$tif|SQRL::tifcode("CLIENT_FAILURE")); + + } else { + foreach (['ver','cmd'] as $y) + Log::debug(sprintf('API-client-%s [%s]',str_pad($y,5,' '),Arr::get($decode_request,'client.'.$y))); + Log::debug(sprintf('API-client-opt [%s]',join('|',Arr::get($decode_request,'client.opt')))); + Log::debug(sprintf('API-client-idk [%s]',base64_encode(Arr::get($decode_request,'client.idk')))); + Log::debug(sprintf('API-server [%s]',serialize(Arr::get($decode_request,'server')))); + + Log::debug(sprintf('API-type [%s]',$sqrl_nonce->type)); + + switch ($sqrl_nonce->type) { + case 'auth': + $response = SQRLAuth\Auth::process(Arr::get($decode_request,'client'),Arr::get($decode_request,'server'),$sqrl_nonce,$tif); + break; + + case 'question': + $response = QuestionSQRLController::processRequestSQRL($decode_request["client"], $decode_request["server"], $sqrl_nonce, $validatedData["nut"], $tif); + break; + + default: + Log::error(__METHOD__.':Nonce Type Unsupported',['n'=>$validatedData['nut'],'tif'=>$tif|SQRL::tifcode('CLIENT_FAILURE')]); + $response = SQRLAuth\Response::problem($sqrl_nonce->nonce,$tif|SQRL::tifcode('COMMAND_FAILED')); + } + } + + return SQRLAuth\Response::create($response); + } + + /** + * Checks if nonce is valid, if nonce is valid the next page url + * or user response to a question is returned. + * + * @param Request $request + * @return JsonResponse + */ + public static function isReady(Request $request): JsonResponse + { + if ($request->get('nut')) { + $o = SQRLAuth\Nonce::check($request->get('nut'),'orig_nonce'); + + // If the nonce is old or doesnt exist. + if (! $o) { + return response()->json([ + 'isReady'=>FALSE, + 'msg'=>'Invalid Nonce, or Nonce expired' + ],200); + } + + // Validate the IP matches - since the request would come from the same device client + if ($o->ip !== $request->ip()) { + return response()->json([ + 'isReady'=>FALSE, + 'msg' => 'IP Mismatch', + ],200); + } + + // Has the nonce be validated + if ($o->verified != 1) { + return response()->json([ + 'isReady'=>FALSE, + 'msg'=>'Not Ready' + ],200); + } + + if ($o->pubkey && $o->pubkey->disabled) { + return response()->json([ + 'isReady'=>FALSE, + 'msg'=>'SQRL disabled for user' + ],200); + } + + switch ($o->type) { + case 'auth': + return response()->json([ + 'isReady'=>TRUE, + 'msg'=>'SQRL authenticated', + 'nextPage'=>SQRL::base64_decode_url($o->url), + ],200); + + case 'question': + break; + } + + } else { + return response()->json([ + 'isReady'=>FALSE, + 'msg'=>'Not Found!' + ],404); + } + } +} diff --git a/src/SQRLServiceProvider.php b/src/SQRLServiceProvider.php new file mode 100644 index 0000000..bafe16c --- /dev/null +++ b/src/SQRLServiceProvider.php @@ -0,0 +1,30 @@ +loadRoutesFrom(__DIR__.'/routes.php'); + $this->loadViewsFrom(__DIR__.'/views','sqrl'); + $this->loadMigrationsFrom(__DIR__.'/database/migrations'); + $this->mergeConfigFrom(__DIR__.'/config/database.php','database'); + $this->mergeConfigFrom(__DIR__.'/config/sqrl.php','sqrl'); + } +} diff --git a/src/config/database.php b/src/config/database.php new file mode 100644 index 0000000..32305ec --- /dev/null +++ b/src/config/database.php @@ -0,0 +1,14 @@ + [ + // Dont forget to touch database/sqrl.sqlite + 'sqrl' => [ + 'driver' => 'sqlite', + 'url' => env('SQRL_DATABASE_URL'), + 'database' => env('SQRL_DATABASE',database_path('sqrl.sqlite')), + 'prefix' => 'sqrl_', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS',true), + ], + ] +]; \ No newline at end of file diff --git a/src/config/sqrl.php b/src/config/sqrl.php new file mode 100644 index 0000000..c8784a2 --- /dev/null +++ b/src/config/sqrl.php @@ -0,0 +1,10 @@ +env('SQRL_API_ROUTE','/api/sqrl'), + 'domain'=>env('SQRL_KEY_DOMAIN', 'sqrl'), + 'database'=>env('SQRL_DATABASE','default'), + 'nonce_age'=>env('SQRL_NONCE_MAX_AGE_MINUTES',5), + 'salt'=>env('SQRL_NONCE_SALT','DEFAULT'), + 'url'=>env('SQRL_URL_LOGIN','https://sqrl/login'), +]; diff --git a/src/database/migrations/2020_08_01_000000_create_sqrl_tables.php b/src/database/migrations/2020_08_01_000000_create_sqrl_tables.php new file mode 100644 index 0000000..3bd8224 --- /dev/null +++ b/src/database/migrations/2020_08_01_000000_create_sqrl_tables.php @@ -0,0 +1,57 @@ +create('pubkeys',function (Blueprint $table) { + $table->bigIncrements('id'); + $table->timestamps(); + + $table->string('public_key')->unique(); + $table->string('vuk'); + $table->string('suk'); + $table->tinyInteger('disabled')->default(0); + $table->tinyInteger('sqrl_only_allowed')->default(0); + $table->tinyInteger('hardlock')->default(0); + }); + + Schema::connection(config('sqrl.database'))->create('nonces',function (Blueprint $table) { + $table->bigIncrements('id'); + $table->timestamps(); + + $table->string('nonce')->unique(); + $table->enum('type', ['auth', 'question']); + $table->ipAddress('ip'); + $table->longText('url'); + $table->longText('can'); + $table->tinyInteger('verified')->default(0); + $table->longText('question')->nullable(); + $table->tinyInteger('btn_answer')->nullable(); + $table->string('orig_nonce')->nullable(); + + $table->bigInteger('pubkey_id')->unsigned()->nullable(); + $table->foreign('pubkey_id')->references('id')->on('pubkeys')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::connection($this->connection)->dropIfExists('nonces'); + Schema::connection($this->connection)->dropIfExists('pubkeys'); + } +} diff --git a/src/routes.php b/src/routes.php new file mode 100644 index 0000000..f45aa90 --- /dev/null +++ b/src/routes.php @@ -0,0 +1,39 @@ +'sqrl','namespace'=>'Leenooks\SQRL'], function() { + // Return our Login URL + Route::get('/login','SQRLController@auth')->name('login'); + + // Perform login + Route::post('/login','SQRLController@@login'); + }); + + Route::group(['prefix'=>'api','namespace'=>'Leenooks\SQRL'], function() { + // Check if our nonce has been verified + Route::get('/sqrl','SQRLController@isReady'); + + // Route to the API + Route::post('/sqrl','SQRLController@api'); + }); + +} else { + // Lumen Routes + $this->app->router->group(['prefix' => 'sqrl','namespace'=>'\Leenooks\SQRL'], function ($router) { + // Return our Login URL + $router->get('/login','SQRLController@auth'); + + // Perform login + //$router->post('/login','SQRLController@api'); + }); + + $this->app->router->group(['prefix' => 'api','namespace'=>'\Leenooks\SQRL'], function ($router) { + // Check if our nonce has been verified + $router->get('/sqrl','SQRLController@isReady'); + + // Route to the API + $router->post('/sqrl','SQRLController@api'); + }); +} diff --git a/src/views/login.blade.php b/src/views/login.blade.php new file mode 100644 index 0000000..3a752c9 --- /dev/null +++ b/src/views/login.blade.php @@ -0,0 +1,157 @@ + + + + + + + + SQRL Login to {{ config('app.name') }} + + + + + + + + + + +
+
+
+ + +
+ @if (! isset($LUMEN)) + @includeIf('login_form'); + @else +

Login using your SQRL app.

+ @endif +
+
+ + +
+
+ + + + + + + + diff --git a/src/views/login_form.blade.php b/src/views/login_form.blade.php new file mode 100644 index 0000000..4f30a99 --- /dev/null +++ b/src/views/login_form.blade.php @@ -0,0 +1,24 @@ +
+
Login
+ + @if($errors->first()) + + @endif + +
+ {{ csrf_field() }} + +
+ + +
+ +
+ + +
+ + + Forgot Password
+
+