Initial commit with query/ident implemented
This commit is contained in:
commit
60d0519583
43
README.md
Normal file
43
README.md
Normal 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!
|
18
SQRL_logo_vector_outline.svg
Normal file
18
SQRL_logo_vector_outline.svg
Normal 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
29
composer.json
Normal 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
20
src/Models/Nonce.php
Normal 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
15
src/Models/Pubkey.php
Normal 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
216
src/SQRL.php
Normal 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
141
src/SQRL/Auth.php
Normal 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
102
src/SQRL/Nonce.php
Normal 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
39
src/SQRL/Pubkey.php
Normal 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
86
src/SQRL/Response.php
Normal 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
189
src/SQRLController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
30
src/SQRLServiceProvider.php
Normal file
30
src/SQRLServiceProvider.php
Normal 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
14
src/config/database.php
Normal 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
10
src/config/sqrl.php
Normal 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'),
|
||||
];
|
@ -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
39
src/routes.php
Normal 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
157
src/views/login.blade.php
Normal 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>
|
24
src/views/login_form.blade.php
Normal file
24
src/views/login_form.blade.php
Normal 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>
|
Loading…
Reference in New Issue
Block a user