diff --git a/account/mkusercert.in b/account/mkusercert.in
index 0d7339f841e6b32267ff5493bced373fb09d26b7..d8f3ba3217aad282b580e0ba98d1580e85cb1d6e 100755
--- a/account/mkusercert.in
+++ b/account/mkusercert.in
@@ -1,6 +1,6 @@
 #!/usr/bin/perl -wT
 #
-# Copyright (c) 2000-2012 University of Utah and the Flux Group.
+# Copyright (c) 2000-2014 University of Utah and the Flux Group.
 # 
 # {{{EMULAB-LICENSE
 # 
@@ -619,6 +619,9 @@ if ($encrypted) {
     chmod(0600, $target)
 	or fatal("Could not chmod $target: $!");
 
+    goto skipssh
+	if ($target_user->IsNonLocal());
+
     #
     # Create an SSH key from the private key. Mostly for geni users,
     # who tend not to know how to do such things.
@@ -662,6 +665,7 @@ if ($encrypted) {
 	       "      -f $sshdir/encrypted.pub")
 	    == 0 or fatal("Could not add pubkey $sshdir/encrypted.pub");
     }
+  skipssh:
 }
 
 TBScriptUnlock();
diff --git a/db/User.pm.in b/db/User.pm.in
index a5b22e92f925c1b2ee9e79f4645041518a250d40..2f0ba33d4dd875e0a11018edf7400f04944c7612 100644
--- a/db/User.pm.in
+++ b/db/User.pm.in
@@ -38,7 +38,7 @@ use File::Basename;
 use overload ('""' => 'Stringify');
 use vars qw($NEWUSER_FLAGS_PROJLEADER $NEWUSER_FLAGS_WIKIONLY
 	    $NEWUSER_FLAGS_WEBONLY $NEWUSER_FLAGS_ARCHIVED
-	    $NEWUSER_FLAGS_NOUUID $NEWUSER_FLAGS_NONLOCAL $NEWUSER_FLAGS_VIAAPT
+	    $NEWUSER_FLAGS_NOUUID $NEWUSER_FLAGS_NONLOCAL
 	    $USERSTATUS_ACTIVE $USERSTATUS_FROZEN
 	    $USERSTATUS_UNAPPROVED $USERSTATUS_UNVERIFIED
 	    $USERSTATUS_NEWUSER $USERSTATUS_ARCHIVED $USERSTATUS_NONLOCAL
@@ -66,7 +66,6 @@ $NEWUSER_FLAGS_WEBONLY		= 0x04;
 $NEWUSER_FLAGS_ARCHIVED		= 0x08;
 $NEWUSER_FLAGS_NOUUID		= 0x80;
 $NEWUSER_FLAGS_NONLOCAL		= 0x40;
-$NEWUSER_FLAGS_VIAAPT		= 0x20;
 
 # Status values.
 $USERSTATUS_ACTIVE		= "active";
@@ -80,7 +79,7 @@ $USERSTATUS_NONLOCAL	        = "nonlocal";
 # Why, why, why?
 @EXPORT_OK = qw($NEWUSER_FLAGS_PROJLEADER $NEWUSER_FLAGS_WIKIONLY
 		$NEWUSER_FLAGS_WEBONLY $NEWUSER_FLAGS_ARCHIVED
-		$NEWUSER_FLAGS_NOUUID $NEWUSER_FLAGS_VIAAPT
+		$NEWUSER_FLAGS_NOUUID
 		$USERSTATUS_ACTIVE $USERSTATUS_FROZEN
 		$USERSTATUS_UNAPPROVED $USERSTATUS_UNVERIFIED
 		$USERSTATUS_NEWUSER $USERSTATUS_ARCHIVED $USERSTATUS_NONLOCAL);
@@ -110,7 +109,6 @@ sub Lookup($$)
 {
     my ($class, $token) = @_;
     my $status_archived = $USERSTATUS_ARCHIVED;
-    my $status_nonlocal = $USERSTATUS_NONLOCAL;
     my $query_result;
 
     # Look in cache first
@@ -133,8 +131,7 @@ sub Lookup($$)
 	$query_result =
 	    DBQueryWarn("select * from users ".
 			"where uid='$token' and ".
-			"      status!='$status_archived' and ".
-			"      status!='$status_nonlocal'");
+			"      status!='$status_archived'");
     }
     else {
 	return undef;
@@ -294,13 +291,11 @@ sub LookupByWikiName($$)
 {
     my ($class, $wikiname) = @_;
     my $status_archived = $USERSTATUS_ARCHIVED;
-    my $status_nonlocal = $USERSTATUS_NONLOCAL;
     
     my $query_result =
 	DBQueryFatal("select uid_idx from users ".
 		     "where wikiname='$wikiname' and ".
-		     "      status!='$status_archived' and ".
-		     "      status!='$status_nonlocal'");
+		     "      status!='$status_archived'");
 
     return undef
 	if (! $query_result || !$query_result->numrows);
@@ -318,13 +313,11 @@ sub LookupByEmail($$)
 {
     my ($class, $email) = @_;
     my $status_archived = $USERSTATUS_ARCHIVED;
-    my $status_nonlocal = $USERSTATUS_NONLOCAL;
 
     my $query_result =
 	DBQueryFatal("select uid_idx from users ".
 		     "where LCASE(usr_email)=LCASE('$email') and ".
-		     "      status!='$status_archived' and ".
-		     "      status!='$status_nonlocal'");
+		     "      status!='$status_archived'");
 		     
 
     return undef
@@ -342,14 +335,12 @@ sub LookupByUUID($$)
 {
     my ($class, $uuid) = @_;
     my $status_archived = $USERSTATUS_ARCHIVED;
-    my $status_nonlocal = $USERSTATUS_NONLOCAL;
     my $safe_uuid = DBQuoteSpecial($uuid);
 
     my $query_result =
 	DBQueryFatal("select uid_idx from users ".
 		     "where uid_uuid=$safe_uuid and ".
-		     "      status!='$status_archived' and ".
-		     "      status!='$status_nonlocal'");
+		     "      status!='$status_archived'");
 
     return undef
 	if (! $query_result || !$query_result->numrows);
@@ -365,13 +356,13 @@ sub LookupByUUID($$)
 sub LookupNonLocal($$)
 {
     my ($class, $urn) = @_;
-    my $status_nonlocal = $USERSTATUS_NONLOCAL;
+    my $status_archived = $USERSTATUS_ARCHIVED;
     my $safe_urn = DBQuoteSpecial($urn);
 
     my $query_result =
 	DBQueryFatal("select uid_idx from users ".
 		     "where nonlocal_id=$safe_urn and ".
-		     "      status='$status_nonlocal'");
+		     "      status!='$status_archived'");
 
     return undef
 	if (! $query_result || !$query_result->numrows);
@@ -395,7 +386,6 @@ sub Create($$$$)
     my $archived = ($flags & $NEWUSER_FLAGS_ARCHIVED   ? 1 : 0);
     my $nonlocal = ($flags & $NEWUSER_FLAGS_NONLOCAL   ? 1 : 0);
     my $nouuid   = ($flags & $NEWUSER_FLAGS_NOUUID     ? 1 : 0);
-    my $viaapt   = ($flags & $NEWUSER_FLAGS_VIAAPT     ? 1 : 0);
 
     #
     # If no uid, we need to generate a unique one for the user.
@@ -576,15 +566,10 @@ sub Create($$$$)
 		"Must provide nonlocal_id and nonlocal_type!\n";
 	    return undef;
 	}
-	push(@insert_data, "status='$USERSTATUS_NONLOCAL'");
+	push(@insert_data, "status='$USERSTATUS_ACTIVE'");
 	push(@insert_data, "pswd_expires=now()");
 	push(@insert_data, "usr_pswd='*'");
     }
-    elsif ($viaapt) {
-	push(@insert_data, "status='$USERSTATUS_UNAPPROVED'");
-	push(@insert_data, "pswd_expires=date_add(now(), interval 5 year)");
-	push(@insert_data, "viaAPT='1'");
-    }
     else {
 	push(@insert_data, "status='$USERSTATUS_NEWUSER'");
 	push(@insert_data, "pswd_expires=date_add(now(), interval 1 year)");
@@ -620,6 +605,8 @@ sub Delete($)
 
     my $uid_idx = $self->uid_idx();
 
+    DBQueryWarn("delete from user_credentials where uid_idx='$uid_idx'")
+	or return -1;
     DBQueryWarn("delete from user_pubkeys where uid_idx='$uid_idx'")
 	or return -1;
     DBQueryWarn("delete from user_sslcerts where uid_idx='$uid_idx'")
@@ -1861,6 +1848,15 @@ sub ValidUID($$)
 			  TBDB_CHECKDBSLOT_ERROR());
 }
 
+sub ValidEmail($$)
+{
+    my ($class, $email) = @_;
+
+    return TBcheck_dbslot($email, "users", "usr_email",
+			  TBDB_CHECKDBSLOT_WARN()|
+			  TBDB_CHECKDBSLOT_ERROR());
+}
+
 #
 # Default project. If not set in the users table, then look at the
 # project membership, and if only one project then use that. 
@@ -1977,6 +1973,48 @@ sub HomeDirOkay($;$)
     return 0;
 }
 
+#
+# Set/Get credential for a user. These are used by APT to store a speaksfor
+# credential for a nonlocal user, but might also use it later to 
+#
+sub StoreCredential($$$$)
+{
+    my ($self, $cred, $expires, $cert) = @_;
+
+    my $uid = $self->uid();
+    my $uid_idx = $self->uid_idx();
+    my $safe_credential = DBQuoteSpecial($cred);
+    my $safe_certificate = DBQuoteSpecial($cert);
+
+    return -1
+	if (!DBQueryWarn("replace into user_credentials set ".
+			 "  uid='$uid', uid_idx='$uid_idx',created=now(), ".
+			 "  expires='$expires', ".
+			 "  credential_string=$safe_credential, ".
+			 "  certificate_string=$safe_certificate"));
+
+    return 0;
+}
+
+sub GetStoredCredential($)
+{
+    my ($self) = @_;
+
+    my $uid = $self->uid();
+    my $uid_idx = $self->uid_idx();
+
+    my $query_result =
+	DBQueryWarn("select credential_string,certificate_string ".
+		    "  from user_credentials ".
+		    "where uid_idx='$uid_idx'");
+
+    return undef
+	if (!$query_result || !$query_result->numrows);
+    
+    my ($cred, $cert) = $query_result->fetchrow_array();
+    return ($cred, $cert);
+}
+
 # _Always_ make sure that this 1 is at the end of the file...
 1;
 
diff --git a/protogeni/scripts/GNUmakefile.in b/protogeni/scripts/GNUmakefile.in
index 892e6556b78df9a95bc220515835e0e6698d179d..b4aa28ddf26036cbb0f0a33bbfcaa6c73de8c52a 100644
--- a/protogeni/scripts/GNUmakefile.in
+++ b/protogeni/scripts/GNUmakefile.in
@@ -49,7 +49,8 @@ PSBIN_STUFF	= register_resources expire_daemon gencrl postcrl \
 		  updatecert fixcerts initcerts cacontrol webcacontrol \
 		  genextendcred rspeclint chstats listactive \
 		  maptoslice webmaptoslice setexpiration quickvm webquickvm \
-		  mondbd
+		  mondbd parsecert creategeniuser webcreategeniuser \
+		  updategeniuser webupdategeniuser
 
 ifeq ($(ISCLEARINGHOUSE),1)
 PSBIN_STUFF     += ch_daemon
@@ -76,6 +77,8 @@ install: apt-install \
 	$(addprefix $(INSTALL_SBINDIR)/protogeni/, $(PSBIN_STUFF)) \
 	$(INSTALL_LIBEXECDIR)/webquickvm \
 	$(INSTALL_LIBEXECDIR)/webcacontrol \
+	$(INSTALL_LIBEXECDIR)/webcreategeniuser \
+	$(INSTALL_LIBEXECDIR)/webupdategeniuser \
 	$(INSTALL_LIBEXECDIR)/webmaptoslice
 	-rm -f $(INSTALL_SBINDIR)/protogeni/cleanupticket
 
diff --git a/protogeni/scripts/creategeniuser.in b/protogeni/scripts/creategeniuser.in
new file mode 100644
index 0000000000000000000000000000000000000000..0f25d24d77fd8f9b14107430712af61e2f0ccbd9
--- /dev/null
+++ b/protogeni/scripts/creategeniuser.in
@@ -0,0 +1,224 @@
+#!/usr/bin/perl -w
+#
+# Copyright (c) 2008-2014 University of Utah and the Flux Group.
+# 
+# {{{GENIPUBLIC-LICENSE
+# 
+# GENI Public License
+# 
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and/or hardware specification (the "Work") to
+# deal in the Work without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Work, and to permit persons to whom the Work
+# is furnished to do so, subject to the following conditions:
+# 
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Work.
+# 
+# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS
+# IN THE WORK.
+# 
+# }}}
+#
+use strict;
+use English;
+use Getopt::Std;
+use Data::Dumper;
+
+#
+# Create a geni user. This is a nonlocal user, derived from the
+# only info we have which is the urn/email. We use this from CloudLab
+# web interface (geni-login) to create a stub local account for a
+# geni user. 
+#
+sub usage()
+{
+    print STDERR "Usage: $0 [-n] <urn> <email>\n";
+    exit(1);
+}
+my $optlist  = "nr";
+my $impotent = 0;
+my $delete   = 0;
+
+# Configure ...
+my $TB		  = "@prefix@";
+my $TBACCT	  = "$TB/sbin/tbacct";
+my $MKUSERCERT    = "$TB/sbin/mkusercert";
+my $MODGROUPS     = "$TB/sbin/modgroups";
+
+use lib '@prefix@/lib';
+use emutil;
+use User;
+use Project;
+use GeniCertificate;
+use GeniHRN;
+use EmulabConstants;
+
+# Protos.
+sub DeleteGeniUser($);
+
+sub fatal($)
+{
+    my ($msg) = @_;
+
+    die("*** $0:\n".
+	"    $msg\n");
+}
+
+#
+# Parse command arguments. Once we return from getopts, all that should be
+# left are the required arguments.
+#
+my %options = ();
+if (! getopts($optlist, \%options)) {
+    usage();
+}
+if (defined($options{"n"})) {
+    $impotent = 1;
+}
+if (defined($options{"r"})) {
+    usage()
+	if (@ARGV != 1);
+    $delete = 1;
+}
+else {
+    usage()
+	if (@ARGV != 2);
+}
+my $urn     = $ARGV[0];
+fatal("Invalid urn")
+    if (! GeniHRN::IsValid($urn));
+
+# Make sure we can get this project.
+my $project = Project->Lookup("CloudLab");
+if (!defined($project)) {
+    fatal("Cannot find the project.");
+}
+if ($delete) {
+    exit(DeleteGeniUser($urn));
+}
+
+my $email   = $ARGV[1];
+my $usr_uid;
+fatal("Invalid email")
+    if (! User->ValidEmail($email));
+
+# Must not be a user with same nonlocal ID.
+if (User->LookupNonLocal($urn)) {
+    fatal("We already have a user with that nonlocal ID (urn)");
+}
+
+#
+# Parse urn and email, maybe we can get a unique uid out of one.
+#
+my (undef,undef,$uid) = GeniHRN::Parse($urn);
+fatal("Could not parse urn")
+    if (!defined($uid));
+if (User->ValidUID($uid) && !User->Lookup($uid)) {
+    $usr_uid = $uid;
+}
+else {
+    #
+    # Split email and try that.
+    #
+    my ($token) = split("@", $email);
+    if (defined($token) &&
+	User->ValidUID($token) && !User->Lookup($token)) {
+	$usr_uid = $token;
+    }
+}
+#
+# Neither worked, so need to generate something. Ick.
+#
+if (!defined($usr_uid)) {
+    if (!User->ValidUID($uid)) {
+	# Random
+	$usr_uid = "g" . substr(lc(emutil::GenHash()), 0, 6);
+    }
+    else {
+	my $i;
+	$uid = substr($uid, 0, 7);
+	for ($i = 0 ; $i <= 9; $i++) {
+	    if (!User->Lookup("${uid}${i}")) {
+		$usr_uid = "${uid}${i}";
+		last;
+	    }
+	}
+	if ($i > 9) {
+	    $usr_uid = "g" . substr(lc(emutil::GenHash()), 0, 6);
+	}
+    }
+}
+if ($impotent) {
+    print "Would create nolocal user '$usr_uid' ...\n";
+    exit(0);
+}
+
+#
+# Okay, create new account in the DB.
+#
+my $user = User->Create($usr_uid,
+			$User::NEWUSER_FLAGS_NONLOCAL,
+			{"usr_name"      => "Geni User $usr_uid",
+			 "usr_email"     => $email,
+			 "nonlocal_id"   => $urn,
+			 "nonlocal_type" => "geni",
+			});
+fatal("Could not create user!")
+    if (!defined($user));
+
+#
+# Add them to the holding project. This will need more thought.
+#
+if ($project->AddMemberShip($user, $Group::MemberShip::TRUSTSTRING_LOCALROOT)) {
+    $user->Delete();
+    fatal("Could not add new user to project");
+}
+
+# And then instantiate the user.
+system("$TBACCT add $usr_uid");
+if ($?) {
+    $project->DeleteMemberShip($user);
+    $user->Delete();
+    fatal("Could not instantiate user account!")
+}
+
+# We need to generate the encrypted ssl certificate to keep
+# things happy.
+my $certpass = substr(lc(emutil::GenHash()), 0, 10);
+system("$MKUSERCERT -p $certpass $usr_uid");
+if ($?) {
+    $project->DeleteMemberShip($user);
+    $user->Delete();
+    fatal("Could not create local SSL certificate");
+}
+exit(0);
+
+#
+# Delete (purge!) geni user. Not to be used generally, please use
+# the normal archive path. This is for debugging.
+#
+sub DeleteGeniUser($)
+{
+    my ($urn) = @_;
+    my $user = User->LookupNonLocal($urn);
+    if (!defined($user)) {
+	fatal("No such local user!");
+    }
+    my $uid = $user->uid();
+    my $pid = $project->pid();
+    system("$MODGROUPS -r $pid:$pid $uid");
+
+    system("$TBACCT -f del $uid") == 0 or
+	fatal("$TBACCT $uid failed!");
+
+    $user->Delete();
+    return 0;
+}
diff --git a/protogeni/scripts/parsecert.in b/protogeni/scripts/parsecert.in
new file mode 100644
index 0000000000000000000000000000000000000000..29c362216552ea22ac172d628841a722963fab97
--- /dev/null
+++ b/protogeni/scripts/parsecert.in
@@ -0,0 +1,95 @@
+#!/usr/bin/perl -w
+#
+# Copyright (c) 2008-2014 University of Utah and the Flux Group.
+# 
+# {{{GENIPUBLIC-LICENSE
+# 
+# GENI Public License
+# 
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and/or hardware specification (the "Work") to
+# deal in the Work without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Work, and to permit persons to whom the Work
+# is furnished to do so, subject to the following conditions:
+# 
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Work.
+# 
+# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS
+# IN THE WORK.
+# 
+# }}}
+#
+use strict;
+use English;
+use Getopt::Std;
+use Data::Dumper;
+
+#
+# Parse a certificate and print out useful info. Used by the web interface
+# so that it does not need to duplicate code in GeniCertificate. 
+#
+sub usage()
+{
+    print STDERR "Usage: $0 [-a] <cert file> <outfile>\n";
+    exit(1);
+}
+my $optlist  = "a";
+my $showall  = 0;
+
+# Configure ...
+my $TB		  = "@prefix@";
+
+use lib '@prefix@/lib';
+use GeniCertificate;
+use GeniHRN;
+
+sub fatal($)
+{
+    my ($msg) = @_;
+
+    die("*** $0:\n".
+	"    $msg\n");
+}
+
+#
+# Parse command arguments. Once we return from getopts, all that should be
+# left are the required arguments.
+#
+my %options = ();
+if (! getopts($optlist, \%options)) {
+    usage();
+}
+if (defined($options{"a"})) {
+    $showall = 1;
+}
+usage()
+    if (@ARGV != 2);
+my $certfile = $ARGV[0];
+my $outfile  = $ARGV[1];
+
+my $certificate = GeniCertificate->LoadFromFile($certfile);
+if (!defined($certificate)) {
+    fatal("Could not parse certificate");
+}
+if (!open(OUT, ">$outfile")) {
+    fatal("Could not open file for output");
+}
+print OUT "<attributes>\n";
+print OUT "<attribute name='urn'>" . $certificate->urn();
+print OUT "</attribute>\n";
+print OUT "<attribute name='email'>" . $certificate->email();
+print OUT "</attribute>\n";
+print OUT "<attribute name='uuid'>" . $certificate->uuid();
+print OUT "</attribute>\n";
+print OUT "</attributes>\n";
+close(OUT);
+exit(0);
+
diff --git a/protogeni/scripts/quickvm.in b/protogeni/scripts/quickvm.in
index 6bc816cec38ab30a7d9c700129e3318715c0f3cb..6dd0b7a304f31294c6474b4902373eb86be3a268 100755
--- a/protogeni/scripts/quickvm.in
+++ b/protogeni/scripts/quickvm.in
@@ -40,27 +40,28 @@ use Cwd qw(realpath);
 # 
 sub usage()
 {
-    print "Usage: quickvm [-l] [-u uuid] [-a aggregate] <xmlfile>\n";
+    print "Usage: quickvm [-u uuid] [-a aggregate] <xmlfile>\n";
     print "Usage: quickvm -k <uuid>\n";
     print "Usage: quickvm -e <seconds> <uuid>\n";
     print "Usage: quickvm -s <uuid> <sliver_urn> <imagename>\n";
     exit(1);
 }
-my $optlist = "dkve:lu:a:st:f";
+my $optlist = "dkve:u:a:st:f";
 my $debug   = 0;
 my $verbose = 1;
 my $killit  = 0;
 my $utahddc = 1;
 my $DDCURN  = "urn:publicid:IDN+utahddc.geniracks.net+authority+cm";
-my $localuser = 0;
 my $xmlfile;
 my $extend;
 my $webtask;
 my $webtask_id;
 my $snapshot;
 my $foreground = 0;
+my $localuser  = 0;
 my $quickuuid;
 my $aggregate;
+my $this_user;
 
 # Protos
 sub fatal($);
@@ -68,6 +69,7 @@ sub UserError($);
 sub Terminate($);
 sub Extend($$);
 sub SnapShot($$$);
+sub GenCredentials($$$$);
 
 #
 # Configure variables
@@ -82,6 +84,7 @@ my $SACERT	  = "$TB/etc/genisa.pem";
 my $CMCERT	  = "$TB/etc/genicm.pem";
 my $SSHKEYGEN     = "/usr/bin/ssh-keygen";
 my $SSHSETUP      = "$TB/sbin/aptssh-setup";
+my $UPDATEGENIUSER= "$TB/sbin/protogeni/updategeniuser";
 my $VERSIONING    = @PROFILEVERSIONS@;
 
 # un-taint path
@@ -131,9 +134,6 @@ if (defined($options{"d"})) {
 if (defined($options{"v"})) {
     $verbose = 1;
 }
-if (defined($options{"l"})) {
-    $localuser = 1;
-}
 if (defined($options{"f"})) {
     $foreground = 1;
 }
@@ -178,15 +178,14 @@ else {
     # file in /tmp.
     #
     if (getpwuid($UID) ne "nobody") {
-	my $this_user = User->ThisUser();
+	$this_user = User->ThisUser();
 
 	if (! defined($this_user)) {
 	    fatal("You ($UID) do not exist!");
 	}
-	fatal("Only admins can run this script.")
-	    if (!$this_user->IsAdmin());
+	$localuser = 1;
     }
-    else {
+    if (!defined($this_user) || !$this_user->IsAdmin()) {
 	if ($xmlfile =~ /^([-\w\.\/]+)$/) {
 	    $xmlfile = $1;
 	}
@@ -443,10 +442,21 @@ if (!$localuser && $MAINSITE) {
     $speaker_signer = "/usr/testbed/etc/utah-apt.sa";
 }
 
-# Remember key. For now we accept only one key. We store it simply
-# so we can display it again for the user in the web interface.
-# We allow key reuse for existing users, see above.
-if (!$localuser && defined($sshkey)) {
+if ($localuser) {
+    my $emulab_user = $geniuser->emulab_user();
+    if ($emulab_user->IsNonLocal()) {
+	system("$UPDATEGENIUSER -s " . $emulab_user->uid());
+	if ($?) {
+	    fatal("Could not update ssh keys for nonlocal user");
+	}
+    }
+}
+elsif (!$localuser && defined($sshkey)) {
+    #
+    # Remember key. For now we accept only one key. We store it simply
+    # so we can display it again for the user in the web interface.
+    # We allow key reuse for existing users, see above.
+    #
     $geniuser->DeleteKeys();
     $geniuser->AddKey($sshkey);
 }
@@ -501,40 +511,17 @@ if ($slice->SetExpiration(time() + (($localuser ? 16 : 3) * 3600)) != 0) {
 }
 my $slice_uuid = $slice->uuid();
 
-# Create a slice credential
-my $slice_credential =
-    GeniCredential->Create($slice,
-			   $geniuser);
-if (!defined($slice_credential)) {
-    $slice->Delete();
-    fatal("Could not create credential for $slice_urn");
-}
-
 #
-# Need to set the credential expiration to match the slice expiration,
-# before we sign it.
+# Generate credentials we need.
 #
-$slice_credential->SetExpiration(time() + (($localuser ? 16 : 3) * 3600));
-
-# And sign it.
-if ($slice_credential->Sign($GeniCredential::LOCALSA_FLAG) != 0) {
-    $slice_credential->Delete();
+my ($slice_credential, $speaksfor_credential) =
+    GenCredentials($slice, $geniuser, $sa_authority, $speaker_signer);
+if (! (defined($speaksfor_credential) &&
+       defined($slice_credential))) {
     $slice->Delete();
-    fatal("Could not sign credential");
+    fatal("Could not generate credentials");
 }
 
-#
-# In order to connect as the SA instead of the user we just created,
-# lets generate a speaksfor credential that allows the SA to speakfor
-# for the new user. Fancy, eh?
-#
-my $speaksfor_credential = GeniCredential->Create($geniuser, $sa_authority);
-fatal("Could not create speaksfor credential")
-    if (!defined($speaksfor_credential));
-$speaksfor_credential->SetType("speaksfor");
-fatal("Could not sign speaksfor credential")
-    if ($speaksfor_credential->Sign($speaker_signer));
-
 #
 # Got this far, lets create a quickvm record.
 #
@@ -688,6 +675,68 @@ sub UserError($) {
     exit(1);
 }
 
+#
+# Generate or yank the speaks for credential out of the DB.
+#
+sub GenCredentials($$$$)
+{
+    my ($target, $geniuser, $authority, $signer) = @_;
+    my ($speaksfor, $credential);
+
+    #
+    # If a local user account, but a nonlocal id, then we should
+    # have a speaksfor credential stored, as well as a certificate
+    # for the user.
+    #
+    if ($geniuser->IsLocal() && $geniuser->emulab_user()->IsNonLocal()) {
+	my ($speaksfor_string, $certificate_string) =
+	    $geniuser->emulab_user()->GetStoredCredential();
+	if (! (defined($speaksfor_string) &&
+	       defined($certificate_string))) {
+	    print STDERR "No stored speaksfor/certificate for $geniuser\n";
+	    goto bad;
+	}
+	$speaksfor = GeniCredential->CreateFromSigned($speaksfor_string);
+	if (!defined($speaksfor)) {
+	    print STDERR "Could not create speaksfor credential\n";
+	    goto bad;
+	}
+	my $certificate =
+	    GeniCertificate->LoadFromString($certificate_string);
+	if (!defined($certificate)) {
+	    print STDERR "Could not load certificate from string\n";
+	    goto bad;
+	}
+	$credential = GeniCredential->Create($target, $certificate);
+    }
+    else {
+	$speaksfor = GeniCredential->Create($geniuser, $authority);
+	if (!defined($speaksfor)) {
+	    print STDERR "Could not create speaksfor credential\n";
+	    goto bad;
+	}
+	$speaksfor->SetType("speaksfor");
+	if ($speaksfor->Sign($signer)) {
+	    print STDERR "Could not sign speaksfor credential\n";
+	    goto bad;
+	}
+	$credential = GeniCredential->Create($target, $geniuser);
+    }
+    if (!defined($credential)) {
+	print STDERR "Could not create credential for $target\n";
+	goto bad;
+    }
+    # And sign it.
+    if ($credential->Sign($GeniCredential::LOCALSA_FLAG) != 0) {
+	$credential->Delete();
+	print STDERR "Could not sign $target credential\n";
+	goto bad;
+    }
+    return ($credential, $speaksfor);
+  bad:
+    return ();
+}
+
 #
 # Terminate a quick VM.
 #
@@ -722,21 +771,15 @@ sub Terminate($)
 	}
 	fatal("No slice for quick VM: $uuid");
     }
-    # Create a slice credential
-    my $slice_credential =
-	GeniCredential->CreateSigned($slice,
-				     $geniuser,
-				     $GeniCredential::LOCALSA_FLAG);
-    if (!defined($slice_credential)) {
-	fatal("Could not create credential for $slice");
+    #
+    # Generate credentials we need.
+    #
+    my ($slice_credential, $speaksfor_credential) =
+	GenCredentials($slice, $geniuser, $sa_authority, $speaker_signer);
+    if (! (defined($speaksfor_credential) &&
+	   defined($slice_credential))) {
+	fatal("Could not generate credentials");
     }
-    my $speaksfor_credential = GeniCredential->Create($geniuser,
-						      $sa_authority);
-    fatal("Could not create speaksfor credential")
-	if (!defined($speaksfor_credential));
-    $speaksfor_credential->SetType("speaksfor");
-    fatal("Could not sign speaksfor credential")
-	if ($speaksfor_credential->Sign($speaker_signer));
 
     #
     # Lock the slice in case it is doing something else, like taking
@@ -849,23 +892,16 @@ sub Extend($$)
     # Need to update slice before creating new credential. 
     $slice->AddToExpiration($extend);
     my $new_expires = $slice->ExpirationGMT();
-    
-    # Create a slice credential
-    my $slice_credential =
-	GeniCredential->CreateSigned($slice,
-				     $geniuser,
-				     $GeniCredential::LOCALSA_FLAG);
-    if (!defined($slice_credential)) {
-	fatal("Could not create credential for $slice");
-    }
-    my $speaksfor_credential = GeniCredential->Create($geniuser,
-						      $sa_authority);
-    fatal("Could not create speaksfor credential")
-	if (!defined($speaksfor_credential));
-    $speaksfor_credential->SetType("speaksfor");
-    fatal("Could not sign speaksfor credential")
-	if ($speaksfor_credential->Sign($speaker_signer));
 
+    #
+    # Generate credentials we need.
+    #
+    my ($slice_credential, $speaksfor_credential) =
+	GenCredentials($slice, $geniuser, $sa_authority, $speaker_signer);
+    if (! (defined($speaksfor_credential) &&
+	   defined($slice_credential))) {
+	fatal("Could not generate credentials");
+    }
     my $response =
 	Genixmlrpc::CallMethod($cm_authority->url(), undef,
 			       "RenewSlice",
@@ -893,7 +929,7 @@ sub SnapShot($$$)
 {
     my ($uuid, $sliver_urn, $imagename) = @_;
 
-    my $this_user = User->ThisUser();
+    $this_user = User->ThisUser();
     if (! defined($this_user)) {
 	fatal("You ($UID) do not exist!");
     }
@@ -933,21 +969,15 @@ sub SnapShot($$$)
 	fatal("No slice for quick VM: $uuid");
     }
 
-    # Create a slice credential
-    my $slice_credential =
-	GeniCredential->CreateSigned($slice,
-				     $geniuser,
-				     $GeniCredential::LOCALSA_FLAG);
-    if (!defined($slice_credential)) {
-	fatal("Could not create credential for $slice");
+    #
+    # Generate credentials we need.
+    #
+    my ($slice_credential, $speaksfor_credential) =
+	GenCredentials($slice, $geniuser, $sa_authority, $speaker_signer);
+    if (! (defined($speaksfor_credential) &&
+	   defined($slice_credential))) {
+	fatal("Could not generate credentials");
     }
-    my $speaksfor_credential = GeniCredential->Create($geniuser,
-						      $sa_authority);
-    fatal("Could not create speaksfor credential")
-	if (!defined($speaksfor_credential));
-    $speaksfor_credential->SetType("speaksfor");
-    fatal("Could not sign speaksfor credential")
-	if ($speaksfor_credential->Sign($speaker_signer));
 
     #
     # We do this with slice locked.
diff --git a/protogeni/scripts/updategeniuser.in b/protogeni/scripts/updategeniuser.in
new file mode 100644
index 0000000000000000000000000000000000000000..da085c1471fa5e11274d45c934c3f5521a95b171
--- /dev/null
+++ b/protogeni/scripts/updategeniuser.in
@@ -0,0 +1,307 @@
+#!/usr/bin/perl -w
+#
+# Copyright (c) 2008-2014 University of Utah and the Flux Group.
+# 
+# {{{GENIPUBLIC-LICENSE
+# 
+# GENI Public License
+# 
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and/or hardware specification (the "Work") to
+# deal in the Work without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Work, and to permit persons to whom the Work
+# is furnished to do so, subject to the following conditions:
+# 
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Work.
+# 
+# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS
+# IN THE WORK.
+# 
+# }}}
+#
+use strict;
+use English;
+use Getopt::Std;
+use Data::Dumper;
+
+#
+# Update a geni user.
+#
+sub usage()
+{
+    print STDERR "Usage: $0 [-c <credfile> -e <certfile>] [-s] <user>\n";
+    exit(1);
+}
+my $optlist   = "c:se:n";
+my $dosshkeys = 0;
+my $impotent  = 0;
+my $credfile;
+my $certfile;
+
+# Configure ...
+my $TB		  = "@prefix@";
+my $SACERT	  = "$TB/etc/genisa.pem";
+my $ADDPUBKEY     = "$TB/sbin/addpubkey";
+
+# Do this early so that we talk to the right DB. 
+use vars qw($GENI_DBNAME);
+BEGIN { $GENI_DBNAME = "geni"; }
+
+use lib '@prefix@/lib';
+use libtestbed;
+use emutil;
+use User;
+use GeniCertificate;
+use GeniCredential;
+use GeniAuthority;
+use Genixmlrpc;
+use GeniResponse;
+use GeniHRN;
+
+# Protos
+sub UpdateCredential();
+sub UpdateSSHKeys();
+
+sub fatal($)
+{
+    my ($msg) = @_;
+
+    die("*** $0:\n".
+	"    $msg\n");
+}
+
+#
+# Parse command arguments. Once we return from getopts, all that should be
+# left are the required arguments.
+#
+my %options = ();
+if (! getopts($optlist, \%options)) {
+    usage();
+}
+if (defined($options{"s"})) {
+    $dosshkeys = 1;
+}
+if (defined($options{"n"})) {
+    $impotent = 1;
+}
+if (defined($options{"c"})) {
+    $credfile = $options{"c"};
+}
+if (defined($options{"e"})) {
+    $certfile = $options{"e"};
+}
+usage()
+    if (@ARGV != 1);
+
+my $target_user = User->Lookup($ARGV[0]);
+if (!defined($target_user)) {
+    fatal("No such user");
+}
+my $target_uid = $target_user->uid();
+
+my $this_user = User->ThisUser();
+if (!defined($this_user)) {
+    fatal("Who are you?");
+}
+if (! ($this_user->SameUser($target_user) || $this_user->IsAdmin())) {
+    fatal("Not allowed to update user; must be an admin");
+}
+
+if (defined($credfile)) {
+    usage()
+	if (!defined($certfile));
+}
+if (defined($certfile)) {
+    usage()
+	if (!defined($credfile));
+    UpdateCredential()
+}
+if ($dosshkeys) {
+    UpdateSSHKeys();
+}
+exit(0);
+
+#
+# Update the speaksfor credential for the user.
+#
+sub UpdateCredential()
+{
+    fatal("No such file: $credfile")
+	if (! -e $credfile);
+    fatal("No such file: $certfile")
+	if (! -e $certfile);
+    my $credential = GeniCredential->LoadFromFile($credfile);
+    if (!defined($credential)) {
+	fatal("Could not parse credential from file");
+    }
+    fatal("Not a speaksfor credential")
+	if (! ($credential->type() eq "speaksfor" ||
+	       $credential->type() eq "abac"));
+
+    my $certificate = GeniCertificate->LoadFromFile($certfile);
+    if (!defined($certificate)) {
+	fatal("Could not parse certificate from file");
+    }
+    $target_user->StoreCredential($credential->asString(),
+				  $credential->expires(),
+				  $certificate->cert())
+	== 0 or fatal("Could not store credential for user");
+
+    return 0;
+}
+
+#
+# Update ssh keys. 
+#
+sub UpdateSSHKeys()
+{
+    #
+    # Load the SA cert to act as caller context.
+    #
+    my $sa_certificate = GeniCertificate->LoadFromFile($SACERT);
+    if (!defined($sa_certificate)) {
+	fatal("Could not load certificate from $SACERT\n");
+    }
+    my $context = Genixmlrpc->Context($sa_certificate);
+    if (!defined($context)) {
+	fatal("Could not create context to talk to MA");
+    }
+    
+    #
+    # Need the credential and the certificate. The certificate allows us
+    # to figure out who to talk to, to get the keys. For protogeni it is
+    # the URL in the certificate. For the GCF, well just hardwire it to
+    # the common federation api URL. 
+    #
+    my ($cred,$cert) = $target_user->GetStoredCredential();
+    fatal("No stored credential for $target_user")
+	if (!defined($cred) || !defined($cert));
+    
+    my $speaksfor = GeniCredential->CreateFromSigned($cred);
+    if (!defined($speaksfor)) {
+	fatal("Could not parse credential from string");
+    }
+    my $geni_type = ($speaksfor->type() eq "abac") ? "geni_abac" : "geni_sfa";
+    my $geni_vers = ($speaksfor->type() eq "abac") ? 1 : 3;
+	
+    my $certificate = GeniCertificate->LoadFromString($cert);
+    if (!defined($certificate)) {
+	fatal("Could not parse certificate from string");
+    }
+    my $user_urn = $certificate->urn();
+
+    #
+    # We need a URL to make the RPC. IG certs have that url in
+    # the certificate (clever people that we are), but GPO certs refer
+    # to a nonexistent SA. So just hardwire it, just like flack
+    # does.
+    #
+    # We are going to use the FED API. 
+    #
+    my @params = ([{"geni_type" => $geni_type,
+		    "geni_version" => $geni_vers,
+		    "geni_value" => $speaksfor->asString()}
+		  ],
+		  # Options array.
+		  {"speaking_for" => $user_urn,
+		   "geni_speaking_for" => $user_urn,
+		   "match"   => {'KEY_MEMBER' => $user_urn},
+		   "filter"  => ['KEY_PUBLIC'],
+		  });
+    my $method;
+    my $url;
+    my ($auth,$type,$id) = GeniHRN::Parse($user_urn);
+    if ($auth =~ /geni\.net/) {
+	$url = "https://ch.geni.net/MA";
+	$method = "lookup";
+	@params = ("KEY", @params);
+    }
+    else {
+	$url = $certificate->url();
+	$url =~ s/sa$/geni-ma/;
+	$url = "https://www.emulab.net:12369/protogeni/stoller/xmlrpc/geni-ma";
+	$method = "lookup_keys";
+    }
+    my $response =
+	Genixmlrpc::CallMethod($url, $context, $method, @params);
+    if (!defined($response)) {
+	fatal("Internal error getting self credential");
+    }
+    if ($response->code() != GENIRESPONSE_SUCCESS) {
+	fatal("Could not get keys: " . $response->output());
+    }
+    if (! (ref($response->value()) &&
+	   exists($response->value()->{$user_urn}) &&
+	   ref($response->value()->{$user_urn}))) {
+	fatal("Returned keys do not look right");
+    }
+    my @keys = @{ $response->value()->{$user_urn} };
+    if (!@keys) {
+	fatal("No keys returned for user!");
+    }
+    my $filename = TBMakeTempFile("geniuserkey");
+
+    #
+    # First loop and verify all the keys. 
+    #
+    foreach my $ref (@keys) {
+	fatal("Bad format in key array; no KEY_PUBLIC")
+	    if (!exists($ref->{'KEY_PUBLIC'}));
+	my $key = $ref->{'KEY_PUBLIC'};
+	next
+	    if ($key =~ /sslcert/);
+	open(KEY, ">$filename") or
+	    fatal("Could not open $filename for writing");
+	print KEY $key . "\n";
+	close(KEY);
+
+	system("$ADDPUBKEY -n -f $filename");
+	if ($?) {
+	    fatal("Key does not verify: $key");
+	}
+    }
+    if ($impotent) {
+	print "Exiting without doing anything ...\n";
+	exit(0);
+    }
+    
+    #
+    # Delete current keys and add all of the new ones.
+    #
+    $target_user->DeleteSSHKeys() == 0
+	or fatal("Could not delete current ssh keys");
+
+    foreach my $ref (@keys) {
+	my $key = $ref->{'KEY_PUBLIC'};
+	next
+	    if ($key =~ /sslcert/);
+	open(KEY, ">$filename") or
+	    fatal("Could not open $filename for writing");
+	print KEY $key . "\n";
+	close(KEY);
+
+	system("$ADDPUBKEY -s -u $target_uid -f $filename");
+	if ($?) {
+	    fatal("Could not add key: $key");
+	}
+    }
+    unlink($filename);
+
+    #
+    # Regenerate the authkeys file,
+    #
+    system("$ADDPUBKEY -w $target_uid");
+    if ($?) {
+	fatal("Could not regenerate authorized_keys file");
+    }
+    return 0;
+}
+exit(0);
diff --git a/protogeni/scripts/webcreategeniuser.in b/protogeni/scripts/webcreategeniuser.in
new file mode 100644
index 0000000000000000000000000000000000000000..9b0d129fccc2acd74c7ceeda71a6da33847eed1b
--- /dev/null
+++ b/protogeni/scripts/webcreategeniuser.in
@@ -0,0 +1,41 @@
+#!/usr/bin/perl -w
+#
+# Copyright (c) 2000-2014 University of Utah and the Flux Group.
+# 
+# {{{EMULAB-LICENSE
+# 
+# This file is part of the Emulab network testbed software.
+# 
+# This file is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or (at
+# your option) any later version.
+# 
+# This file is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
+# License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this file.  If not, see <http://www.gnu.org/licenses/>.
+# 
+# }}}
+#
+use English;
+
+#
+# This gets invoked from the Web interface. Simply a wrapper ...
+# Automatically generated from the template in the toplevel dir.
+#
+
+#
+# Configure variables
+#
+my $PROG = "@prefix@/sbin/protogeni/creategeniuser";
+
+#
+# Run the real thing, and never return.
+# 
+exec $PROG, @ARGV;
+
+die("Could not exec $PROG: $!");
diff --git a/protogeni/scripts/webupdategeniuser.in b/protogeni/scripts/webupdategeniuser.in
new file mode 100644
index 0000000000000000000000000000000000000000..5564912830e920cad8ecaaca683cc226f3cfe178
--- /dev/null
+++ b/protogeni/scripts/webupdategeniuser.in
@@ -0,0 +1,41 @@
+#!/usr/bin/perl -w
+#
+# Copyright (c) 2000-2014 University of Utah and the Flux Group.
+# 
+# {{{EMULAB-LICENSE
+# 
+# This file is part of the Emulab network testbed software.
+# 
+# This file is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or (at
+# your option) any later version.
+# 
+# This file is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
+# License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this file.  If not, see <http://www.gnu.org/licenses/>.
+# 
+# }}}
+#
+use English;
+
+#
+# This gets invoked from the Web interface. Simply a wrapper ...
+# Automatically generated from the template in the toplevel dir.
+#
+
+#
+# Configure variables
+#
+my $PROG = "@prefix@/sbin/protogeni/updategeniuser";
+
+#
+# Run the real thing, and never return.
+# 
+exec $PROG, @ARGV;
+
+die("Could not exec $PROG: $!");
diff --git a/www/aptui/geni-login.ajax b/www/aptui/geni-login.ajax
index dfee2f232b81d7b8e265c49397e6b8f0cef251f0..4a3cae0077dbca7c99abad5d0c1190d03e1a3337 100644
--- a/www/aptui/geni-login.ajax
+++ b/www/aptui/geni-login.ajax
@@ -24,6 +24,78 @@
 chdir("..");
 include_once("geni_defs.php");
 chdir("apt");
+include_once("instance_defs.php");
+
+#
+# So we can capture stderr. Sheesh.
+# 
+function myexec($cmd)
+{
+    ignore_user_abort(1);
+
+    $myexec_output_array = array();
+    $myexec_output       = "";
+    $myexec_retval       = 0;
+    
+    exec("$cmd 2>&1", $myexec_output_array, $myexec_retval);
+    if ($myexec_retval) {
+	for ($i = 0; $i < count($myexec_output_array); $i++) {
+	    $myexec_output .= "$myexec_output_array[$i]\n";
+	}
+	$foo  = "Shell Program Error. Exit status: $myexec_retval\n";
+	$foo .= "  '$cmd'\n";
+	$foo .= "\n";
+	$foo .= $myexec_output;
+	TBERROR($foo, 0);
+	return 1;
+    }
+    return 0;
+}
+
+#
+# Return info to allow the client to load and start the auth process.
+# This is entirely cause we want to have a login button on each page,
+# but not have to load all the signer stuff unless its actually used.
+#
+function Do_GetSignerInfo()
+{
+    $hash = GENHASH();
+
+    # We use a session to hold stuff across the ajax calls
+    session_start();
+    session_regenerate_id(TRUE);
+
+    $blob = array();
+    $blob["HOST"]  = "https://www.emulab.net";
+    $blob["PATH"]  = "/protogeni/speaks-for/index.html";
+    $blob["ID"]    = "urn:publicid:IDN+emulab.net+authority+sa";
+    $blob["AUTH"]  = "https://www.emulab.net/protogeni/speaks-for/geni-auth.js";
+    $blob["CERT"]  = 
+	"-----BEGIN CERTIFICATE-----\n" .
+	"MIIDoTCCAwqgAwIBAgIDAS/uMA0GCSqGSIb3DQEBBAUAMIG4MQswCQYDVQQGEwJV\n" .
+	"UzENMAsGA1UECBMEVXRhaDEXMBUGA1UEBxMOU2FsdCBMYWtlIENpdHkxHTAbBgNV\n" .
+	"BAoTFFV0YWggTmV0d29yayBUZXN0YmVkMR4wHAYDVQQLExVDZXJ0aWZpY2F0ZSBB\n" .
+	"dXRob3JpdHkxGDAWBgNVBAMTD2Jvc3MuZW11bGFiLm5ldDEoMCYGCSqGSIb3DQEJ\n" .
+	"ARYZdGVzdGJlZC1vcHNAZmx1eC51dGFoLmVkdTAeFw0xMTEwMDUxOTUxMDZaFw0x\n" .
+	"NzAzMjcyMDUxMDZaMIGsMQswCQYDVQQGEwJVUzENMAsGA1UECBMEVXRhaDEdMBsG\n" .
+	"A1UEChMUVXRhaCBOZXR3b3JrIFRlc3RiZWQxFjAUBgNVBAsTDXV0YWhlbXVsYWIu\n" .
+	"c2ExLTArBgNVBAMTJDJiNDM3ZmFhLWFhMDAtMTFkZC1hZDFmLTAwMTE0M2U0NTNm\n" .
+	"ZTEoMCYGCSqGSIb3DQEJARYZdGVzdGJlZC1vcHNAZmx1eC51dGFoLmVkdTCBnzAN\n" .
+	"BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1ayN3cGHH9hsmTgVWVjb2ZOqF8zFJ1Ew\n" .
+	"TFRpXVtI//wk05+Z7uunpxn/QL1F3NjdcIEToEupo1q2tRUfCc2hquLBgC5zNfut\n" .
+	"YD/b5ukEsF5COKHb+pYl2RZly9BVckt+ySFLnC23erKW7ILyO2fGBD/QzHZNPhdY\n" .
+	"/fs18iCh58cCAwEAAaOBwjCBvzAdBgNVHQ4EFgQUU2CjacFUMyUNL++CplFi++MF\n" .
+	"Sl0wMwYDVR0RBCwwKoYodXJuOnB1YmxpY2lkOklETitlbXVsYWIubmV0K2F1dGhv\n" .
+	"cml0eStzYTAPBgNVHRMBAf8EBTADAQH/MFgGCCsGAQUFBwEBBEwwSjBIBhRpg8yT\n" .
+	"gKiYzKjHvbGngICqrteKG4YwaHR0cHM6Ly93d3cuZW11bGFiLm5ldDoxMjM2OS9w\n" .
+	"cm90b2dlbmkveG1scnBjL3NhMA0GCSqGSIb3DQEBBAUAA4GBAIDXwcvEu3HJApFQ\n" .
+	"bQduTiHGXQ8Og/2ZIFLXHkqu4SW81RaYVbHwRFxnKHOktKm7js9wjEPo/F0tqIRT\n" .
+	"21x7yE7uOce/8tWNW241fVuIRyO/o/DNd/FVFyFU5WNqP6f/rzEu92iuO6zIJPBg\n" .
+	"fmkqRvZqMOm5R//SSNBFl83lZzlu\n" .
+	"-----END CERTIFICATE-----";
+
+    SPITAJAX_RESPONSE($blob);
+}
 
 #
 # 
@@ -71,20 +143,13 @@ function Do_CreateSecret()
     $fp = fopen($infname, "w");
     fwrite($fp, $r1_encrypted);
     fclose($fp);
-    $exec_output_array = array();
-    $exec_retval       = 0;
 
-    exec("/usr/bin/openssl smime -decrypt -inform PEM -inkey ".
-	 "${TBDIR}/etc/genisa.pem -in $infname -out $outfname",
-	 $exec_output_array, $exec_retval);
+    $retval =
+	myexec("/usr/bin/openssl smime -decrypt -inform PEM -inkey ".
+	       "${TBDIR}/etc/genisa.pem -in $infname -out $outfname");
 
-    if ($exec_retval) {
-	$exec_output = "";
-	for ($i = 0; $i < count($exec_output_array); $i++) {
-	    $exec_output .= "$exec_output_array[$i]\n";
-	}
-	TBERROR("Could not decrypt pkcs7 data:\n\n$exec_output", 0);
-	SPITAJAX_ERROR(-1, "Internal Error");
+    if ($retval) {
+	SPITAJAX_ERROR(-1, "Internal decryption error");
 	return;
     }
     $r1_decrypted = file_get_contents($outfname);
@@ -101,21 +166,16 @@ function Do_CreateSecret()
     fwrite($fp, $certificate);
     fclose($fp);
 
-    exec("/usr/bin/openssl smime -encrypt -outform PEM ".
-	 "-in $infname -out $outfname -aes256 $userCertName",
-	 $exec_output_array, $exec_retval);
+    $retval =
+	myexec("/usr/bin/openssl smime -encrypt -outform PEM ".
+	       "-in $infname -out $outfname -aes256 $userCertName");
 
-    if ($exec_retval) {
-	$exec_output = "";
-	for ($i = 0; $i < count($exec_output_array); $i++) {
-	    $exec_output .= "$exec_output_array[$i]\n";
-	}
-	TBERROR("Could not encrypt random string:\n\n$exec_output", 0);
-	SPITAJAX_ERROR(-1, "Internal Error");
+    if ($retval) {
+	SPITAJAX_ERROR(-1, "Internal encryption error");
 	return;
     }
     $r2_encrypted = file_get_contents($outfname);
-    $secret = $r1_decrypted . $r2_decrypted;#bin2hex(pack('H*', $r1_decrypted) ^ pack('H*', $r2_decrypted));
+    $secret = $r1_decrypted . $r2_decrypted;
 
     $blob = array();
     $blob["r2_encrypted"] = $r2_encrypted;
@@ -127,6 +187,7 @@ function Do_CreateSecret()
 
     unlink($infname);
     unlink($outfname);
+    unlink($userCertName);
     SPITAJAX_RESPONSE($blob);
 }
 
@@ -136,8 +197,8 @@ function Do_CreateSecret()
 function Do_VerifySpeaksfor()
 {
     global $ajax_args;
-    global $TBDIR;
-    global $TBAUTHCOOKIE, $TBLOGINCOOKIE, $TBAUTHTIMEOUT;
+    global $TBDIR, $COOKDIEDOMAIN;
+    global $TBAUTHCOOKIE, $TBLOGINCOOKIE, $TBAUTHTIMEOUT, $TBNAMECOOKIE;
 
     # Restore the session.
     if (!session_start()) {
@@ -187,20 +248,13 @@ function Do_VerifySpeaksfor()
     $fp = fopen($infname, "w");
     fwrite($fp, $_SESSION["certificate"]);
     fclose($fp);
-    $exec_output_array = array();
-    $exec_retval       = 0;
-    $exec_output       = "";
 
-    exec("$TBDIR/sbin/protogeni/parsecert $infname $outfname",
-	 $exec_output_array, $exec_retval);
+    $retval = 
+	myexec("$TBDIR/sbin/protogeni/parsecert $infname $outfname");
 
-    if ($exec_retval) {
-	$exec_output = "";
-	for ($i = 0; $i < count($exec_output_array); $i++) {
-	    $exec_output .= "$exec_output_array[$i]\n";
-	}
-	TBERROR("Could not parse user certificate:\n\n$exec_output", 0);
-	SPITAJAX_ERROR(-1, "Internal Error");
+    if ($retval) {
+	SPITAJAX_ERROR(-1, "Internal certificate parse error");
+	session_destroy();
 	return;
     }
     $parse_output = file_get_contents($outfname);
@@ -214,6 +268,7 @@ function Do_VerifySpeaksfor()
     if (!$parsed) {
 	TBERROR("Could not parse XML output:\n$parse_output\n", 0);
 	SPITAJAX_ERROR(-1, "Internal Error");
+	session_destroy();
 	return;
     }
     $info = array();
@@ -224,12 +279,26 @@ function Do_VerifySpeaksfor()
     #
     # Find the user and log them in, returning the cookies to the caller.
     #
-    $this_user = User::LookupByUUID($info["uuid"]);
+    $this_user = User::LookupNonLocal($info["urn"]);
     if (!$this_user) {
-	SPITAJAX_ERROR(1, "Could not find local user account");
+	if (CreateNonLocalUser($info["urn"], $info["email"])) {
+	    SPITAJAX_ERROR(-1, "Internal error creating new user");
+	    session_destroy();
+	    return;
+	}
+	$this_user = User::LookupNonLocal($info["urn"]);
+	if (!$this_user) {
+	    SPITAJAX_ERROR(-1, "Internal error looking up new user");
+	    session_destroy();
+	    return;
+	}
+    }
+    if (UpdateCredentials($this_user, $_SESSION["certificate"], $speaksfor)) {
+	SPITAJAX_ERROR(-1, "Internal error updating user credentials");
 	session_destroy();
 	return;
     }
+    
     list ($loginhash, $logincrc) =
 	DOLOGIN_MAGIC($this_user->uid(), $this_user->uid_idx(), null, 0, 1);
     if (! ($loginhash && $logincrc)) {
@@ -238,11 +307,16 @@ function Do_VerifySpeaksfor()
 	return;
     }
     $blob = array();
+    $blob["domain"]    = $COOKDIEDOMAIN;
     $blob["hashname"]  = $TBAUTHCOOKIE;
     $blob["hash"]      = $loginhash;
-    $blob["loginname"] = $logincrc;
-    $blob["login"]     = $TBLOGINCOOKIE;
-    $blob["timeout"]   = $TBAUTHTIMEOUT;
+    $blob["crcname"]   = $TBLOGINCOOKIE;
+    $blob["crc"]       = $logincrc;
+    $blob["username"]  = $TBNAMECOOKIE;
+    $blob["user"]      = $this_user->uid_idx();
+    $blob["timeout"]   = time() + $TBAUTHTIMEOUT;
+    $blob["url"]       = (Instance::UserHasInstances($this_user)
+			  ? "myexperiments.php" : "instantiate.php");
     session_destroy();
     SPITAJAX_RESPONSE($blob);
 }
@@ -250,11 +324,51 @@ function Do_VerifySpeaksfor()
 #
 # Create a new user. All we have is the email, urn, and uuid.
 #
-function CreateNonLocalUser($urn, $email, $cert, $cred)
+function CreateNonLocalUser($urn, $email)
 {
-    #
-    # 
-    #
+    global $TBOPSPID;
+    $safe_urn = escapeshellarg($urn);
+    $safe_email = escapeshellarg($email);
+    
+    $retval = SUEXEC("elabman", $TBOPSPID,
+		     "webcreategeniuser $safe_urn $safe_email",
+		     SUEXEC_ACTION_CONTINUE);
+    if ($retval)
+	return -1;
+
+    return 0;
+}
+
+#
+# Update the certificate/credential for the user.
+# 
+function UpdateCredentials($user, $cert, $cred)
+{
+    $uid = $user->uid();
+    
+    $credfile = tempnam("/tmp", "cert");
+    $certfile = tempnam("/tmp", "cred");
+
+    $fp = fopen($credfile, "w");
+    fwrite($fp, $cred);
+    fclose($fp);
+    $fp = fopen($certfile, "w");
+    fwrite($fp, $cert);
+    fclose($fp);
+    chmod($certfile, 0666);
+    chmod($credfile, 0666);
+    
+    $retval = SUEXEC($uid, "CloudLab",
+		     "webupdategeniuser -c $credfile -e $certfile $uid",
+		     SUEXEC_ACTION_CONTINUE);
+
+    unlink($credfile);
+    unlink($certfile);
+    
+    if ($retval)
+	return -1;
+
+    return 0;
 }
 
 # Local Variables:
diff --git a/www/aptui/geni-login.php b/www/aptui/geni-login.php
index d0f1269c6ba10a2fcc09f1f65b59a11f2775eba1..368aaae0f68039fdd570c0e487fc1812f6dc4cc7 100644
--- a/www/aptui/geni-login.php
+++ b/www/aptui/geni-login.php
@@ -34,53 +34,17 @@ $page_title = "Login";
 #
 RedirectSecure();
 $this_user = CheckLogin($check_status);
-if (0 && $CHECKLOGIN_STATUS & CHECKLOGIN_LOGGEDIN) {
+if ($CHECKLOGIN_STATUS & CHECKLOGIN_LOGGEDIN) {
     SPITUSERERROR("You are already logged in!");
 }
 $hash = GENHASH();
 
-# We use a session to hold stuff across the ajax calls
-session_start();
-session_regenerate_id(TRUE);
-
 SPITHEADER(1);
 
 # Place to hang the toplevel template.
 echo "<div id='page-body'></div>\n";
-
-echo "<script type='text/javascript'>\n";
-echo "    window.HOST    = 'https://www.emulab.net';\n";
-echo "    window.PATH    = '/protogeni/speaks-for/index.html';\n";
-echo "    window.HASH    = '$hash';\n";
-echo "    window.AJAXURL = 'server-ajax.php';\n";
-echo "    window.ID      = 'urn:publicid:IDN+emulab.net+authority+s';\n";
-echo "    window.CERT    = ";
-?>
-'-----BEGIN CERTIFICATE-----\n' +
-'MIIDoTCCAwqgAwIBAgIDAS/uMA0GCSqGSIb3DQEBBAUAMIG4MQswCQYDVQQGEwJV\n' +
-'UzENMAsGA1UECBMEVXRhaDEXMBUGA1UEBxMOU2FsdCBMYWtlIENpdHkxHTAbBgNV\n' +
-'BAoTFFV0YWggTmV0d29yayBUZXN0YmVkMR4wHAYDVQQLExVDZXJ0aWZpY2F0ZSBB\n' +
-'dXRob3JpdHkxGDAWBgNVBAMTD2Jvc3MuZW11bGFiLm5ldDEoMCYGCSqGSIb3DQEJ\n' +
-'ARYZdGVzdGJlZC1vcHNAZmx1eC51dGFoLmVkdTAeFw0xMTEwMDUxOTUxMDZaFw0x\n' +
-'NzAzMjcyMDUxMDZaMIGsMQswCQYDVQQGEwJVUzENMAsGA1UECBMEVXRhaDEdMBsG\n' +
-'A1UEChMUVXRhaCBOZXR3b3JrIFRlc3RiZWQxFjAUBgNVBAsTDXV0YWhlbXVsYWIu\n' +
-'c2ExLTArBgNVBAMTJDJiNDM3ZmFhLWFhMDAtMTFkZC1hZDFmLTAwMTE0M2U0NTNm\n' +
-'ZTEoMCYGCSqGSIb3DQEJARYZdGVzdGJlZC1vcHNAZmx1eC51dGFoLmVkdTCBnzAN\n' +
-'BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1ayN3cGHH9hsmTgVWVjb2ZOqF8zFJ1Ew\n' +
-'TFRpXVtI//wk05+Z7uunpxn/QL1F3NjdcIEToEupo1q2tRUfCc2hquLBgC5zNfut\n' +
-'YD/b5ukEsF5COKHb+pYl2RZly9BVckt+ySFLnC23erKW7ILyO2fGBD/QzHZNPhdY\n' +
-'/fs18iCh58cCAwEAAaOBwjCBvzAdBgNVHQ4EFgQUU2CjacFUMyUNL++CplFi++MF\n' +
-'Sl0wMwYDVR0RBCwwKoYodXJuOnB1YmxpY2lkOklETitlbXVsYWIubmV0K2F1dGhv\n' +
-'cml0eStzYTAPBgNVHRMBAf8EBTADAQH/MFgGCCsGAQUFBwEBBEwwSjBIBhRpg8yT\n' +
-'gKiYzKjHvbGngICqrteKG4YwaHR0cHM6Ly93d3cuZW11bGFiLm5ldDoxMjM2OS9w\n' +
-'cm90b2dlbmkveG1scnBjL3NhMA0GCSqGSIb3DQEBBAUAA4GBAIDXwcvEu3HJApFQ\n' +
-'bQduTiHGXQ8Og/2ZIFLXHkqu4SW81RaYVbHwRFxnKHOktKm7js9wjEPo/F0tqIRT\n' +
-'21x7yE7uOce/8tWNW241fVuIRyO/o/DNd/FVFyFU5WNqP6f/rzEu92iuO6zIJPBg\n' +
-'fmkqRvZqMOm5R//SSNBFl83lZzlu\n' +
-'-----END CERTIFICATE-----';
-<?php
-echo "</script>\n";
-echo "<script src='https://www.emulab.net/protogeni/speaks-for/geni-auth.js'></script>\n";
+echo "<script src='https://www.emulab.net/protogeni/speaks-for/geni-auth.js'>
+      </script>\n";
 echo "<script src='js/lib/jquery-2.0.3.min.js'></script>\n";
 echo "<script src='js/lib/bootstrap.js'></script>\n";
 echo "<script src='js/lib/require.js' data-main='js/geni-login'></script>\n";
diff --git a/www/aptui/instance_defs.php b/www/aptui/instance_defs.php
index 215a3fe939561cc669b3bc8f17627ec8a64736d5..3e7885c211b71819e47a7f4ab00f816cde27a9aa 100644
--- a/www/aptui/instance_defs.php
+++ b/www/aptui/instance_defs.php
@@ -25,6 +25,8 @@
 
 $am_array = array('Utah DDC' =>
 		     "urn:publicid:IDN+utahddc.geniracks.net+authority+cm",
+		  'Utah APT' =>
+		     "urn:publicid:IDN+apt.emulab.net+authority+cm",
 		  'Utah PG'  =>
 		     "urn:publicid:IDN+emulab.net+authority+cm");
 
@@ -148,16 +150,16 @@ class Instance
 	    fclose($fp);
 	    chmod($xmlname, 0666);
 	}
+	# 
+	# With a real user, run as that user. 
 	#
-	# This option is used to tell the backend that it is okay to look
-	# in the emulab users table.
-	#
-	$options .= ($creator ? " -l" : "");
+	$uid = ($creator ? $creator->uid() : "nobody");
+	$pid = ($creator ? $creator->FirstApprovedProject()->pid() : "nobody");
 
 	if (isset($_SERVER['REMOTE_ADDR'])) { 
 	    putenv("REMOTE_ADDR=" . $_SERVER['REMOTE_ADDR']);
 	}
-	$retval = SUEXEC("nobody", "nobody",
+	$retval = SUEXEC($uid, $pid,
 			 "webquickvm $options -u $uuid $xmlname",
 			 SUEXEC_ACTION_CONTINUE);
 
diff --git a/www/aptui/js/common.js b/www/aptui/js/common.js
index 79671d5ffdecc2b2649c168200211e350de725d9..5ee9b5780c75e59be28a8b4052dd18df632fe983 100644
--- a/www/aptui/js/common.js
+++ b/www/aptui/js/common.js
@@ -34,5 +34,34 @@ window.APT_OPTIONS.configObject = {
 
 window.APT_OPTIONS.initialize = function (sup)
 {
+    var geniauth = "https://www.emulab.net/protogeni/speaks-for/geni-auth.js";
+
+    // Every page calls this, and since the Login button is on every
+    // page, do this initialization here. 
+    if ($('#quickvm_geni_login_button').length) {
+	$('#quickvm_geni_login_button').click(function (event) {
+	    event.preventDefault();
+	    sup.HideModal("#quickvm_login_modal");
+	    sup.StartGeniLogin();
+	    return false;
+	});
+    }
+    // When the user clicks on the login button, we not only display
+    // the modal, but fire off the load of the geni-auth.js file so
+    // that the code is loaded. Something to do with popup rules from
+    // javascript event handlers, blah blah blah. Ask Jon.
+    if ($('#loginbutton').length) {
+	$('#loginbutton').click(function (event) {
+	    event.preventDefault();
+	    sup.ShowModal('#quickvm_login_modal');
+	    console.info("Loading geni auth code");
+	    sup.InitGeniLogin();
+	    require([geniauth], function() {
+		console.info("Geni auth code has been loaded");
+		$('#quickvm_geni_login_button').removeAttr("disabled");
+	    });
+	    return false;
+	});
+    }
     $('body').show();
 }
diff --git a/www/aptui/js/geni-login.js b/www/aptui/js/geni-login.js
index cc5bbd5dd74ed63da6a4e9286c31f4ba479b706c..ce6678cf00a4ae8707946870426566de18d9616a 100644
--- a/www/aptui/js/geni-login.js
+++ b/www/aptui/js/geni-login.js
@@ -1,87 +1,25 @@
 require(window.APT_OPTIONS.configObject,
 	['underscore', 'js/quickvm_sup',
-	 'https://www.emulab.net/protogeni/speaks-for/lib/forge/forge',
-	 'js/lib/text!template/geni-login.html'],
-function (_, sup, forge, loginString)
+	 'js/lib/text!template/geni-login.html',
+	 'js/lib/text!template/waitwait-modal.html'],
+function (_, sup, loginString, waitwaitString)
 {
     'use strict';
-    var ajaxurl;
     
     function initialize()
     {
-	window.APT_OPTIONS.initialize(sup);
-	ajaxurl = window.AJAXURL;
-
-	genilib.trustedHost = window.HOST;
-	genilib.trustedPath = window.PATH;
 	$('#page-body').html(loginString);
-
+	$('#waitwait_div').html(waitwaitString);
+	// We share code with the modal version of login, and the
+	// handler for the button is installed in initialize().
+	// See comment there.
+	sup.InitGeniLogin();
 	$('#authorize').click(function (event) {
 	    event.preventDefault();
-	    genilib.authorize({
-		id: window.ID,
-		toolCertificate: window.CERT,
-		complete: complete,
-		authenticate: authenticate
-	    });
+	    sup.StartGeniLogin();
 	    return false;
 	});
-
-//	CreateSecret(foo, mycert);
-    }
-
-    function VerifySpeaksfor(speaksfor, signature)
-    {
-	var callback = function(json) {
-	    if (json.code) {
-		alert("Could not verify speaksfor: " + json.value);
-		return;
-	    }
-	    console.info(json.value);
-
-	    //
-	    // Need to set the cookies we get back so that we can
-	    // redirect to the status page.
-	    //
-	    document.cookie =
-		json.value.hashname + '=' + json.value.hash +
-		'; max-age=' + json.value.timeout + '; path=/; secure';
-	    document.cookie =
-		json.value.loginname + '=' + json.value.login +
-		'; max-age=' + json.value.timeout + '; path=/';
-	}
-	var $xmlthing = sup.CallServerMethod(ajaxurl,
-					     "geni-login", "VerifySpeaksfor",
-					     {"speaksfor" : speaksfor,
-					      "signature" : signature});
-	$xmlthing.done(callback);
-    }
-
-    function authenticate(cert, r1, success, failure)
-    {
-	var callback = function(json) {
-	    console.log('callback');
-	    if (json.code) {
-		alert("Could not generate secret: " + json.value);
-		failure();
-	    } else {
-		console.info(json.value);
-		success(json.value.r2_encrypted);
-	    }
-	}
-	var $xmlthing = sup.CallServerMethod(ajaxurl,
-					     "geni-login", "CreateSecret",
-					     {"r1_encrypted" : r1,
-					      "certificate"  : cert});
-	$xmlthing.done(callback);
-    }
-
-    function complete(credential, signature)
-    {
-	// signature is undefined if something failed before
-	VerifySpeaksfor(credential, signature);
-//	console.log(credential);
-//	console.log(signature);
+	window.APT_OPTIONS.initialize(sup);
     }
     $(document).ready(initialize);
 });
diff --git a/www/aptui/js/instantiate.js b/www/aptui/js/instantiate.js
index c059a16922ae174aeb5787ca9e77407d14240e6e..e062f20d56d96e7eb10d332d2ba8270dbdf38841 100644
--- a/www/aptui/js/instantiate.js
+++ b/www/aptui/js/instantiate.js
@@ -11,7 +11,7 @@ function (_, sup, aboutaptString, aboutcloudString)
 
     function initialize()
     {
-	window.APT_OPTIONS.initialize();
+	window.APT_OPTIONS.initialize(sup);
 	ajaxurl = window.AJAXURL;
 
 	// The about panel.
diff --git a/www/aptui/js/quickvm_sup.js b/www/aptui/js/quickvm_sup.js
index 5c0f7b4a74026e1a34f85cfe4df09ebc1b853439..ad96e694339ed650b85f51da5801b6b95444ba64 100755
--- a/www/aptui/js/quickvm_sup.js
+++ b/www/aptui/js/quickvm_sup.js
@@ -17,6 +17,10 @@ function CallServerMethod(url, route, method, args)
 {
     if (url == null) {
 	url = 'https://' + window.location.host + '/apt/server-ajax.php';
+	url = 'server-ajax.php';
+    }
+    if (args == null) {
+	args = {"noargs" : "noargs"};
     }
     return $.ajax({
 	// the URL for the request
@@ -89,6 +93,105 @@ function SpitOops(id, msg)
     ShowModal(modal_name);
 }
 
+function GeniAuthenticate(cert, r1, success, failure)
+{
+    var callback = function(json) {
+	console.log('callback');
+	if (json.code) {
+	    alert("Could not generate secret: " + json.value);
+	    failure();
+	} else {
+	    console.info(json.value);
+	    success(json.value.r2_encrypted);
+	}
+    }
+    var $xmlthing = CallServerMethod(null,
+				     "geni-login", "CreateSecret",
+				     {"r1_encrypted" : r1,
+				      "certificate"  : cert});
+    $xmlthing.done(callback);
+}
+
+function GeniComplete(credential, signature)
+{
+    //console.log(credential);
+    //console.log(signature);
+    // signature is undefined if something failed before
+    VerifySpeaksfor(credential, signature);
+}
+
+var BLOB = null;
+    
+function InitGeniLogin()
+{
+    // Ask the server for the stuff we need to start and go.
+    var callback = function(json) {
+	console.info(json);
+	BLOB = json.value;
+    }
+    var $xmlthing = CallServerMethod(null, "geni-login", "GetSignerInfo", null);
+    $xmlthing.done(callback);
+}
+
+function StartGeniLogin()
+{
+    genilib.trustedHost = BLOB.HOST;
+    genilib.trustedPath = BLOB.PATH;
+    genilib.authorize({
+	id: BLOB.ID,
+	toolCertificate: BLOB.CERT,
+	complete: GeniComplete,
+	authenticate: GeniAuthenticate
+    });
+}
+
+function VerifySpeaksfor(speaksfor, signature)
+{
+    var callback = function(json) {
+	HideModal("#quickvm_login_waitwait");
+	    
+	if (json.code) {
+	    alert("Could not verify speaksfor: " + json.value);
+	    return;
+	}
+	//console.info(json.value);
+
+	//
+	// Need to set the cookies we get back so that we can
+	// redirect to the status page.
+	//
+	// Delete existing cookies first
+	var expires = "expires=Thu, 01 Jan 1970 00:00:01 GMT;";
+	document.cookie = json.value.hashname + '=; ' + expires;
+	document.cookie = json.value.crcname  + '=; ' + expires;
+	document.cookie = json.value.username + '=; ' + expires;
+	    
+	var cookie1 = 
+	    json.value.hashname + '=' + json.value.hash +
+	    '; domain=' + json.value.domain +
+	    '; max-age=' + json.value.timeout + '; path=/; secure';
+	var cookie2 =
+	    json.value.crcname + '=' + json.value.crc +
+	    '; domain=' + json.value.domain +
+	    '; max-age=' + json.value.timeout + '; path=/';
+	var cookie3 =
+	    json.value.username + '=' + json.value.user +
+	    '; domain=' + json.value.domain +
+	    '; max-age=' + json.value.timeout + '; path=/';
+
+	document.cookie = cookie1;
+	document.cookie = cookie2;
+	document.cookie = cookie3;
+	window.location.replace(json.value.url);
+    }
+    ShowModal("#quickvm_login_waitwait");
+    var $xmlthing = CallServerMethod(null,
+				     "geni-login", "VerifySpeaksfor",
+				     {"speaksfor" : speaksfor,
+				      "signature" : signature});
+    $xmlthing.done(callback);
+}
+
 // Exports from this module for use elsewhere
 return {
     ShowModal: ShowModal,
@@ -96,5 +199,7 @@ return {
     CallServerMethod: CallServerMethod,
     maketopmap: maketopmap,
     SpitOops: SpitOops,
+    StartGeniLogin: StartGeniLogin,
+    InitGeniLogin: InitGeniLogin,
 };
 });
diff --git a/www/aptui/login.php b/www/aptui/login.php
index ab630c8b216e94f5252798a603297ff19ae517ff..3ac0ed5b9151ba0dfa3ca95973d47bdd45fca01e 100644
--- a/www/aptui/login.php
+++ b/www/aptui/login.php
@@ -100,7 +100,7 @@ function SPITFORM($uid, $referrer, $error)
            <div class='panel-heading'>
               <h3 class='panel-title'>
                  Login</h3></div>
-           <div class='panel-body'>\n";
+           <div class='panel-body form-horizontal'>\n";
 
     if ($error) {
         echo "<span class='help-block'><font color=red>";
@@ -125,24 +125,38 @@ function SPITFORM($uid, $referrer, $error)
     elseif ($refer) {
         echo "<span class='help-block'>Please login before continuing</span>";
     }
-
-    echo "  <div class='form-group'>
-                <input name='uid' id='uid'
-		       value='$uid'
-                       class='form-control'
-                       placeholder='Email or Username' autofocus type='text'>
-            </div>
-            <div class='form-group'>
-                <input name='password' id='password' type='password'
-                       class='form-control'
-                       placeholder='$pwlab' type='text' />
-            </div>
-            <button class='btn btn-primary btm-sm'
-              type='submit' name='login'>Login
-            </button>\n";
     if ($referrer) {
 	echo "<input type=hidden name=referrer value=$referrer>\n";
     }
+?>
+             <div class='form-group'>
+                <label for='uid' class='col-sm-2 control-label'>Username</label>
+                <div class='col-sm-10'>
+                    <input name='uid' class='form-control'
+                           placeholder='<?php echo $pwlab ?>'
+                           autofocus type='text'>
+                </div>
+             </div>
+             <div class='form-group'>
+                <label for='password' class='col-sm-2 control-label'>Password
+					  </label>
+                <div class='col-sm-10'>
+                   <input name='password' class='form-control'
+                          placeholder='Password'
+                          type='password'>
+                </div>
+             </div>
+             <div class='form-group'>
+               <div class='col-sm-offset-2 col-sm-10'>
+                 <button class='btn btn-info btn-sm pull-left'
+		         type='button' 
+                         id='quickvm_geni_login_button'>Geni User?</button>
+                 <button class='btn btn-primary btn-sm pull-right'
+                         id='quickvm_login_modal_button'
+                         type='submit' name='login'>Login</button>
+               </div>
+             </div>
+<?php
     echo "
             <br> 
            </div>
@@ -242,7 +256,7 @@ elseif (isset($referrer)) {
 }
 else {
     if (Instance::UserHasInstances($CHECKLOGIN_USER)) {
-	header("Location: $APTBASE/myexperments.php");
+	header("Location: $APTBASE/myexperiments.php");
     }
     else {
 	header("Location: $APTBASE/instantiate.php");
diff --git a/www/aptui/quickvm_sup.php b/www/aptui/quickvm_sup.php
index 87d6119bc730c380a3ea3d07bced09d946a34b08..1fa30d4569e19a14f2450c5f6464b19d6c3de916 100644
--- a/www/aptui/quickvm_sup.php
+++ b/www/aptui/quickvm_sup.php
@@ -22,6 +22,9 @@
 # }}}
 #
 $APTHOST	= "$WWWHOST";
+# No sure why tbauth uses WWWHOST for the login cookies, but it
+# causes confusion in geni-login.ajax. 
+$COOKDIEDOMAIN  = "$WWWHOST";
 $APTBASE	= "$TBBASE/apt";
 $APTMAIL        = $TBMAIL_OPS;
 $APTTITLE       = "APT";
@@ -48,15 +51,18 @@ $disable_accounts = 0;
 if ($TBMAINSITE && $_SERVER["SERVER_NAME"] == "www.aptlab.net") {
     $ISVSERVER    = 1;
     $TBAUTHDOMAIN = ".aptlab.net";
+    $COOKDIEDOMAIN= $TBAUTHDOMAIN;
     $APTHOST      = "www.aptlab.net";
     $WWWHOST      = "www.aptlab.net";
     $APTBASE      = "https://www.aptlab.net";
     $APTMAIL      = "APT Operations <testbed-ops@aptlab.net>";
     $GOOGLEUA     = 'UA-42844769-3';
+    $TBMAILTAG    = "aptlab.net";
 }
 elseif (($TBMAINSITE && $_SERVER["SERVER_NAME"] == "www.cloudlab.us")) {
     $ISVSERVER    = 1;
     $TBAUTHDOMAIN = ".cloudlab.us";
+    $COOKDIEDOMAIN= $TBAUTHDOMAIN;
     $APTHOST      = "www.cloudlab.us";
     $WWWHOST      = "www.cloudlab.us";
     $APTBASE      = "https://www.cloudlab.us";
@@ -67,6 +73,7 @@ elseif (($TBMAINSITE && $_SERVER["SERVER_NAME"] == "www.cloudlab.us")) {
     $APTSTYLE     = "cloudlab.css";
     $ISAPT	  = 0;
     $GOOGLEUA     = 'UA-42844769-2';
+    $TBMAILTAG    = "cloudlab.us";
 }
 
 #
@@ -181,10 +188,7 @@ function SPITHEADER($thinheader = 0)
 		if ($page_title != "Login") {
 		    echo "<li id='loginitem' class='apt-left'>" .
 			   "<form><a class='btn btn-primary navbar-btn'
-                              id='loginbutton'
-	                      data-toggle='modal'
-                              href='#quickvm_login_modal'
-                              data-target='#quickvm_login_modal'>
+                              id='loginbutton'>
                             Login</a></form></li>
                           \n";
 		}
@@ -227,7 +231,10 @@ function SPITHEADER($thinheader = 0)
            </div>
          </div>\n";
 
-    SpitLoginModal("quickvm_login_modal");
+    if (!NOLOGINS() && !$login_user && $page_title != "Login") {
+	SpitLoginModal("quickvm_login_modal");
+	SpitWaitModal("quickvm_login_waitwait");
+    }
     echo " <!-- Page content -->
            <div class='container-fluid'>\n";
 }
@@ -354,16 +361,12 @@ function SpitLoginModal($id)
     global $APTTITLE, $ISAPT;
     $pwlab = ($ISAPT ? "Aptlab.net" : "CloudLab.net") .
 	" or Emulab.net Username";
-    $pwlab = "'$pwlab'";
-
+    $pwlab = "$pwlab";
+    $referrer = CleanString($_SERVER['REQUEST_URI']);
 ?>
     <!-- This is the login modal -->
     <div id='<?php echo $id ?>' class='modal fade' role='dialog'>
         <div class='modal-dialog'>
-        <form id='quickvm_login_form'
-              role='form'
-              method='post' action='login.php'>
-        <input type=hidden name=refer value=1>
         <div id='quickvm_login_form_error'
              class='align-center'></div>
         <div class='modal-content'>
@@ -372,35 +375,43 @@ function SpitLoginModal($id)
                aria-hidden='true'>&times;</button>
                <h4 class='modal-title'>Log in to <?php echo $APTTITLE ?></h4>
            </div>
+           <form id='quickvm_login_form'
+                 role='form'
+                 method='post' action='login.php'>
+           <input type=hidden name=referrer value='<?php echo $referrer ?>'>
            <div class='modal-body form-horizontal'>
              <div class='form-group'>
-                       <label for='uid' class='col-sm-2 control-label'>Username</label>
-                       <div class='col-sm-10'>
-                           <input name='uid' class='form-control'
-                                  placeholder=<?php echo $pwlab ?>
-                                  autofocus type='text'>
-                       </div>
-                   </div>
-                   <div class='form-group'>
-                       <label for='password' class='col-sm-2 control-label'>Password </label>
-                       <div class='col-sm-10'>
-                           <input name='password' class='form-control'
-                                  placeholder='Password'
-                                  type='password'>
-                       </div>
-                   </div>
+                <label for='uid' class='col-sm-2 control-label'>Username</label>
+                <div class='col-sm-10'>
+                    <input name='uid' class='form-control'
+                           placeholder='<?php echo $pwlab ?>'
+                           autofocus type='text'>
+                </div>
              </div>
-             <div class='modal-footer'>
-                   <div class='form-group'>
-                        <button class='btn btn-success btn-sm'
-                            id='quickvm_login_modal_button'
-                            class='form-control'
-                            type='submit' name='login'>
-                            Login</button>
-                   </div>
+             <div class='form-group'>
+                <label for='password' class='col-sm-2 control-label'>Password
+					  </label>
+                <div class='col-sm-10'>
+                   <input name='password' class='form-control'
+                          placeholder='Password'
+                          type='password'>
+                </div>
+             </div>
+             <div class='form-group'>
+               <div class='col-sm-offset-2 col-sm-10'>
+                 <button class='btn btn-info btn-sm pull-left' disabled
+		    type='button'
+                    data-toggle="tooltip" data-placement="left"
+		    title="You can use your geni credentials to login"
+                    id='quickvm_geni_login_button'>Geni User?</button>
+                 <button class='btn btn-primary btn-sm pull-right'
+                         id='quickvm_login_modal_button'
+                         type='submit' name='login'>Login</button>
+               </div>
              </div>
+           </div>
+           </form>
         </div>
-        </form>
         </div>
      </div>
 <?php
diff --git a/www/aptui/server-ajax.php b/www/aptui/server-ajax.php
index d3958dd86a78e709c77193bf04ab2d367859e311..4f020364ffad2877ae9f018766368f1f1757d77f 100644
--- a/www/aptui/server-ajax.php
+++ b/www/aptui/server-ajax.php
@@ -39,7 +39,9 @@ $routing = array("myprofiles" =>
 		 "geni-login" =>
 			array("file"    => "geni-login.ajax",
 			      "guest"   => true,
-			      "methods" => array("CreateSecret" =>
+			      "methods" => array("GetSignerInfo" =>
+						      "Do_GetSignerInfo",
+						 "CreateSecret" =>
 						      "Do_CreateSecret",
 						 "VerifySpeaksfor" =>
 						      "Do_VerifySpeaksfor")),
diff --git a/www/aptui/template/geni-login.html b/www/aptui/template/geni-login.html
index 4e8eaf66034a233d1da5e8352a1dca9b28ff94e3..2fd8894270ee94630b5e35fd3cdd7417e353197e 100644
--- a/www/aptui/template/geni-login.html
+++ b/www/aptui/template/geni-login.html
@@ -24,5 +24,7 @@
     <textarea id="credential" class='hidden'
 	      cols=85 rows="10">credential</textarea>
   </center>
+  <!-- place to hang the modals for now -->
+  <div id='waitwait_div'></div>
 </div>
 
diff --git a/www/dbdefs.php3.in b/www/dbdefs.php3.in
index 97bc473e4b476a8ecc07169dc8b2ab24a3fdd297..6fe698b6d7ecca67be8f4046472ebc20ffad3e86 100644
--- a/www/dbdefs.php3.in
+++ b/www/dbdefs.php3.in
@@ -1,6 +1,6 @@
 <?php
 #
-# Copyright (c) 2000-2013 University of Utah and the Flux Group.
+# Copyright (c) 2000-2014 University of Utah and the Flux Group.
 # 
 # {{{EMULAB-LICENSE
 # 
@@ -85,6 +85,7 @@ define("TBDB_NEWACCOUNT_REGULAR",	0x0);
 define("TBDB_NEWACCOUNT_PROJLEADER",	0x1);
 define("TBDB_NEWACCOUNT_WIKIONLY",	0x2);
 define("TBDB_NEWACCOUNT_WEBONLY",	0x4);
+define("TBDB_NEWACCOUNT_NONLOCAL",	0x8);
 
 #
 # Trust. Define the trust level as an increasing value. Then define a
diff --git a/www/tbauth.php3 b/www/tbauth.php3
index fa12b78b5cc585ffa201ec09ceca7f34bc3f1c51..7658941b61c33607b81db462eda7f7b5bf0de5af 100644
--- a/www/tbauth.php3
+++ b/www/tbauth.php3
@@ -958,7 +958,8 @@ function DOLOGIN($token, $password, $adminmode = 0, $nopassword = 0) {
     return DOLOGIN_STATUS_ERROR;
 }
 
-function DOLOGIN_MAGIC($uid, $uid_idx, $email = null, $adminon = 0)
+function DOLOGIN_MAGIC($uid, $uid_idx, $email = null,
+		       $adminon = 0, $nosetcookies = 0)
 {
     global $TBAUTHCOOKIE, $TBAUTHDOMAIN, $TBAUTHTIMEOUT, $WWWHOST;
     global $TBNAMECOOKIE, $TBLOGINCOOKIE, $TBSECURECOOKIES, $TBEMAILCOOKIE;
@@ -992,6 +993,11 @@ function DOLOGIN_MAGIC($uid, $uid_idx, $email = null, $adminon = 0)
 		 "  (uid,uid_idx,hashkey,hashhash,timeout,adminon,opskey) values ".
 		 "  ('$uid', $uid_idx, '$hashkey', '$crc', '$timeout', $adminon, '$opskey')");
 
+    # Does the caller just want the cookies for itself.
+    if ($nosetcookies) {
+	return array($hashkey, $crc);
+    }
+
     #
     # Issue the cookie requests so that subsequent pages come back
     # with the hash value and auth usr embedded.
diff --git a/www/user_defs.php b/www/user_defs.php
index 64864066d694c9a5cf148c36e68d90e6af027aef..63d438d1b8d29be75315815018c1cc578e319fc4 100644
--- a/www/user_defs.php
+++ b/www/user_defs.php
@@ -92,13 +92,11 @@ class User
     function LookupByUid($uid) {
 	$safe_uid = addslashes($uid);
 	$status_archived = TBDB_USERSTATUS_ARCHIVED;
-	$status_nonlocal = TBDB_USERSTATUS_NONLOCAL;
 
 	$query_result =
 	    DBQueryWarn("select uid_idx from users ".
 			"where uid='$safe_uid' and ".
-			"      status!='$status_archived' and ".
-			"      status!='$status_nonlocal'");
+			"      status!='$status_archived'");
 
 	if (!$query_result || !mysql_num_rows($query_result)) {
 	    return null;
@@ -114,13 +112,11 @@ class User
     function LookupByEmail($email) {
 	$safe_email = addslashes($email);
 	$status_archived = TBDB_USERSTATUS_ARCHIVED;
-	$status_nonlocal = TBDB_USERSTATUS_NONLOCAL;
 
 	$query_result =
 	    DBQueryWarn("select uid_idx from users ".
 			"where LCASE(usr_email)=LCASE('$safe_email') and ".
-			"      status!='$status_archived' and ".
-			"      status!='$status_nonlocal'");
+			"      status!='$status_archived'");
 
 	if (!$query_result || !mysql_num_rows($query_result)) {
 	    return null;
@@ -136,13 +132,11 @@ class User
     function LookupByWikiName($wikiname) {
 	$safe_wikiname = addslashes($wikiname);
 	$status_archived = TBDB_USERSTATUS_ARCHIVED;
-	$status_nonlocal = TBDB_USERSTATUS_NONLOCAL;
 
 	$query_result =
 	    DBQueryWarn("select uid_idx from users ".
 			"where wikiname='$safe_wikiname' and ".
-			"      status!='$status_archived' and".
-			"      status!='$status_nonlocal'");
+			"      status!='$status_archived'");
 
 	if (!$query_result || !mysql_num_rows($query_result)) {
 	    return null;
@@ -156,13 +150,29 @@ class User
     function LookupByUUID($uuid) {
 	$safe_uuid = addslashes($uuid);
 	$status_archived = TBDB_USERSTATUS_ARCHIVED;
-	$status_nonlocal = TBDB_USERSTATUS_NONLOCAL;
 
 	$query_result =
 	    DBQueryWarn("select uid_idx from users ".
 			"where uid_uuid='$safe_uuid' and ".
-			"      status!='$status_archived' and ".
-			"      status!='$status_nonlocal'");
+			"      status!='$status_archived'");
+
+	if (!$query_result || !mysql_num_rows($query_result)) {
+	    return null;
+	}
+	$row = mysql_fetch_array($query_result);
+	$idx = $row['uid_idx'];
+
+	return User::Lookup($idx);
+    }
+    
+    function LookupNonLocal($urn) {
+	$safe_urn = addslashes($urn);
+	$status_archived = TBDB_USERSTATUS_ARCHIVED;
+
+	$query_result =
+	    DBQueryWarn("select uid_idx from users ".
+			"where nonlocal_id='$safe_urn' and ".
+			"      status!='$status_archived'");
 
 	if (!$query_result || !mysql_num_rows($query_result)) {
 	    return null;
@@ -584,6 +594,8 @@ class User
 	    $typearg = "-t wikionly";
 	elseif ($flags & TBDB_NEWACCOUNT_WEBONLY)
 	    $typearg = "-t webonly";
+	elseif ($flags & TBDB_NEWACCOUNT_NONLOCAL)
+	    $typearg = "-t nonlocal";
 
 	if (! ($xmlname = User::NewNewUserXML($args, $error))) {
 	    return null;