Repository: Kinto/kinto

Issue:
prompt=none not carried for silent authentication
The [documentation](https://auth0.com/docs/api-auth/tutorials/silent-authentication) is more complex that it needs to be. 

To authenticate you visit `/v1/openid/auth0/login?callback=http...&scopes=...` it redirects to the OIDC provider (e.g. `https://peterbecom.auth0.com/authorize?client_id=...&response_type=code&scope=openid+email+profile&redirect_uri=...&state=...`) which then redirects you back to the `callback` (if all goes well with the password check) with a code that kinto trades in for an access token which it then appends to the `callback` URL.

Now, if that access token expires (or is *about to* expire!) we need to authenticate again. The default expiration time for the access token (at least with Auth0) is 1 day. To do that, you can simply add `&prompt=none` to the URL. E.g. `https://peterbecom.auth0.com/authorize?client_id=...&response_type=code&scope=openid+email+profile&redirect_uri=...&state=...&prompt=none`. What the OIDC provider does is...

> "Silent authentication lets you perform an authentication flow where Auth0 will only reply with redirects, and never with a login page."

It's basically the same URL as authorization but with `&prompt=none` added. 
prompt=none not carried for silent authentication
The [documentation](https://auth0.com/docs/api-auth/tutorials/silent-authentication) is more complex that it needs to be. 

To authenticate you visit `/v1/openid/auth0/login?callback=http...&scopes=...` it redirects to the OIDC provider (e.g. `https://peterbecom.auth0.com/authorize?client_id=...&response_type=code&scope=openid+email+profile&redirect_uri=...&state=...`) which then redirects you back to the `callback` (if all goes well with the password check) with a code that kinto trades in for an access token which it then appends to the `callback` URL.

Now, if that access token expires (or is *about to* expire!) we need to authenticate again. The default expiration time for the access token (at least with Auth0) is 1 day. To do that, you can simply add `&prompt=none` to the URL. E.g. `https://peterbecom.auth0.com/authorize?client_id=...&response_type=code&scope=openid+email+profile&redirect_uri=...&state=...&prompt=none`. What the OIDC provider does is...

> "Silent authentication lets you perform an authentication flow where Auth0 will only reply with redirects, and never with a login page."

It's basically the same URL as authorization but with `&prompt=none` added. 


Discussion:

Resolving patch:
diff --git a/kinto/plugins/openid/views.py b/kinto/plugins/openid/views.py
--- a/kinto/plugins/openid/views.py
+++ b/kinto/plugins/openid/views.py
@@ -51,6 +51,9 @@ class LoginQuerystringSchema(colander.MappingSchema):
     """
     callback = URL()
     scope = colander.SchemaNode(colander.String())
+    prompt = colander.SchemaNode(colander.String(),
+                                 validator=colander.Regex("none"),
+                                 missing=colander.drop)
 
 
 class LoginSchema(colander.MappingSchema):
@@ -86,6 +89,7 @@ def get_login(request):
 
     scope = request.GET['scope']
     callback = request.GET['callback']
+    prompt = request.GET.get('prompt')
 
     # Check that email scope is requested if userid field is configured as email.
     if userid_field == 'email' and 'email' not in scope:
@@ -105,6 +109,9 @@ def get_login(request):
     token_uri = request.route_url('openid_token', provider=provider) + '?'
     params = dict(client_id=client_id, response_type='code', scope=scope,
                   redirect_uri=token_uri, state=state)
+    if prompt:
+        # The 'prompt' parameter is optional.
+        params['prompt'] = prompt
     redirect = '{}?{}'.format(auth_endpoint, urllib.parse.urlencode(params))
     raise httpexceptions.HTTPTemporaryRedirect(redirect)
 


Test patch:
diff --git a/tests/plugins/test_openid.py b/tests/plugins/test_openid.py
--- a/tests/plugins/test_openid.py
+++ b/tests/plugins/test_openid.py
@@ -191,6 +191,13 @@ def test_returns_400_if_email_is_not_in_scope_when_userid_field_is_email(self):
         self.app.get('/openid/google/login', params={'callback': cb, 'scope': scope},
                      status=400)
 
+    def test_returns_400_if_prompt_is_not_recognized(self):
+        scope = 'openid'
+        cb = 'http://ui'
+        self.app.get('/openid/auth0/login',
+                     params={'callback': cb, 'scope': scope, 'prompt': 'junk'},
+                     status=400)
+
     def test_redirects_to_the_identity_provider(self):
         params = {'callback': 'http://ui', 'scope': 'openid'}
         resp = self.app.get('/openid/auth0/login', params=params, status=307)
@@ -200,6 +207,16 @@ def test_redirects_to_the_identity_provider(self):
         assert 'scope=openid' in location
         assert 'client_id=abc' in location
 
+    def test_redirects_to_the_identity_provider_with_prompt_none(self):
+        params = {'callback': 'http://ui', 'scope': 'openid', 'prompt': 'none'}
+        resp = self.app.get('/openid/auth0/login', params=params, status=307)
+        location = resp.headers['Location']
+        assert 'auth0.com/authorize?' in location
+        assert '%2Fv1%2Fopenid%2Fauth0%2Ftoken' in location
+        assert 'scope=openid' in location
+        assert 'client_id=abc' in location
+        assert 'prompt=none' in location
+
     def test_callback_is_stored_in_cache(self):
         params = {'callback': 'http://ui', 'scope': 'openid'}
         with mock.patch('kinto.plugins.openid.views.random_bytes_hex') as m: