Giter Site home page Giter Site logo

softwaremill / akka-http-session Goto Github PK

View Code? Open in Web Editor NEW
439.0 49.0 58.0 809 KB

Web & mobile client-side akka-http sessions, with optional JWT support

Home Page: https://softwaremill.com/open-source/

License: Apache License 2.0

Scala 50.63% HTML 0.91% JavaScript 1.66% Java 46.80%
akka-http scala akka session java session-management csrf session-cookie

akka-http-session's Introduction

akka-http-session

Build Status Join the chat at https://gitter.im/softwaremill/akka-http-session Maven Central

akka-http is an Akka module, originating from spray.io, for building reactive REST services with an elegant DSL. pekko-http is an open-source fork of akka-http.

akka-http is a great toolkit for building backends for single-page or mobile applications. In almost all apps there is a need to maintain user sessions, make sure session data is secure and cannot be tampered with.

akka-http-session provides directives for client-side session management in web and mobile applications, using cookies or custom headers + local storage, with optional Json Web Tokens format support.

A comprehensive FAQ is available, along with code examples (in Java, but easy to translate to Scala) which answers many common questions on how sessions work, how to secure them and implement using akka-http.

Each akka-http-session module for akka-http has a corresponding pekko-http module, with a different group id (see bottom of the readme).

What is a session?

Session data typically contains at least the id or username of the logged in user. This id must be secured so that a session cannot be "stolen" or forged easily.

Sessions can be stored on the server, either in-memory or in a database, with the session id sent to the client, or entirely on the client in a serialized format. The former approach requires sticky sessions or additional shared storage, while using the latter (which is supported by this library) sessions can be easily deserialized on any server.

A session is a string token which is sent to the client and should be sent back to the server on every request.

To prevent forging, serialized session data is signed using a server secret. The signature is appended to the session data that is sent to the client, and verified when the session token is received back.

akka-http-session features

  • type-safe client-side sessions
  • sessions can be encrypted
  • sessions contain an expiry date
  • cookie or custom header transport
  • support for JWT
  • refresh token support (e.g. to implement "remember me")
  • CSRF tokens support
  • Java & Scala APIs

Example

You can try out a simple example by running com.softwaremill.example.ScalaExample or com.softwaremill.example.JavaExample and opening http://localhost:8080.

SessionManager & configuration

All directives require an (implicit for scala) instance of a SessionManager[T] (or SessionManager<T>), which can be created by providing a server secret (via a SessionConfig). The secret should be a long, random string unique to each environment your app is running in. You can generate one with SessionUtil.randomServerSecret(). Note that when you change the secret, all sessions will become invalid.

A SessionConfig instance can be created using Typesafe config. The only value that you need to provide is akka.http.session.server-secret, preferably via application.conf (then you can safely call SessionConfig.fromConfig) or by using SessionConfig.default().

You can customize any of the default config options either by modifying them through application.conf or by modifying the SessionConfig case class. If a value has type Option[], you can set it to None by using a none value in the config file (for both java and scala).

When using cookies, by default the secure attribute of cookies is not set (for development), however it is recommended that all sites use https and all cookies have this attribute set.

Client-side sessions

All session-related directives take at least two parameters:

  • session continuity: oneOff vs refreshable; specifies what should happen when the session expires. If refreshable and a refresh token is present, the session will be re-created. See below for details.
  • session transport: usingCookies vs usingHeaders

Typically, you would create aliases for the session-related directives which use the right parameters basing on the current request and logic specific to your application.

Cookies vs header

Session data can be sent to the client using cookies or custom headers. The first approach is the simplest to use, as cookies are automatically sent to the server on each request.

However, cookies have some security vulnerabilities, and are typically not used in mobile applications. For these scenarios, session data can be transported using custom headers (the names of the headers are configurable in the config).

When using headers, you need to store the session (and, if used, refresh-) tokens yourself. These tokens can be stored in-memory, or persistently e.g. using the browser's local storage.

You can dynamically decide which transport to use, basing e.g. on the user-agent or other request properties.

Basic usage

Sessions are typed. The T type parameter in SessionManager[T] (or SessionManager<T>) determines what data is stored in the session. Basic types like String, Int, Long, Float, Double and Map[String, String] (Map<String, String>) are supported out-of-the box. Support for other types can be added by providing a (an implicit for scala) SessionSerializer[T, String] (SessionSerializer<T, String>). For case classes, it's most convenient to use a MultiValueSessionSerializer[T] or (MultiValueSessionSerializer<T>) which should convert the instance into a String -> String map (nested types are not supported on purpose, as session data should be small & simple). Examples of SessionSerializer and MultiValueSessionSerializer usage can be found here for scala and here for java.

Here are code samples in scala and java illustrating how to create a session manager where the session content will be a single Long number.

The basic directives enable you to set, read and invalidate the session. To create a new client-side session (create and set a new session cookie), you need to use the setSession directive. See how it's done in java and scala.

Note that when using cookies, their size is limited to 4KB, so you shouldn't put too much data in there (the signature takes about 50 characters).

You can require a session to be present, optionally require a session or get a full description of possible session decode outcomes. Check java and scala examples for details.

If a required session is not present, by default a 403 HTTP status code is returned. Finally, a session can be invalidated. See how it's done in examples for java and scala.

Encrypting the session

It is possible to encrypt the session data by modifying the akka.http.session.encrypt-data config option. When sessions are encrypted, it's not possible to read their content on the client side.

The key used for encrypting will be calculated basing on the server secret.

Session expiry/timeout

By default, sessions expire after a week. This can be disabled or changed with the akka.http.session.max-age config option.

Note that when using cookies, even though the cookie sent will be a session cookie, it is possible that the client will have the browser open for a very long time, uses Chrome or FF, or if an attacker steals the cookie, it can be re-used. Hence having an expiry date for sessions is highly recommended.

JWT: encoding sessions

By default, sessions are encoded into a string using a custom format, where expiry/data/signature parts are separated using -, and data fields are separated using = and url-encoded.

You can also encode sessions in the Json Web Tokens format, by adding the additional jwt dependency, which makes use of json4s.

When using JWT, you need to provide a serializer which serializes session data to a JValue instead of a String. A number of serializers for the basic types are present in JValueSessionSerializer, as well as a generic serializer for case classes (used above).

You may also find it helpful to include the json4s-ext library which provides serializers for common Java types such as java.util.UUID, org.joda.time._ and Java enumerations.

Grab some java and scala examples.

There are many tools available to read JWT session data using various platforms, e.g. for Angular.

It is also possible to customize the session data content generated by overriding appropriate methods in JwtSessionEncoder (e.g. provide additional claims in the payload).

Registered JWT claims support

This library supports all registered claims mentioned in RFC 7519, Section 4.1.

Static claims such as iss (Issuer), sub (Subject) and aud (Audience) can be configured by setting akka.http.session.jwt.iss, akka.http.session.jwt.sub and akka.http.session.jwt.aud string properties, respectively.

Because claims such as exp (Expiration Time) and nbf (Not Before) depend on the time at which the JWT was issued, configuration expects durations instead of fixed timestamps. Effective claim values are then calculated by adding these offsets to the current timestamp (the JWT's issue time). The offset values are configured via keys defined under akka.http.session.jwt.exp-timeout and akka.http.session.jwt.nbf-offset.

If exp-timeout is not defined, value of akka.http.session.max-age would be used instead.

iat (Issued At) claim represents issue time and cannot be customized. Although you can decide to include this claim in your tokens or not by setting akka.http.session.jwt.include-iat to true or false. By default, this claim is not included.

jti (JWT ID) claim is a case-sensitive string containing a unique identifier for the JWT. It must be unique per token and collisions must be prevented even among values produced by different issuers. Akka-http-session will compute and include jti claim if akka.http.session.jwt.include-jti is set to true (it's disabled by default). Token ids are generated using the below scheme:

<iss claim value>-<random UUID> or just <random UUID>, depending on the iss claim presence.

You can find a sample claims configuration below:

akka.http.session {
  jwt {
    iss = "Issuer"
    sub = "Subject"
    aud = "Audience"
    exp-timeout = 7 days
    nbf-offset = 5 minutes
    include-iat = true
    include-jti = true
  }
}

Signing session using a configurable algorithm

In the case of JWT, it's possible to configure which JWS algorithm should be used. Currently, supported ones are:

  • HS256 - HMAC using SHA-256 (used by default)
  • RS256 - RSA (RSASSA-PKCS1-v1_5) using SHA-256

All non-JWT sessions use HMAC with SHA-256 and this cannot be configured.

Configuring JWS (for JSON Web Tokens only)

In order to start using RSA algorithm you have to configure akka.http.session.jws.alg and akka.http.session.jws.rsa-private-key properties:

akka.http.session {
  jws {
    alg = "RS256"
    rsa-private-key = "<your private PKCS#8 key goes here>"
  }
}

Because HS256 is used by default you may skip the jws configuration and rely on a reference configuration delivered with the library. Alternatively, if you prefer to be more explicit, you might follow this configuration template:

akka.http.session {
  server-secret = "<at least 64-digits length secret goes here>"
  jws {
    alg = "HS256"
  }
}

You might notice that even if you want to sign your sessions using RSA key and encryption is disabled, you still have to define the server-secret property.

That's because all non-JWT sessions still depend on HMAC with SHA256 algorithm which requires the server secret and the library cannot determine which session encoder(s) will be used (it's specified in the client's code).

CSRF protection (cookie transport only)

CSRF is a kind of an attack where an attacker issues a GET or POST request on behalf of a user, if the user e.g. clicks on a specially constructed link. See the OWASP page or the Play! docs for a thorough introduction.

Web apps which use cookies for session management should be protected against CSRF attacks. This implementation:

  • assumes that GET requests are non-mutating (have no side effects)
  • uses double-submit cookies to verify requests
  • requires the token to be set in a custom header or (optionally) in a form field
  • generates a new token on the first GET request that doesn't have the token cookie set

Note that if the token is passed in a form field, the website isn't protected by HTTPS or you don't control all subdomains, this scheme can be broken. Currently, setting a custom header seems to be a secure solution, and is what a number of projects do (that's why, when using custom headers to send session data, no additional protection is needed).

It is recommended to generate a new CSRF token after logging in, see this SO question. A new token can be generated using the setNewCsrfToken directive.

By default the name of the CSRF cookie and the custom header matches what AngularJS expects and sets. These can be customized in the config.

Refresh tokens (a.k.a "remember me")

If you'd like to implement persistent, "remember-me" sessions, you should use refreshable instead of oneOff sessions. This is especially useful in mobile applications, where you log in once, and the session is remembered for a long time. Make sure to adjust the akka.http.session.refresh-token.max-age config option appropriately (defaults to 1 month)!

You can dynamically decide, basing on the request properties (e.g. a query parameter), if a session should be refreshable or not. Just pass the right parameter to setSession.

When using refreshable sessions, in addition to an (implicit) SessionManager instance, you need to provide an implementation of the RefreshTokenStorage trait. This trait has methods to lookup, store and delete refresh tokens. Typically it would use some persistent storage.

The tokens are never stored directly, instead only token hashes are passed to the storage. That way even if the token database is leaked, it won't be possible to forge sessions using the hashes. Moreover, in addition to the token hash, a selector value is stored. That value is used to lookup stored hashes; tokens are compared using a special constant-time comparison method, to prevent timing attacks.

When a session expires or is not present, but the refresh token is (sent from the client using either a cookie, or a custom header), a new session will be created (using the RefreshTokenLookupResult.createSession function), and a new refresh token will be created.

Note that you can differentiate between sessions created from refresh tokens and from regular authentication by storing appropriate information in the session data. That way, you can force the user to re-authenticate if the session was created by a refresh token before crucial operations.

It is of course possible to read oneOff-session using requiredSession(refreshable, ...). If a session was created as oneOff, using refreshable has no additional effect.

Touching sessions

The semantics of touch[Required|Optional]Session() are a bit subtle. You can still use expiring client sessions when using refresh tokens. You will then have 2 stages of expiration: expiration of the client session (should be shorter), and expiry of the refresh token. That way you can have strongly-authenticated sessions which expire fast, and weaker-authenticated re-creatable sessions (as described in the paragraph above).

When touching an existing session, the refresh token will not be re-generated and extended, only the session cookie.

Links

Using from SBT

For akka-http version 10+:

libraryDependencies += "com.softwaremill.akka-http-session" %% "core" % "0.7.1"
libraryDependencies += "com.softwaremill.akka-http-session" %% "jwt"  % "0.7.1" // optional

For pekko-http:

libraryDependencies += "com.softwaremill.pekko-http-session" %% "core" % "0.7.1"
libraryDependencies += "com.softwaremill.pekko-http-session" %% "jwt"  % "0.7.1" // optional

Updating

Certain releases changed the client token encoding/serialization. In those cases, it's important to enable the appropriate token migrations, otherwise existing client sessions will be invalid (and your users will be logged out).

When updating from a version before 0.5.3, set akka.http.session.token-migration.v0-5-3.enabled = true.

When updating from a version before 0.5.2, set akka.http.session.token-migration.v0-5-2.enabled = true.

Note that when updating through multiple releases, be sure to enable all the appropriate migrations.

For versions prior to 0.5.0, no migration path is provided. However, you can implement your own encoders/serializers to support migrating from whatever version you are using.

Since token changes may be security related, migrations should be enabled for the shortest period of time after which the vast majority of client tokens have been migrated.

Commercial Support

We offer commercial support for akka-http-session and related technologies, as well as development services. Contact us to learn more about our offer!

Copyright

Copyright (C) 2016-2023 SoftwareMill https://softwaremill.com.

akka-http-session's People

Contributors

aaabramov avatar adamw avatar bmjsmith avatar firehooper avatar gitter-badger avatar gokyo avatar kciesielski avatar kevinavery avatar kijanowski avatar mkubala avatar mpoplavkov avatar opalo avatar pjfanning avatar queimadus avatar sahataba avatar sethtisue 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  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

akka-http-session's Issues

RSA signing for JWT

I need to integrate my akka-http-session based app with some L7 proxy and communication bus, which requires RS256 or ES256 to be used as the JWT alg.

Unfortunately, at the moment library supports only HS256 (HmacSHA256).

I'd like to add support for RSA keys and make JwtSessionEncoder configurable.

Allow separate access/refresh transport

Problem
Currently there the API doesn't support different access and refresh tokens transports (for example one should be stored in a HTTP only cookie, while the other in header).

Motivation
If separate transports were possible, one could store refresh token in a HTTP only cookie, and access token in headers (which would later be saved by the client in localstorage). This approach could yield better security if both tokens were required to generate a new access token:

  • A rouge JS script / XSS attack could only steal access token and use it until it's expiry
  • CSRF wouldn't be an issue when access token was stored in localStorage and both refresh and access (possibly expired, but with valid signature) tokens were required to generate a new access token*
  • If refresh token was to be somehow stolen (via man in the middle attack without HTTPS), an attacker would still need to obtain matching access token.*

*- This would require embedding refresh token selector in JWT to figure out if the two match.

Current workarounds
This can be somewhat mitigated when reading by composing requiredSession(oneOff, usingHeaders) { _ => requiredSession(refreshable, usingCookies) { ..., however this will result in the latter directive never finding our access token, and generating a new one.

For writing with setSession (upon user login with basic credentials), one could work around this by composing setSession calls, and manually removing excess cookies/headers.

Redirect unauthenticated request

Hi guys,

I'm wondering whether there is any existing directive that allows a request to be re-directed if the request is made to an un-authenticated user to a secured endpoint.

I am using this in a web console, and currently I am seeing:

The supplied authentication is not authorized to access this resource

It would be more useful however to redirect the request to a login page. Is this possible at the minute, am I just missing something? I haven't been able to find anything in the main docs or the faqs.

ScalatestRouteTest does not support session test

in my example:
there are three route: login, upload, download
to test it, the client must login first to upload
then with the result from previous upload, can only the client initiate the download test
it seems that ScalatestRouteTest does not support this kinds of tests

package com.yimei.zflow.util.asset.routes

import akka.actor.ActorSystem
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, Multipart}
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.testkit.ScalatestRouteTest
import com.typesafe.config.ConfigFactory
import com.yimei.zflow.util.FlywayDB
import com.yimei.zflow.util.asset.routes.Models._
import com.yimei.zflow.util.organ.OrganSession
import org.scalatest.{Inside, Matchers, WordSpec}

object AssetRouteUT extends {
  implicit val coreSystem: ActorSystem = ActorSystem("TestSystem", ConfigFactory.parseString(
    """
      |database {
      |  jdbcUrl = "jdbc:mysql://127.0.0.1/cyflow?useUnicode=true&characterEncoding=utf8"
      |  username = "mysql"
      |  password = "mysql"
      |}
      |file.root = "/tmp"
      |akka.http.session.server-secret = "1234567891234567891234567891234567891234567890000012345678901234"
      |
    """.stripMargin))
} with AssetRoute {
  val fileField = "file"

  def prepare() = {
    val config = coreSystem.settings.config
    val jdbcUrl = config.getString("database.jdbcUrl")
    val username = config.getString("database.username")
    val password = config.getString("database.password")
    val flyway = new FlywayDB(jdbcUrl, username, password);
    flyway.drop()
    flyway.migrate()
  }

  import akka.http.scaladsl.server.Directives._
  def loginRoute: Route = path("login") {
    organSetSession(OrganSession("hary", "uid", "party", "instanceId", "company")) { ctx =>
      ctx.complete("ok")
    }
  }
}

/**
  * Created by hary on 17/1/10.
  */
class AssetRouteTest extends WordSpec with Matchers with Inside with ScalatestRouteTest with SprayJsonSupport {

  import AssetRouteUT.{assetRoute, loginRoute, prepare}

  override protected def beforeAll(): Unit = {
    super.beforeAll()
    prepare

    // must be tested with login session

  }

  val xml = "<int>42</int>"  // ไธŠไผ ๆ–‡ไปถ
  val simpleMultipartUpload =
    Multipart.FormData(Multipart.FormData.BodyPart.Strict(
      "file",
      HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml),
      Map("filename" โ†’ "age.xml")))

  var assetId: String = null

  "AssetRouteTest" should {

    "login is ok" in {
      Post("/login") ~> loginRoute ~> check {
        responseAs[String] shouldBe "ok"
      }

    }

    "uploadFile is ok" in {
      Post("/upload") ~> assetRoute ~> check {
        inside(responseAs[UploadResult]) {
          case UploadResult(id) =>
            assetId = id
            1 shouldBe 1
        }
      }
    }

    "downloadFile is in" in {
      Get("/download/" + assetId) ~> assetRoute ~> check {
        responseAs[String] shouldEqual xml
      }
    }
  }

}

RefreshTokenStorage schedule documentation

Hello,
I'm interested in using this project, but there are still some obscure undocumented aspects. The first one is the schedule method in the RefreshTokenStorage. Can you please clarify what is this for and how is it intended to be implemented?
Also the after parameter should probably be a FiniteDuration rather than simply Duration, should it not?
Is there a plan to work on some better documentation in general?
Many thanks

on secret management

Hi,
I would see two possibilities of managing the secret per env:

  • via Kubernetes secrets, that is as a specific kind of configuration (secret one)
  • automatically generating a secret when an environment starts - but where to keep it? Would the database be a proper place? I know for secret data there is Vault for instance, but that would make it too complex for us to choose.
    What is your advice?
    Thank you,
    Nicu

Authorizations

Great project, thanks! Works effortlessly.

Any interest in Shiro as an authorization provider? Other ideas come to mind? This being the Scala world, it strikes me that someone out there has created the perfect RBAC in five lines with scalaz, but alas, I don't have those five lines :)

InvalidKeyException when setting `encrypt-data = true`

I get

java.security.InvalidKeyException: Invalid AES key length: 18 bytes
    at com.sun.crypto.provider.AESCipher.engineGetKeySize(AESCipher.java:495)
    at javax.crypto.Cipher.passCryptoPermCheck(Cipher.java:1067)
    at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1025)
    at javax.crypto.Cipher.implInit(Cipher.java:801)
    at javax.crypto.Cipher.chooseProvider(Cipher.java:864)
    at javax.crypto.Cipher.init(Cipher.java:1249)
    at javax.crypto.Cipher.init(Cipher.java:1186)
    at com.softwaremill.session.Crypto$.encrypt_AES(Crypto.scala:29)
    at com.softwaremill.session.BasicSessionEncoder.encode(SessionEncoder.scala:34)
    at com.softwaremill.session.ClientSessionManager$class.encode(SessionManager.scala:60)
    at com.softwaremill.session.SessionManager$$anon$1.encode(SessionManager.scala:14)
    at com.softwaremill.session.ClientSessionManager$class.createCookie(SessionManager.scala:40)
    at com.softwaremill.session.SessionManager$$anon$1.createCookie(SessionManager.scala:14)
    at com.softwaremill.session.OneOffSessionDirectives$class.setOneOffSession(SessionDirectives.scala:104)
    at com.softwaremill.session.SessionDirectives$.setOneOffSession(SessionDirectives.scala:85)
    at com.softwaremill.session.SessionDirectives$class.setSession(SessionDirectives.scala:20)
    at com.softwaremill.session.SessionDirectives$.setSession(SessionDirectives.scala:85)

when setting encrypt-data = true. Without encryption, the library works fine. I'm using

"com.softwaremill.akka-http-session" %% "core" % "0.2.4"

with Scala-2.11.8.

CSRF protection can be bypassed

Hi again,

Another question about the CSRF protection. It can be bypassed by forging a request that contains the same value for both the X-XSRF-TOKEN header and the XSRF-TOKEN cookie value. Any value will do since the only check that is now performed in randomTokenCsrfProtection is for those two values to be equal and non-empty.

Would it be somehow possible to conform to the OWASP recommendations described here what do you think would be the best approach?

Thanks,
Willem

non blocking interface

setSession implies a blocking interface. It would be nice if it could accept a Future[T]

Question: can Cassandra be used as "session"?

Hi, we have AkkaHttp & Cassandra microservices and would need to keep access/refresh tokens by users for the authorization code flow. Is the http session pluggable in AkkaHttp? Or should we directly work with C* (Cassandra) to store/retrieve tokens? thank you

Please support Java 9!

With Java 9 I get

chakka-core[ERROR] java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
chakka-core[ERROR] 	at com.softwaremill.session.SessionUtil$.toHexString(SessionUtil.scala:36)
chakka-core[ERROR] 	at com.softwaremill.session.Crypto$.sign_HmacSHA1_hex(Crypto.scala:17)
chakka-core[ERROR] 	at com.softwaremill.session.BasicSessionEncoder.verifySignature$1(SessionEncoder.scala:50)
chakka-core[ERROR] 	at com.softwaremill.session.BasicSessionEncoder.$anonfun$decode$4(SessionEncoder.scala:71)
chakka-core[ERROR] 	at scala.util.Success.$anonfun$map$1(Try.scala:251)
chakka-core[ERROR] 	at scala.util.Success.map(Try.scala:209)
chakka-core[ERROR] 	at com.softwaremill.session.BasicSessionEncoder.$anonfun$decode$3(SessionEncoder.scala:70)
chakka-core[ERROR] 	at scala.util.Try$.apply(Try.scala:209)
chakka-core[ERROR] 	at com.softwaremill.session.BasicSessionEncoder.decode(SessionEncoder.scala:53)
chakka-core[ERROR] 	at com.softwaremill.session.ClientSessionManager.decode(SessionManager.scala:61)
chakka-core[ERROR] 	at com.softwaremill.session.ClientSessionManager.decode$(SessionManager.scala:60)
chakka-core[ERROR] 	at com.softwaremill.session.SessionManager$$anon$1.decode(SessionManager.scala:14)
chakka-core[ERROR] 	at com.softwaremill.session.OneOffSessionDirectives.$anonfun$oneOffSession$1(SessionDirectives.scala:132)

With Java 8 everything works as expected ...

Header error regarding JWT implementation

First off, Thank you so much for your project (time and effort!).
On this moment I'm experiencing some errors regarding the JWT header (name/value) combination.

The error throw = Illegal request header: Illegal 'authorization' header: Invalid input '=', expected auth-param, OWS, token68, 'EOI' or tchar

Using the directive optionalSession(oneOff, usingHeaders)
This directive always returns an Option[T] containing a None

Session token compatibility across upgrades

Session tokens created under v0.5.1 are invalid after upgrading to v0.5.2. This means upgrading the library causes all users to be logged out.

It looks like this was the relevant change: 5ef0577

Separately, upgrading from v0.5.2 to v0.5.3 also breaks existing session tokens.

Is there a token migration path for users with old tokens?

custom header

Please I can't seem to see my custom headers. Anything I am doing wrong? I have configured as follows:

akka { loglevel = INFO stdout-loglevel = INFO loggers = ["akka.event.slf4j.Slf4jLogger"] default-dispatcher { fork-join-executor { parallelism-min = 8 } } test { timefactor = 1 } actor { deployment { /ServiceManagerRouter { router = "round-robin" nr-of-instances = 20 } } } http { session { server-secret = "####......###" header { send-to-client-name = "X-BLAH1" get-from-client-name = "Y-BLAH2" } refresh-token { cookie { name = "_refreshtoken" domain = none path = / secure = false http-only = true } header { send-to-client-name = "X-BLAH" get-from-client-name = "R-Y-BLAH" } max-age = 30 days remove-used-token-after = 5 seconds } } } }

Infinite loop in RefreshTokenManager

I'm using akka-http-session 0.4.0 in scastie https://github.com/scalacenter/scastie/blob/master/server/src/main/scala/com.olegych.scastie.web/oauth2/GithubUserSession.scala

I have two threads at 100% cpu usage. This the result of jstack:

"Web-akka.actor.default-dispatcher-214" #273 prio=5 os_prio=0 tid=0x00007f69fc024000 nid=0x33ba runnable [0x00007f69bfbf8000]
   java.lang.Thread.State: RUNNABLE
        at scala.collection.mutable.HashTable.removeEntry(HashTable.scala:193)
        at scala.collection.mutable.HashTable.removeEntry$(HashTable.scala:181)
        at scala.collection.mutable.HashMap.removeEntry(HashMap.scala:40)
        at scala.collection.mutable.HashMap.remove(HashMap.scala:123)
        at com.softwaremill.session.InMemoryRefreshTokenStorage.remove(RefreshTokenStorage.scala:59)
        at com.softwaremill.session.InMemoryRefreshTokenStorage.remove$(RefreshTokenStorage.scala:57)
        at com.olegych.scastie.web.oauth2.GithubUserSession$$anon$1.remove(GithubUserSession.scala:52)
        at com.softwaremill.session.RefreshTokenManager.$anonfun$rotateToken$4(SessionManager.scala:130)
        at com.softwaremill.session.RefreshTokenManager$$Lambda$1909/515478046.apply(Unknown Source)
        at com.softwaremill.session.InMemoryRefreshTokenStorage.schedule(RefreshTokenStorage.scala:64)
        at com.softwaremill.session.InMemoryRefreshTokenStorage.schedule$(RefreshTokenStorage.scala:62)
        at com.olegych.scastie.web.oauth2.GithubUserSession$$anon$1.schedule(GithubUserSession.scala:52)
        at com.softwaremill.session.RefreshTokenManager.$anonfun$rotateToken$3(SessionManager.scala:130)
        at com.softwaremill.session.RefreshTokenManager.$anonfun$rotateToken$3$adapted(SessionManager.scala:127)
        at com.softwaremill.session.RefreshTokenManager$$Lambda$1736/616177409.apply(Unknown Source)
        at scala.Option.foreach(Option.scala:257)
 at com.softwaremill.session.RefreshTokenManager.rotateToken(SessionManager.scala:127)
        at com.softwaremill.session.RefreshTokenManager.rotateToken$(SessionManager.scala:115)
        at com.softwaremill.session.SessionManager$$anon$3.rotateToken(SessionManager.scala:24)
        at com.softwaremill.session.RefreshableSessionDirectives.$anonfun$setRefreshToken$1(SessionDirectives.scala:202)
        at com.softwaremill.session.RefreshableSessionDirectives$$Lambda$1731/488540775.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive$SingleValueModifiers.$anonfun$flatMap$1(Directive.scala:141)
        at akka.http.scaladsl.server.Directive$SingleValueModifiers$$Lambda$616/1043639521.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$tflatMap$2(Directive.scala:69)
        at akka.http.scaladsl.server.Directive$$Lambda$625/913242382.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$tmap$2(Directive.scala:63)
        at akka.http.scaladsl.server.Directive$$Lambda$624/2117642238.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$4(Directive.scala:93)
        at akka.http.scaladsl.server.Directive$$Lambda$1100/1660614532.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.BasicDirectives.$anonfun$textract$2(BasicDirectives.scala:154)
        at akka.http.scaladsl.server.directives.BasicDirectives$$Lambda$626/870322840.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$2(Directive.scala:93)
        at akka.http.scaladsl.server.Directive$$Lambda$1098/542507242.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.BasicDirectives.$anonfun$mapRouteResult$2(BasicDirectives.scala:61)
        at akka.http.scaladsl.server.directives.BasicDirectives$$Lambda$1028/41947542.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.OnSuccessMagnet$$anon$1.$anonfun$directive$3(FutureDirectives.scala:97)
        at akka.http.scaladsl.server.directives.OnSuccessMagnet$$anon$1$$Lambda$1583/151472226.apply(Unknown Source)
        at akka.http.scaladsl.util.FastFuture$.strictTransform$1(FastFuture.scala:41)
        at akka.http.scaladsl.util.FastFuture$.transformWith$extension1(FastFuture.scala:55)
        at akka.http.scaladsl.util.FastFuture$.flatMap$extension(FastFuture.scala:26)
        at akka.http.scaladsl.server.directives.OnSuccessMagnet$$anon$1.$anonfun$directive$2(FutureDirectives.scala:97)
        at akka.http.scaladsl.server.directives.OnSuccessMagnet$$anon$1$$Lambda$1580/1652741769.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$4(Directive.scala:93)
        at akka.http.scaladsl.server.Directive$$Lambda$1100/1660614532.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.BasicDirectives.$anonfun$textract$2(BasicDirectives.scala:154)
        at akka.http.scaladsl.server.directives.BasicDirectives$$Lambda$626/870322840.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$2(Directive.scala:93)
 at akka.http.scaladsl.server.Directive$$Lambda$1100/1660614532.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.BasicDirectives.$anonfun$textract$2(BasicDirectives.scala:154)
        at akka.http.scaladsl.server.directives.BasicDirectives$$Lambda$626/870322840.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$2(Directive.scala:93)
        at akka.http.scaladsl.server.Directive$$Lambda$1098/542507242.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$5(Directive.scala:94)
        at akka.http.scaladsl.server.Directive$$Lambda$1135/82863731.apply(Unknown Source)
        at akka.http.scaladsl.util.FastFuture$.strictTransform$1(FastFuture.scala:41)
        at akka.http.scaladsl.util.FastFuture$.transformWith$extension1(FastFuture.scala:45)
        at akka.http.scaladsl.util.FastFuture$.flatMap$extension(FastFuture.scala:26)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$2(Directive.scala:93)
        at akka.http.scaladsl.server.Directive$$Lambda$1098/542507242.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$4(Directive.scala:93)
        at akka.http.scaladsl.server.Directive$$Lambda$1100/1660614532.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$4(Directive.scala:93)
        at akka.http.scaladsl.server.Directive$$Lambda$1100/1660614532.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$4(Directive.scala:93)
        at akka.http.scaladsl.server.Directive$$Lambda$1100/1660614532.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.BasicDirectives.$anonfun$textract$2(BasicDirectives.scala:154)
        at akka.http.scaladsl.server.directives.BasicDirectives$$Lambda$626/870322840.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$2(Directive.scala:93)
        at akka.http.scaladsl.server.Directive$$Lambda$1098/542507242.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$2(Directive.scala:93)
        at akka.http.scaladsl.server.Directive$$Lambda$1098/542507242.apply(Unknown Source)
        at akka.http.scaladsl.server.Directive.$anonfun$recover$2(Directive.scala:93)
        at akka.http.scaladsl.server.Directive$$Lambda$1098/542507242.apply(Unknown Source)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation.$anonfun$$tilde$2(RouteConcatenation.scala:47)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$Lambda$1016/771814596.apply(Unknown Source)
        at akka.http.scaladsl.util.FastFuture$.strictTransform$1(FastFuture.scala:41)
        at akka.http.scaladsl.util.FastFuture$.transformWith$extension1(FastFuture.scala:45)
        at akka.http.scaladsl.util.FastFuture$.flatMap$extension(FastFuture.scala:26)
 at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation.$anonfun$$tilde$1(RouteConcatenation.scala:44)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$Lambda$648/726212590.apply(Unknown Source)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation.$anonfun$$tilde$1(RouteConcatenation.scala:44)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$Lambda$648/726212590.apply(Unknown Source)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation.$anonfun$$tilde$1(RouteConcatenation.scala:44)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$Lambda$648/726212590.apply(Unknown Source)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation.$anonfun$$tilde$1(RouteConcatenation.scala:44)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$Lambda$648/726212590.apply(Unknown Source)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation.$anonfun$$tilde$1(RouteConcatenation.scala:44)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$Lambda$648/726212590.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.BasicDirectives.$anonfun$mapRequestContext$2(BasicDirectives.scala:43)
        at akka.http.scaladsl.server.directives.BasicDirectives$$Lambda$1070/2000999693.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.BasicDirectives.$anonfun$textract$2(BasicDirectives.scala:154)
        at akka.http.scaladsl.server.directives.BasicDirectives$$Lambda$626/870322840.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.BasicDirectives.$anonfun$textract$2(BasicDirectives.scala:154)
        at akka.http.scaladsl.server.directives.BasicDirectives$$Lambda$626/870322840.apply(Unknown Source)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation.$anonfun$$tilde$2(RouteConcatenation.scala:47)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$Lambda$1016/771814596.apply(Unknown Source)
        at akka.http.scaladsl.util.FastFuture$.strictTransform$1(FastFuture.scala:41)
        at akka.http.scaladsl.util.FastFuture$.transformWith$extension1(FastFuture.scala:45)
        at akka.http.scaladsl.util.FastFuture$.flatMap$extension(FastFuture.scala:26)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation.$anonfun$$tilde$1(RouteConcatenation.scala:44)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$Lambda$648/726212590.apply(Unknown Source)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation.$anonfun$$tilde$1(RouteConcatenation.scala:44)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$Lambda$648/726212590.apply(Unknown Source)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation.$anonfun$$tilde$1(RouteConcatenation.scala:44)
        at akka.http.scaladsl.server.RouteConcatenation$RouteWithConcatenation$$Lambda$648/726212590.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.BasicDirectives.$anonfun$mapRouteResultWith$2(BasicDirectives.scala:67)
        at akka.http.scaladsl.server.directives.BasicDirectives$$Lambda$1015/218875704.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.BasicDirectives.$anonfun$textract$2(BasicDirectives.scala:154)
        at akka.http.scaladsl.server.directives.BasicDirectives$$Lambda$626/870322840.apply(Unknown Source)
        at akka.http.scaladsl.server.directives.ExecutionDirectives.$anonfun$handleExceptions$2(ExecutionDirectives.scala:32)
        at akka.http.scaladsl.server.directives.ExecutionDirectives$$Lambda$878/485475507.apply(Unknown Source)
        at akka.http.scaladsl.server.Route$.$anonfun$asyncHandler$1(Route.scala:79)
        at akka.http.scaladsl.server.Route$$$Lambda$879/1869247587.apply(Unknown Source)
        at akka.stream.impl.fusing.MapAsync$$anon$23.onPush(Ops.scala:1172)
 at akka.stream.impl.fusing.GraphInterpreter.processPush(GraphInterpreter.scala:499)
        at akka.stream.impl.fusing.GraphInterpreter.processEvent(GraphInterpreter.scala:462)
        at akka.stream.impl.fusing.GraphInterpreter.execute(GraphInterpreter.scala:368)
        at akka.stream.impl.fusing.GraphInterpreterShell.runBatch(ActorGraphInterpreter.scala:571)
        at akka.stream.impl.fusing.GraphInterpreterShell$AsyncInput.execute(ActorGraphInterpreter.scala:457)
        at akka.stream.impl.fusing.GraphInterpreterShell.processEvent(ActorGraphInterpreter.scala:546)
        at akka.stream.impl.fusing.ActorGraphInterpreter.akka$stream$impl$fusing$ActorGraphInterpreter$$processEvent(ActorGraphInterpreter.scala:728)
        at akka.stream.impl.fusing.ActorGraphInterpreter$$anonfun$receive$1.applyOrElse(ActorGraphInterpreter.scala:743)
        at akka.actor.Actor.aroundReceive(Actor.scala:517)
        at akka.actor.Actor.aroundReceive$(Actor.scala:515)
        at akka.stream.impl.fusing.ActorGraphInterpreter.aroundReceive(ActorGraphInterpreter.scala:653)
        at akka.actor.ActorCell.receiveMessage(ActorCell.scala:527)
        at akka.actor.ActorCell.invoke(ActorCell.scala:496)
        at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:257)
        at akka.dispatch.Mailbox.run(Mailbox.scala:224)
        at akka.dispatch.Mailbox.exec(Mailbox.scala:234)
        at akka.dispatch.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
        at akka.dispatch.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
        at akka.dispatch.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
        at akka.dispatch.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)

issue refreshing token

I am using refreshable and cookie options for session handling.

OneOff option works as expected but I am unable to get the refreshable option to work.

Setting session max-age to less than refresh max-age results in session timing (as expected).

If using directive requiredSession and session expires I get the following exception thrown (appears to be thrown by processing of directive:

LB, 13:15:43.135UTC ERROR[TheSystem-akka.actor.default-dispatcher-9] ActorSystemImpl - Error during processing of request: 'java.lang.NullPointerException (No error message supplied)'. Completing with 500 Internal Server Error response. To change default exception handling behavior, provide a custom ExceptionHandler. 
java.lang.NullPointerException: null
	at scala.concurrent.impl.Promise$Transformation.<init>(Promise.scala:382)
	at scala.concurrent.impl.Promise$DefaultPromise.flatMap(Promise.scala:140)
	at com.softwaremill.session.RefreshTokenManager.sessionFromValue(SessionManager.scala:180)
	at com.softwaremill.session.RefreshTokenManager.sessionFromValue$(SessionManager.scala:177)

I see the same behavior if I use directive optionalSession.

My understanding is that the refreshing of token (if using cookie based continuity) is done behind the scenes with no required interaction - is this correct?

Am I missing something? I assume I must be doing something wrong as I would imagine this issue would have been reported.

AkkaJava

Hi,
I m writing app using java akka
can i use akka-http-session in java ?
please give me an example

Grammar error in README

I have a local branch ready to be pushed (case of "using using" instead of "using"), but get a 403. How can I become a contributor?

TouchRequiredSession generates new refresh token when access token is expired

I am using touchRequiredSession with refresh tokens and custom headers. I am noticing that the refresh-token is rotated when the access token is expired and the refresh token is not. It is my understanding that using touch should not result in a new refresh token, only a new access token. I verified that the refresh token was being rotated with an extended expiry in the data store.

I could be wrong, but it seems like requiredSession(), which is called by touch, always does the rotation when expired. https://github.com/softwaremill/akka-http-session/blob/master/core/src/main/scala/com/softwaremill/session/SessionDirectives.scala#L81

Example using headers please

Is there an example using headers anywhere? I want to interface an android app to akka-http and headers would be the logical choice.

CSRF protection can be bypassed with empty header and empty cookie

Hi,
When you send a POST to an endpoint protected by randomTokenCsrfProtection and pass in an empty X-XSRF-TOKEN header and a XSRF-TOKEN cookie with empty value, the filter will let you pass.

I think is due to the check in randomTokenCsrfProtection on line 26:
if (submitted == cookie) {
pass
}
but the value itself is not inspected and could possibly be empty.

Hope this can be fixed, thanks,
Willem

Couple of Issues/Questions

CSRF Protection, does it only validate CSRF token on POST requests? If so how does it protect attacker from GET requests?

I am unable to make usingHeaders to work. In the example it shows Cookies authorization but nothing with headers.

What header name is it looking for when authentication a session?

After invalidateSession I'm able to access secured endpoint with "invalidated" session

Hello

Looks like the invalidateSession function doesn't invalidate the session as it should. I'll explain below what I mean. If you want to look at steps to reproduce without technical details, scroll to the end :)

I use "com.softwaremill.akka-http-session" %% "core" % "0.4.0" with Scala version 2.12.1

I use following configs for session:

session {
  server-secret = "YzszrU1UkqsMqCNEnuLI8DDWs6Wqacj2z4dbtquSjB8GbsFpBA7GG38yk0DaIyrB"
  encrypt-data = true
  header {
    send-to-client-name = "Set-Authorization"
    get-from-client-name = "Authorization"
  }
}

Here is my session serialization (de-)

case class Session(role: String, email: String)
object Session {
implicit def serializer: SessionSerializer[Session, String] =
  new MultiValueSessionSerializer[Session](
    (session => Map(
      "role" -> session.role,
      "email" -> session.email)),
    (map => Try {
      Session(
        map.get("role").get,
        map.get("email").get)
    })
  )
}

And finally routes:

val routes = path("login") {
post {
  entity(as[Credentials]) { credentials =>
    onSuccess(userActor ? Authenticate(credentials)) {
      case loggedIn: LoggedIn => {
        setSession(oneOff, usingHeaders, Session(loggedIn.user.role, loggedIn.user.email)) {
          complete(HttpResponse(StatusCodes.OK))
        }
      }
      case noSuchEmail: NoUserWithEmail => complete(HttpResponse(StatusCodes.BadRequest))
      case InvalidPassword => complete(HttpResponse(StatusCodes.BadRequest))
    }
  }
}
} ~ path("me") {
get {
  requiredSession(oneOff, usingHeaders) { session =>
    complete(session.role)
  }
}
} ~ path("logout") {
post {
  requiredSession(oneOff, usingHeaders) { session =>
    invalidateSession(oneOff, usingHeaders) {
      complete(HttpResponse(StatusCodes.OK))
    }
  }
}
}

Here is what I do:

  1. Call POST /login and receive back in the header long_encrypted_token_A
  2. Call GET /me with the long_encrypted_token_A header and receive back appropriate response with ADMIN value
  3. Call POST /logout and receive back 200 response (here I assume that the session is invalidated)
  4. Call GET /me with the long_encrypted_token_A header and receive back appropriate response with ADMIN value

So the question:

Why I can still successfully can use the token after invalidation?

Thanks

requireSession breaks CORS

My CORS configuration works perfectly until I use requireSession. I have tried many different configurations with no success in getting both CORS and Sessions working together. I'm guessing that requireSession overwrites headers that I set for CORS, but I am uncertain of this. Any suggestions?

JWT: Add support for `iss` and `aud` claims

These claims are required by some service proxies that supports JWS verification.
Unfortunately claims cannot be injected using a custom serialiser in the application code, since its output is nested together with other payload fields under the data key.

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.