/devicecheck

Reduce fraudulent use of your services by managing device state and asserting app integrity via Apple DeviceCheck API with this Python wrapper.

Primary LanguagePythonMIT LicenseMIT

Apple DeviceCheck

Accessing and Modifying Per-Device Data

Use a token from your app to validate requests, query and modify two per-device binary digits stored on Apple servers.

Features

  • Prevent API & Content abuse with validating requests via Apple device token
  • Query and modify two bits of data to achieve up to four remote states saved on Apple servers
  • Easy to use configuration
  • Examples
  • Integrations with modern web frameworks

Prepare

Visit https://developer.apple.com/account/resources/authkeys/list and create new Key with DeviceCheck permission

Install

pip install devicecheck

Usage (Python)

Setup

from devicecheck import DeviceCheck

device_check = DeviceCheck(
    team_id="XX7AN23E0Z",  # https://developer.apple.com/account/#/membership/
    bundle_id="com.akentev.app",
    key_id="JSAD983ENA",  # Generated at https://developer.apple.com/account/resources/authkeys/list
    private_key="/path/to/AuthKey_JSAD983ENA.p8",
    # Generated file at https://developer.apple.com/account/resources/authkeys/list
    dev_environment=True,  # True if using development Apple environment, False if using in production.
    # Remember to set dev_environment=False in production!
)

Asyncio setup

from devicecheck.asyncio import AsyncioDeviceCheck

The rest will be the same, except for network methods must be await'ed

Validate device

result = device_check.validate_device_token(device_token)

if result.is_ok:
    print('OK! Device is valid')
else:
    print('Bad news. Unable to validate device')

Update bits data

# Can use both integers, strings and booleans. Will be converted with bool(value)
result = device_check.update_two_bits(device_token, bit_0=1, bit_1=False)

# Can update bits separately
result = device_check.update_two_bits(device_token, bit_0=True)

if result.is_ok:
    print('Bits updated')
else:
    print(f'Something went wrong. {result}')

Query bits data

# Can use both integers, strings and booleans
result = device_check.query_two_bits(device_token)

if result.is_ok:
    print(f'First bit {result.bit_0}')  # True
    print(f'Second bit {result.bit_1}')  # False
    print(f'Last update time {result.bits_last_update_time}')  # 2020-04
else:
    print(f'Something went wrong. {result}')

Web server decorators

You can easily integrate devicecheck to your webserver using a decorator. Specify a supported framework, or leave None to try universal parser.

from devicecheck.decorators import validate_device  # for sync code
from devicecheck.decorators import DCSupportedFrameworks
from devicecheck import DeviceCheck

device_check = DeviceCheck(...)

# Set response that will be returned on invalid token
INVALID_TOKEN_RESPONSE = ('Invalid device_token', 403)


@app.route('/validate')
@validate_device(device_check, framework=DCSupportedFrameworks.flask, on_invalid_token=INVALID_TOKEN_RESPONSE)
def endpoint():
    return 'Content'

Sync code

Use sync decorator

from devicecheck.decorators import validate_device
from devicecheck.decorators import DCSupportedFrameworks

Flask

INVALID_TOKEN_RESPONSE = ('Invalid device_token', 403)
framework = DCSupportedFrameworks.flask

Async code

Use Async decorator

from devicecheck.decorators import async_validate_device
from devicecheck.decorators import DCSupportedAsyncFrameworks

Sanic

from sanic.response import text

INVALID_TOKEN_RESPONSE = text('Invalid device_token', status=403)
framework = DCSupportedAsyncFrameworks.sanic

FastAPI

from fastapi.responses import PlainTextResponse

INVALID_TOKEN_RESPONSE = PlainTextResponse('Invalid device_token', status_code=403)
framework = DCSupportedAsyncFrameworks.fastapi

Tests & Mock

Well, it's kinda hard to automate testing, because Devicecheck requires real device (Simulators won't work). In case you need to disable decorators, pass SKIP_DEVICE_CHECK_DECORATOR=True environment variable.

You can also mock validation, pass MOCK_DEVICE_CHECK_DECORATOR_TOKEN=XXXXXXXXXXXXX, it will be a hardcoded valid token value.

MOCK_DEVICE_CHECK_DECORATOR_TOKEN="device-check-token" python -m unittest tests/integrational/main.py

For Debug logs, including requests body, pass a DEBUG environment variable.

Exceptions

Library represents an AppleException class with attributes status_code and description Requires raise_on_error=True parameter for DeviceCheck instance.

Usage (Swift)

Generate device token

import DeviceCheck

public func getDeviceToken(completion: @escaping (String?) -> ()) {
    if #available(iOS 11.0, *) {
        let currentDevice = DCDevice.current
        if currentDevice.isSupported
        {
            currentDevice.generateToken(completionHandler: { (data, error) in
                if let tokenData = data {
                    let tokenString = tokenData.base64EncodedString()
                    print("Received device token")
                    completion(tokenString)
                } else{
                    print("Error generating token: \(error!.localizedDescription)")
                }
            })
        } else {
            print("Device is not supported") // Simulators or etc.
        }
    } else {
        print("Device OS is lower than iOS 11")
    }
}

Pass device token in HTTP request

Header or Body

getDeviceToken { deviceToken in
    var request = URLRequest(url: "...")
    request.httpMethod = "POST"
    
    // Header
    request.setValue(deviceToken, forHTTPHeaderField: "Device-Token")
    
    // Body
    request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
    let json = ["device_token": deviceToken] as [String : Any]
    let jsonData = try! JSONSerialization.data(withJSONObject: json)
    request.httpBody = jsonData as Data
    
    // Send it to server
    let downloadTask = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
        ...
    })
}

License

MIT