Initial commit with query/ident implemented

This commit is contained in:
Deon George 2020-08-12 16:03:50 +10:00
commit 60d0519583
18 changed files with 1229 additions and 0 deletions

43
README.md Normal file
View File

@ -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!

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="2400px" height="2400px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 2400 2400"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:black}
]]>
</style>
</defs>
<g id="Capa_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<path class="fil0" d="M213.938 2365.35c0,-99.017 80.2649,-179.287 179.283,-179.287l137.001 0c-85.1817,-95.1585 -137.001,-220.803 -137.001,-358.568l0 -448.21 -179.283 0c-49.5096,0 -89.6448,-40.1352 -89.6448,-89.6404 0,-49.5085 40.1352,-89.6437 89.6448,-89.6437l179.283 0 0 -179.284 -89.6426 0c-148.522,0 -268.928,-120.406 -268.928,-268.928 0,-297.048 240.807,-537.855 537.853,-537.855 0,-99.0136 80.2716,-179.284 179.285,-179.284l0 179.284 89.6426 0c31.4357,0 61.5996,5.41251 89.6404,15.326l0 -15.326c0,-99.0136 80.2716,-179.284 179.287,-179.284l0 896.423c0,123.74 50.1821,235.792 131.301,316.911l43.429 43.429c113.571,113.571 183.838,270.444 183.838,443.655l0 361.354c0,49.5085 40.1341,89.6404 89.6448,89.6404l268.922 0c198.035,0 358.573,-160.533 358.573,-358.568l0 -537.85c0,-49.5085 -40.1352,-89.6437 -89.6448,-89.6437l-537.85 0c-148.527,0 -268.928,-120.401 -268.928,-268.928l0 -358.567c0,-297.049 240.803,-537.856 537.85,-537.856 297.048,0 537.856,240.807 537.856,537.856l0 89.6393c0,99.0181 -80.2705,179.287 -179.283,179.287l0 -268.927c0,-198.035 -160.538,-358.572 -358.573,-358.572 -198.031,0 -358.567,160.537 -358.567,358.572l0 358.567c0,49.5085 40.1341,89.6437 89.6448,89.6437l537.85 0c148.525,0 268.928,120.402 268.928,268.928l0 537.85c0,297.049 -240.808,537.856 -537.856,537.856l-1613.56 0zm1091.03 -179.287l0 0c-9.91237,-28.0408 -15.3215,-58.2047 -15.3215,-89.6404l0 -361.354c0,-123.738 -50.1843,-235.792 -131.301,-316.91l-43.429 -43.4279c-113.573,-113.573 -183.843,-270.446 -183.843,-443.658l0 -448.211c0,-49.5085 -40.1308,-89.6404 -89.6404,-89.6404l-268.928 0c-198.031,0 -358.566,160.537 -358.566,358.567 0,49.5096 40.1308,89.6437 89.6404,89.6437l134.463 0c74.2633,0 134.462,60.2012 134.462,134.461l0 851.601c0,198.035 160.538,358.568 358.568,358.568l373.894 0zm-732.462 -1680.79l0 0c-49.5085,0 -89.6426,40.1352 -89.6426,89.6437 0,49.5085 40.1341,89.6437 89.6426,89.6437 49.5096,0 89.6448,-40.1352 89.6448,-89.6437 0,-49.5085 -40.1352,-89.6437 -89.6448,-89.6437zm1254.99 918.836l0 0c-74.2588,0 -134.462,60.2012 -134.462,134.46 0,55.4501 33.5655,103.053 81.4833,123.618l-81.4833 190.132 268.927 0 -81.4844 -190.132c47.9189,-20.5651 81.4844,-68.1682 81.4844,-123.618 0,-74.2588 -60.2001,-134.46 -134.464,-134.46z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

29
composer.json Normal file
View File

@ -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"
],
}
}
}

20
src/Models/Nonce.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace Leenooks\SQRL\Models;
use Illuminate\Database\Eloquent\Model;
class Nonce extends Model
{
public function __construct(array $attributes = [])
{
$this->connection = config('sqrl.database');
parent::__construct($attributes);
}
public function pubkey()
{
return $this->belongsTo(Pubkey::class);
}
}

15
src/Models/Pubkey.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace Leenooks\SQRL\Models;
use Illuminate\Database\Eloquent\Model;
class Pubkey extends Model
{
public function __construct(array $attributes = [])
{
$this->connection = config('sqrl.database');
parent::__construct($attributes);
}
}

216
src/SQRL.php Normal file
View File

@ -0,0 +1,216 @@
<?php
namespace Leenooks\SQRL;
use Illuminate\Support\Arr;
use SimpleSoftwareIO\QrCode\Generator as QRCode;
use Leenooks\SQRL\SQRL\Nonce;
use Leenooks\SQRL\SQRL\Pubkey;
class SQRL
{
//TIF Codes
protected static $tif_codes = [
'ID_MATCH' => 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;
}
}

141
src/SQRL/Auth.php Normal file
View File

@ -0,0 +1,141 @@
<?php
namespace Leenooks\SQRL\SQRL;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Leenooks\SQRL\Models\Nonce;
use Leenooks\SQRL\SQRL;
class Auth
{
public static function process(array $client_decode,$server_decode,Nonce $sqrl_nonce,int $tif): string
{
switch (Arr::get($client_decode,'cmd')) {
case 'query':
$response = self::processQuery($client_decode,$server_decode,$sqrl_nonce,$tif);
break;
case "ident":
$response = self::processIdent($client_decode,$server_decode,$sqrl_nonce,$tif);
break;
default:
$response = SQRL\Response::problem($sqrl_nonce->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,
'',
);
}
}

102
src/SQRL/Nonce.php Normal file
View File

@ -0,0 +1,102 @@
<?php
namespace Leenooks\SQRL\SQRL;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request;
use Leenooks\SQRL\Models\Nonce as Model;
use Leenooks\SQRL\SQRL;
class Nonce
{
protected static $alg = 'sha256';
/**
* Create a new unique nonce
*
* @param string $source
* @return string|null
*/
protected static function create(string $source)
{
$nonce = NULL;
do {
$nonce = hash_hmac(self::$alg,uniqid($source,true),config('sqrl.salt'));
} while (Model::where('nonce','=',$nonce)->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;
}
}

39
src/SQRL/Pubkey.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace Leenooks\SQRL\SQRL;
use Leenooks\SQRL\SQRL;
use Leenooks\SQRL\Models\Pubkey as Model;
class Pubkey
{
/**
* Check Pubkey exists from a previous session
*
* @param string $public_key
* @return Model|null
*/
public static function check(string $public_key): ?Model
{
return Model::where('public_key',SQRL::base64_encode_url($public_key))->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;
}
}

86
src/SQRL/Response.php Normal file
View File

@ -0,0 +1,86 @@
<?php
namespace Leenooks\SQRL\SQRL;
use Illuminate\Support\Facades\Log;
use Leenooks\SQRL\SQRL;
class Response
{
/**
* Create the HTTP response back to the client
*
* @param string $response
* @return \Illuminate\Http\Response
*/
public static function create(string $response): \Illuminate\Http\Response
{
$response = response($response,200);
return $response
->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);
}
}

189
src/SQRLController.php Normal file
View File

@ -0,0 +1,189 @@
<?php
namespace Leenooks\SQRL;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Leenooks\SQRL\SQRL as SQRLAuth;
/**
* Class SQRLController
* @package Leenooks\SQRL
* @todo CHECK THAT WE ARE RECEIVING BACK WHAT WE GIVE TO THE CLIENT
* * QUERY: = sqrl url, eg: sqrl://domain/api/sqrl?nut=84cc3ef3b58b01dbe22931d1ceabdd6be2c27a481516755757c53b0162287bb8
* * IDENT: = RESPONSE TO QUERY
* @todo JOB TO DELETE OLD NONCES
*/
class SQRLController extends Controller
{
/**
* Return our login page
*
* @param Request $request
* @return array|View
*/
public function auth(Request $request)
{
// Validate the nonce if it has been given.
if ($request->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);
}
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Leenooks\SQRL;
class SQRLServiceProvider extends \Illuminate\Support\ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
$this->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');
}
}

14
src/config/database.php Normal file
View File

@ -0,0 +1,14 @@
<?php
return [
'connections' => [
// 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),
],
]
];

10
src/config/sqrl.php Normal file
View File

@ -0,0 +1,10 @@
<?php
return [
'api_route'=>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'),
];

View File

@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSQRLTables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::connection(config('sqrl.database'))->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');
}
}

39
src/routes.php Normal file
View File

@ -0,0 +1,39 @@
<?php
// If this laravel, check if the user has been logged in
if (app() instanceof \Illuminate\Foundation\Application) {
// Laravel Routes
Route::group(['prefix'=>'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');
});
}

157
src/views/login.blade.php Normal file
View File

@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SQRL Login to {{ config('app.name') }}</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.0/css/bootstrap.min.css" integrity="sha384-SI27wrMjH3ZZ89r4o+fGIJtnzkAnFs3E4qz9DIYioCQ5l9Rd/7UAa8DHcaL8jkWt" crossorigin="anonymous">
<!-- Styles -->
<style>
body {
height: 90vh;
width: 100%;
background: linear-gradient(to bottom, rgba(255,255,255,0.15) 0%, rgba(0,0,0,0.15) 100%), radial-gradient(at top center, rgba(255,255,255,0.40) 0%, rgba(0,0,0,0.40) 120%) #989898;
background-blend-mode: multiply,multiply;
}
.container {
height: 100%;
width: 100%;
margin-top: 10vh;
}
.card {
background-color: #e7e7e7;
border: 2px solid #565656;
border-radius: 15px;
max-width: 400px;
}
.sqrl-logo {
height: 100px !important;
width: 100px !important;
margin: 30px;
margin-bottom: 15px;
padding: 10px;
background-color: #007cc3;
border-radius: 15px;
}
.sqrl-qr {
margin: 30px;
margin-top: 15px;
}
</style>
</head>
<body>
<div class="container">
<div class="card mx-auto my-auto">
<div class="row">
<div class="col-md-4">
<a class="mx-auto" id="sqrl" href="{{ $sqrl_url }}" onclick="sqrlLinkClick(this);return true;" sqrl-url-encoded="{{ $sqrl_url_encoded }}" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" class="sqrl-logo" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" viewBox="0 0 2400 2400" xmlns:xlink="http://www.w3.org/1999/xlink">
<path style="fill:white;" d="M213.938 2365.35c0,-99.017 80.2649,-179.287 179.283,-179.287l137.001 0c-85.1817,-95.1585 -137.001,-220.803 -137.001,-358.568l0 -448.21 -179.283 0c-49.5096,0 -89.6448,-40.1352 -89.6448,-89.6404 0,-49.5085 40.1352,-89.6437 89.6448,-89.6437l179.283 0 0 -179.284 -89.6426 0c-148.522,0 -268.928,-120.406 -268.928,-268.928 0,-297.048 240.807,-537.855 537.853,-537.855 0,-99.0136 80.2716,-179.284 179.285,-179.284l0 179.284 89.6426 0c31.4357,0 61.5996,5.41251 89.6404,15.326l0 -15.326c0,-99.0136 80.2716,-179.284 179.287,-179.284l0 896.423c0,123.74 50.1821,235.792 131.301,316.911l43.429 43.429c113.571,113.571 183.838,270.444 183.838,443.655l0 361.354c0,49.5085 40.1341,89.6404 89.6448,89.6404l268.922 0c198.035,0 358.573,-160.533 358.573,-358.568l0 -537.85c0,-49.5085 -40.1352,-89.6437 -89.6448,-89.6437l-537.85 0c-148.527,0 -268.928,-120.401 -268.928,-268.928l0 -358.567c0,-297.049 240.803,-537.856 537.85,-537.856 297.048,0 537.856,240.807 537.856,537.856l0 89.6393c0,99.0181 -80.2705,179.287 -179.283,179.287l0 -268.927c0,-198.035 -160.538,-358.572 -358.573,-358.572 -198.031,0 -358.567,160.537 -358.567,358.572l0 358.567c0,49.5085 40.1341,89.6437 89.6448,89.6437l537.85 0c148.525,0 268.928,120.402 268.928,268.928l0 537.85c0,297.049 -240.808,537.856 -537.856,537.856l-1613.56 0zm1091.03 -179.287l0 0c-9.91237,-28.0408 -15.3215,-58.2047 -15.3215,-89.6404l0 -361.354c0,-123.738 -50.1843,-235.792 -131.301,-316.91l-43.429 -43.4279c-113.573,-113.573 -183.843,-270.446 -183.843,-443.658l0 -448.211c0,-49.5085 -40.1308,-89.6404 -89.6404,-89.6404l-268.928 0c-198.031,0 -358.566,160.537 -358.566,358.567 0,49.5096 40.1308,89.6437 89.6404,89.6437l134.463 0c74.2633,0 134.462,60.2012 134.462,134.461l0 851.601c0,198.035 160.538,358.568 358.568,358.568l373.894 0zm-732.462 -1680.79l0 0c-49.5085,0 -89.6426,40.1352 -89.6426,89.6437 0,49.5085 40.1341,89.6437 89.6426,89.6437 49.5096,0 89.6448,-40.1352 89.6448,-89.6437 0,-49.5085 -40.1352,-89.6437 -89.6448,-89.6437zm1254.99 918.836l0 0c-74.2588,0 -134.462,60.2012 -134.462,134.46 0,55.4501 33.5655,103.053 81.4833,123.618l-81.4833 190.132 268.927 0 -81.4844 -190.132c47.9189,-20.5651 81.4844,-68.1682 81.4844,-123.618 0,-74.2588 -60.2001,-134.46 -134.464,-134.46z"/>
</svg>
<div class="sqrl-qr">{!! $qrcode !!}</div>
</a>
</div>
<div class="col-md-8">
@if (! isset($LUMEN))
@includeIf('login_form');
@else
<h1 class="m-3 pt-5 text-center">Login using your SQRL app.</h1>
@endif
</div>
</div>
<div class="card-footer">
<div class="alert alert-danger" role="alert" id="error_message" hidden></div>
</div>
</div>
</div>
</body>
<script>
var syncQuery = window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('MSXML2.XMLHTTP.3.0');
var encodedSqrlUrl = false, sqrlScheme = true;
var gifProbe = new Image(); // create an instance of a memory-based probe image
var localhostRoot = 'http://localhost:25519/'; // the SQRL client listener
const poll = 500;
// define our load-success function
gifProbe.onload = function() {
sqrlScheme = false; // prevent retriggering of the SQRL QR code.
document.location.href = localhostRoot+encodedSqrlUrl;
};
// define our load-failure function
gifProbe.onerror = function() {
setTimeout(function() {
gifProbe.src = localhostRoot+Date.now()+'.gif';
},250);
};
// Poll to see if authentication has proceeded
function pollForNextPage() {
if (document.hidden) { // before probing for any page change, we check to
setTimeout(pollForNextPage,poll); // see whether the page is visible. If the user is
return; // not viewing the page, check again in 5 seconds.
}
syncQuery.open('GET','{{ $check_state_on }}'); // the page is visible, so let's check for any update
syncQuery.onreadystatechange = function() {
if (syncQuery.readyState === 4 ) {
if (syncQuery.status === 200 ) {
var response = JSON.parse(syncQuery.response);
if (response.isReady) {
document.location.href = response.nextPage;
} else {
if (response.msg === "Invalid Nonce, or Nonce expired"
|| response.msg === "IP Mismatch"
|| response.msg === "SQRL disabled for user")
{
var div = document.getElementById('error_message');
div.innerHTML = "<string>"+response.msg+"</strong><br><small>QR Code needs to be refresh - reload the page.<small>";
div.removeAttribute("hidden");
} else {
setTimeout(pollForNextPage,poll);
}
}
} else {
setTimeout(pollForNextPage,poll);
}
}
};
// Send our request to check authenticated status
syncQuery.send();
}
// if we have an encoded URL to jump to, initiate our GIF probing before jumping
function sqrlLinkClick(e) {
encodedSqrlUrl = e.getAttribute('encoded-sqrl-url');
if (encodedSqrlUrl) {
gifProbe.onerror();
}
}
pollForNextPage();
</script>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.0/js/bootstrap.min.js" integrity="sha384-3qaqj0lc6sV/qpzrc1N5DC6i1VRn/HyX4qdPaiEFbn54VjQBEU341pvjz7Dv3n6P" crossorigin="anonymous"></script>
</html>

View File

@ -0,0 +1,24 @@
<div class="card-body">
<h5 class="card-title">Login</h5>
@if($errors->first())
<div class="alert alert-danger" role="alert">{{$errors->first()}}</div>
@endif
<form method="post" action="/login">
{{ csrf_field() }}
<div class="form-group">
<label for="email">Username</label>
<input name="email" type="text" class="form-control" id="email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input name="password" type="password" class="form-control" id="password">
</div>
<button type="submit" class="btn btn-dark">Login</button>
<a href="/resetpw" class="btn btn-link text-dark">Forgot Password</a></br>
</form>
</div>