1

I am working on a Laravel app where I am building some API for other websites. But I am trying to make the implementation of my API as easy as possible. My expectation is that the user will only use this tag in the HTML head:

<script src="api.mydomain.com">

Now I have a controller on this URL that provides the source javascript with the content-type header, but before it goes there, the router will first execute my authentication middleware. Let's say it looks something like this:

public static $users = [
    'client1.com',
    'client2.com',
    'client3.com'
];

public function handle(Request $request, Closure $next)
{
    $origin = "HERE I NEED THE ORIGIN URL"; // e.g. client4.com

    if ( !in_array($origin, self::$users) ) {

        abort(401);

    }

    return $next($request);
}

As you can see from the code, I need to retrieve the $origin variable. So if a website client1.com will try to insert my javascript, it will successfully get the javascript code. If client4.com tries to access it, it will get a 401 error.

I found out methods with $_SERVER['HTTP_REFERER'] or Laravel's $request->server('HTTP_REFERER'), but this data might be spoofed, right?

In the best-case scenario, I would like to retrieve the original domain and when not available (e.g. from a private cURL request), I would like to get the IP address. And of course, I need it to be secure - clients1/2/3 paid for my API, others didn't.

How can I do it? Or is there any better method for origin authentication?

David Sojka
  • 73
  • 1
  • 9
  • 1
    Take a look at [this](https://stackoverflow.com/q/10636611/11801683) – jewishmoses Jun 28 '20 at 15:46
  • @jewishmoses Yeah I thought of CORS, but as far as I can tell, you can define only one domain in this header. And I even saw some websites, that work as a proxy, which can overcome the CORS protection (I'm not aware of how they do that). – David Sojka Jun 28 '20 at 19:34

2 Answers2

1

All that referer stuff can be spoofed.

Best way for paid API is to issue API calling key.

You API can display results or error depending if the client has proper API key and is Paid for.

You should also keep logs table for API calls with timestamp and clientID and IP addresses. So from time to time you can check if one of your paid client is sharing his key with others etc from call frequency and IP patterns.

Clean up this logs table from time to time to keep it small and efficient.

Keral Patel
  • 329
  • 2
  • 11
  • 1
    But how will the client use the key? If they will use something like ` – David Sojka Jun 28 '20 at 19:29
  • Okay then for this kind of situation also give your paid clients some secret key which can decrypt the API data on their end. That way only they can make sense of it. I know you want to simplify it but it is getting too simple to be secure so you will have to add something this or that either way to make it secure. – Keral Patel Jun 28 '20 at 19:54
  • If I imagine that my clients will get some decryption key, I would assume that as long as JS is a frontend language, their visitors will still be able to read that script, because it will actually be decrypted in the browser. – David Sojka Jun 29 '20 at 06:57
  • Not if the client makes the API call on server side language and then outputs the results as JS. People will just see the JS and not the underlying code. – Keral Patel Jun 29 '20 at 07:23
  • That would require server-side implementation which might be difficult for clients that use some pre-built solutions. However, people will still be able to copy the decrypted JS code, won't they? – David Sojka Jun 29 '20 at 08:31
  • Yes they would be able to download decrypted one but that is just data for that instance or call. They cannot make API queries with that info. Now if someone codes a scraper to scrape things from your clients website you can do nothing about it. It is out of your control. – Keral Patel Jun 29 '20 at 12:41
0

So I figured it out by adding headers (thanks for inspiration @jewishmoses) in the middleware handler. My Javascript is available basically to everyone, but it provides only a button, that tries to create a new element with an iframe inside (my app which also works as an API).

Let's say I have an associative array on the server, that I can dynamically fill from any database:

$clients = [
    'client1' => 'paying-customer.com',
    'client2' => 'also-paying-customer.com',
];

...my route for API is defined as 'api.mydomain.com/{client}' and 'api.mydomain.com/{client}/iframe' for iframed app. This handler takes care of adding headers:

public function handle(Request $request, Closure $next)
{
    $client = $request->route('client',null);

    $clientSet = $client !== null;
    $clientAccepted = isset($clients[$client]);

    if ( $clientSet and !$clientAccepted ) {

        abort(401);

    }

    $response = $next($request);


    if( $clientSet and isset($response->headers) and $response->headers instanceof ResponseHeaderBag){

        $clientDomain = $clients[$client];

        $response->headers->add([
            'Content-Security-Policy' => "frame-ancestors https://*.$clientDomain/ https://$clientDomain/"
        ]);
    }



    return $response;
}

Now what might happen:

  1. client1 successfully imports javascript from api.mydomain.com/client1, which will try to access api.mydomain.com/client1/iframe (also successfully)
  2. client3 unsuccessfully tries to import javascript from api.mydomain.com/client3
  3. client3 successfully imports javascript from api.mydomain.com/client1, which will try to access api.mydomain.com/client1/iframe (refused by headers)

Maybe there is a more elegant way to block loading the javascript, but providing my own app as API (in an iframe) is in my opinion secured enough because I can control who can use it and modern browsers will help me to stop the "thieves". This resource was most helpful to solve my problem.

David Sojka
  • 73
  • 1
  • 9