Preventing users from having multiple sessions

is there a way to prevent users connecting from multiple devices ?

This is in lua-
One way to prevent multiple session would be to create an rpc like below to check whether user has multiple sessions or not using the notification stream.
Call this rpc after successful authentication.
It uses default notification stream count for that user_id.

function o_u_h.check_user_on_stream(context, payload)

    local result = {
        session_already_connected = false
    }

    -- checking for notification stream
    local count = nk.stream_count({mode = 0, subject = context.user_id})
    if (count > 1) then
        -- user is already connected
        result.session_already_connected = true
    end

    return nk.json_encode(result)

end
nk.register_rpc(o_u_h.check_user_on_stream, "check_user_on_stream")
2 Likes

@novabyte also shared it’s equivalent Golang code on community channel -
Here’s that

type SessionCheckResponse struct {
    AlreadyConnected bool `json:"already_connected"`
}

func rpcSessionCheck(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
    userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
    if !ok {
        return "", runtime.NewError("no ID for user; must be authenticated", 3)
    }
    // See how many presences the user has on their notification stream.
    count, err := nk.StreamCount(0, userID, "", "")
    if err != nil {
        return "", fmt.Errorf("unable to count notification stream for user: %s", userID)
    }
    response, err := json.Marshal(&SessionCheckResponse{AlreadyConnected: count > 1})
    if err != nil {
        logger.Error("unable to encode json: %v", err)
        return "", errors.New("failed to encode json")
    }
    return string(response), nil
}

You register it within your InitModule function like so:

 if err := initializer.RegisterRpc("SessionCheck", rpcSessionCheck); err != nil {
    return err
}
2 Likes

& I think, Instead of creating RPC, You can also hook that function code on the before hook for the authentication, but I don’t know if there is a way to return custom errors from the before hook to know the cause on client side.

Thanks a lot , u save my day

2 Likes

Thanks @hdjay0129. That’s a great answer. I’ll add more detail for @naskha01 and anyone who stumbles across this question.

When a socket connection is opened with a session to Nakama the session is implicitly placed onto the notification stream. This allows the user to receive in-app notifications to that session. With the low level streams feature in Nakama you can take advantage of this info to check whether a session for the user already exists with a stream count of that notification stream.

i.e. From the code @hdjay0129 shared:

local user_id = context.user_id
local mode = 0 -- mode 0 is the notification stream for the user.
local count = nk.stream_count({ mode = mode, subject = user_id })
if (count > 1) then
    nk.logger_info(("User %q has %q active sessions"):format(user_id, count))
end

Streams are very powerful and underpin most of the realtime engine in the game server but they should be used with care! :slight_smile:

You can apply the above check with an RPC function called from the client or socket object in one of the sdks. For example with the Unity client sdk:

var client = new Client("http", "127.0.0.1", 7350, "defaultkey");
var session = ... // Authenticated user session. See docs
await client.RpcAsync(session, "SessionCheck"); // rpc id

Finally for when to initiate this check against the server from the game client depends on what client operations you execute at game start and whether you want to make the check only at a specific moment (like if the user wants to play a multiplayer match).

Exactly when to do it depends on the gameplay design but you could attach it as a preflight to a common request. For example in a before hook for an account get request:

local nk = require("nakama")
local function beforeAccountGet(context, payload)
    local count = nk.stream_count({ mode = 0, subject = context.user_id })
    if (count > 1) then
        error(("User already active: %q"):format(count))
    end
end
nk.register_req_before(beforeAccountGet, "GetAccount")

Hope this helps.

1 Like