diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php
index ed3c7f67..0678d895 100644
--- a/src/app/Http/Controllers/API/V4/OpenViduController.php
+++ b/src/app/Http/Controllers/API/V4/OpenViduController.php
@@ -1,174 +1,261 @@
 <?php
 
 namespace App\Http\Controllers\API\V4;
 
 use App\Http\Controllers\Controller;
 use App\OpenVidu\Room;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Validator;
 
 class OpenViduController extends Controller
 {
     /**
      * Close the room session.
      *
      * @param string $id Room identifier (name)
      *
      * @return \Illuminate\Http\JsonResponse
      */
     public function closeRoom($id)
     {
         $room = Room::where('name', $id)->first();
 
         // This isn't a room, bye bye
         if (!$room) {
             return $this->errorResponse(404, \trans('meet.room-not-found'));
         }
 
         $user = Auth::guard()->user();
 
         // Only the room owner can do it
         if (!$user || $user->id != $room->user_id) {
             return $this->errorResponse(403);
         }
 
         if (!$room->deleteSession()) {
             return $this->errorResponse(500, \trans('meet.session-close-error'));
         }
 
         return response()->json([
                 'status' => 'success',
                 'message' => __('meet.session-close-success'),
         ]);
     }
 
     /**
      * Listing of rooms that belong to the current user.
      *
      * @return \Illuminate\Http\JsonResponse
      */
     public function index()
     {
         $user = Auth::guard()->user();
 
         $rooms = Room::where('user_id', $user->id)->orderBy('name')->get();
 
         if (count($rooms) == 0) {
             // Create a room for the user (with a random and unique name)
             while (true) {
                 $name = strtolower(\App\Utils::randStr(3, 3, '-'));
                 if (!Room::where('name', $name)->count()) {
                     break;
                 }
             }
 
             $room = Room::create([
                     'name' => $name,
                     'user_id' => $user->id
             ]);
 
             $rooms = collect([$room]);
         }
 
         $result = [
             'list' => $rooms,
             'count' => count($rooms),
         ];
 
         return response()->json($result);
     }
 
     /**
      * Join the room session. Each room has one owner, and the room isn't open until the owner
      * joins (and effectively creates the session).
      *
      * @param string $id Room identifier (name)
      *
      * @return \Illuminate\Http\JsonResponse
      */
     public function joinRoom($id)
     {
         $room = Room::where('name', $id)->first();
 
         // Room does not exist, or the owner is deleted
         if (!$room || !$room->owner) {
             return $this->errorResponse(404, \trans('meet.room-not-found'));
         }
 
         // Check if there's still a valid beta entitlement for the room owner
         $sku = \App\Sku::where('title', 'meet')->first();
         if ($sku && !$room->owner->entitlements()->where('sku_id', $sku->id)->first()) {
             return $this->errorResponse(404, \trans('meet.room-not-found'));
         }
 
         $user = Auth::guard()->user();
+        $isOwner = $user && $user->id == $room->user_id;
 
         // There's no existing session
         if (!$room->hasSession()) {
             // Participants can't join the room until the session is created by the owner
-            if (!$user || $user->id != $room->user_id) {
+            if (!$isOwner) {
                 return $this->errorResponse(423, \trans('meet.session-not-found'));
             }
 
             // The room owner can create the session on request
             if (empty(request()->input('init'))) {
                 return $this->errorResponse(424, \trans('meet.session-not-found'));
             }
 
             $session = $room->createSession();
 
             if (empty($session)) {
                 return $this->errorResponse(500, \trans('meet.session-create-error'));
             }
         }
 
+        $password = (string) $room->getSetting('password');
+
+        $config = [
+            'locked' => $room->getSetting('locked') === 'true',
+            'password' => $isOwner ? $password : '',
+            'requires_password' => !$isOwner && strlen($password),
+        ];
+
+        // Validate room password
+        if (!$isOwner && strlen($password)) {
+            $request_password = request()->input('password');
+            if ($request_password !== $password) {
+                // Note: We send the config to the client so it knows to display the password field
+                $response = [
+                    'config' => $config,
+                    'message' => \trans('meet.session-password-error'),
+                    'status' => 'error',
+                ];
+
+                return response()->json($response, 425);
+            }
+        }
+
         // Create session token for the current user/connection
         $response = $room->getSessionToken('PUBLISHER');
 
         if (empty($response)) {
             return $this->errorResponse(500, \trans('meet.session-join-error'));
         }
 
         // Create session token for screen sharing connection
         if (!empty(request()->input('screenShare'))) {
             $add_token = $room->getSessionToken('PUBLISHER');
 
             $response['shareToken'] = $add_token['token'];
         }
 
         // Tell the UI who's the room owner
-        $response['owner'] = $user && $user->id == $room->user_id;
+        $response['owner'] = $isOwner;
+
+        // Append the room configuration
+
+        $response['config'] = $config;
 
         return response()->json($response);
     }
 
+    /**
+     * Set the domain configuration.
+     *
+     * @param string $id Room identifier (name)
+     *
+     * @return \Illuminate\Http\JsonResponse|void
+     */
+    public function setRoomConfig($id)
+    {
+        $room = Room::where('name', $id)->first();
+
+        // Room does not exist, or the owner is deleted
+        if (!$room || !$room->owner) {
+            return $this->errorResponse(404);
+        }
+
+        $user = Auth::guard()->user();
+
+        // Only room owner can configure the room
+        if ($user->id != $room->user_id) {
+            return $this->errorResponse(403);
+        }
+
+        $input = request()->input();
+        $errors = [];
+
+        foreach ($input as $key => $value) {
+            switch ($key) {
+                case 'password':
+                    if ($value === null || $value === '') {
+                        $input[$key] = null;
+                    } else {
+                        // TODO: Do we have to validate the password in any way?
+                    }
+                    break;
+
+                case 'locked':
+                    $input[$key] = $value ? 'true' : null;
+                    break;
+
+                default:
+                    $errors[$key] = \trans('meet.room-unsupported-option-error');
+            }
+        }
+
+        if (!empty($errors)) {
+            return response()->json(['status' => 'error', 'errors' => $errors], 422);
+        }
+
+        if (!empty($input)) {
+            $room->setSettings($input);
+        }
+
+        return response()->json([
+                'status' => 'success',
+                'message' => \trans('meet.room-setconfig-success'),
+        ]);
+    }
+
     /**
      * Webhook as triggered from OpenVidu server
      *
      * @param \Illuminate\Http\Request $request The API request.
      *
      * @return \Illuminate\Http\Response The response
      */
     public function webhook(Request $request)
     {
         \Log::debug($request->getContent());
 
         switch ((string) $request->input('event')) {
             case 'sessionDestroyed':
                 // When all participants left the room OpenVidu dispatches sessionDestroyed
                 // event. We'll remove the session reference from the database.
                 $sessionId = $request->input('sessionId');
                 $room = Room::where('session_id', $sessionId)->first();
 
                 if ($room) {
                     $room->session_id = null;
                     $room->save();
                 }
 
                 break;
         }
 
         return response('Success', 200);
     }
 }
diff --git a/src/app/OpenVidu/Room.php b/src/app/OpenVidu/Room.php
index da291736..3579fab5 100644
--- a/src/app/OpenVidu/Room.php
+++ b/src/app/OpenVidu/Room.php
@@ -1,166 +1,174 @@
 <?php
 
 namespace App\OpenVidu;
 
 use App\Traits\SettingsTrait;
 use Illuminate\Database\Eloquent\Model;
 
+/**
+ * The eloquent definition of a Room.
+ *
+ * @property int     $id         Room identifier
+ * @property string  $name       Room name
+ * @property int     $user_id    Room owner
+ * @property ?string $session_id OpenVidu session identifier
+ */
 class Room extends Model
 {
     use SettingsTrait;
 
     protected $fillable = [
         'user_id',
         'name'
     ];
 
     protected $table = 'openvidu_rooms';
 
     /** @var \GuzzleHttp\Client|null HTTP client instance */
     private static $client = null;
 
 
     /**
      * Creates HTTP client for connections to OpenVidu server
      *
      * @return \GuzzleHttp\Client HTTP client instance
      */
     private function client()
     {
         if (!self::$client) {
             self::$client = new \GuzzleHttp\Client(
                 [
                     'http_errors' => false, // No exceptions from Guzzle
                     'base_uri' => \config('openvidu.api_url'),
                     'verify' => \config('openvidu.api_verify_tls'),
                     'auth' => [
                         \config('openvidu.api_username'),
                         \config('openvidu.api_password')
                     ]
                 ]
             );
         }
 
         return self::$client;
     }
 
     /**
      * Create a OpenVidu session
      *
      * @return array|null Session data on success, NULL otherwise
      */
     public function createSession(): ?array
     {
         $response = $this->client()->request(
             'POST',
             "sessions",
             [
                 'json' => [
                     'mediaMode' => 'ROUTED',
                     'recordingMode' => 'MANUAL'
                 ]
             ]
         );
 
         if ($response->getStatusCode() !== 200) {
             $this->session_id = null;
             $this->save();
         }
 
         $session = json_decode($response->getBody(), true);
 
         $this->session_id = $session['id'];
         $this->save();
 
         return $session;
     }
 
     /**
      * Delete a OpenVidu session
      *
      * @return bool
      */
     public function deleteSession(): bool
     {
         if (!$this->session_id) {
             return true;
         }
 
         $response = $this->client()->request(
             'DELETE',
             "sessions/" . $this->session_id,
         );
 
         if ($response->getStatusCode() == 204) {
             $this->session_id = null;
             $this->save();
 
             return true;
         }
 
         return false;
     }
 
     /**
      * Create a OpenVidu session (connection) token
      *
      * @return array|null Token data on success, NULL otherwise
      */
     public function getSessionToken($role = 'PUBLISHER'): ?array
     {
         $response = $this->client()->request(
             'POST',
             'tokens',
             [
                 'json' => [
                     'session' => $this->session_id,
                     'role' => $role
                 ]
             ]
         );
 
         if ($response->getStatusCode() == 200) {
             $json = json_decode($response->getBody(), true);
 
             return $json;
         }
 
         return null;
     }
 
     /**
      * Check if the room has an active session
      *
      * @return bool True when the session exists, False otherwise
      */
     public function hasSession(): bool
     {
         if (!$this->session_id) {
             return false;
         }
 
         $response = $this->client()->request('GET', "sessions/{$this->session_id}");
 
         return $response->getStatusCode() == 200;
     }
 
     /**
      * The room owner.
      *
      * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
      */
     public function owner()
     {
         return $this->belongsTo('\App\User', 'user_id', 'id');
     }
 
     /**
      * Any (additional) properties of this room.
      *
      * @return \Illuminate\Database\Eloquent\Relations\HasMany
      */
     public function settings()
     {
         return $this->hasMany('App\OpenVidu\RoomSetting', 'room_id');
     }
 }
diff --git a/src/resources/js/meet.js b/src/resources/js/meet.js
index 4665f826..c52aed93 100644
--- a/src/resources/js/meet.js
+++ b/src/resources/js/meet.js
@@ -1,38 +1,40 @@
 /**
  * Application code for the Meet UI
  */
 
 import routes from './routes-meet.js'
 
 window.routes = routes
 window.isAdmin = false
 
 require('./app')
 
 // Register additional icons
 import { library } from '@fortawesome/fontawesome-svg-core'
 
 import {
     faAlignLeft,
     faCompress,
     faDesktop,
     faExpand,
     faMicrophone,
     faPowerOff,
     faUser,
+    faShieldAlt,
     faVideo,
     faVolumeMute
 } from '@fortawesome/free-solid-svg-icons'
 
 // Register only these icons we need
 library.add(
     faAlignLeft,
     faCompress,
     faDesktop,
     faExpand,
     faMicrophone,
     faPowerOff,
     faUser,
+    faShieldAlt,
     faVideo,
     faVolumeMute
 )
diff --git a/src/resources/lang/en/meet.php b/src/resources/lang/en/meet.php
index 7958bd85..6f0341c7 100644
--- a/src/resources/lang/en/meet.php
+++ b/src/resources/lang/en/meet.php
@@ -1,22 +1,26 @@
 <?php
 
 return [
 
     /*
     |--------------------------------------------------------------------------
     | Pagination Language Lines
     |--------------------------------------------------------------------------
     |
     | The following language lines are used by the paginator library to build
     | the simple pagination links. You are free to change them to anything
     | you want to customize your views to better match your application.
     |
     */
 
     'room-not-found' => 'The room does not exist.',
+    'room-setconfig-success' => 'Room configuration updated successfully.',
+    'room-unsupported-option-error' => 'Invalid room configuration option.',
     'session-not-found' => 'The session does not exist.',
     'session-create-error' => 'Failed to create the session.',
     'session-join-error' => 'Failed to join the session.',
     'session-close-error' => 'Failed to close the session.',
     'session-close-success' => 'The session has been closed successfully.',
+    'session-password-error' => 'Failed to join the session. Invalid password.',
+
 ];
diff --git a/src/resources/themes/forms.scss b/src/resources/themes/forms.scss
index 5690b8ed..f8bd1d75 100644
--- a/src/resources/themes/forms.scss
+++ b/src/resources/themes/forms.scss
@@ -1,47 +1,77 @@
 .list-input {
     & > div {
         &:not(:last-child) {
             margin-bottom: -1px;
 
             input,
             a.btn {
                 border-bottom-right-radius: 0;
                 border-bottom-left-radius: 0;
             }
         }
 
         &:not(:first-child) {
             input,
             a.btn {
                 border-top-right-radius: 0;
                 border-top-left-radius: 0;
             }
         }
     }
 
     input.is-invalid {
         z-index: 2;
     }
 
     .btn svg {
         vertical-align: middle;
     }
 }
 
 .range-input {
     display: flex;
 
     label {
         margin-right: 0.5em;
     }
 }
 
+.input-group-activable {
+    &.active {
+        :not(.input-group-append):not(.activable) {
+            display: none;
+        }
+    }
+
+    &:not(.active) {
+        .activable {
+            display: none;
+        }
+    }
+
+    // Label is always visible
+    .label {
+        color: $body-color;
+        display: initial !important;
+    }
+
+    .input-group-text {
+        border-color: transparent;
+        background: transparent;
+        padding-left: 0;
+
+        &:not(.label) {
+            flex: 1;
+        }
+    }
+}
+
 .form-control-plaintext .btn-sm {
     margin-top: -0.25rem;
 }
 
 form.read-only {
     .row {
         margin-bottom: 0;
     }
 }
diff --git a/src/resources/themes/meet.scss b/src/resources/themes/meet.scss
index ed13c224..00bdbf11 100644
--- a/src/resources/themes/meet.scss
+++ b/src/resources/themes/meet.scss
@@ -1,251 +1,255 @@
 .meet-video {
     position: relative;
     background: $menu-bg-color;
     // Use flexbox for centering .watermark
     display: flex;
     align-items: center;
     justify-content: center;
 
     .watermark {
         color: darken($menu-bg-color, 20%);
         width: 50%;
         height: 50%;
     }
 
     video {
         // To make object-fit:cover working we have to set the height in pixels
         // on the wrapper element. This is what javascript method will do.
         object-fit: cover;
         width: 100%;
         height: 100%;
         background: #000;
 
         & + .watermark {
             display: none;
         }
     }
 
     &.fullscreen {
         video {
             // We don't want the video to be cut in fullscreen
             // This will preserve the aspect ratio of the video stream
             object-fit: contain;
         }
     }
 
     .controls {
         position: absolute;
         bottom: 0;
         right: 0;
         margin: 0.5em;
         padding: 0 0.05em;
         line-height: 2em;
         border-radius: 1em;
         background: rgba(#000, 0.7);
 
         button {
             line-height: 2;
             border-radius: 50%;
             padding: 0;
             width: 2em;
         }
     }
 
     .status {
         position: absolute;
         bottom: 0;
         left: 0;
         margin: 0.5em;
         line-height: 2em;
 
         span {
             display: inline-block;
             color: #fff;
             border-radius: 50%;
             width: 2em;
             text-align: center;
             margin-right: 0.25em;
         }
     }
 
     .nickname {
         position: absolute;
         top: 0;
         left: 0;
         margin: 0.5em;
         padding: 0 1em;
         line-height: 2em;
         border-radius: 1em;
         max-width: calc(100% - 1em);
         background: rgba(#fff, 0.8);
         overflow: hidden;
         text-overflow: ellipsis;
         white-space: nowrap;
 
         button {
             display: none;
         }
 
         span {
             outline: none;
         }
     }
 
     &.publisher .nickname {
         cursor: pointer;
         background: rgba($main-color, 0.9);
 
         &:focus-within {
             box-shadow: $btn-focus-box-shadow;
         }
 
         span:empty {
             display: block;
             height: 2em;
 
             &:not(:focus) + button {
                 display: block;
                 position: absolute;
                 top: 0;
                 left: 0;
                 width: 2em;
                 height: 2em;
                 border-radius: 50%;
                 padding: 0;
                 color: $menu-gray;
             }
         }
     }
 }
 
 #meet-component {
     flex-grow: 1;
     display: flex;
     flex-direction: column;
 
     & + .filler {
         display: none;
     }
 }
 
 #app.meet {
     height: 100%;
 
     #meet-component {
         overflow: hidden;
     }
 }
 
 #meet-setup {
     max-width: 720px;
+
+    .input-group svg {
+        width: 1em;
+    }
 }
 
 #meet-auth {
     margin-top: 2rem;
     margin-bottom: 2rem;
     flex: 1;
 }
 
 #meet-session-toolbar {
     display: flex;
     justify-content: center;
 }
 
 #meet-session-menu {
     button {
         font-size: 1.3em;
         padding: 0 0.25em;
         margin: 0.5em;
         position: relative;
 
         .badge {
             font-size: 0.5em;
             position: absolute;
             right: -0.5em;
 
             &:empty {
                 display: none;
             }
         }
     }
 }
 
 #meet-session-layout {
     flex: 1;
     overflow: hidden;
 }
 
 #meet-session {
     display: flex;
     justify-content: center;
     flex-wrap: wrap;
     flex: 1;
     //overflow: hidden;
 }
 
 #meet-chat {
     width: 0;
     display: none;
     flex-direction: column;
 
     &.open {
         width: 30%;
         display: flex !important;
 
         .mobile & {
             width: 100%;
             z-index: 1;
             background: $body-bg;
         }
     }
 
     .chat {
         flex: 1;
         overflow-y: auto;
     }
 
     .message {
         margin: 0 0.5em 0.5em 0.5em;
         padding: 0.25em 0.5em;
         border-radius: 1em;
         background: $menu-bg-color;
         overflow-wrap: break-word;
 
         &.self {
             background: lighten($main-color, 30%);
         }
     }
 
     .nickname {
         font-size: 80%;
         color: $secondary;
         text-align: right;
     }
 
     // TODO: mobile mode
 }
 
 #setup-preview {
     display: flex;
 
     video {
         width: 100%;
         transform: rotateY(180deg);
         background: #000;
     }
 
     .volume {
         height: 50%;
         position: absolute;
         bottom: 1em;
         right: 2em;
         width: 0.5em;
         background: rgba(0, 0, 0, 0.5);
 
         .bar {
             width: 100%;
             position: absolute;
             bottom: 0;
         }
     }
 }
diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue
index 549755eb..f5951b11 100644
--- a/src/resources/vue/Meet/Room.vue
+++ b/src/resources/vue/Meet/Room.vue
@@ -1,352 +1,397 @@
 <template>
     <div id="meet-component">
         <div id="meet-session-toolbar" class="hidden">
             <div id="meet-session-menu">
                 <button class="btn btn-link link-audio" @click="switchSound" title="Mute audio">
                     <svg-icon icon="microphone"></svg-icon>
                 </button>
                 <button class="btn btn-link link-video" @click="switchVideo" title="Mute video">
                     <svg-icon icon="video"></svg-icon>
                 </button>
                 <button class="btn btn-link link-screen text-danger" @click="switchScreen" :disabled="!canShareScreen" title="Share screen">
                     <svg-icon icon="desktop"></svg-icon>
                 </button>
                 <button class="btn btn-link link-chat text-danger" @click="switchChat" title="Chat">
                     <svg-icon icon="align-left"></svg-icon>
                 </button>
                 <button class="btn btn-link link-fullscreen closed hidden" @click="switchFullscreen" title="Full screen">
                     <svg-icon icon="expand"></svg-icon>
                 </button>
                 <button class="btn btn-link link-fullscreen open hidden" @click="switchFullscreen" title="Full screen">
                     <svg-icon icon="compress"></svg-icon>
                 </button>
+                <button class="btn btn-link link-security" v-if="session && session.owner" @click="securityOptions" title="Security options">
+                    <svg-icon icon="shield-alt"></svg-icon>
+                </button>
                 <button class="btn btn-link link-logout" @click="logout" title="Leave session">
                     <svg-icon icon="power-off"></svg-icon>
                 </button>
             </div>
         </div>
 
         <div id="meet-setup" class="card container mt-2 mt-md-5 mb-5">
             <div class="card-body">
                 <div class="card-title">Set up your session</div>
                 <div class="card-text">
                     <form class="setup-form row">
                         <div id="setup-preview" class="col-sm-6 mb-3 mb-sm-0">
                             <video class="rounded"></video>
                             <div class="volume"><div class="bar"></div></div>
                         </div>
-                        <div class="col-sm-6">
-                            <div class="form-group">
-                                <label for="setup-microphone">Microphone</label>
+                        <div class="col-sm-6 align-self-center">
+                            <div class="input-group">
+                                <label for="setup-microphone" class="input-group-prepend mb-0">
+                                    <span class="input-group-text" title="Microphone"><svg-icon icon="microphone"></svg-icon></span>
+                                </label>
                                 <select class="custom-select" id="setup-microphone" v-model="microphone" @change="setupMicrophoneChange">
                                     <option value="">None</option>
                                     <option v-for="mic in setup.microphones" :value="mic.deviceId" :key="mic.deviceId">{{ mic.label }}</option>
                                 </select>
                             </div>
-                            <div class="form-group">
-                                <label for="setup-camera">Camera</label>
+                            <div class="input-group mt-2">
+                                <label for="setup-camera" class="input-group-prepend mb-0">
+                                    <span class="input-group-text" title="Camera"><svg-icon icon="video"></svg-icon></span>
+                                </label>
                                 <select class="custom-select" id="setup-camera" v-model="camera" @change="setupCameraChange">
                                     <option value="">None</option>
                                     <option v-for="cam in setup.cameras" :value="cam.deviceId" :key="cam.deviceId">{{ cam.label }}</option>
                                 </select>
                             </div>
-                            <div class="form-group mb-0">
-                                <label for="setup-nickname">Nickname</label>
-                                <input class="form-control" type="text" id="setup-nickname" v-model="nickname">
+                            <div class="input-group mt-2">
+                                <label for="setup-nickname" class="input-group-prepend mb-0">
+                                    <span class="input-group-text" title="Nickname"><svg-icon icon="user"></svg-icon></span>
+                                </label>
+                                <input class="form-control" type="text" id="setup-nickname" v-model="nickname" placeholder="Your name">
+                            </div>
+                            <div class="input-group mt-2" v-if="session.config && session.config.requires_password">
+                                <label for="setup-password" class="input-group-prepend mb-0">
+                                    <span class="input-group-text" title="Password"><svg-icon icon="key"></svg-icon></span>
+                                </label>
+                                <input type="password" class="form-control" id="setup-password" v-model="password" placeholder="Password">
+                            </div>
+                            <div class="mt-3">
+                                <button v-if="roomState == 'ready' || roomState == 424 || roomState == 425"
+                                        type="button"
+                                        @click="joinSession"
+                                        :class="'btn w-100 btn-' + (roomState == 'ready' ? 'success' : 'primary')"
+                                >JOIN</button>
+                                <button v-if="roomState == 423"
+                                        type="button"
+                                        @click="joinSession"
+                                        class="btn btn-primary w-100"
+                                >I'm the owner</button>
                             </div>
                         </div>
-                        <div class="text-center mt-4 col-sm-12">
-                            <status-message :status="roomState" :status-labels="roomStateLabels" class="mb-3"></status-message>
-                            <button v-if="roomState == 'ready' || roomState == 424"
-                                    type="button"
-                                    @click="joinSession"
-                                    class="btn btn-primary pl-5 pr-5"
-                            >JOIN</button>
-                            <button v-if="roomState == 423"
-                                    type="button"
-                                    @click="joinSession"
-                                    class="btn btn-primary pl-5 pr-5"
-                            >I'm the owner</button>
+                        <div class="mt-4 col-sm-12">
+                            <status-message :status="roomState" :status-labels="roomStateLabels"></status-message>
                         </div>
                     </form>
                 </div>
             </div>
         </div>
 
         <div id="meet-session-layout" class="d-flex hidden">
             <div id="meet-session"></div>
             <div id="meet-chat">
                 <div class="chat"></div>
                 <div class="chat-input m-2">
                     <textarea class="form-control" rows="1"></textarea>
                 </div>
             </div>
         </div>
 
         <logon-form id="meet-auth" class="hidden" :dashboard="false" @success="authSuccess"></logon-form>
 
         <div id="leave-dialog" class="modal" tabindex="-1" role="dialog">
             <div class="modal-dialog" role="document">
                 <div class="modal-content">
                     <div class="modal-header">
                         <h5 class="modal-title">Room closed</h5>
                         <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                             <span aria-hidden="true">&times;</span>
                         </button>
                     </div>
                     <div class="modal-body">
                         <p>The session has been closed by the room owner.</p>
                     </div>
                     <div class="modal-footer">
                         <button type="button" class="btn btn-danger modal-action" data-dismiss="modal">Close</button>
                     </div>
                 </div>
             </div>
         </div>
+
+        <session-security-options v-if="session.config" :config="session.config" :room="room" @config-update="configUpdate"></session-security-options>
     </div>
 </template>
 
 <script>
     import Meet from '../../js/meet/app.js'
     import StatusMessage from '../Widgets/StatusMessage'
     import LogonForm from '../Login'
+    import SessionSecurityOptions from './SessionSecurityOptions'
 
     export default {
         components: {
             LogonForm,
+            SessionSecurityOptions,
             StatusMessage
         },
         data() {
             return {
                 setup: {
                     cameras: [],
                     microphones: [],
                 },
                 canShareScreen: false,
                 camera: '',
                 meet: null,
                 microphone: '',
                 nickname: '',
+                password: '',
                 room: null,
                 roomState: 'init',
                 roomStateLabels: {
                     init: 'Checking the room...',
                     404: 'The room does not exist.',
                     423: 'The room is closed. Please, wait for the owner to start the session.',
                     424: 'The room is closed. It will be open for others after you join.',
+                    425: 'The room is ready. Please, provide a valid password.',
                     500: 'Failed to create a session. Server error.'
                 },
                 session: {}
             }
         },
         mounted() {
             this.room = this.$route.params.room
 
             // Initialize OpenVidu and do some basic checks
             this.meet = new Meet($('#meet-session')[0]);
             this.canShareScreen = this.meet.isScreenSharingSupported()
 
             // Check the room and init the session
             this.initSession()
 
             // Setup the room UI
             this.setupSession()
         },
         beforeDestroy() {
             clearTimeout(window.roomRequest)
 
             if (this.meet) {
                 this.meet.leaveRoom()
             }
         },
         methods: {
             authSuccess() {
                 // The user (owner) authentication succeeded
                 this.roomState = 'init'
                 this.initSession()
 
                 $('#meet-setup').removeClass('hidden')
                 $('#meet-auth').addClass('hidden')
             },
+            configUpdate(config) {
+                this.session.config = Object.assign({}, this.session.config, config)
+            },
             initSession(init) {
-                let params = []
-
-                if (this.canShareScreen) {
-                    params.push('screenShare=1')
+                this.post = {
+                    password: this.password,
+                    nickname: this.nickname,
+                    screenShare: this.canShareScreen ? 1 : 0,
+                    init: init ? 1 : 0
                 }
 
-                if (init) {
-                    params.push('init=1')
-                }
+                $('#setup-password').removeClass('is-invalid')
 
-                const url = '/api/v4/openvidu/rooms/' + this.room + '?' + params.join('&')
-
-                axios.get(url, { ignoreErrors: true })
+                axios.post('/api/v4/openvidu/rooms/' + this.room, this.post, { ignoreErrors: true })
                     .then(response => {
                         // Response data contains: session, token and shareToken
                         this.roomState = 'ready'
                         this.session = response.data
 
                         if (init) {
                             this.joinSession()
                         }
                     })
                     .catch(error => {
                         this.roomState = String(error.response.status)
 
-                        // Waiting for the owner to open the room...
-                        if (error.response.status == 423) {
-                            // Update room state every 10 seconds
-                            window.roomRequest = setTimeout(() => { this.initSession() }, 10000)
+                        if (error.response.data && error.response.data.config) {
+                            this.session.config = error.response.data.config
+                        }
+
+                        switch (this.roomState) {
+                            case '423':
+                                // Waiting for the owner to open the room...
+                                // Update room state every 10 seconds
+                                window.roomRequest = setTimeout(() => { this.initSession() }, 10000)
+                                break;
+
+                            case '425':
+                                // Missing/invalid password
+                                if (init) {
+                                    $('#setup-password').addClass('is-invalid').focus()
+                                }
+                                break;
                         }
                     })
 
                 if (document.fullscreenEnabled) {
                     $('#meet-session-menu').find('.link-fullscreen.closed').removeClass('hidden')
                 }
             },
             joinSession() {
                 if (this.roomState == 423) {
                     $('#meet-setup').addClass('hidden')
                     $('#meet-auth').removeClass('hidden')
                     return
                 }
 
-                if (this.roomState == 424) {
+                if (this.roomState == 424 || this.roomState == 425) {
                     this.initSession(true)
                     return
                 }
 
                 clearTimeout(window.roomRequest)
 
                 $('#app').addClass('meet')
                 $('#meet-setup').addClass('hidden')
                 $('#meet-session-toolbar,#meet-session-layout').removeClass('hidden')
 
+                if (!this.canShareScreen) {
+                    this.setMenuItem('screen', false, true)
+                }
+
                 this.session.nickname = this.nickname
                 this.session.menuElement = $('#meet-session-menu')[0]
                 this.session.chatElement = $('#meet-chat')[0]
                 this.session.onDestroy = event => {
                     // TODO: Handle nicely other reasons: disconnect, forceDisconnectByUser,
                     //       forceDisconnectByServer, networkDisconnect?
                     if (event.reason == 'sessionClosedByServer' && !this.session.owner) {
                         $('#leave-dialog').on('hide.bs.modal', () => {
                             // FIXME: Where exactly the user should land? Currently he'll land
                             //        on dashboard (if he's logged in) or login form (if he's not).
 
                             window.location = window.config['app.url']
                         }).modal()
                     }
                 }
 
                 this.meet.joinRoom(this.session)
             },
             logout() {
                 if (this.session.owner) {
                     axios.post('/api/v4/openvidu/rooms/' + this.room + '/close')
                         .then(response => {
                             this.meet.leaveRoom()
                             this.meet = null
                             window.location = window.config['app.url']
                         })
                 } else {
                     this.meet.leaveRoom()
                     this.meet = null
                     window.location = window.config['app.url']
                 }
             },
+            securityOptions() {
+                $('#security-options-dialog').modal()
+            },
             setMenuItem(type, state, disabled) {
                 let button = $('#meet-session-menu').find('.link-' + type)
 
                 button[state ? 'removeClass' : 'addClass']('text-danger')
 
                 if (disabled !== undefined) {
                     button.prop('disabled', disabled)
                 }
             },
             setupSession() {
                 this.meet.setup({
                     videoElement: $('#setup-preview video')[0],
                     volumeElement: $('#setup-preview .volume')[0],
                     onSuccess: setup => {
                         this.setup = setup
                         this.microphone = setup.audioSource
                         this.camera = setup.videoSource
 
                         this.setMenuItem('audio', setup.audioActive)
                         this.setMenuItem('video', setup.videoActive)
                     },
                     onError: error => {
                         this.setMenuItem('audio', false, true)
                         this.setMenuItem('video', false, true)
                     }
                 })
             },
             setupCameraChange() {
                 this.meet.setupSetVideoDevice(this.camera).then(enabled => {
                     this.setMenuItem('video', enabled)
                 })
             },
             setupMicrophoneChange() {
                 this.meet.setupSetAudioDevice(this.microphone).then(enabled => {
                     this.setMenuItem('audio', enabled)
                 })
             },
             switchChat() {
                 let chat = $('#meet-chat')
                 let enabled = chat.is('.open')
 
                 this.setMenuItem('chat', !enabled)
                 chat.toggleClass('open')
 
                 if (!enabled) {
                     chat.find('textarea').focus()
                 }
 
                 // Trigger resize, so participant matrix can update its layout
                 window.dispatchEvent(new Event('resize'));
             },
             switchFullscreen() {
                 const element = this.$el
 
                 $(element).off('fullscreenchange').on('fullscreenchange', (e) => {
                     let enabled = document.fullscreenElement == element
                     let buttons = $('#meet-session-menu').find('.link-fullscreen')
 
                     buttons.first()[enabled ? 'addClass' : 'removeClass']('hidden')
                     buttons.last()[!enabled ? 'addClass' : 'removeClass']('hidden')
                 })
 
                 if (document.fullscreenElement) {
                     document.exitFullscreen()
                 } else {
                     element.requestFullscreen()
                 }
             },
             switchSound() {
                 const enabled = this.meet.switchAudio()
                 this.setMenuItem('audio', enabled)
             },
             switchVideo() {
                 const enabled = this.meet.switchVideo()
                 this.setMenuItem('video', enabled)
             },
             switchScreen() {
                 this.meet.switchScreen(enabled => {
                     this.setMenuItem('screen', enabled)
 
                     // After one screen sharing session ended request a new token
                     // for the next screen sharing session
                     if (!enabled) {
-                        axios.get('/api/v4/openvidu/rooms/' + this.room, { ignoreErrors: true })
+                        // TODO: This might need to be a different route. E.g. the room password might have
+                        //       changed since user joined the session
+                        axios.post('/api/v4/openvidu/rooms/' + this.room, this.post, { ignoreErrors: true })
                             .then(response => {
                                 // Response data contains: session, token and shareToken
                                 this.session.shareToken = response.data.token
                                 this.meet.updateSession(this.session)
                             })
                     }
                 })
             }
         }
     }
 </script>
diff --git a/src/resources/vue/Meet/SessionSecurityOptions.vue b/src/resources/vue/Meet/SessionSecurityOptions.vue
new file mode 100644
index 00000000..8d6a2b78
--- /dev/null
+++ b/src/resources/vue/Meet/SessionSecurityOptions.vue
@@ -0,0 +1,110 @@
+<template>
+    <div v-if="config">
+        <div id="security-options-dialog" class="modal" tabindex="-1" role="dialog">
+            <div class="modal-dialog" role="document">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <h5 class="modal-title">Security options</h5>
+                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                            <span aria-hidden="true">&times;</span>
+                        </button>
+                    </div>
+                    <div class="modal-body">
+                        <form id="security-options-password">
+                            <div id="password-input" class="input-group input-group-activable">
+                                <span class="input-group-text label">Password:</span>
+                                <span v-if="config.password" id="password-input-text" class="input-group-text">{{ config.password }}</span>
+                                <span v-else id="password-input-text" class="input-group-text text-muted">none</span>
+                                <input type="text" :value="config.password" name="password" class="form-control rounded-left activable">
+                                <div class="input-group-append">
+                                    <button type="button" @click="passwordSave" id="password-save-btn" class="btn btn-outline-primary activable rounded-right">Save</button>
+                                    <button type="button" v-if="config.password" id="password-clear-btn" @click="passwordClear" class="btn btn-outline-danger rounded">Clear password</button>
+                                    <button type="button" v-else @click="passwordSet" id="password-set-btn" class="btn btn-outline-primary rounded">Set password</button>
+                                </div>
+                            </div>
+                            <small class="form-text text-muted">
+                                You can add a password to your meeting. Participants will have to provide
+                                the password before they are allowed to join the meeting.
+                            </small>
+                        </form>
+                        <hr v-if="false">
+                        <form v-if="false" id="security-options-lock">
+                            <div id="room-lock" class="">
+                                <span class="">Locked room:</span>
+                                <input type="checkbox" name="lock" value="1" :checked="config.locked" @click="lockSave">
+                            </div>
+                            <small class="form-text text-muted">
+                                When the room is locked participants have to be approved by you
+                                before they could join the meeting.
+                            </small>
+                        </form>
+                    </div>
+                    <div class="modal-footer">
+                        <button type="button" class="btn btn-secondary modal-action" data-dismiss="modal">Close</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+    export default {
+        props: {
+            config: { type: Object, default: () => null },
+            room: { type: String, default: () => null }
+        },
+        data() {
+            return {
+            }
+        },
+        mounted() {
+            $('#security-options-dialog').on('show.bs.modal', e => {
+                $(e.target).find('.input-group-activable.active').removeClass('active')
+            })
+        },
+        methods: {
+            configSave(name, value, callback) {
+                const post = {}
+                post[name] = value
+
+                axios.post('/api/v4/openvidu/rooms/' + this.room + '/config', post)
+                    .then(response => {
+                        this.config[name] = value
+                        if (callback) {
+                            callback(response.data)
+                        }
+                        this.$emit('config-update', this.config)
+                        this.$toast.success(response.data.message)
+                    })
+            },
+            lockSave(e) {
+                this.configSave('locked', $(e.target).prop('checked') ? 1 : 0)
+            },
+            passwordClear() {
+                this.configSave('password', '')
+            },
+            passwordSave() {
+                this.configSave('password', $('#password-input input').val(), () => {
+                        $('#password-input').removeClass('active')
+                })
+            },
+            passwordSet() {
+                $('#password-input').addClass('active').find('input')
+                    .off('keydown.pass')
+                    .on('keydown.pass', e => {
+                        if (e.which == 13) {
+                            // On ENTER save the password
+                            this.passwordSave()
+                            e.preventDefault()
+                        } else if (e.which == 27) {
+                            // On ESC escape from the input, but not the dialog
+                            $('#password-input').removeClass('active')
+                            e.stopPropagation()
+                        }
+                    })
+                    .focus()
+            }
+        }
+    }
+</script>
diff --git a/src/routes/api.php b/src/routes/api.php
index ae56d9b7..86421a64 100644
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -1,149 +1,150 @@
 <?php
 
 use Illuminate\Http\Request;
 
 /*
 |--------------------------------------------------------------------------
 | API Routes
 |--------------------------------------------------------------------------
 |
 | Here is where you can register API routes for your application. These
 | routes are loaded by the RouteServiceProvider within a group which
 | is assigned the "api" middleware group. Enjoy building your API!
 |
 */
 
 $prefix = \trim(\parse_url(\config('app.url'), PHP_URL_PATH), '/') . '/';
 
 Route::group(
     [
         'middleware' => 'api',
         'prefix' => $prefix . 'api/auth'
     ],
     function ($router) {
         Route::post('login', 'API\AuthController@login');
 
         Route::group(
             ['middleware' => 'auth:api'],
             function ($router) {
                 Route::get('info', 'API\AuthController@info');
                 Route::post('logout', 'API\AuthController@logout');
                 Route::post('refresh', 'API\AuthController@refresh');
             }
         );
     }
 );
 
 Route::group(
     [
         'domain' => \config('app.domain'),
         'middleware' => 'api',
         'prefix' => $prefix . 'api/auth'
     ],
     function ($router) {
         Route::post('password-reset/init', 'API\PasswordResetController@init');
         Route::post('password-reset/verify', 'API\PasswordResetController@verify');
         Route::post('password-reset', 'API\PasswordResetController@reset');
 
         Route::get('signup/plans', 'API\SignupController@plans');
         Route::post('signup/init', 'API\SignupController@init');
         Route::post('signup/verify', 'API\SignupController@verify');
         Route::post('signup', 'API\SignupController@signup');
     }
 );
 
 Route::group(
     [
         'domain' => \config('app.domain'),
         'middleware' => 'auth:api',
         'prefix' => $prefix . 'api/v4'
     ],
     function () {
         Route::apiResource('domains', API\V4\DomainsController::class);
         Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm');
         Route::get('domains/{id}/status', 'API\V4\DomainsController@status');
 
         Route::apiResource('entitlements', API\V4\EntitlementsController::class);
         Route::apiResource('packages', API\V4\PackagesController::class);
         Route::apiResource('skus', API\V4\SkusController::class);
         Route::apiResource('users', API\V4\UsersController::class);
         Route::get('users/{id}/skus', 'API\V4\SkusController@userSkus');
         Route::get('users/{id}/status', 'API\V4\UsersController@status');
 
         Route::apiResource('wallets', API\V4\WalletsController::class);
         Route::get('wallets/{id}/transactions', 'API\V4\WalletsController@transactions');
         Route::get('wallets/{id}/receipts', 'API\V4\WalletsController@receipts');
         Route::get('wallets/{id}/receipts/{receipt}', 'API\V4\WalletsController@receiptDownload');
 
         Route::post('payments', 'API\V4\PaymentsController@store');
         Route::get('payments/mandate', 'API\V4\PaymentsController@mandate');
         Route::post('payments/mandate', 'API\V4\PaymentsController@mandateCreate');
         Route::put('payments/mandate', 'API\V4\PaymentsController@mandateUpdate');
         Route::delete('payments/mandate', 'API\V4\PaymentsController@mandateDelete');
 
         Route::get('openvidu/rooms', 'API\V4\OpenViduController@index');
         Route::post('openvidu/rooms/{id}/close', 'API\V4\OpenViduController@closeRoom');
+        Route::post('openvidu/rooms/{id}/config', 'API\V4\OpenViduController@setRoomConfig');
     }
 );
 
 // Note: In Laravel 7.x we could just use withoutMiddleware() instead of a separate group
 Route::group(
     [
         'domain' => \config('app.domain'),
         'prefix' => $prefix . 'api/v4'
     ],
     function () {
-        Route::get('openvidu/rooms/{id}', 'API\V4\OpenViduController@joinRoom');
+        Route::post('openvidu/rooms/{id}', 'API\V4\OpenViduController@joinRoom');
     }
 );
 
 Route::group(
     [
         'domain' => \config('app.domain'),
         'middleware' => 'api',
         'prefix' => $prefix . 'api/v4'
     ],
     function ($router) {
         Route::post('support/request', 'API\V4\SupportController@request');
     }
 );
 
 Route::group(
     [
         'domain' => \config('app.domain'),
         'prefix' => $prefix . 'api/webhooks',
     ],
     function () {
         Route::post('payment/{provider}', 'API\V4\PaymentsController@webhook');
         Route::post('meet/openvidu', 'API\V4\OpenViduController@webhook');
     }
 );
 
 Route::group(
     [
         'domain' => 'admin.' . \config('app.domain'),
         'middleware' => ['auth:api', 'admin'],
         'prefix' => $prefix . 'api/v4',
     ],
     function () {
         Route::apiResource('domains', API\V4\Admin\DomainsController::class);
         Route::get('domains/{id}/confirm', 'API\V4\Admin\DomainsController@confirm');
         Route::post('domains/{id}/suspend', 'API\V4\Admin\DomainsController@suspend');
         Route::post('domains/{id}/unsuspend', 'API\V4\Admin\DomainsController@unsuspend');
 
         Route::apiResource('entitlements', API\V4\Admin\EntitlementsController::class);
         Route::apiResource('packages', API\V4\Admin\PackagesController::class);
         Route::apiResource('skus', API\V4\Admin\SkusController::class);
         Route::apiResource('users', API\V4\Admin\UsersController::class);
         Route::post('users/{id}/reset2FA', 'API\V4\Admin\UsersController@reset2FA');
         Route::get('users/{id}/skus', 'API\V4\Admin\SkusController@userSkus');
         Route::post('users/{id}/suspend', 'API\V4\Admin\UsersController@suspend');
         Route::post('users/{id}/unsuspend', 'API\V4\Admin\UsersController@unsuspend');
         Route::apiResource('wallets', API\V4\Admin\WalletsController::class);
         Route::post('wallets/{id}/one-off', 'API\V4\Admin\WalletsController@oneOff');
         Route::get('wallets/{id}/transactions', 'API\V4\Admin\WalletsController@transactions');
         Route::apiResource('discounts', API\V4\Admin\DiscountsController::class);
 
         Route::get('stats/chart/{chart}', 'API\V4\Admin\StatsController@chart');
     }
 );
diff --git a/src/routes/websocket.php b/src/routes/websocket.php
index 64240f04..3fd6d85b 100644
--- a/src/routes/websocket.php
+++ b/src/routes/websocket.php
@@ -1,41 +1,37 @@
 <?php
 
 use Illuminate\Http\Request;
 use SwooleTW\Http\Websocket\Facades\Websocket;
 
 /*
 |--------------------------------------------------------------------------
 | Websocket Routes
 |--------------------------------------------------------------------------
 |
 | Here is where you can register websocket events for your application.
 |
 */
 
 Websocket::on(
     'connect',
     function ($websocket, Request $request) {
-        \Log::debug("someone connected");
-        $websocket->emit(
-            'message',
-            'welcome'
-        );
+        return;
     }
 );
 
 Websocket::on(
     'open',
     function ($websocket, Request $request) {
-        \Log::debug("socket opened");
+        return;
     }
 );
 
 Websocket::on(
     'disconnect',
     function ($websocket) {
-        \Log::debug("someone disconnected");
+        return;
     }
 );
 
-Websocket::on('message', 'App\Http\Controllers\WebsocketController@message');
-Websocket::on('ping', 'App\Http\Controllers\WebsocketController@ping');
+//Websocket::on('message', 'App\Http\Controllers\WebsocketController@message');
+//Websocket::on('ping', 'App\Http\Controllers\WebsocketController@ping');
diff --git a/src/tests/Browser/Meet/RoomControlsTest.php b/src/tests/Browser/Meet/RoomControlsTest.php
index 7be87eeb..67f1f0c0 100644
--- a/src/tests/Browser/Meet/RoomControlsTest.php
+++ b/src/tests/Browser/Meet/RoomControlsTest.php
@@ -1,341 +1,342 @@
 <?php
 
 namespace Tests\Browser\Meet;
 
 use App\OpenVidu\Room;
 use Tests\Browser;
 use Tests\Browser\Pages\Meet\Room as RoomPage;
 use Tests\TestCaseDusk;
 
 class RoomControlsTest extends TestCaseDusk
 {
     /**
      * {@inheritDoc}
      */
     public function setUp(): void
     {
         parent::setUp();
         $this->clearBetaEntitlements();
     }
 
     public function tearDown(): void
     {
         $this->clearBetaEntitlements();
         parent::tearDown();
     }
 
     /**
      * Test fullscreen buttons
      *
      * @group openvidu
      */
     public function testFullscreen(): void
     {
         // TODO: This test does not work in headless mode
         $this->markTestIncomplete();
 
         // Make sure there's no session yet
         $room = Room::where('name', 'john')->first();
         if ($room->session_id) {
             $room->session_id = null;
             $room->save();
         }
 
         $this->assignBetaEntitlement('john@kolab.org', 'meet');
 
         $this->browse(function (Browser $browser) {
             // Join the room as an owner (authenticate)
             $browser->visit(new RoomPage('john'))
                 ->click('@setup-button')
                 ->assertMissing('@toolbar')
                 ->assertMissing('@menu')
                 ->assertMissing('@session')
                 ->assertMissing('@chat')
                 ->assertMissing('@setup-form')
                 ->assertVisible('@login-form')
                 ->submitLogon('john@kolab.org', 'simple123')
                 ->waitFor('@setup-form')
                 ->assertMissing('@login-form')
                 ->waitUntilMissing('@setup-status-message.loading')
                 ->click('@setup-button')
                 ->waitFor('@session')
 
                 // Test fullscreen for the whole room
                 ->click('@menu button.link-fullscreen.closed')
                 ->assertVisible('@toolbar')
                 ->assertVisible('@session')
                 ->assertMissing('nav')
                 ->assertMissing('@menu button.link-fullscreen.closed')
                 ->click('@menu button.link-fullscreen.open')
                 ->assertVisible('nav')
 
                 // Test fullscreen for the participant video
                 ->click('@session button.link-fullscreen.closed')
                 ->assertVisible('@session')
                 ->assertMissing('@toolbar')
                 ->assertMissing('nav')
                 ->assertMissing('@session button.link-fullscreen.closed')
                 ->click('@session button.link-fullscreen.open')
                 ->assertVisible('nav')
                 ->assertVisible('@toolbar');
         });
     }
 
     /**
      * Test nickname and muting audio/video
      *
      * @group openvidu
      */
     public function testNicknameAndMuting(): void
     {
         // Make sure there's no session yet
         $room = Room::where('name', 'john')->first();
         if ($room->session_id) {
             $room->session_id = null;
             $room->save();
         }
 
         $this->assignBetaEntitlement('john@kolab.org', 'meet');
 
         $this->browse(function (Browser $owner, Browser $guest) {
             // Join the room as an owner (authenticate)
             $owner->visit(new RoomPage('john'))
                 ->click('@setup-button')
                 ->submitLogon('john@kolab.org', 'simple123')
                 ->waitFor('@setup-form')
                 ->waitUntilMissing('@setup-status-message.loading')
                 ->type('@setup-nickname-input', 'john')
                 ->click('@setup-button')
                 ->waitFor('@session');
 
             // In another browser act as a guest
             $guest->visit(new RoomPage('john'))
                 ->waitFor('@setup-form')
                 ->waitUntilMissing('@setup-status-message.loading')
                 ->assertMissing('@setup-status-message')
                 ->assertSeeIn('@setup-button', "JOIN")
                 // Join the room, disable cam/mic
                 ->select('@setup-mic-select', '')
                 ->select('@setup-cam-select', '')
                 ->click('@setup-button')
                 ->waitFor('@session');
 
             // Assert current UI state
             $owner->assertToolbar([
                     'audio' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
                     'video' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
-                    'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+                    'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED,
                     'chat' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED,
                     'fullscreen' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
+                    'security' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
                     'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
                 ])
                 ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
                     $browser->assertVisible('video')
                         ->assertAudioMuted('video', true)
                         ->assertSeeIn('.nickname', 'john')
                         ->assertMissing('.nickname button')
                         ->assertVisible('.controls button.link-fullscreen')
                         ->assertMissing('.controls button.link-audio')
                         ->assertMissing('.status .status-audio')
                         ->assertMissing('.status .status-video');
                 })
                 ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
                     $browser->assertMissing('video')
                         ->assertMissing('.nickname')
                         ->assertVisible('.controls button.link-fullscreen')
                         ->assertVisible('.controls button.link-audio')
                         ->assertVisible('.status .status-audio')
                         ->assertVisible('.status .status-video');
                 })
                 ->assertElementsCount('@session div.meet-video', 2);
 
             // Assert current UI state
             $guest->assertToolbar([
                     'audio' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
                     'video' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
-                    'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED,
+                    'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED,
                     'chat' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED,
                     'fullscreen' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
                     'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED,
                 ])
                 ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
                     $browser->assertVisible('video')
                         //->assertAudioMuted('video', true)
                         ->assertVisible('.nickname button')
                         ->assertMissing('.nickname span')
                         ->assertVisible('.controls button.link-fullscreen')
                         ->assertMissing('.controls button.link-audio')
                         ->assertVisible('.status .status-audio')
                         ->assertVisible('.status .status-video');
                 })
                 ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
                     $browser->assertVisible('video')
                         ->assertSeeIn('.nickname', 'john')
                         ->assertMissing('.nickname button')
                         ->assertVisible('.controls button.link-fullscreen')
                         ->assertVisible('.controls button.link-audio')
                         ->assertMissing('.status .status-audio')
                         ->assertMissing('.status .status-video');
                 })
                 ->assertElementsCount('@session div.meet-video', 2);
 
             // Test nickname change propagation
 
             // Use script() because type() does not work with this contenteditable widget
             $guest->setNickname('div.meet-video.publisher', 'guest');
             $owner->waitFor('div.meet-video:not(.publisher) .nickname')
                 ->assertSeeIn('div.meet-video:not(.publisher) .nickname', 'guest');
 
             // Test muting audio
             $owner->click('@menu button.link-audio')
                 ->assertToolbarButtonState('audio', RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED)
                 ->assertVisible('div.meet-video.publisher .status .status-audio');
 
             // FIXME: It looks that we can't just check the <video> element state
             //        We might consider using OpenVidu API to make sure
             $guest->waitFor('div.meet-video:not(.publisher) .status .status-audio');
 
             // Test unmuting audio
             $owner->click('@menu button.link-audio')
                 ->assertToolbarButtonState('audio', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED)
                 ->assertMissing('div.meet-video.publisher .status .status-audio');
 
             $guest->waitUntilMissing('div.meet-video:not(.publisher) .status .status-audio');
 
             // Test muting video
             $owner->click('@menu button.link-video')
                 ->assertToolbarButtonState('video', RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED)
                 ->assertVisible('div.meet-video.publisher .status .status-video');
 
             // FIXME: It looks that we can't just check the <video> element state
             //        We might consider using OpenVidu API to make sure
             $guest->waitFor('div.meet-video:not(.publisher) .status .status-video');
 
             // Test unmuting video
             $owner->click('@menu button.link-video')
                 ->assertToolbarButtonState('video', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED)
                 ->assertMissing('div.meet-video.publisher .status .status-video');
 
             $guest->waitUntilMissing('div.meet-video:not(.publisher) .status .status-video');
 
             // Test muting other user
             $guest->with('div.meet-video:not(.publisher)', function (Browser $browser) {
                 $browser->click('.controls button.link-audio')
                     ->assertAudioMuted('video', true)
                     ->assertVisible('.controls button.link-audio.text-danger')
                     ->click('.controls button.link-audio')
                     ->assertAudioMuted('video', false)
                     ->assertVisible('.controls button.link-audio:not(.text-danger)');
             });
         });
     }
 
     /**
      * Test text chat
      *
      * @group openvidu
      * @depends testNicknameAndMuting
      */
     public function testChat(): void
     {
         $this->assignBetaEntitlement('john@kolab.org', 'meet');
 
         $this->browse(function (Browser $owner, Browser $guest) {
             // Join the room as an owner
             $owner->visit(new RoomPage('john'))
                 ->waitFor('@setup-form')
                 ->waitUntilMissing('@setup-status-message.loading')
                 ->type('@setup-nickname-input', 'john')
                 ->click('@setup-button')
                 ->waitFor('@session');
 
             // In another browser act as a guest
             $guest->visit(new RoomPage('john'))
                 ->waitFor('@setup-form')
                 ->waitUntilMissing('@setup-status-message.loading')
                 ->assertMissing('@setup-status-message')
                 ->assertSeeIn('@setup-button', "JOIN")
                 // Join the room, disable cam/mic
                 ->select('@setup-mic-select', '')
                 ->select('@setup-cam-select', '')
                 ->click('@setup-button')
                 ->waitFor('@session');
 
             // Test chat elements
 
             $owner->click('@menu button.link-chat')
                 ->assertToolbarButtonState('chat', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED)
                 ->assertVisible('@chat')
                 ->assertVisible('@session')
                 ->assertFocused('@chat-input')
                 ->assertElementsCount('@chat-list .message', 0)
                 ->keys('@chat-input', 'test1', '{enter}')
                 ->assertValue('@chat-input', '')
                 ->assertElementsCount('@chat-list .message', 1)
                 ->assertSeeIn('@chat-list .message .nickname', 'john')
                 ->assertSeeIn('@chat-list .message div:last-child', 'test1');
 
             $guest->waitFor('@menu button.link-chat .badge')
                 ->assertSeeIn('@menu button.link-chat .badge', '1')
                 ->click('@menu button.link-chat')
                 ->assertToolbarButtonState('chat', RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED)
                 ->assertMissing('@menu button.link-chat .badge')
                 ->assertVisible('@chat')
                 ->assertVisible('@session')
                 ->assertElementsCount('@chat-list .message', 1)
                 ->assertSeeIn('@chat-list .message .nickname', 'john')
                 ->assertSeeIn('@chat-list .message div:last-child', 'test1');
 
             // Test the number of (hidden) incoming messages
             $guest->click('@menu button.link-chat')
                 ->assertMissing('@chat');
 
             $owner->keys('@chat-input', 'test2', '{enter}', 'test3', '{enter}')
                 ->assertElementsCount('@chat-list .message', 1)
                 ->assertSeeIn('@chat-list .message .nickname', 'john')
                 ->assertElementsCount('@chat-list .message div', 4)
                 ->assertSeeIn('@chat-list .message div:last-child', 'test3');
 
             $guest->waitFor('@menu button.link-chat .badge')
                 ->assertSeeIn('@menu button.link-chat .badge', '2')
                 ->click('@menu button.link-chat')
                 ->assertElementsCount('@chat-list .message', 1)
                 ->assertSeeIn('@chat-list .message .nickname', 'john')
                 ->assertSeeIn('@chat-list .message div:last-child', 'test3')
                 ->keys('@chat-input', 'guest1', '{enter}')
                 ->assertElementsCount('@chat-list .message', 2)
                 ->assertMissing('@chat-list .message:last-child .nickname')
                 ->assertSeeIn('@chat-list .message:last-child div:last-child', 'guest1');
 
             $owner->assertElementsCount('@chat-list .message', 2)
                 ->assertMissing('@chat-list .message:last-child .nickname')
                 ->assertSeeIn('@chat-list .message:last-child div:last-child', 'guest1');
 
             // Test nickname change is propagated to chat messages
 
             $guest->setNickname('div.meet-video.publisher', 'guest')
                 ->keys('@chat-input', 'guest2', '{enter}')
                 ->assertElementsCount('@chat-list .message', 2)
                 ->assertSeeIn('@chat-list .message:last-child .nickname', 'guest')
                 ->assertSeeIn('@chat-list .message:last-child div:last-child', 'guest2');
 
             $owner->assertElementsCount('@chat-list .message', 2)
                 ->assertSeeIn('@chat-list .message:last-child .nickname', 'guest')
                 ->assertSeeIn('@chat-list .message:last-child div:last-child', 'guest2');
 
             // TODO: Test text chat features, e.g. link handling
         });
     }
 
     /**
      * Test screen sharing
      *
      * @group openvidu
      */
     public function testShareScreen(): void
     {
         // It looks that screen sharing API is not available in headless chrome
         // Note that other tests already assert that the button is disabled
         $this->markTestIncomplete();
     }
 }
diff --git a/src/tests/Browser/Meet/RoomSecurityTest.php b/src/tests/Browser/Meet/RoomSecurityTest.php
new file mode 100644
index 00000000..9974c1dc
--- /dev/null
+++ b/src/tests/Browser/Meet/RoomSecurityTest.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Tests\Browser\Meet;
+
+use App\OpenVidu\Room;
+use Tests\Browser;
+use Tests\Browser\Components\Dialog;
+use Tests\Browser\Components\Toast;
+use Tests\Browser\Pages\Meet\Room as RoomPage;
+use Tests\TestCaseDusk;
+
+class RoomSecurityTest extends TestCaseDusk
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function setUp(): void
+    {
+        parent::setUp();
+
+        $this->clearBetaEntitlements();
+        $this->assignBetaEntitlement('john@kolab.org', 'meet');
+
+        $room = Room::where('name', 'john')->first();
+        $room->setSettings(['password' => null, 'locked' => null]);
+    }
+
+    public function tearDown(): void
+    {
+        $this->clearBetaEntitlements();
+        $room = Room::where('name', 'john')->first();
+        $room->setSettings(['password' => null, 'locked' => null]);
+
+        parent::tearDown();
+    }
+
+    /**
+     * Test password protected room
+     *
+     * @group openvidu
+     */
+    public function testRoomPassword(): void
+    {
+        $this->browse(function (Browser $owner, Browser $guest) {
+            // Make sure there's no session yet
+            $room = Room::where('name', 'john')->first();
+            if ($room->session_id) {
+                $room->session_id = null;
+                $room->save();
+            }
+
+            // Join the room as an owner (authenticate)
+            $owner->visit(new RoomPage('john'))
+                ->click('@setup-button')
+                ->submitLogon('john@kolab.org', 'simple123')
+                ->waitFor('@setup-form')
+                ->waitUntilMissing('@setup-status-message.loading')
+                ->assertMissing('@setup-password-input')
+                ->click('@setup-button')
+                ->waitFor('@session')
+                // Enter Security option dialog
+                ->click('@menu button.link-security')
+                ->with(new Dialog('#security-options-dialog'), function (Browser $browser) use ($room) {
+                    $browser->assertSeeIn('@title', 'Security options')
+                        ->assertSeeIn('@button-action', 'Close')
+                        ->assertElementsCount('.modal-footer button', 1)
+                        ->assertSeeIn('#password-input .label', 'Password:')
+                        ->assertSeeIn('#password-input-text.text-muted', 'none')
+                        ->assertVisible('#password-input + small')
+                        ->assertSeeIn('#password-set-btn', 'Set password')
+                        ->assertElementsCount('#password-input button', 1)
+                        ->assertMissing('#password-input input')
+                        // Test setting a password
+                        ->click('#password-set-btn')
+                        ->assertMissing('#password-input-text')
+                        ->assertVisible('#password-input input')
+                        ->assertValue('#password-input input', '')
+                        ->assertSeeIn('#password-input #password-save-btn', 'Save')
+                        ->assertElementsCount('#password-input button', 1)
+                        ->type('#password-input input', 'pass')
+                        ->click('#password-input #password-save-btn')
+                        ->assertToast(Toast::TYPE_SUCCESS, 'Room configuration updated successfully.')
+                        ->assertMissing('#password-input input')
+                        ->assertSeeIn('#password-input-text:not(.text-muted)', 'pass')
+                        ->assertSeeIn('#password-clear-btn.btn-outline-danger', 'Clear password')
+                        ->assertElementsCount('#password-input button', 1)
+                        ->click('@button-action');
+
+                    $this->assertSame('pass', $room->fresh()->getSetting('password'));
+                });
+
+            // In another browser act as a guest, expect password required
+            $guest->visit(new RoomPage('john'))
+                ->waitFor('@setup-form')
+                ->waitUntilMissing('@setup-status-message.loading')
+                ->assertSeeIn('@setup-status-message.text-danger', "Please, provide a valid password.")
+                ->assertVisible('@setup-form .input-group:nth-child(4) svg')
+                ->assertAttribute('@setup-form .input-group:nth-child(4) .input-group-text', 'title', 'Password')
+                ->assertAttribute('@setup-password-input', 'placeholder', 'Password')
+                ->assertValue('@setup-password-input', '')
+                ->assertSeeIn('@setup-button', "JOIN")
+                // Try to join w/o password
+                ->click('@setup-button')
+                ->waitFor('#setup-password.is-invalid')
+                // Try to join with a valid password
+                ->type('#setup-password', 'pass')
+                ->click('@setup-button')
+                ->waitFor('@session');
+
+            // Test removing the password
+            $owner->click('@menu button.link-security')
+                ->with(new Dialog('#security-options-dialog'), function (Browser $browser) use ($room) {
+                    $browser->assertSeeIn('@title', 'Security options')
+                        ->assertSeeIn('#password-input-text:not(.text-muted)', 'pass')
+                        ->assertSeeIn('#password-clear-btn.btn-outline-danger', 'Clear password')
+                        ->assertElementsCount('#password-input button', 1)
+                        ->click('#password-clear-btn')
+                        ->assertToast(Toast::TYPE_SUCCESS, 'Room configuration updated successfully.')
+                        ->assertMissing('#password-input input')
+                        ->assertSeeIn('#password-input-text.text-muted', 'none')
+                        ->assertSeeIn('#password-set-btn', 'Set password')
+                        ->assertElementsCount('#password-input button', 1)
+                        ->click('@button-action');
+
+                    $this->assertSame(null, $room->fresh()->getSetting('password'));
+                });
+        });
+    }
+}
diff --git a/src/tests/Browser/Meet/RoomSetupTest.php b/src/tests/Browser/Meet/RoomSetupTest.php
index 3b968710..3864d666 100644
--- a/src/tests/Browser/Meet/RoomSetupTest.php
+++ b/src/tests/Browser/Meet/RoomSetupTest.php
@@ -1,276 +1,281 @@
 <?php
 
 namespace Tests\Browser\Meet;
 
 use App\OpenVidu\Room;
 use Tests\Browser;
 use Tests\Browser\Components\Dialog;
 use Tests\Browser\Components\Menu;
 use Tests\Browser\Pages\Meet\Room as RoomPage;
 use Tests\TestCaseDusk;
 
 class RoomSetupTest extends TestCaseDusk
 {
     /**
      * {@inheritDoc}
      */
     public function setUp(): void
     {
         parent::setUp();
         $this->clearBetaEntitlements();
     }
 
     public function tearDown(): void
     {
         $this->clearBetaEntitlements();
         parent::tearDown();
     }
 
     /**
      * Test non-existing room
      *
      * @group openvidu
      */
     public function testRoomNonExistingRoom(): void
     {
         $this->browse(function (Browser $browser) {
             $browser->visit(new RoomPage('unknown'))
                 ->within(new Menu(), function ($browser) {
                     $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
                 });
 
             if ($browser->isDesktop()) {
                 $browser->within(new Menu('footer'), function ($browser) {
                     $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'login']);
                 });
             } else {
                 $browser->assertMissing('#footer-menu .navbar-nav');
             }
 
             $browser->assertMissing('@toolbar')
                 ->assertMissing('@menu')
                 ->assertMissing('@session')
                 ->assertMissing('@chat')
                 ->assertMissing('@login-form')
                 ->assertVisible('@setup-form')
                 ->assertSeeIn('@setup-status-message', "The room does not exist.")
                 ->assertMissing('@setup-button');
         });
     }
 
     /**
      * Test the room setup page
      *
      * @group openvidu
      */
     public function testRoomSetup(): void
     {
         // Make sure there's no session yet
         $room = Room::where('name', 'john')->first();
         if ($room->session_id) {
             $room->session_id = null;
             $room->save();
         }
 
         $this->assignBetaEntitlement('john@kolab.org', 'meet');
 
         $this->browse(function (Browser $browser) {
             $browser->visit(new RoomPage('john'))
                 ->within(new Menu(), function ($browser) {
                     $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
                 });
 
             if ($browser->isDesktop()) {
                 $browser->within(new Menu('footer'), function ($browser) {
                     $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'login']);
                 });
             } else {
                 $browser->assertMissing('#footer-menu .navbar-nav');
             }
 
             // Note: I've found out that if I have another Chrome instance running
             //       that uses media, here the media devices will not be available
 
             // TODO: Test enabling/disabling cam/mic in the setup widget
 
             $browser->assertMissing('@toolbar')
                 ->assertMissing('@menu')
                 ->assertMissing('@session')
                 ->assertMissing('@chat')
                 ->assertMissing('@login-form')
                 ->assertVisible('@setup-form')
                 ->assertSeeIn('@setup-title', 'Set up your session')
                 ->assertVisible('@setup-video')
-                ->assertSeeIn('@setup-form .form-group:nth-child(1) label', 'Microphone')
+                ->assertVisible('@setup-form .input-group:nth-child(1) svg')
+                ->assertAttribute('@setup-form .input-group:nth-child(1) .input-group-text', 'title', 'Microphone')
                 ->assertVisible('@setup-mic-select')
-                ->assertSeeIn('@setup-form .form-group:nth-child(2) label', 'Camera')
+                ->assertVisible('@setup-form .input-group:nth-child(2) svg')
+                ->assertAttribute('@setup-form .input-group:nth-child(2) .input-group-text', 'title', 'Camera')
                 ->assertVisible('@setup-cam-select')
-                ->assertSeeIn('@setup-form .form-group:nth-child(3) label', 'Nickname')
+                ->assertVisible('@setup-form .input-group:nth-child(3) svg')
+                ->assertAttribute('@setup-form .input-group:nth-child(3) .input-group-text', 'title', 'Nickname')
                 ->assertValue('@setup-nickname-input', '')
+                ->assertAttribute('@setup-nickname-input', 'placeholder', 'Your name')
+                ->assertMissing('@setup-password-input')
                 ->assertSeeIn(
                     '@setup-status-message',
                     "The room is closed. Please, wait for the owner to start the session."
                 )
                 ->assertSeeIn('@setup-button', "I'm the owner");
         });
     }
 
     /**
      * Test two users in a room (joining/leaving and some basic functionality)
      *
      * @group openvidu
      * @depends testRoomSetup
      */
     public function testTwoUsersInARoom(): void
     {
         $this->assignBetaEntitlement('john@kolab.org', 'meet');
 
         $this->browse(function (Browser $browser, Browser $guest) {
             // In one browser window act as a guest
             $guest->visit(new RoomPage('john'))
                 ->assertMissing('@toolbar')
                 ->assertMissing('@menu')
                 ->assertMissing('@session')
                 ->assertMissing('@chat')
                 ->assertMissing('@login-form')
                 ->waitFor('@setup-form')
                 ->waitUntilMissing('@setup-status-message.loading')
                 ->assertSeeIn(
                     '@setup-status-message',
                     "The room is closed. Please, wait for the owner to start the session."
                 )
                 ->assertSeeIn('@setup-button', "I'm the owner");
 
             // In another window join the room as the owner (authenticate)
             $browser->on(new RoomPage('john'))
                 ->assertSeeIn('@setup-button', "I'm the owner")
                 ->click('@setup-button')
                 ->assertMissing('@toolbar')
                 ->assertMissing('@menu')
                 ->assertMissing('@session')
                 ->assertMissing('@chat')
                 ->assertMissing('@setup-form')
                 ->assertVisible('@login-form')
                 ->submitLogon('john@kolab.org', 'simple123')
                 ->waitFor('@setup-form')
                 ->assertMissing('@login-form')
                 ->waitUntilMissing('@setup-status-message.loading')
                 ->assertSeeIn('@setup-status-message', "The room is closed. It will be open for others after you join.")
                 ->assertSeeIn('@setup-button', "JOIN")
                 ->type('@setup-nickname-input', 'john')
                 // Join the room
                 ->click('@setup-button')
                 ->waitFor('@session')
                 ->assertMissing('@setup-form')
                 ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
                     $browser->assertVisible('video')
                         ->assertSeeIn('.nickname', 'john')
                         ->assertVisible('.controls button.link-fullscreen')
                         ->assertMissing('.controls button.link-audio')
                         ->assertMissing('.status .status-audio')
                         ->assertMissing('.status .status-video');
                 })
                 ->within(new Menu(), function ($browser) {
-                    $browser->assertMenuItems(['explore', 'blog', 'support', 'logout']);
+                    $browser->assertMenuItems(['explore', 'blog', 'support', 'dashboard', 'logout']);
                 });
 
             if ($browser->isDesktop()) {
                 $browser->within(new Menu('footer'), function ($browser) {
-                    $browser->assertMenuItems(['explore', 'blog', 'support', 'tos', 'logout']);
+                    $browser->assertMenuItems(['explore', 'blog', 'support', 'tos', 'dashboard', 'logout']);
                 });
             }
 
             // After the owner "opened the room" guest should be able to join
             $guest->waitUntilMissing('@setup-status-message', 10)
                 ->assertSeeIn('@setup-button', "JOIN")
                 // Join the room, disable cam/mic
                 ->select('@setup-mic-select', '')
                 ->select('@setup-cam-select', '')
                 ->click('@setup-button')
                 ->waitFor('@session')
                 ->assertMissing('@setup-form')
                 ->whenAvailable('div.meet-video.publisher', function (Browser $browser) {
                     $browser->assertVisible('video')
                         ->assertVisible('.nickname button')
                         ->assertMissing('.nickname span')
                         ->assertVisible('.controls button.link-fullscreen')
                         ->assertMissing('.controls button.link-audio')
                         ->assertVisible('.status .status-audio')
                         ->assertVisible('.status .status-video');
                 })
                 ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
                     $browser->assertVisible('video')
                         ->assertSeeIn('.nickname', 'john')
                         ->assertVisible('.controls button.link-fullscreen')
                         ->assertVisible('.controls button.link-audio')
                         ->assertMissing('.status .status-audio')
                         ->assertMissing('.status .status-video');
                 })
                 ->assertElementsCount('@session div.meet-video', 2)
                 ->within(new Menu(), function ($browser) {
                     $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'login']);
                 });
 
             if ($guest->isDesktop()) {
                 $guest->within(new Menu('footer'), function ($browser) {
                     $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'tos', 'login']);
                 });
             }
 
             // Check guest's elements in the owner's window
             $browser->waitFor('@session div.meet-video:nth-child(2)')
                 ->assertElementsCount('@session div.meet-video', 2)
                 ->whenAvailable('div.meet-video:not(.publisher)', function (Browser $browser) {
                     $browser->assertMissing('video')
                         ->assertMissing('.nickname')
                         ->assertVisible('.controls button.link-fullscreen')
                         ->assertVisible('.controls button.link-audio')
                         ->assertVisible('.status .status-audio')
                         ->assertVisible('.status .status-video');
                 });
 
             // Test leaving the room
 
             // Guest is leaving
             $guest->click('@menu button.link-logout')
                 ->waitForLocation('/login');
 
             // Expect the participant removed from other users windows
             $browser->waitUntilMissing('@session div.meet-video:nth-child(2)');
 
             // Join the room as guest again
             $guest->visit(new RoomPage('john'))
                 ->assertMissing('@toolbar')
                 ->assertMissing('@menu')
                 ->assertMissing('@session')
                 ->assertMissing('@chat')
                 ->assertMissing('@login-form')
                 ->waitFor('@setup-form')
                 ->waitUntilMissing('@setup-status-message.loading')
                 ->assertMissing('@setup-status-message')
                 ->assertSeeIn('@setup-button', "JOIN")
                 // Join the room, disable cam/mic
                 ->select('@setup-mic-select', '')
                 ->select('@setup-cam-select', '')
                 ->click('@setup-button')
                 ->waitFor('@session');
 
             // Leave the room as the room owner
             // TODO: Test leaving the room by closing the browser window,
             //       it should not destroy the session
             $browser->click('@menu button.link-logout')
                 ->waitForLocation('/dashboard');
 
             // Expect other participants be informed about the end of the session
             $guest->with(new Dialog('#leave-dialog'), function (Browser $browser) {
                     $browser->assertSeeIn('@title', 'Room closed')
                         ->assertSeeIn('@body', "The session has been closed by the room owner.")
                         ->assertMissing('@button-cancel')
                         ->assertSeeIn('@button-action', 'Close')
                         ->click('@button-action');
             })
                 ->assertMissing('#leave-dialog')
                 ->waitForLocation('/login');
         });
     }
 }
diff --git a/src/tests/Browser/Pages/Meet/Room.php b/src/tests/Browser/Pages/Meet/Room.php
index 46ee3132..3b58b031 100644
--- a/src/tests/Browser/Pages/Meet/Room.php
+++ b/src/tests/Browser/Pages/Meet/Room.php
@@ -1,198 +1,199 @@
 <?php
 
 namespace Tests\Browser\Pages\Meet;
 
 use Laravel\Dusk\Page;
 use PHPUnit\Framework\Assert;
 
 class Room extends Page
 {
     public const BUTTON_ACTIVE = 1;
     public const BUTTON_ENABLED = 2;
     public const BUTTON_INACTIVE = 4;
     public const BUTTON_DISABLED = 8;
 
     protected $roomName;
 
     /**
      * Object constructor.
      *
      * @param string $name Room name
      */
     public function __construct($name)
     {
         $this->roomName = $name;
     }
 
     /**
      * Get the URL for the page.
      *
      * @return string
      */
     public function url()
     {
         return '/meet/' . $this->roomName;
     }
 
     /**
      * Assert that the browser is on the page.
      *
      * @param \Laravel\Dusk\Browser $browser The browser object
      *
      * @return void
      */
     public function assert($browser)
     {
         $browser->waitForLocation($this->url())
             ->waitUntilMissing('.app-loader')
             ->waitUntilMissing('#meet-setup div.status-message.loading');
     }
 
     /**
      * Get the element shortcuts for the page.
      *
      * @return array
      */
     public function elements()
     {
         return [
             '@app' => '#app',
 
             '@setup-form' => '#meet-setup form',
             '@setup-title' => '#meet-setup .card-title',
             '@setup-mic-select' => '#setup-microphone',
             '@setup-cam-select' => '#setup-camera',
             '@setup-nickname-input' => '#setup-nickname',
+            '@setup-password-input' => '#setup-password',
             '@setup-preview' => '#setup-preview',
             '@setup-volume' => '#setup-preview .volume',
             '@setup-video' => '#setup-preview video',
             '@setup-status-message' => '#meet-setup div.status-message',
             '@setup-button' => '#meet-setup form button',
 
             '@toolbar' => '#meet-session-toolbar',
 
             '@menu' => '#meet-session-menu',
 
             '@session' => '#meet-session',
 
             '@chat' => '#meet-chat',
             '@chat-input' => '#meet-chat textarea',
             '@chat-list' => '#meet-chat .chat',
 
             '@login-form' => '#meet-auth',
             '@login-email-input' => '#inputEmail',
             '@login-password-input' => '#inputPassword',
             '@login-second-factor-input' => '#secondfactor',
             '@login-button' => '#meet-auth button',
         ];
     }
 
     /**
      * Assert menu state.
      *
      * @param \Tests\Browser $browser The browser object
      * @param array          $menu    Menu items/state
      */
     public function assertToolbar($browser, array $menu): void
     {
         $browser->assertElementsCount('@menu button', count($menu));
 
         foreach ($menu as $item => $state) {
             $this->assertToolbarButtonState($browser, $item, $state);
         }
     }
 
     /**
      * Assert menu button state.
      *
      * @param \Tests\Browser $browser The browser object
      * @param string         $button  Button name
      * @param int            $state   Expected button state (sum of BUTTON_* consts)
      */
     public function assertToolbarButtonState($browser, $button, $state): void
     {
         $class = '';
 
         if ($state & self::BUTTON_ACTIVE) {
             $class .= ':not(.text-danger)';
         }
 
         if ($state & self::BUTTON_INACTIVE) {
             $class .= '.text-danger';
         }
 
         if ($state & self::BUTTON_DISABLED) {
             $class .= '[disabled]';
         }
 
         if ($state & self::BUTTON_ENABLED) {
             $class .= ':not([disabled])';
         }
 
         $browser->assertVisible('@menu button.link-' . $button . $class);
     }
 
     /**
      * Assert the <video> element's 'muted' property state
      *
      * @param \Tests\Browser $browser  The browser object
      * @param string         $selector Video element selector
      * @param bool           $state    Expected state
      */
     public function assertAudioMuted($browser, $selector, $state): void
     {
         $selector = addslashes($browser->resolver->format($selector));
 
         $result = $browser->script(
             "var video = document.querySelector('$selector'); return video.muted"
         );
 
         Assert::assertSame((bool) $result[0], $state);
     }
 
     /**
      * Set the nickname for the participant
      *
      * @param \Tests\Browser $browser  The browser object
      * @param string         $selector Participant element selector
      * @param string         $nickname Nickname
      */
     public function setNickname($browser, $selector, $nickname): void
     {
         // Use script() because type() does not work with this contenteditable widget
         $selector = $selector . ' .nickname span';
         $browser->script(
             "var element = document.querySelector('$selector');"
             . "element.focus();"
             . "element.innerText = '$nickname';"
             . "element.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 27 }))"
         );
     }
 
     /**
      * Submit logon form.
      *
      * @param \Tests\Browser $browser  The browser object
      * @param string         $username User name
      * @param string         $password User password
      * @param array          $config   Client-site config
      */
     public function submitLogon($browser, $username, $password, $config = []): void
     {
         $browser->type('@login-email-input', $username)
             ->type('@login-password-input', $password);
 
         if ($username == 'ned@kolab.org') {
             $code = \App\Auth\SecondFactor::code('ned@kolab.org');
             $browser->type('@login-second-factor-input', $code);
         }
 
         if (!empty($config)) {
             $browser->script(
                 sprintf('Object.assign(window.config, %s)', \json_encode($config))
             );
         }
 
         $browser->click('@login-button');
     }
 }
diff --git a/src/tests/Feature/Controller/OpenViduTest.php b/src/tests/Feature/Controller/OpenViduTest.php
index db52b28e..fc87cfde 100644
--- a/src/tests/Feature/Controller/OpenViduTest.php
+++ b/src/tests/Feature/Controller/OpenViduTest.php
@@ -1,202 +1,317 @@
 <?php
 
 namespace Tests\Feature\Controller;
 
 use App\Http\Controllers\API\V4\OpenViduController;
 use App\OpenVidu\Room;
 use Tests\TestCase;
 
 class OpenViduTest extends TestCase
 {
     /**
      * {@inheritDoc}
      */
     public function setUp(): void
     {
         parent::setUp();
+
         $this->clearBetaEntitlements();
+        $room = Room::where('name', 'john')->first();
+        $room->setSettings(['password' => null, 'locked' => null]);
     }
 
     public function tearDown(): void
     {
         $this->clearBetaEntitlements();
+        $room = Room::where('name', 'john')->first();
+        $room->setSettings(['password' => null, 'locked' => null]);
+
         parent::tearDown();
     }
 
     /**
      * Test listing user rooms
      *
      * @group openvidu
      */
     public function testIndex(): void
     {
         $john = $this->getTestUser('john@kolab.org');
         $jack = $this->getTestUser('jack@kolab.org');
         Room::where('user_id', $jack->id)->delete();
 
         // Unauth access not allowed
         $response = $this->get("api/v4/openvidu/rooms");
         $response->assertStatus(401);
 
         // John has one room
         $response = $this->actingAs($john)->get("api/v4/openvidu/rooms");
         $response->assertStatus(200);
 
         $json = $response->json();
 
         $this->assertSame(1, $json['count']);
         $this->assertCount(1, $json['list']);
         $this->assertSame('john', $json['list'][0]['name']);
 
         // Jack has no room, but it will be auto-created
         $response = $this->actingAs($jack)->get("api/v4/openvidu/rooms");
         $response->assertStatus(200);
 
         $json = $response->json();
 
         $this->assertSame(1, $json['count']);
         $this->assertCount(1, $json['list']);
         $this->assertRegExp('/^[0-9a-z-]{11}$/', $json['list'][0]['name']);
     }
 
     /**
      * Test joining the room
      *
      * @group openvidu
      */
     public function testJoinRoom(): void
     {
         $john = $this->getTestUser('john@kolab.org');
         $jack = $this->getTestUser('jack@kolab.org');
         $room = Room::where('name', 'john')->first();
         $room->session_id = null;
         $room->save();
 
         $this->assignBetaEntitlement($john, 'meet');
 
         // Unauth access, no session yet
-        $response = $this->get("api/v4/openvidu/rooms/{$room->name}");
+        $response = $this->post("api/v4/openvidu/rooms/{$room->name}");
         $response->assertStatus(423);
 
         // Non-existing room name
-        $response = $this->actingAs($john)->get("api/v4/openvidu/rooms/non-existing");
+        $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing");
         $response->assertStatus(404);
 
         // Non-owner, no session yet
-        $response = $this->actingAs($jack)->get("api/v4/openvidu/rooms/{$room->name}");
+        $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}");
         $response->assertStatus(423);
 
         // Room owner, no session yet
-        $response = $this->actingAs($john)->get("api/v4/openvidu/rooms/{$room->name}");
+        $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}");
         $response->assertStatus(424);
 
-        $response = $this->actingAs($john)->get("api/v4/openvidu/rooms/{$room->name}?init=1");
+        $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]);
         $response->assertStatus(200);
 
         $json = $response->json();
 
         $session_id = $room->fresh()->session_id;
 
         $this->assertSame('PUBLISHER', $json['role']);
         $this->assertSame($session_id, $json['session']);
         $this->assertTrue(is_string($session_id) && !empty($session_id));
         $this->assertTrue(strpos($json['token'], 'wss://') === 0);
         $this->assertTrue(!array_key_exists('shareToken', $json));
 
         $john_token = $json['token'];
 
         // Non-owner, now the session exists
-        $response = $this->actingAs($jack)->get("api/v4/openvidu/rooms/{$room->name}");
+        $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}");
         $response->assertStatus(200);
 
         $json = $response->json();
 
         $this->assertSame('PUBLISHER', $json['role']);
         $this->assertSame($session_id, $json['session']);
         $this->assertTrue(strpos($json['token'], 'wss://') === 0);
         $this->assertTrue($json['token'] != $john_token);
         $this->assertTrue(!array_key_exists('shareToken', $json));
+        $this->assertEmpty($json['config']['password']);
+        $this->assertEmpty($json['config']['requires_password']);
+
+        // Non-owner, password protected room, password not provided
+        $room->setSettings(['password' => 'pass']);
+        $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}");
+        $response->assertStatus(425);
+
+        $json = $response->json();
+
+        $this->assertCount(3, $json);
+        $this->assertSame('error', $json['status']);
+        $this->assertSame('Failed to join the session. Invalid password.', $json['message']);
+        $this->assertEmpty($json['config']['password']);
+        $this->assertTrue($json['config']['requires_password']);
+
+        // Non-owner, password protected room, invalid provided
+        $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['password' => 'aa']);
+        $response->assertStatus(425);
+
+        // Non-owner, password protected room, valid password provided
+        $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['password' => 'pass']);
+        $response->assertStatus(200);
+
+        $json = $response->json();
+
+        $this->assertSame($session_id, $json['session']);
+
+        // Make sure the room owner can access the password protected room w/o password
+        $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}");
+        $response->assertStatus(200);
 
         // TODO: Test accessing an existing room of deleted owner
     }
 
     /**
      * Test joining the room
      *
      * @group openvidu
      * @depends testJoinRoom
      */
     public function testJoinRoomGuest(): void
     {
         $this->assignBetaEntitlement('john@kolab.org', 'meet');
 
         // There's no asy way to logout the user in the same test after
         // using actingAs(). That's why this is moved to a separate test
         $room = Room::where('name', 'john')->first();
 
         // Guest, request with screenShare token
-        $response = $this->get("api/v4/openvidu/rooms/{$room->name}?screenShare=1");
+        $response = $this->post("api/v4/openvidu/rooms/{$room->name}", ['screenShare' => 1]);
         $response->assertStatus(200);
 
         $json = $response->json();
 
         $this->assertSame('PUBLISHER', $json['role']);
         $this->assertSame($room->session_id, $json['session']);
         $this->assertTrue(strpos($json['token'], 'wss://') === 0);
         $this->assertTrue(strpos($json['shareToken'], 'wss://') === 0);
         $this->assertTrue($json['shareToken'] != $json['token']);
     }
 
     /**
      * Test closing the room (session)
      *
      * @group openvidu
      * @depends testJoinRoom
      */
     public function testCloseRoom(): void
     {
         $john = $this->getTestUser('john@kolab.org');
         $jack = $this->getTestUser('jack@kolab.org');
         $room = Room::where('name', 'john')->first();
 
         // Unauth access not allowed
         $response = $this->post("api/v4/openvidu/rooms/{$room->name}/close", []);
         $response->assertStatus(401);
 
         // Non-existing room name
         $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing/close", []);
         $response->assertStatus(404);
 
         // Non-owner
         $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/close", []);
         $response->assertStatus(403);
 
         // Room owner
         $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/close", []);
         $response->assertStatus(200);
 
         $json = $response->json();
 
         $this->assertNull($room->fresh()->session_id);
         $this->assertSame('success', $json['status']);
         $this->assertSame("The session has been closed successfully.", $json['message']);
         $this->assertCount(2, $json);
 
         // TODO: Test if the session is removed from the OpenVidu server too
 
         // Test error handling when it's not possible to delete the session on
         // the OpenVidu server (use fake session_id)
         $room->session_id = 'aaa';
         $room->save();
 
         $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/close", []);
         $response->assertStatus(500);
 
         $json = $response->json();
 
         $this->assertSame('aaa', $room->fresh()->session_id);
         $this->assertSame('error', $json['status']);
         $this->assertSame("Failed to close the session.", $json['message']);
         $this->assertCount(2, $json);
     }
+
+    /**
+     * Test configuring the room (session)
+     *
+     * @group openvidu
+     */
+    public function testSetRoomConfig(): void
+    {
+        $john = $this->getTestUser('john@kolab.org');
+        $jack = $this->getTestUser('jack@kolab.org');
+        $room = Room::where('name', 'john')->first();
+
+        // Unauth access not allowed
+        $response = $this->post("api/v4/openvidu/rooms/{$room->name}/config", []);
+        $response->assertStatus(401);
+
+        // Non-existing room name
+        $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing/config", []);
+        $response->assertStatus(404);
+
+        // TODO: Test a room with a deleted owner
+
+        // Non-owner
+        $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/config", []);
+        $response->assertStatus(403);
+
+        // Room owner
+        $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/config", []);
+        $response->assertStatus(200);
+
+        $json = $response->json();
+
+        $this->assertCount(2, $json);
+        $this->assertSame('success', $json['status']);
+        $this->assertSame("Room configuration updated successfully.", $json['message']);
+
+        // Set password and room lock
+        $post = ['password' => 'aaa', 'locked' => 1];
+        $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/config", $post);
+        $response->assertStatus(200);
+
+        $json = $response->json();
+
+        $this->assertCount(2, $json);
+        $this->assertSame('success', $json['status']);
+        $this->assertSame("Room configuration updated successfully.", $json['message']);
+        $room->refresh();
+        $this->assertSame('aaa', $room->getSetting('password'));
+        $this->assertSame('true', $room->getSetting('locked'));
+
+        // Unset password and room lock
+        $post = ['password' => '', 'locked' => 0];
+        $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/config", $post);
+        $response->assertStatus(200);
+
+        $json = $response->json();
+
+        $this->assertCount(2, $json);
+        $this->assertSame('success', $json['status']);
+        $this->assertSame("Room configuration updated successfully.", $json['message']);
+        $room->refresh();
+        $this->assertSame(null, $room->getSetting('password'));
+        $this->assertSame(null, $room->getSetting('locked'));
+
+        // Test invalid option error
+        $post = ['password' => 'eee', 'unknown' => 0];
+        $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}/config", $post);
+        $response->assertStatus(422);
+
+        $json = $response->json();
+
+        $this->assertCount(2, $json);
+        $this->assertSame('error', $json['status']);
+        $this->assertSame("Invalid room configuration option.", $json['errors']['unknown']);
+
+        $room->refresh();
+        $this->assertSame(null, $room->getSetting('password'));
+    }
 }