Skip to content

Authentication ​

A consumer with an Authenticate method checks each connection once, before any handler runs. The client learns the result from an authentication message, and a failed connection closes with a code that tells it not to retry.

Authenticate ​

go
type ChatConsumer struct {
	Tokens Tokens
}

// verify maps a token to a user. A chanx.Forbidden error is reported as 403;
// any other error as 401 "Invalid token.".
func (c ChatConsumer) verify(token string) (User, error) {
	user, ok := c.Tokens[token]
	switch {
	case !ok:
		return User{}, errUnknownToken
	case user.Banned:
		return User{}, chanx.Forbidden("This account is suspended.")
	}
	return user, nil
}

// Authenticate runs once per connection, before any handler. auth.Bearer reads the
// token from the Authorization header, the "token" query parameter or the
// "bearer" subprotocol.
func (c ChatConsumer) Authenticate(ctx *chanx.Context, r *chanx.Request) (any, error) {
	return auth.Bearer(r, c.verify)
}

// AuthenticationData fills the data of the authentication message.
func (ChatConsumer) AuthenticationData(ctx *chanx.Context, user any) any {
	return map[string]string{"name": user.(User).Name}
}

// Credentials documents where Authenticate looks, as AsyncAPI security schemes.
func (ChatConsumer) Credentials() []chanx.Credential { return auth.BearerCredentials() }

What Authenticate returns is the connection's user:

go
// HandleWhoami reads the user Authenticate returned.
func (ChatConsumer) HandleWhoami(ctx *chanx.Context, _ WhoAmI) (YouAre, error) {
	return YouAre{Payload: ctx.Conn.User.(User).Name}, nil
}

The result ​

On success, the first frame the client receives is:

json
{"action": "authentication", "payload": {"status_code": 200, "status_text": "OK", "data": {"name": "ann"}}}

data is what AuthenticationData returns, null without it. On failure, the same message with the status, then the socket closes with 4000 + the status:

Returned errorMessageClose code
chanx.Unauthenticated("Invalid token.")401, "Invalid token."4401
chanx.Forbidden("This account is suspended.")403, "This account is suspended."4403
&chanx.AuthError{Status: 429, Detail: "Slow down"}429, "Slow down"4429
any other error401, "Unauthorized", its text logged, never sent4401
a panic500, "Internal Server Error"4500
json
{"action": "authentication", "payload": {"status_code": 401, "status_text": "Authentication credentials were not provided.", "data": null}}

A client seeing a close code from 4400 to 4599 knows retrying with the same credentials will not help.

Tokens ​

Browsers cannot set headers on a WebSocket. Package auth reads a bearer token from wherever a client can put one:

FunctionReads
auth.Token(r)the first of the three below
auth.BearerToken(r)Authorization: Bearer <token>
auth.QueryToken(r, "token")?token=<token>
auth.SubprotocolToken(r, "bearer")Sec-WebSocket-Protocol: bearer, <token>

auth.Bearer(r, verify) reads the token and calls your verify: a missing token fails with "Authentication credentials were not provided.", a verify error with "Invalid token.", unless it is a chanx.Forbidden or another *chanx.AuthError.

From framework middleware ​

If your framework's middleware already authenticates, take the user from it with the chanx.UserFrom route option, and write no Authenticate at all:

go
// UserKey is the context key the middleware stores the user under.
type UserKey struct{}

// RequireUser is ordinary net/http middleware: it runs before the upgrade, and
// what it stores reaches the consumer.
func RequireUser(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		name := r.Header.Get("X-User") // in real life: a session or a verified token
		if name == "" {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), UserKey{}, User{Name: name})))
	})
}

// LobbyConsumer has no Authenticate: the route takes the user from the middleware
// with chanx.UserFrom(UserKey{}).
type LobbyConsumer struct{}

func (LobbyConsumer) HandleWhoami(ctx *chanx.Context, _ WhoAmI) (YouAre, error) {
	return YouAre{Payload: ctx.Conn.User.(User).Name}, nil
}
go
engine.Handle("/ws/lobby", LobbyConsumer{}, chanx.UserFrom(UserKey{}))
mux.Handle("/ws/", RequireUser(chanxhttp.Dispatcher(engine)))

A missing value fails with 401. With gin and echo the key is what you passed to c.Set("user", u), with Fiber what you passed to c.Locals. Inside Authenticate, r.Value(key) reads the same values.

Rejecting before the upgrade ​

By default a failed connection is upgraded, told why, then closed: a browser can read the reason. chanx.Config{RejectBeforeUpgrade: true}, or the chanx.RejectBeforeUpgrade route option, runs Authenticate before the upgrade instead and answers a plain HTTP status, 401 or 403, through the framework's error handling. Use it when unauthenticated upgrades are a cost you do not want to pay.

Documenting it ​

Credentials declares where a client puts its credentials. Each one becomes an AsyncAPI security scheme, listed on the channel's operations, and query and header credentials go in the channel's WebSocket binding:

go
func (ChatConsumer) Credentials() []chanx.Credential {
	return []chanx.Credential{
		{Name: "token", Location: "query", Description: "Access token"},
		{Name: "sessionid", Location: "cookie"},
	}
}

auth.BearerCredentials() is the pair auth.Token reads: the Authorization header and the token query parameter. See AsyncAPI.

Next: AsyncAPI.

MIT licensed