Source code for yubiotp.client

from base64 import b64decode, b64encode
from hashlib import sha1
import hmac
from random import choice
import string
from urllib.parse import urlencode
from urllib.request import urlopen


[docs] class YubiClient10(object): """ Client for the Yubico validation service, version 1.0. http://code.google.com/p/yubikey-val-server-php/wiki/ValidationProtocolV10 :param int api_id: Your API id. :param bytes api_key: Your base64-encoded API key. :param bool ssl: ``True`` if we should use https URLs by default. .. attribute:: base_url The base URL of the validation service. Set this if you want to use a custom validation service. Defaults to ``'http[s]://api.yubico.com/wsapi/verify'``. """ _NONCE_CHARS = string.ascii_letters + string.digits def __init__(self, api_id=1, api_key=None, ssl=False): self.api_id = api_id self.api_key = api_key self.ssl = ssl
[docs] def verify(self, token): """ Verify a single Yubikey OTP against the validation service. :param str token: A modhex-encoded YubiKey OTP, as generated by a YubiKey device. :returns: A response from the validation service. :rtype: :class:`YubiResponse` """ nonce = self.nonce() url = self.url(token, nonce) stream = urlopen(url) response = YubiResponse( stream.read().decode('utf-8'), self.api_key, token, nonce ) stream.close() return response
[docs] def url(self, token, nonce=None): """ Generates the validation URL without sending a request. :param str token: A modhex-encoded YubiKey OTP, as generated by a YubiKey. :param str nonce: A nonce string, or ``None`` to generate a random one. :returns: The URL that we would use to validate the token. :rtype: str """ if nonce is None: nonce = self.nonce() return '{0}?{1}'.format(self.base_url, self.param_string(token, nonce))
_base_url = None @property def base_url(self): if self._base_url is None: self._base_url = self.default_base_url() return self._base_url @base_url.setter def base_url(self, url): self._base_url = url @base_url.deleter def base_url(self): delattr(self, '_base_url') def default_base_url(self): if self.ssl: return 'https://api.yubico.com/wsapi/verify' else: return 'http://api.yubico.com/wsapi/verify' def nonce(self): return ''.join(choice(self._NONCE_CHARS) for i in range(32)) def param_string(self, token, nonce): params = self.params(token, nonce) if self.api_key is not None: signature = param_signature(params, self.api_key) params.append(('h', b64encode(signature))) return urlencode(params) def params(self, token, nonce): return [ ('id', self.api_id), ('otp', token), ]
[docs] class YubiClient11(YubiClient10): """ Client for the Yubico validation service, version 1.1. http://code.google.com/p/yubikey-val-server-php/wiki/ValidationProtocolV11 :param int api_id: Your API id. :param bytes api_key: Your base64-encoded API key. :param bool ssl: ``True`` if we should use https URLs by default. :param bool timestamp: ``True`` if we want the server to include timestamp and counter information in the response. .. attribute:: base_url The base URL of the validation service. Set this if you want to use a custom validation service. Defaults to ``'http[s]://api.yubico.com/wsapi/verify'``. """ def __init__(self, api_id=1, api_key=None, ssl=False, timestamp=False): super(YubiClient11, self).__init__(api_id, api_key, ssl) self.timestamp = timestamp def params(self, token, nonce): params = super(YubiClient11, self).params(token, nonce) if self.timestamp: params.append(('timestamp', '1')) return params
[docs] class YubiClient20(YubiClient11): """ Client for the Yubico validation service, version 2.0. http://code.google.com/p/yubikey-val-server-php/wiki/ValidationProtocolV20 :param int api_id: Your API id. :param bytes api_key: Your base64-encoded API key. :param bool ssl: ``True`` if we should use https URLs by default. :param bool timestamp: ``True`` if we want the server to include timestamp and counter information in the response. :param sl: See protocol spec. :param timeout: See protocol spec. .. attribute:: base_url The base URL of the validation service. Set this if you want to use a custom validation service. Defaults to ``'http[s]://api.yubico.com/wsapi/2.0/verify'``. """ def __init__( self, api_id=1, api_key=None, ssl=False, timestamp=False, sl=None, timeout=None ): super(YubiClient20, self).__init__(api_id, api_key, ssl, timestamp) self.sl = sl self.timeout = timeout def default_base_url(self): if self.ssl: return 'https://api.yubico.com/wsapi/2.0/verify' else: return 'http://api.yubico.com/wsapi/2.0/verify' def params(self, token, nonce): params = super(YubiClient20, self).params(token, nonce) params.append(('nonce', nonce)) if self.sl is not None: params.append(('sl', self.sl)) if self.timeout is not None: params.append(('timeout', self.timeout)) return params
[docs] class YubiResponse(object): """ A response from the Yubico validation service. .. attribute:: fields A dictionary of the response fields (excluding 'h'). """ def __init__(self, raw, api_key, token, nonce): self.raw = raw self.api_key = api_key self.token = token self.nonce = nonce self.fields = {} self.signature = None self._parse_response() def _parse_response(self): self.fields = dict( tuple(line.split('=', 1)) for line in self.raw.splitlines() if '=' in line ) if 'h' in self.fields: self.signature = b64decode(self.fields['h'].encode()) del self.fields['h']
[docs] def is_ok(self): """ Returns true if all validation checks pass and the status is 'OK'. :rtype: bool """ return self.is_valid() and (self.fields.get('status') == 'OK')
[docs] def status(self): """ If the response is valid, this returns the value of the status field. Otherwise, it returns the special status ``'BAD_RESPONSE'`` """ status = self.fields.get('status') if status == 'BAD_SIGNATURE' or self.is_valid(strict=False): return status else: return 'BAD_RESPONSE'
[docs] def is_valid(self, strict=True): """ Performs all validity checks (signature, token, and nonce). :param bool strict: If ``True``, all validity checks must pass unambiguously. Otherwise, this only requires that no validity check fails. :returns: ``True`` if none of the validity checks fail. :rtype: bool """ results = [ self.is_signature_valid(), self.is_token_valid(), self.is_nonce_valid(), ] if strict: is_valid = all(results) else: is_valid = False not in results return is_valid
[docs] def is_signature_valid(self): """ Validates the response signature. :returns: ``True`` if the signature is valid or if we did not sign the request. ``False`` if the signature is invalid. :rtype: bool """ if self.api_key is not None: signature = param_signature(self.fields.items(), self.api_key) is_valid = signature == self.signature else: is_valid = True return is_valid
[docs] def is_token_valid(self): """ Validates the otp token sent in the response. :returns: ``True`` if the token in the response is the same as the one in the request; ``False`` if not; ``None`` if the response does not contain a token. :rtype: bool for a positive result or ``None`` for an ambiguous result. """ if 'otp' in self.fields: is_valid = self.fields['otp'] == self.token else: is_valid = None return is_valid
[docs] def is_nonce_valid(self): """ Validates the nonce value sent in the response. :returns: ``True`` if the nonce in the response matches the one we sent (or didn't send). ``False`` if the two do not match. ``None`` if we sent a nonce and did not receive one in the response: this is often true of error responses. :rtype: bool for a positive result or ``None`` for an ambiguous result. """ reply = self.fields.get('nonce') if (self.nonce is not None) and (reply is None): is_valid = None else: is_valid = reply == self.nonce return is_valid
@property def public_id(self): """ Returns the public id of the response token as a modhex string. :rtype: str or ``None``. """ try: public_id = self.fields['otp'][:-32] except KeyError: public_id = None return public_id
def param_signature(params, api_key): """ Returns the signature over a list of Yubico validation service parameters. Note that the signature algorithm packs the paramters into a form similar to URL parameters, but without any escaping. :param params: An association list of parameters, such as you would give to urllib.urlencode. :type params: list of 2-tuples :param bytes api_key: The Yubico API key (raw, not base64-encoded). :returns: The parameter signature (raw, not base64-encoded). :rtype: bytes """ param_string = '&'.join('{0}={1}'.format(k, v) for k, v in sorted(params)) signature = hmac.new(api_key, param_string.encode('utf-8'), sha1).digest() return signature