Skip to content
This repository has been archived by the owner on Jun 13, 2022. It is now read-only.

Commit

Permalink
Merge 83ae8b2 into e3463a3
Browse files Browse the repository at this point in the history
  • Loading branch information
DarkGhostHunter committed Jul 10, 2020
2 parents e3463a3 + 83ae8b2 commit 0f47436
Show file tree
Hide file tree
Showing 48 changed files with 2,640 additions and 184 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ build
composer.lock
docs
vendor
coverage
coverage
.idea
225 changes: 177 additions & 48 deletions README.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions config/larapass.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,17 @@
*/

'fallback' => true,

/*
|--------------------------------------------------------------------------
| Device Confirmation
|--------------------------------------------------------------------------
|
| If you're using the "webauthn.confirm" middleware in your routes you may
| want to adjust the time the confirmation is remembered in the browser.
| This is measured in seconds, but it can be overridden in the route.
|
*/

'confirm_timeout' => 10800, // 3 hours
];
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use DarkGhostHunter\Larapass\Eloquent\WebAuthnCredential;

class CreateWebAuthnCredentialsTable extends Migration
class CreateWebAuthnTables extends Migration
{
/**
* Run the migrations.
Expand Down Expand Up @@ -39,6 +39,12 @@ public function up()

$table->primary(['id', 'user_id']);
});

Schema::create('web_authn_recoveries', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}

/**
Expand All @@ -49,5 +55,6 @@ public function up()
public function down()
{
Schema::dropIfExists('web_authn_authentications');
Schema::dropIfExists('web_authn_recoveries');
}
}
17 changes: 13 additions & 4 deletions resources/js/larapass.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,16 +232,25 @@ class Larapass
* Throws the entire response if is not OK (HTTP 2XX).
*
* @param response {Response}
* @returns Promise<any>
* @returns Promise<JSON|ArrayBuffer|ArrayBufferView|Blob|FormData|string|URLSearchParams>
* @throws Response
*/
static #handleResponse(response)
{
if (response.ok) {
return response.json();
if (! response.ok) {
throw response;
}

throw response;
// Here we will do a small trick. Since most of the responses from the server
// are JSON, we will automatically parse the JSON body from the response. If
// it's not JSON, we will push the body verbatim and let the dev handle it.
return new Promise(resolve => {
if (response) {
response.json().then(json => resolve(json)).catch(() => resolve(response.body))
} else {
resolve(response.body)
}
})
}

/**
Expand Down
6 changes: 6 additions & 0 deletions resources/lang/en/confirm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

return [
'title' => 'Please confirm with your device before continuing',
'button' => 'Confirm'
];
24 changes: 24 additions & 0 deletions resources/lang/en/recovery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

return [
'title' => 'Account recovery',

'description' => 'If you can\'t login with your device, you can register another by opening an email there.',
'details' => 'Ensure you open the email on a device you fully own.',

'instructions' => 'Press the button to use this device for your account and follow your the instructions.',
'unique' => 'Disable all others devices except this.',

'button' => [
'send' => 'Send account recovery',
'register' => 'Register this device',
],

'sent' => 'If the email is correct, you should receive an email with a recovery link shortly.',
'attached' => 'A new device has been attached to your account to authenticate.',
'user' => 'We can\'t find a user with that email address.',
'token' => 'The token is invalid or has expired.',
'throttled' => 'Please wait before retrying.',

'failed' => 'The recovery failed. Try again.',
];
36 changes: 36 additions & 0 deletions resources/views/confirm.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@extends('larapass::layout')

@section('title', __('Authenticator confirmation'))

@section('body')
<form id="form">
<h2 class="card-title h5 text-center">{{ __('Please confirm with your device before continuing') }}</h2>
<hr>
<div class="text-center">
<button type="submit" class="btn btn-primary btn-lg">
{{ __('Confirm') }}
</button>
</div>
</form>
@endsection

@push('scripts')
<script src="{{ asset('vendor/larapass/js/larapass.js') }}"></script>
<script>
const larapass = new Larapass({
login: '/webauthn/confirm',
loginOptions: '/webauthn/confirm/options'
});
document.getElementById('form').addEventListener('submit', function (event) {
event.preventDefault()
larapass.login()
.then(response => window.location.replace(response.redirectTo))
.catch(response => {
alert('{{ __('Confirmation unsuccessful, try again!') }}')
console.error('Confirmation unsuccessful', response);
})
})
</script>
@endpush
41 changes: 41 additions & 0 deletions resources/views/layout.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="{{ config('app.locale') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<title>@yield('title')</title>
<style>
#box-container {
min-height: 100vh;
}
#box {
margin-bottom: 6rem;
}
.cool-shadow {
box-shadow: 0 2.8px 2.2px rgba(0, 0, 0, 0.1),
0 6.7px 5.3px rgba(0, 0, 0, 0.072),
0 12.5px 10px rgba(0, 0, 0, 0.06),
0 22.3px 17.9px rgba(0, 0, 0, 0.05),
0 41.8px 33.4px rgba(0, 0, 0, 0.04),
0 100px 80px rgba(0, 0, 0, 0.028);
}
</style>
</head>
<body class="bg-light">
<div class="container">
<div id="box-container" class="row justify-content-center align-items-center">
<div id="form-container" class="col-lg-6 col-md-8 col-sm-10 col-12">
<div id="box" class="card border-0 cool-shadow">
<section class="card-body">
@yield('body')
</section>
</div>
</div>
</div>
</div>
@stack('scripts')
</body>
</html>
33 changes: 33 additions & 0 deletions resources/views/lost.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@extends('larapass::layout')

@section('title', trans('larapass::recovery.title'))

@section('body')
<form id="form" action="{{ route('webauthn.lost.send') }}" method="post">
@csrf
<h2 class="card-title h5 text-center">{{ trans('larapass::recovery.title') }}</h2>
<hr>
<p>{{ trans('larapass::recovery.description') }}</p>
@if($errors->any())
<div class="alert alert-danger small">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@elseif(session('status'))
<div class="alert alert-success small">
{{ session('status') }}
</div>
@endif
<div class="form-group pb-3">
<label for="email">Email</label>
<input id="email" type="email" name="email" class="form-control" placeholder="john.doe@mail.com" required>
<small class="form-text text-muted">{{ trans('larapass::recovery.details') }}</small>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary btn-lg">{{ trans('larapass::recovery.button.send') }}</button>
</div>
</form>
@endsection
60 changes: 60 additions & 0 deletions resources/views/recover.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@extends('larapass::layout')

@section('title', trans('larapass::recovery.title'))

@section('body')
<form id="form">
<input type="hidden" name="email" value="{{ $email }}">
<input type="hidden" name="token" value="{{ $token }}">
<h2 class="card-title h5 text-center">{{ trans('larapass::recovery.title') }}</h2>
<hr>
<p>{{ trans('larapass::recovery.instructions') }}</p>
@if ($errors->any())
<div class="alert alert-danger small">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="form-group text-center">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="unique">
<label class="custom-control-label" for="unique">{{ trans('larapass::recovery.unique') }}</label>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary btn-lg">
{{ trans('larapass::recovery.button.register') }}
</button>
</div>
</form>
@endsection

@push('scripts')
<script src="{{ asset('vendor/larapass/js/larapass.js') }}"></script>
<script>
const larapass = new Larapass({
register: '/webauthn/recover/register',
registerOptions: '/webauthn/recover/options'
})
document.getElementById('form').addEventListener('submit', function (event) {
event.preventDefault()
let entries = Object.fromEntries(new FormData(this).entries())
larapass.register(entries, {
token: entries.token,
email: entries.email,
'WebAuthn-Unique': entries.unique ? true : false,
})
.then(response => window.location.replace(response.redirectTo))
.catch(response => {
alert('{{ trans('larapass::recovery.failed') }}')
console.error('Recovery failed', response)
})
})
</script>
@endpush
93 changes: 93 additions & 0 deletions src/Auth/CredentialBroker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace DarkGhostHunter\Larapass\Auth;

use Closure;
use Illuminate\Auth\Passwords\PasswordBroker;
use DarkGhostHunter\Larapass\Contracts\WebAuthnAuthenticatable;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class CredentialBroker extends PasswordBroker
{
/**
* Constant representing a successfully sent reminder.
*
* @var string
*/
public const RESET_LINK_SENT = 'larapass::recovery.sent';

/**
* Constant representing a successfully reset password.
*
* @var string
*/
public const PASSWORD_RESET = 'larapass::recovery.reset';

/**
* Constant representing the user not found response.
*
* @var string
*/
public const INVALID_USER = 'larapass::recovery.user';

/**
* Constant representing an invalid token.
*
* @var string
*/
public const INVALID_TOKEN = 'larapass::recovery.token';

/**
* Constant representing a throttled reset attempt.
*
* @var string
*/
public const RESET_THROTTLED = 'larapass::recovery.throttled';

/**
* Send a password reset link to a user.
*
* @param array $credentials
* @return string
*/
public function sendResetLink(array $credentials)
{
$user = $this->getUser($credentials);

if (! $user instanceof WebAuthnAuthenticatable) {
return static::INVALID_USER;
}

if ($this->tokens->recentlyCreatedToken($user)) {
return static::RESET_THROTTLED;
}

$user->sendCredentialRecoveryNotification(
$this->tokens->create($user)
);

return static::RESET_LINK_SENT;
}

/**
* Reset the password for the given token.
*
* @param array $credentials
* @param \Closure $callback
* @return mixed
*/
public function reset(array $credentials, Closure $callback)
{
$user = $this->validateReset($credentials);

if (! $user instanceof CanResetPasswordContract || ! $user instanceof WebAuthnAuthenticatable) {
return $user;
}

$callback($user);

$this->tokens->delete($user);

return static::PASSWORD_RESET;
}
}
Loading

0 comments on commit 0f47436

Please sign in to comment.