17
17
import base64
18
18
import copy
19
19
import datetime
20
+ import httplib2
20
21
import json
21
22
import time
22
23
26
27
from oauth2client ._helpers import _from_bytes
27
28
from oauth2client ._helpers import _urlsafe_b64encode
28
29
from oauth2client import util
30
+ from oauth2client .client import _apply_user_agent
31
+ from oauth2client .client import _initialize_headers
32
+ from oauth2client .client import AccessTokenInfo
29
33
from oauth2client .client import AssertionCredentials
34
+ from oauth2client .client import clean_headers
30
35
from oauth2client .client import EXPIRY_FORMAT
36
+ from oauth2client .client import GoogleCredentials
31
37
from oauth2client .client import SERVICE_ACCOUNT
38
+ from oauth2client .client import TokenRevokeError
39
+ from oauth2client .client import _UTCNOW
32
40
from oauth2client import crypt
33
41
34
42
@@ -426,6 +434,32 @@ def create_scoped(self, scopes):
426
434
result ._private_key_pkcs12 = self ._private_key_pkcs12
427
435
result ._private_key_password = self ._private_key_password
428
436
return result
437
+
438
+ def create_with_claims (self , claims ):
439
+ """Create credentials that specify additional claims.
440
+
441
+ Args:
442
+ claims: dict, key-value pairs for claims.
443
+
444
+ Returns:
445
+ ServiceAccountCredentials, a copy of the current service account
446
+ credentials with updated claims to use when obtaining access tokens.
447
+ """
448
+ new_kwargs = dict (self ._kwargs )
449
+ new_kwargs .update (claims )
450
+ result = self .__class__ (self ._service_account_email ,
451
+ self ._signer ,
452
+ scopes = self ._scopes ,
453
+ private_key_id = self ._private_key_id ,
454
+ client_id = self .client_id ,
455
+ user_agent = self ._user_agent ,
456
+ ** new_kwargs )
457
+ result .token_uri = self .token_uri
458
+ result .revoke_uri = self .revoke_uri
459
+ result ._private_key_pkcs8_pem = self ._private_key_pkcs8_pem
460
+ result ._private_key_pkcs12 = self ._private_key_pkcs12
461
+ result ._private_key_password = self ._private_key_password
462
+ return result
429
463
430
464
def create_delegated (self , sub ):
431
465
"""Create credentials that act as domain-wide delegation of authority.
@@ -446,18 +480,158 @@ def create_delegated(self, sub):
446
480
ServiceAccountCredentials, a copy of the current service account
447
481
updated to act on behalf of ``sub``.
448
482
"""
449
- new_kwargs = dict (self ._kwargs )
450
- new_kwargs ['sub' ] = sub
451
- result = self .__class__ (self ._service_account_email ,
452
- self ._signer ,
453
- scopes = self ._scopes ,
454
- private_key_id = self ._private_key_id ,
455
- client_id = self .client_id ,
456
- user_agent = self ._user_agent ,
457
- ** new_kwargs )
483
+ return self .create_with_claims ({'sub' : sub })
484
+
485
+
486
+ def _datetime_to_secs (utc_time ):
487
+ # TODO(issue 298): use time_delta.total_seconds()
488
+ # time_delta.total_seconds() not supported in Python 2.6
489
+ epoch = datetime .datetime (1970 , 1 , 1 )
490
+ time_delta = utc_time - epoch
491
+ return time_delta .days * 86400 + time_delta .seconds
492
+
493
+
494
+ class _JWTAccessCredentials (ServiceAccountCredentials ):
495
+ """Self signed JWT credentials.
496
+
497
+ Makes an assertion to server using a self signed JWT from service account
498
+ credentials. These credentials do NOT use OAuth 2.0 and instead
499
+ authenticate directly.
500
+ """
501
+ _MAX_TOKEN_LIFETIME_SECS = 3600
502
+ """Max lifetime of the token (one hour, in seconds)."""
503
+
504
+ def __init__ (self ,
505
+ service_account_email ,
506
+ signer ,
507
+ scopes = None ,
508
+ private_key_id = None ,
509
+ client_id = None ,
510
+ user_agent = None ,
511
+ additional_claims = None ):
512
+ if additional_claims is None :
513
+ additional_claims = {}
514
+ super (_JWTAccessCredentials , self ).__init__ (
515
+ service_account_email ,
516
+ signer ,
517
+ private_key_id = private_key_id ,
518
+ client_id = client_id ,
519
+ user_agent = user_agent ,
520
+ ** additional_claims )
521
+
522
+ def authorize (self , http ):
523
+ """Authorize an httplib2.Http instance with a JWT assertion.
524
+
525
+ Unless specified, the 'aud' of the assertion will be the base
526
+ uri of the request.
527
+
528
+ Args:
529
+ http: An instance of ``httplib2.Http`` or something that acts
530
+ like it.
531
+ Returns:
532
+ A modified instance of http that was passed in.
533
+ Example::
534
+ h = httplib2.Http()
535
+ h = credentials.authorize(h)
536
+ """
537
+ request_orig = http .request
538
+ request_auth = super (_JWTAccessCredentials , self ).authorize (http ).request
539
+
540
+ # The closure that will replace 'httplib2.Http.request'.
541
+ def new_request (uri , method = 'GET' , body = None , headers = None ,
542
+ redirections = httplib2 .DEFAULT_MAX_REDIRECTS ,
543
+ connection_type = None ):
544
+ if 'aud' in self ._kwargs :
545
+ # Preemptively refresh token, this is not done for OAuth2
546
+ if self .access_token is None or self .access_token_expired :
547
+ self .refresh (None )
548
+ return request_auth (uri , method , body ,
549
+ headers , redirections ,
550
+ connection_type )
551
+ else :
552
+ # If we don't have an 'aud' (audience) claim,
553
+ # create a 1-time token with the uri root as the audience
554
+ headers = _initialize_headers (headers )
555
+ _apply_user_agent (headers , self .user_agent )
556
+ uri_root = uri .split ('?' , 1 )[0 ]
557
+ token , unused_expiry = self ._create_token ({'aud' : uri_root })
558
+
559
+ headers ['Authorization' ] = 'Bearer ' + token
560
+ return request_orig (uri , method , body ,
561
+ clean_headers (headers ),
562
+ redirections , connection_type )
563
+
564
+ # Replace the request method with our own closure.
565
+ http .request = new_request
566
+
567
+ return http
568
+
569
+ def get_access_token (self , http = None , additional_claims = None ):
570
+ """Create a signed jwt.
571
+
572
+ Args:
573
+ http: unused
574
+ additional_claims: dict, additional claims to add to
575
+ the payload of the JWT.
576
+ Returns:
577
+ An AccessTokenInfo with the signed jwt
578
+ """
579
+ if additional_claims is None :
580
+ if self .access_token is None or self .access_token_expired :
581
+ self .refresh (None )
582
+ return AccessTokenInfo (access_token = self .access_token ,
583
+ expires_in = self ._expires_in ())
584
+ else :
585
+ # Create a 1 time token
586
+ token , unused_expiry = self ._create_token (additional_claims )
587
+ return AccessTokenInfo (access_token = token ,
588
+ expires_in = self ._MAX_TOKEN_LIFETIME_SECS )
589
+
590
+ def revoke (self , http ):
591
+ """Cannot revoke JWTAccessCredentials tokens."""
592
+ pass
593
+
594
+ def create_scoped_required (self ):
595
+ # JWTAccessCredentials are unscoped by definition
596
+ return True
597
+
598
+ def create_scoped (self , scopes ):
599
+ # Returns an OAuth2 credentials with the given scope
600
+ result = ServiceAccountCredentials (self ._service_account_email ,
601
+ self ._signer ,
602
+ scopes = scopes ,
603
+ private_key_id = self ._private_key_id ,
604
+ client_id = self .client_id ,
605
+ user_agent = self ._user_agent ,
606
+ ** self ._kwargs )
458
607
result .token_uri = self .token_uri
459
608
result .revoke_uri = self .revoke_uri
460
- result ._private_key_pkcs8_pem = self ._private_key_pkcs8_pem
461
- result ._private_key_pkcs12 = self ._private_key_pkcs12
462
- result ._private_key_password = self ._private_key_password
609
+ if self ._private_key_pkcs8_pem is not None :
610
+ result ._private_key_pkcs8_pem = self ._private_key_pkcs8_pem
611
+ if self ._private_key_pkcs12 is not None :
612
+ result ._private_key_pkcs12 = self ._private_key_pkcs12
613
+ if self ._private_key_password is not None :
614
+ result ._private_key_password = self ._private_key_password
463
615
return result
616
+
617
+ def refresh (self , http ):
618
+ self ._refresh (None )
619
+
620
+ def _refresh (self , http_request ):
621
+ self .access_token , self .token_expiry = self ._create_token ()
622
+
623
+ def _create_token (self , additional_claims = None ):
624
+ now = _UTCNOW ()
625
+ expiry = now + datetime .timedelta (seconds = self ._MAX_TOKEN_LIFETIME_SECS )
626
+ payload = {
627
+ 'iat' : _datetime_to_secs (now ),
628
+ 'exp' : _datetime_to_secs (expiry ),
629
+ 'iss' : self ._service_account_email ,
630
+ 'sub' : self ._service_account_email
631
+ }
632
+ payload .update (self ._kwargs )
633
+ if additional_claims is not None :
634
+ payload .update (additional_claims )
635
+ jwt = crypt .make_signed_jwt (self ._signer , payload ,
636
+ key_id = self ._private_key_id )
637
+ return jwt .decode ('ascii' ), expiry
0 commit comments