GithubHelp home page GithubHelp logo

kreait / firebase-tokens-php Goto Github PK

View Code? Open in Web Editor NEW
218.0 5.0 32.0 505 KB

A PHP library to work with Firebase tokens

License: MIT License

PHP 100.00%
firebase jwt php php7 php-library firebase-tokens

firebase-tokens-php's People

Contributors

ankurk91 avatar arma7x avatar bereczi avatar danizord avatar dependabot[bot] avatar github-actions[bot] avatar jeromegamez avatar jreciofreepik avatar lhpalacio avatar snapshotpl avatar timz99 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

firebase-tokens-php's Issues

Error:Generator class is reserved for internal use.

When trying out your code, as per the example in the readme. I am getting an error as below:

[Sun Jun 11 02:14:08.042454 2017] [:error] [pid 1813] [client 192.168.0.118:63309] PHP Fatal error: Uncaught Error: The "Generator" class is reserved for internal use and cannot be manually instantiated in /var/www/html/firebase/generate_token.php:10\nStack trace:\n#0 {main}\n thrown in /var/www/html/firebase/generate_token.php on line 10, referer: http://192.168.0.105/firebase/

My sample code is given below:

<?php 
use Firebase\Auth\Token\Handler;

$projectId = 'test'; //replaced
$clientEmail = '[email protected]'; //replaced

$generator = new Generator($clientEmail, $privateKey);

$uid = '1';
$claims = ['email' => '[email protected]',
           'username' => 'some1'
          
          ];

$token = $generator->createCustomToken($uid, $claims); 
// Returns a Lcobucci\JWT\Token instance

echo $token; // "eyJ0eXAiOiJKV1..."
?>

Add integration tests using the Firebase Auth Emulator

Auth Emulator Support is currently only tested implicitly by the Firebase Admin SDK, but not directly here (just locally on my computer).

  • Add a GitHub workflow that uses the Firebase Auth Emulator
  • Add utility functions in the context of the tests to create users in the Auth Emulator, create custom tokens for those users, and verify ID tokens returned by the simulator.

The Firebase Admin SDK is able to create users but can't be used here (recursive dependency)

Do you really need to force developers to install Composer?

Is it so hard to type include('domain/generator.php') and so on to include the required files, that you force developers to install the Composer extension to save a few bytes?

Honestly I never understood that policy of adding unnecessary extensions that only create problems when trying to setup a new server or migrate an existing one. Please reconsider it in your future projects if you want to make them ready to use without headaches and complications for fellow developers, thanks.

Version 4.2.0 of lcobucci/jwt breaks this package

After updating dependencies and getting a new peer-dependency of lcobucci/jwt my application started throwing "InvalidKeyProvided with message 'Key cannot be empty'" exception. After digging through the source code it seems to have been added on purpose, and then finding this: lcobucci/jwt#877 confirms it. Which means this package needs to be updated accordingly I guess?

Cache for Laravel?

Hi there,

I was wondering if you know of a way to use the cache ie

use Symfony\Component\Cache\Simple\FilesystemCache;

$cache = new FilesystemCache();

with the Laravel wrapper which is built on top of Symfony?

I tried to send do it like this but got an error:

$verifier = IdTokenVerifier::createWithProjectIdAndCache($projectId, 
            \Illuminate\Support\Facades\Cache::class);

Error: "The cache must implement Psr\\SimpleCache\\CacheInterface or Psr\\Cache\\CacheItemPoolInterface

Feature Request - Optional Implementation of OWASP recommendations

OWASP has a bunch of recommendations for JWT security to prevent:

  • None hashing algorithm
  • Token sidejacking
  • Token revocation (block list)
  • etc

It would be great if these were configurable options on the Firebase Authentication of this library that we could enable and configure on an as-needs basis.

For example, a potential implementation of the Token Sidejacking validation would be for
src/JWT/Action/VerifyIdToken/WithLcobucciJWT.php handle() function to add an additional validator to validate .withClaim("userFingerprint", userFingerprintHash).

As this is not a core function of the encoding/decoding of the token (which would imply updating the Lcobucci/JWT library), I think these changes live in this library.

The custom token format is incorrect

I am using the bellow snippet to generate custom tokens for clients who are using my Flutter mobile app.

$privateKey 	= json_decode(file_get_contents( storage_path()  . '/app/google-services.json'), true)['private_key'];
$generator 	    = CustomTokenGenerator::withClientEmailAndPrivateKey($clientEmail, $privateKey);
$token 		    = $generator->createCustomToken('uid', ['first_claim' => 'first_value' /* ... */]);
return json_encode(["status_code" => "200", "token" => $token->toString() ]);
 

When I am reusing the $token->toString() in my FirebaseAuth.instance.signInWithCustomToken ( token: '${httpResponse['token']}') I get firebase error “FirebaseAuthInvalidCredentialsException: The custom token format is incorrect.” .

According to firebase group it is a generic error message for if the maximum expiration time is more than one hour
https://groups.google.com/forum/#!msg/firebase-talk/tYRfde4nwBY/yAn5I_spBQAJ . Does the CustomTokenGenerator::withClientEmailAndPrivateKey take any maximum expiration time parameter?

Also I wonder if the $token->toString() is the correct value which I should return from my server to my mobile app ?

Thank you for you troubles.

HTTP Overhead for Public Key Retrieval

It seems that the library fetches Firebase public keys from https://www.googleapis.com/robot/v1/metadata/x509/[email protected] URL for every request. If I'm missing something please correct me.

If this is the case, wouldn't it be better to cache public keys on the first run and try to re-fetch them only for a cache-miss scenario? This way a huge HTTP overhead would be avoided for the tokens with the known same public keys.

What do you think of this proposal?

Token future time error

I am trying to verify the firebase token but sometimes, it is throwing token issued at future timestamp error, as in the following exception:

This token has been issued in the future at 2019-06-07T08:43:57+00:00, is your system time correct? {"exception":"[object] (Firebase\\Auth\\Token\\Exception\\IssuedInTheFuture(code: 0): This token has been issued in the future at 2019-06-07T08:43:57+00:00, is your system time correct? at /var/www/html/vendor/kreait/firebase-tokens/src/Verifier.php:95)

I debugged the the code, and I saw the following rows are the problem:

    private function verifyAuthTime(Token $token)
    {
       ...
        if ($token->getClaim('auth_time') > time()) {
            throw new InvalidToken($token, "The user's authentication time must be in the past");
        }
    }

The difference is between the auth_time and time() only a few seconds.

createCustomToken method returns wrong token type

Hello!

I am trying to upgrade to version 2.2.0 of this package from version 1.16.1 but I am getting the following error:

auth::createCustomToken(): Return value must be of type Lcobucci\JWT\UnencryptedToken, Kreait\Firebase\JWT\Token returned

This is what my code calling the method looks like:

$customToken = Firebase::project(applicationId())->auth()->createCustomToken($identifier, [ 'email' => $model->email, ]);

clarification about how token verification works

This library is up and running on my little api, thanks.

For each request I am passing in the Auth... bearer... header.

On the api side im doing this using middleware on every request:

    /**
     * Parse token.
     *
     * @param string $token The JWT
     *
     * @return Token The parsed token
     */
    public function createParsedToken(string $token): Token
    {
        $verifier = IdTokenVerifier::createWithProjectId($this->projectId);

        return $verifier->verifyIdToken($token);
    }

    /**
     * Validate the access token.
     *
     * @param string $accessToken The JWT
     *
     * @return bool The status
     */
    public function validateToken(string $accessToken): bool
    {
        $token = null;

        // create + validate
        try {
            $token = $this->createParsedToken($accessToken);
        } catch (IdTokenVerificationFailed $e) {
            // signature is not valid
            return false;
        }

        // user must have verified email to proceed
        if($token->payload()["email_verified"] === false) {
            return false;
        }

        return true;
    }

I just have the default in memory cache setup.

I'm worried about what this code is actually doing.

So what is actually happening with the code? Is this correct:

  • it downloads a copy of they keys and then stores them for a bit
  • every request validates the jwt somehow

If the keys are cached then does it need to call out to Firebase Auth to validate the token or is it all done server-side?

One of the request is going to be polled every few seconds, and I have this concern that I've misunderstood what I'm doing and I'm buzzing the firebase servers every 2 seconds with verification requests?

docs - include an example of the payload reply

Maybe it's just me not knowing enough PHP but it was tricky to know what the reply was when my api was running on a server. I had to write a custom api route to just dump it out (and figure out if I should have been using headers() or payload())

An example of the payload() array in the documentation would have helped eg:

        {
            "iss": "https://securetoken.google.com/{{PROJECTID}}",
            "aud": "{{PROJECTID}}",
            "auth_time": 1578755578,
            "user_id": "c7LXiEYnbPqqbr8OZcNeBcI2PIi1",
            "sub": "c7LXiEYnbPqqbr8OZcNeBcI2PIi1",
            "iat": 1580038499,
            "exp": 1580042099,
            "email": "[email protected]",
            "email_verified": true,
            "firebase": {
                "identities": {
                    "email": [
                         "[email protected]"
                    ]
                },
                "sign_in_provider": "password"
            }
        }

signInWithCustomToken

Hello, thanks for putting this and your other package together. I notice there is no method to sign back into Firebase with a custom token. The docs show an example using the method signInWithCustomToken and I was wondering if there is a way to implement this?

I have a Laravel backend where our authentication is taking place. I am trying to build a way for our mobile app to authenticate to the Laravel app and have the Laravel app generate a Firebase token the mobile app can use to access the real time db/Firestore. Is this something that can be accomplished?

I have been able to generate the token using this code:

$serviceAccount = ServiceAccount::fromJsonFile(DIR . '/firebase-adminsdk-baomg-ed357f2f41.json');

$firebase = (new Factory)
    ->withServiceAccount($serviceAccount)
    ->create();

$auth = $firebase->getAuth();

$uid = 'AE0xzvmtu7Ox85S9jYJ0zt7Y8yH2';
$additionalClaims = [
    'premiumAccount' => true,
];

$customToken = $firebase->getAuth()->createCustomToken($uid, $additionalClaims);

$customTokenString = (string) $customToken;

``

but when I try to authenticate using this code:

try {
        $verifiedIdToken = $firebase->getAuth()->verifyIdToken($customTokenString);
    } catch (InvalidToken $e) {
        echo $e->getMessage();
    }

    $uid = $verifiedIdToken->getClaim('sub');
    $user = $firebase->getAuth()->getUser($uid);

    dd($user);

I am gettting:
message": "The header \"kid\" is missing.",

From what I have read, it seems the verifyIdToken method is used to verify a valid token already on Firebase and I am looking for a way to validate the custom token generated by my Laravel backend auth.

Thanks again, I look forward to any advice or suggestions you may have.

Missing key when declaring a validator

{ "message": "Unknown key", "error": "A key with ID \"The token with ID \"%s\" is unknown.\" could not be found." }

I get that error after declaring $verifier = new Verifier($projectId);

Looks like it expects to get the key from some header value or a declared value when the object in created. public function __construct(string $projectId, KeyStore $keys = null, Signer $signer = null)

Would you happen to have a better working code example?

Fatal error: Uncaught Error: Class 'IdTokenVerifier' not found in ...

Fatal error: Uncaught Error: Class 'IdTokenVerifier' not found in public_html/session.php:18 Stack trace: #0 {main} thrown in

used composer require kreait/firebase-tokens for install it

and using your example

<?php

use Kreait\Firebase\JWT\Error\IdTokenVerificationFailed;
use Kreait\Firebase\JWT\IdTokenVerifier;

$projectId = '...';
$idToken = 'eyJhb...'; // An ID token given to your backend by a Client application

$verifier = IdTokenVerifier::createWithProjectId($projectId);

try {
    $token = $verifier->verifyIdToken($idToken);
} catch (IdTokenVerificationFailed $e) {
    echo $e->getMessage();
    // Example Output:
    // The value 'eyJhb...' is not a verified ID token:
    // - The token is expired.
    exit;
}

try {
    $token = $verifier->verifyIdTokenWithLeeway($idToken, $leewayInSeconds = 10000000);
} catch (IdTokenVerificationFailed $e) {
    print $e->getMessage();
    exit;
}



php8 compat

Need to upgrade dependencies to lcobucci/jwt:^4.0, otherwise library can be incompatible with php 8

There is a bug with one of the dependencies that was updated on November 25, 2020

Describe the bug
I ran composer update, "lcobucci/jwt" has been updated from 3.3.3 to 3.4.0, then authentication doesn't work anymore

To Reproduce
install fresh or update current install and try to verify a token

app('firebase.auth')->verifyIdToken($request->get("jwt"));

Expected behavior
it should auth correctly

Environment (please complete the following information):

  • OS: Windows 10
  • PHP version: 7.4.0
  • kreait/firebase-tokens Version: 1.11.0
  • firebase/php-jwt Version : v5.2.0
  • kreait/firebase-php Version: 5.11.0

Additional context
Error message

The token has an invalid signature: Implicit conversion of keys from strings is deprecated. Please use InMemory or LocalFileReference classes.

Stacktrace

\vendor\kreait\firebase-tokens\src\Firebase\Auth\Token\Verifier.php:145
\vendor\kreait\firebase-tokens\src\Firebase\Auth\Token\Verifier.php:68
\vendor\kreait\firebase-php\src\Firebase\Auth\IdTokenVerifier.php:53
\vendor\kreait\firebase-php\src\Firebase\Auth.php:581

Example

Hi, would you mind to have a sample?
I don't know how to load the privateKey. How?
Is clientEmail my admin email or my app user email?

issued at verification

Hello,
I faced an issue with tokens being randomly rejected due to IAT being in future. I figured out my hosting provider time is fraction of second behind from firebase server and this caused random failures (depending on user connection) of IAT verification because token was sometime issued practically at the same time as checked on server side.
Temporary, I disabled IAT verification but it is obvious security hole. I was thinking - would you be interested in adding possibility to enable/disable verifications with flags or some config? I can imagine for example some sort of offset for IAT verification. Developers could add few secs tolerance so 1-2 seconds "in future" would not cause an issue.

Undefined method 'getClaims'

Describe the bug

Hello, I'm trying to get the uid from the verified Firebase token, but I'm unable to use the 'getClaims('sub')' method. I'm using the same exact code under the 'Verify an ID token' heading, just adding the token from Firebase, but when I add '$token->getClaims('sub')' to try get retrieve the uid, I get the error below. I'm fairly new to implementing libraries and I'm just unsure how to retrieve the sub-claim. Also, I'm not using Firebase Admin SDK as my backend.

Please help, thanks!

image

Installed packages

{
    "require": {
        "kreait/firebase-tokens": "^4.2",
        "kreait/firebase-php": "^7.6",
        "lcobucci/jwt": "^5.1",
        "firebase/php-jwt": "^6.9"
    }
}

PHP version and extensions

composer               2.6.5    Composer package
composer-plugin-api    2.6.0    The Composer Plugin API
composer-runtime-api   2.2.2    The Composer Runtime API
ext-bcmath             8.2.4    The bcmath PHP extension
ext-bz2                8.2.4    The bz2 PHP extension
ext-calendar           8.2.4    The calendar PHP extension
ext-ctype              8.2.4    The ctype PHP extension
ext-curl               8.2.4    The curl PHP extension
ext-date               8.2.4    The date PHP extension
ext-dom                20031129 The dom PHP extension
ext-exif               8.2.4    The exif PHP extension
ext-fileinfo           8.2.4    The fileinfo PHP extension
ext-filter             8.2.4    The filter PHP extension
ext-ftp                8.2.4    The ftp PHP extension
ext-gettext            8.2.4    The gettext PHP extension
ext-hash               8.2.4    The hash PHP extension
ext-iconv              8.2.4    The iconv PHP extension
ext-json               8.2.4    The json PHP extension
ext-libxml             8.2.4    The libxml PHP extension
ext-mbstring           8.2.4    The mbstring PHP extension
ext-mysqli             8.2.4    The mysqli PHP extension
ext-mysqlnd            0        The mysqlnd PHP extension (actual version: mysqlnd 8.2.4)
ext-openssl            8.2.4    The openssl PHP extension
ext-pcre               8.2.4    The pcre PHP extension
ext-pdo                8.2.4    The PDO PHP extension
ext-pdo_mysql          8.2.4    The pdo_mysql PHP extension
ext-pdo_sqlite         8.2.4    The pdo_sqlite PHP extension
ext-phar               8.2.4    The Phar PHP extension
ext-random             8.2.4    The random PHP extension
ext-readline           8.2.4    The readline PHP extension
ext-reflection         8.2.4    The Reflection PHP extension
ext-session            8.2.4    The session PHP extension
ext-simplexml          8.2.4    The SimpleXML PHP extension
ext-sodium             8.2.4    The sodium PHP extension
ext-spl                8.2.4    The SPL PHP extension
ext-tokenizer          8.2.4    The tokenizer PHP extension
ext-xml                8.2.4    The xml PHP extension
ext-xmlreader          8.2.4    The xmlreader PHP extension
ext-xmlwriter          8.2.4    The xmlwriter PHP extension
ext-zip                1.21.1   The zip PHP extension
ext-zlib               8.2.4    The zlib PHP extension
lib-bz2                1.0.8    The bz2 library
lib-curl               7.85.0   The curl library
lib-curl-libssh2       1.10.0   curl libssh2 version
lib-curl-openssl       3.0.8    curl OpenSSL version (3.0.8)
lib-curl-zlib          1.2.12   curl zlib version
lib-date-timelib       2022.05  date timelib version
lib-date-zoneinfo      2022.7   zoneinfo ("Olson") database for date
lib-fileinfo-libmagic  540      fileinfo libmagic version
lib-iconv              1.16     The iconv library
lib-libsodium          1.0.18   The libsodium library
lib-libxml             2.10.3   libxml library version
lib-mbstring-libmbfl   1.3.2    mbstring libmbfl version
lib-mbstring-oniguruma 6.9.8    mbstring oniguruma version
lib-openssl            3.0.8    OpenSSL 3.0.8 7 Feb 2023
lib-pcre               10.40    The pcre library
lib-pcre-unicode       14.0.0   PCRE Unicode version support
lib-pdo_sqlite-sqlite  3.39.2   The pdo_sqlite-sqlite library
lib-zip-libzip         1.7.1    The zip-libzip library
lib-zlib               1.2.12   The zlib library
php                    8.2.4    The PHP interpreter
php-64bit              8.2.4    The PHP interpreter, 64bit
php-ipv6               8.2.4    The PHP interpreter, with IPv6 support
php-zts                8.2.4    The PHP interpreter, with Zend Thread Safety

Steps to reproduce the issue.

<?php 
    require __DIR__ . '/vendor/autoload.php';
    use Kreait\Firebase\JWT\Error\IdTokenVerificationFailed;
    use Kreait\Firebase\JWT\IdTokenVerifier;

    $projectID = '...'; // firebase project id
    $verifier = IdTokenVerifier::createWithProjectId($projectID);
    $userIdToken = 'eyJh...';

    try {
        $token = $verifier->verifyIdToken($userIdToken);
        $uid = $token->getClaims('sub');
        echo "valid token";
    } catch (IdTokenVerificationFailed $e) {
        echo $e->getMessage();
    }
?>

Error message/Stack trace

-

Additional information

No response

How to clear cache?

Cache results from the Google Secure Token Store

In order to verify ID tokens, the verifier makes a call to fetch Firebase's currently available public keys.
The keys are cached in memory by default.

How do I clear this cache? I created a new project and it is still looking for the old project name.

Unable to verify token Exception Firebase\Auth\Token\Exception\UnknownKey

Firebase verifyIdToken fails with exception as Firebase\Auth\Token\Exception\UnknownKey
Stacktrace
A key with ID "3494b1e786cdad092e423766bbe37f54ed87b22d" could not be found. {"exception":"[object] (Firebase\\Auth\\Token\\Exception\\UnknownKey(code: 0): A key with ID \"3494b1e786cdad092e423766bbe37f54ed87b22d\" could not be found. at /home/fgx3uhiothty/public_html/vendor/kreait/firebase-tokens/src/Verifier.php:132)

'Lcobucci\JWT\Signer\Key\InMemory' not found

Hi, I just installed the lib with composer and I have a problem with the lib "Lcobucci/JWT".

Error:

Fatal error: Uncaught Error: Class 'Lcobucci\JWT\Signer\Key\InMemory' not found in /wp-content/plugins/myplugin/Firebase/vendor/kreait/firebase-tokens/src/Firebase/Auth/Token/Generator.php on line 38

My composer

    "minimum-stability": "dev",
    "require": {
        "kreait/firebase-php": "^5.19",
        "kreait/firebase-tokens": "^1.15"
    }

My php file

  use Kreait\Firebase\Factory;
  use  Kreait\Firebase\Auth;
  
  $factory = (new Factory)->withServiceAccount(__DIR__ . '/google-service.json);
    
  $auth = $factory->createAuth();

Do you know what the error is, please ?

Maxium Expiration Time duration flexiblity

In src/JWT/action/CreateCustomToken.php

MAXIMUM_TTL is set to PHT1H which expires in an hour i want it P1D

Even your function createCustomToken doens't let me go beyond 1 Hour can you remove the validation or provide a solution.

Thanks

What is the purpose of HttpKeyStore.php

Hey, first of all great library, I used this library for developing a Laravel package for authenticating firebase token. would you be kind to tell me what is the purpose of HttpKeyStore.php ? thankyou

Why Handler class is final ?

Hello,

Why final class Handler implements Domain\Generator, Domain\Verifier class is final ?
I'd like to extend it to add a method to only verify if the token is expired ...

Thank you

Firebase\Auth\Token\Exception\UnknownKey

Key with ID "%s" not found.

I Have found one more solution to this issue is other than what people have suggested is
to add this one more URL which has the keys that were used to sign idTokens coming from kotlin android app and ios app

kreait/ firebase-token/src/Firebase/Auth/Token/HttpKeyStore

const KEYS_URL_1 = 'https://www.googleapis.com/robot/v1/metadata/x509/[email protected]';
const KEYS_URL_2 = 'https://www.googleapis.com/oauth2/v1/certs';

then the keys array will have all 4 keys (2 web keys and the other 2 keys as well)

Can we have this added in the coming version on kreait/firebase-tokens-php

readme - sentence trails off before end

In the advanced usage / cache section the sentence is not finished:

If you want to cache the public keys more effectively, you can use an implementation of psr/simple-cache or psr/cache to wrap the

Tag release with Guzzle 7 support

It looks like composer.json was updated for Guzzle 7 stable, but a new release hasn't been tagged.

Laravel 8 now requires Guzzle 7.0 so this prevents it from being installed alongside.

Could we please get a new tagged release?

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.