Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK-3860] Add support for private_key_jwt #456

Merged
merged 9 commits into from
Jan 17, 2023
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
21 changes: 3 additions & 18 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,9 @@ executors:
python_3_10:
docker:
- image: cimg/python:3.10
python_2_7:
docker:
- image: cimg/python:2.7

jobs:
python_3:
build:
executor: python_3_10
steps:
- checkout
Expand All @@ -25,21 +22,10 @@ jobs:
- run: bash <(curl -s https://codecov.io/bash)
- run: make -C docs html

python_2:
executor: python_2_7
steps:
- checkout
- python/install-packages:
pkg-manager: pip-dist
path-args: ".[test]"
- run: coverage run -m unittest discover -s auth0/v3/test -t .
- codecov/upload

workflows:
main:
jobs:
- python_3
- python_2
- build
- ship/python-publish:
prefix-tag: false
context:
Expand All @@ -50,5 +36,4 @@ workflows:
only:
- master
requires:
- python_3
- python_2
- build
112 changes: 18 additions & 94 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

- [Authentication SDK](#authentication-sdk)
- [ID token validation](#id-token-validation)
- [Organizations](#organizations)
- [Authenticating with a application configured to use `private_key_jwt` token endpoint auth method](#authenticating-with-a-application-configured-to-use-private-key-jwt-token-endpoint-auth-method)
- [Management SDK](#management-sdk)
- [Connections](#connections)
- [Error handling](#error-handling)
Expand Down Expand Up @@ -50,102 +50,26 @@ tv.verify(id_token)

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

[Organizations](https://auth0.com/docs/organizations) is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications.

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()``:

```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:

```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``

```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")
```

#### Authorizing users from an Organization

If an `org_id` claim is present in the Access Token, then the claim should be validated by the API to ensure that the value received is expected or known.

In particular:

- The issuer (`iss`) claim should be checked to ensure the token was issued by Auth0
- The organization ID (`org_id`) claim should be checked to ensure it is a value that is already known to the application. This could be validated against a known list of organization IDs, or perhaps checked in conjunction with the current request URL. e.g. the sub-domain may hint at what organization should be used to validate the Access Token.

Normally, validating the issuer would be enough to ensure that the token was issued by Auth0. In the case of organizations, additional checks should be made so that the organization within an Auth0 tenant is expected.

If the claim cannot be validated, then the application should deem the token invalid.

The snippet below attempts to illustrate how this verification could look like using the external [PyJWT](https://pyjwt.readthedocs.io/en/latest/usage.html#encoding-decoding-tokens-with-rs256-rsa) library. This dependency will take care of pulling the RS256 Public Key that was used by the server to sign the Access Token. It will also validate its signature, expiration, and the audience value. After the basic verification, get the `org_id` claim and check it against the expected value. The code assumes your application is configured to sign tokens using the RS256 algorithm. Check the [Validate JSON Web Tokens](https://auth0.com/docs/tokens/json-web-tokens/validate-json-web-tokens) article to learn more about this verification.
### Authenticating with a application configured to use `private_key_jwt` token endpoint auth method

```python
import jwt # PyJWT
from jwt import PyJWKClient

access_token = # access token from the request
url = 'https://{YOUR AUTH0 DOMAIN}/.well-known/jwks.json'
jwks_client = PyJWKClient(url)
signing_key = jwks_client.get_signing_key_from_jwt(access_token)
data = jwt.decode(
access_token,
signing_key.key,
algorithms=['RS256'],
audience='{YOUR API AUDIENCE}'
from auth0.v3.authentication import GetToken

private_key = """-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAwfUb0nUC0aKB3WiytFhnCIg455BYC+dR3MUGadWpIg7S6lbi
...
2tjIvH4GN9ZkIGwzxIOP61wkUGwGaIzacOTIWOvqRI0OaYr9U18Ep1trvgGR
-----END RSA PRIVATE KEY-----
"""

get_token = GetToken(
"my-domain.us.auth0.com",
"my-client-id",
client_assertion_signing_key=private_key,
)
token = get_token.client_credentials(
"https://my-domain.us.auth0.com/api/v2/"
)

organization = # expected organization ID
if data['org_id'] != organization:
raise Exception('Organization (org_id) claim mismatch')

# if this line is reached, validation is successful
```

## Management SDK
Expand Down
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,29 +38,29 @@ For example:
```python
from auth0.v3.authentication import Social

social = Social('myaccount.auth0.com')
social = Social('my-domain.us.auth0.com', 'my-client-id')

social.login(client_id='...', access_token='...', connection='facebook')
social.login(access_token='...', connection='facebook')
```

If you need to sign up a user using their email and password, you can use the Database object.

```python
from auth0.v3.authentication import Database

database = Database('myaccount.auth0.com'')
database = Database('my-domain.us.auth0.com', 'my-client-id')

database.signup(client_id='...', email='[email protected]', password='secr3t', connection='Username-Password-Authentication')
database.signup(email='[email protected]', password='secr3t', connection='Username-Password-Authentication')
```

If you need to authenticate a user using their email and password, you can use the `GetToken` object, which enables making requests to the `/oauth/token` endpoint.

```python
from auth0.v3.authentication import GetToken

token = GetToken('myaccount.auth0.com')
token = GetToken('my-domain.us.auth0.com', 'my-client-id', client_secret='my-client-secret')

token.login(client_id='...', client_secret='...', username='[email protected]', password='secr3t', realm='Username-Password-Authentication')
token.login(username='[email protected]', password='secr3t', realm='Username-Password-Authentication')
```

#### Management SDK
Expand All @@ -73,9 +73,8 @@ domain = 'myaccount.auth0.com'
non_interactive_client_id = 'exampleid'
non_interactive_client_secret = 'examplesecret'

get_token = GetToken(domain)
token = get_token.client_credentials(non_interactive_client_id,
non_interactive_client_secret, 'https://{}/api/v2/'.format(domain))
get_token = GetToken(domain, non_interactive_client_id, client_secret=non_interactive_client_secret)
token = get_token.client_credentials('https://{}/api/v2/'.format(domain))
mgmt_api_token = token['access_token']
```

Expand All @@ -98,7 +97,6 @@ For more code samples on how to integrate the auth0-python SDK in your Python ap

### Authentication Endpoints

- API Authorization - Authorization Code Grant (`authentication.AuthorizeClient`)
- Database ( `authentication.Database` )
- Delegated ( `authentication.Delegated` )
- Enterprise ( `authentication.Enterprise` )
Expand Down Expand Up @@ -140,6 +138,7 @@ For more code samples on how to integrate the auth0-python SDK in your Python ap
- UserBlocks() (`Auth0().user_blocks` )
- UsersByEmail() ( `Auth0().users_by_email` )
- Users() ( `Auth0().users` )

## Feedback

### Contributing
Expand Down
38 changes: 38 additions & 0 deletions V4_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# V4 Migration Guide

Guide to migrating from `3.x` to `4.x`

- [Python <3.6 is no longer supported](#python-36-is-no-longer-supported)
- [Client ID and client secret are now specified in the constructor for authentication clients](#client-id-and-client-secret-are-now-specified-in-the-constructor-for-authentication-clients)
- [AuthorizeClient and Logout have been removed](#authorizeclient-and-logout-have-been-removed)

## Python <3.6 is no longer supported

Python 3.5 and Python 2 are EOL and are no longer supported.

## Client ID and client secret are now specified in the constructor for authentication clients

### Before

```py
from auth0.v3.authentication import GetToken

get_token = GetToken('my-domain.us.auth0.com')

get_token.client_credentials('my-client-id', 'my-client-secret', 'my-api')
```

### After

```py
from auth0.v3.authentication import GetToken

# `client_secret` is optional (you can now use `client_assertion_signing_key` as an alternative)
get_token = GetToken('my-domain.us.auth0.com', 'my-client-id', client_secret='my-client-secret')

get_token.client_credentials('my-api')
```

## AuthorizeClient and Logout have been removed

The authorize and logout requests need to be done in a user agent, so it didn't make sense to include them in a REST client.
4 changes: 0 additions & 4 deletions auth0/v3/authentication/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
from .authorize_client import AuthorizeClient
from .database import Database
from .delegated import Delegated
from .enterprise import Enterprise
from .get_token import GetToken
from .logout import Logout
from .passwordless import Passwordless
from .revoke_token import RevokeToken
from .social import Social
from .users import Users

__all__ = (
"AuthorizeClient",
"Database",
"Delegated",
"Enterprise",
"GetToken",
"Logout",
"Passwordless",
"RevokeToken",
"Social",
Expand Down
40 changes: 0 additions & 40 deletions auth0/v3/authentication/authorize_client.py

This file was deleted.

Loading