def test_pkce_token(): kb = KeyBundle(JWKS["keys"]) kj = KeyJar() kj.issuer_keys[""] = [kb] constructor = JWTToken( "A", keyjar=kj, lt_pattern={"": 900}, iss="https://example.com/as", sign_alg="RS256", encrypt=True, ) sid = rndstr(32) session_info = { "sub": "subject_id", "client_id": "https://example.com/rp", "response_type": ["code"], "authzreq": "{}", } _cli = Client(config={"code_challenge": {"method": "S512", "length": 96}}) args, cv = _cli.add_code_challenge() access_grant = constructor( sid, sinfo=session_info, kid="sign1", code_challenge=args["code_challenge"], code_challenge_method=args["code_challenge_method"], ) _info = constructor.get_info(access_grant) assert _info["code_challenge_method"] == args["code_challenge_method"] assert _info["code_challenge"] == args["code_challenge"]
def test_pkce_token(): kb = KeyBundle(JWKS["keys"]) kj = KeyJar() kj.issuer_keys[''] = [kb] constructor = JWTToken('A', keyjar=kj, lt_pattern={'': 900}, iss='https://example.com/as', sign_alg='RS256', encrypt=True) sid = rndstr(32) session_info = { 'sub': 'subject_id', 'client_id': 'https://example.com/rp', 'response_type': ['code'], 'authzreq': '{}' } _cli = Client(config={'code_challenge': {'method': 'S512', 'length': 96}}) args, cv = _cli.add_code_challenge() access_grant = constructor( sid, sinfo=session_info, kid='sign1', code_challenge=args['code_challenge'], code_challenge_method=args['code_challenge_method']) _info = constructor.get_info(access_grant) assert _info['code_challenge_method'] == args['code_challenge_method'] assert _info['code_challenge'] == args['code_challenge']
def test_authenticated(self): client = Client(**CLIENT_CONFIG) client.authorization_endpoint = 'https://example.com/as' sid = rndstr(8) args = { 'redirect_uri': "http://localhost:8087/authz", "state": sid, "response_type": 'code' } url, body, ht_args, csi = client.request_info(AuthorizationRequest, 'GET', request_args=args) resp = self.provider.authorization_endpoint(urlparse(url).query) assert resp.status_code == 303 resp = urlparse(resp.message).query aresp = client.parse_authz_response(resp) assert isinstance(aresp, AuthorizationResponse) assert _eq(aresp.keys(), ['state', 'code', 'client_id', 'iss']) assert _eq(client.grant[sid].keys(), [ 'tokens', 'code', 'exp_in', 'seed', 'id_token', 'grant_expiration_time' ])
def test_authenticated(self): client = Client(**CLIENT_CONFIG) client.authorization_endpoint = "https://example.com/as" sid = rndstr(8) args = { "redirect_uri": "http://localhost:8087/authz", "state": sid, "response_type": "code", } url, body, ht_args, csi = client.request_info( AuthorizationRequest, "GET", request_args=args ) resp = self.provider.authorization_endpoint(urlparse(url).query) assert resp.status_code == 303 resp = urlparse(resp.message).query aresp = client.parse_authz_response(resp) assert isinstance(aresp, AuthorizationResponse) assert _eq(aresp.keys(), ["state", "code", "client_id", "iss"]) assert _eq( client.grant[sid].keys(), ["tokens", "code", "exp_in", "seed", "id_token", "grant_expiration_time"], )
def test_pkce_verify_512(): _cli = Client(config={'code_challenge': {'method': 'S512', 'length': 96}}) args, cv = _cli.add_code_challenge() authn_broker = AuthnBroker() authn_broker.add("UNDEFINED", DummyAuthn(None, "username")) _prov = Provider("as", sdb.SessionDB(SERVER_INFO["issuer"]), CDB, authn_broker, Implicit(), verify_client) assert _prov.verify_code_challenge(cv, args['code_challenge'],'S512') is True
def test_pkce_verify_512(): _cli = Client(config={'code_challenge': {'method': 'S512', 'length': 96}}) args, cv = _cli.add_code_challenge() authn_broker = AuthnBroker() authn_broker.add("UNDEFINED", DummyAuthn(None, "username")) _prov = Provider("as", sdb.SessionDB(SERVER_INFO["issuer"]), CDB, authn_broker, Implicit(), verify_client) assert _prov.verify_code_challenge(cv, args['code_challenge'], 'S512') is True
def test_pkce_verify_512(session_db_factory): _cli = Client(config={'code_challenge': {'method': 'S512', 'length': 96}}) args, cv = _cli.add_code_challenge() authn_broker = AuthnBroker() authn_broker.add("UNDEFINED", DummyAuthn(None, "username")) _prov = Provider("as", session_db_factory(SERVER_INFO["issuer"]), CDB, authn_broker, Implicit(), verify_client) assert _prov.verify_code_challenge(cv, args['code_challenge'], 'S512') is True resp = _prov.verify_code_challenge('XXX', args['code_challenge']) assert isinstance(resp, Response) assert resp.info()['status_code'] == 401
def test_authenticated_token(self): client = Client(**CLIENT_CONFIG) client.authorization_endpoint = "https://example.com/as" sid = rndstr(8) args = {"redirect_uri": "http://localhost:8087/authz", "state": sid, "response_type": "token"} url, body, ht_args, csi = client.request_info(AuthorizationRequest, "GET", request_args=args) QUERY_STRING = url.split("?")[1] resp = self.provider.authorization_endpoint(QUERY_STRING) auth_resp = parse_qs(urlparse(resp.message).fragment) assert "access_token" in auth_resp assert auth_resp["token_type"][0] == "Bearer"
def __init__(self, provider_configuration, redirect_uri): """ Args: provider_configuration (flask_pyoidc.provider_configuration.ProviderConfiguration) """ self._provider_configuration = provider_configuration self._client = Client(client_authn_method=CLIENT_AUTHN_METHOD, settings=provider_configuration.client_settings) # Token Introspection is implemented under extension sub-package of # the client in pyoidc. self._client_extension = ClientExtension( client_authn_method=CLIENT_AUTHN_METHOD, settings=provider_configuration.client_settings) # Client Credentials Flow is implemented under oauth2 sub-package of # the client in pyoidc. self._oauth2_client = Oauth2Client( client_authn_method=CLIENT_AUTHN_METHOD, message_factory=CCMessageFactory, settings=self._provider_configuration.client_settings) provider_metadata = provider_configuration.ensure_provider_metadata( self._client) self._client.handle_provider_config( ProviderConfigurationResponse(**provider_metadata.to_dict()), provider_metadata['issuer']) if self._provider_configuration.registered_client_metadata: client_metadata = self._provider_configuration.registered_client_metadata.to_dict( ) client_metadata.update(redirect_uris=list(redirect_uri)) self._store_registration_info(client_metadata) self._redirect_uri = redirect_uri
def test_authenticated(self): client = Client(**CLIENT_CONFIG) client.authorization_endpoint = "https://example.com/as" sid = rndstr(8) args = {"redirect_uri": "http://localhost:8087/authz", "state": sid, "response_type": "code"} url, body, ht_args, csi = client.request_info(AuthorizationRequest, "GET", request_args=args) resp = self.provider.authorization_endpoint(urlparse(url).query) assert resp.status == "302 Found" resp = urlparse(resp.message).query aresp = client.parse_authz_response(resp) assert isinstance(aresp, AuthorizationResponse) assert _eq(aresp.keys(), ["state", "code", "client_id", "iss"]) assert _eq(client.grant[sid].keys(), ["tokens", "code", "exp_in", "seed", "id_token", "grant_expiration_time"])
def test_authenticated_token(self): client = Client(**CLIENT_CONFIG) client.authorization_endpoint = 'https://example.com/as' sid = rndstr(8) args = {'redirect_uri': "http://localhost:8087/authz", "state": sid, "response_type": 'token'} url, body, ht_args, csi = client.request_info(AuthorizationRequest, 'GET', request_args=args) QUERY_STRING = url.split("?")[1] resp = self.provider.authorization_endpoint(QUERY_STRING) auth_resp = parse_qs(urlparse(resp.message).fragment) assert "access_token" in auth_resp assert auth_resp["token_type"][0] == "Bearer"
def test_pkce_verify_512(session_db_factory): _cli = Client(config={"code_challenge": {"method": "S512", "length": 96}}) args, cv = _cli.add_code_challenge() authn_broker = AuthnBroker() authn_broker.add("UNDEFINED", DummyAuthn(None, "username")) _prov = Provider( "as", session_db_factory(SERVER_INFO["issuer"]), CDB, authn_broker, Implicit(), verify_client, ) assert _prov.verify_code_challenge(cv, args["code_challenge"], "S512") is True resp = _prov.verify_code_challenge("XXX", args["code_challenge"]) assert isinstance(resp, Response) assert resp.info()["status_code"] == 401
def test_authenticated(self): client = Client(**CLIENT_CONFIG) client.authorization_endpoint = 'https://example.com/as' sid = rndstr(8) args = { 'redirect_uri': "http://localhost:8087/authz", "state": sid, "response_type": 'code'} url, body, ht_args, csi = client.request_info( AuthorizationRequest, 'GET', request_args=args) resp = self.provider.authorization_endpoint(urlparse(url).query) assert resp.status == "303 See Other" resp = urlparse(resp.message).query aresp = client.parse_authz_response(resp) assert isinstance(aresp, AuthorizationResponse) assert _eq(aresp.keys(), ['state', 'code', 'client_id', 'iss']) assert _eq(client.grant[sid].keys(), ['tokens', 'code', 'exp_in', 'seed', 'id_token', 'grant_expiration_time'])
def test_pkce_create(): _cli = Client(config={'code_challenge': {'method': 'S256', 'length': 64}}) args, cv = _cli.add_code_challenge() assert args['code_challenge_method'] == 'S256' assert _eq(list(args.keys()), ['code_challenge_method', 'code_challenge'])
class PyoidcFacade: """ Wrapper around pyoidc library, coupled with config for a simplified API for flask-pyoidc. """ def __init__(self, provider_configuration, redirect_uri): """ Args: provider_configuration (flask_pyoidc.provider_configuration.ProviderConfiguration) """ self._provider_configuration = provider_configuration self._client = Client(client_authn_method=CLIENT_AUTHN_METHOD, settings=provider_configuration.client_settings) # Token Introspection is implemented under extension sub-package of # the client in pyoidc. self._client_extension = ClientExtension( client_authn_method=CLIENT_AUTHN_METHOD, settings=provider_configuration.client_settings) # Client Credentials Flow is implemented under oauth2 sub-package of # the client in pyoidc. self._oauth2_client = Oauth2Client( client_authn_method=CLIENT_AUTHN_METHOD, message_factory=CCMessageFactory, settings=self._provider_configuration.client_settings) provider_metadata = provider_configuration.ensure_provider_metadata( self._client) self._client.handle_provider_config( ProviderConfigurationResponse(**provider_metadata.to_dict()), provider_metadata['issuer']) if self._provider_configuration.registered_client_metadata: client_metadata = self._provider_configuration.registered_client_metadata.to_dict( ) client_metadata.update(redirect_uris=list(redirect_uri)) self._store_registration_info(client_metadata) self._redirect_uri = redirect_uri def _store_registration_info(self, client_metadata): registration_response = RegistrationResponse(**client_metadata) self._client.store_registration_info(registration_response) self._client_extension.store_registration_info(registration_response) # Set client_id and client_secret for _oauth2_client. This is used # by Client Credentials Flow. self._oauth2_client.client_id = registration_response['client_id'] self._oauth2_client.client_secret = registration_response[ 'client_secret'] def is_registered(self): return bool(self._provider_configuration.registered_client_metadata) def register(self): client_metadata = self._provider_configuration.register_client( self._client) logger.debug(f'client registration response: {client_metadata}') self._store_registration_info(client_metadata) def authentication_request(self, state, nonce, extra_auth_params): """ Args: state (str): authentication request parameter 'state' nonce (str): authentication request parameter 'nonce' extra_auth_params (Mapping[str, str]): extra authentication request parameters Returns: AuthorizationRequest: the authentication request """ args = { 'client_id': self._client.client_id, 'response_type': 'code', 'scope': ['openid'], 'redirect_uri': self._redirect_uri, 'state': state, 'nonce': nonce, } args.update(self._provider_configuration.auth_request_params) args.update(extra_auth_params) auth_request = self._client.construct_AuthorizationRequest( request_args=args) logger.debug('sending authentication request: %s', auth_request.to_json()) return auth_request def login_url(self, auth_request): """ Args: auth_request (AuthorizationRequest): authentication request Returns: str: Authentication request as a URL to redirect the user to the provider. """ return auth_request.request(self._client.authorization_endpoint) def parse_authentication_response(self, response_params): """ Parameters ---------- response_params: Mapping[str, str] authentication response parameters. Returns ------- Union[AuthorizationResponse, AuthorizationErrorResponse] The parsed authorization response. """ auth_resp = self._client.parse_response(AuthorizationResponse, info=response_params, sformat='dict') if 'id_token' in response_params: auth_resp['id_token_jwt'] = response_params['id_token'] return auth_resp def exchange_authorization_code(self, authorization_code: str, state: str): """Requests tokens from an authorization code. Parameters ---------- authorization_code: str authorization code issued to client after user authorization state: str state is used to keep track of responses to outstanding requests. Returns ------- Union[AccessTokenResponse, TokenErrorResponse, None] The parsed token response, or None if no token request was performed. """ if not self._client.token_endpoint: return None request_args = { 'grant_type': 'authorization_code', 'code': authorization_code, 'redirect_uri': self._redirect_uri } logger.debug('making token request: %s', request_args) client_auth_method = self._client.registration_response.get( 'token_endpoint_auth_method', 'client_secret_basic') token_response = self._client.do_access_token_request( state=state, request_args=request_args, authn_method=client_auth_method, endpoint=self._client.token_endpoint) logger.info('Received token response.') return token_response def verify_id_token(self, id_token, auth_request): """ Verifies the ID Token. Args: id_token (Mapping[str, str]): ID token claims auth_request (Mapping[str, str]): original authentication request parameters to validate against (nonce, acr_values, max_age, etc.) Raises: PyoidcError: If the ID token is invalid. """ self._client.verify_id_token(id_token, auth_request) def refresh_token(self, refresh_token: str): """Requests new tokens using a refresh token. Parameters ---------- refresh_token: str refresh token issued to client after user authorization. Returns ------- Union[AccessTokenResponse, TokenErrorResponse, None] The parsed token response, or None if no token request was performed. """ request_args = { 'grant_type': 'refresh_token', 'refresh_token': refresh_token, 'redirect_uri': self._redirect_uri } client_auth_method = self._client.registration_response.get( 'token_endpoint_auth_method', 'client_secret_basic') return self._client.do_access_token_refresh( request_args=request_args, authn_method=client_auth_method, token=Token(resp={'refresh_token': refresh_token}), endpoint=self._client.token_endpoint) def userinfo_request(self, access_token: str): """Retrieves ID token. Parameters ---------- access_token: str Bearer access token to use when fetching userinfo. Returns ------- Union[OpenIDSchema, UserInfoErrorResponse, ErrorResponse, None] """ http_method = self._provider_configuration.userinfo_endpoint_method if not access_token or http_method is None or not self._client.userinfo_endpoint: return None logger.debug('making userinfo request') userinfo_response = self._client.do_user_info_request( method=http_method, token=access_token) logger.debug('received userinfo response: %s', userinfo_response) return userinfo_response def _token_introspection_request( self, access_token: str) -> TokenIntrospectionResponse: """Make token introspection request. Parameters ---------- access_token: str Access token to be validated. Returns ------- TokenIntrospectionResponse Response object contains result of the token introspection. """ request_args = { 'token': access_token, 'token_type_hint': 'access_token' } client_auth_method = self._client.registration_response.get( 'introspection_endpoint_auth_method', 'client_secret_basic') logger.info('making token introspection request') token_introspection_response = self._client_extension.do_token_introspection( request_args=request_args, authn_method=client_auth_method, endpoint=self._client.introspection_endpoint) return token_introspection_response def client_credentials_grant(self, scope: list = None, **kwargs) -> AccessTokenResponse: """Public method to request access_token using client_credentials flow. This is useful for service to service communication where user-agent is not available which is required in authorization code flow. Your service can request access_token in order to access APIs of other services. On API call, token introspection will ensure that only valid token can be used to access your APIs. Parameters ---------- scope: list, optional List of scopes to be requested. **kwargs : dict, optional Extra arguments to client credentials flow. Returns ------- AccessTokenResponse Examples -------- :: auth = OIDCAuthentication({'default': provider_config}, access_token_required=True) auth.init_app(app) auth.clients['default'].client_credentials_grant() Optionally, you can specify scopes for the access token. :: auth.clients['default'].client_credentials_grant( scope=['read', 'write']) You can also specify extra keyword arguments to client credentials flow. :: auth.clients['default'].client_credentials_grant( scope=['read', 'write'], audience=['client_id1', 'client_id2']) """ request_args = {'grant_type': 'client_credentials', **kwargs} if scope: request_args['scope'] = ' '.join(scope) client_auth_method = self._client.registration_response.get( 'token_endpoint_auth_method', 'client_secret_basic') access_token = self._oauth2_client.do_access_token_request( request_args=request_args, authn_method=client_auth_method, endpoint=self._client.token_endpoint) return access_token @property def session_refresh_interval_seconds(self): return self._provider_configuration.session_refresh_interval_seconds @property def provider_end_session_endpoint(self): provider_metadata = self._provider_configuration.ensure_provider_metadata( self._client) return provider_metadata.get('end_session_endpoint') @property def post_logout_redirect_uris(self): return self._client.registration_response.get( 'post_logout_redirect_uris')
def test_pkce_create(): _cli = Client(config={"code_challenge": {"method": "S256", "length": 64}}) args, cv = _cli.add_code_challenge() assert args["code_challenge_method"] == "S256" assert _eq(list(args.keys()), ["code_challenge_method", "code_challenge"])