Giter Club home page Giter Club logo

okta-sdk-python's Introduction

License Support

PyPI PyPI - Python Version Code Style

Okta Python Management SDK

This repository contains the Okta management SDK for Python. This SDK can be used in your server-side code to interact with the Okta management API and

Requires Python version 3.7.0 or higher.

You can also learn more on the Okta + Python page in our documentation.

Release status

This library uses semantic versioning and follows Okta's Library Version Policy.

Version Status
0.x ⚠️ Beta Release (Retired)
1.x ⚠️ Retired
2.x ✔️ Release

The latest release can always be found on the releases page.

Need help?

If you run into problems using the SDK, you can:

Getting started

To install the Okta Python SDK in your project:

pip install okta

You'll also need

Construct a client instance by passing it your Okta domain name and API token:

import asyncio

from okta.client import Client as OktaClient

# Instantiating with a Python dictionary in the constructor
config = {
    'orgUrl': 'https://{yourOktaDomain}',
    'token': 'YOUR_API_TOKEN'
}
okta_client = OktaClient(config)

# example of usage, list all users and print their first name and last name
async def main():
    users, resp, err = await okta_client.list_users()
    for user in users:
        print(user.profile.first_name, user.profile.last_name)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Another way to instantiate okta_client (config should be provided in other place as described below):

# Instantiating without in-text credentials
okta_client = OktaClient()

Http session was introduced within v2.3.0 to allow custom SSL contest. Starting with SDK v2.4.0 you can reuse http session to gain better performance:

import asyncio
import aiohttp

from okta.client import Client as OktaClient

config = {
    'orgUrl': 'https://{yourOktaDomain}',
    'token': 'YOUR_API_TOKEN'
}


async def main():
    async with OktaClient(config) as client:
        # perform all queries within same session
        users, okta_resp, err = await client.list_users()
        user, okta_resp, err = await client.get_user(users[0].id)


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Context Manager is a preferable way to use OktaClient to perform bunch of API requests.

Using a Python dictionary to hard-code the Okta domain and API token is encouraged for development; In production, you should use a more secure way of storing these values. This library supports a few different configuration sources, covered in the configuration reference section.

OAuth 2.0

Okta allows you to interact with Okta APIs using scoped OAuth 2.0 access tokens. Each access token enables the bearer to perform specific actions on specific Okta endpoints, with that ability controlled by which scopes the access token contains.

This SDK supports this feature (OAuth 2.0) only for service-to-service applications. Check out our guides to learn more about how to register a new service application using a private and public key pair.

When using this approach you won't need an API Token because the SDK will request an access token for you. In order to use OAuth 2.0, construct a client instance by passing the following parameters:

from okta.client import Client as OktaClient
config = {
    'orgUrl': 'https://{yourOktaDomain}',
    'authorizationMode': 'PrivateKey',
    'clientId': '{yourClientId}',
    'scopes': ['okta.users.manage'],
    'privateKey': 'YOUR_PRIVATE_JWK', # this parameter should be type of str
    'kid': 'YOUR_PRIVATE_KEY_ID' # if a key ID needs to be provided, it can be provided here or part of the privateKey under "kid"
}
okta_client = OktaClient(config)


# example of usage, list all users and print their first name and last name
async def main():
    users, resp, err = await okta_client.list_users()
    for user in users:
        print(user.profile.first_name, user.profile.last_name)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Note, that privateKey can be passed in JWK format or in PEM format, i.e. (examples generated with https://mkjwk.org):

{
    "p": "4VmEO2ztlIHvalMHX797rWhETbKgB6bRbdGevYpRLZH167hKHB5vsuRjIAXJdujQw8W3rnas9Z-_Ddv1TbR5Qnz0UmhnxQAIbdDDUE9r5P_LEholrjY9Jz0P-W4jey-7cDATeITYHb3t67HcIwVbxQF5fkRdJAhfO029RqkH3OE",
    "kty": "RSA",
    "q": "x8ngsUMrDGReVVpeGdlZzGTSFxrNP89DF4WEQZ7zCpSe3_GpuUPbzgslYQEiX6XJY5ssavavVNOmmQEAt0xsMcxxVOPYCYy7LBE8cJQiFb_bMf2H1-zTlPn_KF4D10h45cLXhu-xh4c52Rh9WDMYZmKWLkAJQ6L_eueGoZkIDmU",
    "d": "R38UamnZiEhOLxD7FYUN5AKj9mHQneRWizblxfNq2T1Nfk4matfZrrlMq_nz9tYZ3-TOCu3u-7k_igM0Tml365mbU_HzkfCrD-ou7cGSrqNgnipj_VQSgJfKRFKATEf4hMfdpKSd4rZzf8OJnq8s-kpRVC4kdHJtJjja59VvHEQRIrN_dkycNHSBWu5UjZbXOO5X3mjwuIh9gpLGZ-nHTqgTpT324q5BLVsH8_ywRGifIj-HQL1O5bJO2Q2_18iL1TbnMSbDwrKdb1edb4bgDuWB4o0xSTXsherTgeXu76gN9FY28tuAKSd34yqp7GZaYcjtkskbWPRtYhOID2cOgQ",
    "e": "AQAB",
    "use": "sig",
    "kid": "test",
    "qi": "FZGFuvW1W9VF31JyrMYJy_BH7vja3d9iZlhFzttNZ-wmiXG4irrI_fLJgmXK6dI3MfIhKPAYi9nnza2kcR1qEV9QObA4NV86RWnc8sAHbDGooe9VK5eJ5jjD7Tq_ZZiLiHGOZit3HylNilOb0k3VsgMcp0F3ZQaMbg35K9rSgZE",
    "dp": "i4D6HjupvCTQDNdHmluU-d2xYxQwg2we_EgnaBkHdhmEzx8wKcYhyfIe90T92jH4gymUM1neatQw1yiS7D7MTn_CVH2zt730ed8h-kageYxsr1EmgHmtU-w2RmiLaIg9Fg99Dj_W9lqMvjtGFxwLGqN2DdfOfS79nV3bzbF4X6E",
    "alg": "RS256",
    "dq": "CT79iBacsmkeuIKDIl0du8jatDkIULCt4TPLqCHMC6xPIfwUJ7_NN17qru-XgKeyh0qSJq0d9iYJasFSICmIRFG62PvmbqK1stdlXaxtW2ZSpaCfHc4XCKj9NwgK03bGKZP314XWSHhoo_RvMJrEwVBEtQU_qIKtoil-4JGtfsU",
    "n": "r95K3WIN8-4dB-tEKHjyTIIZZUMbHz8ad5oBX2BGiGxfPGfHbz2RH4QLT9ffzL-tgEo8IKs0Myh0VTwauiwz0cdHuS2gUTasK9OsosX1h1scSu_eZ-g-__lXBogU-SvBXBAgjv8hdcZjqWYQwmhJp2Ilv0CuXKxQwZyjso775PDjWDCH5HkVcSxHyUvpThLfWfkfz5PNDZvRpuPltv55ILRaVZhwPb7VXLAm2ebfeYUdybUKpGnEogKQdaL7TdNvP-HRnUSXTiYeXWHzU04FaXJ7yLmtXOQ52FT9dwkwLrCDOmDSBGafZ9asUtgOKhKN6wQW5mndhMK_1zThfjZyxQ"
}

or

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCv3krdYg3z7h0H
60QoePJMghllQxsfPxp3mgFfYEaIbF88Z8dvPZEfhAtP19/Mv62ASjwgqzQzKHRV
PBq6LDPRx0e5LaBRNqwr06yixfWHWxxK795n6D7/+VcGiBT5K8FcECCO/yF1xmOp
ZhDCaEmnYiW/QK5crFDBnKOyjvvk8ONYMIfkeRVxLEfJS+lOEt9Z+R/Pk80Nm9Gm
4+W2/nkgtFpVmHA9vtVcsCbZ5t95hR3JtQqkacSiApB1ovtN028/4dGdRJdOJh5d
YfNTTgVpcnvIua1c5DnYVP13CTAusIM6YNIEZp9n1qxS2A4qEo3rBBbmad2Ewr/X
NOF+NnLFAgMBAAECggEAR38UamnZiEhOLxD7FYUN5AKj9mHQneRWizblxfNq2T1N
fk4matfZrrlMq/nz9tYZ3+TOCu3u+7k/igM0Tml365mbU/HzkfCrD+ou7cGSrqNg
nipj/VQSgJfKRFKATEf4hMfdpKSd4rZzf8OJnq8s+kpRVC4kdHJtJjja59VvHEQR
IrN/dkycNHSBWu5UjZbXOO5X3mjwuIh9gpLGZ+nHTqgTpT324q5BLVsH8/ywRGif
Ij+HQL1O5bJO2Q2/18iL1TbnMSbDwrKdb1edb4bgDuWB4o0xSTXsherTgeXu76gN
9FY28tuAKSd34yqp7GZaYcjtkskbWPRtYhOID2cOgQKBgQDhWYQ7bO2Uge9qUwdf
v3utaERNsqAHptFt0Z69ilEtkfXruEocHm+y5GMgBcl26NDDxbeudqz1n78N2/VN
tHlCfPRSaGfFAAht0MNQT2vk/8sSGiWuNj0nPQ/5biN7L7twMBN4hNgdve3rsdwj
BVvFAXl+RF0kCF87Tb1GqQfc4QKBgQDHyeCxQysMZF5VWl4Z2VnMZNIXGs0/z0MX
hYRBnvMKlJ7f8am5Q9vOCyVhASJfpcljmyxq9q9U06aZAQC3TGwxzHFU49gJjLss
ETxwlCIVv9sx/YfX7NOU+f8oXgPXSHjlwteG77GHhznZGH1YMxhmYpYuQAlDov96
54ahmQgOZQKBgQCLgPoeO6m8JNAM10eaW5T53bFjFDCDbB78SCdoGQd2GYTPHzAp
xiHJ8h73RP3aMfiDKZQzWd5q1DDXKJLsPsxOf8JUfbO3vfR53yH6RqB5jGyvUSaA
ea1T7DZGaItoiD0WD30OP9b2Woy+O0YXHAsao3YN1859Lv2dXdvNsXhfoQKBgAk+
/YgWnLJpHriCgyJdHbvI2rQ5CFCwreEzy6ghzAusTyH8FCe/zTde6q7vl4CnsodK
kiatHfYmCWrBUiApiERRutj75m6itbLXZV2sbVtmUqWgnx3OFwio/TcICtN2ximT
99eF1kh4aKP0bzCaxMFQRLUFP6iCraIpfuCRrX7FAoGAFZGFuvW1W9VF31JyrMYJ
y/BH7vja3d9iZlhFzttNZ+wmiXG4irrI/fLJgmXK6dI3MfIhKPAYi9nnza2kcR1q
EV9QObA4NV86RWnc8sAHbDGooe9VK5eJ5jjD7Tq/ZZiLiHGOZit3HylNilOb0k3V
sgMcp0F3ZQaMbg35K9rSgZE=
-----END PRIVATE KEY-----

Using a Python dictionary to hard-code the Okta domain and API token is encouraged for development; In production, you should use a more secure way of storing these values. This library supports a few different configuration sources, covered in the configuration reference section.

Extending the Client

When creating a new client, we allow for you to pass custom instances of okta.request_executor, okta.http_client and okta.cache.cache.

from okta.client import Client as OktaClient
# Assuming implementations are in project.custom
from project.custom.request_executor_impl import RequestExecImpl
from project.custom.http_client_impl import HTTPClientImpl
from project.custom.cache_impl import CacheImpl


config = {
    'orgUrl': 'https://{yourOktaDomain}',
    'token': 'YOUR_API_TOKEN',
    'requestExecutor': RequestExecImpl,
    'httpClient': HTTPClientImpl,
    'cacheManager': CacheImpl(), # pass instance of CacheImpl
    'cache': {'enabled': True}
}


async def main():
    client = OktaClient(config)
    user_info, resp, err = await client.get_user({YOUR_USER_ID})
    print(user_info)


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Extending or Creating New Classes

Example: You can create a custom cache driver by implementing okta.cache.cache

# Fully working example for Custom Cache class
from okta.cache.cache import Cache


class CacheImpl(Cache):
    def __init__(self):
        super().__init__()
        self.cache_dict = {}

    def add(self, key, value):
        self.cache_dict[key] = value

    def get(self, key):
        return self.cache_dict.get(key, None)

    def contains(self, key):
        return key in self.cache_dict

    def delete(self, key):
        if self.contains(key):
            del self.cache_dict[key]

A similar approach can be used to extend okta.request_executor:

from okta.request_executor import RequestExecutor


class RequestExecImpl(RequestExecutor):
    def __init__(self, config, cache, http_client=None):
        super().__init__(config, cache, http_client)
        # custom code

    # Note, this method shoud be defined as async
    async def create_request(self, method: str, url: str, body: dict = None,
                             headers: dict = {}, oauth=False):
        """
        Creates request for request executor's HTTP client.

        Args:
            method (str): HTTP Method to be used
            url (str): URL to send request to
            body (dict, optional): Request body. Defaults to None.
            headers (dict, optional): Request headers. Defaults to {}.

        Returns:
            dict, Exception: Tuple of Dictionary repr of HTTP request and
            exception raised during execution
        """
        # custom code

    # Note, this method shoud be defined as async
    async def execute(self, request, response_type=None):
        """
        This function is the high level request execution method. Performs the
        API call and returns a formatted response object

        Args:
            request (dict): dictionary object containing request details

        Returns:
            (OktaAPIResponse, Exception): Response obj for the Okta API, Error
        """
        # custom code

and okta.http_client:

from okta.http_client import HTTPClient


class HTTPClientImpl(HTTPClient):
    def __init__(self, http_config={}):
        super().__init__(http_config)
        # custom code

    # Note, this method shoud be defined as async
    async def send_request(self, request):
        """
        This method fires HTTP requests

        Arguments:
            request {dict} -- This dictionary contains all information needed
            for the request.
            - HTTP method (as str)
            - Headers (as dict)
            - Request body (as dict)

        Returns:
            Tuple(RequestInfo, ClientResponse, JSONBody, ErrorObject)
            -- A tuple containing the request and response of the HTTP call
        """
        # custom code

Usage guide

These examples will help you understand how to use this library.

Once you initialize a client, you can call methods to make requests to the Okta API. The client uses asynchronous methods to operate. Most methods are grouped by the API endpoint they belong to. For example, methods that call the Users API are organized under the User resource client (okta.resource_clients.user_client.py).

Asynchronous I/O is fairly new to Python after making its debut in Python 3.5. It's powered by the asyncio library which provides avenues to produce concurrent code. This allows developers to define async functions and await asynchronous calls within them. For more information, you can check out the Python docs.

Calls using await must be made in an async def function. That function must be called by asyncio (see example below).

from okta.client import Client as OktaClient
import asyncio

async def main():
    client = OktaClient()
    users, resp, err = await client.list_users()
    print(len(users))

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Authenticate a User

This library should only be used with the Okta management API. To call the Authentication API, you should construct your own HTTP requests.

Assume the client is instantiated before each example below.

from okta.client import Client as OktaClient
import okta.models as models
client = OktaClient({'orgUrl': 'https://test.okta.com', 'token': 'YOUR_API_TOKEN'})

Get and set custom attributes

Custom attributes must first be defined in the Okta profile editor. Then, you can work with custom attributes on a user:

Feature is fully supported with SDK version >= 1.2.0

""" Setting attributes """
# Creating an instance through a Python Dictionary
from okta.models import UserProfile

user_profile = UserProfile({
  'firstName': 'John',
  'lastName': 'Foe',
  'email': '[email protected]',
  'login': '[email protected]',
  'customAttr': 'custom value'
})

print(user_profile.customAttr)

# Creating an empty object and using variables
user_profile = models.UserProfile()
user_profile.first_name = 'John'
user_profile.last_name = 'Doe'
user_profile.email = '[email protected]'
user_profile.login = '[email protected]'
user_profile.customAttr = 'custom value'

NOTE: Custom Attributes case must be the same as defined in the Profile tab of your organization UI.

To maintain consistent code, you can use camelCase for all default attributes:

Feature is fully supported with SDK version >= 1.6.1

user_profile = models.UserProfile()
user_profile.firstName = 'John'
user_profile.lastName = 'Doe'
user_profile.email = '[email protected]'
user_profile.login = '[email protected]'
user_profile.customAttr = 'custom value'

Full example:

from okta.client import Client as OktaClient
import asyncio

async def main():
    client = OktaClient()

    # create user with custom attribute
    body = {
      "profile": {
        "firstName": "John",
        "lastName": "Smith",
        "email": "[email protected]",
        "login": "[email protected]",
        "customAttr": "custom value"
      },
      "credentials": {
        "password" : { "value": "Knock*knock*neo*111" }
      }
    }
    result = await client.create_user(body)

    # create user without custom attribute
    body = {
      "profile": {
        "firstName": "Neo",
        "lastName": "Anderson",
        "email": "[email protected]",
        "login": "[email protected]"
      },
      "credentials": {
        "password" : { "value": "Knock*knock*neo*111" }
      }
    }
    result = await client.create_user(body)

    users, resp, err = await client.list_users()
    for user in users:
        print(user.profile.first_name, user.profile.last_name)
        try:
            print(user.profile.customAttr)
        except:
            print('User has no customAttr')

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Output should look like the following (removed pre-existing users from output):

John Smith
custom value
Neo Anderson
User has no customAttr

Get and set custom headers

Feature appears in v1.3.0

It is possible to set custom headers, which will be sent with each request:

import asyncio

from okta.client import Client as OktaClient

async def main():
    client = OktaClient()

    # set custom headers
    client.set_custom_headers({'Custom-Header': 'custom value'})

    # perform different requests with custom headers
    users, resp, err = await client.list_users()
    for user in users:
        print(user.profile.first_name, user.profile.last_name)

    # clear all custom headers
    client.clear_custom_headers()

    # output should be: {}
    print(client.get_custom_headers())


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Note, that custom headers will be overwritten with default headers with the same name. This doesn't allow breaking the client. Get default headers:

client.get_default_headers()

Get a User

user, resp, err = await client.get_user(user.id)
# OR using their login
user, resp, err = await client.get_user(user.profile.login)

List all Users

users, resp, err = await client.list_users()

Filter or search for Users

# Query parameters are optional on methods that can use them!
# Check the method definition for details on which query parameters are accepted.
query_parameters = {'filter': 'status eq "ACTIVE"'}
users, resp, err = await client.list_users(query_parameters)

Create a User

# Create Password
password = models.PasswordCredential({
    'value': 'Password123'
})

# Create User Credentials
user_creds = models.UserCredentials({
    'password': password
})

# Create User Profile and CreateUser Request
user_profile = models.UserProfile()
user_profile.first_name = 'John'
user_profile.last_name = 'Doe'
user_profile.email = 'John.Doe'
user_profile.login = 'John.Doe'

create_user_req = models.CreateUserRequest({
    'credentials': user_creds,
    'profile': user_profile
})

# Create User
user, resp, err = await client.create_user(create_user_req)

Update a User

# Assume user object saved to variable `user`
# Craft new profile and get user object
new_profile = user.profile
new_profile.nick_name = 'Oktanaut'
updated_user_obj = models.User({'profile': new_profile})

# Update User with new details
updated_user, _, err = await client.update_user(user.id, updated_user_obj)

Remove a User

You must first deactivate the user, and then you can delete the user.

# Assuming user starts off with a status of 'ACTIVE'

# Deactivate
resp, err = await client.deactivate_or_delete_user(user.id)
# Then delete
resp, err = await client.deactivate_or_delete_user(user.id)

List a User's Groups

users_groups, resp, err = await client.list_user_groups(user.id)

Create a Group

# Create Group Model
group_profile = models.GroupProfile({
    'name': 'Group-Test'
})
group_model = models.Group({
    'profile': group_profile
})

# Create Group
group, resp, err = await client.create_group(group_model)

Add a User to a Group

resp, err = await client.add_user_to_group(group.id, user.id)

List a User's enrolled Factors

supported_factors, resp, err = await client.list_supported_factors(user.id)

Enroll a User in a new Factor

# Create and enroll factor
sms_factor = models.SmsUserFactor({
    'profile': models.SmsUserFactorProfile({
        'phoneNumber': '+12345678901'
    })
})
enrolled_factor, _, err = await client.enroll_factor(created_user.id, sms_factor)

Activate a Factor

activate_factor_request = models.ActivateFactorRequest({
  'passCode': '123456'
})
activated_factor, resp, err = await client.activate_factor(user.id, factor.id, activate_factor_request)

Verify a Factor

verify_factor_request = models.ActivateFactorRequest({
  'passCode': '123456'
})
verified_factor, resp, err = await client.activate_factor(user.id, factor.id, verify_factor_request)

List all Applications

apps, resp, err = await client.list_applications()

Get an Application

app, resp, err = await client.get_application(app.id)

Create a SWA Application

# Create SWA Application model and SWA Application in Okta
swa_app_settings_app = models.SwaApplicationSettingsApplication({
    'buttonField': 'btn-login',
    'passwordField': 'txt-box-password',
    'usernameField': 'txt-box-username',
    'url': 'https://example.com/login.html',
    'loginUrlRegex': '^https://example.com/login.html$'
})

swa_app_settings = models.SwaApplicationSettings({
    'app': swa_app_settings_app
})

swa_app_model = models.SwaApplication({
    'label': 'SWA Test App',
    'settings': swa_app_settings,
})

app, resp, err = await client.create_application(swa_app_model)

Manage Group Schema custom atributes

There are 2 ways of creating custom attribute for Group Schema Profile:

  1. via UI of your ORG (Directory -> Profile Editor -> Groups)
  2. with the following request (create custom attribute with name "testCustomAttr"):
definition = {'custom':
                {'id': '#custom',
                 'properties':
                     {'testCustomAttr':
                         {'description': 'Custom attribute for testing purposes',
                                         'maxLength': 20,
                                         'minLength': 1,
                                         'permissions': [{'action': 'READ_WRITE',
                                                          'principal': 'SELF'}],
                                         'required': False,
                                         'title': 'Test Custom Attribute',
                                         'type': 'string'},
                          'required': []},
                 'type': 'object'
                }
             }
resp, _, err = await client.update_group_schema({'definitions': definition})

Update existing attribute:

# Get existing GroupSchema
resp, _, err = await client.get_group_schema()
# Set new title for custom attribute 'testCustomAttr'
resp.definitions.custom.properties['testCustomAttr']['title'] = 'New Title'
# Launch api request to update GroupSchema
resp, _, err = await client.update_group_schema(resp)

Call other API endpoints

Not every API endpoint is represented by a method in this library. You can call any Okta management API endpoint using this generic syntax:

# Example that doesn't return Object
request, error = await client.get_request_executor().create_request(
  method='POST',
  url='/api/v1/users/USER_ID_HERE/lifecycle/activate',
  body={},
  headers={},
  oauth=False
)

response, error = await client.get_request_executor().execute(request, None)
response_body = response.get_body()

# Example that does return Object
request, error = await client.get_request_executor().create_request(
  method='GET',
  url='/api/v1/users/USER_ID_HERE',
  body={},
  headers={},
  oauth=False
)

response, error = await client.get_request_executor().execute(request, models.User)

response_body = client.form_response_body(response.get_body())
user = response.get_type()(response_body)

Perform requests with empty parameters

Some group of api calls requires no empty parameters in request body, i.e. it is not allowed to pass within body the following samples {"some_var": ""} or {"some_var": []}. Meanwhile, other group of api calls allows setting empty parameters, which is used to clear value of related parameter on the server-side. By default all api calls are being performed without "empty" parameters, i.e. all empty parameters are being removed automatically before actual request. Starting from v2.1.0 users can control this behavior with parameter keep_empty_params:

This feature is supported with SDK version >= 2.1.0

# Default behavior, parameter "badgeNumber" won't be present in actual request
from okta.client import Client as OktaClient
import asyncio

async def main():
    client = OktaClient()
    user_id = 'REDACTED'
    user_params = {"profile": {"phoneNumber": "1234567890", "badgeNumber": ""}}
    updated_user, resp, err = await client.partial_update_user(user_id, user_params)
    print(updated_user)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

The same behavior when "keep_empty_params" is set to "False" (which is default for most methods, except UserSchema methods):

# Parameter "badgeNumber" won't be present in actual request
from okta.client import Client as OktaClient
import asyncio

async def main():
    client = OktaClient()
    user_id = 'REDACTED'
    user_params = {"profile": {"phoneNumber": "1234567890", "badgeNumber": ""}}
    updated_user, resp, err = await client.partial_update_user(user_id, user_params, keep_empty_params=False)
    print(updated_user)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

All empty parameters will be present in request when "keep_empty_params" is set to "True":

# Parameter "badgeNumber" will be present in actual request
from okta.client import Client as OktaClient
import asyncio

async def main():
    client = OktaClient()
    user_id = 'REDACTED'
    user_params = {"profile": {"phoneNumber": "1234567890", "badgeNumber": ""}}
    updated_user, resp, err = await client.partial_update_user(user_id, user_params, keep_empty_params=True)
    print(updated_user)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Exceptions

Starting from v1.1.0 SDK introduces exceptions, which are disabled by default, thus feature is backward compatible. To force client raise an exception instead of returning custom error, option 'raiseException' should be provided:

import asyncio

from okta.client import Client as OktaClient
from okta.exceptions import OktaAPIException


async def main():
    config = {'orgUrl': 'https://{yourOktaDomain}',
              'token': 'bad_token',
              'raiseException': True}
    client = OktaClient(config)
    try:
        users, resp, err = await client.list_users()
        for user in users:
            print(user.profile.first_name, user.profile.last_name)
    except OktaAPIException as err:
        print(err)


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Result should look like:

{'errorCode': 'E0000011', 'errorSummary': 'Invalid token provided', 'errorLink': 'E0000011', 'errorId': 'oaeqWcqizEUQ_-iHc2hCbH9LA', 'errorCauses': []}

List of available exceptions: OktaAPIException, HTTPException (to raise instead of returning errors OktaAPIError and HTTPError respectively). It is possible to inherit and/or extend given exceptions:

from okta.exceptions import HTTPException


class MyHTTPException(HTTPException):
    pass


raise MyHTTPException('My HTTP Exception')

Pagination

If your request comes back with more than the default or set limit (resp.has_next() == True), you can request the next page.

Example of listing users 1 at a time:

query_parameters = {'limit': '1'}
users, resp, err = await client.list_users(query_parameters)

# Check if there more pages follow
if resp.has_next():
  users, err = await resp.next()  # Returns list of 1 user after the last retrieved user

# Iterate through all of the rest of the pages
while resp.has_next():
  users, err = await resp.next()
  # Do stuff with users in users

print(resp.has_next()) # False
try:
  await resp.next()
except StopAsyncIteration:
  # Handle Exception raised

Here's a complete example:

from okta.client import Client as OktaClient
import asyncio

async def main():
    client = OktaClient()
    users, resp, err = await client.list_users()
    while True:
        for user in users:
            print(user.profile.login) # Add more properties here.
        if resp.has_next():
            users, err = await resp.next()
        else:
            break

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

If you require access to the response headers during a pagination operation, set the includeResponse parameter to True in the call to the next method; this returns the response as a third tuple value. See the following example:

from okta.client import Client as OktaClient
import asyncio

async def main():
    client = OktaClient()
    users, resp, err = await client.list_users()
    while True:
        for user in users:
            print(user.profile.login) 
        if resp.has_next():
            users, err, response = await resp.next(True) # Specify True to receive the response object as the third part of the tuple for further analysis
        else:
            break

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Logging

Feature appears in version 1.5.0

SDK v1.5.0 introduces logging for debug purposes. Logs are disabled by default, thus SDK behavior remains the same. Logging should be enabled explicitly via client configuration or via a configuration file:

from okta.client import Client as OktaClient


config = {"logging": {"enabled": True}}
client = OktaClient(config)

SDK utilizes the standard Python library logging. By default, log level INFO is set. You can set another log level via config:

from okta.client import Client as OktaClient
import logging

config = {"logging": {"enabled": True, "logLevel": logging.DEBUG}}
client = OktaClient(config)

NOTE: DO NOT SET DEBUG LEVEL IN PRODUCTION!

Here's a complete example:

from okta.client import Client as OktaClient
import asyncio
import logging

async def main():
    config = {"logging": {"enabled": True, "logLevel": logging.DEBUG}}
    client = OktaClient(config)
    users, resp, err = await client.list_users()
    assert users is not None

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

You should now see logs in your console. Actual API Tokens will be logged to the console, so use caution and never use DEBUG level logging in production.

What it being logged: requests, http errors, caching responses.

Configuration reference

This library looks for configuration in the following sources:

  1. An okta.yaml file in a .okta folder in the current user's home directory (~/.okta/okta.yaml or %userprofile%\.okta\okta.yaml). See a sample YAML Configuration
  2. A okta.yaml file in the application or project's root directory. See a sample YAML Configuration
  3. Environment variables
  4. Configuration explicitly passed to the constructor (see the example in Getting started)

Only ONE source needs to be provided!

Higher numbers win. In other words, configuration passed via the constructor will OVERRIDE configuration found in environment variables, which will override configuration in the designated okta.yaml files.

YAML configuration

When you use an API Token instead of OAuth 2.0 the full YAML configuration looks like:

okta:
  client:
    connectionTimeout: 30 # seconds
    orgUrl: "https://{yourOktaDomain}"
    proxy:
      port: { proxy_port }
      host: { proxy_host }
      username: { proxy_username }
      password: { proxy_password }
    token: "YOUR_API_TOKEN"
    requestTimeout: 0 # seconds
    rateLimit:
      maxRetries: 4
    logging:
      enabled: true
      logLevel: INFO

When you use OAuth 2.0 the full YAML configuration looks like:

okta:
  client:
    connectionTimeout: 30 # seconds
    orgUrl: "https://{yourOktaDomain}"
    proxy:
      port: { proxy_port }
      host: { proxy_host }
      username: { proxy_username }
      password: { proxy_password }
    authorizationMode: "PrivateKey"
    clientId: "YOUR_CLIENT_ID"
    scopes:
      - scope.1
      - scope.2
    privateKey: |
      -----BEGIN RSA PRIVATE KEY-----
      MIIEogIBAAKCAQEAl4F5CrP6Wu2kKwH1Z+CNBdo0iteHhVRIXeHdeoqIB1iXvuv4
      THQdM5PIlot6XmeV1KUKuzw2ewDeb5zcasA4QHPcSVh2+KzbttPQ+RUXCUAr5t+r
      0r6gBc5Dy1IPjCFsqsPJXFwqe3RzUb...
      -----END RSA PRIVATE KEY-----
    requestTimeout: 0 # seconds
    rateLimit:
      maxRetries: 4
    logging:
      enabled: true
      logLevel: INFO

If a proxy is not going to be used for the SDK, you may omit the okta.client.proxy section from your okta.yaml file

Environment variables

Each one of the configuration values above can be turned into an environment variable name with the _ (underscore) character and UPPERCASE characters. The following are accepted:

  • OKTA_CLIENT_AUTHORIZATIONMODE
  • OKTA_CLIENT_ORGURL
  • OKTA_CLIENT_TOKEN
  • OKTA_CLIENT_CLIENTID
  • OKTA_CLIENT_SCOPES
  • OKTA_CLIENT_PRIVATEKEY
  • OKTA_CLIENT_USERAGENT
  • OKTA_CLIENT_CONNECTIONTIMEOUT
  • OKTA_CLIENT_REQUESTTIMEOUT
  • OKTA_CLIENT_CACHE_ENABLED
  • OKTA_CLIENT_CACHE_DEFAULTTTI
  • OKTA_CLIENT_CACHE_DEFAULTTTL
  • OKTA_CLIENT_PROXY_PORT
  • OKTA_CLIENT_PROXY_HOST
  • OKTA_CLIENT_PROXY_USERNAME
  • OKTA_CLIENT_PROXY_PASSWORD
  • OKTA_CLIENT_RATELIMIT_MAXRETRIES
  • OKTA_TESTING_TESTINGDISABLEHTTPSCHECK

Other configuration options

Starting with SDK v2.3.0 you can provide custom SSL context:

import asyncio
import ssl

from okta.client import Client as OktaClient


async def main():
    # create default context for demo purpose
    ssl_context = ssl.create_default_context()
    client = OktaClient({"sslContext": ssl_context})
    users, resp, err = await client.list_users()
    print(users)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Rate Limiting

The Okta API will return 429 responses if too many requests are made within a given time. Please see Rate Limiting at Okta for a complete list of which endpoints are rate limited. When a 429 error is received, the X-Rate-Limit-Reset header will tell you the time at which you can retry. This section discusses the method for handling rate limiting with this SDK.

Built-In Retry

This SDK uses the built-in retry strategy to automatically retry on 429 errors. You can use the default configuration options for the built-in retry strategy, or provide your desired values via the client configuration.

You can configure the following options when using the built-in retry strategy:

Configuration Option Description
client.requestTimeout The waiting time in seconds for a request to be resolved by the client. Less than or equal to 0 means "no timeout". The default value is 0 (None).
client.rateLimit.maxRetries The number of times to retry.

Check out the Configuration Reference section for more details about how to set these values via configuration.

Building the SDK

In most cases, you won't need to build the SDK from source. If you want to build it yourself, you'll need these prerequisites:

  • Clone the repo
  • Run python setup.py build from the root of the project (assuming Python is installed)
  • Ensure tests run succesfully. Install tox if not installed already using: pip install tox. Run tests using tox in the root directory of the project.

Contributing

We're happy to accept contributions and PRs! Please see the Contribution Guide to understand how to structure a contribution.

okta-sdk-python's People

Contributors

alimcmaster1 avatar bjr-okta avatar bretterer avatar bryanapellanes-okta avatar coreysmithring avatar corylevine avatar drewcarmichael-okta avatar gabrielsroka avatar glebinsky avatar jmelberg-okta avatar joelfranusic-okta avatar justinabrokwah-okta avatar keeshendriks avatar lboyette-okta avatar lebarondecharlus avatar mattcumminswmg avatar nbarbettini avatar nicolastrres avatar omidraha avatar ph-ngn avatar rdegges avatar scheblein avatar serhiibuniak-okta avatar shiqiyang-okta avatar tbelote-okta avatar vijetmahabaleshwar-okta avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

okta-sdk-python's Issues

Full user profile

It seems like the user profile only sets a subset of the profile fields.

Is there a way to get the full profile?

list_applications returning KeyError

I'm trying to list all the applications in an okta instance and getting the following result from the coroutine.

(None, <okta.api_response.OktaAPIResponse object at 0x7f2e4490dd50>, KeyError('office365'))

import asyncio
import os
from okta.client import Client

OKTA_URL = os.environ.get("OKTA_URL")
OKTA_SSWS = os.environ.get("OKTA_SSWS")

client = Client({"orgUrl": OKTA_URL, "token": OKTA_SSWS})
loop = asyncio.get_event_loop()
x = loop.run_until_complete(client.list_applications())
print(x)

Pipenv Won't Install Version 1.0.0 of Okta

When trying to run pipenv install okta to install version 1.0.0 on Python 3.8 I get:

Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/pipenv/resolver.py", line 807, in <module>
    main()
  File "/usr/local/lib/python3.8/site-packages/pipenv/resolver.py", line 802, in main
    _main(parsed.pre, parsed.clear, parsed.verbose, parsed.system, parsed.write,
  File "/usr/local/lib/python3.8/site-packages/pipenv/resolver.py", line 785, in _main
    resolve_packages(pre, clear, verbose, system, write, requirements_dir, packages)
  File "/usr/local/lib/python3.8/site-packages/pipenv/resolver.py", line 746, in resolve_packages
    results, resolver = resolve(
  File "/usr/local/lib/python3.8/site-packages/pipenv/resolver.py", line 728, in resolve
    return resolve_deps(
  File "/usr/local/lib/python3.8/site-packages/pipenv/utils.py", line 1378, in resolve_deps
    results, hashes, markers_lookup, resolver, skipped = actually_resolve_deps(
  File "/usr/local/lib/python3.8/site-packages/pipenv/utils.py", line 1095, in actually_resolve_deps
    resolver.resolve_constraints()
  File "/usr/local/lib/python3.8/site-packages/pipenv/utils.py", line 847, in resolve_constraints
    marker = marker_from_specifier(candidate.requires_python)
  File "/usr/local/lib/python3.8/site-packages/pipenv/vendor/requirementslib/models/markers.py", line 719, in marker_from_specifier
    for marker_segment in cleanup_pyspecs(spec):
  File "/usr/local/lib/python3.8/site-packages/pipenv/vendor/requirementslib/models/markers.py", line 288, in cleanup_pyspecs
    for op_and_version_type, versions in _group_by_op(tuple(specs)):
  File "/usr/local/lib/python3.8/site-packages/pipenv/vendor/requirementslib/models/markers.py", line 203, in _group_by_op
    specs = [_get_specs(x) for x in list(specs)]
  File "/usr/local/lib/python3.8/site-packages/pipenv/vendor/requirementslib/models/markers.py", line 203, in <listcomp>
    specs = [_get_specs(x) for x in list(specs)]
  File "/usr/local/lib/python3.8/site-packages/pipenv/vendor/requirementslib/models/markers.py", line 187, in _get_specs
    for spec in set(specset):
TypeError: 'LegacySpecifier' object is not iterable

Running it with -v points to an issue when it's attempting to generate the hashes for Okta:

Generating hashes:
  flask-pyoidc
  cffi
  pluggy
  pyasn1-modules
  pyasn1
  ecdsa
  jsonschema
  attrs
  future
  flatdict
  httplib2
  flask-restful-swagger-2
  webargs
  aniso8601
  pytest-recording
  flask-swagger-ui
  pyparsing
  certifi
  iniconfig
  pycryptodome
  oic
  pyfakefs
  urllib3
  flake8
  oauth2client
  multidict
  flask
  requests
  importlib-resources
  py-healthcheck
  pycodestyle
  flask-smorest
  pytest-asyncio
  pycparser
  pyrsistent
  safrs
  packaging
  vcrpy
  pymysql
  mccabe
  pytest-mock
  toml
  mako
  markupsafe
  pytest
  click
  pydash
  rsa
  pytz
  itsdangerous
  wrapt
  beaker
  flask-cors
  pyjwt
  simplejson
  defusedxml
  py
  xmltodict
  chardet
  flask-oidc
  flask-caching
  cryptography
  pyflakes
  idna
  typing-extensions
  flask-restful
  mistune
  werkzeug
  pyjwkest
  marshmallow
  aiohttp
  apispec
  six
  sqlalchemy
  pyyaml
  jinja2
  asyncio
  pycryptodomex
  async-timeout
  flasgger
  pymongo
  flask-sqlalchemy
  okta
  python-jose
  inflect
  yarl
  aenum
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/pipenv/resolver.py", line 807, in <module>
    main()
  File "/usr/local/lib/python3.8/site-packages/pipenv/resolver.py", line 802, in main
    _main(parsed.pre, parsed.clear, parsed.verbose, parsed.system, parsed.write,
  File "/usr/local/lib/python3.8/site-packages/pipenv/resolver.py", line 785, in _main
    resolve_packages(pre, clear, verbose, system, write, requirements_dir, packages)
  File "/usr/local/lib/python3.8/site-packages/pipenv/resolver.py", line 746, in resolve_packages
    results, resolver = resolve(
  File "/usr/local/lib/python3.8/site-packages/pipenv/resolver.py", line 728, in resolve
    return resolve_deps(
  File "/usr/local/lib/python3.8/site-packages/pipenv/utils.py", line 1378, in resolve_deps
    results, hashes, markers_lookup, resolver, skipped = actually_resolve_deps(
  File "/usr/local/lib/python3.8/site-packages/pipenv/utils.py", line 1095, in actually_resolve_deps
    resolver.resolve_constraints()
  File "/usr/local/lib/python3.8/site-packages/pipenv/utils.py", line 847, in resolve_constraints
    marker = marker_from_specifier(candidate.requires_python)
  File "/usr/local/lib/python3.8/site-packages/pipenv/vendor/requirementslib/models/markers.py", line 719, in marker_from_specifier
    for marker_segment in cleanup_pyspecs(spec):
  File "/usr/local/lib/python3.8/site-packages/pipenv/vendor/requirementslib/models/markers.py", line 288, in cleanup_pyspecs
    for op_and_version_type, versions in _group_by_op(tuple(specs)):
  File "/usr/local/lib/python3.8/site-packages/pipenv/vendor/requirementslib/models/markers.py", line 203, in _group_by_op
    specs = [_get_specs(x) for x in list(specs)]
  File "/usr/local/lib/python3.8/site-packages/pipenv/vendor/requirementslib/models/markers.py", line 203, in <listcomp>
    specs = [_get_specs(x) for x in list(specs)]
  File "/usr/local/lib/python3.8/site-packages/pipenv/vendor/requirementslib/models/markers.py", line 187, in _get_specs
    for spec in set(specset):
TypeError: 'LegacySpecifier' object is not iterable

I am able to install version 0.0.4 without issues

Can't get values for existing custom attributes

When using the Okta User API (/api/v1/users/${userId}) with requests or Postman, I'm able to retrieve all attributes, including existing custom attributes.

When using the SDK I only get a predefined list of attributes from the UserProfile class. The README mentions how to modify UserProfile and set new values for a custom attribute. However, I can't figure out a way to retrieve existing values for custom attributes.

Assign roles to User

Hi everyone, I am looking for a way to assign roles to a user. Specifically I am trying to make a user administrator of an application. Is there some way to do that using the oktasdk at this moment?

Where is the new location of okta.framework.OktaError?

First off congrats on the 1.0.0 release! :-)

We have some code that uses OktaError and expects to find it at from okta.framework.OktaError import OktaError as in your 0.0.4 release, but it looks like things have changed since 1.0.0. What is the new exception type that I should use now, and does it still have the same error codes as before? E.g. "E0000006".

Here's the error message that I get:

cartography/intel/okta/__init__.py:3: in <module>
    from okta.framework.OktaError import OktaError
E   ModuleNotFoundError: No module named 'okta.framework'

And here's the line: https://github.com/lyft/cartography/blob/2f3016bfe1cc4543c0d526fa067d166a70116b26/cartography/intel/okta/__init__.py#L3

And here's our failing CI build: https://travis-ci.org/github/lyft/cartography/builds/737221215#L529

Possibly related to

Appreciate the help!

asyncio conflict with python 3.8?

After including the okta sdk in my django project I got the following error:

Traceback (most recent call last):
  File "./manage.py", line 19, in <module>
    from configurations.management import execute_from_command_line
  File "/home/appuser/.local/lib/python3.8/site-packages/configurations/__init__.py", line 1, in <module>
    from .base import Configuration  # noqa
  File "/home/appuser/.local/lib/python3.8/site-packages/configurations/base.py", line 5, in <module>
    from django.conf import global_settings
  File "/home/appuser/.local/lib/python3.8/site-packages/django/conf/__init__.py", line 19, in <module>
    from django.core.validators import URLValidator
  File "/home/appuser/.local/lib/python3.8/site-packages/django/core/validators.py", line 8, in <module>
    from django.utils.encoding import punycode
  File "/home/appuser/.local/lib/python3.8/site-packages/django/utils/encoding.py", line 8, in <module>
    from django.utils.deprecation import RemovedInDjango40Warning
  File "/home/appuser/.local/lib/python3.8/site-packages/django/utils/deprecation.py", line 1, in <module>
    import asyncio
  File "/home/appuser/.local/lib/python3.8/site-packages/asyncio/__init__.py", line 21, in <module>
    from .base_events import *
  File "/home/appuser/.local/lib/python3.8/site-packages/asyncio/base_events.py", line 296
    future = tasks.async(future, loop=self)
                   ^
SyntaxError: invalid syntax

After some googling I think it's because the asyncio package is in conflict with what's already included in python core 3.8. Has anybody else run into the same problem?

Looks like asyncio should no longer be needed for python version beyond 3.3.
https://pypi.org/project/asyncio/

is this here in order to support python versions even lower than 3.3?

AttributeError when retrieving paginated logs

Hello I'm running into an error when using pagination with client.gets_logs().

Here is my code:

#!/usr/bin/env python3.7

from okta.client import Client as OktaClient
from datetime import datetime, timedelta
import asyncio

# Okta config
okta_config = {
    'orgUrl': 'https://company.okta.com',
    'token': 'redacted'
}


async def get_okta_logs():
    logs = []
    client = OktaClient(okta_config)
    params = {
        'filter': 'eventType eq "user.account.lock"',
        'since': (datetime.now() - timedelta(7)).isoformat()[:-3] + 'Z',
        'until': datetime.now().isoformat()[:-3] + 'Z',
        'limit': '1'
    }

    events, response, error = await client.get_logs(query_params=params)
    for event in events:
        logs.append(event)
    if response.has_next():  # Returns True
        events, error = await response.next()
        for event in events:
            logs.append(event)

    if error:
        print(f'Okta API Error: {error}')

    print(logs)

The error I receive:

Traceback (most recent call last):
  File "okta_logs.py", line 40, in <module>
    asyncio.run(get_okta_logs())
  File "/usr/local/lib/python3.8/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/usr/local/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
    return future.result()
  File "okta_logs.py", line 30, in get_okta_logs
    events, error = await response.next()
  File "/usr/local/lib/python3.8/site-packages/okta/api_response.py", line 129, in next
    result.append(self._type(item))
  File "/usr/local/lib/python3.8/site-packages/okta/models/log_event.py", line 70, in __init__
    self.authentication_context = log_authentication_context.LogAuthenticationContext(
  File "/usr/local/lib/python3.8/site-packages/okta/models/log_authentication_context.py", line 48, in __init__
    config["authenticationProvider"].upper()
AttributeError: 'NoneType' object has no attribute 'upper'

My code has no issues receiving the first page, however when I use response.next() I keep seeing the attribute error.

User model lacks some fields

The user/userProfile models are lacking some important attributes. In particular, 'employeeNumber' is missing. The PowerShell scripting does have some of these other attributes. However, what would be even better is if we could utilize other fields, particularly those which we may have added on a custom basis.

countryCode not included in UserProfile

countryCode should really be included as an argument of the UserProfile. Really, all the default attributes should be, but this is specifically necessary for provisioning licenses in O365 (or so my O365 admin tells me).

It has to be added in the following places:

  • As an attribute of the UserProfile class
  • As a key/value pair (value str) in the types dictionary which is an attribute of the UserProfile class
  • As an element of the profile_attrs list in the init method of the User class

I was able to make the above changes and successfully create users with the countryCode value as a kwarg when creating a User object.

New user is not assigned to an application

When I follow the doc:

from okta import UsersClient
# http://developer.okta.com/docs/api/getting_started/getting_a_token.html
usersClient = UsersClient('https://example.oktapreview.com/',
                          '01a2B3Cd4E5fGHiJ6K7l89mNOPQRsT0uVwXYZA1BCd')

new_user = User(login='[email protected]',
                email='[email protected]',
                firstName='Saml',
                lastName='Jackson')
user = usersClient.create_user(new_user, activate=False)

The user is created, but not assigned to an application. I have two applications after running the migration process from Stormpath. Is this the problem? That it does not know which application to assign the new user to? How do I resolve this?

Using get_logs shows an HTTP 405 error

Hello, I'm testing the SDK for our services and I keep getting an HTTP 405 error when trying to retrieve our system logs.

EVENTS: None
RESPONSE: <okta.api_response.OktaAPIResponse object at 0x7f2623dd2d30>
ERROR: {'message': "HTTP 405 {'errorCode': 'E0000022', 'errorSummary': 'HTTP 405 Method Not Allowed', 'errorId': '9cbbc6a3-3e53-488b-9229-e2a5c784a5f7'}"}

Here is my code:

from okta.client import Client as OktaClient
import asyncio
from datetime import datetime, timedelta

config = {
    'orgUrl': 'https://somecompany.okta.com',
    'token': 'redacted'
}

async def main():
    client = OktaClient(config)
    params = {
        'filter': 'eventType eq "user.session.start" and outcome.reason eq "UNKNOWN_USER"',
        'since': (datetime.now() - timedelta(7)).isoformat()[:-3] + 'Z',
        'until': datetime.now().isoformat()[:-3] + 'Z'
    }
    events, response, error = await client.get_logs(query_params=params)
    print(f"EVENTS: {events}")
    print(f"RESPONSE: {response}")
    print(f"ERROR: {error}")

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Tried checking if it was my parameters so I tried removing 'filter' or using only 'since' for the past day. I even tried with no parameters and I'm still getting a 405 error code.

sample to create a new App

I am trying to create a new app through the create_app_instance method but without success.

The configuration of the AppInstance is not obvious.

app_client = AppInstanceClient("...","...")
app = AppInstance()
app.label = "my-new-app"
app.signOnMode = "OPENID_CONNECT"
app_client.create_app_instance(app)

gets me a
okta.framework.OktaError.OktaError: Internal Server Error

If I also set credentials, visibility, settings and accessibility (by just using values from an existing app I get a
okta.framework.OktaError.OktaError: Api validation failed: mediated

Could you post some simple sample to create an App ?

What is missing

Here is my script, almost identical to the sample script in the README

from okta.client import Client as OktaClient
import asyncio
import os

config = {
    'orgUrl': os.getenv("OKTA_DOMAIN"),
    'token': os.getenv("OKTA_API_TOKEN")
}

client = OktaClient(config)

async def main():
    users, resp, err = await client.list_users()
    print(len(users))

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Here is the error message

Traceback (most recent call last):
  File "okta-query.py", line 1, in <module>
    from okta.client import Client as OktaClient
  File "/Users/user/.python_3_envs/work_3.6/lib/python3.6/site-packages/okta/client.py", line 20, in <module>
    from okta.config.config_setter import ConfigSetter
  File "/Users/user/.python_3_envs/work_3.6/lib/python3.6/site-packages/okta/config/config_setter.py", line 4, in <module>
    from okta.constants import _GLOBAL_YAML_PATH, _LOCAL_YAML_PATH
  File "/Users/user/.python_3_envs/work_3.6/lib/python3.6/site-packages/okta/constants.py", line 2, in <module>
    import okta.models as models
  File "/Users/user/.python_3_envs/work_3.6/lib/python3.6/site-packages/okta/models/__init__.py", line 21, in <module>
    import okta.models.activate_factor_request as activate_factor_request
AttributeError: module 'okta' has no attribute 'models'

I'm using Python 3.6.8 and the okta library is 1.0.2 (most recent). Am I missing a library or setup step? Thanks.

HTTP delete requests returns `204 No Content` But keyword fail

It fails because the method try to return Utils.deserialize when response.text is empty --> "No JSON object could be decoded"

So either

  1. Return None on all 204 expected keywords
    Or
  2. At Utils.deserialize add one more if case:
    Util.py line:19
        if from_data:
            json_dump = {}
        elif isinstance(from_data, six.text_type) or isinstance(from_data, six.string_types):
            json_dump = json.loads(from_data)
        else:
            json_dump = from_data

Or whatever you think is fit here

Rate limit date format isn't handled properly and prevents hitting backoff code

in okta.request_executor, it seems that when you get rate limited, the server passes back a Date header that contains a string containing a date/time with an unexpected format.

It seems that string is formatted in a way that the okta.util.convert_date_time_to_seconds doesn't expect and throws an exception.

if attempts < max_retries and (error or check_429):
      date_time = headers.get("Date", "")
      if date_time:
          date_time = convert_date_time_to_seconds(date_time)

It results in this (incomplete) stack trace:

Traceback (most recent call last):
   File "/usr/local/lib/python3.6/dist-packages/okta/resource_clients/application_client.py", line 1536, in assign_user_to_application
    .execute(request, AppUser)
  File "/usr/local/lib/python3.6/dist-packages/okta/request_executor.py", line 138, in execute
    _, response, response_body, error = await self.fire_request(request)
  File "/usr/local/lib/python3.6/dist-packages/okta/request_executor.py", line 176, in fire_request
    self.fire_request_helper(request, 0, time.time())
  File "/usr/local/lib/python3.6/dist-packages/okta/request_executor.py", line 229, in fire_request_helper
    date_time = convert_date_time_to_seconds(date_time)
  File "/usr/local/lib/python3.6/dist-packages/okta/utils.py", line 53, in convert_date_time_to_seconds
    DATETIME_FORMAT)
  File "/usr/lib/python3.6/_strptime.py", line 565, in _strptime_datetime
    tt, fraction = _strptime(data_string, format)
  File "/usr/lib/python3.6/_strptime.py", line 362, in _strptime
    (data_string, format))
ValueError: time data 'Fri, 15 Jan 2021 05:53:21 GMT' does not match format '%Y-%m-%dT%H:%M:%S.%fZ'

I suggest standardizing this internally (ideally using ISO-8601 datetime format)

'RADIUS_FOR_CISCO_ASA' & 'AWS_FEDERATED_LOGIN' not recognized as valid ApplicationSignOnModes

When getting a list of all applications via: apps, resp, err = await client.list_applications() it fails with the message: 'RADIUS_FOR_CISCO_ASA' is not a valid ApplicationSignOnMode & 'AWS_FEDERATED_LOGIN' is not a valid ApplicationSignOnMode respectively. It seems that those two application sign on modes need to be added to the application_sign_on_mode.py file. Also, equivalent models need to be created for each. Once those models are created, they need to be added to the OKTA_APP_SIGN_ON_TO_MODEL variable in the constants.py file.

Issue with authenticate() method

Following is my code snippet:

def authenticate(self, username, password):
        self.logger.debug("Attempting to authenticate user: %s" % username)
        auth_client = AuthClient(base_url=self.base_url, api_token=self.api_key)
        auth_result = auth_client.authenticate(username=username, password=password)
        self.logger.debug("Authentication result: username: %s status: %s" % (username, auth_result.status))
        return auth_result.status

I get the following error while attempting to authenticate a user that has been successfully created on the Okta server.

>>> okta_backend.authenticate(username, password)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/shetty/work/authentication/models.py", line 136, in authenticate
    return auth_engine.authenticate(username, password)
  File "/Users/shetty/work/authentication/auth_okta.py", line 35, in authenticate
    auth_result = auth_client.authenticate(username=username, password=password)
  File "/Users/shetty/pyenv/src/okta/okta/AuthClient.py", line 41, in authenticate
    response = ApiClient.post_path(self, '/', request, params=params)
  File "/Users/shetty/pyenv/src/okta/okta/framework/ApiClient.py", line 88, in post_path
    return self.post(self.base_url + url_path, data, params)
  File "/Users/shetty/pyenv/src/okta/okta/framework/ApiClient.py", line 67, in post
    if self.__check_response(resp, attempts):
  File "/Users/shetty/pyenv/src/okta/okta/framework/ApiClient.py", line 102, in __check_response
    raise OktaError(json.loads(resp.text))
OktaError: Authentication failed

Would be nice to have a sample code as a use case to pull custom okta user attributes

I am looking for an example code that would be a replacement for the code below:
oidc = OpenIDConnect(app)
okta_client = UsersClient("{{ OKTA_ORG_URL }}", "{{ OKTA_AUTH_TOKEN }}")

@app.before_request
def before_request():
if oidc.user_loggedin:
g.user = okta_client.get_user(oidc.user_getfield("sub"))
else:
g.user = None

I want to be able to access custom attributes thru g.user

Thanks

Expose request "headers" to provide additional functionality

Expose the ability to provide additional "headers" (such as "X-Forwarded-For") for factor verification (example method = "verify_factor")

Some API calls have the ability to send additional headers.

I may consider forking this for a pull request.

Is this repository actively maintained?

Hi all,

Just wondering if this repo is actively maintained or whether I should just be using the REST API directly with something like requests. I ask because I see there are a few unanswered issues and PRs over the past few months.

The request body is not well-formed during User update

When reading a user from the API, altering some of the details on the profile, and then updating details via the API I see the following error

okta.framework.OktaError.OktaError: The request body was not well-formed`

Code:

try:
    # Okta returns [] if not found
    okta_users = users_client.get_users(
        query=supplier_details['email']
    )
except OktaError as okta_error:
    raise OktaCommsError(str(okta_error))

okta_user = okta_users[0]
okta_user.profile.email = supplier_details['email']
okta_user.profile.firstName = supplier_details['first_name']
okta_user.profile.lastName = supplier_details['last_name']
okta_user.profile.login = supplier_details['login']
okta_user.profile.displayName = supplier_details['display_name']
okta_user.profile.primaryPhone = supplier_details['primary_phone']
okta_user.profile.city = supplier_details['city']

try:
    users_client.update_user(okta_user)
except OktaError as okta_error:  # Error raises here
    # Something failed in the update
    raise OktaCommsError(str(okta_error))

Is there something I'm missing here?

pip install broken?

root:~# pip install okta
Collecting okta
  Could not find a version that satisfies the requirement okta (from versions: )
No matching distribution found for okta

root:~# pip --version
pip 7.0.3 from /usr/local/lib/python2.7/dist-packages (python 2.7)

HTTP/400 Errors if the API Token contains a newline

Ran into an issue where I am decrypting an [AWS encrypted] Okta API token that ended with a newline.
Normally, you would not include a newline on the token, but because of the tools I was using it inserted an extra \n. It was non-obvious as the inputs to my app were encrypted.
Some deeper urllib3 debug logging helped

When requests module constructed the POST, it contained the SSWS token value with the extra \n, which is not a valid HTTP request.
The Apache front end webserver at okta rejects the malformed request, and returns some standard apache HTML error page.
The okta-sdk library never expects HTML payloads in the response, and throws a JSON parsing exception.

(The request is not reaching the Okta application servers where JSON can be returned in the response. It is being dropped as a HTTP/400 by the Apache service fronting the application servers).

For defensiveness, I am suggesting the module handles non-HTML responses gracefully, and/or strips any whitespace/newlines from the user specified token.

I have since reencrypted the token without the extra newline, and everything works, but in case someone else has this problem the module could handle this edge case.

Issue while initializing an AuthClient instance

The following error is seen while initializing an AuthClient object.

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/shetty/work/django/pyenv/src/okta/okta/AuthClient.py", line 8, in __init__
    ApiClient.__init__(self, base_url + '/api/v1/authn', api_token)
  File "/Users/shetty/work/django/pyenv/src/okta/okta/framework/ApiClient.py", line 12, in __init__
    self.base_url = kwargs['base_url'] or args[0]
KeyError: 'base_url'```

Client doesn't work on Python3.5 (likely other versions as well)

Forgive me, I'm new to python. However, I've had a friend also look at this to verify and he was able to reproduce on py3.2. Looks like the pathing is relative in https://github.com/okta/oktasdk-python/blob/master/okta/__init__.py. So, when python tries to import it's looking for (in my case i'm using a virtual env) site-packages/AppInstanceClient, instead of okta/AppInstanceClient.

My guess is that the imports should look like okta.AppInstanceClient inside init.py. This appears to mirror what I've seen in various other modules that work properly. I've tried to address the issue that way and did get past the problem below, but then ran into other pathing issues within the module.

(venv) xxx:/tmp$ pip install okta
Collecting okta
  Using cached okta-0.0.3-py2.py3-none-any.whl
Collecting six>=1.9.0 (from okta)
  Using cached six-1.10.0-py2.py3-none-any.whl
Collecting python-dateutil>=2.4.2 (from okta)
  Using cached python_dateutil-2.4.2-py2.py3-none-any.whl
Collecting requests>=2.5.3 (from okta)
  Using cached requests-2.9.1-py2.py3-none-any.whl
Installing collected packages: six, python-dateutil, requests, okta
Successfully installed okta-0.0.3 python-dateutil-2.4.2 requests-2.9.1 six-1.10.0
(venv) xxx:/tmp$ vim foo.py
(venv) xxx:/tmp$ python3 foo.py
Traceback (most recent call last):
  File "foo.py", line 1, in <module>
    from okta import AuthClient
  File "/private/tmp/venv/lib/python3.5/site-packages/okta/__init__.py", line 12, in <module>
    from AppInstanceClient import AppInstanceClient
ImportError: No module named 'AppInstanceClient'
(venv) xxx:/tmp$ cat foo.py
from okta import AuthClient

State token is set to 'None' on successful authentication

When I make a call to the authenticate() method, the returned AuthResult object has the stateToken field set to 'None'. The authentication status was deemed to be successful and the user being authenticated is active in the Okta's people directory. Please see code snippet below:

>>> res = auth_client.authenticate("[email protected]", "Password123")
>>> res.stateToken
>>> res.status
u'SUCCESS'
>>> print res.stateToken
None
>>>

How can I paginate throuhgh all users? (alpha 1.0.0 release)

The readme shows an example of paginating to get the next page but that does appear to return the "resp" object so I can then recursively run the .has_next and get all of the values. In my case I am referring to users but they use they same object for the OktaApiResponse if im not mistaken. The example shown in the readme shows

if resp.has_next:
    next_users, err = await resp.next()

but how can I continue to iterate through all the responses to get all users for example.

Thanks

hello world?

can we update the README with a hello world? for example, i'm trying to get this to work, but it's not working

from okta.client import Client as OktaClient
import asyncio

async def main():
    client = OktaClient()
    users, resp, err = await client.list_users()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Using list_group_users returns an HTTP 405 error

Hi all,

I'm testing out list_group_users for a new service, and it's returning a 405 error on the call:

{'message': 'Okta HTTP 405 E0000022 The endpoint does not support the provided HTTP method'}

The code I'm using is as follows, shortened for relevance:

import asyncio
from okta.client import Client as OktaClient

okta_group = redacted

okta_config = {
    'orgUrl': redacted,
    'token': redacted
}

async def okta_group_members():
    employees = []
    params = {
        'limit': 200
    }
    try:
        users, response, error = await okta_client.list_group_users(
            groupId=okta_group, query_params=params
        )
        while True:
            if users is not None:
                for user in users:
                    user_sanitized = {
                        'first_name': user.profile.first_name,
                        'last_name': user.profile.last_name,
                        'user_id': user.id,
                        'email': user.profile.email,
                        'status': user.status
                    }
                    employees.append(user_sanitized)
                if response.has_next():
                    users, error = await response.next()
                else:
                    break
            elif error is not None:
                logger.error(f'Okta API Error: {error}')
    except Exception as e:
        logger.exception(f'Error retrieving users from the group: {e}')
    return employees

async def main():
    okta_member_list = await okta_group_members()
    print(okta_member_list)

if __name__ == '__main__':
    asyncio.run(main())

I'm able to make this call without issues using requests, though.

NameError: name 'User' is not defined

When I try to run the example code in the quickstart.rst, I get this error:

Traceback (most recent call last):
  File "create_client.py", line 6, in <module>
    new_user = User(login='[email protected]',
NameError: name 'User' is not defined

Here is the example code that I'm trying to execute:

from okta import UsersClient
# http://developer.okta.com/docs/api/getting_started/getting_a_token.html
usersClient = UsersClient('https://dev-212920-admin.okta.com',
                              'xxxxxxxxsxxxxxxxxxxxxxxxxxxx')
                              
new_user = User(login='[email protected]',
                    email='[email protected]',
                    firstName='Saml',
                    lastName='Jackson')
user = usersClient.create_user(new_user, activate=False)

user = usersClient.get_user('[email protected]')
usersClient.activate_user(user.id)

users = usersClient.get_paged_users()
while True:
    for user in users.result:
        print u"First Name: {}".format(user.profile.firstName)
        print u"Last Name:  {}".format(user.profile.lastName)
        print u"Login:      {}".format(user.profile.login)
        print u"User ID:    {}\n".format(user.id)
    if not users.is_last_page():
            # Keep on fetching pages of users until the last page
        users = usersClient.get_paged_users(url=users.next_url)
    else:
        break

get_group_users returns only 1000 elements

groups_client = UserGroupsClient(base_url=settings.OKTA_BASE_URL, api_token=settings.OKTA_API_TOKEN)
users = groups_client.get_group_users(gid)

users contains only first 1000 items.
After debug:
response = ApiClient.get_path(self, '/{0}/users'.format(gid))
'next' in response.links returns True

url = 'https://xxxx.okta.com/api/v1/groups/xxxxxxx/users'
params = None

Based on the description should return 10,000.
https://developer.okta.com/docs/api/resources/groups/#list-group-members
In any case, the case of large groups is not taken into account.

Also checked with postman. The same behavior.
I also suspect that other API methods may have a similar problem.

No new release since Oct 2015

There has not been a new release since the initial release in Oct 2015. Is the expectation that we need to clone the source code regularly or can we expect to have released versions in the future?

Security status and support

Hi, is there any chance for Okta to support this library again? I managed to get this working on my Flask application and it's working well.

Is it secure enough for corporate use?

Thanks!

asyncio should not put under requirements.txt

Hi community,
I am now using python 3.7+ with pip-compile.
And I will get error with asyncio if I installed it from pip.
From the doc in asyncio,

asyncio requires Python 3.3 or later! The asyncio module is part of the Python standard library since Python 3.4.

And also,

This version is only relevant for Python 3.3, which does not include asyncio in its stdlib.

So, my suggestion here is to remove the asyncio and release the okta package with 3.3+ only.
Thank you!

pip install is broken

Installing okta python sdk via pip, only fetches the files in the root folder (AppInstanceClient.py, AuthClient.py, etc.), the folders "framework" and "models" are not download, hence the okta python sdk won't work.

sudo pip install okta

python okta_python_test.py
Traceback (most recent call last):
  File "okta_python", line 2, in <module>
    from okta import AppInstanceClient
  File "/Library/Python/2.7/site-packages/okta/AppInstanceClient.py", line 1, in <module>
    from okta.framework.ApiClient import ApiClient
ImportError: No module named framework.ApiClient

This is how the python package looks after using sudo pip install okta:

ls -l /Library/Python/2.7/site-packages/okta
total 176
-rw-r--r--  1 root  wheel  1971 Sep  6 18:52 AppInstanceClient.py
-rw-r--r--  1 root  wheel  3241 Sep  6 18:52 AppInstanceClient.pyc
-rw-r--r--  1 root  wheel  6098 Sep  6 18:52 AuthClient.py
-rw-r--r--  1 root  wheel  6274 Sep  6 18:52 AuthClient.pyc
-rw-r--r--  1 root  wheel   759 Sep  6 18:52 EventsClient.py
-rw-r--r--  1 root  wheel  1448 Sep  6 18:52 EventsClient.pyc
-rw-r--r--  1 root  wheel  1016 Sep  6 18:52 FactorsAdminClient.py
-rw-r--r--  1 root  wheel  1836 Sep  6 18:52 FactorsAdminClient.pyc
-rw-r--r--  1 root  wheel  4285 Sep  6 18:52 FactorsClient.py
-rw-r--r--  1 root  wheel  5645 Sep  6 18:52 FactorsClient.pyc
-rw-r--r--  1 root  wheel  1716 Sep  6 18:52 SessionsClient.py
-rw-r--r--  1 root  wheel  2924 Sep  6 18:52 SessionsClient.pyc
-rw-r--r--  1 root  wheel  1533 Sep  6 18:52 UserGroupsClient.py
-rw-r--r--  1 root  wheel  2585 Sep  6 18:52 UserGroupsClient.pyc
-rw-r--r--  1 root  wheel  2843 Sep  6 18:52 UsersClient.py
-rw-r--r--  1 root  wheel  3997 Sep  6 18:52 UsersClient.pyc
-rw-r--r--  1 root  wheel    44 Sep  6 18:52 __init__.py
-rw-r--r--  1 root  wheel   168 Sep  6 18:52 __init__.pyc

If I manually download the folders "framework" and "models" and add them to "/Library/Python/2.7/site-packages/okta" everything works.

bonus: have a look at the pull requests and either accept them or send them back.

SamlApplicationSettingsApplication/missing `app` key

I'm currently working on a DR solution for Okta that allows us to backup our configuration in serialised format to be later restored.

To do this, we're relying primarily on the ability to instantiate the appropriate OktaObject type using the config dict, and then calling the relevant create_x method.

Most of the other application types (e.g BookmarkApplication) have a corresponding class to which it delegates a settings key (e.g BookmarkApplicationSettings), which in turn delegates an app key to another corresponding class (e.g BookmarkApplicationSettingsApplication. It appears that this convention is missing for SAML application types, and as a result we are missing crucial information needed to be able to restore a SAML application.

An example partial response from Okta API containing this information:

{'name': 'yourcausesaml', 'label': 'YourCause',
....
'settings': {'app': {'envType': 'yourcause', 'subDomain': 'foo'}
...
}
SamlApplicationSettings(config=saml_api_response['settings']).app
{}

SDK does not officially support update of user profile custom attributes

Due to the way the UserProfile model is built, it has a set of properties that it uses in its dict when request data is serialized or response data is deserialized.

The set of properties is static which means that if you set a user profile attribute that is not part of the standard Okta attributes, it vanishes when you make your requests, and likewise when looking up a user and serializing their user profile, the user profile appears to not have any of the custom attributes that were added.

In order to work around this, it's possible to do something like this:

user_profile = UserProfile(...).as_dict()
user_profile[{custom_attribute}] = value

When providing the user profile to things like create/update user, user_profile can be passed instead of the actual UserProfile model object itself.

The ability to add custom attributes in a supported way would be nice to have when working with the SDK.

Request body not well formed during verify

Hello,

I'm trying to prompt the user for push auth via the SDK for testing purposes. According to the documentation this is a valid request :

factor_response = factorClient.verify_factor(user.id, factor.id)

This results in the following call"

curl -i -s -k  -X $'POST' \
    -H $'User-Agent: python-requests/2.18.4' -H $'Content-Type: application/json' -H $'Authorization: SSWS XXXXXX' \
    --data-binary $'{\"activationToken\":null,\"answer\":null,\"passCode\":null,\"nextPassCode\":null}' \
    $'https://example.oktapreview.com/api/v1/users/XXXXX/factors/XXXXX/verify'

Which returns an error The request body was not well-formed.

Fiddling around w/ Postman, submitting no body at all gives the desired results.

Line 20 in utils.py throws exception

When calling UserClient.delete_user(uid), oktasdk-python/okta/framework/Utils.py line 20 throws an exception (ValueError: No JSON object could be decoded). Presumably, whatever the okta API is returning is either not JSON, or is being improperly parsed.
What's that about?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.