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