Skip to content

Authentication

Gerasimos (Makis) Maropoulos edited this page Oct 26, 2019 · 3 revisions

You can check for authentication and force-close a client at any state of your application, on Server.OnConnect or through event callbacks for example. The recommended way is to do that on the HTTP handshake, before upgrade to the websocket subprotocol, but it really depends on your app's flow and requirements.

Let's see three small examples here, two with net/http and one more real-world example using JWT authentication and Iris.

The first one is simple enough, it does the authentication before the websocket server's handler execution, through a middleware using the std net/http library.

Let's assume that the isAuthenticated(r *http.Request) bool function is our method to check if a client request is authenticated or not.

// [websocketServer := neffos.New...]

router := http.NewServeMux()
router.HandleFunc("/websocket_endpoint", func(w http.ResponseWriter, r *http.Request){
    if !isAuthenticated(r) {
        statusCode := http.StatusUnauthorized // 401.
        http.Error(w, http.StatusText(statusCode), statusCode)
        return
    }

    // if succeed continue to our websocket app.
    websocketServer.ServeHTTP(w,r)
})

http.ListenAndServe(":8080", router)

The second one is simple too, we just move the authentication process to the Server.OnConnect instead. You can get the original http.Request by Conn.Socket().Request().

Remember, if a Server.OnConnect event returns a non-nil error then the server immediately closes the connection before everything else. The client-side will receive this error through its client, err := neffos.Dial(...) for Go client and catch callback of the javascript's neffos.dial(...) function(see below).

// [websocketServer := neffos.New...]
websocketServer.OnConnect = func(c *neffos.Conn) error {
    r := c.Socket().Request()
    if !isAuthenticated(r) {
        // this error will return back to
        // the client's `neffos.Dial` fucntion.
        return errors.New("not authenticated")
    }

    // if succeed...
    log.Printf("[%s] connected to the server", c.ID())
    return nil
}
// [client, err := neffos.Dial...]
if err != nil {
    // err.Error() == "not authenticated"
    // [handle unauthenticated error...]
}

Finally, let's continue with a more real-world example using JWT and Iris. Iris offers a community-driven jwt middleware to use.

1. Install it by executing the following shell command:

$ go get github.com/iris-contrib/middleware/jwt
# and if you don't have Iris installed yet:
$ go get github.com/kataras/iris/v12@latest

2. This example extracts the token through a "token" url parameter. Authenticated clients should be designed to set that with a signed token. This middleware has two methods, the first one is the Serve method - it is an iris.Handler and the second one is the CheckJWT(iris.Context) bool.

import (
    // [...]
    "github.com/iris-contrib/middleware/jwt"
)

// [...]

j := jwt.New(jwt.Config{
    // Extract by the "token" url.
    Extractor: jwt.FromParameter("token"),

    ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
	return []byte("My Secret"), nil
    },
    SigningMethod: jwt.SigningMethodHS256,
})

3. To register it before websocket upgrade process, you simply prepend the jwt j middleware to the registered websocket endpoint route, before the websocket.Handler(websocketServer).

import (
    "github.com/kataras/iris/v12"
    "github.com/kataras/iris/v12/websocket"
    "github.com/kataras/neffos"

    "github.com/iris-contrib/middleware/jwt"
)

func main(){
    app := iris.New()
    // [websocketServer := neffos.New....]
    app.Get("/echo", j.Serve, websocket.Handler(websocketServer))
    app.Run(iris.Addr(":8080"))
}

3.1. To register it on an event you can use jwt middleware's CheckJWT(iris.Context) error method and return any error to the event callback.

// [websocketServer := neffos.New....]
websocketServer.OnConnect = func(c *neffos.Conn) error {
    ctx := websocket.GetContext(c)
    if err := j.CheckJWT(ctx); err != nil {
        // will send the above error on the client
        // and will not allow it to connect
        // to the websocket server at all.
        return err
    }

    log.Printf("[%s] connected to the server", c.ID())
    return nil
}

4. To get the claims/the payload inside a websocket event callback:

  • when you have access to the jwt middleware you can just use its Get method.
ctx := websocket.GetContext(...)
user :=  j.Get(ctx)
  • otherwise use the ctx.Values().Get("jwt") which is the key that the *jwt.Token value is stored on authenticated requests.
func (nsConn *neffos.NSConn, msg neffos.Message) error {
    ctx := websocket.GetContext(nsConn.Conn)
    user := ctx.Values().Get("jwt").(*jwt.Token)

    log.Printf("This is an authenticated request\n")
    log.Printf("Claim content:")
    log.Printf("%#+v\n", user.Claims)
    // [...]
}

5. And the client-side, at this case a browser, might look something like that.

var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIx"+
"MjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0Ijoz"+
"MjEzMjF9.8waEX7-vPKACa-Soi1pQvW3Rl8QY-SUFcHKTLZI4mvU";

var wsURL = "ws://localhost:8080/echo?token=" + token;
// [...]
try {
    var conn = await neffos.dial(wsURL, { namespace: events...});
} catch(err) {
    // [handle unauthenticated error...]
}

Of course in production, your server side must contain routes that their job will be to create the tokens for each of your users and then the client-side must store it and send it as HTTP Header on each request to the server, the default jwt's behavior to extract a token value is by the Authentication: Bearer $TOKEN header.