Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,75 @@ The following example demonstrates the verification of an ID token signed with t

If the token verification fails, a ``TokenValidationError`` will be raised. In that scenario, the ID token should be deemed invalid and its contents should not be trusted.

===========================
Organizations (Closed Beta)
===========================

Organizations is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications.

Using Organizations, you can:
* Represent teams, business customers, partner companies, or any logical grouping of users that should have different ways of accessing your applications, as organizations.
* Manage their membership in a variety of ways, including user invitation.
* Configure branded, federated login flows for each organization.
* Implement role-based access control, such that users can have different roles when authenticating in the context of different organizations.
* Build administration capabilities into your products, using Organizations APIs, so that those businesses can manage their own organizations.

Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans.

-------------------------
Log in to an organization
-------------------------

Log in to an organization by specifying the ``organization`` property when calling ``authorize()``:

.. code-block:: python

from auth0.v3.authentication.authorize_client import AuthorizeClient

client = AuthorizeClient('my.domain.com')

client.authorize(client_id='client_id',
redirect_uri='http://localhost',
organization="org_abc")

When logging into an organization, it is important to ensure the ``org_id`` claim of the ID Token matches the expected organization value. The ``TokenVerifier`` can be be used to ensure the ID Token contains the expected ``org_id`` claim value:

.. code-block:: python

from auth0.v3.authentication.token_verifier import TokenVerifier, AsymmetricSignatureVerifier

domain = 'myaccount.auth0.com'
client_id = 'exampleid'

# After authenticating
id_token = auth_result['id_token']

jwks_url = 'https://{}/.well-known/jwks.json'.format(domain)
issuer = 'https://{}/'.format(domain)

sv = AsymmetricSignatureVerifier(jwks_url) # Reusable instance
tv = TokenVerifier(signature_verifier=sv, issuer=issuer, audience=client_id)

# pass the expected organization the user logged in to:
tv.verify(id_token, organization='org_abc')

-----------------------
Accept user invitations
-----------------------

Accept a user invitation by specifying the ``invitation`` property when calling ``authorize()``. Note that you must also specify the ``organization`` if providing an ``invitation``.
The ID of the invitation and organization are available as query parameters on the invitation URL, e.g., ``https://your-domain.auth0.com/login?invitation=invitation_id&organization=org_id&organization_name=org_name``

.. code-block:: python

from auth0.v3.authentication.authorize_client import AuthorizeClient

client = AuthorizeClient('my.domain.com')

client.authorize(client_id='client_id',
redirect_uri='http://localhost',
organization='org_abc',
invitation="invitation_123")

====================
Management SDK Usage
Expand Down
6 changes: 4 additions & 2 deletions auth0/v3/authentication/authorize_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class AuthorizeClient(AuthenticationBase):
"""

def authorize(self, client_id, audience=None, state=None, redirect_uri=None,
response_type='code', scope='openid'):
response_type='code', scope='openid', organization=None, invitation=None):
"""Authorization code grant

This is the OAuth 2.0 grant that regular web apps utilize in order to access an API.
Expand All @@ -21,7 +21,9 @@ def authorize(self, client_id, audience=None, state=None, redirect_uri=None,
'response_type': response_type,
'scope': scope,
'state': state,
'redirect_uri': redirect_uri
'redirect_uri': redirect_uri,
'organization': organization,
'invitation': invitation
}

return self.get(
Expand Down
17 changes: 14 additions & 3 deletions auth0/v3/authentication/token_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,15 @@ def __init__(self, signature_verifier, issuer, audience, leeway=0):
token (str): The JWT to verify.
nonce (str, optional): The nonce value sent during authentication.
max_age (int, optional): The max_age value sent during authentication.
organization (str, optional): The expected organization ID (org_id) claim value. This should be specified
when logging in to an organization.

Raises:
TokenValidationError: when the token cannot be decoded, the token signing algorithm is not the expected one,
the token signature is invalid or the token has a claim missing or with unexpected value.
"""

def verify(self, token, nonce=None, max_age=None):
def verify(self, token, nonce=None, max_age=None, organization=None):
# Verify token presence
if not token or not isinstance(token, str):
raise TokenValidationError("ID token is required but missing.")
Expand All @@ -242,9 +244,9 @@ def verify(self, token, nonce=None, max_age=None):
payload = self._sv.verify_signature(token)

# Verify claims
self._verify_payload(payload, nonce, max_age)
self._verify_payload(payload, nonce, max_age, organization)

def _verify_payload(self, payload, nonce=None, max_age=None):
def _verify_payload(self, payload, nonce=None, max_age=None, organization=None):
try:
# on Python 2.7, 'str' keys as parsed as 'unicode'
# But 'unicode' was removed on Python 3.7
Expand Down Expand Up @@ -307,6 +309,15 @@ def _verify_payload(self, payload, nonce=None, max_age=None):
'Nonce (nonce) claim mismatch in the ID token; expected "{}", '
'found "{}"'.format(nonce, payload['nonce']))

# Organization
if organization:
if 'org_id' not in payload or not isinstance(payload['org_id'], (str, ustr)):
raise TokenValidationError('Organization (org_id) claim must be a string present in the ID token')
if payload['org_id'] != organization:
raise TokenValidationError(
'Organization (org_id) claim mismatch in the ID token; expected "{}", '
'found "{}"'.format(organization, payload['org_id']))

# Authorized party
if isinstance(payload['aud'], list) and len(payload['aud']) > 1:
if 'azp' not in payload or not isinstance(payload['azp'], (str, ustr)):
Expand Down
29 changes: 27 additions & 2 deletions auth0/v3/test/authentication/test_authorize_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ def test_login(self, mock_get):
state='st',
redirect_uri='http://localhost',
response_type='token',
scope='openid profile')
scope='openid profile',
organization='org_123',
invitation='invitation_abc')

args, kwargs = mock_get.call_args

Expand All @@ -26,5 +28,28 @@ def test_login(self, mock_get):
'state': 'st',
'redirect_uri': 'http://localhost',
'response_type': 'token',
'scope': 'openid profile'
'scope': 'openid profile',
'organization': 'org_123',
'invitation': 'invitation_abc'
})

@mock.patch('auth0.v3.authentication.authorize_client.AuthorizeClient.get')
def test_login_default_param_values(self, mock_get):

a = AuthorizeClient('my.domain.com')

a.authorize(client_id='cid')

args, kwargs = mock_get.call_args

self.assertEqual(args[0], 'https://my.domain.com/authorize')
self.assertEqual(kwargs['params'], {
'audience': None,
'invitation': None,
'organization': None,
'redirect_uri': None,
'state': None,
'client_id': 'cid',
'response_type': 'code',
'scope': 'openid'
})
38 changes: 36 additions & 2 deletions auth0/v3/test/authentication/test_token_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def asymmetric_signature_verifier_mock():
verifier._fetch_key.return_value = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(RSA_PUB_KEY_1_JWK))
return verifier

def assert_fails_with_error(self, token, error_message, signature_verifier=None, audience=expectations['audience'], issuer=expectations['issuer'], nonce=None, max_age=None, clock=MOCKED_CLOCK):
def assert_fails_with_error(self, token, error_message, signature_verifier=None, audience=expectations['audience'], issuer=expectations['issuer'], nonce=None, max_age=None, clock=MOCKED_CLOCK, organization=None):
sv = signature_verifier or self.asymmetric_signature_verifier_mock()
tv = TokenVerifier(
signature_verifier=sv,
Expand All @@ -231,7 +231,7 @@ def assert_fails_with_error(self, token, error_message, signature_verifier=None,
)
tv._clock = clock
with self.assertRaises(TokenValidationError) as err:
tv.verify(token, nonce, max_age)
tv.verify(token, nonce, max_age, organization)
self.assertEqual(str(err.exception), error_message)

def test_fails_at_creation_with_invalid_signature_verifier(self):
Expand Down Expand Up @@ -369,3 +369,37 @@ def test_fails_when_max_age_sent_with_auth_time_invalid(self):
mocked_clock = expected_auth_time + 1

self.assert_fails_with_error(token, "Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time ({}) is after last auth at ({})".format(mocked_clock, expected_auth_time), max_age=max_age, clock=mocked_clock)

def test_passes_when_org_present_but_not_required(self):
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfaWQiOiJvcmdfMTIzIiwiaXNzIjoiaHR0cHM6Ly90b2tlbnMtdGVzdC5hdXRoMC5jb20vIiwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjF9.hjSPgJpg0Dn2z0giCdGqVLD5Kmqy_yMYlSkgwKD7ahQ"
sv = SymmetricSignatureVerifier(HMAC_SHARED_SECRET)
tv = TokenVerifier(
signature_verifier=sv,
issuer=expectations['issuer'],
audience=expectations['audience']
)
tv._clock = MOCKED_CLOCK
tv.verify(token)

def test_passes_when_org_present_and_matches(self):
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfaWQiOiJvcmdfMTIzIiwiaXNzIjoiaHR0cHM6Ly90b2tlbnMtdGVzdC5hdXRoMC5jb20vIiwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjF9.hjSPgJpg0Dn2z0giCdGqVLD5Kmqy_yMYlSkgwKD7ahQ"
sv = SymmetricSignatureVerifier(HMAC_SHARED_SECRET)
tv = TokenVerifier(
signature_verifier=sv,
issuer=expectations['issuer'],
audience=expectations['audience']
)
tv._clock = MOCKED_CLOCK
tv.verify(token, organization='org_123')

def test_fails_when_org_specified_but_not_present(self):
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.wotJnUdD5IfdZMewF_-BnHc0pI56uwzwr5qaSXvSu9w"
self.assert_fails_with_error(token, "Organization (org_id) claim must be a string present in the ID token", signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), organization='org_123')

def test_fails_when_org_specified_but_not_(self):
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfaWQiOjQyLCJpc3MiOiJodHRwczovL3Rva2Vucy10ZXN0LmF1dGgwLmNvbS8iLCJleHAiOjE1ODc3NjUzNjEsImlhdCI6MTU4NzU5MjU2MX0.fGL1_akaHikdovS7NRYla3flne1xdtCjP0ei_CRxO6k"
self.assert_fails_with_error(token, "Organization (org_id) claim must be a string present in the ID token", signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), organization='org_123')

def test_fails_when_org_specified_but_does_not_match(self):
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHxzZGs0NThma3MiLCJhdWQiOiJ0b2tlbnMtdGVzdC0xMjMiLCJvcmdfaWQiOiJvcmdfMTIzIiwiaXNzIjoiaHR0cHM6Ly90b2tlbnMtdGVzdC5hdXRoMC5jb20vIiwiZXhwIjoxNTg3NzY1MzYxLCJpYXQiOjE1ODc1OTI1NjF9.hjSPgJpg0Dn2z0giCdGqVLD5Kmqy_yMYlSkgwKD7ahQ"
self.assert_fails_with_error(token, 'Organization (org_id) claim mismatch in the ID token; expected "org_abc", found "org_123"', signature_verifier=SymmetricSignatureVerifier(HMAC_SHARED_SECRET), organization='org_abc')