Coder Social home page Coder Social logo

pow-auth / assent Goto Github PK

View Code? Open in Web Editor NEW
387.0 8.0 45.0 568 KB

Multi-provider framework in Elixir

Home Page: https://powauth.com

License: MIT License

Elixir 100.00%
multi-provider elixir oauth2 oauth apple-sign-in sign-in-with-apple auth0 azure-active-directory basecamp discord

assent's Introduction

Assent

Github CI hexdocs.pm hex.pm

Multi-provider authentication framework.

Features

  • Includes the following base strategies:
    • OAuth 1.0 - Assent.Strategy.OAuth
    • OAuth 2.0 - Assent.Strategy.OAuth2
    • OpenID Connect - Assent.Strategy.OIDC
  • Includes the following provider strategies:
    • Apple Sign In - Assent.Strategy.Apple
    • Auth0 - Assent.Strategy.Auth0
    • Azure AD - Assent.Strategy.AzureAD
    • Basecamp - Assent.Strategy.Basecamp
    • DigitalOcean - Assent.Strategy.DigitalOcean
    • Discord - Assent.Strategy.Discord
    • Facebook - Assent.Strategy.Facebook
    • Github - Assent.Strategy.Github
    • Gitlab - Assent.Strategy.Gitlab
    • Google - Assent.Strategy.Google
    • Instagram - Assent.Strategy.Instagram
    • LINE Login - Assent.Strategy.LINE
    • Linkedin - Assent.Strategy.Linkedin
    • Spotify - Assent.Strategy.Spotify
    • Strava - Assent.Strategy.Strava
    • Slack - Assent.Strategy.Slack
    • Stripe Connect - Assent.Strategy.Stripe
    • Twitter - Assent.Strategy.Twitter
    • VK - Assent.Strategy.VK

Installation

Add Assent to your list of dependencies in mix.exs:

defp deps do
  [
    # ...
    {:assent, "~> 0.2.9"}
  ]
end

Run mix deps.get to install it.

HTTP client installation

By default, Req is used if you have it in your dependency list. If not, Erlang's :httpc will be used instead.

If you are using :httpc you should add the following dependencies to enable SSL validation:

defp deps do
  [
    # ...
    # Required for SSL validation when using the `:httpc` adapter
    {:certifi, "~> 2.4"},
    {:ssl_verify_fun, "~> 1.1"}
  ]
end

You must also add :inets to :extra_applications in mix.exs:

def application do
  [
    # ...
    extra_applications: [
      # ...
      :inets
    ]
  ]
end

This is not necessary if you use another HTTP adapter like Req or Finch.

Getting started

A strategy consists of two phases; request and callback. In the request phase, the user would normally be redirected to the provider for authentication and then returned to initiate the callback phase.

Single provider example

defmodule ProviderAuth do
  import Plug.Conn

  alias Assent.{Config, Strategy.Github}

  @config [
    client_id: "REPLACE_WITH_CLIENT_ID",
    client_secret: "REPLACE_WITH_CLIENT_SECRET",
    redirect_uri: "http://localhost:4000/auth/github/callback"
  ]

  # http://localhost:4000/auth/github
  def request(conn) do
    @config
    |> Github.authorize_url()
    |> case do
      {:ok, %{url: url, session_params: session_params}} ->
        # Session params (used for OAuth 2.0 and OIDC strategies) will be
        # retrieved when user returns for the callback phase
        conn = put_session(conn, :session_params, session_params)

        # Redirect end-user to Github to authorize access to their account
        conn
        |> put_resp_header("location", url)
        |> send_resp(302, "")

      {:error, error} ->
        # Something went wrong generating the request authorization url
    end
  end

  # http://localhost:4000/auth/github/callback
  def callback(conn) do
    # End-user will return to the callback URL with params attached to the
    # request. These must be passed on to the strategy. In this example we only
    # expect GET query params, but the provider could also return the user with
    # a POST request where the params is in the POST body.
    %{params: params} = fetch_query_params(conn)

    # The session params (used for OAuth 2.0 and OIDC strategies) stored in the
    # request phase will be used in the callback phase
    session_params = get_session(conn, :session_params)

    @config
    # Session params should be added to the config so the strategy can use them
    |> Config.put(:session_params, session_params)
    |> Github.callback(params)
    |> case do
      {:ok, %{user: user, token: token}} ->
        # Authorization succesful

      {:error, error} ->
        # Authorizaiton failed
    end
  end
end

Multi-provider example

This is a generalized flow that's similar to what's used in PowAssent.

config :my_app, :strategies,
  github: [
    client_id: "REPLACE_WITH_CLIENT_ID",
    client_secret: "REPLACE_WITH_CLIENT_SECRET",
    strategy: Assent.Strategy.Github
  ],
  # ...
defmodule MultiProviderAuth do
  alias Assent.Config

  @spec request(atom()) :: {:ok, map()} | {:error, term()}
  def request(provider) do
    config = config!(provider)

    config[:strategy].authorize_url()
  end

  @spec callback(atom(), map(), map()) :: {:ok, map()} | {:error, term()}
  def callback(provider, params, session_params) do
    config = config!(provider)

    config
    |> Assent.Config.put(:session_params, session_params)
    |> config[:strategy].callback(params)
  end

  defp config!(provider) do
    config =
      Application.get_env(:my_app, :strategies)[provider] ||
        raise "No provider configuration for #{provider}"
    
    Config.put(config, :redirect_uri, "http://localhost:4000/oauth/#{provider}/callback")
  end
end

Custom provider

You can create custom strategies. Here's an example of an OAuth 2.0 implementation using Assent.Strategy.OAuth2.Base:

defmodule TestProvider do
  use Assent.Strategy.OAuth2.Base

  @impl true
  def default_config(_config) do
    [
      # `:base_url` will be used for any paths below
      base_url: "http://localhost:4000/api/v1",
       # Definining an absolute URI overrides the `:base_url`
      authorize_url: "http://localhost:4000/oauth/authorize",
      token_url: "/oauth/access_token",
      user_url: "/user",
      authorization_params: [scope: "email profile"],
      auth_method: :client_secret_post
    ]
  end

  @impl true
  def normalize(_config, user) do
    {:ok,
      # Conformed to https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.5.1
      %{
        "sub"      => user["sub"],
        "name"     => user["name"],
        "nickname" => user["username"],
        "email"    => user["email"]
      # },
      # # Provider specific data not part of the standard claims spec
      # %{
      #   "http://localhost:4000/bio" => user["bio"]
      }
    }
  end
end

The normalized user map should conform to the OpenID Connect Core 1.0 Standard Claims spec, and should return either {:ok, userinfo_claims} or {:ok, userinfo_claims, additional}. Any keys defined in the userinfo claims that aren't part of the specs will not be included in the user map. Instead, they should be set in the additional data that will then be merged on top of the userinfo claims excluding any keys that have already been set.

You can use any of the Assent.Strategy.OAuth2.Base, Assent.Strategy.OAuth.Base, and Assent.Strategy.OIDC.Base macros to set up the strategy.

If you need more control over the strategy than what the macros give you, you can implement your provider using the Assent.Strategy behaviour:

defmodule TestProvider do
  @behaviour Assent.Strategy

  @spec authorize_url(Keyword.t()) :: {:ok, %{url: binary()}} | {:error, term()}
  def authorize_url(config) do
    # Generate authorization url
  end

  @spec callback(Keyword.t(), map()) :: {:ok, %{user: map(), token: map()}} | {:error, term()}
  def callback(config, params) do
    # Handle callback response
  end
end

HTTP Client

Assent supports Req, Finch, and :httpc out of the box. The Req HTTP client adapter will be used by default if enabled, otherwise Erlang's :httpc adapter will be included.

You can explicitly set the HTTP client adapter in the configuration:

config = [
  client_id: "REPLACE_WITH_CLIENT_ID",
  client_secret: "REPLACE_WITH_CLIENT_SECRET",
  http_adapter: Assent.HTTPAdapter.Httpc
]

Or globally in the config:

config :assent, http_adapter: Assent.HTTPAdapter.Httpc

Req

Req doesn't require any additional configuration and will work out of the box:

defp deps do
  [
    # ...
    {:req, "~> 0.4"}
  ]
end

:httpc

If Req is not available, Erlangs built-in :httpc is used for requests. SSL verification is automatically enabled when :certifi and :ssl_verify_fun packages are available. :httpc only supports HTTP/1.1.

defp deps do
  [
    # ...
    # Required for SSL validation if using the `:httpc` adapter
    {:certifi, "~> 2.4"},
    {:ssl_verify_fun, "~> 1.1"}
  ]
end

You must include :inets to :extra_applications to include :httpc in your release.

Finch

Finch will require a supervisor in your application.

Update mix.exs:

defp deps do
  [
    # ...
    {:finch, "~> 0.16"}
  ]
end

Ensure you start the Finch supervisor in your application, and set :http_adapter in your provider configuration using your connection pool:

config = [
  client_id: "REPLACE_WITH_CLIENT_ID",
  client_secret: "REPLACE_WITH_CLIENT_SECRET",
  http_adapter: {Assent.HTTPAdapter.Finch, supervisor: MyFinch}
]

JWT Adapter

By default the built-in Assent.JWTAdapter.AssentJWT is used for JWT parsing, but you can change it to any third-party library with a custom Assent.JWTAdapter. A JOSE adapter Assent.JWTAdapter.JOSE is included.

To use JOSE, update mix.exs:

defp deps do
  [
    # ...
    {:jose, "~> 1.8"}
  ]
end

And pass the :jwt_adapter with your provider configuration:

config = [
  client_id: "REPLACE_WITH_CLIENT_ID",
  client_secret: "REPLACE_WITH_CLIENT_SECRET",
  jwt_adapter: Assent.JWTAdapter.JOSE
]

Or globally in the config:

config :assent, jwt_adapter: AssAssent.JWTAdapter.JOSE

LICENSE

(The MIT License)

Copyright (c) 2019-present Dan Schultzer & the Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

assent's People

Contributors

0x6a68 avatar danschultzer avatar dtluther avatar evnu avatar fabiosammy avatar gabrielrinaldi avatar hwuethrich avatar joetrimble avatar kianmeng avatar mbuhot avatar nayshins avatar ohmree avatar praveenperera avatar raygesualdo avatar schultzer avatar sjednac avatar sondr3 avatar wingyplus avatar yordis 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  avatar

assent's Issues

AssentJWT adapter returns unexpected value

The possible return values of Assent.JWTAdapter.AssentJWT.verify/3 do not match the callback definition of Assent.JWTAdapter.verify/3 🤔

Here is the callback definition:

@callback verify(binary(), binary() | map() | nil, Keyword.t()) :: {:ok, map()} | {:error, any()}

Calling Assent.JWTAdapter.AssentJWT.verify/2 can also return just :error 😲 Have a look into the example below:

token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImV4YW1wbGUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhc3NlbnQtaXNzdWUiLCJleHAiOjE3MTk0ODk5MDcsInN1YiI6IjQyIn0.o3Lijaa_Q5m1Tr7A6myopsdtO771SlVu9rK-ElIqdYcj5eo5PochP3cqG7NFw76TqyRMdNwkr-OQevAWUbcireZ-dSKtkhtYm91M2sVM_RI5s7LTfdbUTHEleCAg_x0pC1mkDRmKSKBPuooEKypIl6P6ME0GogC_4SthzYRx3gks6m9TNI8wCzRhOaGuLEvnOm8KoQXpreVLfibe_C_v30AiJVzxMoqskQKJ9hagE34I9SB6ut3_ZsczMisjXe5cGLcsWSctAoi-JaIqUk3pMyfgUVVvt8ZzbV2LvqPz2O1vOAT-QDui68szYFAJh9IaNbwVC9QdLmthdg01DLYrA"

secret = %{
  "alg" => "RS256",
  "e" => "AQAB",
  "kid" => "example",
  "kty" => "RSA",
  "n" => "g5IE_tFgft5wRYPwivPY4QpNc6IpbZv5w24tW2rKdS74ntwhPQo38yahAOUujTAbpUvN3motDtJOkjov9O6fxhFkjOEA4zxXFGxHWyMwloRjNen9uScbi88EuVaSuTWKoq4C9FE650222QrtU0SImMSgi166sbTbi5bvS9hanSphksmw8vdVff56aFE5jOpVXNEoFoj3CFTRfesIhht9qXJp-HFbbSkDcyeQ1Y5rv5nzKQXMONTxxhV-qaJ03BNLxFVOP9eqeVTUHQoQ262qpbz-3qRWxxqy5Q2V0g4yk-IJPV2u6yNd5SojsRK3V5YIC3b0_VuDXN_we0o-sOjESQ",
  "use" => "sig"
}

Assent.JWTAdapter.AssentJWT.verify(id_token, secret, [json_library: Jason])

The :error is caused by a broken token I passed in. When using a valid token (see below), you get an {:ok, _} tuple.

eyJhbGciOiJSUzI1NiIsImtpZCI6ImV4YW1wbGUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhc3NlbnQtaXNzdWUiLCJleHAiOjE3MTk0ODk5MDcsInN1YiI6IjQyIn0.Zo3Lijaa_Q5m1Tr7A6myopsdtO771SlVu9rK-ElIqdYcj5eo5PochP3cqG7NFw76TqyRMdNwkr-OQevAWUbcireZ-dSKtkhtYm91M2sVM_RI5s7LTfdbUTHEleCAg_x0pC1mkDRmKSKBPuooEKypIl6P6ME0GogC_4SthzYRx3gks6m9TNI8wCzRhOaGuLEvnOm8KoQXpreVLfibe_C_v30AiJVzxMoqskQKJ9hagE34I9SB6ut3_ZsczMisjXe5cGLcsWSctAoi-JaIqUk3pMyfgUVVvt8ZzbV2LvqPz2O1vOAT-QDui68szYFAJh9IaNbwVC9QdLmthdg01DLYrA

Error with multitenant Azure login

[error] #PID<0.888.0> running TaskAppWeb.Endpoint (connection #PID<0.828.0>, stream id 27) terminated
Server: localhost:4006 (https)
Request: POST /auth/azure/callback
** (exit) an exception was raised:
** (RuntimeError) Invalid issuer "https://login.microsoftonline.com/270a4662-e407-4044-b299-1a62945d3893/v2.0" in ID Token
(pow_assent 0.4.6) lib/pow_assent/phoenix/controllers/authorization_controller.ex:209: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1
(pow_assent 0.4.6) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
(pow_assent 0.4.6) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
(phoenix 1.4.16) lib/phoenix/router.ex:288: Phoenix.Router.call/2
(task_app 0.1.0) lib/task_app_web/endpoint.ex:1: TaskAppWeb.Endpoint.plug_builder_call/2
(task_app 0.1.0) lib/plug/debugger.ex:132: TaskAppWeb.Endpoint."call (overridable 3)"/2
(task_app 0.1.0) lib/task_app_web/endpoint.ex:1: TaskAppWeb.Endpoint.call/2
(phoenix 1.4.16) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4

OIDC normalize/2 to conform to OAuth2 normalize/2

I realize that the typespec for normalize/2 belonging to the OIDC strategy is different than the one described in the docs, and the OAuth2 one, and it is making my dialyzer fail.

I believe it should be changed to

@callback normalize(Keyword.t(), map()) :: {:ok, map()} | {:ok, map(), map()} | {:error, term()}

to allow claims that don't conform to the OIDC spec

Assent.Strategy.Apple callback verification fails when using Assent.JWTAdapter.JOSE

When using the Assent.JWTAdapter.JOSE for Assent.Strategy.Apple, the following error occurs:

** (FunctionClauseError) no function clause matching in Assent.JWTAdapter.JOSE.jwk/2    
    
    The following arguments were given to Assent.JWTAdapter.JOSE.jwk/2:
    
        # 1
        "RS256"
    
        # 2
        nil
    
    Attempted function clauses (showing 3 out of 3):
    
        defp jwk(<<"HS"::binary(), _rest::binary()>>, secret)
        defp jwk(_alg, key) when is_binary(key)
        defp jwk(_alg, key) when is_map(key)
    
    (assent) lib/assent/jwt_adapter/jose.ex:24: Assent.JWTAdapter.JOSE.jwk/2
    (assent) lib/assent/jwt_adapter/jose.ex:46: Assent.JWTAdapter.JOSE.verify/3
    (assent) lib/assent/strategies/apple.ex:108: Assent.Strategy.Apple.get_user/2
    (assent) lib/assent/strategies/oauth2.ex:237: Assent.Strategy.OAuth2.fetch_user/3
    (assent) lib/assent/strategies/oauth2/base.ex:69: Assent.Strategy.OAuth2.Base.callback/3

The error is caused by a nil parameter being passed for the public key in the following line:

with {:ok, jwt} <- Helpers.verify_jwt(token["id_token"], nil, config) do

I was able to work around the issue by providing the Apple Public Key from https://appleid.apple.com/auth/keys as a configuration parameter as follows:

  def get_user(config, token) do
    public_key = Config.get(config, :public_key, nil)
    with {:ok, jwt} <- Helpers.verify_jwt(token["id_token"], public_key, config) do
      {:ok, jwt.claims}
    end
  end

with the :public_key specified in my config as follows:

config :my_app, :pow_assent,
  providers: [
    apple: [
      strategy: Assent.Strategy.Apple,
      client_id: System.get_env("APPLE_CLIENT_ID"),
      team_id: System.get_env("APPLE_TEAM_ID"),
      private_key_id: System.get_env("APPLE_PRIVATE_KEY_ID"),
      private_key: System.get_env("APPLE_PRIVATE_KEY"),
      jwt_adapter: Assent.JWTAdapter.JOSE,
      public_key: %{
        "kty" => "RSA",
        "kid" => "AIDOPK1",
        "use" => "sig",
        "alg" => "RS256",
        "n" =>
          "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w",
        "e" => "AQAB"
      }
    ]
  ]

Hard coding the Apple public key is just a work around to get me up and running. I notice that the Ueberauth Apple strategy fetches the Apple public key during the verification phase and I guess the Assent Apple strategy should do the same?

Facebook user info is not decoded from JSON

It looks like the result for /me for Facebook is returning a JSON payload but the payload is never decoded and passed to normalize where it fails. I don't believe this a configuration issue but I could be wrong. I've included my config and some of the dbg trail below:

config #=> [
  strategy: Assent.Strategy.Facebook,
  base_url: "https://graph.facebook.com/v4.0",
  authorize_url: "https://www.facebook.com/v4.0/dialog/oauth",
  token_url: "/oauth/access_token",
  user_url: "/me",
  authorization_params: [scope: "email"],
  user_url_request_fields: "email,name,first_name,last_name,middle_name,link",
  auth_method: :client_secret_post,
  session_params: %{state: "b12f74c55b98953535c75f3143cd8d4e475f8c3b2a"},
  redirect_uri: "https://local.host:5301/auth/facebook/callback",
  client_id: "<redacted>",
  client_secret: "<redacted>",
  redirect_path: "/auth/facebook/callback"
]
|> process_user_response() #=> {:ok,
 "{\"email\":\"doneth7\\u0040me.com\",\"name\":\"John Doneth\",\"first_name\":\"John\",\"last_name\":\"Doneth\",\"id\":\"7425858337461382\"}"}
** (FunctionClauseError) no function clause matching in Access.get/3
   (elixir 1.16.1) lib/access.ex:307: Access.get("{\"email\":\"doneth7\\u0040me.com\",\"name\":\"John Doneth\",\"first_name\":\"John\",\"last_name\":\"Doneth\",\"id\":\"7425858337461382\"}", "id", nil)
   (assent 0.2.9) lib/assent/strategies/facebook.ex:92: Assent.Strategy.Facebook.normalize/2

Allow passing access token to callback

Right now assent assumes that the client will pass code in parameter list in the callback phase. There are cases, though, when the client has an access token already (assent does not need to fetch it and does not have the means to do so). Would it be possible and desirable to accept access tokens as well in OAuth2 strategy.

My specific use case is Facebook login on mobile devices where client side libraries only allow to fetch the access token straight away.

Assent.Strategy.Apple doesn't work with Assent.JWTAdapter.AssentJWT

The client_secret generated by Assent.JWTAdapter.AssentJWT doesn't have a valid signature and returns an "invalid_client" error when handling the callback from Apple. I confirmed that the signature was not valid by decoding the client secret at https://jwt.io/ and providing the public key for my Apple generated private key.

In case this helps, I used the following command to generate a public key from the private key:

openssl ec -in AuthKey_xxxxxxxxx.p8 -pubout -out public.pem

Switching to Assent.JWTAdapter.JOSE adapter does generate the correct client secret.

OTP 24 :crypto.hmac/3 doesn't exist anymore

warning: :crypto.hmac/3 is undefined or private. Did you mean one of:

      * mac/3
      * mac/4
      * macN/4
      * macN/5

  lib/assent/strategies/facebook.ex:122: Assent.Strategy.Facebook.appsecret_proof/2

warning: :crypto.hmac/3 is undefined or private. Did you mean one of:

      * mac/3
      * mac/4
      * macN/4
      * macN/5

  lib/assent/strategies/oauth.ex:173: Assent.Strategy.OAuth.gen_signature/6

warning: :crypto.hmac/3 is undefined or private. Did you mean one of:

      * mac/3
      * mac/4
      * macN/4
      * macN/5

  lib/assent/jwt_adapter/assent_jwt.ex:43: Assent.JWTAdapter.AssentJWT.sign_message/3

Yandex OAuth Strategy

Hello there, would you mind if I added strategy for Yandex OAuth? It might be useful for someone who uses that provider.

Clarify apple usage

Hi guys,

While implementing the endboss, aka sign in with Apple I ran into some issues.

The docs in Assent.Strategy.Apple specifically talk about the JS SDK, but I couldn't figure out how to get the state to be passed
in and read back correctly.

In the end I resorted to just using the Routes.pow_assent_authorization_url/3, which worked like a charm.

Long story short, it would be great to give people a pointer to just using the regular /auth/apple/new flow, especially since the apple docs only talk about their JS SDK as well.

Bad Request - Invalid Header with Entra ID (Azure AD) and Finch

I am getting a 400 when sending requests to login.microsoftonline.com while using the Finch adapter.

%{
  name: InternalFinch,
  request: %Finch.Request{
    body: nil,
    headers: [{"User-Agent", "Assent-0.2.9"}],
    host: "login.microsoftonline.com",
    method: "GET",
    path: "/organizations/v2.0/.well-known/openid-configuration",
    port: 443,
    private: %{},
    query: nil,
    scheme: :https,
    unix_socket: nil
  },
  result: {:ok,
   %Finch.Response{
     body: "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\"http://www.w3.org/TR/html4/strict.dtd\">\r\n<HTML><HEAD><TITLE>Bad Request</TITLE>\r\n<META HTTP-EQUIV=\"Content-Type\" Content=\"text/html; charset=us-ascii\"></HEAD>\r\n<BODY><h2>Bad Request - Invalid Header</h2>\r\n<hr><p>HTTP Error 400. The request has an invalid header name.</p>\r\n</BODY></HTML>\r\n",
     headers: [
       {"content-type", "text/html; charset=us-ascii"},
       {"date", "Mon, 11 Mar 2024 15:55:17 GMT"},
       {"connection", "close"},
       {"content-length", "339"}
     ],
     status: 400
   }},
}

I'm not sure if Microsoft is flagging this User-Agent as a bot, or there's actually an issue with the payload, but here are the only headers that seem to be sent in the request:

    headers: [{"User-Agent", "Assent-0.2.9"}],

Do we have any control over the headers sent in the payload while using an http adapter? I'd like to try and debug if adding/removing this header can make a difference.

Here are my dependencies:

{:assent, "~> 0.2.9"},
{:finch, "~> 0.16"},

any help would be much appreciated!

Sign in with Apple: Strategy failed with error: :enoent

So i'm trying to implement sign in with apple using pow_assent, but after signing in, it just returns an error flash Something went wrong, and you couldn't be signed in. Please try again

When I looked at the logs it shows:

Screenshot 2023-05-30 at 5 32 24 PM

Do you have any idea what causes and how to fix this? Thanks!

Auth0: Support Variable-Length Access Token and Authorization Codes

Hello Dan & Pow crew!

I've found tremendous value in this library and I've just realized I should be pushing for us to sponsor the project!

Auth0 has notified me about a deprecation in their API for fixed length access tokens and authorization codes. They're apparently doing a hard cut-over on April 12th. At that point, I expect things will stop working properly as my Auth0 log says the following when I log in:

Fixed Length of Access Token & Authorization Code: This feature is being deprecated. Please see https://auth0.com/docs/product-lifecycle/deprecations-and-migrations#opaque-access-token-for-userinfo for more information.

What's the best course of action to make sure I remain compatible? I'm going to need to dig in deeper, but this is a bit foreign to me. I could certainly use a hand!

And thank you so much for the time & energy put into the pow/assent ecosystem.

Adam

Add connection pool to Mint HTTP adapter (turn it into a GenServer)

Based on pow-auth/pow_assent#89

So the current implementation of PowAssent.HTTPAdapter.Mint doesn't pool HTTP/2 connections. For it to do that, I'll need to set it up as a GenServer. This way all connections can be kept open and all requests can be faster and more efficient. As new requests are coming in, the connections will be created if they don't exist in the GenServer state.

I haven't decided yet if the GenServer should be an extra layer on top of the current implementation so they work in parallel, or it should just replace it entirely and you are required to start it up to use Mint. I feel the latter is the better choice, since there is not really much of a point in using Mint without connection pool.

Any feedback/thoughts are welcome, I only have a superficial understanding of HTTP/2 🙂

Reliance on inets?

Hi,

Using assent to authenticate through oauth2 on a micro-app that had no ecto backup nor need to record user information past authorization, I encountered an error after the built succeed:

web_1  | ** (exit) an exception was raised:
web_1  |     ** (UndefinedFunctionError) function :httpc.request/4 is undefined (module :httpc is not available)
web_1  |         :httpc.request(:post, {'https://api.intra.42.fr/oauth/token', [{'User-Agent', 'Assent-0.1.16'}], 'application/x-www-form-urlencoded', 'code=d44d4959376eb30953852cc66cb4ebce50c4018dfda2dc79ebd26414bcac3ab2&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fsession%2Fcallback&grant_type=authorization_code&client_id=54d568770a5f2984fdacf76bcf38d469edbe71b55ccd050dd83a63fd94f0ac6b&client_secret=b237063058109ba31701030463a9a77616fecab6b866e53f1b50e1c03da9d3e5'}, [], [])
web_1  |         (assent) lib/assent/http_adapter/httpc.ex:25: Assent.HTTPAdapter.Httpc.request/5
web_1  |         (assent) lib/assent/strategy.ex:42: Assent.Strategy.request/5
web_1  |         (assent) lib/assent/strategies/oauth2.ex:250: Assent.Strategy.OAuth2.grant_access_token/3
web_1  |         (assent) lib/assent/strategies/oauth2.ex:129: Assent.Strategy.OAuth2.callback/3
# ... here be where the binding was used in my app

That happened during the callback of the oauth2 transaction, and completely baffled me. It looked like something like a missing dependency or something, yet I did not see anything like this in assent's docs, and when I googled it, it looked exactly like the inets lib was missing...

I finally figured out how to fix this, adding inets to the extra_applications:

# in app/mix.exs
  def application do
    [
      # ...
      extra_applications: [
        # ...
        :inets
      ]
    ]
  end

...aaaand... that worked.

Maybe it should be noted in the installation docs that inets is necessary to be started for it to work. I'm willing to make a PR explaining it, but I'd like for someone knowing assent from the inside out to explain why and in which cases it's necessary to have inets as a dependency first (so I don't mislead others either).

So, there. Thank you for that awesome lib by the way.

Google Sign in for mobile strategy

Currently, assent has support for Google Sign in for the web, the client sends the code and scope and the strategy retrieves from google the JWT with the data.

This works fine for the web, but not so much for mobile apps where normally the client will retrieve the JWT (id token) directly and the server job is to validate it.

Since I need exactly this case, I created a new strategy for it, you can add it to Assent if you want, and also I would appreciate it if you can take a look and see if there is some flaw in the logic.

The strategy itself is very simple, you simply send the id_token to id and it will validate it.

defmodule Core.Identity.GoogleSignIn.Strategy do
  @moduledoc false

  alias Core.Identity.GoogleSignIn.JWTManager

  @behaviour Assent.Strategy

  @impl true
  def callback(_, params) do
    %{id_token: id_token} = params

    case JWTManager.verify_and_validate(id_token) do
      {:ok, user} -> {:ok, %{user: user}}
      {:error, _} -> {:error, :invalid_token}
    end
  end

  @impl true
  def authorize_url(_), do: throw("not implemented")
end

For validation, here are my modules:

defmodule Core.Identity.GoogleSignIn.JWTVerifyHook do
  @moduledoc false

  use Joken.Hooks

  @impl true
  def before_verify(_, {jwt, %Joken.Signer{}}) do
    with {:ok, %{"kid" => kid}} <- Joken.peek_header(jwt),
         {:ok, algorithm, key} <- GoogleCerts.fetch(kid) do
      {:cont, {jwt, Joken.Signer.create(algorithm, key)}}
    else
      _ -> {:halt, {:error, :no_signer}}
    end
  end
end
defmodule Core.Identity.GoogleSignIn.JWTManager do
  @moduledoc false

  alias Core.Identity.GoogleSignIn.JWTVerifyHook

  use Joken.Config, default_signer: nil

  add_hook(JWTVerifyHook)

  @iss "https://accounts.google.com"

  @impl true
  def token_config do
    default_claims(skip: [:aud, :iss])
    |> add_claim("iss", nil, fn iss -> iss == @iss end)
    |> add_claim("aud", nil, fn aud -> aud == aud() end)
  end

  defp aud do
    Application.fetch_env!(:core, :pow_assent)
       |> Keyword.fetch!(:providers)
       |> Keyword.fetch!(:google_sign_in)
       |> Keyword.fetch!(:client_id)
  end
end

Should OIDC fetch_user, fetch_userinfo, and validate_id_token allow for dynamic OpenID configuration?

Hi, and thanks for building and maintaining this 👋.

In the configuration documentation for OIDC, :openid_configuration isn't strictly required since it can be fetched from :openid_configuration_uri if it isn't defined. Similarly, :openid_configuration_uri is also optional, since it defaults to /.well-known/openid-configuration based on :site.

Both authorize_url/1 and callback/3 work this way by calling openid_configuration/1. However, fetch_user/2, fetch_userinfo/2, and validate_id_token/2 are using Config.fetch/2 to resolve configuration, so they aren't getting it dynamically.

Should these work consistently? I'm a bit new to both Elixir and OIDC, so it's quite possible my understanding is off here.

If the intent is to have them all get :openid_configuration dynamically, I'm happy to try submitting a PR.

Thanks for your time!

Warnings with Elixir 1.11

During compilation, I get the warnings below repeatedly. It seems to be because oauther needs to have :crypto and :public_key its :extra_applications config.

I raised it here: lexmag/oauther#18

I also notice it's not seen a commit in over 2.5 years, so I thought I would bring the point up over here, as well.

Thank you, Dan, for all the hard work & sharing!

Multiple audiences

Hello! I'm pretty new to Elixir so forgive me if this has an easy answer. I'm creating a custom OIDC provider (Netsuite) that issues 2 audiences in a list, one of which is the client ID.

In the oidc.ex, audience validation simply matches what's returned in the claim with the client_id but unfortunately this always produces the "Invalid audience" error.

How would I go about filtering the data so that I could either remove the other audience or add it to what's being matched? I'm trying to follow along with the Apple OIDC provider but can't quite make sense of it.

support code_verifier/code_challenge in OAuth2

It is becoming an OAuth recommendation that state and code_verifier/code_challenge be used together to ensure that nobody can hijack your OAuth exchange code in server flows.

This simply allows you to pass your verifier to the oauth2 callback (it was literally impossible before) in case you need to. It is up to the user to generate your challenge and encode it.

This is an MVP for my use case so I am opening an issue and immediately opening a pull request for it.

MSIS9691: Received invalid OAuth request. The Basic Authorization header must be Base64 encoded.

I am trying to use PowAssent with OAuth2 strategy against an ADFS-provider and it's almost working, but even though I can see the access_token as a param in the request, something is not quite right yet. Probably I am just missing something relevant. I thought, I'd report just in case anyone else faces this (ungoogleable) error code in the subject.

Glad for any pointers, let me know, if you need more info.
Thank you and cheers!

P.S. I did this:
PPS: I realize now that this might belong into the Assent repo 🤕

scope "/" do
    pipe_through :skip_csrf_protection

    post "/auth/:provider/callback", PowAssent.Phoenix.AuthorizationController, :callback
end

================================================
warning: This request will NOT be verified for valid SSL certificate
(assent) lib/assent/http_adapter/httpc.ex:22: Assent.HTTPAdapter.Httpc.request/5
(assent) lib/assent/strategy.ex:42: Assent.Strategy.request/5
(assent) lib/assent/strategies/oauth2.ex:222: Assent.Strategy.OAuth2.get_access_token/2
(assent) lib/assent/strategies/oauth2.ex:119: Assent.Strategy.OAuth2.callback/3
(assent) lib/assent/strategies/oauth2/base.ex:69: Assent.Strategy.OAuth2.Base.callback/3
(pow_assent) lib/pow_assent/plug.ex:74: PowAssent.Plug.callback/4
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:39: PowAssent.Phoenix.AuthorizationController.process_callback/2
(pow) lib/pow/phoenix/controllers/controller.ex:99: Pow.Phoenix.Controller.action/3
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
(phoenix) lib/phoenix/router.ex:288: Phoenix.Router.call/2
(my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
(my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
(my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
(phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4
(cowboy) c:/src/my_app/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
(cowboy) c:/src/my_app/deps/cowboy/src/cowboy_stream_h.erl:320: :cowboy_stream_h.execute/3

[error] #PID<0.1578.0> running SmartpowerWeb.Endpoint (connection #PID<0.1549.0>, stream id 5) terminated

Server: localhost:4200 (http)
Request: POST /auth/oauth2/callback
** (exit) an exception was raised:
** (Assent.RequestError) Server responded with status: 400

Headers:
cache-control: no-store
date: Mon, 11 Nov 2019 21:27:16 GMT
pragma: no-cache
server: Microsoft-HTTPAPI/2.0 Microsoft-HTTPAPI/2.0
content-length: 146
content-type: application/json;charset=UTF-8

Body:
%{"error" => "invalid_request", "error_description" => "MSIS9691: Received invalid OAuth request. The Basic Authorization header must be Base64 encoded."}

         (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:190: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1
    (pow) lib/pow/phoenix/controllers/controller.ex:99: Pow.Phoenix.Controller.action/3
    (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
    (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
    (phoenix) lib/phoenix/router.ex:288: Phoenix.Router.__call__/2
    (myapp) lib/myapp_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
    (myapp) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
    (myapp) lib/myapp_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
    (phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4
    (cowboy) c:/src/myapp/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
    (cowboy) c:/src/myapp/deps/cowboy/src/cowboy_stream_h.erl:320: :cowboy_stream_h.execute/3
    (cowboy) c:/src/myapp/deps/cowboy/src/cowboy_stream_h.erl:302: :cowboy_stream_h.request_process/3
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

JWT logic

There is very weak JWT parsing in the current library. It would probably be best if there actually is some validation done on the JWT instead. It should either be a built-in module, or a third-party module that can be changed like with :http_adapter.

Callback param "user" is not parsed when using Apple strategy with "name" scope

When using the "name" scope with sign-in with Apple, it doesn't seem like the "user" map that contains "firstName" and "lastName" is decoded, or added to the token map in OAuth2.callback/3. This means that when Strategy.Apple attempts to merge the name params into the other user params, nothing is there. In my application this results in "given_name" and "family_name" being absent when trying to insert a new user into the database.

I was able to fix this in my project by decoding the "user" map in OAuth2.callback/3, and assigning it to token before passing it off to fetch_user_with_strategy as follows:

  @impl true
  @spec callback(Config.t(), map(), atom()) :: {:ok, %{user: map(), token: map()}} | {:error, term()}
  def callback(config, params, strategy \\ __MODULE__) do
    with {:ok, session_params} <- Config.fetch(config, :session_params),
         :ok                   <- check_error_params(params),
         {:ok, code}           <- fetch_code_param(params),
         {:ok, redirect_uri}   <- Config.fetch(config, :redirect_uri),
         :ok                   <- maybe_check_state(session_params, params),
-        {:ok, token}          <- grant_access_token(config, "authorization_code", code: code, redirect_uri: redirect_uri) do
+        {:ok, token}          <- grant_access_token(config, "authorization_code", code: code, redirect_uri: redirect_uri),
+        token                 <- maybe_add_user_callback_params(config, token, params) do

      fetch_user_with_strategy(config, token, strategy)
    end
  end

+ defp maybe_add_user_callback_params(config, token, %{"provider" => "apple", "user" => user}) do
+   Map.put(token, "user", Config.json_library(config).decode!(user))
+ end
+ defp maybe_add_user_callback_params(_config, token, _params), do: token

Does this seem right? I've looked pretty closely at the code paths involved, and don't see any other place where this information seems to be handled. Happy to open a PR if this is a real issue, and if the fix seems appropriate!

Boolean field may contain an error-tuple which can cause issues

The map returned by Assent.JWTAdapter.AssentJWT.verify/3 may have the verified?-key set to an {:error, _} tuple, which may cause issues when doing a simple check for trueness.

token = "..."
secret = %{...}

{:ok, details} = Assent.JWTAdapter.AssentJWT.verify(token, secret, [json_adapter: Jason])

if details.verified? do
  IO.puts("Everything is cool!")
else
  IO.puts("Verification failed.")
end

You may see Everthing is cool! although an error occurred, because verified? contains an {:error, _} tuple. Here's an example of where the tuple can come from:

defp decode_key(jwk) when is_map(jwk), do: {:error, "Can't decode JWK"}

Sadly, I cannot provide you with the token and secret I stumpled upon that error, but by looking at the code, you already see that the error-tuple is a possible value for the verified?-field.

Azure AD should expose more user info

I'm currently struggling to find a way how to map additional claims from Azure AD provider into user_identity changeset.

These are the claims that Azure AD returns:

claims: %{
    "aud" => "93572ac3-0740-4aa5-ad35-18ba25d5fe22",
    "email" => "[email protected]",
    "exp" => 1642465568,
    "iat" => 1642461668,
    "iss" => "https://login.microsoftonline.com/6ee623a2-0b05-4ea4-b931-79c555955cb1/v2.0",
    "name" => "Moravec Albert",
    "nbf" => 1642461668,
    "oid" => "09cbdc15-ccf1-43e9-a2fb-8e9d9513d5cc",
    "preferred_username" => "[email protected]",
    "rh" => "0.AToAoiPmbgULpE65MXnFVZVcscMqV5NAB6VKrTUYuiXV_iI6AO4.",
    "roles" => ["administrator", "manager"],
    "sub" => "1ct7-HTE7-CM5h5H7009_9lRRLdHiAHt1hY30ogqji0",
    "tid" => "6ee623a2-0b05-4ea4-b931-79c555955cb1",
    "uti" => "5aLPSh0CRUW_doLHsZxjAA",
    "ver" => "2.0"
}

Since standard OpenID Connect mapping is used, all of the claims except the standardized ones are thrown away. Is there a way to override this behavior?
I personally expected this to be doable at least on the Strategy level by customizing the normalize/2, but the user there is already stripped of all the claims.

Using Tesla for HTTP

Hey, first of all, thanks for the library, really good, but I have one question:

Do you think it could make sense to use Tesla as the HTTP abstraction so the library could use any of the Tesla adapters out there? Or we could have a an Assent.HTTPAdapter for Tesla, but then we would have adapter of the adapter xD.

Tesla currently have adapters for Finch, Gun, hackney, httpc, ibrowse and Mint.

Tesla also have a test adapter which might come in handy.

https://hexdocs.pm/tesla/Tesla.Adapter.html#content

`defoverride` for definitions from `Assent.Strategy` when `use`ing `Assent.Strategy.OAuth2.Base`

I'm implementing a provider, which needs an aditional body parameter when calling the token endpoint.
in order to override &Assent.Strategy.callback/2, i think that the additional defoverridable Helpers (or defoverridable callback: 2) is needed, otherwhise the implementation in my custom module is ignored.

** Should this overridable definition not be included in the __using/2 definition of Assent.Strategy.OAuth2.Base?

The use definition defines these behavoiurs:

@behaviour Assent.Strategy
@behaviour unquote(__MODULE__)

The only defoverridable in the Assent.Strategy.OAuth2.Base.__using__/1 Macro is this one:

defoverridable unquote(__MODULE__)

In order to override the callback/2 in my implementation module i need to do this:

defmodule CanteAuth.Assent.Strategy.Anaf do
  use Assent.Strategy.OAuth2.Base
 #Shouldn't this be in Assent.Strategy.OAuth2.Base.__using__/1?
  defoverridable Helpers
 ...
  @impl Helpers
  def callback(config, params) do

Is the macro Assent.Strategy.OAuth2.Base.__using__/1 really missing the line

defoverridable Helpers

or am I missing something obvious, for overriding functions imported from Assent.Strategy?

Thank you for this wonderful library! ❤️

Telegram authentication stratery

Telegram offers a convenient way of authenticating users by allowing them to enter their Telegram-linked phone number and confirm the request directly in the Telegram messenger app without a password.

However, they don’t support the traditional method of redirecting to their OAuth server to enter credentials. Instead, it is necessary to include their JS library on the page and either use an embedded iframe with a standard button (with limited styles) or - via an undocumented method - directly call the library's auth(callback) function with a callback to get the results. The official login widget also supports the redirect mode to a provided callback URL. This means that the authorize_url/1 call is useless for this implementation (more on this later).

Additionally, Telegram has published the Web Mini App API. This API allows direct interaction with many internal Telegram features. The Mini Web App is intended to be opened directly from the messenger app and to work inside the app.

To authenticate a user of a web app, Telegram sends a URL-encoded query string (protected by a hash) that contains all the initial data when opening the web app. This string doesn’t pass through a WebView instance and should be collected inside the frontend and sent to the backend for authentication. The backend needs to check the data’s authenticity by calculating the hash of the string and comparing it with the provided one, as well as validating the authentication date.

#152 implements the strategy.

Returning to the authorization_url/1: it is possible to use it to create a script to embed in the HTML page. The generator, depending on config parameters, can then generate different embedding options. However, this contradicts the direct meaning of authorization_url call.

WDYT?

Slack always "asks" for permission

Hello!

When a user uses the "Sign in with Slack" functionality to authenticate, Slack always "asks" for permission. That confuses users. Usually the identity provider asks once and just redirects on subsequent sign ins.

Screenshot 2021-09-30 at 10 59 38

I've asked Slack Support about it and they said:

It is the case that the prior SIWS (Sign in With Slack) functionality (which used identity.* scopes) asks for permission each time users accesses your resources.

However, we have updated this to a flow that's based on the OpenID Connect standard, and uses the openid scope. You can read more about that on the following updated SIWS page:

https://api.slack.com/authentication/sign-in-with-slack

I just wanted to track it here first. Probably I'll have some time to dig deeper about that in Assent. Any feedback is welcome :)

How to get the authorization code on react native for the Facebook strategy

I'm sorry, I know that this is not really directly related to assent.

The Facebook strategy requires an authorization code for the callback.

In this issue you say that "The Facebook strategy docs now highlights how to fetch the code client side to submit server side".

#34 (comment)

Our problem is that we are using https://github.com/facebook/react-native-fbsdk to implement the frontend facebook auth in react native. So we I don't think we can use the JS sdk to get the signed request and get the authorization code.

Please could you point me in the right direction on how to get the code from the accessToken in this situation?

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.