Ur/Web Tutorial

Ur/Web Tutorial

Step 7

Previous | Next

Obviously from the last step you are free to improve the look and feel of your blog system as you wish. However, we're going to move onto another important feature for any public-facing web application: authentication.

The general scheme for web authentication applies in Ur/Web as it would in almost any other language, except Ur/Web does a few things for us that makes life easier. Cookies are owned by a particular module such that we don't really have to worry too much about other modules misusing our cookies. Cryptographic signing of cookies takes place whenever they are sent to form handlers that might cause database side effects, to protect against cross-site forgery. Finally, because database tables are owned by a module, we can be sure that we're not going to accidentally leak sensitive data.

We're going to go ahead and create a new Auth module, with the relevant files being auth.ur and auth.urs.

auth.urs

Our signature file is going to define a couple of helpful externally-visible functions - one that will display a transaction page iff the user is authenticated, and another to permit logging in:

val displayIfAuthenticated : transaction page -> transaction page
val login : unit -> transaction page

auth.ur

We begin skeletally with our implementation. We'll make our display function just return the page no matter what, and we'll design a login form that goes to a nil handler:

fun displayIfAuthenticated page = page

fun loginHandler row = return <xml><head/><body/></xml>

fun login () = return
   <xml>
      <head><title>Login to UrBlog</title></head>
      <body>
         <form>
            <p>Username:<br/><textbox{#Username}/><br/>
               Password:<br/><password{#Password}/><br/>
               <submit value="Login" action={loginHandler}/>
            </p>
         </form>
      </body>
   </xml>

This will at least satisfy our signature, despite not being terribly interesting. Let's go ahead and add auth to our urblog.urp file so that our new module will be included:

urblog.urp

[...]

$/string
auth
crud
urblog

The order in which these modules appear is somewhat significant, but it shouldn't take a great deal of effort to figure out how dependencies are resolved in this way.

Authentication

The basic mechanism for our authentication is going to involve checking credentials against a user table, and in the case that the credentials match, setting a cookie that includes the username and password. Checking if a user is logged in will then just require re-checking the credentials in the cookie against those in the database.

Defining a cookie is pretty simple in Ur/Web. It involves using the cookie keyword and defining the kinds of data we would like to put into it:

cookie userSession : {Username : string, Password : string}

Contrite aside: We're being willfully irresponsible here, putting plaintext passwords into cookies (and indeed, storing passwords in plaintext in a database). In a production system you would want to go through the usual process of applying some kind of cryptographically strong hashing function, but for the moment we're not going to bother to keep things very simple. It is very important that you do something about this before attempting to run this code in a production setting

We might as well go ahead and add our user table now, too:

table user : { Id : int, Username : string, Password : string, Email : string }
   PRIMARY KEY Id

Naturally, you are free to add some other fields that are of interest to you. Let's go ahead, however, and implement our login handler based on this new table:

fun loginHandler row =
   re' <- oneOrNoRows1(SELECT user.Id
                        FROM user 
                       WHERE user.Username = {[row.Username]} 
                         AND user.Password = {[row.Password]});
   case re' of
      None => error <xml>Invalid Login</xml>
    | Some re => 
           setCookie userSession 
               {Value = row, 
                Expires = None,
                Secure = False};
         return <xml>
            <head><title>Logged in!</title></head>
            <body>
               <h1>Logged in</h1>
            </body>
         </xml>

Interestingly enough, we've actually just implemented displayIfAuthenticated as well! Well, almost. Our function requires a record with fields Username and Password. Our cookie has this property too! So let's go ahead and generalise this function:

fun ifAuthenticated page row =
   re' <- oneOrNoRows1(SELECT user.Id
                        FROM user 
                       WHERE user.Username = {[row.Username]} 
                         AND user.Password = {[row.Password]});
   case re' of
      None => error <xml>Invalid Login</xml>
    | Some re => 
           setCookie userSession 
               {Value = row, 
                Expires = None,
                Secure = False};
                page
         
fun loginHandler row = 
   ifAuthenticated (return <xml>
            <head><title>Logged in!</title></head>
            <body>
               <h1>Logged in</h1>
            </body>
         </xml>) row

fun displayIfAuthenticated page =
   c <- getCookie userSession;
   case c of
      None => return 
         <xml><head/>
            <body>
            <h1>Not logged in.</h1>
            <p><a link={login()}>Login</a></p>
            </body>
         </xml>
    | Some c' => ifAuthenticated page c'

So we've replaced our loginHandler function with a call to our new ifAuthenticated helper function, which is also able to be used to implement our displayIfAuthenticated function that retrieves the values from the cookie.

So the final thing to do is to wrap any content we want to be password-protected with displayIfAuthenticated calls. Here's one such example from crud.ur:

	[...]
    and admin () =
        ls <- editList ();
        Auth.displayIfAuthenticated (
        return <xml><head>
          <title>{cdata M.title}</title>
        </head><body>

          <h1>{cdata M.title}</h1>

          {ls}
        </body></xml>)

Testing

In order to test this, you'll need to add some test data to the uw_Auth_user table. After that, you should be able to visit the 'admin' page (http://localhost:8080/Urblog/admin) and attempt to login using these test credentials. Having logged in successfully, you should be able to revisit the admin link and obtain access.

Exercises

Exercise 7.1

Adding the other appropriate protections are left as an exercise to the reader. You might also like to add a link to Auth.login somewhere.

Exercise 7.2

Creating user accounts is not currently possible without manually editing the database. Can you figure out how to reuse the existing Crud module within the Auth module to implement functionality for adding, modifying and deleting users?

Exercise 7.3

Add a logout link to the Auth module.

Previous | Next