diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index bb9d032..499f0ce 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -2,13 +2,14 @@ namespace App\Http\Controllers\Auth; -use App\Http\Controllers\Controller; -use App\Providers\RouteServiceProvider; use Carbon\Carbon; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use App\Http\Controllers\Controller; +use App\Providers\RouteServiceProvider; + class LoginController extends Controller { /* @@ -38,7 +39,8 @@ class LoginController extends Controller */ public function __construct() { - $this->middleware('guest')->except('logout'); + $this->middleware('guest') + ->except('logout'); } public function login(Request $request) @@ -70,6 +72,7 @@ class LoginController extends Controller if (file_exists('login_note.txt')) $login_note = file_get_contents('login_note.txt'); - return view('auth.login')->with('login_note',$login_note); + return view('auth.login') + ->with('login_note',$login_note); } } diff --git a/app/Http/Requests/AddressMerge.php b/app/Http/Requests/AddressMerge.php index c3e2d12..69ad2da 100644 --- a/app/Http/Requests/AddressMerge.php +++ b/app/Http/Requests/AddressMerge.php @@ -14,7 +14,7 @@ class AddressMerge extends FormRequest public function authorize() { - return Gate::allows( 'admin'); + return Gate::allows('admin'); } public function rules(Request $request) diff --git a/app/Http/Requests/AreafixRequest.php b/app/Http/Requests/AreafixRequest.php index 35b0eca..f5f7cda 100644 --- a/app/Http/Requests/AreafixRequest.php +++ b/app/Http/Requests/AreafixRequest.php @@ -12,7 +12,7 @@ class AreafixRequest extends FormRequest { public function authorize() { - return Gate::allows( 'admin'); + return Gate::allows('admin'); } public function rules(Request $request) diff --git a/app/Http/Requests/DomainRequest.php b/app/Http/Requests/DomainRequest.php index 37abf28..2a3dfbf 100644 --- a/app/Http/Requests/DomainRequest.php +++ b/app/Http/Requests/DomainRequest.php @@ -12,7 +12,7 @@ class DomainRequest extends FormRequest { public function authorize(Domain $o) { - return Gate::allows( 'admin',$o); + return Gate::allows('admin',$o); } public function rules(Request $request) diff --git a/app/Http/Requests/SetupRequest.php b/app/Http/Requests/SetupRequest.php index 5660bd9..462a14e 100644 --- a/app/Http/Requests/SetupRequest.php +++ b/app/Http/Requests/SetupRequest.php @@ -10,7 +10,7 @@ class SetupRequest extends FormRequest { public function authorize() { - return Gate::allows( 'admin'); + return Gate::allows('admin'); } public function rules(Request $request) diff --git a/app/Http/Requests/UserRequest.php b/app/Http/Requests/UserRequest.php index 3331f06..8fc6a17 100644 --- a/app/Http/Requests/UserRequest.php +++ b/app/Http/Requests/UserRequest.php @@ -7,13 +7,11 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; use Illuminate\Validation\Rule; -use App\Models\User; - class UserRequest extends FormRequest { public function authorize() { - return Gate::allows( 'admin'); + return Gate::any(['admin','update'],$this->route('o')); } public function rules(Request $request) diff --git a/app/Models/User.php b/app/Models/User.php index 1aabe28..8495450 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -57,7 +57,8 @@ class User extends Authenticatable implements MustVerifyEmail */ protected $casts = [ 'email_verified_at' => 'datetime', - 'last_on' => 'datetime:Y-m-d H:i:s' + 'last_on' => 'datetime:Y-m-d H:i:s', + 'passkey' => 'json', ]; /* RELATIONS */ diff --git a/composer.json b/composer.json index f1538bc..8e48307 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "laravel/sanctum": "^3.2", "laravel/ui": "^4.0", "league/flysystem-aws-s3-v3": "^3.0", + "leenooks/passkey": "^0.1.0", "nunomaduro/laravel-console-summary": "^1.9", "rennokki/laravel-eloquent-query-cache": "^3.3", "repat/laravel-job-models": "^0.8", @@ -47,6 +48,10 @@ } }, "repositories": { + "passkey": { + "type": "vcs", + "url": "https://gitea.dege.au/laravel/passkey.git" + }, "laravel-console-summary": { "type": "vcs", "url": "https://github.com/leenooks/laravel-console-summary" diff --git a/composer.lock b/composer.lock index d96ac74..673ff9f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b9697fb5a34c3ed05cea0ce107b63d3f", + "content-hash": "0f0e0e75d37ebcbcbc03996fb1f3e18b", "packages": [ { "name": "aglipanci/laravel-eloquent-case", @@ -1700,6 +1700,51 @@ }, "time": "2023-05-09T19:47:28+00:00" }, + { + "name": "lbuchs/webauthn", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/lbuchs/WebAuthn.git", + "reference": "e73ff007e8a1099e72e0dbdd9d0884057409fc54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lbuchs/WebAuthn/zipball/e73ff007e8a1099e72e0dbdd9d0884057409fc54", + "reference": "e73ff007e8a1099e72e0dbdd9d0884057409fc54", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "lbuchs\\WebAuthn\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukas Buchs", + "role": "Developer" + } + ], + "description": "A simple PHP WebAuthn (FIDO2) server library", + "homepage": "https://github.com/lbuchs/webauthn", + "keywords": [ + "Authentication", + "webauthn" + ], + "support": { + "issues": "https://github.com/lbuchs/WebAuthn/issues", + "source": "https://github.com/lbuchs/WebAuthn/tree/v2.1.1" + }, + "time": "2024-01-15T15:46:57+00:00" + }, { "name": "league/commonmark", "version": "2.4.1", @@ -2160,6 +2205,47 @@ ], "time": "2023-10-17T14:13:20+00:00" }, + { + "name": "leenooks/passkey", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://gitea.dege.au/laravel/passkey.git", + "reference": "4872d55ed65863e03102eea1046504e630f80ae2" + }, + "require": { + "lbuchs/webauthn": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Leenooks\\Passkey\\PasskeyServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Leenooks\\Passkey\\": "src" + } + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Deon George", + "email": "deon@dege.au" + } + ], + "description": "Leenooks Laravel Passkey Implementation", + "keywords": [ + "dege", + "laravel", + "passkey" + ], + "time": "2024-04-25T05:05:51+00:00" + }, { "name": "masterminds/html5", "version": "2.8.1", diff --git a/public/passkey/passkey.js b/public/passkey/passkey.js new file mode 100644 index 0000000..8cc55c2 --- /dev/null +++ b/public/passkey/passkey.js @@ -0,0 +1,225 @@ +/* + * Passkey Implementation + */ +let passkey_debug = false; + +/** + * Convert a ArrayBuffer to Base64 + * @param {ArrayBuffer} buffer + * @returns {String} + */ +function arrayBufferToBase64(buffer) { + let binary = ''; + let bytes = new Uint8Array(buffer); + let len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode( bytes[ i ] ); + } + return window.btoa(binary); +} + +/** + * convert RFC 1342-like base64 strings to array buffer + * @param {mixed} obj + * @returns {undefined} + */ +function recursiveBase64StrToArrayBuffer(obj) { + let prefix = '=?BINARY?B?'; + let suffix = '?='; + if (typeof obj === 'object') { + for (let key in obj) { + if (typeof obj[key] === 'string') { + let str = obj[key]; + if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) { + str = str.substring(prefix.length, str.length - suffix.length); + + let binary_string = window.atob(str); + let len = binary_string.length; + let bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + obj[key] = bytes.buffer; + } + } else { + recursiveBase64StrToArrayBuffer(obj[key]); + } + } + } +} + +function passkey_check_browser() +{ + // check browser support + if ((! window.fetch) || (! navigator.credentials) || (! navigator.credentials.create)) + throw new Error('Browser not supported.'); + + /* + // Availability of `window.PublicKeyCredential` means WebAuthn is usable. + // `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable. + // `isConditionalMediationAvailable` means the feature detection is usable. + if (window.PublicKeyCredential && + PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable && + PublicKeyCredential.isConditionalMediationAvailable) { + // Check if user verifying platform authenticator is available. + Promise.all([ + PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(), + PublicKeyCredential.isConditionalMediationAvailable(), + ]).then(results => { + if (results.every(r => r === true)) { + // Display "Create a new passkey" button + } + }); + } + */ + + if (passkey_debug) + console.log('Passkey: Browser OK'); + + return true; +} + +/** + * Register/Create a passkey for a user + */ +async function passkey_register(csrf_token,icon_dom,icon,icon_shell_success,icon_shell_fail) +{ + try { + if (! passkey_check_browser()) + return; + + // Change our icon so that it is obvious we are doing something + icon_dom.find('i').removeClass(icon).addClass('spinner-grow spinner-grow-sm'); + + // Get our arguments + var createArgs; + $.ajax({ + url: '/passkey/register', + type: 'GET', + dataType: 'json', + async: false, + cache: false, + success: function(data) { + if (passkey_debug) + console.log('Passkey: Get Register Success'); + + recursiveBase64StrToArrayBuffer(data); + createArgs = data; + }, + error: function(e,status,error) { + throw new Error(status || 'Unknown error occurred'); + } + }); + + // Create credentials + try { + const cred = await navigator.credentials.create(createArgs); + + const authenticatorAttestationResponse = { + id: cred.id, + rawId: arrayBufferToBase64(cred.rawId), + transports: cred.response.getTransports ? cred.response.getTransports() : null, + clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, + attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null, + authenticatorAttachment: cred.authenticatorAttachment, + _token: csrf_token, + }; + + $.ajax({ + url: '/passkey/check', + type: 'POST', + data: authenticatorAttestationResponse, + cache: false, + success: function(data) { + if (passkey_debug) + console.log('Passkey: Registration Success'); + + icon_dom.find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm'); + icon_dom.removeClass('btn-outline-primary').addClass('btn-primary'); + }, + error: function(e,status,error) { + throw new Error(status || 'Unknown error occurred'); + } + }); + + } catch (status) { + if (passkey_debug) + console.log(status || 'Passkey: User Aborted Register'); + + // Restore the icon + icon_dom.find('i').addClass(icon).removeClass('spinner-grow spinner-grow-sm'); + + return; + } + + } catch (err) { + window.alert(err || 'An UNKNOWN error occurred?'); + } +} + +/** + * Check a passkey being presented + */ +async function passkey_check(csrf_token,redirect) +{ + if (passkey_debug) + console.log('Passkey: Check User Passkey'); + + try { + if (! passkey_check_browser()) + return; + + // Get our arguments + var getArgs; + $.ajax({ + url: '/passkey/get', + type: 'GET', + dataType: 'json', + async: false, + cache: false, + success: function(data) { + if (passkey_debug) + console.log('Passkey: Get Args Success'); + + recursiveBase64StrToArrayBuffer(data); + getArgs = data; + }, + error: function(e,status,error) { + throw new Error(status || 'Unknown error occurred'); + } + }); + + // check credentials with hardware + const cred = await navigator.credentials.get(getArgs); + + // create object for transmission to server + const authenticatorAttestationResponse = { + id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, + clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, + authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, + signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null, + userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null, + _token: csrf_token + }; + + $.ajax({ + url: '/passkey/process', + type: 'POST', + data: authenticatorAttestationResponse, + cache: false, + success: function(data) { + if (passkey_debug) + console.log('Passkey: Process Success'); + + // Direct to the home page + window.location.href = (redirect !== undefined) ? redirect : '/'; + }, + error: function(e,status,error) { + throw new Error(status || 'Unknown error occurred'); + } + }); + + } catch (err) { + window.alert(err || 'An UNKNOWN error occurred?'); + } +} \ No newline at end of file diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index f6f95b6..bfb4908 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -5,7 +5,7 @@ @endsection @section('content') - @if(isset($login_note) AND $login_note) + @if(isset($login_note) && $login_note)