Coder Social home page Coder Social logo

liferay-oidc-plugin's People

Contributors

gvanderploeg avatar

Watchers

 avatar  avatar  avatar

liferay-oidc-plugin's Issues

Not able to use OIDC plugin - not a valid Token Error

Hi
I found this article
https://medium.com/@chintanaw/using-openid-connect-plugin-in-liferay-with-wso2-identity-server-7f3405c4453d#.1n4arfnyv

I'm really interested in reproducing this, but I'm stucked on an error in liferay.

I have a local liferay CE 6.2 GA 2 on https://150.145.133.108:8443/
and a wso2 5.0 on https://wso2.geosdi.org:9443/carbon
(local /etc/hosts resolution)

I followed the mentioned article and I set up the configurations on the wso2 service provider and on liferay portal-ext.properties

when I connect to http://150.145.133.108:8080/c/portal/login I'm redirected to the WSO login page. After the login I'm redirected to something similar to:
https://150.145.133.108:8443/c/portal/login?state=f6e0e0a00175036e25599921542b1540&code=18549269cc6a65b333f924297541a13

with a white html page.

On liferay tomcato log I get this error:

ERROR [http-bio-8443-exec-3][LibFilter:83] java.lang.IllegalArgumentException: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0=__.eyJleHAiOjE1NTE5Mzk1MzEsImF6cCI6IkdlN0FsSTVrTWJPOWcyOWdqVnc0cVdieXZWTWEiLCJz__dWIiOiJhZG1pbkBjYXJib24uc3VwZXIiLCJhdWQiOiJHZTdBbEk1a01iTzlnMjlnalZ3NHFXYnl2__Vk1hIiwiaXNzIjoiaHR0cHM6XC9cL3dzbzIuZ2Vvc2RpLm9yZzo5NDQzXC9vYXV0aDJlbmRwb2lu__dHNcL3Rva2VuIiwiaWF0IjoxNTQ4MzM5NTMxfQ==__.is not a valid Token, it does not match with the pattern: ([a-zA-Z0-9/+=]+)\.([a-zA-Z0-9/+=]+)\.(.+) [Sanitized] java.lang.IllegalArgumentException: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0=__.eyJleHAiOjE1NTE5Mzk1MzEsImF6cCI6IkdlN0FsSTVrTWJPOWcyOWdqVnc0cVdieXZWTWEiLCJz__dWIiOiJhZG1pbkBjYXJib24uc3VwZXIiLCJhdWQiOiJHZTdBbEk1a01iTzlnMjlnalZ3NHFXYnl2__Vk1hIiwiaXNzIjoiaHR0cHM6XC9cL3dzbzIuZ2Vvc2RpLm9yZzo5NDQzXC9vYXV0aDJlbmRwb2lu__dHNcL3Rva2VuIiwiaWF0IjoxNTQ4MzM5NTMxfQ==__.is not a valid Token, it does not match with the pattern: ([a-zA-Z0-9/+=]+)\.([a-zA-Z0-9/+=]+)\.(.+) [Sanitized] at org.apache.oltu.commons.encodedtoken.TokenReader.read(TokenReader.java:62) at nl.finalist.liferay.oidc.OpenIdConnectResponse.init(OpenIdConnectResponse.java:36) at org.apache.oltu.oauth2.client.response.OAuthClientResponseFactory.createCustomResponse(OAuthClientResponseFactory.java:60) at org.apache.oltu.oauth2.client.URLConnectionClient.execute(URLConnectionClient.java:111) at org.apache.oltu.oauth2.client.OAuthClient.accessToken(OAuthClient.java:65) at org.apache.oltu.oauth2.client.OAuthClient.accessToken(OAuthClient.java:55) at nl.finalist.liferay.oidc.LibFilter.exchangeCodeForAccessToken(LibFilter.java:168) at nl.finalist.liferay.oidc.LibFilter.processFilter(LibFilter.java:132) at nl.finalist.liferay.oidc.OpenIDConnectFilter.processFilter(OpenIDConnectFilter.java:50) at com.liferay.portal.kernel.servlet.BaseFilter.doFilter(BaseFilter.java:59) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at com.liferay.portal.kernel.bean.ClassLoaderBeanHandler.invoke(ClassLoaderBeanHandler.java:67) at com.sun.proxy.$Proxy485.doFilter(Unknown Source) at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.processDoFilter(InvokerFilterChain.java:204) at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.doFilter(InvokerFilterChain.java:109) at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilter.doFilter(InvokerFilter.java:96) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99) at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1023) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:589) at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:312) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745)

I'm consuming a lot of time trying to understand the problem but untill now I was not able to solve it.

I will be really really happy if you can give me some help.

Best
L.

Can't use liferay-oidc-plugin with KeyCloak

Hello,

I try to use OpenID Connect with KeyCloak. Unfortunately, there is an exception:

--- Stacktrace ---

14:14:42,503 ERROR [default task-26][I18nServlet:121] java.io.IOException: While exchanging code for access token and retrieving user info
java.io.IOException: While exchanging code for access token and retrieving user info
        at nl.finalist.liferay.oidc.LibFilter.exchangeCodeForAccessToken(LibFilter.java:191)
        at nl.finalist.liferay.oidc.LibFilter.processFilter(LibFilter.java:132)
        at nl.finalist.liferay.oidc.OpenIDConnectFilter.processFilter(OpenIDConnectFilter.java:60)
        at com.liferay.portal.kernel.servlet.BaseFilter.doFilter(BaseFilter.java:48)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.processDoFilter(InvokerFilterChain.java:207)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.doFilter(InvokerFilterChain.java:112)
        at com.liferay.portal.kernel.servlet.BaseFilter.processFilter(BaseFilter.java:142)
        at com.liferay.portal.servlet.filters.uploadservletrequest.UploadServletRequestFilter.processFilter(UploadServletRequestFilter.java:93)
        at com.liferay.portal.kernel.servlet.BaseFilter.doFilter(BaseFilter.java:48)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.processDoFilter(InvokerFilterChain.java:207)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.doFilter(InvokerFilterChain.java:112)
        at com.liferay.portal.kernel.servlet.BaseFilter.processFilter(BaseFilter.java:142)
        at com.liferay.portal.servlet.filters.strip.StripFilter.processFilter(StripFilter.java:336)
        at com.liferay.portal.kernel.servlet.BaseFilter.doFilter(BaseFilter.java:48)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.processDoFilter(InvokerFilterChain.java:207)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.doFilter(InvokerFilterChain.java:112)
        at com.liferay.portal.kernel.servlet.BaseFilter.processFilter(BaseFilter.java:142)
        at com.liferay.portal.servlet.filters.gzip.GZipFilter.processFilter(GZipFilter.java:125)
        at com.liferay.portal.kernel.servlet.BaseFilter.doFilter(BaseFilter.java:48)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.processDoFilter(InvokerFilterChain.java:207)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.doFilter(InvokerFilterChain.java:112)
        at com.liferay.portal.kernel.servlet.BaseFilter.processFilter(BaseFilter.java:142)
        at com.liferay.portal.servlet.filters.secure.SecureFilter.processFilter(SecureFilter.java:303)
        at com.liferay.portal.kernel.servlet.BaseFilter.doFilter(BaseFilter.java:48)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.processDoFilter(InvokerFilterChain.java:207)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.doFilter(InvokerFilterChain.java:112)
        at com.liferay.portal.kernel.servlet.BaseFilter.processFilter(BaseFilter.java:142)
        at com.liferay.portal.servlet.filters.jsoncontenttype.JSONContentTypeFilter.processFilter(JSONContentTypeFilter.java:42)
        at com.liferay.portal.kernel.servlet.BaseFilter.doFilter(BaseFilter.java:48)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.processDoFilter(InvokerFilterChain.java:207)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.doFilter(InvokerFilterChain.java:112)
        at com.liferay.portal.kernel.servlet.BaseFilter.processFilter(BaseFilter.java:142)
        at com.liferay.portal.servlet.filters.autologin.AutoLoginFilter.processFilter(AutoLoginFilter.java:268)
        at com.liferay.portal.kernel.servlet.BaseFilter.doFilter(BaseFilter.java:48)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.processDoFilter(InvokerFilterChain.java:207)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilterChain.doFilter(InvokerFilterChain.java:112)
        at com.liferay.portal.kernel.servlet.filters.invoker.InvokerFilter.doFilter(InvokerFilter.java:115)
        at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:60)
.... (skipped) ...
Caused by: OAuthProblemException{error='invalid_request', description='Missing form parameter: grant_type', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}
        at org.apache.oltu.oauth2.common.exception.OAuthProblemException.error(OAuthProblemException.java:59)
        at org.apache.oltu.oauth2.client.validator.OAuthClientValidator.validateErrorResponse(OAuthClientValidator.java:63)
        at org.apache.oltu.oauth2.client.validator.OAuthClientValidator.validate(OAuthClientValidator.java:48)
        at org.apache.oltu.oauth2.client.response.OAuthClientResponse.validate(OAuthClientResponse.java:64)
        at org.apache.oltu.oauth2.client.response.OAuthClientResponse.init(OAuthClientResponse.java:59)
        at org.apache.oltu.oauth2.client.response.OAuthAccessTokenResponse.init(OAuthAccessTokenResponse.java:52)
        at nl.finalist.liferay.oidc.OpenIdConnectResponse.init(OpenIdConnectResponse.java:35)
        at org.apache.oltu.oauth2.client.response.OAuthClientResponseFactory.createCustomResponse(OAuthClientResponseFactory.java:60)
        at org.apache.oltu.oauth2.client.URLConnectionClient.execute(URLConnectionClient.java:111)
        at org.apache.oltu.oauth2.client.OAuthClient.accessToken(OAuthClient.java:65)
        at org.apache.oltu.oauth2.client.OAuthClient.accessToken(OAuthClient.java:55)
        at nl.finalist.liferay.oidc.LibFilter.exchangeCodeForAccessToken(LibFilter.java:168)
        ... 125 more
14:14:42,727 ERROR [default task-26][status_jsp:922] While exchanging code for access token and retrieving user info

--- protal-ext.properties ---

openidconnect.enableOpenIDConnect=true
openidconnect.token-location=http://localhost:8180/auth/realms/LifeRay-OpenID/protocol/openid-connect/token
openidconnect.authorization-location=http://localhost:8180/auth/realms/LifeRay-OpenID/protocol/openid-connect/auth
openidconnect.profile-uri=http://localhost:8180/auth/realms/LifeRay-OpenID/protocol/openid-connect/userinfo
openidconnect.issuer=http://localhost:8180/auth/realms/LifeRay-OpenID
openidconnect.client-id=liferay-openid-connector
openidconnect.secret=xyz
openidconnect.scope=openid

I think KeyCloak expects an additional value (grant_type) in the form data, but I can not see where I could set this in the properties.

I am using liferay-ce-portal-7.0-ga3 (Wildfly-Bundle) and KeyCloak 2.2.1.
The logging for nl.finalist is on DEBUG, but there is no additional information in the log.

With kind regards
Ronald

Connecting liferay to keycloak: exception

I'm trying to connect liferay to keycloak for SSO.

Here are my settings:
openidconnect.enableOpenIDConnect=true openidconnect.token-location=http://172.23.4.40:8888/auth/realms/alfresco/protocol/openid-connect/token openidconnect.authorization-location=http://172.23.4.40:8888/auth/realms/alfresco/protocol/openid-connect/auth openidconnect.profile-uri=http://172.23.4.40:8888/auth/realms/alfresco/protocol/openid-connect/userinfo openidconnect.issuer=http://172.23.4.40:8888/auth/realms/alfresco openidconnect.client-id=tango openidconnect.secret=295a4e97-4170-4fef-a6b9-bc0665297ac5 openidconnect.scope=openid email profile

When I access liferay, it correctly redirects me to keycloak but once I enter username/pwd it returns 500 error

Here are the logs

00:50:44,725 TRACE [http-nio-8080-exec-3][Liferay62Adapter:31] In processFilter()... 00:50:44,741 TRACE [http-nio-8080-exec-3][Liferay62Adapter:31] About to redirect to OpenID Provider 00:50:44,741 DEBUG [http-nio-8080-exec-3][Liferay62Adapter:41] Redirecting to URL: http://172.23.4.40:8888/auth/realms/alfresco/protocol/openid-connect/auth?scope=openid+email+profile&response_type=code&redirect_uri=http%3A%2F%2F172.23.3.235%3A8080%2Fc%2Fportal%2Flogin&state=aff0f1a6f4676b76d15529f10c0b70c3&client_id=tango 00:50:52,166 TRACE [http-nio-8080-exec-6][Liferay62Adapter:31] In processFilter()... 00:50:52,166 TRACE [http-nio-8080-exec-6][Liferay62Adapter:31] About to exchange code for access token 00:50:52,166 DEBUG [http-nio-8080-exec-6][Liferay62Adapter:41] Token request to uri: http://172.23.4.40:8888/auth/realms/alfresco/protocol/openid-connect/token 00:50:52,213 TRACE [http-nio-8080-exec-6][Liferay62Adapter:31] Access/id token response: -- JWT --__Raw String: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJGajZMTVpEY0UtLTE4eXZPWDBoSHFEaFVNWnFFUVFkNGFxeDJnZ1VINjlnIn0.eyJleHAiOjE2NDU3MTM3OTQsImlhdCI6MTY0NTcxMzYxNCwiYXV0aF90aW1lIjoxNjQ1NzEzNjE0LCJqdGkiOiJjNzFlZWZhNS0yODE5LTRkMmMtYTIwMC00YmMyNDAxYTNkYzQiLCJpc3MiOiJodHRwOi8vMTcyLjIzLjQuNDA6ODg4OC9hdXRoL3JlYWxtcy9hbGZyZXNjbyIsImF1ZCI6InRhbmdvIiwic3ViIjoiZDgyYWRkMzgtNDYyZC00MTk5LWI1MzktOGJiODc3Y2QyMmEzIiwidHlwIjoiSUQiLCJhenAiOiJ0YW5nbyIsInNlc3Npb25fc3RhdGUiOiJlNGVhYThjOS0yYjA2LTQwYjctYmVhMS1hYjgwMzJjY2FlMzIiLCJhdF9oYXNoIjoiY040bjhJOUVWWG8tVUl2T2R0em9PZyIsImFjciI6IjEiLCJzaWQiOiJlNGVhYThjOS0yYjA2LTQwYjctYmVhMS1hYjgwMzJjY2FlMzIiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibmFtZSI6IlphaW4iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ6YWluIiwiZ2l2ZW5fbmFtZSI6IlphaW4iLCJlbWFpbCI6InphaW5Aa2V5Y2xvYWsuY29tIn0.KtYR14v6_4KPhqYZ1hizEIZEDHTND0QDqtB5loJ2nJwALYo6yhTg2juYE-okOWOTfixANS1ldqrZM-o6xGEWF8y0aYH4iFQGWNl4ENHo4iRwPq1wZg7-Sf9nX-P-y1wxkuuQnhNk2iNHyLYqrCxnkv_rYvVBbKVU5S6Q6pRlp1LE38sqJBw9_gbh_JXTSdZUrVGUsDJRpobFz0gS7wRt4ohk5UPDrX9rsWhsF0ojK1K2WoKp3IumesiRwM3xHpqB7h7ME6nIQa0t5lhjZhoipgMoFT-pycy_iJXx7gqBTfd_EwOZVHpj6suFL6qM_TpHRYzHol9_0Guf8OFY6WnAcQ__Header: {"typ": "JWT", "alg": "RS256", "cty": "null" , "kid": "Fj6LMZDcE--18yvOX0hHqDhUMZqEQQd4aqx2ggUH69g"}__Claims Set: {"iss": "http://172.23.4.40:8888/auth/realms/alfresco", "sub": "d82add38-462d-4199-b539-8bb877cd22a3", "aud": ["tango"], "exp": 1645713794, "nbf": "null", "iat": 1645713614, "jti": "c71eefa5-2819-4d2c-a200-4bc2401a3dc4", "typ": "ID" }__Signature: KtYR14v6_4KPhqYZ1hizEIZEDHTND0QDqtB5loJ2nJwALYo6yhTg2juYE-okOWOTfixANS1ldqrZM-o6xGEWF8y0aYH4iFQGWNl4ENHo4iRwPq1wZg7-Sf9nX-P-y1wxkuuQnhNk2iNHyLYqrCxnkv_rYvVBbKVU5S6Q6pRlp1LE38sqJBw9_gbh_JXTSdZUrVGUsDJRpobFz0gS7wRt4ohk5UPDrX9rsWhsF0ojK1K2WoKp3IumesiRwM3xHpqB7h7ME6nIQa0t5lhjZhoipgMoFT-pycy_iJXx7gqBTfd_EwOZVHpj6suFL6qM_TpHRYzHol9_0Guf8OFY6WnAcQ__--------- [Sanitized] 00:50:52,213 TRACE [http-nio-8080-exec-6][Liferay62Adapter:31] UserInfo request to uri: http://172.23.4.40:8888/auth/realms/alfresco/protocol/openid-connect/userinfo 00:50:52,307 WARN [http-nio-8080-exec-6][errorPage_jsp:139] {code="500", msg="Not Found", uri=/c/portal/login} 00:50:52,463 WARN [http-nio-8080-exec-4][errorPage_jsp:139] {code="404", msg="/errors/css/font-faces.css", uri=/errors/css/font-faces.css}

Issue in matching state param

I'm testing OIDC plugin with WSO2 Identity Server. When it redirect after a successful login I get the following error in Liferay.

03:04:13,636 INFO  [http-bio-8080-exec-7][OpenIDConnectFilter:147] Provided state parameter '2e48dc844de1a6e94be1dc08b3eedf95' does not equal expected '92c251587d5cc607c4c7f0f2df1d5a37', cannot continue.
Oct 04, 2016 3:04:13 AM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [Main Servlet] in context with path [] threw exception
java.io.IOException: Invalid state parameter
...

When I look at the redirects it seems like Identity Server is sending the correct state parameter. See below trace,

When I try to authenticate Liferay generate the following request and send to Identity Server

HTTP/1.1 302 Found
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1
Set-Cookie: JSESSIONID=FD4DE44FE549C6BDAACD9615A9761224; Path=/; HttpOnly
Location: https://localhost:9443/oauth2/authorize?scope=openid+profile+email&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fc%2Fportal%2Flogin&state=2e48dc844de1a6e94be1dc08b3eedf95&client_id=4U2friUHdvkyTPazAlGyblSWi6Ia
Content-Length: 0
Date: Tue, 04 Oct 2016 03:04:09 GMT

Identity Server, after a successful login redirect with following,

GET /c/portal/login?code=7bc0b59a-b36f-33ea-87eb-30f63f857d8f&state=2e48dc844de1a6e94be1dc08b3eedf95&session_state=6551ef8e441ae6713a3e543b47a2040653498e85435c388439a10917450eb6b2.3Db30aZASYeVb2IV0mcQgA HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:49.0) Gecko/20100101 Firefox/49.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en
Accept-Encoding: gzip, deflate
Cookie: COOKIE_SUPPORT=true; GUEST_LANGUAGE_ID=en_US
DNT: 1
Connection: keep-alive
Upgrade-Insecure-Requests: 1

To me it seems like when the OIDC plugin generate the state, that's not somehow persisted for the access token generation request?

BTW is the code for the plugin available somewhere? Thanks.

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.