pape5.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. """An implementation of the OpenID Provider Authentication Policy
  2. Extension 1.0, Draft 5
  3. @see: http://openid.net/developers/specs/
  4. @since: 2.1.0
  5. """
  6. __all__ = [
  7. 'Request',
  8. 'Response',
  9. 'ns_uri',
  10. 'AUTH_PHISHING_RESISTANT',
  11. 'AUTH_MULTI_FACTOR',
  12. 'AUTH_MULTI_FACTOR_PHYSICAL',
  13. 'LEVELS_NIST',
  14. 'LEVELS_JISA',
  15. ]
  16. from openid.extension import Extension
  17. import warnings
  18. import re
  19. ns_uri = "http://specs.openid.net/extensions/pape/1.0"
  20. AUTH_MULTI_FACTOR_PHYSICAL = \
  21. 'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical'
  22. AUTH_MULTI_FACTOR = \
  23. 'http://schemas.openid.net/pape/policies/2007/06/multi-factor'
  24. AUTH_PHISHING_RESISTANT = \
  25. 'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'
  26. AUTH_NONE = \
  27. 'http://schemas.openid.net/pape/policies/2007/06/none'
  28. TIME_VALIDATOR = re.compile('^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ$')
  29. LEVELS_NIST = 'http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf'
  30. LEVELS_JISA = 'http://www.jisa.or.jp/spec/auth_level.html'
  31. class PAPEExtension(Extension):
  32. _default_auth_level_aliases = {
  33. 'nist': LEVELS_NIST,
  34. 'jisa': LEVELS_JISA,
  35. }
  36. def __init__(self):
  37. self.auth_level_aliases = self._default_auth_level_aliases.copy()
  38. def _addAuthLevelAlias(self, auth_level_uri, alias=None):
  39. """Add an auth level URI alias to this request.
  40. @param auth_level_uri: The auth level URI to send in the
  41. request.
  42. @param alias: The namespace alias to use for this auth level
  43. in this message. May be None if the alias is not
  44. important.
  45. """
  46. if alias is None:
  47. try:
  48. alias = self._getAlias(auth_level_uri)
  49. except KeyError:
  50. alias = self._generateAlias()
  51. else:
  52. existing_uri = self.auth_level_aliases.get(alias)
  53. if existing_uri is not None and existing_uri != auth_level_uri:
  54. raise KeyError('Attempting to redefine alias %r from %r to %r',
  55. alias, existing_uri, auth_level_uri)
  56. self.auth_level_aliases[alias] = auth_level_uri
  57. def _generateAlias(self):
  58. """Return an unused auth level alias"""
  59. for i in xrange(1000):
  60. alias = 'cust%d' % (i,)
  61. if alias not in self.auth_level_aliases:
  62. return alias
  63. raise RuntimeError('Could not find an unused alias (tried 1000!)')
  64. def _getAlias(self, auth_level_uri):
  65. """Return the alias for the specified auth level URI.
  66. @raises KeyError: if no alias is defined
  67. """
  68. for (alias, existing_uri) in self.auth_level_aliases.iteritems():
  69. if auth_level_uri == existing_uri:
  70. return alias
  71. raise KeyError(auth_level_uri)
  72. class Request(PAPEExtension):
  73. """A Provider Authentication Policy request, sent from a relying
  74. party to a provider
  75. @ivar preferred_auth_policies: The authentication policies that
  76. the relying party prefers
  77. @type preferred_auth_policies: [str]
  78. @ivar max_auth_age: The maximum time, in seconds, that the relying
  79. party wants to allow to have elapsed before the user must
  80. re-authenticate
  81. @type max_auth_age: int or NoneType
  82. @ivar preferred_auth_level_types: Ordered list of authentication
  83. level namespace URIs
  84. @type preferred_auth_level_types: [str]
  85. """
  86. ns_alias = 'pape'
  87. def __init__(self, preferred_auth_policies=None, max_auth_age=None,
  88. preferred_auth_level_types=None):
  89. super(Request, self).__init__()
  90. if preferred_auth_policies is None:
  91. preferred_auth_policies = []
  92. self.preferred_auth_policies = preferred_auth_policies
  93. self.max_auth_age = max_auth_age
  94. self.preferred_auth_level_types = []
  95. if preferred_auth_level_types is not None:
  96. for auth_level in preferred_auth_level_types:
  97. self.addAuthLevel(auth_level)
  98. def __nonzero__(self):
  99. return bool(self.preferred_auth_policies or
  100. self.max_auth_age is not None or
  101. self.preferred_auth_level_types)
  102. def addPolicyURI(self, policy_uri):
  103. """Add an acceptable authentication policy URI to this request
  104. This method is intended to be used by the relying party to add
  105. acceptable authentication types to the request.
  106. @param policy_uri: The identifier for the preferred type of
  107. authentication.
  108. @see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-05.html#auth_policies
  109. """
  110. if policy_uri not in self.preferred_auth_policies:
  111. self.preferred_auth_policies.append(policy_uri)
  112. def addAuthLevel(self, auth_level_uri, alias=None):
  113. self._addAuthLevelAlias(auth_level_uri, alias)
  114. if auth_level_uri not in self.preferred_auth_level_types:
  115. self.preferred_auth_level_types.append(auth_level_uri)
  116. def getExtensionArgs(self):
  117. """@see: C{L{Extension.getExtensionArgs}}
  118. """
  119. ns_args = {
  120. 'preferred_auth_policies':' '.join(self.preferred_auth_policies),
  121. }
  122. if self.max_auth_age is not None:
  123. ns_args['max_auth_age'] = str(self.max_auth_age)
  124. if self.preferred_auth_level_types:
  125. preferred_types = []
  126. for auth_level_uri in self.preferred_auth_level_types:
  127. alias = self._getAlias(auth_level_uri)
  128. ns_args['auth_level.ns.%s' % (alias,)] = auth_level_uri
  129. preferred_types.append(alias)
  130. ns_args['preferred_auth_level_types'] = ' '.join(preferred_types)
  131. return ns_args
  132. def fromOpenIDRequest(cls, request):
  133. """Instantiate a Request object from the arguments in a
  134. C{checkid_*} OpenID message
  135. """
  136. self = cls()
  137. args = request.message.getArgs(self.ns_uri)
  138. is_openid1 = request.message.isOpenID1()
  139. if args == {}:
  140. return None
  141. self.parseExtensionArgs(args, is_openid1)
  142. return self
  143. fromOpenIDRequest = classmethod(fromOpenIDRequest)
  144. def parseExtensionArgs(self, args, is_openid1, strict=False):
  145. """Set the state of this request to be that expressed in these
  146. PAPE arguments
  147. @param args: The PAPE arguments without a namespace
  148. @param strict: Whether to raise an exception if the input is
  149. out of spec or otherwise malformed. If strict is false,
  150. malformed input will be ignored.
  151. @param is_openid1: Whether the input should be treated as part
  152. of an OpenID1 request
  153. @rtype: None
  154. @raises ValueError: When the max_auth_age is not parseable as
  155. an integer
  156. """
  157. # preferred_auth_policies is a space-separated list of policy URIs
  158. self.preferred_auth_policies = []
  159. policies_str = args.get('preferred_auth_policies')
  160. if policies_str:
  161. for uri in policies_str.split(' '):
  162. if uri not in self.preferred_auth_policies:
  163. self.preferred_auth_policies.append(uri)
  164. # max_auth_age is base-10 integer number of seconds
  165. max_auth_age_str = args.get('max_auth_age')
  166. self.max_auth_age = None
  167. if max_auth_age_str:
  168. try:
  169. self.max_auth_age = int(max_auth_age_str)
  170. except ValueError:
  171. if strict:
  172. raise
  173. # Parse auth level information
  174. preferred_auth_level_types = args.get('preferred_auth_level_types')
  175. if preferred_auth_level_types:
  176. aliases = preferred_auth_level_types.strip().split()
  177. for alias in aliases:
  178. key = 'auth_level.ns.%s' % (alias,)
  179. try:
  180. uri = args[key]
  181. except KeyError:
  182. if is_openid1:
  183. uri = self._default_auth_level_aliases.get(alias)
  184. else:
  185. uri = None
  186. if uri is None:
  187. if strict:
  188. raise ValueError('preferred auth level %r is not '
  189. 'defined in this message' % (alias,))
  190. else:
  191. self.addAuthLevel(uri, alias)
  192. def preferredTypes(self, supported_types):
  193. """Given a list of authentication policy URIs that a provider
  194. supports, this method returns the subsequence of those types
  195. that are preferred by the relying party.
  196. @param supported_types: A sequence of authentication policy
  197. type URIs that are supported by a provider
  198. @returns: The sub-sequence of the supported types that are
  199. preferred by the relying party. This list will be ordered
  200. in the order that the types appear in the supported_types
  201. sequence, and may be empty if the provider does not prefer
  202. any of the supported authentication types.
  203. @returntype: [str]
  204. """
  205. return filter(self.preferred_auth_policies.__contains__,
  206. supported_types)
  207. Request.ns_uri = ns_uri
  208. class Response(PAPEExtension):
  209. """A Provider Authentication Policy response, sent from a provider
  210. to a relying party
  211. @ivar auth_policies: List of authentication policies conformed to
  212. by this OpenID assertion, represented as policy URIs
  213. """
  214. ns_alias = 'pape'
  215. def __init__(self, auth_policies=None, auth_time=None,
  216. auth_levels=None):
  217. super(Response, self).__init__()
  218. if auth_policies:
  219. self.auth_policies = auth_policies
  220. else:
  221. self.auth_policies = []
  222. self.auth_time = auth_time
  223. self.auth_levels = {}
  224. if auth_levels is None:
  225. auth_levels = {}
  226. for uri, level in auth_levels.iteritems():
  227. self.setAuthLevel(uri, level)
  228. def setAuthLevel(self, level_uri, level, alias=None):
  229. """Set the value for the given auth level type.
  230. @param level: string representation of an authentication level
  231. valid for level_uri
  232. @param alias: An optional namespace alias for the given auth
  233. level URI. May be omitted if the alias is not
  234. significant. The library will use a reasonable default for
  235. widely-used auth level types.
  236. """
  237. self._addAuthLevelAlias(level_uri, alias)
  238. self.auth_levels[level_uri] = level
  239. def getAuthLevel(self, level_uri):
  240. """Return the auth level for the specified auth level
  241. identifier
  242. @returns: A string that should map to the auth levels defined
  243. for the auth level type
  244. @raises KeyError: If the auth level type is not present in
  245. this message
  246. """
  247. return self.auth_levels[level_uri]
  248. def _getNISTAuthLevel(self):
  249. try:
  250. return int(self.getAuthLevel(LEVELS_NIST))
  251. except KeyError:
  252. return None
  253. nist_auth_level = property(
  254. _getNISTAuthLevel,
  255. doc="Backward-compatibility accessor for the NIST auth level")
  256. def addPolicyURI(self, policy_uri):
  257. """Add a authentication policy to this response
  258. This method is intended to be used by the provider to add a
  259. policy that the provider conformed to when authenticating the user.
  260. @param policy_uri: The identifier for the preferred type of
  261. authentication.
  262. @see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies
  263. """
  264. if policy_uri == AUTH_NONE:
  265. raise RuntimeError(
  266. 'To send no policies, do not set any on the response.')
  267. if policy_uri not in self.auth_policies:
  268. self.auth_policies.append(policy_uri)
  269. def fromSuccessResponse(cls, success_response):
  270. """Create a C{L{Response}} object from a successful OpenID
  271. library response
  272. (C{L{openid.consumer.consumer.SuccessResponse}}) response
  273. message
  274. @param success_response: A SuccessResponse from consumer.complete()
  275. @type success_response: C{L{openid.consumer.consumer.SuccessResponse}}
  276. @rtype: Response or None
  277. @returns: A provider authentication policy response from the
  278. data that was supplied with the C{id_res} response or None
  279. if the provider sent no signed PAPE response arguments.
  280. """
  281. self = cls()
  282. # PAPE requires that the args be signed.
  283. args = success_response.getSignedNS(self.ns_uri)
  284. is_openid1 = success_response.isOpenID1()
  285. # Only try to construct a PAPE response if the arguments were
  286. # signed in the OpenID response. If not, return None.
  287. if args is not None:
  288. self.parseExtensionArgs(args, is_openid1)
  289. return self
  290. else:
  291. return None
  292. def parseExtensionArgs(self, args, is_openid1, strict=False):
  293. """Parse the provider authentication policy arguments into the
  294. internal state of this object
  295. @param args: unqualified provider authentication policy
  296. arguments
  297. @param strict: Whether to raise an exception when bad data is
  298. encountered
  299. @returns: None. The data is parsed into the internal fields of
  300. this object.
  301. """
  302. policies_str = args.get('auth_policies')
  303. if policies_str:
  304. auth_policies = policies_str.split(' ')
  305. elif strict:
  306. raise ValueError('Missing auth_policies')
  307. else:
  308. auth_policies = []
  309. if (len(auth_policies) > 1 and strict and AUTH_NONE in auth_policies):
  310. raise ValueError('Got some auth policies, as well as the special '
  311. '"none" URI: %r' % (auth_policies,))
  312. if 'none' in auth_policies:
  313. msg = '"none" used as a policy URI (see PAPE draft < 5)'
  314. if strict:
  315. raise ValueError(msg)
  316. else:
  317. warnings.warn(msg, stacklevel=2)
  318. auth_policies = [u for u in auth_policies
  319. if u not in ['none', AUTH_NONE]]
  320. self.auth_policies = auth_policies
  321. for (key, val) in args.iteritems():
  322. if key.startswith('auth_level.'):
  323. alias = key[11:]
  324. # skip the already-processed namespace declarations
  325. if alias.startswith('ns.'):
  326. continue
  327. try:
  328. uri = args['auth_level.ns.%s' % (alias,)]
  329. except KeyError:
  330. if is_openid1:
  331. uri = self._default_auth_level_aliases.get(alias)
  332. else:
  333. uri = None
  334. if uri is None:
  335. if strict:
  336. raise ValueError(
  337. 'Undefined auth level alias: %r' % (alias,))
  338. else:
  339. self.setAuthLevel(uri, val, alias)
  340. auth_time = args.get('auth_time')
  341. if auth_time:
  342. if TIME_VALIDATOR.match(auth_time):
  343. self.auth_time = auth_time
  344. elif strict:
  345. raise ValueError("auth_time must be in RFC3339 format")
  346. fromSuccessResponse = classmethod(fromSuccessResponse)
  347. def getExtensionArgs(self):
  348. """@see: C{L{Extension.getExtensionArgs}}
  349. """
  350. if len(self.auth_policies) == 0:
  351. ns_args = {
  352. 'auth_policies': AUTH_NONE,
  353. }
  354. else:
  355. ns_args = {
  356. 'auth_policies':' '.join(self.auth_policies),
  357. }
  358. for level_type, level in self.auth_levels.iteritems():
  359. alias = self._getAlias(level_type)
  360. ns_args['auth_level.ns.%s' % (alias,)] = level_type
  361. ns_args['auth_level.%s' % (alias,)] = str(level)
  362. if self.auth_time is not None:
  363. if not TIME_VALIDATOR.match(self.auth_time):
  364. raise ValueError('auth_time must be in RFC3339 format')
  365. ns_args['auth_time'] = self.auth_time
  366. return ns_args
  367. Response.ns_uri = ns_uri