Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import binascii 

2from codecs import utf_8_decode 

3from codecs import utf_8_encode 

4from collections import namedtuple 

5import hashlib 

6import base64 

7import re 

8import time as time_mod 

9import warnings 

10 

11from zope.interface import implementer 

12 

13from webob.cookies import CookieProfile 

14 

15from pyramid.compat import ( 

16 long, 

17 text_type, 

18 binary_type, 

19 url_unquote, 

20 url_quote, 

21 bytes_, 

22 ascii_native_, 

23 native_, 

24) 

25 

26from pyramid.interfaces import IAuthenticationPolicy, IDebugLogger 

27 

28from pyramid.security import Authenticated, Everyone 

29 

30from pyramid.util import strings_differ 

31from pyramid.util import SimpleSerializer 

32 

33VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$") 

34 

35 

36class CallbackAuthenticationPolicy(object): 

37 """ Abstract class """ 

38 

39 debug = False 

40 callback = None 

41 

42 def _log(self, msg, methodname, request): 

43 logger = request.registry.queryUtility(IDebugLogger) 

44 if logger: 

45 cls = self.__class__ 

46 classname = cls.__module__ + '.' + cls.__name__ 

47 methodname = classname + '.' + methodname 

48 logger.debug(methodname + ': ' + msg) 

49 

50 def _clean_principal(self, princid): 

51 if princid in (Authenticated, Everyone): 

52 princid = None 

53 return princid 

54 

55 def authenticated_userid(self, request): 

56 """ Return the authenticated userid or ``None``. 

57 

58 If no callback is registered, this will be the same as 

59 ``unauthenticated_userid``. 

60 

61 If a ``callback`` is registered, this will return the userid if 

62 and only if the callback returns a value that is not ``None``. 

63 

64 """ 

65 debug = self.debug 

66 userid = self.unauthenticated_userid(request) 

67 if userid is None: 

68 debug and self._log( 

69 'call to unauthenticated_userid returned None; returning None', 

70 'authenticated_userid', 

71 request, 

72 ) 

73 return None 

74 if self._clean_principal(userid) is None: 

75 debug and self._log( 

76 ( 

77 'use of userid %r is disallowed by any built-in Pyramid ' 

78 'security policy, returning None' % userid 

79 ), 

80 'authenticated_userid', 

81 request, 

82 ) 

83 return None 

84 

85 if self.callback is None: 

86 debug and self._log( 

87 'there was no groupfinder callback; returning %r' % (userid,), 

88 'authenticated_userid', 

89 request, 

90 ) 

91 return userid 

92 callback_ok = self.callback(userid, request) 

93 if callback_ok is not None: # is not None! 

94 debug and self._log( 

95 'groupfinder callback returned %r; returning %r' 

96 % (callback_ok, userid), 

97 'authenticated_userid', 

98 request, 

99 ) 

100 return userid 

101 debug and self._log( 

102 'groupfinder callback returned None; returning None', 

103 'authenticated_userid', 

104 request, 

105 ) 

106 

107 def effective_principals(self, request): 

108 """ A list of effective principals derived from request. 

109 

110 This will return a list of principals including, at least, 

111 :data:`pyramid.security.Everyone`. If there is no authenticated 

112 userid, or the ``callback`` returns ``None``, this will be the 

113 only principal: 

114 

115 .. code-block:: python 

116 

117 return [Everyone] 

118 

119 If the ``callback`` does not return ``None`` and an authenticated 

120 userid is found, then the principals will include 

121 :data:`pyramid.security.Authenticated`, the ``authenticated_userid`` 

122 and the list of principals returned by the ``callback``: 

123 

124 .. code-block:: python 

125 

126 extra_principals = callback(userid, request) 

127 return [Everyone, Authenticated, userid] + extra_principals 

128 

129 """ 

130 debug = self.debug 

131 effective_principals = [Everyone] 

132 userid = self.unauthenticated_userid(request) 

133 

134 if userid is None: 

135 debug and self._log( 

136 'unauthenticated_userid returned %r; returning %r' 

137 % (userid, effective_principals), 

138 'effective_principals', 

139 request, 

140 ) 

141 return effective_principals 

142 

143 if self._clean_principal(userid) is None: 

144 debug and self._log( 

145 ( 

146 'unauthenticated_userid returned disallowed %r; returning ' 

147 '%r as if it was None' % (userid, effective_principals) 

148 ), 

149 'effective_principals', 

150 request, 

151 ) 

152 return effective_principals 

153 

154 if self.callback is None: 

155 debug and self._log( 

156 'groupfinder callback is None, so groups is []', 

157 'effective_principals', 

158 request, 

159 ) 

160 groups = [] 

161 else: 

162 groups = self.callback(userid, request) 

163 debug and self._log( 

164 'groupfinder callback returned %r as groups' % (groups,), 

165 'effective_principals', 

166 request, 

167 ) 

168 

169 if groups is None: # is None! 

170 debug and self._log( 

171 'returning effective principals: %r' % (effective_principals,), 

172 'effective_principals', 

173 request, 

174 ) 

175 return effective_principals 

176 

177 effective_principals.append(Authenticated) 

178 effective_principals.append(userid) 

179 effective_principals.extend(groups) 

180 

181 debug and self._log( 

182 'returning effective principals: %r' % (effective_principals,), 

183 'effective_principals', 

184 request, 

185 ) 

186 return effective_principals 

187 

188 

189@implementer(IAuthenticationPolicy) 

190class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): 

191 """ A :app:`Pyramid` :term:`authentication policy` which 

192 obtains data from the :mod:`repoze.who` 1.X WSGI 'API' (the 

193 ``repoze.who.identity`` key in the WSGI environment). 

194 

195 Constructor Arguments 

196 

197 ``identifier_name`` 

198 

199 Default: ``auth_tkt``. The :mod:`repoze.who` plugin name that 

200 performs remember/forget. Optional. 

201 

202 ``callback`` 

203 

204 Default: ``None``. A callback passed the :mod:`repoze.who` identity 

205 and the :term:`request`, expected to return ``None`` if the user 

206 represented by the identity doesn't exist or a sequence of principal 

207 identifiers (possibly empty) representing groups if the user does 

208 exist. If ``callback`` is None, the userid will be assumed to exist 

209 with no group principals. 

210 

211 Objects of this class implement the interface described by 

212 :class:`pyramid.interfaces.IAuthenticationPolicy`. 

213 """ 

214 

215 def __init__(self, identifier_name='auth_tkt', callback=None): 

216 self.identifier_name = identifier_name 

217 self.callback = callback 

218 

219 def _get_identity(self, request): 

220 return request.environ.get('repoze.who.identity') 

221 

222 def _get_identifier(self, request): 

223 plugins = request.environ.get('repoze.who.plugins') 

224 if plugins is None: 

225 return None 

226 identifier = plugins[self.identifier_name] 

227 return identifier 

228 

229 def authenticated_userid(self, request): 

230 """ Return the authenticated userid or ``None``. 

231 

232 If no callback is registered, this will be the same as 

233 ``unauthenticated_userid``. 

234 

235 If a ``callback`` is registered, this will return the userid if 

236 and only if the callback returns a value that is not ``None``. 

237 

238 """ 

239 identity = self._get_identity(request) 

240 

241 if identity is None: 

242 self.debug and self._log( 

243 'repoze.who identity is None, returning None', 

244 'authenticated_userid', 

245 request, 

246 ) 

247 return None 

248 

249 userid = identity['repoze.who.userid'] 

250 

251 if userid is None: 

252 self.debug and self._log( 

253 'repoze.who.userid is None, returning None' % userid, 

254 'authenticated_userid', 

255 request, 

256 ) 

257 return None 

258 

259 if self._clean_principal(userid) is None: 

260 self.debug and self._log( 

261 ( 

262 'use of userid %r is disallowed by any built-in Pyramid ' 

263 'security policy, returning None' % userid 

264 ), 

265 'authenticated_userid', 

266 request, 

267 ) 

268 return None 

269 

270 if self.callback is None: 

271 return userid 

272 

273 if self.callback(identity, request) is not None: # is not None! 

274 return userid 

275 

276 def unauthenticated_userid(self, request): 

277 """ Return the ``repoze.who.userid`` key from the detected identity.""" 

278 identity = self._get_identity(request) 

279 if identity is None: 

280 return None 

281 return identity['repoze.who.userid'] 

282 

283 def effective_principals(self, request): 

284 """ A list of effective principals derived from the identity. 

285 

286 This will return a list of principals including, at least, 

287 :data:`pyramid.security.Everyone`. If there is no identity, or 

288 the ``callback`` returns ``None``, this will be the only principal. 

289 

290 If the ``callback`` does not return ``None`` and an identity is 

291 found, then the principals will include 

292 :data:`pyramid.security.Authenticated`, the ``authenticated_userid`` 

293 and the list of principals returned by the ``callback``. 

294 

295 """ 

296 effective_principals = [Everyone] 

297 identity = self._get_identity(request) 

298 

299 if identity is None: 

300 self.debug and self._log( 

301 ( 

302 'repoze.who identity was None; returning %r' 

303 % effective_principals 

304 ), 

305 'effective_principals', 

306 request, 

307 ) 

308 return effective_principals 

309 

310 if self.callback is None: 

311 groups = [] 

312 else: 

313 groups = self.callback(identity, request) 

314 

315 if groups is None: # is None! 

316 self.debug and self._log( 

317 ( 

318 'security policy groups callback returned None; returning ' 

319 '%r' % effective_principals 

320 ), 

321 'effective_principals', 

322 request, 

323 ) 

324 return effective_principals 

325 

326 userid = identity['repoze.who.userid'] 

327 

328 if userid is None: 

329 self.debug and self._log( 

330 ( 

331 'repoze.who.userid was None; returning %r' 

332 % effective_principals 

333 ), 

334 'effective_principals', 

335 request, 

336 ) 

337 return effective_principals 

338 

339 if self._clean_principal(userid) is None: 

340 self.debug and self._log( 

341 ( 

342 'unauthenticated_userid returned disallowed %r; returning ' 

343 '%r as if it was None' % (userid, effective_principals) 

344 ), 

345 'effective_principals', 

346 request, 

347 ) 

348 return effective_principals 

349 

350 effective_principals.append(Authenticated) 

351 effective_principals.append(userid) 

352 effective_principals.extend(groups) 

353 return effective_principals 

354 

355 def remember(self, request, userid, **kw): 

356 """ Store the ``userid`` as ``repoze.who.userid``. 

357 

358 The identity to authenticated to :mod:`repoze.who` 

359 will contain the given userid as ``userid``, and 

360 provide all keyword arguments as additional identity 

361 keys. Useful keys could be ``max_age`` or ``userdata``. 

362 """ 

363 identifier = self._get_identifier(request) 

364 if identifier is None: 

365 return [] 

366 environ = request.environ 

367 identity = kw 

368 identity['repoze.who.userid'] = userid 

369 return identifier.remember(environ, identity) 

370 

371 def forget(self, request): 

372 """ Forget the current authenticated user. 

373 

374 Return headers that, if included in a response, will delete the 

375 cookie responsible for tracking the current user. 

376 

377 """ 

378 identifier = self._get_identifier(request) 

379 if identifier is None: 

380 return [] 

381 identity = self._get_identity(request) 

382 return identifier.forget(request.environ, identity) 

383 

384 

385@implementer(IAuthenticationPolicy) 

386class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy): 

387 """ A :app:`Pyramid` :term:`authentication policy` which 

388 obtains data from the ``REMOTE_USER`` WSGI environment variable. 

389 

390 Constructor Arguments 

391 

392 ``environ_key`` 

393 

394 Default: ``REMOTE_USER``. The key in the WSGI environ which 

395 provides the userid. 

396 

397 ``callback`` 

398 

399 Default: ``None``. A callback passed the userid and the request, 

400 expected to return None if the userid doesn't exist or a sequence of 

401 principal identifiers (possibly empty) representing groups if the 

402 user does exist. If ``callback`` is None, the userid will be assumed 

403 to exist with no group principals. 

404 

405 ``debug`` 

406 

407 Default: ``False``. If ``debug`` is ``True``, log messages to the 

408 Pyramid debug logger about the results of various authentication 

409 steps. The output from debugging is useful for reporting to maillist 

410 or IRC channels when asking for support. 

411 

412 Objects of this class implement the interface described by 

413 :class:`pyramid.interfaces.IAuthenticationPolicy`. 

414 """ 

415 

416 def __init__(self, environ_key='REMOTE_USER', callback=None, debug=False): 

417 self.environ_key = environ_key 

418 self.callback = callback 

419 self.debug = debug 

420 

421 def unauthenticated_userid(self, request): 

422 """ The ``REMOTE_USER`` value found within the ``environ``.""" 

423 return request.environ.get(self.environ_key) 

424 

425 def remember(self, request, userid, **kw): 

426 """ A no-op. The ``REMOTE_USER`` does not provide a protocol for 

427 remembering the user. This will be application-specific and can 

428 be done somewhere else or in a subclass.""" 

429 return [] 

430 

431 def forget(self, request): 

432 """ A no-op. The ``REMOTE_USER`` does not provide a protocol for 

433 forgetting the user. This will be application-specific and can 

434 be done somewhere else or in a subclass.""" 

435 return [] 

436 

437 

438@implementer(IAuthenticationPolicy) 

439class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): 

440 """A :app:`Pyramid` :term:`authentication policy` which 

441 obtains data from a Pyramid "auth ticket" cookie. 

442 

443 Constructor Arguments 

444 

445 ``secret`` 

446 

447 The secret (a string) used for auth_tkt cookie signing. This value 

448 should be unique across all values provided to Pyramid for various 

449 subsystem secrets (see :ref:`admonishment_against_secret_sharing`). 

450 Required. 

451 

452 ``callback`` 

453 

454 Default: ``None``. A callback passed the userid and the 

455 request, expected to return ``None`` if the userid doesn't 

456 exist or a sequence of principal identifiers (possibly empty) if 

457 the user does exist. If ``callback`` is ``None``, the userid 

458 will be assumed to exist with no principals. Optional. 

459 

460 ``cookie_name`` 

461 

462 Default: ``auth_tkt``. The cookie name used 

463 (string). Optional. 

464 

465 ``secure`` 

466 

467 Default: ``False``. Only send the cookie back over a secure 

468 conn. Optional. 

469 

470 ``include_ip`` 

471 

472 Default: ``False``. Make the requesting IP address part of 

473 the authentication data in the cookie. Optional. 

474 

475 For IPv6 this option is not recommended. The ``mod_auth_tkt`` 

476 specification does not specify how to handle IPv6 addresses, so using 

477 this option in combination with IPv6 addresses may cause an 

478 incompatible cookie. It ties the authentication ticket to that 

479 individual's IPv6 address. 

480 

481 ``timeout`` 

482 

483 Default: ``None``. Maximum number of seconds which a newly 

484 issued ticket will be considered valid. After this amount of 

485 time, the ticket will expire (effectively logging the user 

486 out). If this value is ``None``, the ticket never expires. 

487 Optional. 

488 

489 ``reissue_time`` 

490 

491 Default: ``None``. If this parameter is set, it represents the number 

492 of seconds that must pass before an authentication token cookie is 

493 automatically reissued as the result of a request which requires 

494 authentication. The duration is measured as the number of seconds 

495 since the last auth_tkt cookie was issued and 'now'. If this value is 

496 ``0``, a new ticket cookie will be reissued on every request which 

497 requires authentication. 

498 

499 A good rule of thumb: if you want auto-expired cookies based on 

500 inactivity: set the ``timeout`` value to 1200 (20 mins) and set the 

501 ``reissue_time`` value to perhaps a tenth of the ``timeout`` value 

502 (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower 

503 than the ``reissue_time`` value, as the ticket will never be reissued 

504 if so. However, such a configuration is not explicitly prevented. 

505 

506 Optional. 

507 

508 ``max_age`` 

509 

510 Default: ``None``. The max age of the auth_tkt cookie, in 

511 seconds. This differs from ``timeout`` inasmuch as ``timeout`` 

512 represents the lifetime of the ticket contained in the cookie, 

513 while this value represents the lifetime of the cookie itself. 

514 When this value is set, the cookie's ``Max-Age`` and 

515 ``Expires`` settings will be set, allowing the auth_tkt cookie 

516 to last between browser sessions. It is typically nonsensical 

517 to set this to a value that is lower than ``timeout`` or 

518 ``reissue_time``, although it is not explicitly prevented. 

519 Optional. 

520 

521 ``path`` 

522 

523 Default: ``/``. The path for which the auth_tkt cookie is valid. 

524 May be desirable if the application only serves part of a domain. 

525 Optional. 

526 

527 ``http_only`` 

528 

529 Default: ``False``. Hide cookie from JavaScript by setting the 

530 HttpOnly flag. Not honored by all browsers. 

531 Optional. 

532 

533 ``wild_domain`` 

534 

535 Default: ``True``. An auth_tkt cookie will be generated for the 

536 wildcard domain. If your site is hosted as ``example.com`` this 

537 will make the cookie available for sites underneath ``example.com`` 

538 such as ``www.example.com``. 

539 Optional. 

540 

541 ``parent_domain`` 

542 

543 Default: ``False``. An auth_tkt cookie will be generated for the 

544 parent domain of the current site. For example if your site is 

545 hosted under ``www.example.com`` a cookie will be generated for 

546 ``.example.com``. This can be useful if you have multiple sites 

547 sharing the same domain. This option supercedes the ``wild_domain`` 

548 option. 

549 Optional. 

550 

551 ``domain`` 

552 

553 Default: ``None``. If provided the auth_tkt cookie will only be 

554 set for this domain. This option is not compatible with ``wild_domain`` 

555 and ``parent_domain``. 

556 Optional. 

557 

558 ``hashalg`` 

559 

560 Default: ``sha512`` (the literal string). 

561 

562 Any hash algorithm supported by Python's ``hashlib.new()`` function 

563 can be used as the ``hashalg``. 

564 

565 Cookies generated by different instances of AuthTktAuthenticationPolicy 

566 using different ``hashalg`` options are not compatible. Switching the 

567 ``hashalg`` will imply that all existing users with a valid cookie will 

568 be required to re-login. 

569 

570 Optional. 

571 

572 ``debug`` 

573 

574 Default: ``False``. If ``debug`` is ``True``, log messages to the 

575 Pyramid debug logger about the results of various authentication 

576 steps. The output from debugging is useful for reporting to maillist 

577 or IRC channels when asking for support. 

578 

579 ``samesite`` 

580 

581 Default: ``'Lax'``. The 'samesite' option of the session cookie. Set 

582 the value to ``None`` to turn off the samesite option. 

583 

584 This option is available as of :app:`Pyramid` 1.10. 

585 

586 .. versionchanged:: 1.4 

587 

588 Added the ``hashalg`` option, defaulting to ``sha512``. 

589 

590 .. versionchanged:: 1.5 

591 

592 Added the ``domain`` option. 

593 

594 Added the ``parent_domain`` option. 

595 

596 .. versionchanged:: 1.10 

597 

598 Added the ``samesite`` option and made the default ``'Lax'``. 

599 

600 Objects of this class implement the interface described by 

601 :class:`pyramid.interfaces.IAuthenticationPolicy`. 

602 

603 """ 

604 

605 def __init__( 

606 self, 

607 secret, 

608 callback=None, 

609 cookie_name='auth_tkt', 

610 secure=False, 

611 include_ip=False, 

612 timeout=None, 

613 reissue_time=None, 

614 max_age=None, 

615 path="/", 

616 http_only=False, 

617 wild_domain=True, 

618 debug=False, 

619 hashalg='sha512', 

620 parent_domain=False, 

621 domain=None, 

622 samesite='Lax', 

623 ): 

624 self.cookie = AuthTktCookieHelper( 

625 secret, 

626 cookie_name=cookie_name, 

627 secure=secure, 

628 include_ip=include_ip, 

629 timeout=timeout, 

630 reissue_time=reissue_time, 

631 max_age=max_age, 

632 http_only=http_only, 

633 path=path, 

634 wild_domain=wild_domain, 

635 hashalg=hashalg, 

636 parent_domain=parent_domain, 

637 domain=domain, 

638 samesite=samesite, 

639 ) 

640 self.callback = callback 

641 self.debug = debug 

642 

643 def unauthenticated_userid(self, request): 

644 """ The userid key within the auth_tkt cookie.""" 

645 result = self.cookie.identify(request) 

646 if result: 

647 return result['userid'] 

648 

649 def remember(self, request, userid, **kw): 

650 """ Accepts the following kw args: ``max_age=<int-seconds>, 

651 ``tokens=<sequence-of-ascii-strings>``. 

652 

653 Return a list of headers which will set appropriate cookies on 

654 the response. 

655 

656 """ 

657 return self.cookie.remember(request, userid, **kw) 

658 

659 def forget(self, request): 

660 """ A list of headers which will delete appropriate cookies.""" 

661 return self.cookie.forget(request) 

662 

663 

664def b64encode(v): 

665 return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'') 

666 

667 

668def b64decode(v): 

669 return base64.b64decode(bytes_(v)) 

670 

671 

672# this class licensed under the MIT license (stolen from Paste) 

673class AuthTicket(object): 

674 """ 

675 This class represents an authentication token. You must pass in 

676 the shared secret, the userid, and the IP address. Optionally you 

677 can include tokens (a list of strings, representing role names), 

678 'user_data', which is arbitrary data available for your own use in 

679 later scripts. Lastly, you can override the cookie name and 

680 timestamp. 

681 

682 Once you provide all the arguments, use .cookie_value() to 

683 generate the appropriate authentication ticket. 

684 

685 Usage:: 

686 

687 token = AuthTicket('sharedsecret', 'username', 

688 os.environ['REMOTE_ADDR'], tokens=['admin']) 

689 val = token.cookie_value() 

690 

691 """ 

692 

693 def __init__( 

694 self, 

695 secret, 

696 userid, 

697 ip, 

698 tokens=(), 

699 user_data='', 

700 time=None, 

701 cookie_name='auth_tkt', 

702 secure=False, 

703 hashalg='md5', 

704 ): 

705 self.secret = secret 

706 self.userid = userid 

707 self.ip = ip 

708 self.tokens = ','.join(tokens) 

709 self.user_data = user_data 

710 if time is None: 

711 self.time = time_mod.time() 

712 else: 

713 self.time = time 

714 self.cookie_name = cookie_name 

715 self.secure = secure 

716 self.hashalg = hashalg 

717 

718 def digest(self): 

719 return calculate_digest( 

720 self.ip, 

721 self.time, 

722 self.secret, 

723 self.userid, 

724 self.tokens, 

725 self.user_data, 

726 self.hashalg, 

727 ) 

728 

729 def cookie_value(self): 

730 v = '%s%08x%s!' % ( 

731 self.digest(), 

732 int(self.time), 

733 url_quote(self.userid), 

734 ) 

735 if self.tokens: 

736 v += self.tokens + '!' 

737 v += self.user_data 

738 return v 

739 

740 

741# this class licensed under the MIT license (stolen from Paste) 

742class BadTicket(Exception): 

743 """ 

744 Exception raised when a ticket can't be parsed. If we get far enough to 

745 determine what the expected digest should have been, expected is set. 

746 This should not be shown by default, but can be useful for debugging. 

747 """ 

748 

749 def __init__(self, msg, expected=None): 

750 self.expected = expected 

751 Exception.__init__(self, msg) 

752 

753 

754# this function licensed under the MIT license (stolen from Paste) 

755def parse_ticket(secret, ticket, ip, hashalg='md5'): 

756 """ 

757 Parse the ticket, returning (timestamp, userid, tokens, user_data). 

758 

759 If the ticket cannot be parsed, a ``BadTicket`` exception will be raised 

760 with an explanation. 

761 """ 

762 ticket = native_(ticket).strip('"') 

763 digest_size = hashlib.new(hashalg).digest_size * 2 

764 digest = ticket[:digest_size] 

765 try: 

766 timestamp = int(ticket[digest_size : digest_size + 8], 16) 

767 except ValueError as e: 

768 raise BadTicket('Timestamp is not a hex integer: %s' % e) 

769 try: 

770 userid, data = ticket[digest_size + 8 :].split('!', 1) 

771 except ValueError: 

772 raise BadTicket('userid is not followed by !') 

773 userid = url_unquote(userid) 

774 if '!' in data: 

775 tokens, user_data = data.split('!', 1) 

776 else: # pragma: no cover (never generated) 

777 # @@: Is this the right order? 

778 tokens = '' 

779 user_data = data 

780 

781 expected = calculate_digest( 

782 ip, timestamp, secret, userid, tokens, user_data, hashalg 

783 ) 

784 

785 # Avoid timing attacks (see 

786 # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) 

787 if strings_differ(expected, digest): 

788 raise BadTicket( 

789 'Digest signature is not correct', expected=(expected, digest) 

790 ) 

791 

792 tokens = tokens.split(',') 

793 

794 return (timestamp, userid, tokens, user_data) 

795 

796 

797# this function licensed under the MIT license (stolen from Paste) 

798def calculate_digest( 

799 ip, timestamp, secret, userid, tokens, user_data, hashalg='md5' 

800): 

801 secret = bytes_(secret, 'utf-8') 

802 userid = bytes_(userid, 'utf-8') 

803 tokens = bytes_(tokens, 'utf-8') 

804 user_data = bytes_(user_data, 'utf-8') 

805 hash_obj = hashlib.new(hashalg) 

806 

807 # Check to see if this is an IPv6 address 

808 if ':' in ip: 

809 ip_timestamp = ip + str(int(timestamp)) 

810 ip_timestamp = bytes_(ip_timestamp) 

811 else: 

812 # encode_ip_timestamp not required, left in for backwards compatibility 

813 ip_timestamp = encode_ip_timestamp(ip, timestamp) 

814 

815 hash_obj.update( 

816 ip_timestamp + secret + userid + b'\0' + tokens + b'\0' + user_data 

817 ) 

818 digest = hash_obj.hexdigest() 

819 hash_obj2 = hashlib.new(hashalg) 

820 hash_obj2.update(bytes_(digest) + secret) 

821 return hash_obj2.hexdigest() 

822 

823 

824# this function licensed under the MIT license (stolen from Paste) 

825def encode_ip_timestamp(ip, timestamp): 

826 ip_chars = ''.join(map(chr, map(int, ip.split('.')))) 

827 t = int(timestamp) 

828 ts = ( 

829 (t & 0xFF000000) >> 24, 

830 (t & 0xFF0000) >> 16, 

831 (t & 0xFF00) >> 8, 

832 t & 0xFF, 

833 ) 

834 ts_chars = ''.join(map(chr, ts)) 

835 return bytes_(ip_chars + ts_chars) 

836 

837 

838class AuthTktCookieHelper(object): 

839 """ 

840 A helper class for use in third-party authentication policy 

841 implementations. See 

842 :class:`pyramid.authentication.AuthTktAuthenticationPolicy` for the 

843 meanings of the constructor arguments. 

844 """ 

845 

846 parse_ticket = staticmethod(parse_ticket) # for tests 

847 AuthTicket = AuthTicket # for tests 

848 BadTicket = BadTicket # for tests 

849 now = None # for tests 

850 

851 userid_type_decoders = { 

852 'int': int, 

853 'unicode': lambda x: utf_8_decode(x)[0], # bw compat for old cookies 

854 'b64unicode': lambda x: utf_8_decode(b64decode(x))[0], 

855 'b64str': lambda x: b64decode(x), 

856 } 

857 

858 userid_type_encoders = { 

859 int: ('int', str), 

860 long: ('int', str), 

861 text_type: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])), 

862 binary_type: ('b64str', lambda x: b64encode(x)), 

863 } 

864 

865 def __init__( 

866 self, 

867 secret, 

868 cookie_name='auth_tkt', 

869 secure=False, 

870 include_ip=False, 

871 timeout=None, 

872 reissue_time=None, 

873 max_age=None, 

874 http_only=False, 

875 path="/", 

876 wild_domain=True, 

877 hashalg='md5', 

878 parent_domain=False, 

879 domain=None, 

880 samesite='Lax', 

881 ): 

882 

883 serializer = SimpleSerializer() 

884 

885 self.cookie_profile = CookieProfile( 

886 cookie_name=cookie_name, 

887 secure=secure, 

888 max_age=max_age, 

889 httponly=http_only, 

890 path=path, 

891 serializer=serializer, 

892 samesite=samesite, 

893 ) 

894 

895 self.secret = secret 

896 self.cookie_name = cookie_name 

897 self.secure = secure 

898 self.include_ip = include_ip 

899 self.timeout = timeout if timeout is None else int(timeout) 

900 self.reissue_time = ( 

901 reissue_time if reissue_time is None else int(reissue_time) 

902 ) 

903 self.max_age = max_age if max_age is None else int(max_age) 

904 self.wild_domain = wild_domain 

905 self.parent_domain = parent_domain 

906 self.domain = domain 

907 self.hashalg = hashalg 

908 

909 def _get_cookies(self, request, value, max_age=None): 

910 cur_domain = request.domain 

911 

912 domains = [] 

913 if self.domain: 

914 domains.append(self.domain) 

915 else: 

916 if self.parent_domain and cur_domain.count('.') > 1: 

917 domains.append('.' + cur_domain.split('.', 1)[1]) 

918 else: 

919 domains.append(None) 

920 domains.append(cur_domain) 

921 if self.wild_domain: 

922 domains.append('.' + cur_domain) 

923 

924 profile = self.cookie_profile(request) 

925 

926 kw = {} 

927 kw['domains'] = domains 

928 if max_age is not None: 

929 kw['max_age'] = max_age 

930 

931 headers = profile.get_headers(value, **kw) 

932 return headers 

933 

934 def identify(self, request): 

935 """ Return a dictionary with authentication information, or ``None`` 

936 if no valid auth_tkt is attached to ``request``""" 

937 environ = request.environ 

938 cookie = request.cookies.get(self.cookie_name) 

939 

940 if cookie is None: 

941 return None 

942 

943 if self.include_ip: 

944 remote_addr = environ['REMOTE_ADDR'] 

945 else: 

946 remote_addr = '0.0.0.0' 

947 

948 try: 

949 timestamp, userid, tokens, user_data = self.parse_ticket( 

950 self.secret, cookie, remote_addr, self.hashalg 

951 ) 

952 except self.BadTicket: 

953 return None 

954 

955 now = self.now # service tests 

956 

957 if now is None: 

958 now = time_mod.time() 

959 

960 if self.timeout and ((timestamp + self.timeout) < now): 

961 # the auth_tkt data has expired 

962 return None 

963 

964 userid_typename = 'userid_type:' 

965 user_data_info = user_data.split('|') 

966 for datum in filter(None, user_data_info): 

967 if datum.startswith(userid_typename): 

968 userid_type = datum[len(userid_typename) :] 

969 decoder = self.userid_type_decoders.get(userid_type) 

970 if decoder: 

971 userid = decoder(userid) 

972 

973 reissue = self.reissue_time is not None 

974 

975 if reissue and not hasattr(request, '_authtkt_reissued'): 

976 if (now - timestamp) > self.reissue_time: 

977 # See https://github.com/Pylons/pyramid/issues#issue/108 

978 tokens = list(filter(None, tokens)) 

979 headers = self.remember( 

980 request, userid, max_age=self.max_age, tokens=tokens 

981 ) 

982 

983 def reissue_authtkt(request, response): 

984 if not hasattr(request, '_authtkt_reissue_revoked'): 

985 for k, v in headers: 

986 response.headerlist.append((k, v)) 

987 

988 request.add_response_callback(reissue_authtkt) 

989 request._authtkt_reissued = True 

990 

991 environ['REMOTE_USER_TOKENS'] = tokens 

992 environ['REMOTE_USER_DATA'] = user_data 

993 environ['AUTH_TYPE'] = 'cookie' 

994 

995 identity = {} 

996 identity['timestamp'] = timestamp 

997 identity['userid'] = userid 

998 identity['tokens'] = tokens 

999 identity['userdata'] = user_data 

1000 return identity 

1001 

1002 def forget(self, request): 

1003 """ Return a set of expires Set-Cookie headers, which will destroy 

1004 any existing auth_tkt cookie when attached to a response""" 

1005 request._authtkt_reissue_revoked = True 

1006 return self._get_cookies(request, None) 

1007 

1008 def remember(self, request, userid, max_age=None, tokens=()): 

1009 """ Return a set of Set-Cookie headers; when set into a response, 

1010 these headers will represent a valid authentication ticket. 

1011 

1012 ``max_age`` 

1013 The max age of the auth_tkt cookie, in seconds. When this value is 

1014 set, the cookie's ``Max-Age`` and ``Expires`` settings will be set, 

1015 allowing the auth_tkt cookie to last between browser sessions. If 

1016 this value is ``None``, the ``max_age`` value provided to the 

1017 helper itself will be used as the ``max_age`` value. Default: 

1018 ``None``. 

1019 

1020 ``tokens`` 

1021 A sequence of strings that will be placed into the auth_tkt tokens 

1022 field. Each string in the sequence must be of the Python ``str`` 

1023 type and must match the regex ``^[A-Za-z][A-Za-z0-9+_-]*$``. 

1024 Tokens are available in the returned identity when an auth_tkt is 

1025 found in the request and unpacked. Default: ``()``. 

1026 """ 

1027 max_age = self.max_age if max_age is None else int(max_age) 

1028 

1029 environ = request.environ 

1030 

1031 if self.include_ip: 

1032 remote_addr = environ['REMOTE_ADDR'] 

1033 else: 

1034 remote_addr = '0.0.0.0' 

1035 

1036 user_data = '' 

1037 

1038 encoding_data = self.userid_type_encoders.get(type(userid)) 

1039 

1040 if encoding_data: 

1041 encoding, encoder = encoding_data 

1042 else: 

1043 warnings.warn( 

1044 "userid is of type {}, and is not supported by the " 

1045 "AuthTktAuthenticationPolicy. Explicitly converting to string " 

1046 "and storing as base64. Subsequent requests will receive a " 

1047 "string as the userid, it will not be decoded back to the " 

1048 "type provided.".format(type(userid)), 

1049 RuntimeWarning, 

1050 ) 

1051 encoding, encoder = self.userid_type_encoders.get(text_type) 

1052 userid = str(userid) 

1053 

1054 userid = encoder(userid) 

1055 user_data = 'userid_type:%s' % encoding 

1056 

1057 new_tokens = [] 

1058 for token in tokens: 

1059 if isinstance(token, text_type): 

1060 try: 

1061 token = ascii_native_(token) 

1062 except UnicodeEncodeError: 

1063 raise ValueError("Invalid token %r" % (token,)) 

1064 if not (isinstance(token, str) and VALID_TOKEN.match(token)): 

1065 raise ValueError("Invalid token %r" % (token,)) 

1066 new_tokens.append(token) 

1067 tokens = tuple(new_tokens) 

1068 

1069 if hasattr(request, '_authtkt_reissued'): 

1070 request._authtkt_reissue_revoked = True 

1071 

1072 ticket = self.AuthTicket( 

1073 self.secret, 

1074 userid, 

1075 remote_addr, 

1076 tokens=tokens, 

1077 user_data=user_data, 

1078 cookie_name=self.cookie_name, 

1079 secure=self.secure, 

1080 hashalg=self.hashalg, 

1081 ) 

1082 

1083 cookie_value = ticket.cookie_value() 

1084 return self._get_cookies(request, cookie_value, max_age) 

1085 

1086 

1087@implementer(IAuthenticationPolicy) 

1088class SessionAuthenticationPolicy(CallbackAuthenticationPolicy): 

1089 """ A :app:`Pyramid` authentication policy which gets its data from the 

1090 configured :term:`session`. For this authentication policy to work, you 

1091 will have to follow the instructions in the :ref:`sessions_chapter` to 

1092 configure a :term:`session factory`. 

1093 

1094 Constructor Arguments 

1095 

1096 ``prefix`` 

1097 

1098 A prefix used when storing the authentication parameters in the 

1099 session. Defaults to 'auth.'. Optional. 

1100 

1101 ``callback`` 

1102 

1103 Default: ``None``. A callback passed the userid and the 

1104 request, expected to return ``None`` if the userid doesn't 

1105 exist or a sequence of principal identifiers (possibly empty) if 

1106 the user does exist. If ``callback`` is ``None``, the userid 

1107 will be assumed to exist with no principals. Optional. 

1108 

1109 ``debug`` 

1110 

1111 Default: ``False``. If ``debug`` is ``True``, log messages to the 

1112 Pyramid debug logger about the results of various authentication 

1113 steps. The output from debugging is useful for reporting to maillist 

1114 or IRC channels when asking for support. 

1115 

1116 """ 

1117 

1118 def __init__(self, prefix='auth.', callback=None, debug=False): 

1119 self.callback = callback 

1120 self.prefix = prefix or '' 

1121 self.userid_key = prefix + 'userid' 

1122 self.debug = debug 

1123 

1124 def remember(self, request, userid, **kw): 

1125 """ Store a userid in the session.""" 

1126 request.session[self.userid_key] = userid 

1127 return [] 

1128 

1129 def forget(self, request): 

1130 """ Remove the stored userid from the session.""" 

1131 if self.userid_key in request.session: 

1132 del request.session[self.userid_key] 

1133 return [] 

1134 

1135 def unauthenticated_userid(self, request): 

1136 return request.session.get(self.userid_key) 

1137 

1138 

1139@implementer(IAuthenticationPolicy) 

1140class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): 

1141 """ A :app:`Pyramid` authentication policy which uses HTTP standard basic 

1142 authentication protocol to authenticate users. To use this policy you will 

1143 need to provide a callback which checks the supplied user credentials 

1144 against your source of login data. 

1145 

1146 Constructor Arguments 

1147 

1148 ``check`` 

1149 

1150 A callback function passed a username, password and request, in that 

1151 order as positional arguments. Expected to return ``None`` if the 

1152 userid doesn't exist or a sequence of principal identifiers (possibly 

1153 empty) if the user does exist. 

1154 

1155 ``realm`` 

1156 

1157 Default: ``"Realm"``. The Basic Auth Realm string. Usually displayed 

1158 to the user by the browser in the login dialog. 

1159 

1160 ``debug`` 

1161 

1162 Default: ``False``. If ``debug`` is ``True``, log messages to the 

1163 Pyramid debug logger about the results of various authentication 

1164 steps. The output from debugging is useful for reporting to maillist 

1165 or IRC channels when asking for support. 

1166 

1167 **Issuing a challenge** 

1168 

1169 Regular browsers will not send username/password credentials unless they 

1170 first receive a challenge from the server. The following recipe will 

1171 register a view that will send a Basic Auth challenge to the user whenever 

1172 there is an attempt to call a view which results in a Forbidden response:: 

1173 

1174 from pyramid.httpexceptions import HTTPUnauthorized 

1175 from pyramid.security import forget 

1176 from pyramid.view import forbidden_view_config 

1177 

1178 @forbidden_view_config() 

1179 def forbidden_view(request): 

1180 if request.authenticated_userid is None: 

1181 response = HTTPUnauthorized() 

1182 response.headers.update(forget(request)) 

1183 return response 

1184 return HTTPForbidden() 

1185 """ 

1186 

1187 def __init__(self, check, realm='Realm', debug=False): 

1188 self.check = check 

1189 self.realm = realm 

1190 self.debug = debug 

1191 

1192 def unauthenticated_userid(self, request): 

1193 """ The userid parsed from the ``Authorization`` request header.""" 

1194 credentials = extract_http_basic_credentials(request) 

1195 if credentials: 

1196 return credentials.username 

1197 

1198 def remember(self, request, userid, **kw): 

1199 """ A no-op. Basic authentication does not provide a protocol for 

1200 remembering the user. Credentials are sent on every request. 

1201 

1202 """ 

1203 return [] 

1204 

1205 def forget(self, request): 

1206 """ Returns challenge headers. This should be attached to a response 

1207 to indicate that credentials are required.""" 

1208 return [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)] 

1209 

1210 def callback(self, username, request): 

1211 # Username arg is ignored. Unfortunately 

1212 # extract_http_basic_credentials winds up getting called twice when 

1213 # authenticated_userid is called. Avoiding that, however, 

1214 # winds up duplicating logic from the superclass. 

1215 credentials = extract_http_basic_credentials(request) 

1216 if credentials: 

1217 username, password = credentials 

1218 return self.check(username, password, request) 

1219 

1220 

1221HTTPBasicCredentials = namedtuple( 

1222 'HTTPBasicCredentials', ['username', 'password'] 

1223) 

1224 

1225 

1226def extract_http_basic_credentials(request): 

1227 """ A helper function for extraction of HTTP Basic credentials 

1228 from a given :term:`request`. 

1229 

1230 Returns a :class:`.HTTPBasicCredentials` 2-tuple with ``username`` and 

1231 ``password`` attributes or ``None`` if no credentials could be found. 

1232 

1233 """ 

1234 authorization = request.headers.get('Authorization') 

1235 if not authorization: 

1236 return None 

1237 

1238 try: 

1239 authmeth, auth = authorization.split(' ', 1) 

1240 except ValueError: # not enough values to unpack 

1241 return None 

1242 

1243 if authmeth.lower() != 'basic': 

1244 return None 

1245 

1246 try: 

1247 authbytes = b64decode(auth.strip()) 

1248 except (TypeError, binascii.Error): # can't decode 

1249 return None 

1250 

1251 # try utf-8 first, then latin-1; see discussion in 

1252 # https://github.com/Pylons/pyramid/issues/898 

1253 try: 

1254 auth = authbytes.decode('utf-8') 

1255 except UnicodeDecodeError: 

1256 auth = authbytes.decode('latin-1') 

1257 

1258 try: 

1259 username, password = auth.split(':', 1) 

1260 except ValueError: # not enough values to unpack 

1261 return None 

1262 

1263 return HTTPBasicCredentials(username, password)