Source code for yubiotp.client

from base64 import b64encode, b64decode
from hashlib import sha1
import hmac
from random import choice
import string

from six.moves.urllib.parse import urlencode
from six.moves.urllib.request import urlopen
from six.moves import xrange


[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
[docs] 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 xrange(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
[docs] 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