def init_app(self,
                 provider_metadata_extras=None,
                 client_metadata_extras=None,
                 **kwargs):
        required_provider_metadata = {
            'issuer': self.PROVIDER_BASEURL,
            'authorization_endpoint': self.PROVIDER_BASEURL + '/auth',
            'jwks_uri': self.PROVIDER_BASEURL + '/jwks'
        }
        if provider_metadata_extras:
            required_provider_metadata.update(provider_metadata_extras)
        provider_metadata = ProviderMetadata(**required_provider_metadata)

        required_client_metadata = {
            'client_id': self.CLIENT_ID,
            'client_secret': 'secret1'
        }
        if client_metadata_extras:
            required_client_metadata.update(client_metadata_extras)
        client_metadata = ClientMetadata(**required_client_metadata)

        provider_configurations = {
            self.PROVIDER_NAME:
            ProviderConfiguration(provider_metadata=provider_metadata,
                                  client_metadata=client_metadata,
                                  **kwargs)
        }
        authn = OIDCAuthentication(provider_configurations)
        authn.init_app(self.app)
        return authn
Esempio n. 2
0
 def provider_metadata(self):
     return ProviderMetadata(
         issuer="https://{DOMAIN}/".format(DOMAIN=self.oidc_config.OIDC_DOMAIN,),
         authorization_endpoint=self.oidc_config.auth_endpoint(),
         token_endpoint=self.oidc_config.token_endpoint(),
         userinfo_endpoint=self.oidc_config.userinfo_endpoint(),
     )
Esempio n. 3
0
    def init_app(self,
                 provider_metadata_extras=None,
                 client_metadata_extras=None,
                 **kwargs):
        required_provider_metadata = {
            "issuer": self.PROVIDER_BASEURL,
            "authorization_endpoint": self.PROVIDER_BASEURL + "/auth",
            "jwks_uri": self.PROVIDER_BASEURL + "/jwks",
        }
        if provider_metadata_extras:
            required_provider_metadata.update(provider_metadata_extras)
        provider_metadata = ProviderMetadata(**required_provider_metadata)

        required_client_metadata = {
            "client_id": self.CLIENT_ID,
            "client_secret": "secret1"
        }
        if client_metadata_extras:
            required_client_metadata.update(client_metadata_extras)
        client_metadata = ClientMetadata(**required_client_metadata)

        provider_configurations = {
            self.PROVIDER_NAME:
            ProviderConfiguration(provider_metadata=provider_metadata,
                                  client_metadata=client_metadata,
                                  **kwargs)
        }
        authn = OIDCAuthentication(provider_configurations)
        authn.init_app(self.app)
        return authn
    def test_should_register_client_if_not_registered_before(
            self, post_logout_redirect_uris):
        registration_endpoint = self.PROVIDER_BASEURL + '/register'
        provider_metadata = ProviderMetadata(
            self.PROVIDER_BASEURL,
            self.PROVIDER_BASEURL + '/auth',
            self.PROVIDER_BASEURL + '/jwks',
            registration_endpoint=registration_endpoint)
        client_metadata = {}
        if post_logout_redirect_uris:
            client_metadata[
                'post_logout_redirect_uris'] = post_logout_redirect_uris
        provider_configurations = {
            self.PROVIDER_NAME:
            ProviderConfiguration(
                provider_metadata=provider_metadata,
                client_registration_info=ClientRegistrationInfo(
                    **client_metadata))
        }
        authn = OIDCAuthentication(provider_configurations)
        authn.init_app(self.app)

        # register logout view to force 'post_logout_redirect_uris' to be included in registration request
        logout_view_mock = self.get_view_mock()
        self.app.add_url_rule('/logout', view_func=logout_view_mock)
        authn.oidc_logout(logout_view_mock)

        responses.add(responses.POST,
                      registration_endpoint,
                      json={
                          'client_id': 'client1',
                          'client_secret': 'secret1'
                      })
        view_mock = self.get_view_mock()
        with self.app.test_request_context('/'):
            auth_redirect = authn.oidc_auth(self.PROVIDER_NAME)(view_mock)()

        self.assert_auth_redirect(auth_redirect)
        registration_request = json.loads(
            responses.calls[0].request.body.decode('utf-8'))
        with self.app.app_context():
            full_redirect_uri = flask.url_for(
                registration_request['redirect_uris'][0])
            registration_request['redirect_uris'] = full_redirect_uri

        expected_post_logout_redirect_uris = post_logout_redirect_uris if post_logout_redirect_uris else [
            'http://{}/logout'.format(self.CLIENT_DOMAIN)
        ]
        expected_registration_request = {
            'redirect_uris':
            'http://{}/redirect_uri'.format(self.CLIENT_DOMAIN),
            'post_logout_redirect_uris': expected_post_logout_redirect_uris
        }
        assert registration_request == expected_registration_request
 def test_should_detect_nonce_mismatch(self, client_mock):
     client = PyoidcFacade(
         ProviderConfiguration(
             provider_metadata=ProviderMetadata(issuer=self.ISSUER),
             client_metadata=ClientMetadata(client_id=self.CLIENT_ID)),
         redirect_uri='https://client.example.com/redirect')
     client.exchange_authorization_code = MagicMock(
         return_value=self.TOKEN_RESPONSE)
     auth_request = {
         'state': self.AUTH_RESPONSE['state'],
         'nonce': 'other_nonce'
     }
     with pytest.raises(InvalidIdTokenError):
         AuthResponseHandler(client).process_auth_response(
             self.AUTH_RESPONSE, auth_request)
Esempio n. 6
0
def get_provider_metadata(login_hint_token, mccmnc):
    client_id = os.getenv('CLIENT_ID')
    d_url = os.getenv('DISCOVERY_URL')
    url = '%s?config=true&client_id=%s&login_hint_token=%s&mccmnc=%s' % (
        d_url, client_id, login_hint_token, mccmnc)
    response = requests.get(url)
    response_json = response.json()
    if (response_json == {} or response_json['issuer'] == None):
        return None

    metadata = ProviderMetadata(
        issuer=response_json['issuer'],
        authorization_endpoint=response_json['authorization_endpoint'],
        jwks_uri=response_json['jwks_uri'],
        token_endpoint=response_json['token_endpoint'],
        userinfo_endpoint=response_json['userinfo_endpoint'])
    return metadata
Esempio n. 7
0
def create_app(config=None):
    app = Flask(__name__, instance_relative_config=True)

    # Load application config from various sources
    # ------------------------------------------------------------------------------
    # 1. Defaults from this package
    app.config.from_object(DefaultConfig)

    # 2. From a config.py file in the application directory
    app.config.from_pyfile(filename=os.path.join(os.getcwd(), "config.py"),
                           silent=True)

    # 3. From a dynamically configurable file location
    if os.environ.get('CONFIG_LOCATION'):
        app.config.from_pyfile(filename=os.environ.get('CONFIG_LOCATION'),
                               silent=False)

    # 4. Testing config
    if config:
        app.config.from_mapping(config)

    # 5. Load some final computed config
    #    NOTE: This is placed here as it relies on other config values that
    #          may be configured after the user provided config for example.
    oidc_logout_redirect_uri = os.environ.get(
        'OIDC_LOGOUT_REDIRECT_URI',
        'https://' + app.config['SERVER_NAME'] + '/logout')
    oidc_auth_request_params = json.loads(
        os.environ.get('OIDC_AUTH_REQUEST_PARAMS', '{}'))
    if oidc_auth_request_params:
        if app.config['OIDC_EXTRA_AUTH_REQUEST_PARAMS']:
            app.logger.warning(
                'OIDC_EXTRA_AUTH_REQUEST_PARAMS is being overridden by OIDC_AUTH_REQUEST_PARAMS being explicitly set.'
            )
    else:
        oidc_auth_request_params['scope'] = " ".join(
            re.split(",| ", app.config['OIDC_SCOPE']))
        if app.config['OIDC_EXTRA_AUTH_REQUEST_PARAMS']:
            oidc_auth_request_params.update(
                app.config['OIDC_EXTRA_AUTH_REQUEST_PARAMS'])

    app.config.from_mapping({
        'OIDC_LOGOUT_REDIRECT_URI':
        oidc_logout_redirect_uri,
        'OIDC_CLIENT_METADATA': {
            'client_id': app.config['OIDC_CLIENT_ID'],
            'client_secret': app.config['OIDC_CLIENT_SECRET'],
            'post_logout_redirect_uris': str.split(oidc_logout_redirect_uri,
                                                   ",")
        },
        'OIDC_AUTH_REQUEST_PARAMS':
        oidc_auth_request_params,
    })

    # Initialize OpenID Connect extension
    # ------------------------------------------------------------------------------

    # The client metadata will be consumed no matter what...
    # https://github.com/zamzterz/Flask-pyoidc#dynamic-provider-configuration
    client_metadata = ClientMetadata(**app.config['OIDC_CLIENT_METADATA'])

    # ... but if explicit OIDC provider information is provided, we use that
    # instead of the information dynamically provided by the
    # .well-known/openid-configuration endpoint.
    if app.config['OIDC_PROVIDER_METADATA']:
        provider_metadata = ProviderMetadata(
            **app.config['OIDC_PROVIDER_METADATA'])
        provider = ProviderConfiguration(
            provider_metadata=provider_metadata,
            client_metadata=client_metadata,
            auth_request_params=app.config['OIDC_AUTH_REQUEST_PARAMS'],
        )
    else:
        provider = ProviderConfiguration(
            issuer=app.config['OIDC_ISSUER'],
            client_metadata=client_metadata,
            auth_request_params=app.config['OIDC_AUTH_REQUEST_PARAMS'],
        )

    auth = OIDCAuthentication(
        provider_configurations={
            'default': provider,
        },
        app=app,
    )

    # The /health endpoint returns a JSON string like...
    # {"hostname": "a3731af16461", "status": "success", "timestamp": 1551186453.8854501, "results": []}
    health = HealthCheck(app, "/health")

    # If an OAuth error response is received, either in the authentication or
    # token response, it will be passed to the "error view".
    @auth.error_view
    def error(error=None, error_description=None):
        return jsonify({'error': error, 'message': error_description})

    @app.route('/')
    def index():
        """
        If a user tries to access this application directly,
        just redirect them to Discourse.
        :return: Redirect to the configurated DISCOURSE_URL
        """
        return redirect(app.config.get('DISCOURSE_URL'), 302)

    @app.route('/sso/login')
    def payload_check():
        """
        Verify the payload and signature coming from a Discourse server and if
        correct redirect to the authentication page after saving the nonce in
        the session as discourse_nonce.
        
        :return: The redirection page to the authentication page
        """

        # Get payload and signature from Discourse request
        payload = request.args.get('sso', '')
        signature = request.args.get('sig', '')

        if not payload or not signature:
            app.logger.info(
                '/sso/login -> 400: missing payload="%s" or signature="%s"',
                payload, signature)
            abort(400)

        app.logger.debug('Request to login with payload="%s" signature="%s"',
                         payload, signature)
        app.logger.debug('Session Secret Key: %s', app.secret_key)
        app.logger.debug('SSO Secret Key: %s',
                         app.config.get('DISCOURSE_SECRET_KEY'))

        # Calculate and compare request signature
        dig = hmac.new(
            app.config.get('DISCOURSE_SECRET_KEY', '').encode('utf-8'),
            payload.encode('utf-8'), hashlib.sha256).hexdigest()
        app.logger.debug('Calculated hash: %s', dig)

        if dig != signature:
            app.logger.info(
                '/sso/login -> 400: dig / signature mismatch. dig="%s" and signature="%s"',
                dig, signature)
            abort(400)

        # Decode the payload and store in session
        decoded_msg = base64.b64decode(payload).decode('utf-8')
        session[
            'discourse_nonce'] = decoded_msg  # This can't just be 'nonce' as Flask-pyoidc will steamroll it

        # Redirect to authorization endpoint
        return redirect(url_for('sso_auth'))

    @app.route('/sso/auth')
    @auth.oidc_auth('default')
    def sso_auth():
        """
        Read the user attributes provided by Flask-pyoidc and
        create the payload to send to Discourse.
        :return: The redirection page to Discourse
        """

        # Check to make sure we have a valid session
        if 'discourse_nonce' not in session:
            app.logger.info(
                '/sso/auth -> 403: discourse_nonce not found in session, arriving here without coming from /sso/login?'
            )
            abort(403)

        attribute_map = app.config.get('USERINFO_SSO_MAP')

        sso_attributes = {}
        userinfo = session['userinfo']

        # Check if the provided userinfo should be used to set information to be
        # passed to discourse. Do it by checking if the userinfo field is...
        # 1. explicitly mapped using the provided map
        # 2. if it can match one of the known attributes with discourse_ prefixed
        # 3. if it can match one of the known attributes directly
        for userinfo_key, userinfo_value in userinfo.items():
            attribute_key = attribute_map.get(userinfo_key)

            if attribute_key:
                pass
            elif userinfo_key in [
                    "discourse_" + attr for attr in ALL_ATTRIBUTES
            ]:
                attribute_key = userinfo_key[len("discourse_"):]
            elif userinfo_key in ALL_ATTRIBUTES:
                attribute_key = userinfo_key

            if attribute_key:
                if attribute_key in BOOL_ATTRIBUTES:
                    userinfo_value = "false" if str.lower(
                        str(userinfo_value)) in ['false', 'f', '0'] else "true"
                sso_attributes[attribute_key] = userinfo_value

        # Check if we have a default value that should be used
        default_sso_attributes = app.config.get('DEFAULT_SSO_ATTRIBUTES')
        for default_attribute_key, default_attribute_value in default_sso_attributes.items(
        ):
            if default_attribute_key not in sso_attributes:
                sso_attributes[default_attribute_key] = default_attribute_value

        # Check if we got the required attributes
        for required_attribute in REQUIRED_ATTRIBUTES:
            if not sso_attributes.get(required_attribute):
                app.logger.info(
                    f'/sso/auth -> 403: {required_attribute} not found in userinfo: '
                    + json.dumps(session['userinfo']))
                abort(403)

        # All systems are go!
        app.logger.debug(
            f'Authenticating "{sso_attributes.get("external_id")}", named "{sso_attributes.get("name")}" with email: "{sso_attributes.get("email")}"'
        )

        # Construct the response inner query parameters
        query = session['discourse_nonce']
        for sso_attribute_key, sso_attribute_value in sso_attributes.items():
            query += f'&{sso_attribute_key}={quote(str(sso_attribute_value))}'
        app.logger.debug('Query string to return: %s', query)

        # Encode response
        query_b64 = base64.b64encode(query.encode('utf-8'))
        app.logger.debug('Base64 query string to return: %s', query_b64)

        # Build URL-safe response
        query_urlenc = quote(query_b64)
        app.logger.debug('URLEnc query string to return: %s', query_urlenc)

        # Generate signature for response
        sig = hmac.new(
            app.config.get('DISCOURSE_SECRET_KEY').encode('utf-8'),
            query_b64,
            hashlib.sha256,
        ).hexdigest()

        app.logger.debug('Signature: %s', sig)

        # Build redirect URL
        redirect_url = (app.config.get('DISCOURSE_URL') + '/session/sso_login?'
                        'sso=' + query_urlenc + '&sig=' + sig)

        # Redirect back to Discourse
        return redirect(redirect_url)

    @app.route('/logout')
    @auth.oidc_logout
    def logout():
        """
        Handle logging a user out. Flask-pyoidc does the heavy lifting here.
        :return: Redirect to the application index
        """
        return redirect(url_for('index'), 302)

    @app.errorhandler(403)
    def attribute_not_provided(error):
        """
        Render a custom error page in case the IdP authenticate the user but does
        not provide the requested attributes
        :type error: object
        """
        app.logger.info(f'403: error: "{error}"')
        return render_template('403.html'), 403

    return app
Esempio n. 8
0
class TestPyoidcFacade(object):
    PROVIDER_BASEURL = "http://rp.example.com"
    PROVIDER_METADATA = ProviderMetadata(PROVIDER_BASEURL,
                                         PROVIDER_BASEURL + "/auth",
                                         PROVIDER_BASEURL + "/jwks")
    CLIENT_METADATA = ClientMetadata("client1", "secret1")
    CLIENT_DOMAIN = "client.example.com"
    REDIRECT_URI = "redirect_uri"
    FULL_REDIRECT_URI = "http://client.example.com/redirect_uri"

    @pytest.fixture(autouse=True)
    def create_flask_app(self):
        self.app = Flask(__name__)
        self.app.add_url_rule("/redirect_uri", "redirect_uri")
        self.app.config.update({
            "SERVER_NAME": self.CLIENT_DOMAIN,
            "SECRET_KEY": "test_key"
        })

    def test_registered_client_metadata_is_forwarded_to_pyoidc(self):
        config = ProviderConfiguration(
            provider_metadata=self.PROVIDER_METADATA,
            client_metadata=self.CLIENT_METADATA,
        )
        facade = PyoidcFacade(config, self.REDIRECT_URI)
        assert facade._client.registration_response

    def test_no_registered_client_metadata_is_handled(self):
        config = ProviderConfiguration(
            provider_metadata=self.PROVIDER_METADATA,
            client_registration_info=ClientRegistrationInfo(),
        )
        facade = PyoidcFacade(config, self.REDIRECT_URI)
        assert not facade._client.registration_response

    def test_is_registered(self):
        unregistered = ProviderConfiguration(
            provider_metadata=self.PROVIDER_METADATA,
            client_registration_info=ClientRegistrationInfo(),
        )
        registered = ProviderConfiguration(
            provider_metadata=self.PROVIDER_METADATA,
            client_metadata=self.CLIENT_METADATA,
        )
        assert PyoidcFacade(unregistered,
                            self.REDIRECT_URI).is_registered() is False
        assert PyoidcFacade(registered,
                            self.REDIRECT_URI).is_registered() is True

    @responses.activate
    def test_register(self):
        registration_endpoint = self.PROVIDER_BASEURL + "/register"
        responses.add(responses.POST,
                      registration_endpoint,
                      json=self.CLIENT_METADATA.to_dict())

        provider_metadata = self.PROVIDER_METADATA.copy(
            registration_endpoint=registration_endpoint)
        unregistered = ProviderConfiguration(
            provider_metadata=provider_metadata,
            client_registration_info=ClientRegistrationInfo(),
        )
        facade = PyoidcFacade(unregistered, self.REDIRECT_URI)
        facade.register()
        assert facade.is_registered() is True

    def test_authentication_request(self):
        extra_user_auth_params = {"foo": "bar", "abc": "xyz"}
        config = ProviderConfiguration(
            provider_metadata=self.PROVIDER_METADATA,
            client_metadata=self.CLIENT_METADATA,
            auth_request_params=extra_user_auth_params,
        )

        state = "test_state"
        nonce = "test_nonce"

        facade = PyoidcFacade(config, self.REDIRECT_URI)
        extra_lib_auth_params = {"foo": "baz", "qwe": "rty"}
        auth_request = facade.authentication_request(state, nonce,
                                                     self.FULL_REDIRECT_URI,
                                                     extra_lib_auth_params)
        assert auth_request.startswith(
            self.PROVIDER_METADATA["authorization_endpoint"])

        auth_request_params = dict(parse_qsl(urlparse(auth_request).query))
        expected_auth_params = {
            "scope": "openid",
            "response_type": "code",
            "client_id": self.CLIENT_METADATA["client_id"],
            "redirect_uri": self.FULL_REDIRECT_URI,
            "state": state,
            "nonce": nonce,
        }
        expected_auth_params.update(extra_user_auth_params)
        expected_auth_params.update(extra_lib_auth_params)
        assert auth_request_params == expected_auth_params

    def test_parse_authentication_response(self):
        facade = PyoidcFacade(
            ProviderConfiguration(
                provider_metadata=self.PROVIDER_METADATA,
                client_metadata=self.CLIENT_METADATA,
            ),
            self.REDIRECT_URI,
        )
        auth_code = "auth_code-1234"
        state = "state-1234"
        auth_response = AuthorizationResponse(**{
            "state": state,
            "code": auth_code
        })
        parsed_auth_response = facade.parse_authentication_response(
            auth_response.to_dict())
        assert isinstance(parsed_auth_response, AuthorizationResponse)
        assert parsed_auth_response.to_dict() == auth_response.to_dict()

    def test_parse_authentication_response_handles_error_response(self):
        facade = PyoidcFacade(
            ProviderConfiguration(
                provider_metadata=self.PROVIDER_METADATA,
                client_metadata=self.CLIENT_METADATA,
            ),
            self.REDIRECT_URI,
        )
        error_response = AuthorizationErrorResponse(**{
            "error": "invalid_request",
            "state": "state-1234"
        })
        parsed_auth_response = facade.parse_authentication_response(
            error_response)
        assert isinstance(parsed_auth_response, AuthorizationErrorResponse)
        assert parsed_auth_response.to_dict() == error_response.to_dict()

    @responses.activate
    def test_parse_authentication_response_preserves_id_token_jwt(self):
        facade = PyoidcFacade(
            ProviderConfiguration(
                provider_metadata=self.PROVIDER_METADATA,
                client_metadata=self.CLIENT_METADATA,
            ),
            self.REDIRECT_URI,
        )
        state = "state-1234"
        now = int(time.time())
        id_token, id_token_signing_key = signed_id_token({
            "iss":
            self.PROVIDER_METADATA["issuer"],
            "sub":
            "test_sub",
            "aud":
            "client1",
            "exp":
            now + 1,
            "iat":
            now,
        })
        responses.add(
            responses.GET,
            self.PROVIDER_METADATA["jwks_uri"],
            json={"keys": [id_token_signing_key.serialize()]},
        )
        auth_response = AuthorizationResponse(**{
            "state": state,
            "id_token": id_token
        })
        parsed_auth_response = facade.parse_authentication_response(
            auth_response)
        assert isinstance(parsed_auth_response, AuthorizationResponse)
        assert parsed_auth_response["state"] == state
        assert parsed_auth_response["id_token_jwt"] == id_token

    @responses.activate
    def test_token_request(self):
        token_endpoint = self.PROVIDER_BASEURL + "/token"
        now = int(time.time())
        id_token_claims = {
            "iss": self.PROVIDER_METADATA["issuer"],
            "sub": "test_user",
            "aud": [self.CLIENT_METADATA["client_id"]],
            "exp": now + 1,
            "iat": now,
            "nonce": "test_nonce",
        }
        id_token_jwt, id_token_signing_key = signed_id_token(id_token_claims)
        token_response = AccessTokenResponse(access_token="test_access_token",
                                             token_type="Bearer",
                                             id_token=id_token_jwt)
        responses.add(responses.POST,
                      token_endpoint,
                      json=token_response.to_dict())

        provider_metadata = self.PROVIDER_METADATA.copy(
            token_endpoint=token_endpoint)
        facade = PyoidcFacade(
            ProviderConfiguration(
                provider_metadata=provider_metadata,
                client_metadata=self.CLIENT_METADATA,
            ),
            self.REDIRECT_URI,
        )

        auth_code = "auth_code-1234"
        responses.add(
            responses.GET,
            self.PROVIDER_METADATA["jwks_uri"],
            json={"keys": [id_token_signing_key.serialize()]},
        )
        with self.app.app_context():
            token_response = facade.token_request(auth_code)

        assert isinstance(token_response, AccessTokenResponse)
        expected_token_response = token_response.to_dict()
        expected_token_response["id_token"] = id_token_claims
        expected_token_response["id_token_jwt"] = id_token_jwt
        assert token_response.to_dict() == expected_token_response

        token_request = dict(parse_qsl(responses.calls[0].request.body))
        expected_token_request = {
            "grant_type": "authorization_code",
            "code": auth_code,
            "redirect_uri": self.FULL_REDIRECT_URI
        }
        assert token_request == expected_token_request

    @responses.activate
    def test_token_request_handles_error_response(self):
        token_endpoint = self.PROVIDER_BASEURL + "/token"
        token_response = TokenErrorResponse(
            error="invalid_request",
            error_description="test error description")
        responses.add(responses.POST,
                      token_endpoint,
                      json=token_response.to_dict(),
                      status=400)

        provider_metadata = self.PROVIDER_METADATA.copy(
            token_endpoint=token_endpoint)
        facade = PyoidcFacade(
            ProviderConfiguration(
                provider_metadata=provider_metadata,
                client_metadata=self.CLIENT_METADATA,
            ),
            self.REDIRECT_URI,
        )

        with self.app.app_context():
            assert facade.token_request("1234") == token_response

    def test_token_request_handles_missing_provider_token_endpoint(self):
        facade = PyoidcFacade(
            ProviderConfiguration(
                provider_metadata=self.PROVIDER_METADATA,
                client_metadata=self.CLIENT_METADATA,
            ),
            self.REDIRECT_URI,
        )
        assert facade.token_request("1234") is None

    @pytest.mark.parametrize("userinfo_http_method", ["GET", "POST"])
    @responses.activate
    def test_configurable_userinfo_endpoint_method_is_used(
            self, userinfo_http_method):
        userinfo_endpoint = self.PROVIDER_BASEURL + "/userinfo"
        userinfo_response = OpenIDSchema(sub="user1")
        responses.add(userinfo_http_method,
                      userinfo_endpoint,
                      json=userinfo_response.to_dict())

        provider_metadata = self.PROVIDER_METADATA.copy(
            userinfo_endpoint=userinfo_endpoint)
        facade = PyoidcFacade(
            ProviderConfiguration(
                provider_metadata=provider_metadata,
                client_metadata=self.CLIENT_METADATA,
                userinfo_http_method=userinfo_http_method,
            ),
            self.REDIRECT_URI,
        )
        assert facade.userinfo_request("test_token") == userinfo_response

    def test_no_userinfo_request_is_made_if_no_userinfo_http_method_is_configured(
            self):
        facade = PyoidcFacade(
            ProviderConfiguration(
                provider_metadata=self.PROVIDER_METADATA,
                client_metadata=self.CLIENT_METADATA,
                userinfo_http_method=None,
            ),
            self.REDIRECT_URI,
        )
        assert facade.userinfo_request("test_token") is None

    def test_no_userinfo_request_is_made_if_no_userinfo_endpoint_is_configured(
            self):
        facade = PyoidcFacade(
            ProviderConfiguration(
                provider_metadata=self.PROVIDER_METADATA,
                client_metadata=self.CLIENT_METADATA,
            ),
            self.REDIRECT_URI,
        )
        assert facade.userinfo_request("test_token") is None

    def test_no_userinfo_request_is_made_if_no_access_token(self):
        provider_metadata = self.PROVIDER_METADATA.copy(
            userinfo_endpoint=self.PROVIDER_BASEURL + "/userinfo")
        facade = PyoidcFacade(
            ProviderConfiguration(
                provider_metadata=provider_metadata,
                client_metadata=self.CLIENT_METADATA,
            ),
            self.REDIRECT_URI,
        )
        assert facade.userinfo_request(None) is None
class TestPyoidcFacade(object):
    PROVIDER_BASEURL = 'https://op.example.com'
    PROVIDER_METADATA = ProviderMetadata(PROVIDER_BASEURL,
                                         PROVIDER_BASEURL + '/auth',
                                         PROVIDER_BASEURL + '/jwks')
    CLIENT_METADATA = ClientMetadata('client1', 'secret1')

    def test_registered_client_metadata_is_forwarded_to_pyoidc(self):
        config = ProviderConfiguration(
            provider_metadata=self.PROVIDER_METADATA,
            client_metadata=self.CLIENT_METADATA)
        facade = PyoidcFacade(config, REDIRECT_URI)
        assert facade._client.registration_response

    def test_no_registered_client_metadata_is_handled(self):
        config = ProviderConfiguration(
            provider_metadata=self.PROVIDER_METADATA,
            client_registration_info=ClientRegistrationInfo())
        facade = PyoidcFacade(config, REDIRECT_URI)
        assert not facade._client.registration_response

    def test_is_registered(self):
        unregistered = ProviderConfiguration(
            provider_metadata=self.PROVIDER_METADATA,
            client_registration_info=ClientRegistrationInfo())
        registered = ProviderConfiguration(
            provider_metadata=self.PROVIDER_METADATA,
            client_metadata=self.CLIENT_METADATA)
        assert PyoidcFacade(unregistered,
                            REDIRECT_URI).is_registered() is False
        assert PyoidcFacade(registered, REDIRECT_URI).is_registered() is True

    @responses.activate
    def test_register(self):
        registration_endpoint = self.PROVIDER_BASEURL + '/register'
        responses.add(responses.POST,
                      registration_endpoint,
                      json=self.CLIENT_METADATA.to_dict())

        provider_metadata = self.PROVIDER_METADATA.copy(
            registration_endpoint=registration_endpoint)
        unregistered = ProviderConfiguration(
            provider_metadata=provider_metadata,
            client_registration_info=ClientRegistrationInfo())
        facade = PyoidcFacade(unregistered, REDIRECT_URI)
        facade.register()
        assert facade.is_registered() is True

    def test_authentication_request(self):
        extra_user_auth_params = {'foo': 'bar', 'abc': 'xyz'}
        config = ProviderConfiguration(
            provider_metadata=self.PROVIDER_METADATA,
            client_metadata=self.CLIENT_METADATA,
            auth_request_params=extra_user_auth_params)

        state = 'test_state'
        nonce = 'test_nonce'

        facade = PyoidcFacade(config, REDIRECT_URI)
        extra_lib_auth_params = {'foo': 'baz', 'qwe': 'rty'}
        auth_request = facade.authentication_request(state, nonce,
                                                     extra_lib_auth_params)
        assert auth_request.startswith(
            self.PROVIDER_METADATA['authorization_endpoint'])

        auth_request_params = dict(parse_qsl(urlparse(auth_request).query))
        expected_auth_params = {
            'scope': 'openid',
            'response_type': 'code',
            'client_id': self.CLIENT_METADATA['client_id'],
            'redirect_uri': REDIRECT_URI,
            'state': state,
            'nonce': nonce
        }
        expected_auth_params.update(extra_user_auth_params)
        expected_auth_params.update(extra_lib_auth_params)
        assert auth_request_params == expected_auth_params

    def test_parse_authentication_response(self):
        facade = PyoidcFacade(
            ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA,
                                  client_metadata=self.CLIENT_METADATA),
            REDIRECT_URI)
        auth_code = 'auth_code-1234'
        state = 'state-1234'
        auth_response = AuthorizationResponse(**{
            'state': state,
            'code': auth_code
        })
        parsed_auth_response = facade.parse_authentication_response(
            auth_response.to_dict())
        assert isinstance(parsed_auth_response, AuthorizationResponse)
        assert parsed_auth_response.to_dict() == auth_response.to_dict()

    def test_parse_authentication_response_handles_error_response(self):
        facade = PyoidcFacade(
            ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA,
                                  client_metadata=self.CLIENT_METADATA),
            REDIRECT_URI)
        error_response = AuthorizationErrorResponse(**{
            'error': 'invalid_request',
            'state': 'state-1234'
        })
        parsed_auth_response = facade.parse_authentication_response(
            error_response)
        assert isinstance(parsed_auth_response, AuthorizationErrorResponse)
        assert parsed_auth_response.to_dict() == error_response.to_dict()

    @responses.activate
    def test_parse_authentication_response_preserves_id_token_jwt(self):
        facade = PyoidcFacade(
            ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA,
                                  client_metadata=self.CLIENT_METADATA),
            REDIRECT_URI)
        state = 'state-1234'
        now = int(time.time())
        id_token, id_token_signing_key = signed_id_token({
            'iss':
            self.PROVIDER_METADATA['issuer'],
            'sub':
            'test_sub',
            'aud':
            'client1',
            'exp':
            now + 1,
            'iat':
            now
        })
        responses.add(responses.GET,
                      self.PROVIDER_METADATA['jwks_uri'],
                      json={'keys': [id_token_signing_key.serialize()]})
        auth_response = AuthorizationResponse(**{
            'state': state,
            'id_token': id_token
        })
        parsed_auth_response = facade.parse_authentication_response(
            auth_response)
        assert isinstance(parsed_auth_response, AuthorizationResponse)
        assert parsed_auth_response['state'] == state
        assert parsed_auth_response['id_token_jwt'] == id_token

    @pytest.mark.parametrize(
        'request_func,expected_token_request',
        [(lambda facade: facade.exchange_authorization_code('auth-code'), {
            'grant_type': 'authorization_code',
            'code': 'auth-code',
            'redirect_uri': REDIRECT_URI
        }),
         (lambda facade: facade.refresh_token('refresh-token'), {
             'grant_type': 'refresh_token',
             'refresh_token': 'refresh-token',
             'redirect_uri': REDIRECT_URI
         })])
    @responses.activate
    def test_token_request(self, request_func, expected_token_request):
        token_endpoint = self.PROVIDER_BASEURL + '/token'
        now = int(time.time())
        id_token_claims = {
            'iss': self.PROVIDER_METADATA['issuer'],
            'sub': 'test_user',
            'aud': [self.CLIENT_METADATA['client_id']],
            'exp': now + 1,
            'iat': now,
            'nonce': 'test_nonce'
        }
        id_token_jwt, id_token_signing_key = signed_id_token(id_token_claims)
        token_response = AccessTokenResponse(access_token='test_access_token',
                                             token_type='Bearer',
                                             id_token=id_token_jwt)
        responses.add(responses.POST,
                      token_endpoint,
                      json=token_response.to_dict())

        provider_metadata = self.PROVIDER_METADATA.copy(
            token_endpoint=token_endpoint)
        facade = PyoidcFacade(
            ProviderConfiguration(provider_metadata=provider_metadata,
                                  client_metadata=self.CLIENT_METADATA),
            REDIRECT_URI)

        responses.add(responses.GET,
                      self.PROVIDER_METADATA['jwks_uri'],
                      json={'keys': [id_token_signing_key.serialize()]})
        token_response = request_func(facade)

        assert isinstance(token_response, AccessTokenResponse)
        expected_token_response = token_response.to_dict()
        expected_token_response['id_token'] = id_token_claims
        expected_token_response['id_token_jwt'] = id_token_jwt
        assert token_response.to_dict() == expected_token_response

        token_request = dict(parse_qsl(responses.calls[0].request.body))
        assert token_request == expected_token_request

    @responses.activate
    def test_token_request_handles_error_response(self):
        token_endpoint = self.PROVIDER_BASEURL + '/token'
        token_response = TokenErrorResponse(
            error='invalid_request',
            error_description='test error description')
        responses.add(responses.POST,
                      token_endpoint,
                      json=token_response.to_dict(),
                      status=400)

        provider_metadata = self.PROVIDER_METADATA.copy(
            token_endpoint=token_endpoint)
        facade = PyoidcFacade(
            ProviderConfiguration(provider_metadata=provider_metadata,
                                  client_metadata=self.CLIENT_METADATA),
            REDIRECT_URI)
        assert facade.exchange_authorization_code('1234') == token_response

    def test_token_request_handles_missing_provider_token_endpoint(self):
        facade = PyoidcFacade(
            ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA,
                                  client_metadata=self.CLIENT_METADATA),
            REDIRECT_URI)
        assert facade.exchange_authorization_code('1234') is None

    @pytest.mark.parametrize('userinfo_http_method', ['GET', 'POST'])
    @responses.activate
    def test_configurable_userinfo_endpoint_method_is_used(
            self, userinfo_http_method):
        userinfo_endpoint = self.PROVIDER_BASEURL + '/userinfo'
        userinfo_response = OpenIDSchema(sub='user1')
        responses.add(userinfo_http_method,
                      userinfo_endpoint,
                      json=userinfo_response.to_dict())

        provider_metadata = self.PROVIDER_METADATA.copy(
            userinfo_endpoint=userinfo_endpoint)
        facade = PyoidcFacade(
            ProviderConfiguration(provider_metadata=provider_metadata,
                                  client_metadata=self.CLIENT_METADATA,
                                  userinfo_http_method=userinfo_http_method),
            REDIRECT_URI)
        assert facade.userinfo_request('test_token') == userinfo_response

    def test_no_userinfo_request_is_made_if_no_userinfo_http_method_is_configured(
            self):
        facade = PyoidcFacade(
            ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA,
                                  client_metadata=self.CLIENT_METADATA,
                                  userinfo_http_method=None), REDIRECT_URI)
        assert facade.userinfo_request('test_token') is None

    def test_no_userinfo_request_is_made_if_no_userinfo_endpoint_is_configured(
            self):
        facade = PyoidcFacade(
            ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA,
                                  client_metadata=self.CLIENT_METADATA),
            REDIRECT_URI)
        assert facade.userinfo_request('test_token') is None

    def test_no_userinfo_request_is_made_if_no_access_token(self):
        provider_metadata = self.PROVIDER_METADATA.copy(
            userinfo_endpoint=self.PROVIDER_BASEURL + '/userinfo')
        facade = PyoidcFacade(
            ProviderConfiguration(provider_metadata=provider_metadata,
                                  client_metadata=self.CLIENT_METADATA),
            REDIRECT_URI)
        assert facade.userinfo_request(None) is None
Esempio n. 10
0
if app.config.get("AUTH_ENABLED") == "OIDC":
    app.logger.info("AUTHENTICATION ENABLED WITH %s" %
                    app.config.get("AUTH_ENABLED"))

    if not os.environ.get("GPM_OIDC_REDIRECT_DOMAIN"):
        app.logger.error(
            "Authentication is enabled with OIDC but GPM_OIDC_REDIRECT_DOMAIN environment variable has not been set."
        )

    provider_metadata = ProviderMetadata(
        issuer=os.environ.get("GPM_OIDC_ISSUER"),
        authorization_endpoint=os.environ.get(
            "GPM_OIDC_AUTHORIZATION_ENDPOINT"),
        jwks_uri=os.environ.get("GPM_OIDC_JWKS_URI"),
        token_endpoint=os.environ.get("GPM_OIDC_TOKEN_ENDPOINT"),
        token_introspection_endpoint=os.environ.get(
            "GPM_OIDC_INTROSPECTION_ENDPOINT"),
        userinfo_endpoint=os.environ.get("GPM_OIDC_USERINFO_ENDPOINT"),
        end_session_endpoint=os.environ.get("GPM_OIDC_END_SESSION_ENDPOINT"),
    )

    provider_config = ProviderConfiguration(
        issuer=os.environ.get("GPM_OIDC_ISSUER"),
        provider_metadata=provider_metadata,
        session_refresh_interval_seconds=10,
        client_metadata=ClientMetadata(
            client_id=os.environ.get("GPM_OIDC_CLIENT_ID"),
            client_secret=os.environ.get("GPM_OIDC_CLIENT_SECRET"),
        ),
    )
Esempio n. 11
0
from cognitodemo.access import AwsAccesser
from cognitodemo.mfa import get_mfa_challenge, verify_mfa_challenge, user_has_software_token_mfa

app = Flask(__name__)

app.config.from_envvar('COGNITO_DEMO_SETTINGS')
app.config.update({
    'OIDC_REDIRECT_URI': 'http://localhost:5000/redirect_uri',
    'DEBUG': True
})

issuer = f'https://{app.config["PROVIDER_NAME"]}'
cognito_config = ProviderConfiguration(
    provider_metadata=ProviderMetadata(
        issuer=issuer,
        authorization_endpoint=f'{app.config["COGNITO_URL"]}/oauth2/authorize',
        jwks_uri=f'{issuer}/.well-known/jwks.json',
        token_endpoint=f'{app.config["COGNITO_URL"]}/oauth2/token',
    ),
    client_metadata=ClientMetadata(app.config["CLIENT_ID"],
                                   app.config["CLIENT_SECRET"]),
    auth_request_params={
        'scope': ['openid', 'aws.cognito.signin.user.admin'
                  ]  # scope required to update MFA for logged-in user
    })
auth = OIDCAuthentication({'cognito': cognito_config})

aws_accesser = AwsAccesser(app.config['AWS_ACCOUNT_ID'],
                           app.config['IDENTITY_POOL_ID'],
                           app.config['PROVIDER_NAME'])

Esempio n. 12
0
 def provider_metadata(self, **kwargs):
     return ProviderMetadata(issuer='',
                             authorization_endpoint='',
                             jwks_uri='',
                             **kwargs)