Subject: Re: [9fans] OAuth2 in factotum

From: [email-masked]
Date: Sun, 22 Aug 2021 16:16:58 -0400

Quoth Demetrius Iatrakis <[email-masked]>:
> This is a preview of OAuth2 support in factotum, as part of this year's GSoC:
> https://github.com/Mitsos101/plan9front/pull/1
> 
> Installation, on 9front:
> 
> git/clone https://github.com/Mitsos101/plan9front plan9front-oauth
> cd plan9front-oauth
> git/branch oauth
> bind sys/include /sys/include
> @{cd sys/src/libauth && mk install}
> @{cd sys/src/cmd/auth && mk install}
> @{cd sys/src/cmd/webfs && mk install}
> 
> This will replace your factotum.
> 
> Usage:
> 
> You need to obtain OAuth credentials from your issuer first. See, for
> example, Google's guide:
> https://developers.google.com/identity/protocols/oauth2.
> 
> % echo 'key proto=oauth issuer=https://accounts.google.com scope=email
> client_id=1234 !client_secret=5678' > /mnt/factotum/ctl
> % auth/oauth 'client_id=1234'
> go to https://google.com/device
> your code is ABCD-EFGH
> <after user consent is provided, the access token is printed>
> 
> auth_oauth is also available in libauth. Webfs uses it to implement
> the preoauth command.
> 
> Bugs:
> 
> This code is specific to 9front, as libjson is required and Plan 9's
> webfs doesn't support preoauth.
> 
> factotum uses the needkey RPC to display the verification URL and code
> to the user. This means that, for now, the needkey file must not be
> open so that fgui doesn't intercept it.
> 
> The module imports lots of code to support HTTP/1.0 so that the
> refresh token doesn't leave factotum's address space.
> 
> Only the device and refresh flows are supported. There is an
> implementation of the authorization code flow (tested on macOS) here:
> https://github.com/Mitsos101/plan9port/pull/1. However, it is not
> included in the module as there is no good browser to plumb the URL
> to.
> 
> Refresh tokens are not saved to persistent storage when factotum
> exits. The user must provide consent every time factotum is restarted.
> 

And, now that we have something working, I wrote
some code to use it. I wrote a patch to add oauth
support to upas/fs -- see attached:

To use the patch, I followed this kind of clunky
process:

	https://developers.google.com/identity/protocols/oauth2

I went to the 'credentials' section on the sidebar
and I created a key for a 'desktop application'; Then
I went to the 'oauth consent screen' and added my work
email account as a 'test user'.

I grabbed the keys, and on my unix box, went to
the patched oauth:

	% cd $HOME/src/plan9port/src/cmd/oauth

and generated a key using the full, browser based
auth flow:

	% python httpd.py
	% ./oauth https://accounts.google.com https://mail.google.com/ $clientkey $clientsecret
	key proto=oauth issuer=https://accounts.google.com client_id=72...

then edited the resulting output to include the appropriate
attributes, adding the attributes in >>...<< for upas/fs:

	key proto=oauth
		>>service=imap server=imap.gmail.com user=[email-masked]<<
		issuer=https://accounts.google.com client_id=<id> 
		token_type=Bearer exptime=1629662303 scope=...

and then added that to factotum:

	echo key=... >/mnt/factotum/ctl

With that, upas/fs just worked with my work email:

	upas/fs -f /imaps/imap.gmail.com/[email-masked]

Bugs: there are way too many steps. Unfortunately, the most
annoying one is generating and adding an oauth client key/secret,
and short of shipping a pregenerated one (is that a good idea?),
I don't think there's a solution.

Beyond that, 2 small bits of polish which I think we
can do:

	- Adding a '-t' flag to oauth (the way auth/rsa does)
	  to add type information to auth/oauth login would
	  make it more convenient to use: the output could
	  be stored directly rather than needing editing.
	- Adding a script that allows spawning a browser and
	  http listener on unix (or redirecting thigns through
	  to plan 9) would make it easier to drive the auth
	  process from plan 9.

Thanks for doing this work, Demetrius!
diff bcfee7b54757eb64cade34e476cf0dba672832f6 uncommitted
--- a/sys/src/cmd/upas/fs/imap.c
+++ b/sys/src/cmd/upas/fs/imap.c
@@ -24,6 +24,7 @@
        Cnolog  = 1<<0,
        Ccram   = 1<<1,
        Cntlm   = 1<<2,
+       Coauth  = 1<<3,
 
        /* flags */
        Fssl    = 1<<0,
@@ -151,7 +152,7 @@
 static void
 imap4cmd(Imap *imap, char *fmt, ...)
 {
-       char buf[256], *p;
+       char buf[1024], *p;
        va_list va;
 
        va_start(va, fmt);
@@ -430,6 +431,8 @@
                                imap->cap |= Ccram;
                        if(strcmp(p, "ntlm") == 0)
                                imap->cap |= Cntlm;
+                       if(strcmp(p, "xoauth2") == 0)
+                               imap->cap |= Coauth;
                }else if(strcmp(t[i], "logindisabled") == 0)
                        imap->cap |= Cnolog;
        }
@@ -733,6 +736,38 @@
 }
 
 static char*
+imap4oauth(Imap *imap)
+{
+       char *s, *auth, *enc;
+       int n;
+       OAuth *oa;
+
+       if(imap->user == nil)
+               return "user required for oauth";
+       oa = auth_getoauth(auth_getkey, "proto=oauth service=imap server=%q user=%q", imap->host, imap->user);
+       if(oa == nil)
+               return "cannot find IMAP oauth token";
+
+       imap->tag = 1;
+       if((auth = smprint("user=%s\x01auth=Bearer %s\x01\x01", imap->user, oa->access_token)) == nil)
+               sysfatal("smprint: %r");
+       if((enc = smprint("%[", auth) == nil)
+               sysfatal("smprint: %r");
+       imap4cmd(imap, "authenticate xoauth2 %s", enc);
+       free(auth);
+       free(enc);
+       free(oa);
+       s = imap4resp(imap);
+       if(isokay(s))
+               return nil;
+       imap4cmd(imap, "");
+       s = imap4resp(imap);
+       if(isokay(s))
+               return nil;
+       return s;
+}
+
+static char*
 imap4passwd(Imap *imap)
 {
        char *s;
@@ -762,6 +797,8 @@
                e = imap4cram(imap);
        else if(imap->cap & Cntlm)
                e = imap4ntlm(imap);
+       else if(imap->cap & Coauth)
+               e = imap4oauth(imap);
        else
                e = imap4passwd(imap);
        if(e)
@@ -1165,6 +1202,7 @@
        imap->host = f[2];
        if(strstr(imap->host, "gmail.com"))
                imap->flags |= Fgmail;
+       imap->flags |= Fdebug;
        imap->refreshtime = 60;
        if(nf < 4)
                imap->user = nil;
------------------------------------------
9fans: 9fans
Permalink: https://9fans.topicbox.com/groups/9fans/T6899bf3f0654295d-M539f47f30599e6cfecb3adb9
Delivery options: https://9fans.topicbox.com/groups/9fans/subscription

---

From: Demetrius Iatrakis <[email-masked]>
Date: Sun, 22 Aug 2021 23:32:13 +0300

Thank you for your work.

Don't forget to add

type is text
dst is oauth
plumb to oauth

to your plumber's rules file to create the oauth port.

I have also added the authorization code flow to the factotum module
(can be enabled by adding `flow=auth` to the key), but it requires the
user to import the plan9port plumber into factotum's namespace. I am
unsure what the best way to do this is. Plan9port provides import(4)
which relies on SSH, but 9front does not provide an SSH server.

-- 
Demetrius

---

From: Demetrius Iatrakis <[email-masked]>
Date: Sun, 22 Aug 2021 23:32:13 +0300

Thank you for your work.

Don't forget to add

type is text
dst is oauth
plumb to oauth

-- 
Demetrius

------------------------------------------
9fans: 9fans
Permalink: https://9fans.topicbox.com/groups/9fans/T6899bf3f0654295d-M0301c221a9c86a5cd726882d
Delivery options: https://9fans.topicbox.com/groups/9fans/subscription

---

From: [email-masked]
Date: Sun, 22 Aug 2021 16:38:13 -0400

> to your plumber's rules file to create the oauth port.

Ah -- right. This is in the plan9port side, and
it's used to send the oauth verification request
to the browser.

(iirc, it's not currently used for anything on the
plan 9 side, though it wil be once we figure out
how to glue together auth flows to either vmx or
some unix browser)

---

From: [email-masked]
Date: Sun, 22 Aug 2021 16:36:17 -0400

> Thanks for doing this work, Demetrius!

Oops, realized that I'd left this line in the patch:

+	imap->flags |= Fdebug;

you'll really want to delete that, or you end up with
a *TON* of debug spew.

Updated patch attached.
 
        /* flags */
        Fssl    = 1<<0,
@@ -151,7 +152,7 @@
 static void
 
        va_start(va, fmt);
@@ -430,6 +431,8 @@
                                imap->cap |= Ccram;
 
 static char*
+imap4oauth(Imap *imap)
+{
+       char *s, *auth, *enc;
+       int n;
+       OAuth *oa;
+
+       if(imap->user == nil)
Permalink: https://9fans.topicbox.com/groups/9fans/T6899bf3f0654295d-Mbcaea13458be56f2909ea6c3

---

From: [email-masked]
Date: Sun, 22 Aug 2021 16:36:17 -0400

+	imap->flags |= Fdebug;

Updated patch attached.
 
 	/* flags */
 	Fssl	= 1<<0,
@@ -151,7 +152,7 @@
 static void
 
 	va_start(va, fmt);
@@ -430,6 +431,8 @@
 				imap->cap |= Ccram;
 
 static char*
+imap4oauth(Imap *imap)
+{
+	char *s, *auth, *enc;
+	int n;
+	OAuth *oa;
+
+	if(imap->user == nil)

---

From: [email-masked]
Date: Sun, 22 Aug 2021 16:38:13 -0400

------------------------------------------
9fans: 9fans
Permalink: https://9fans.topicbox.com/groups/9fans/T6899bf3f0654295d-Mf920328ca00963c2708ecd92
Delivery options: https://9fans.topicbox.com/groups/9fans/subscription

---

From: [email-masked]
Date: Sun, 22 Aug 2021 16:16:58 -0400

% python httpd.py

	key proto=oauth

 
 	/* flags */
 	Fssl	= 1<<0,
@@ -151,7 +152,7 @@
 static void
 
 	va_start(va, fmt);
@@ -430,6 +431,8 @@
 				imap->cap |= Ccram;
 
 static char*
+imap4oauth(Imap *imap)
+{
+	char *s, *auth, *enc;
+	int n;
+	OAuth *oa;
+
+	if(imap->user == nil)