Skip to content

Commit 3b82059

Browse files
authored
OIDCProviderConfig create/update APIs (#443)
1 parent e9174e2 commit 3b82059

File tree

5 files changed

+329
-0
lines changed

5 files changed

+329
-0
lines changed

firebase_admin/_auth_client.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,57 @@ def get_oidc_provider_config(self, provider_id):
406406
"""
407407
return self._provider_manager.get_oidc_provider_config(provider_id)
408408

409+
def create_oidc_provider_config(
410+
self, provider_id, client_id, issuer, display_name=None, enabled=None):
411+
"""Creates a new OIDC provider config from the given parameters.
412+
413+
OIDC provider support requires Google Cloud's Identity Platform (GCIP). To learn more about
414+
GCIP, including pricing and features, see https://cloud.google.com/identity-platform.
415+
416+
Args:
417+
provider_id: Provider ID string. Must have the prefix ``oidc.``.
418+
client_id: Client ID of the new config.
419+
issuer: Issuer of the new config. Must be a valid URL.
420+
display_name: The user-friendly display name to the current configuration (optional).
421+
This name is also used as the provider label in the Cloud Console.
422+
enabled: A boolean indicating whether the provider configuration is enabled or disabled
423+
(optional). A user cannot sign in using a disabled provider.
424+
425+
Returns:
426+
OIDCProviderConfig: The newly created OIDCProviderConfig instance.
427+
428+
Raises:
429+
ValueError: If any of the specified input parameters are invalid.
430+
FirebaseError: If an error occurs while creating the new OIDC provider config.
431+
"""
432+
return self._provider_manager.create_oidc_provider_config(
433+
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
434+
enabled=enabled)
435+
436+
def update_oidc_provider_config(
437+
self, provider_id, client_id=None, issuer=None, display_name=None, enabled=None):
438+
"""Updates an existing OIDC provider config with the given parameters.
439+
440+
Args:
441+
provider_id: Provider ID string. Must have the prefix ``oidc.``.
442+
client_id: Client ID of the new config (optional).
443+
issuer: Issuer of the new config (optional). Must be a valid URL.
444+
display_name: The user-friendly display name to the current configuration (optional).
445+
Pass ``auth.DELETE_ATTRIBUTE`` to delete the current display name.
446+
enabled: A boolean indicating whether the provider configuration is enabled or disabled
447+
(optional).
448+
449+
Returns:
450+
OIDCProviderConfig: The updated OIDCProviderConfig instance.
451+
452+
Raises:
453+
ValueError: If any of the specified input parameters are invalid.
454+
FirebaseError: If an error occurs while updating the OIDC provider config.
455+
"""
456+
return self._provider_manager.update_oidc_provider_config(
457+
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
458+
enabled=enabled)
459+
409460
def delete_oidc_provider_config(self, provider_id):
410461
"""Deletes the OIDCProviderConfig with the given ID.
411462

firebase_admin/_auth_providers.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,49 @@ def get_oidc_provider_config(self, provider_id):
170170
body = self._make_request('get', '/oauthIdpConfigs/{0}'.format(provider_id))
171171
return OIDCProviderConfig(body)
172172

173+
def create_oidc_provider_config(
174+
self, provider_id, client_id, issuer, display_name=None, enabled=None):
175+
"""Creates a new OIDC provider config from the given parameters."""
176+
_validate_oidc_provider_id(provider_id)
177+
req = {
178+
'clientId': _validate_non_empty_string(client_id, 'client_id'),
179+
'issuer': _validate_url(issuer, 'issuer'),
180+
}
181+
if display_name is not None:
182+
req['displayName'] = _auth_utils.validate_string(display_name, 'display_name')
183+
if enabled is not None:
184+
req['enabled'] = _auth_utils.validate_boolean(enabled, 'enabled')
185+
186+
params = 'oauthIdpConfigId={0}'.format(provider_id)
187+
body = self._make_request('post', '/oauthIdpConfigs', json=req, params=params)
188+
return OIDCProviderConfig(body)
189+
190+
def update_oidc_provider_config(
191+
self, provider_id, client_id=None, issuer=None, display_name=None, enabled=None):
192+
"""Updates an existing OIDC provider config with the given parameters."""
193+
_validate_oidc_provider_id(provider_id)
194+
req = {}
195+
if display_name is not None:
196+
if display_name == _user_mgt.DELETE_ATTRIBUTE:
197+
req['displayName'] = None
198+
else:
199+
req['displayName'] = _auth_utils.validate_string(display_name, 'display_name')
200+
if enabled is not None:
201+
req['enabled'] = _auth_utils.validate_boolean(enabled, 'enabled')
202+
if client_id:
203+
req['clientId'] = _validate_non_empty_string(client_id, 'client_id')
204+
if issuer:
205+
req['issuer'] = _validate_url(issuer, 'issuer')
206+
207+
if not req:
208+
raise ValueError('At least one parameter must be specified for update.')
209+
210+
update_mask = _auth_utils.build_update_mask(req)
211+
params = 'updateMask={0}'.format(','.join(update_mask))
212+
url = '/oauthIdpConfigs/{0}'.format(provider_id)
213+
body = self._make_request('patch', url, json=req, params=params)
214+
return OIDCProviderConfig(body)
215+
173216
def delete_oidc_provider_config(self, provider_id):
174217
_validate_oidc_provider_id(provider_id)
175218
self._make_request('delete', '/oauthIdpConfigs/{0}'.format(provider_id))

firebase_admin/auth.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,62 @@ def get_oidc_provider_config(provider_id, app=None):
564564
client = _get_client(app)
565565
return client.get_oidc_provider_config(provider_id)
566566

567+
def create_oidc_provider_config(
568+
provider_id, client_id, issuer, display_name=None, enabled=None, app=None):
569+
"""Creates a new OIDC provider config from the given parameters.
570+
571+
OIDC provider support requires Google Cloud's Identity Platform (GCIP). To learn more about
572+
GCIP, including pricing and features, see https://cloud.google.com/identity-platform.
573+
574+
Args:
575+
provider_id: Provider ID string. Must have the prefix ``oidc.``.
576+
client_id: Client ID of the new config.
577+
issuer: Issuer of the new config. Must be a valid URL.
578+
display_name: The user-friendly display name to the current configuration (optional).
579+
This name is also used as the provider label in the Cloud Console.
580+
enabled: A boolean indicating whether the provider configuration is enabled or disabled
581+
(optional). A user cannot sign in using a disabled provider.
582+
app: An App instance (optional).
583+
584+
Returns:
585+
OIDCProviderConfig: The newly created OIDCProviderConfig instance.
586+
587+
Raises:
588+
ValueError: If any of the specified input parameters are invalid.
589+
FirebaseError: If an error occurs while creating the new OIDC provider config.
590+
"""
591+
client = _get_client(app)
592+
return client.create_oidc_provider_config(
593+
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
594+
enabled=enabled)
595+
596+
597+
def update_oidc_provider_config(
598+
provider_id, client_id=None, issuer=None, display_name=None, enabled=None, app=None):
599+
"""Updates an existing OIDC provider config with the given parameters.
600+
601+
Args:
602+
provider_id: Provider ID string. Must have the prefix ``oidc.``.
603+
client_id: Client ID of the new config (optional).
604+
issuer: Issuer of the new config (optional). Must be a valid URL.
605+
display_name: The user-friendly display name to the current configuration (optional).
606+
Pass ``auth.DELETE_ATTRIBUTE`` to delete the current display name.
607+
enabled: A boolean indicating whether the provider configuration is enabled or disabled
608+
(optional).
609+
app: An App instance (optional).
610+
611+
Returns:
612+
OIDCProviderConfig: The updated OIDCProviderConfig instance.
613+
614+
Raises:
615+
ValueError: If any of the specified input parameters are invalid.
616+
FirebaseError: If an error occurs while updating the OIDC provider config.
617+
"""
618+
client = _get_client(app)
619+
return client.update_oidc_provider_config(
620+
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
621+
enabled=enabled)
622+
567623

568624
def delete_oidc_provider_config(provider_id, app=None):
569625
"""Deletes the OIDCProviderConfig with the given ID.

tests/test_auth_providers.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ def _instrument_provider_mgt(app, status, payload):
5858

5959
class TestOIDCProviderConfig:
6060

61+
VALID_CREATE_OPTIONS = {
62+
'provider_id': 'oidc.provider',
63+
'client_id': 'CLIENT_ID',
64+
'issuer': 'https://oidc.com/issuer',
65+
'display_name': 'oidcProviderName',
66+
'enabled': True,
67+
}
68+
69+
OIDC_CONFIG_REQUEST = {
70+
'displayName': 'oidcProviderName',
71+
'enabled': True,
72+
'clientId': 'CLIENT_ID',
73+
'issuer': 'https://oidc.com/issuer',
74+
}
75+
6176
@pytest.mark.parametrize('provider_id', INVALID_PROVIDER_IDS + ['saml.provider'])
6277
def test_get_invalid_provider_id(self, user_mgt_app, provider_id):
6378
with pytest.raises(ValueError) as excinfo:
@@ -76,6 +91,135 @@ def test_get(self, user_mgt_app):
7691
assert req.method == 'GET'
7792
assert req.url == '{0}{1}'.format(USER_MGT_URL_PREFIX, '/oauthIdpConfigs/oidc.provider')
7893

94+
@pytest.mark.parametrize('invalid_opts', [
95+
{'provider_id': None}, {'provider_id': ''}, {'provider_id': 'saml.provider'},
96+
{'client_id': None}, {'client_id': ''},
97+
{'issuer': None}, {'issuer': ''}, {'issuer': 'not a url'},
98+
{'display_name': True},
99+
{'enabled': 'true'},
100+
])
101+
def test_create_invalid_args(self, user_mgt_app, invalid_opts):
102+
options = dict(self.VALID_CREATE_OPTIONS)
103+
options.update(invalid_opts)
104+
with pytest.raises(ValueError):
105+
auth.create_oidc_provider_config(**options, app=user_mgt_app)
106+
107+
def test_create(self, user_mgt_app):
108+
recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE)
109+
110+
provider_config = auth.create_oidc_provider_config(
111+
**self.VALID_CREATE_OPTIONS, app=user_mgt_app)
112+
113+
self._assert_provider_config(provider_config)
114+
assert len(recorder) == 1
115+
req = recorder[0]
116+
assert req.method == 'POST'
117+
assert req.url == '{0}/oauthIdpConfigs?oauthIdpConfigId=oidc.provider'.format(
118+
USER_MGT_URL_PREFIX)
119+
got = json.loads(req.body.decode())
120+
assert got == self.OIDC_CONFIG_REQUEST
121+
122+
def test_create_minimal(self, user_mgt_app):
123+
recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE)
124+
options = dict(self.VALID_CREATE_OPTIONS)
125+
del options['display_name']
126+
del options['enabled']
127+
want = dict(self.OIDC_CONFIG_REQUEST)
128+
del want['displayName']
129+
del want['enabled']
130+
131+
provider_config = auth.create_oidc_provider_config(**options, app=user_mgt_app)
132+
133+
self._assert_provider_config(provider_config)
134+
assert len(recorder) == 1
135+
req = recorder[0]
136+
assert req.method == 'POST'
137+
assert req.url == '{0}/oauthIdpConfigs?oauthIdpConfigId=oidc.provider'.format(
138+
USER_MGT_URL_PREFIX)
139+
got = json.loads(req.body.decode())
140+
assert got == want
141+
142+
def test_create_empty_values(self, user_mgt_app):
143+
recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE)
144+
options = dict(self.VALID_CREATE_OPTIONS)
145+
options['display_name'] = ''
146+
options['enabled'] = False
147+
want = dict(self.OIDC_CONFIG_REQUEST)
148+
want['displayName'] = ''
149+
want['enabled'] = False
150+
151+
provider_config = auth.create_oidc_provider_config(**options, app=user_mgt_app)
152+
153+
self._assert_provider_config(provider_config)
154+
assert len(recorder) == 1
155+
req = recorder[0]
156+
assert req.method == 'POST'
157+
assert req.url == '{0}/oauthIdpConfigs?oauthIdpConfigId=oidc.provider'.format(
158+
USER_MGT_URL_PREFIX)
159+
got = json.loads(req.body.decode())
160+
assert got == want
161+
162+
@pytest.mark.parametrize('invalid_opts', [
163+
{},
164+
{'provider_id': None}, {'provider_id': ''}, {'provider_id': 'saml.provider'},
165+
{'client_id': ''},
166+
{'issuer': ''}, {'issuer': 'not a url'},
167+
{'display_name': True},
168+
{'enabled': 'true'},
169+
])
170+
def test_update_invalid_args(self, user_mgt_app, invalid_opts):
171+
options = {'provider_id': 'oidc.provider'}
172+
options.update(invalid_opts)
173+
with pytest.raises(ValueError):
174+
auth.update_oidc_provider_config(**options, app=user_mgt_app)
175+
176+
def test_update(self, user_mgt_app):
177+
recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE)
178+
179+
provider_config = auth.update_oidc_provider_config(
180+
**self.VALID_CREATE_OPTIONS, app=user_mgt_app)
181+
182+
self._assert_provider_config(provider_config)
183+
assert len(recorder) == 1
184+
req = recorder[0]
185+
assert req.method == 'PATCH'
186+
mask = ['clientId', 'displayName', 'enabled', 'issuer']
187+
assert req.url == '{0}/oauthIdpConfigs/oidc.provider?updateMask={1}'.format(
188+
USER_MGT_URL_PREFIX, ','.join(mask))
189+
got = json.loads(req.body.decode())
190+
assert got == self.OIDC_CONFIG_REQUEST
191+
192+
def test_update_minimal(self, user_mgt_app):
193+
recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE)
194+
195+
provider_config = auth.update_oidc_provider_config(
196+
'oidc.provider', display_name='oidcProviderName', app=user_mgt_app)
197+
198+
self._assert_provider_config(provider_config)
199+
assert len(recorder) == 1
200+
req = recorder[0]
201+
assert req.method == 'PATCH'
202+
assert req.url == '{0}/oauthIdpConfigs/oidc.provider?updateMask=displayName'.format(
203+
USER_MGT_URL_PREFIX)
204+
got = json.loads(req.body.decode())
205+
assert got == {'displayName': 'oidcProviderName'}
206+
207+
def test_update_empty_values(self, user_mgt_app):
208+
recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE)
209+
210+
provider_config = auth.update_oidc_provider_config(
211+
'oidc.provider', display_name=auth.DELETE_ATTRIBUTE, enabled=False, app=user_mgt_app)
212+
213+
self._assert_provider_config(provider_config)
214+
assert len(recorder) == 1
215+
req = recorder[0]
216+
assert req.method == 'PATCH'
217+
mask = ['displayName', 'enabled']
218+
assert req.url == '{0}/oauthIdpConfigs/oidc.provider?updateMask={1}'.format(
219+
USER_MGT_URL_PREFIX, ','.join(mask))
220+
got = json.loads(req.body.decode())
221+
assert got == {'displayName': None, 'enabled': False}
222+
79223
@pytest.mark.parametrize('provider_id', INVALID_PROVIDER_IDS + ['saml.provider'])
80224
def test_delete_invalid_provider_id(self, user_mgt_app, provider_id):
81225
with pytest.raises(ValueError) as excinfo:

tests/test_tenant_mgt.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@
7979
MOCK_LIST_USERS_RESPONSE = testutils.resource('list_users.json')
8080

8181
OIDC_PROVIDER_CONFIG_RESPONSE = testutils.resource('oidc_provider_config.json')
82+
OIDC_PROVIDER_CONFIG_REQUEST = {
83+
'displayName': 'oidcProviderName',
84+
'enabled': True,
85+
'clientId': 'CLIENT_ID',
86+
'issuer': 'https://oidc.com/issuer',
87+
}
88+
8289
SAML_PROVIDER_CONFIG_RESPONSE = testutils.resource('saml_provider_config.json')
8390
SAML_PROVIDER_CONFIG_REQUEST = body = {
8491
'displayName': 'samlProviderName',
@@ -729,6 +736,34 @@ def test_get_oidc_provider_config(self, tenant_mgt_app):
729736
assert req.url == '{0}/tenants/tenant-id/oauthIdpConfigs/oidc.provider'.format(
730737
PROVIDER_MGT_URL_PREFIX)
731738

739+
def test_create_oidc_provider_config(self, tenant_mgt_app):
740+
client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app)
741+
recorder = _instrument_provider_mgt(client, 200, OIDC_PROVIDER_CONFIG_RESPONSE)
742+
743+
provider_config = client.create_oidc_provider_config(
744+
'oidc.provider', client_id='CLIENT_ID', issuer='https://oidc.com/issuer',
745+
display_name='oidcProviderName', enabled=True)
746+
747+
self._assert_oidc_provider_config(provider_config)
748+
self._assert_request(
749+
recorder, '/oauthIdpConfigs?oauthIdpConfigId=oidc.provider',
750+
OIDC_PROVIDER_CONFIG_REQUEST, prefix=PROVIDER_MGT_URL_PREFIX)
751+
752+
def test_update_oidc_provider_config(self, tenant_mgt_app):
753+
client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app)
754+
recorder = _instrument_provider_mgt(client, 200, OIDC_PROVIDER_CONFIG_RESPONSE)
755+
756+
provider_config = client.update_oidc_provider_config(
757+
'oidc.provider', client_id='CLIENT_ID', issuer='https://oidc.com/issuer',
758+
display_name='oidcProviderName', enabled=True)
759+
760+
self._assert_oidc_provider_config(provider_config)
761+
mask = ['clientId', 'displayName', 'enabled', 'issuer']
762+
url = '/oauthIdpConfigs/oidc.provider?updateMask={0}'.format(','.join(mask))
763+
self._assert_request(
764+
recorder, url, OIDC_PROVIDER_CONFIG_REQUEST, method='PATCH',
765+
prefix=PROVIDER_MGT_URL_PREFIX)
766+
732767
def test_delete_oidc_provider_config(self, tenant_mgt_app):
733768
client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app)
734769
recorder = _instrument_provider_mgt(client, 200, '{}')

0 commit comments

Comments
 (0)