From 340617be108642d6c6e414554efebf6e851ea850 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Feb 09 2022 12:46:38 +0000 Subject: Add support of Out Of Band authentication When the `redirect_uri` has a special value for OOB authentication, display a page prompting the user to copy and paste the code back to the application. Use the code as the page's title as well. This is useful when it's not practical for the client to run an HTTP server and use a `localhost` redirect uri, such as when it's executing on a remote machine. References: - https://github.com/googleapis/google-api-python-client/blob/main/docs/oauth-installed.md#urnietfwgoauth20oob - https://developers.google.com/identity/protocols/oauth2/native-app#step-2:-send-a-request-to-googles-oauth-2.0-server Signed-off-by: Aurélien Bompard --- diff --git a/ipsilon/providers/openidc/auth.py b/ipsilon/providers/openidc/auth.py index ada49ec..98c4539 100644 --- a/ipsilon/providers/openidc/auth.py +++ b/ipsilon/providers/openidc/auth.py @@ -8,7 +8,8 @@ from ipsilon.providers.openidc.api import (Token, TokenInfo, UserInfo) from ipsilon.providers.openidc.provider import (get_url_hostpart, - Registration) + Registration, + OOB_URL) from ipsilon.util.user import UserSession from jwcrypto.jwt import JWT @@ -65,7 +66,9 @@ class AuthenticateRequest(ProviderPageBase): url = request['redirect_uri'] response_mode = request.get('response_mode', None) response_type = request.get('response_type', []) - if 'none' in response_type: + if url == OOB_URL: + response_mode = "oob" + elif 'none' in response_type: response_mode = 'none' self.debug('none response_type, using none response_mode') elif 'id_token' in response_type or 'token' in response_type: @@ -112,6 +115,12 @@ class AuthenticateRequest(ProviderPageBase): "response_info": contents } return self._template(URLROOT + '/form_response.html', **context) + elif response_mode == "oob": + context = { + "title": urlencode(contents), + "response_info": contents + } + return self._template(URLROOT + '/oob_response.html', **context) else: raise InvalidRequest('Invalid response_mode requested') @@ -745,7 +754,7 @@ class OpenIDC(ProviderPageBase): 'response_types_supported': ['code', 'id_token', 'token', 'token id_token'], 'response_modes_supported': ['query', 'fragment', 'form_post', - 'none'], + 'oob', 'none'], 'grant_types_supported': ['authorization_code', 'implicit', 'refresh_token'], 'acr_values_supported': ['0'], diff --git a/ipsilon/providers/openidc/provider.py b/ipsilon/providers/openidc/provider.py index 70202dc..a8a0ba2 100644 --- a/ipsilon/providers/openidc/provider.py +++ b/ipsilon/providers/openidc/provider.py @@ -13,7 +13,12 @@ import six from six.moves.urllib.parse import urlparse +OOB_URL = "urn:ietf:wg:oauth:2.0:oob" + + def get_url_hostpart(url): + if url == OOB_URL: + return url try: o = urlparse(url) return o.hostname diff --git a/less/styles.less b/less/styles.less index 8f2a151..d836a2e 100644 --- a/less/styles.less +++ b/less/styles.less @@ -3499,3 +3499,6 @@ table.datatable th:active { height: 18px; padding-left: 1em; } +.ipsilon-oob pre { + font-size: 130%; +} diff --git a/templates/openidc/oob_response.html b/templates/openidc/oob_response.html new file mode 100644 index 0000000..fd75ebd --- /dev/null +++ b/templates/openidc/oob_response.html @@ -0,0 +1,14 @@ +{% extends "master.html" %} +{% block main %} +
+

You are authenticated! Please copy and paste the following code in the application:

+
+ +
+
{{ response_info|urlencode }}
+
+ +
+

You can close this browser window afterwards.

+
+{% endblock %} diff --git a/tests/helpers/http.py b/tests/helpers/http.py index 3600ca9..0aa4e18 100755 --- a/tests/helpers/http.py +++ b/tests/helpers/http.py @@ -267,6 +267,14 @@ class HttpSessions(object): return ['post', url, {'data': params}] + def get_openidc_oob(self, page): + if not isinstance(page, PageTree): + raise TypeError("Expected PageTree object") + result = page.first_value( + '//div[contains(@class, "ipsilon-oob")]/pre' + ) + return result.text + def fetch_page(self, idp, target_url, follow_redirect=True, krb=False, require_consent=None, return_prefix=None, post_forms=True): """ diff --git a/tests/openidc.py b/tests/openidc.py index ef4835b..80c52cb 100755 --- a/tests/openidc.py +++ b/tests/openidc.py @@ -497,3 +497,54 @@ if __name__ == '__main__': page = sess3.fetch_page(idpname, 'https://127.0.0.11:45081/sp/') check_text_results(page.text, 'OpenID Connect Provider error: access_denied') + + with TC.case('Set IdP authz stack to back to allow'): + sess.disable_plugin(idpname, 'authz', 'deny') + sess.enable_plugin(idpname, 'authz', 'allow') + + sess4 = HttpSessions() + sess4.add_server(idpname, 'https://127.0.0.10:45080', user, 'ipsilon') + sess4.add_server(sp1name, 'https://127.0.0.11:45081') + + with TC.case('Registering test client with OOB'): + client_info = { + 'redirect_uris': ['urn:ietf:wg:oauth:2.0:oob'], + 'response_types': ['code'], + 'grant_types': ['authorization_code'], + 'application_type': 'native', + 'client_name': 'Test suite client', + 'client_uri': 'https://invalid/', + 'token_endpoint_auth_method': 'none' + } + r = requests.post('https://127.0.0.10:45080/idp1/openidc/Registration', + json=client_info) + r.raise_for_status() + reg_resp_oob = r.json() + + with TC.case('Access first SP protected area with OOB'): + page = sess.fetch_page(idpname, + 'https://127.0.0.10:45080/idp1/openidc/' + 'Authorization?scope=openid&response_type=code&' + 'redirect_uri=urn:ietf:wg:oauth:2.0:oob&' + 'client_id=' + reg_resp_oob['client_id']) + code = sess.get_openidc_oob(page) + title_value = page.first_value('/html/head/title').text + if title_value != code: + raise Exception( + "The title of the page must contain the code as well" + ) + code = code.replace('code=', '') + # Now check that we can get a token + token_resp = requests.post( + 'https://127.0.0.10:45080/idp1/openidc/Token', + data={'client_id': reg_resp_oob['client_id'], + 'grant_type': 'authorization_code', + 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', + 'code': code}) + if token_resp.status_code != 200: + raise Exception('Unable to get token from code') + anon_token = token_resp.json() + if not anon_token.get('token_type') == 'Bearer': + raise Exception('Invalid token type returned') + if 'access_token' not in anon_token: + raise Exception('Did not get access token') diff --git a/ui/css/styles.css b/ui/css/styles.css index d46e235..ee3236c 100644 --- a/ui/css/styles.css +++ b/ui/css/styles.css @@ -3499,3 +3499,6 @@ table.datatable th:active { height: 18px; padding-left: 1em; } +.ipsilon-oob pre { + font-size: 130%; +}