diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py
index 97b2937..ceb5227 100644
--- a/IPython/frontend/html/notebook/handlers.py
+++ b/IPython/frontend/html/notebook/handlers.py
@@ -46,6 +46,22 @@ def not_if_readonly(f, self, *args, **kwargs):
     else:
         return f(self, *args, **kwargs)
 
+@decorator
+def authenticate_unless_readonly(f, self, *args, **kwargs):
+    """authenticate this page *unless* readonly view is active.
+    
+    In read-only mode, the notebook list and print view should
+    be accessible without authentication.
+    """
+    
+    @web.authenticated
+    def auth_f(self, *args, **kwargs):
+        return f(self, *args, **kwargs)
+    if self.application.ipython_app.read_only:
+        return f(self, *args, **kwargs)
+    else:
+        return auth_f(self, *args, **kwargs)
+
 #-----------------------------------------------------------------------------
 # Top-level handlers
 #-----------------------------------------------------------------------------
@@ -68,7 +84,7 @@ class AuthenticatedHandler(web.RequestHandler):
 
 class ProjectDashboardHandler(AuthenticatedHandler):
 
-    @web.authenticated
+    @authenticate_unless_readonly
     def get(self):
         nbm = self.application.notebook_manager
         project = nbm.notebook_dir
@@ -81,7 +97,7 @@ class ProjectDashboardHandler(AuthenticatedHandler):
 class LoginHandler(AuthenticatedHandler):
 
     def get(self):
-        self.render('login.html', next='/')
+        self.render('login.html', next=self.get_argument('next', default='/'))
 
     def post(self):
         pwd = self.get_argument('password', default=u'')
@@ -93,7 +109,6 @@ class LoginHandler(AuthenticatedHandler):
 
 class NewHandler(AuthenticatedHandler):
 
-    @not_if_readonly
     @web.authenticated
     def get(self):
         nbm = self.application.notebook_manager
@@ -109,7 +124,7 @@ class NewHandler(AuthenticatedHandler):
 
 class NamedNotebookHandler(AuthenticatedHandler):
 
-    @web.authenticated
+    @authenticate_unless_readonly
     def get(self, notebook_id):
         nbm = self.application.notebook_manager
         project = nbm.notebook_dir
@@ -130,13 +145,11 @@ class NamedNotebookHandler(AuthenticatedHandler):
 
 class MainKernelHandler(AuthenticatedHandler):
 
-    @not_if_readonly
     @web.authenticated
     def get(self):
         km = self.application.kernel_manager
         self.finish(jsonapi.dumps(km.kernel_ids))
 
-    @not_if_readonly
     @web.authenticated
     def post(self):
         km = self.application.kernel_manager
@@ -152,7 +165,6 @@ class KernelHandler(AuthenticatedHandler):
 
     SUPPORTED_METHODS = ('DELETE')
 
-    @not_if_readonly
     @web.authenticated
     def delete(self, kernel_id):
         km = self.application.kernel_manager
@@ -163,7 +175,6 @@ class KernelHandler(AuthenticatedHandler):
 
 class KernelActionHandler(AuthenticatedHandler):
 
-    @not_if_readonly
     @web.authenticated
     def post(self, kernel_id, action):
         km = self.application.kernel_manager
@@ -242,7 +253,6 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
         except:
             logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
 
-    @not_if_readonly
     def on_first_message(self, msg):
         self._inject_cookie_message(msg)
         if self.get_current_user() is None:
@@ -380,13 +390,19 @@ class ShellHandler(AuthenticatedZMQStreamHandler):
 
 class NotebookRootHandler(AuthenticatedHandler):
 
-    @web.authenticated
+    @authenticate_unless_readonly
     def get(self):
+        
+        # communicate read-only via Allow header
+        if self.application.ipython_app.read_only and not self.get_current_user():
+            self.set_header('Allow', 'GET')
+        else:
+            self.set_header('Allow', ', '.join(self.SUPPORTED_METHODS))
+        
         nbm = self.application.notebook_manager
         files = nbm.list_notebooks()
         self.finish(jsonapi.dumps(files))
 
-    @not_if_readonly
     @web.authenticated
     def post(self):
         nbm = self.application.notebook_manager
@@ -405,11 +421,18 @@ class NotebookHandler(AuthenticatedHandler):
 
     SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
 
-    @web.authenticated
+    @authenticate_unless_readonly
     def get(self, notebook_id):
         nbm = self.application.notebook_manager
         format = self.get_argument('format', default='json')
         last_mod, name, data = nbm.get_notebook(notebook_id, format)
+        
+        # communicate read-only via Allow header
+        if self.application.ipython_app.read_only and not self.get_current_user():
+            self.set_header('Allow', 'GET')
+        else:
+            self.set_header('Allow', ', '.join(self.SUPPORTED_METHODS))
+        
         if format == u'json':
             self.set_header('Content-Type', 'application/json')
             self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
@@ -419,7 +442,6 @@ class NotebookHandler(AuthenticatedHandler):
         self.set_header('Last-Modified', last_mod)
         self.finish(data)
 
-    @not_if_readonly
     @web.authenticated
     def put(self, notebook_id):
         nbm = self.application.notebook_manager
@@ -429,7 +451,6 @@ class NotebookHandler(AuthenticatedHandler):
         self.set_status(204)
         self.finish()
 
-    @not_if_readonly
     @web.authenticated
     def delete(self, notebook_id):
         nbm = self.application.notebook_manager
diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py
index 27db5ad..466d165 100644
--- a/IPython/frontend/html/notebook/notebookapp.py
+++ b/IPython/frontend/html/notebook/notebookapp.py
@@ -118,13 +118,22 @@ flags['no-browser']=(
 )
 flags['read-only'] = (
     {'NotebookApp' : {'read_only' : True}},
-    "Launch the Notebook server in read-only mode, not allowing execution or editing"
+    """Allow read-only access to notebooks.
+    
+    When using a password to protect the notebook server, this flag
+    allows unauthenticated clients to view the notebook list, and
+    individual notebooks, but not edit them, start kernels, or run
+    code.
+    
+    This flag only makes sense in conjunction with setting a password,
+    via the ``NotebookApp.password`` configurable.
+    """
 )
 
 # the flags that are specific to the frontend
 # these must be scrubbed before being passed to the kernel,
 # or it will raise an error on unrecognized flags
-notebook_flags = ['no-browser']
+notebook_flags = ['no-browser', 'read-only']
 
 aliases = dict(ipkernel_aliases)
 
diff --git a/IPython/frontend/html/notebook/static/css/base.css b/IPython/frontend/html/notebook/static/css/base.css
index 8d2631c..ba113b3 100644
--- a/IPython/frontend/html/notebook/static/css/base.css
+++ b/IPython/frontend/html/notebook/static/css/base.css
@@ -51,3 +51,12 @@ div#main_app {
     padding: 0.2em 0.8em;
     font-size: 77%;
 }
+
+span#login_widget {
+    float: right;
+}
+
+/* generic class for hidden objects */
+.hidden {
+    display: none;
+}
\ No newline at end of file
diff --git a/IPython/frontend/html/notebook/static/js/cell.js b/IPython/frontend/html/notebook/static/js/cell.js
index a25d2e2..2d86c62 100644
--- a/IPython/frontend/html/notebook/static/js/cell.js
+++ b/IPython/frontend/html/notebook/static/js/cell.js
@@ -15,6 +15,10 @@ var IPython = (function (IPython) {
 
     var Cell = function (notebook) {
         this.notebook = notebook;
+        this.read_only = false;
+        if (notebook){
+            this.read_only = notebook.read_only;
+        }
         this.selected = false;
         this.element = null;
         this.create_element();
diff --git a/IPython/frontend/html/notebook/static/js/codecell.js b/IPython/frontend/html/notebook/static/js/codecell.js
index b557e29..044016e 100644
--- a/IPython/frontend/html/notebook/static/js/codecell.js
+++ b/IPython/frontend/html/notebook/static/js/codecell.js
@@ -37,6 +37,7 @@ var IPython = (function (IPython) {
             indentUnit : 4,
             mode: 'python',
             theme: 'ipython',
+            readOnly: this.read_only,
             onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this)
         });
         input.append(input_area);
diff --git a/IPython/frontend/html/notebook/static/js/notebook.js b/IPython/frontend/html/notebook/static/js/notebook.js
index e8c0f84..b55d7fb 100644
--- a/IPython/frontend/html/notebook/static/js/notebook.js
+++ b/IPython/frontend/html/notebook/static/js/notebook.js
@@ -14,6 +14,7 @@ var IPython = (function (IPython) {
     var utils = IPython.utils;
 
     var Notebook = function (selector) {
+        this.read_only = false;
         this.element = $(selector);
         this.element.scroll();
         this.element.data("notebook", this);
@@ -42,6 +43,7 @@ var IPython = (function (IPython) {
         var that = this;
         var end_space = $('<div class="end_space"></div>').height(150);
         end_space.dblclick(function (e) {
+            if (that.read_only) return;
             var ncells = that.ncells();
             that.insert_code_cell_below(ncells-1);
         });
@@ -54,6 +56,7 @@ var IPython = (function (IPython) {
         var that = this;
         $(document).keydown(function (event) {
             // console.log(event);
+            if (that.read_only) return;
             if (event.which === 38) {
                 var cell = that.selected_cell();
                 if (cell.at_top()) {
@@ -185,11 +188,11 @@ var IPython = (function (IPython) {
         });
 
         $(window).bind('beforeunload', function () {
-            var kill_kernel = $('#kill_kernel').prop('checked');                
+            var kill_kernel = $('#kill_kernel').prop('checked');
             if (kill_kernel) {
                 that.kernel.kill();
             }
-            if (that.dirty) {
+            if (that.dirty && ! that.read_only) {
                 return "You have unsaved changes that will be lost if you leave this page.";
             };
         });
@@ -975,14 +978,26 @@ var IPython = (function (IPython) {
 
 
     Notebook.prototype.notebook_loaded = function (data, status, xhr) {
+        var allowed = xhr.getResponseHeader('Allow');
+        if (allowed && allowed.indexOf('PUT') == -1){
+            this.read_only = true;
+            // unhide login button if it's relevant
+            $('span#login_widget').removeClass('hidden');
+        }else{
+            this.read_only = false;
+        }
         this.fromJSON(data);
         if (this.ncells() === 0) {
             this.insert_code_cell_below();
         };
         IPython.save_widget.status_save();
         IPython.save_widget.set_notebook_name(data.metadata.name);
-        this.start_kernel();
         this.dirty = false;
+        if (this.read_only) {
+            this.handle_read_only();
+        }else{
+            this.start_kernel();
+        }
         // fromJSON always selects the last cell inserted. We need to wait
         // until that is done before scrolling to the top.
         setTimeout(function () {
@@ -992,6 +1007,15 @@ var IPython = (function (IPython) {
     };
 
 
+    Notebook.prototype.handle_read_only = function(){
+        IPython.left_panel.collapse();
+        IPython.save_widget.element.find('button#save_notebook').addClass('hidden');
+        $('button#new_notebook').addClass('hidden');
+        $('div#cell_section').addClass('hidden');
+        $('div#kernel_section').addClass('hidden');
+    }
+
+
     IPython.Notebook = Notebook;
 
 
diff --git a/IPython/frontend/html/notebook/static/js/notebooklist.js b/IPython/frontend/html/notebook/static/js/notebooklist.js
index 5a3bf86..06156d8 100644
--- a/IPython/frontend/html/notebook/static/js/notebooklist.js
+++ b/IPython/frontend/html/notebook/static/js/notebooklist.js
@@ -73,6 +73,15 @@ var IPython = (function (IPython) {
 
 
     NotebookList.prototype.list_loaded = function (data, status, xhr) {
+        var allowed = xhr.getResponseHeader('Allow');
+        if (allowed && allowed.indexOf('PUT') == -1){
+            this.read_only = true;
+            $('#new_notebook').addClass('hidden');
+            // unhide login button if it's relevant
+            $('span#login_widget').removeClass('hidden');
+        }else{
+            this.read_only = false;
+        }
         var len = data.length;
         // Todo: remove old children
         for (var i=0; i<len; i++) {
@@ -80,7 +89,10 @@ var IPython = (function (IPython) {
             var nbname = data[i].name;
             var item = this.new_notebook_item(i);
             this.add_link(notebook_id, nbname, item);
-            this.add_delete_button(item);
+            if (!this.read_only){
+                // hide delete buttons when readonly
+                this.add_delete_button(item);
+            }
         };
     };
 
diff --git a/IPython/frontend/html/notebook/static/js/notebookmain.js b/IPython/frontend/html/notebook/static/js/notebookmain.js
index 095d098..4b884ed 100644
--- a/IPython/frontend/html/notebook/static/js/notebookmain.js
+++ b/IPython/frontend/html/notebook/static/js/notebookmain.js
@@ -33,6 +33,7 @@ $(document).ready(function () {
     IPython.left_panel = new IPython.LeftPanel('div#left_panel', 'div#left_panel_splitter');
     IPython.save_widget = new IPython.SaveWidget('span#save_widget');
     IPython.quick_help = new IPython.QuickHelp('span#quick_help_area');
+    IPython.login_widget = new IPython.LoginWidget('span#login_widget');
     IPython.print_widget = new IPython.PrintWidget('span#print_widget');
     IPython.notebook = new IPython.Notebook('div#notebook');
     IPython.kernel_status_widget = new IPython.KernelStatusWidget('#kernel_status');
diff --git a/IPython/frontend/html/notebook/static/js/projectdashboardmain.js b/IPython/frontend/html/notebook/static/js/projectdashboardmain.js
index 9ae6256..a9ceb72 100644
--- a/IPython/frontend/html/notebook/static/js/projectdashboardmain.js
+++ b/IPython/frontend/html/notebook/static/js/projectdashboardmain.js
@@ -28,6 +28,7 @@ $(document).ready(function () {
     $('div#right_panel').addClass('box-flex');
 
     IPython.notebook_list = new IPython.NotebookList('div#notebook_list');
+    IPython.login_widget = new IPython.LoginWidget('span#login_widget');
     IPython.notebook_list.load_list();
 
     // These have display: none in the css file and are made visible here to prevent FLOUC.
diff --git a/IPython/frontend/html/notebook/static/js/textcell.js b/IPython/frontend/html/notebook/static/js/textcell.js
index 4a24e53..817c0dc 100644
--- a/IPython/frontend/html/notebook/static/js/textcell.js
+++ b/IPython/frontend/html/notebook/static/js/textcell.js
@@ -33,7 +33,8 @@ var IPython = (function (IPython) {
             indentUnit : 4,
             mode: this.code_mirror_mode,
             theme: 'default',
-            value: this.placeholder
+            value: this.placeholder,
+            readOnly: this.read_only,
         });
         // The tabindex=-1 makes this div focusable.
         var render_area = $('<div/>').addClass('text_cell_render').
@@ -65,6 +66,7 @@ var IPython = (function (IPython) {
 
 
     TextCell.prototype.edit = function () {
+        if ( this.read_only ) return;
         if (this.rendered === true) {
             var text_cell = this.element;
             var output = text_cell.find("div.text_cell_render");  
diff --git a/IPython/frontend/html/notebook/templates/notebook.html b/IPython/frontend/html/notebook/templates/notebook.html
index 3039ad5..40e69bc 100644
--- a/IPython/frontend/html/notebook/templates/notebook.html
+++ b/IPython/frontend/html/notebook/templates/notebook.html
@@ -57,7 +57,10 @@
     </span>
     <span id="quick_help_area">
       <button id="quick_help">Quick<u>H</u>elp</button>
-      </span>
+    </span>
+    <span id="login_widget" class="hidden">
+      <button id="login">Login</button>
+    </span>
     <span id="kernel_status">Idle</span>
 </div>
 
@@ -278,6 +281,7 @@
 <script src="static/js/layout.js" type="text/javascript" charset="utf-8"></script>
 <script src="static/js/savewidget.js" type="text/javascript" charset="utf-8"></script>
 <script src="static/js/quickhelp.js" type="text/javascript" charset="utf-8"></script>
+<script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
 <script src="static/js/pager.js" type="text/javascript" charset="utf-8"></script>
 <script src="static/js/panelsection.js" type="text/javascript" charset="utf-8"></script>
 <script src="static/js/printwidget.js" type="text/javascript" charset="utf-8"></script>
diff --git a/IPython/frontend/html/notebook/templates/projectdashboard.html b/IPython/frontend/html/notebook/templates/projectdashboard.html
index 0b9075f..fb87e60 100644
--- a/IPython/frontend/html/notebook/templates/projectdashboard.html
+++ b/IPython/frontend/html/notebook/templates/projectdashboard.html
@@ -19,6 +19,9 @@
 
 <div id="header">
     <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
+    <span id="login_widget" class="hidden">
+      <button id="login">Login</button>
+    </span>
 </div>
 
 <div id="header_border"></div>
@@ -54,6 +57,7 @@
 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
 <script src="static/js/notebooklist.js" type="text/javascript" charset="utf-8"></script>
+<script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
 <script src="static/js/projectdashboardmain.js" type="text/javascript" charset="utf-8"></script>
 
 </body>