Git Product home page Git Product logo

laravel-magiclink's Introduction

MagicLink for Laravels App

Through the MagicLink class we can create a secure link that later being visited will perform certain actions, which will allow us offer secure content and even log in to the application.

Latest Version on Packagist tests style-fix Quality Score Total Downloads

Contents

Installation

You can install this package via composer using:

composer require cesargb/laravel-magiclink

Preparing the database

You need to publish the migration to create the magic_links table:

php artisan vendor:publish --provider="MagicLink\MagicLinkServiceProvider" --tag="migrations"

After that, you need to run migrations.

php artisan migrate

Use case

With this example you can create a link to auto login on your application with the desired user:

use MagicLink\Actions\LoginAction;
use MagicLink\MagicLink;

$urlToAutoLogin =  MagicLink::create(new LoginAction($user))->url

Create a MagicLink

The MagicLink class has the create method to generate a class that through the url property we will obtain the link that we will send to our visitor.

This method requires the action to be performed.

Actions

Each MagicLink is associated with an action, which is what will be performed once the link is visited.

Login Action

Through the LoginAction action, you can log in to the application using the generated link by MagicLink.

Your constructor supports the user who will login. Optionally we can specify the HTTP response using the method response or specify other guard with method guard.

Examples:

use MagicLink\Actions\LoginAction;
use MagicLink\MagicLink;

// Sample 1; Login and redirect to dash board
$action = new LoginAction(User::first());
$action->response(redirect('/dashboard'));

$urlToDashBoard = MagicLink::create($action)->url;

// Sample 2; Login and view forms to password reset and use guard web
$action = new LoginAction(User::first());
$action->response(view('password.reset', ['email' => '[email protected]']));

$urlShowView = MagicLink::create($action)->url;

// Sample 3; Login in other guard and redirect default
$action = new LoginAction(User::first());
$action->guard('customguard');

$urlShowView = MagicLink::create($action)->url;

// Sample 4; Login and remember me
$action = new LoginAction(User::first());
$action->remember();

$urlShowView = MagicLink::create($action)->url;

Download file Action

This action, DownloadFileAction, permit create a link to download a private file.

The constructor require the file path.

Example:

use MagicLink\Actions\DownloadFileAction;
use MagicLink\MagicLink;

// Url to download the file storage_app('private_document.pdf')
$url = MagicLink::create(new DownloadFileAction('private_document.pdf'))->url;

// Download file with other file_name
$action = new DownloadFileAction('private_document.pdf', 'your_document.pdf');
$url = MagicLink::create($action)->url;

// Download file from other disk
$action = new DownloadFileAction('private_document.pdf')->disk('ftp');
$url = MagicLink::create($action)->url;

View Action

With the action ViewAction, you can provide access to the view. You can use in his constructor the same arguments than method view of Laravel.

Example:

use MagicLink\Actions\ViewAction;
use MagicLink\MagicLink;

// Url to view a internal.blade.php
$url = MagicLink::create(new ViewAction('internal', [
    'data' => 'Your private custom content',
]))->url;

Http Response Action

Through the ResponseAction action we can access private content without need login. Its constructor accepts as argument the HTTP response which will be the response of the request.

Examples:

use MagicLink\Actions\ResponseAction;
use MagicLink\MagicLink;

$action = new ResponseAction(function () {
    Auth::login(User::first());

    return redirect('/change_password');
});

$urlToCustomFunction = MagicLink::create($action)->url;

Controller Action

MagicLink can directly call a controller via the ControllerAction action.

The constructor requires one argument, the name of the controller class. With the second argument can call any controller method, by default it will use the __invoke method.

use MagicLink\Actions\ControllerAction;
use MagicLink\MagicLink;

// Call the method __invoke of the controller
$url = MagicLink::create(new ControllerAction(MyController::class))->url;

// Call the method show of the controller
$url = MagicLink::create(new ControllerAction(MyController::class, 'show'))->url;

Custom Action

You can create your own action class, for them you just need to extend with MagicLink\Actions\ActionAbstract

use MagicLink\Actions\ActionAbstract;

class MyCustomAction extends ActionAbstract
{
    public function __construct(public int $variable)
    {
    }

    public function run()
    {
        // Do something

        return response()->json([
            'success' => true,
            'data' => $this->variable,
        ]);
    }
}

You can now generate a Magiclink with the custom action

use MagicLink\MagicLink;

$action = new MyCustomAction('Hello world');

$urlToCustomAction = MagicLink::create($action)->url;

Custom Base URL

To set the base URL for a magic link, you can use the baseUrl method. This method ensures that the provided base URL has a trailing slash, making it ready for URL generation.

$magiclink = MagicLink::create($action);

$magiclink->baseUrl("http://example.com");

$urlShowView = $magiclink->url; // http://example.com/magiclink/...

Protect with an access code

Optionally you can protect the resources with an access code. You can set the access code with method protectWithAccessCode which accepts an argument with the access code.

$magiclink = MagicLink::create(new DownloadFileAction('private_document.pdf'));

$magiclink->protectWithAccessCode('secret');

$urlToSend = $magiclink->url;

Custom view for access code

You can customize the view of the access code form with the config file magiclink.php:

'access_code' => [
    'view' => 'magiclink::access-code', // Change with your view
],

This is the default view

Lifetime

By default a link will be available for 72 hours after your creation. We can modify the life time in minutes of the link by the $lifetime option available in the create method. This argument accepts the value null so that it does not expire in time.

$lifetime = 60; // 60 minutes

$magiclink = MagicLink::create(new ResponseAction(), $lifetime);

$urlToSend = $magiclink->url;

We also have another option $numMaxVisits, with which we can define the number of times the link can be visited, null by default indicates that there are no visit limits.

$lifetime = null; // not expired in the time
$numMaxVisits = 1; // Only can visit one time

$magiclink = MagicLink::create(new ResponseAction(), $lifetime, $numMaxVisits);

$urlToSend = $magiclink->url;

Events

MagicLink fires two events:

  • MagicLink\Events\MagicLinkWasCreated
  • MagicLink\Events\MagicLinkWasVisited

Customization

Config

To custom this package you can publish the config file:

php artisan vendor:publish --provider="MagicLink\MagicLinkServiceProvider" --tag="config"

And edit the file config/magiclink.php

Migrations

To customize the migration files of this package you need to publish the migration files:

php artisan vendor:publish --provider="MagicLink\MagicLinkServiceProvider" --tag="migrations"

You'll find the published files in database/migrations/*

Custom response when magiclink is invalid

When the magicLink is invalid by default the http request return a status 403. You can custom this response with config magiclink.invalid_response.

Response

To return a response, use class MagicLink\Responses\Response::class same response(), you can send the arguments with options

Example:

    'invalid_response' => [
        'class'   => MagicLink\Responses\Response::class,
        'options' => [
            'content' => 'forbidden',
            'status' => 403,
        ],
    ],

Abort

To return a exception and let the framework handle the response, use class MagicLink\Responses\AbortResponse::class. Same abort(), you can send the arguments with options.

Example:

    'invalid_response' => [
        'class'   => MagicLink\Responses\AbortResponse::class,
        'options' => [
            'message' => 'You Shall Not Pass!',
            'status' => 403,
        ],
    ],

Redirect

Define class MagicLink\Responses\RedirectResponse::class to return a redirect()

    'invalid_response' => [
        'class'   => MagicLink\Responses\RedirectResponse::class,
        'options' => [
            'to' => '/not_valid_path',
            'status' => 301,
        ],
    ],

View

Define class MagicLink\Responses\ViewResponse::class to return a view()

    'invalid_response' => [
        'class'   => MagicLink\Responses\ViewResponse::class,
        'options' => [
            'view' => 'invalid',
            'data' => [],
        ],
    ],

Testing

Run the tests with:

composer test

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security-related issues, please email [email protected] instead of using the issue tracker.

License

The MIT License (MIT). Please see License File for more information.

laravel-magiclink's People

Contributors

benlumley avatar cesargb avatar dependabot[bot] avatar evokelektrique avatar iredmedia avatar jamesdb avatar jansgescheit avatar khalid3bdallah avatar laravel-shift avatar marcus-ixau avatar peterquentin avatar rabol avatar robindrost avatar saulens22 avatar savander avatar szepeviktor avatar thesubhendu avatar vanushwashere avatar zupolgec avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

laravel-magiclink's Issues

Serialization of 'Closure' is not allowed

as in your example i'm trying to redirect with the following line
$action->response(redirect(route('drop.private', ['drop' => $drop])));

if i use:

$action->redirect(route('drop.private', ['drop' => $drop]));

it works but only writes out the url not a location header.

Laravel 9
PHP 8.1
magiclink ^2.12

HEAD request increases num_visits

Hello @cesargb :)

I'm sending login links to users that are only valid for one visit. This is to prevent users from sharing their login link with others.
I've found that Outlook users are having issues with this. When they click the link in Outlook the url is "scanned" by Outlook and that "scan" is basicly doing the first visit. So when the browser opens the link, it is no longer valid.

I see in the logs that Outlook performs an HEAD request to the URL. Then the browser performs the GET request.

192.168.0.6 - [10/Apr/2023:14:55:44 +0200] "HEAD /host/login/82875115-1b3f-4303-8f55-4241a4248c53:GjZTBY5kxv HTTP/1.1" 302 -
192.168.0.6 - [10/Apr/2023:14:55:45 +0200] "GET  /host/login/82875115-1b3f-4303-8f55-4241a4248c53:GjZTBY5kxv HTTP/1.1" 301 502

I didnt find anyway to stop HEAD requests in the route, but in the middleware it can be added. I've tested adding this to MagiclinkMiddleware. Works fine.

if ($request->method() != "GET") {
    die();
}

Do you think this is something that makes sense to add to this package?

File Name

I want to change the file name when downloading the file. What can I do about this?

SQL error when changing token length

Database migration creates a column for the token with a maximum length of 100 symbols. But you can change that token length value in configuration to a value bigger than 100 and get the Data too long for column 'token' at row 1 SQL error.

Expiry date

Hi,

I'm using this script in my poll. Generating poll address for customers auto login and text message to mobile. Everything ok when i create new one. But when the expire the link, i need to update sometimes the link lifetime.. I research the database and i found in magic_links table, available_at column. I tried update this column data but still cannot open the poll. Bcos, the link is dead. What should i do?

edit: i checked again for available_at column change:
first; i created new link for poll. Expire date 40 days later and i changed date today and i gave 1 hour for expire and work again. when i change it expired time, its forwarded to expired page and i changed 1 hour later again but its not working. I mean, if any link expired and visited expired link, then its not working anymore..

Erro no arquivo MagicLink

Tem um erro no arquivo MagicLink, na linha 35 para ser mais específico. Tem que retirar o espaço/tab da função config pois ao mudar o path da url no arquivo de config ele não reconhece devido a esse espaço.

Error after publishing magiclink config file

After publishing the config file magiclink laravel throws an exception Illuminate \ Contracts \ Container \ BindingResolutionException Target class [view] does not exist..

I were able to resolve it by changing the response error to a string instead of a response function or view in config/magiclink.php:51

I used a fresh installation of laravel 7.6.2 using PHP version 7.4.5 on Ubuntu 18.04 LTS

How to use Custom Disk

I want to get the file from ftp disk and give it to DownloadFileAction(). What can I do to do this?

Set path in cookie

I think it would be better to set the "path" in cookie to the current path of the request instead of setting it only to "/"

cookie(
    'magic-link-access-code',
    encrypt($request->get('access-code')),
    0,
    $request->getPathInfo()
)

otherwise all MagicLinks with the same access code will be visible if this was entered already on another link.

Possible to set "remember me"?

Great package you have made! Thanks.

Is it possible in any way to set "remember me" for the login action?
I want the login session to last longer than the global SESSION_LIFETIME that is defined in .env.

How to get the reason for invalid link

Hi

The config options invalid_response is working quite okay, but..

How do I figure out the reason for why the link is invalid?

is is because the token is invalid, after the life time or number of visits that is the reason ?

Make a link invalid

After a link has been created and sent to the user, how do i make it invalid so that the user cannot use that link, may be because his access to the site was revoked?

Custom `getConnectionName` not called on extended `MagicLink` model due to usage of `self` instead of `static`

The latest change to the library broke my code:

$magiclink = new self();

I have extended your model class and done some modifications, such as overriding \Illuminate\Database\Eloquent\Model::getConnectionName to my custom one. The self keyword, do break the code, since it refers to the same class in which the new keyword is actually written. In that case, my getConnectionName is ignored and is not called.

The solution would be to use static since it uses late static binding, which refers to the class you called the method on.
It should not break your code, but enable it for further modification/extension.

Is there any reason to do that? Why would you restrict modification of your code?

In this case, other methods will also be affected, for example:

return self::where('id', $tokenId)
->where('token', $tokenSecret)
->where(function ($query) {
$query
->whereNull('available_at')
->orWhere('available_at', '>=', Carbon::now());
})
->where(function ($query) {
$query
->whereNull('max_visits')
->orWhereRaw('max_visits > num_visits');
})
->first();

Unable to create or change a table without a primary key

We have a Managed MySQL Server with DigitalOcean. In it's default config it comes with system variable 'sql_require_primary_key` set.
This prevents us running migrations with the following error

Migrating: 2017_07_06_000000_create_table_magic_links

   Illuminate\Database\QueryException 

  SQLSTATE[HY000]: General error: 3750 Unable to create or change a table without a primary key, when the system variable 'sql_require_primary_key' is set. Add a primary key to the table or unset this variable to avoid this message. Note that tables without a primary key can cause performance problems in row-based replication, so please consult your DBA before changing this setting. (SQL: create table `magic_links` (`id` char(36) not null, `token` varchar(255) not null, `action` text not null, `num_visits` tinyint unsigned not null default '0', `max_visits` tinyint unsigned null, `available_at` timestamp null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci')

My only fix was this inelegant solution - changing the migration file

 public function up()
    {
        Schema::create(config('magiclink.magiclink_table', 'magic_links'), function (Blueprint $table) {
            $table->bigIncrements( 'iid' );
            $table->uuid('id');
            $table->string('token', 255);
            $table->text('action');
            $table->unsignedTinyInteger('num_visits')->default(0);
            $table->unsignedTinyInteger('max_visits')->nullable();
            $table->timestamp('available_at')->nullable();
            $table->timestamps();

        });
    }

Other migrations run as expected - just this one.

Any suggestions?

phpstorm detecting an error

so I'm using multiple authentication guards and when i try to get the id of a user like this

'patient_id' => Auth::guard('users')->user()->id,
I get an error in phpstorm that says this
Screen Shot 2022-06-17 at 3 41 39 PM

but functionally everything works

[QUESTION] How to overwrite or extend middleware

Hello, thank you for sharing this package. I have a question about extending the behavior of the middleware. Let me explain our use case:

  1. We use magic links to provide users with a link to create their profile
  2. After their profile is created, if they use the magic link again, it should take them to the dashboard

Our problem is that after the profile is created, when a user clicks the magic link again, they are redirected to the profile screen. This is a problem because it opens a security loop where someone can reset the password.

We have tried creating our own middleware to handle the check and redirect to the dashboard. However, in all cases, the user is redirected to the profile screen (this is the original redirect route when the magic link is created).

Is it possible to extend the middleware provided by the package? If not, would it be better to use events for this use case? If so, how are events used in this package?

Lastly, if there is a better solution, what would you recommend?

Thank you :)

Data too long for column 'action' at row 1

We've occasionally been hitting this error, and from the looks of the stack trace, it seems like a huge amount of information is being stored in the action column. It would appear the entire User model is being stuffed in there.

We're using the library in a wrapper function, where the $route could be a named route or a URL:

if (Route::has($route)) {
 $redirect = redirect(route($route));
} else $redirect = redirect($route);

return MagicLink::create(new LoginAction($user, $redirect), $lifetime,$maxVisits)

Call to a member function run() on null

We've been seeing this error more frequently which seems to happen when a user tries to use a magic link with an invalid token. We send magic links in an onboarding email to allow users access to complete their profile, and we think users are going back to this email and using the link to try and login again.

I've tracked this back to the getMagicLinkByToken method on the MagicLink class:

public static function getMagicLinkByToken($token)

In our case, this is returning null for whatever reason, and then when the MagicLinkController tries to call run(), its calling it on null and throwing the exception.

Do you have any ideas on how we can work around this? Ideally, if someone visits a link where the token is invalid, we could show them a page with more information and maybe a form to resend a magic link (we would implement this ourselves if possible)?

Thank you for taking time to share this package - it has been very helpful in getting our onboarding flow figured out. Any and all feedback is appreciated, and thank you again 🙏

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.